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)