|
from flask import Flask, request, jsonify, render_template |
|
import json |
|
import requests |
|
import random |
|
import math |
|
import os |
|
from dotenv import load_dotenv |
|
|
|
load_dotenv() |
|
|
|
app = Flask(__name__) |
|
|
|
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') |
|
NOMINATIM_USER_AGENT = "GeoSafeConstruct/1.0 ([email protected])" |
|
|
|
|
|
|
|
def get_location_name_from_coords(lat, lon): |
|
"""Fetches a location name/address from coordinates using Nominatim.""" |
|
headers = {"User-Agent": NOMINATIM_USER_AGENT} |
|
url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}&zoom=16&addressdetails=1" |
|
try: |
|
response = requests.get(url, headers=headers, timeout=10) |
|
response.raise_for_status() |
|
data = response.json() |
|
|
|
address = data.get('address', {}) |
|
parts = [ |
|
address.get('road'), address.get('neighbourhood'), address.get('suburb'), |
|
address.get('city_district'), address.get('city'), address.get('town'), |
|
address.get('village'), address.get('county'), address.get('state'), |
|
address.get('postcode'), address.get('country') |
|
] |
|
|
|
display_name = data.get('display_name', "Unknown Location") |
|
filtered_parts = [part for part in parts if part] |
|
if len(filtered_parts) > 3: |
|
return display_name |
|
elif filtered_parts: |
|
return ", ".join(filtered_parts[:3]) |
|
return display_name |
|
except requests.exceptions.RequestException as e: |
|
print(f"Nominatim reverse geocoding error: {e}") |
|
return f"Area around {lat:.3f}, {lon:.3f}" |
|
except (json.JSONDecodeError, KeyError) as e: |
|
print(f"Nominatim response parsing error: {e}") |
|
return f"Area around {lat:.3f}, {lon:.3f}" |
|
|
|
|
|
def get_coords_from_location_name(query): |
|
"""Fetches coordinates from a location name/address using Nominatim.""" |
|
headers = {"User-Agent": NOMINATIM_USER_AGENT} |
|
url = f"https://nominatim.openstreetmap.org/search?q={requests.utils.quote(query)}&format=json&limit=1" |
|
try: |
|
response = requests.get(url, headers=headers, timeout=10) |
|
response.raise_for_status() |
|
data = response.json() |
|
if data and isinstance(data, list) and len(data) > 0: |
|
return { |
|
"latitude": float(data[0].get("lat")), |
|
"longitude": float(data[0].get("lon")), |
|
"display_name": data[0].get("display_name") |
|
} |
|
return None |
|
except requests.exceptions.RequestException as e: |
|
print(f"Nominatim geocoding error: {e}") |
|
return None |
|
except (json.JSONDecodeError, KeyError, IndexError) as e: |
|
print(f"Nominatim geocoding response parsing error: {e}") |
|
return None |
|
|
|
|
|
|
|
def call_gemini_api(prompt_text): |
|
"""Generic function to call Gemini API.""" |
|
if not GEMINI_API_KEY: |
|
print("GEMINI_API_KEY not found in environment variables.") |
|
return None, "Gemini API key not configured." |
|
|
|
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent" |
|
headers = { |
|
"Content-Type": "application/json", |
|
"x-goog-api-key": GEMINI_API_KEY |
|
} |
|
payload = { |
|
"contents": [{"parts": [{"text": prompt_text}]}], |
|
"generationConfig": { |
|
"response_mime_type": "application/json", |
|
"temperature": 0.6, |
|
"topP": 0.9, |
|
"topK": 40 |
|
}, |
|
"safetySettings": [ |
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, |
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, |
|
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, |
|
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"} |
|
] |
|
} |
|
|
|
try: |
|
response = requests.post(url, json=payload, headers=headers, timeout=60) |
|
response.raise_for_status() |
|
gemini_response_data = response.json() |
|
|
|
if 'candidates' not in gemini_response_data or not gemini_response_data['candidates']: |
|
print("Gemini API Error: No candidates in response.") |
|
print("Full Gemini Response:", gemini_response_data) |
|
if 'promptFeedback' in gemini_response_data and 'blockReason' in gemini_response_data['promptFeedback']: |
|
return None, f"Content blocked by API: {gemini_response_data['promptFeedback']['blockReason']}" |
|
return None, "Gemini API returned no content." |
|
|
|
json_text_content = gemini_response_data['candidates'][0]['content']['parts'][0]['text'] |
|
|
|
|
|
if json_text_content.strip().startswith("```json"): |
|
json_text_content = json_text_content.strip()[7:] |
|
if json_text_content.strip().endswith("```"): |
|
json_text_content = json_text_content.strip()[:-3] |
|
|
|
return json.loads(json_text_content.strip()), None |
|
except requests.exceptions.Timeout: |
|
print("Gemini API call timed out.") |
|
return None, "Analysis request timed out. Please try again." |
|
except requests.exceptions.RequestException as e: |
|
print(f"Error calling Gemini API (RequestException): {e}") |
|
return None, f"Could not connect to analysis service: {e}" |
|
except json.JSONDecodeError as e: |
|
print(f"Error decoding JSON from Gemini API: {e}") |
|
print(f"Received text for JSON parsing: {json_text_content if 'json_text_content' in locals() else 'N/A'}") |
|
return None, "AI returned an invalid response format. Please try again." |
|
except (KeyError, IndexError) as e: |
|
print(f"Error parsing Gemini API response structure: {e}") |
|
print(f"Gemini response: {gemini_response_data if 'gemini_response_data' in locals() else 'N/A'}") |
|
return None, "AI returned an unexpected response structure." |
|
except Exception as e: |
|
print(f"An unexpected error occurred during Gemini call: {e}") |
|
return None, f"An unexpected error occurred: {e}" |
|
|
|
|
|
def get_safety_analysis_with_gemini(latitude, longitude, location_address): |
|
""" |
|
Calls Gemini API to analyze construction safety. |
|
""" |
|
prompt = f""" |
|
**Objective:** Provide a detailed and realistic construction site safety analysis for the location: {location_address} (Latitude: {latitude}, Longitude: {longitude}). |
|
|
|
**Output Format:** STRICTLY JSON. Do NOT include any text outside the JSON structure (e.g., no '```json' or '```' wrappers). |
|
|
|
**JSON Structure:** |
|
{{ |
|
"location_name": "{location_address}", // Use the provided address |
|
"safety_score": "integer (0-100, overall safety score, be critical and realistic)", |
|
"suitability_statement": "string (e.g., 'Generally Suitable with Mitigations', 'High Caution Advised', 'Not Recommended without Major Intervention')", |
|
"summary_assessment": "string (3-4 sentences summarizing key findings, suitability, and major concerns. Be specific.)", |
|
"geological_risks": {{ |
|
"earthquake_risk": {{ |
|
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')", |
|
"details": "string (Specifics like proximity to faults, historical activity, soil liquefaction potential if known. If not assessed, state why.)" |
|
}}, |
|
"landslide_risk": {{ |
|
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')", |
|
"details": "string (Slope stability, soil type, vegetation, historical incidents. If not assessed, state why.)" |
|
}}, |
|
"soil_stability": {{ |
|
"type": "string ('Stable', 'Moderately Stable', 'Unstable', 'Variable', 'Requires Investigation')", |
|
"concerns": ["string array (e.g., 'Expansive clays present', 'High water table', 'Poor drainage', 'Subsidence risk')"] |
|
}} |
|
}}, |
|
"hydrological_risks": {{ |
|
"flood_risk": {{ |
|
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')", |
|
"details": "string (Proximity to water bodies, floodplain maps, historical flooding, drainage issues. If not assessed, state why.)" |
|
}}, |
|
"tsunami_risk": {{ // Only include if geographically relevant (coastal) |
|
"level": "string ('Negligible', 'Low', 'Medium', 'High')", |
|
"details": "string (Distance from coast, elevation, historical data.)" |
|
}} |
|
}}, |
|
"other_environmental_risks": [ // Array of objects, include if relevant |
|
{{ "type": "Wildfire", "level": "string ('Low', 'Medium', 'High')", "details": "string (Vegetation, climate, fire history)" }}, |
|
{{ "type": "Extreme Weather (Hurricanes/Tornadoes/High Winds)", "level": "string ('Low', 'Medium', 'High')", "details": "string (Regional patterns, building code requirements)" }}, |
|
{{ "type": "Industrial Hazards", "level": "string ('Low', 'Medium', 'High')", "details": "string (Proximity to industrial plants, pipelines, hazardous material routes)" }} |
|
], |
|
"key_risk_factors_summary": ["string array (Bulleted list of 3-5 most critical risk factors for this specific site)"], |
|
"mitigation_recommendations": ["string array (Actionable, specific recommendations, e.g., 'Conduct Level 2 Geotechnical Survey focusing on shear strength', 'Implement earthquake-resistant design to Zone IV standards', 'Elevate foundation by 1.5m above base flood elevation')"], |
|
"further_investigations_needed": ["string array (e.g., 'Detailed hydrological study', 'Environmental Impact Assessment', 'Traffic impact study')"], |
|
"alternative_locations_analysis": [ // Up to 2-3 alternatives. If none are significantly better, state that. |
|
{{ |
|
"name_suggestion": "string (Suggest a conceptual name like 'North Ridge Site' or 'Valley View Plot')", |
|
"latitude": "float (as string, e.g., '18.5234')", |
|
"longitude": "float (as string, e.g., '73.8567')", |
|
"estimated_distance_km": "float (as string, e.g., '8.5')", |
|
"brief_justification": "string (Why is this a potential alternative? E.g., 'Appears to be on more stable ground', 'Further from flood plain')", |
|
"potential_pros": ["string array"], |
|
"potential_cons": ["string array"], |
|
"comparative_safety_score_estimate": "integer (0-100, relative to primary site)" |
|
}} |
|
], |
|
"data_confidence_level": "string ('Low', 'Medium', 'High' - based on assumed availability of public data for this general region, not specific site data which is unknown to you)", |
|
"disclaimer": "This AI-generated analysis is for preliminary informational purposes only and not a substitute for professional engineering, geological, and environmental assessments. On-site investigations are crucial." |
|
}} |
|
|
|
**Guidelines for Content:** |
|
- make sumre rnaodmness in score generation not same view ppijn t everytime |
|
- Be realistic. If data for a specific risk is unlikely to be publicly available for a random coordinate, mark it 'Not Assessed' and explain. |
|
- Focus on actionable insights. |
|
- For alternative locations, provide *different* lat/lon coordinates that are plausibly nearby (within 5-20km). |
|
- Ensure all string values are properly quoted. Ensure latitudes and longitudes for alternatives are strings representing floats. |
|
- The safety_score should reflect a comprehensive evaluation of all risks. |
|
- proper spacing and upo down margins shoudl be there |
|
""" |
|
return call_gemini_api(prompt) |
|
|
|
|
|
|
|
@app.route('/') |
|
def index(): |
|
return render_template('index.html') |
|
|
|
|
|
@app.route('/api/geocode', methods=['POST']) |
|
def geocode_location(): |
|
data = request.json |
|
query = data.get('query') |
|
if not query: |
|
return jsonify({"error": "Query parameter is required."}), 400 |
|
|
|
coords_data = get_coords_from_location_name(query) |
|
if coords_data: |
|
return jsonify(coords_data), 200 |
|
else: |
|
return jsonify({"error": "Location not found or geocoding failed."}), 404 |
|
|
|
|
|
@app.route('/api/analyze_location', methods=['POST']) |
|
def analyze_location_route(): |
|
data = request.json |
|
try: |
|
latitude = float(data.get('latitude')) |
|
longitude = float(data.get('longitude')) |
|
except (TypeError, ValueError): |
|
return jsonify({"error": "Invalid latitude or longitude provided."}), 400 |
|
|
|
|
|
primary_location_address = get_location_name_from_coords(latitude, longitude) |
|
|
|
|
|
analysis_data, error = get_safety_analysis_with_gemini(latitude, longitude, primary_location_address) |
|
|
|
if error: |
|
|
|
return jsonify({ |
|
"error_message": error, |
|
"location_name": primary_location_address, |
|
"safety_score": random.randint(20, 50), |
|
"summary_assessment": f"Could not perform detailed analysis due to: {error}. Basic location: {primary_location_address}.", |
|
"suitability_statement": "Analysis Incomplete", |
|
"geological_risks": {"earthquake_risk": {"level": "Not Assessed", "details": error}}, |
|
"hydrological_risks": {"flood_risk": {"level": "Not Assessed", "details": error}}, |
|
"disclaimer": "A technical issue prevented full analysis." |
|
}), 500 |
|
|
|
|
|
if analysis_data.get("alternative_locations_analysis"): |
|
for alt_loc in analysis_data["alternative_locations_analysis"]: |
|
try: |
|
alt_lat = float(alt_loc.get("latitude")) |
|
alt_lon = float(alt_loc.get("longitude")) |
|
alt_loc["actual_name"] = get_location_name_from_coords(alt_lat, alt_lon) |
|
|
|
if not alt_loc.get("estimated_distance_km") or float(alt_loc.get("estimated_distance_km", 0)) == 0: |
|
alt_loc["estimated_distance_km"] = str( |
|
calculate_haversine_distance(latitude, longitude, alt_lat, alt_lon)) |
|
|
|
except (TypeError, ValueError, KeyError) as e: |
|
print(f"Error processing alternative location coords: {e}, data: {alt_loc}") |
|
alt_loc["actual_name"] = "Nearby Area (details unavailable)" |
|
|
|
if "latitude" not in alt_loc or "longitude" not in alt_loc: |
|
alt_loc["latitude"] = str(latitude + (random.random() - 0.5) * 0.05) |
|
alt_loc["longitude"] = str(longitude + (random.random() - 0.5) * 0.05) |
|
|
|
return jsonify(analysis_data), 200 |
|
|
|
|
|
def calculate_haversine_distance(lat1, lon1, lat2, lon2): |
|
R = 6371 |
|
try: |
|
lat1_rad, lon1_rad = math.radians(float(lat1)), math.radians(float(lon1)) |
|
lat2_rad, lon2_rad = math.radians(float(lat2)), math.radians(float(lon2)) |
|
except (ValueError, TypeError): |
|
print(f"Error converting lat/lon to float for Haversine: {lat1}, {lon1}, {lat2}, {lon2}") |
|
return 0.0 |
|
dlon = lon2_rad - lon1_rad |
|
dlat = lat2_rad - lat1_rad |
|
a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 |
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) |
|
distance = R * c |
|
return round(distance, 1) |
|
|
|
|
|
if __name__ == '__main__': |
|
if not GEMINI_API_KEY: |
|
print("CRITICAL ERROR: GEMINI_API_KEY is not set in the environment.") |
|
print("Please create a .env file with GEMINI_API_KEY=YOUR_API_KEY or set it as an environment variable.") |
|
app.run(debug=True, port=5000) |