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)