File size: 16,475 Bytes
05fec25 f2a0c2b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
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() # Load environment variables from .env file
app = Flask(__name__)
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
NOMINATIM_USER_AGENT = "GeoSafeConstruct/1.0 (your.email@example.com)" # IMPORTANT: Change to your app name and contact email
# --- Nominatim API Functions ---
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()
# Construct a readable address, prioritizing certain fields
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')
]
# Filter out None values and join
display_name = data.get('display_name', "Unknown Location")
filtered_parts = [part for part in parts if part]
if len(filtered_parts) > 3: # If many parts, use display_name for brevity
return display_name
elif filtered_parts:
return ", ".join(filtered_parts[:3]) # Take first few relevant parts
return display_name # Fallback to full 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
# --- Gemini API Functions ---
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, # Adjust for creativity vs. factuality
"topP": 0.9,
"topK": 40
},
"safetySettings": [ # Add safety settings if needed
{"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) # Increased timeout
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']
# Attempt to clean JSON string if it's wrapped in markdown
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)
# --- Flask Routes ---
@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
# Get a human-readable name for the primary location
primary_location_address = get_location_name_from_coords(latitude, longitude)
# Call Gemini for the main analysis
analysis_data, error = get_safety_analysis_with_gemini(latitude, longitude, primary_location_address)
if error:
# Fallback to a simpler mock-like structure if Gemini fails critically
return jsonify({
"error_message": error,
"location_name": primary_location_address,
"safety_score": random.randint(20, 50), # Low score to indicate issue
"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
# Post-process alternative locations to get their names
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)
# Ensure distance is calculated if not provided by Gemini or if it's nonsensical
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)"
# Fallback if lat/lon are missing or invalid for an alternative
if "latitude" not in alt_loc or "longitude" not in alt_loc:
alt_loc["latitude"] = str(latitude + (random.random() - 0.5) * 0.05) # Small offset
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 # Radius of Earth in kilometers
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 # Fallback
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) |