Upload 4 files
Browse files- app.py +151 -0
- requirements.txt +9 -0
- templates/index.html +262 -0
- templates/predict.html +132 -0
app.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
+
import requests
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
|
| 9 |
+
# Mapping of SoilGrids parameter codes
|
| 10 |
+
PARAM_MAP = {
|
| 11 |
+
"bdod": "Bulk Density", "cec": "Cation Exchange Capacity", "cfvo": "Coarse Fragment Volume",
|
| 12 |
+
"clay": "Clay Content", "nitrogen": "Nitrogen Content", "ocd": "Organic Carbon Density",
|
| 13 |
+
"ocs": "Organic Carbon Stock", "phh2o": "Soil pH", "sand": "Sand Content",
|
| 14 |
+
"silt": "Silt Content", "soc": "Soil Organic Carbon", "wv0010": "Water Content (0-10cm)",
|
| 15 |
+
"wv0033": "Water Content (0-33cm)", "wv1500": "Water Content (1500mm)"
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@app.route('/')
|
| 19 |
+
def index():
|
| 20 |
+
return render_template('index.html')
|
| 21 |
+
|
| 22 |
+
@app.route('/get_soil_report', methods=['POST'])
|
| 23 |
+
def get_soil_report():
|
| 24 |
+
data = request.get_json()
|
| 25 |
+
lat, lon = data.get("lat"), data.get("lon")
|
| 26 |
+
if not lat or not lon:
|
| 27 |
+
return jsonify({"error": "Latitude and Longitude are required"}), 400
|
| 28 |
+
|
| 29 |
+
headers = {"accept": "application/json"}
|
| 30 |
+
|
| 31 |
+
# Fetch Soil Classification
|
| 32 |
+
try:
|
| 33 |
+
class_response = requests.get(
|
| 34 |
+
"https://rest.isric.org/soilgrids/v2.0/classification/query",
|
| 35 |
+
params={"lon": lon, "lat": lat, "number_classes": 5},
|
| 36 |
+
headers=headers, timeout=15
|
| 37 |
+
)
|
| 38 |
+
class_response.raise_for_status()
|
| 39 |
+
class_data = class_response.json()
|
| 40 |
+
except requests.exceptions.RequestException as e:
|
| 41 |
+
return jsonify({"error": f"Failed to fetch soil classification: {e}"}), 500
|
| 42 |
+
|
| 43 |
+
soil_classification = {
|
| 44 |
+
"soil_type": class_data.get("wrb_class_name", "Unknown"),
|
| 45 |
+
"probabilities": class_data.get("wrb_class_probability", [])
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Fetch Soil Properties
|
| 49 |
+
try:
|
| 50 |
+
prop_response = requests.get(
|
| 51 |
+
"https://rest.isric.org/soilgrids/v2.0/properties/query",
|
| 52 |
+
params={
|
| 53 |
+
"lon": lon, "lat": lat,
|
| 54 |
+
"property": list(PARAM_MAP.keys()),
|
| 55 |
+
"depth": "5-15cm", "value": "mean"
|
| 56 |
+
},
|
| 57 |
+
headers=headers, timeout=15
|
| 58 |
+
)
|
| 59 |
+
prop_response.raise_for_status()
|
| 60 |
+
prop_data = prop_response.json()
|
| 61 |
+
except requests.exceptions.RequestException as e:
|
| 62 |
+
return jsonify({"error": f"Failed to fetch soil properties: {e}"}), 500
|
| 63 |
+
|
| 64 |
+
properties_list = []
|
| 65 |
+
layers = prop_data.get("properties", {}).get("layers", [])
|
| 66 |
+
for layer in layers:
|
| 67 |
+
param_code = layer.get("name")
|
| 68 |
+
name = PARAM_MAP.get(param_code, param_code.upper())
|
| 69 |
+
depth_info = layer.get("depths", [{}])[0]
|
| 70 |
+
value = depth_info.get("values", {}).get("mean")
|
| 71 |
+
unit = layer.get("unit_measure", {}).get("mapped_units", "")
|
| 72 |
+
|
| 73 |
+
if value is not None:
|
| 74 |
+
if param_code == "phh2o":
|
| 75 |
+
value /= 10.0
|
| 76 |
+
unit = "pH"
|
| 77 |
+
elif param_code in ["wv0010", "wv0033", "wv1500"]:
|
| 78 |
+
value /= 100.0
|
| 79 |
+
unit = "cm³/cm³"
|
| 80 |
+
|
| 81 |
+
properties_list.append({"parameter": name, "value": value, "unit": unit})
|
| 82 |
+
|
| 83 |
+
return jsonify({"classification": soil_classification, "properties": properties_list})
|
| 84 |
+
|
| 85 |
+
@app.route('/analyze_soil', methods=['POST'])
|
| 86 |
+
def analyze_soil():
|
| 87 |
+
api_key = os.getenv("GEMINI_API")
|
| 88 |
+
if not api_key:
|
| 89 |
+
error_msg = "API key not configured. The server administrator must set the GEMINI_API environment variable."
|
| 90 |
+
return jsonify({"error": error_msg}), 500
|
| 91 |
+
|
| 92 |
+
data = request.get_json()
|
| 93 |
+
soil_report = data.get("soil_report")
|
| 94 |
+
language = data.get("language", "English")
|
| 95 |
+
|
| 96 |
+
if not soil_report:
|
| 97 |
+
return jsonify({"error": "Soil report data is missing"}), 400
|
| 98 |
+
|
| 99 |
+
prompt = f"""
|
| 100 |
+
Analyze the following soil report and provide recommendations.
|
| 101 |
+
The response MUST be a single, valid JSON object, without any markdown formatting or surrounding text.
|
| 102 |
+
The user wants the analysis in this language: {language}.
|
| 103 |
+
Soil Report Data: {json.dumps(soil_report, indent=2)}
|
| 104 |
+
JSON Structure to follow:
|
| 105 |
+
{{
|
| 106 |
+
"soilType": "The primary soil type from the report",
|
| 107 |
+
"generalInsights": ["Insight 1", "Insight 2", "Insight 3"],
|
| 108 |
+
"parameters": [{{"parameter": "Parameter Name", "value": "Value with Unit", "range": "Low/Normal/High", "comment": "Brief comment."}}],
|
| 109 |
+
"cropRecommendations": [{{"crop": "Crop Name", "reason": "Brief reason."}}],
|
| 110 |
+
"managementRecommendations": {{"fertilization": "Recommendation.", "irrigation": "Recommendation."}}
|
| 111 |
+
}}
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
genai.configure(api_key=api_key)
|
| 116 |
+
|
| 117 |
+
# --- NEW: Fallback Logic Implementation ---
|
| 118 |
+
models_to_try = ['gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-flash']
|
| 119 |
+
analysis_json = None
|
| 120 |
+
last_error = None
|
| 121 |
+
|
| 122 |
+
for model_name in models_to_try:
|
| 123 |
+
try:
|
| 124 |
+
print(f"Attempting to use model: {model_name}")
|
| 125 |
+
model = genai.GenerativeModel(model_name)
|
| 126 |
+
response = model.generate_content(prompt)
|
| 127 |
+
|
| 128 |
+
cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
|
| 129 |
+
analysis_json = json.loads(cleaned_response)
|
| 130 |
+
|
| 131 |
+
print(f"Successfully generated content with {model_name}")
|
| 132 |
+
break # Exit the loop on success
|
| 133 |
+
|
| 134 |
+
except Exception as e:
|
| 135 |
+
print(f"Model {model_name} failed: {e}")
|
| 136 |
+
last_error = e
|
| 137 |
+
continue # Try the next model in the list
|
| 138 |
+
|
| 139 |
+
if not analysis_json:
|
| 140 |
+
# This block is reached only if all models in the loop failed.
|
| 141 |
+
raise Exception("All specified AI models failed to generate a response.") from last_error
|
| 142 |
+
|
| 143 |
+
return jsonify(analysis_json)
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
# This catches the final error if all models fail, or any other setup error.
|
| 147 |
+
print(f"Error during Gemini API processing: {e}")
|
| 148 |
+
return jsonify({"error": f"Failed to get analysis from AI models: {e}"}), 500
|
| 149 |
+
|
| 150 |
+
if __name__ == '__main__':
|
| 151 |
+
app.run(debug=True, port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gunicorn
|
| 2 |
+
flask
|
| 3 |
+
scikit-learn==1.5.2
|
| 4 |
+
tensorflow==2.17.0
|
| 5 |
+
numpy
|
| 6 |
+
requests
|
| 7 |
+
pillow
|
| 8 |
+
pandas
|
| 9 |
+
joblib==1.4.2
|
templates/index.html
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Soil Analysis Dashboard</title>
|
| 7 |
+
<!-- Bootstrap CSS -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Leaflet CSS -->
|
| 10 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
| 11 |
+
<style>
|
| 12 |
+
body { background-color: #f4f8f4; }
|
| 13 |
+
.navbar-brand { font-weight: bold; }
|
| 14 |
+
#map { height: 400px; border-radius: 0.5rem; border: 1px solid #ddd; }
|
| 15 |
+
.card { border: 1px solid #28a745; }
|
| 16 |
+
.btn-success { background-color: #28a745; border-color: #28a745; }
|
| 17 |
+
.btn-success:hover { background-color: #218838; border-color: #1e7e34; }
|
| 18 |
+
.loader { display: none; }
|
| 19 |
+
</style>
|
| 20 |
+
</head>
|
| 21 |
+
<body>
|
| 22 |
+
<nav class="navbar navbar-dark bg-success">
|
| 23 |
+
<div class="container-fluid">
|
| 24 |
+
<span class="navbar-brand mb-0 h1 mx-auto">🌱 Soil Analysis Dashboard</span>
|
| 25 |
+
</div>
|
| 26 |
+
</nav>
|
| 27 |
+
|
| 28 |
+
<div class="container my-4">
|
| 29 |
+
<!-- Step 1: Location -->
|
| 30 |
+
<div class="card mb-4">
|
| 31 |
+
<div class="card-header fw-bold">Step 1: Select Location</div>
|
| 32 |
+
<div class="card-body">
|
| 33 |
+
<div class="row">
|
| 34 |
+
<div class="col-lg-8 mb-3 mb-lg-0">
|
| 35 |
+
<div id="map"></div>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="col-lg-4">
|
| 38 |
+
<div class="mb-3">
|
| 39 |
+
<label for="lat" class="form-label">Latitude</label>
|
| 40 |
+
<input type="text" id="lat" class="form-control" placeholder="Click map or enter manually">
|
| 41 |
+
</div>
|
| 42 |
+
<div class="mb-3">
|
| 43 |
+
<label for="lon" class="form-label">Longitude</label>
|
| 44 |
+
<input type="text" id="lon" class="form-control" placeholder="Click map or enter manually">
|
| 45 |
+
</div>
|
| 46 |
+
<button id="current-location" class="btn btn-secondary w-100 mb-2">Use Current Location</button>
|
| 47 |
+
<button id="fetch-report" class="btn btn-success w-100">
|
| 48 |
+
<span class="spinner-border spinner-border-sm loader" role="status" aria-hidden="true"></span>
|
| 49 |
+
Fetch Soil Report
|
| 50 |
+
</button>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<!-- Alert for errors -->
|
| 57 |
+
<div id="alert-container" class="mt-3"></div>
|
| 58 |
+
|
| 59 |
+
<!-- Step 2: Soil Report (Initially Hidden) -->
|
| 60 |
+
<div id="soil-report-section" class="d-none">
|
| 61 |
+
<div class="card mb-4">
|
| 62 |
+
<div class="card-header fw-bold">Step 2: Soil Report</div>
|
| 63 |
+
<div class="card-body">
|
| 64 |
+
<div class="row">
|
| 65 |
+
<div class="col-md-6" id="classification-report"></div>
|
| 66 |
+
<div class="col-md-6" id="properties-report"></div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Step 3: Analysis (Initially Hidden) -->
|
| 73 |
+
<div id="analysis-section" class="d-none">
|
| 74 |
+
<div class="card mb-4">
|
| 75 |
+
<div class="card-header fw-bold">Step 3: AI Analysis & Recommendations</div>
|
| 76 |
+
<div class="card-body">
|
| 77 |
+
<div class="row align-items-end">
|
| 78 |
+
<div class="col-md-8 mb-3 mb-md-0">
|
| 79 |
+
<label for="language" class="form-label">Response Language</label>
|
| 80 |
+
<select class="form-select" id="language">
|
| 81 |
+
<option value="English">English</option><option value="Hindi">Hindi</option><option value="Bengali">Bengali</option>
|
| 82 |
+
<option value="Telugu">Telugu</option><option value="Marathi">Marathi</option><option value="Tamil">Tamil</option>
|
| 83 |
+
<option value="Gujarati">Gujarati</option><option value="Urdu">Urdu</option><option value="Kannada">Kannada</option>
|
| 84 |
+
<option value="Odia">Odia</option><option value="Malayalam">Malayalam</option>
|
| 85 |
+
</select>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="col-md-4">
|
| 88 |
+
<button id="analyze-soil" class="btn btn-success w-100">
|
| 89 |
+
<span class="spinner-border spinner-border-sm loader" role="status" aria-hidden="true"></span>
|
| 90 |
+
Analyze with AI
|
| 91 |
+
</button>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
<hr>
|
| 95 |
+
<div id="analysis-result"></div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<!-- Bootstrap & Leaflet JS -->
|
| 102 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 103 |
+
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
| 104 |
+
<script>
|
| 105 |
+
// --- Global State ---
|
| 106 |
+
let soilReportData = null;
|
| 107 |
+
let map, marker;
|
| 108 |
+
|
| 109 |
+
// --- UI Helper Functions ---
|
| 110 |
+
const showAlert = (message, type = 'danger') => {
|
| 111 |
+
const alertContainer = document.getElementById('alert-container');
|
| 112 |
+
alertContainer.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
| 113 |
+
${message}
|
| 114 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
| 115 |
+
</div>`;
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
const toggleLoader = (buttonId, show) => {
|
| 119 |
+
const button = document.getElementById(buttonId);
|
| 120 |
+
const loader = button.querySelector('.loader');
|
| 121 |
+
button.disabled = show;
|
| 122 |
+
loader.style.display = show ? 'inline-block' : 'none';
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
// --- Map Initialization ---
|
| 126 |
+
const initializeMap = () => {
|
| 127 |
+
map = L.map('map').setView([20.5937, 78.9629], 5);
|
| 128 |
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
| 129 |
+
attribution: '© OpenStreetMap contributors'
|
| 130 |
+
}).addTo(map);
|
| 131 |
+
|
| 132 |
+
map.on('click', e => {
|
| 133 |
+
const { lat, lng } = e.latlng;
|
| 134 |
+
document.getElementById('lat').value = lat.toFixed(6);
|
| 135 |
+
document.getElementById('lon').value = lng.toFixed(6);
|
| 136 |
+
if (marker) marker.setLatLng(e.latlng);
|
| 137 |
+
else marker = L.marker(e.latlng).addTo(map);
|
| 138 |
+
});
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
// --- Event Listeners ---
|
| 142 |
+
document.getElementById('current-location').addEventListener('click', () => {
|
| 143 |
+
if (navigator.geolocation) {
|
| 144 |
+
navigator.geolocation.getCurrentPosition(pos => {
|
| 145 |
+
const { latitude, longitude } = pos.coords;
|
| 146 |
+
document.getElementById('lat').value = latitude.toFixed(6);
|
| 147 |
+
document.getElementById('lon').value = longitude.toFixed(6);
|
| 148 |
+
const latlng = L.latLng(latitude, longitude);
|
| 149 |
+
map.setView(latlng, 13);
|
| 150 |
+
if (marker) marker.setLatLng(latlng);
|
| 151 |
+
else marker = L.marker(latlng).addTo(map);
|
| 152 |
+
}, () => showAlert('Could not get your location.'));
|
| 153 |
+
} else {
|
| 154 |
+
showAlert('Geolocation is not supported by your browser.');
|
| 155 |
+
}
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
document.getElementById('fetch-report').addEventListener('click', () => {
|
| 159 |
+
const lat = document.getElementById('lat').value;
|
| 160 |
+
const lon = document.getElementById('lon').value;
|
| 161 |
+
if (!lat || !lon) return showAlert('Please provide latitude and longitude.');
|
| 162 |
+
|
| 163 |
+
toggleLoader('fetch-report', true);
|
| 164 |
+
fetch('/get_soil_report', {
|
| 165 |
+
method: 'POST',
|
| 166 |
+
headers: { 'Content-Type': 'application/json' },
|
| 167 |
+
body: JSON.stringify({ lat, lon })
|
| 168 |
+
})
|
| 169 |
+
.then(response => response.json().then(data => ({ ok: response.ok, data })))
|
| 170 |
+
.then(({ ok, data }) => {
|
| 171 |
+
if (!ok) throw new Error(data.error || 'Unknown error occurred.');
|
| 172 |
+
soilReportData = data;
|
| 173 |
+
renderSoilReport(data);
|
| 174 |
+
document.getElementById('soil-report-section').classList.remove('d-none');
|
| 175 |
+
document.getElementById('analysis-section').classList.remove('d-none');
|
| 176 |
+
})
|
| 177 |
+
.catch(err => showAlert(`Error fetching soil report: ${err.message}`))
|
| 178 |
+
.finally(() => toggleLoader('fetch-report', false));
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
document.getElementById('analyze-soil').addEventListener('click', () => {
|
| 182 |
+
if (!soilReportData) return showAlert('Please fetch a soil report first.');
|
| 183 |
+
|
| 184 |
+
toggleLoader('analyze-soil', true);
|
| 185 |
+
document.getElementById('analysis-result').innerHTML = '';
|
| 186 |
+
|
| 187 |
+
// The Authorization header is no longer sent from the frontend
|
| 188 |
+
fetch('/analyze_soil', {
|
| 189 |
+
method: 'POST',
|
| 190 |
+
headers: { 'Content-Type': 'application/json' },
|
| 191 |
+
body: JSON.stringify({
|
| 192 |
+
soil_report: soilReportData,
|
| 193 |
+
language: document.getElementById('language').value
|
| 194 |
+
})
|
| 195 |
+
})
|
| 196 |
+
.then(response => response.json().then(data => ({ ok: response.ok, data })))
|
| 197 |
+
.then(({ ok, data }) => {
|
| 198 |
+
if (!ok) throw new Error(data.error || 'Failed to get analysis.');
|
| 199 |
+
renderAnalysis(data);
|
| 200 |
+
})
|
| 201 |
+
.catch(err => showAlert(`Error during analysis: ${err.message}`))
|
| 202 |
+
.finally(() => toggleLoader('analyze-soil', false));
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
// --- Rendering Functions (No change here) ---
|
| 206 |
+
const renderSoilReport = (data) => {
|
| 207 |
+
let classHtml = `<h5>Soil Classification</h5><p><strong>Type:</strong> ${data.classification.soil_type}</p>`;
|
| 208 |
+
if (data.classification.probabilities.length) {
|
| 209 |
+
classHtml += `<table class="table table-sm table-bordered"><thead><tr><th>Type</th><th>Probability</th></tr></thead><tbody>`;
|
| 210 |
+
data.classification.probabilities.forEach(([type, prob]) => {
|
| 211 |
+
classHtml += `<tr><td>${type}</td><td>${prob}%</td></tr>`;
|
| 212 |
+
});
|
| 213 |
+
classHtml += `</tbody></table>`;
|
| 214 |
+
}
|
| 215 |
+
document.getElementById('classification-report').innerHTML = classHtml;
|
| 216 |
+
|
| 217 |
+
let propHtml = `<h5>Soil Properties (5-15cm)</h5><table class="table table-sm table-striped"><thead><tr><th>Parameter</th><th>Value</th></tr></thead><tbody>`;
|
| 218 |
+
data.properties.forEach(({ parameter, value, unit }) => {
|
| 219 |
+
const displayValue = (typeof value === 'number') ? `${value.toFixed(2)} ${unit}` : 'N/A';
|
| 220 |
+
propHtml += `<tr><td>${parameter}</td><td>${displayValue}</td></tr>`;
|
| 221 |
+
});
|
| 222 |
+
propHtml += `</tbody></table>`;
|
| 223 |
+
document.getElementById('properties-report').innerHTML = propHtml;
|
| 224 |
+
};
|
| 225 |
+
|
| 226 |
+
const renderAnalysis = (data) => {
|
| 227 |
+
let analysisHtml = `
|
| 228 |
+
<div class="mb-4">
|
| 229 |
+
<h4 class="text-success">${data.soilType}</h4>
|
| 230 |
+
<ul class="list-unstyled">
|
| 231 |
+
${data.generalInsights.map(insight => `<li>- ${insight}</li>`).join('')}
|
| 232 |
+
</ul>
|
| 233 |
+
</div>
|
| 234 |
+
|
| 235 |
+
<h5 class="mt-4">Parameter Analysis</h5>
|
| 236 |
+
<table class="table table-bordered">
|
| 237 |
+
<thead class="table-light"><tr><th>Parameter</th><th>Value</th><th>Range</th><th>Comment</th></tr></thead>
|
| 238 |
+
<tbody>
|
| 239 |
+
${data.parameters.map(p => `<tr><td>${p.parameter}</td><td>${p.value}</td><td>${p.range}</td><td>${p.comment}</td></tr>`).join('')}
|
| 240 |
+
</tbody>
|
| 241 |
+
</table>
|
| 242 |
+
|
| 243 |
+
<h5 class="mt-4">Crop Recommendations</h5>
|
| 244 |
+
<table class="table table-striped">
|
| 245 |
+
<thead class="table-light"><tr><th>Crop</th><th>Reason</th></tr></thead>
|
| 246 |
+
<tbody>
|
| 247 |
+
${data.cropRecommendations.map(c => `<tr><td>${c.crop}</td><td>${c.reason}</td></tr>`).join('')}
|
| 248 |
+
</tbody>
|
| 249 |
+
</table>
|
| 250 |
+
|
| 251 |
+
<h5 class="mt-4">Management Recommendations</h5>
|
| 252 |
+
<p><strong>Fertilization:</strong> ${data.managementRecommendations.fertilization}</p>
|
| 253 |
+
<p><strong>Irrigation:</strong> ${data.managementRecommendations.irrigation}</p>
|
| 254 |
+
`;
|
| 255 |
+
document.getElementById('analysis-result').innerHTML = analysisHtml;
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
// --- Initialize App ---
|
| 259 |
+
document.addEventListener('DOMContentLoaded', initializeMap);
|
| 260 |
+
</script>
|
| 261 |
+
</body>
|
| 262 |
+
</html>
|
templates/predict.html
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Pump Prediction</title>
|
| 7 |
+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
| 8 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
body { background-color: #f4f8f4; }
|
| 11 |
+
.card { border-radius: 15px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); }
|
| 12 |
+
.card-header { background-color: #28a745; color: white; font-weight: bold; text-align: center; font-size: 1.5rem; }
|
| 13 |
+
.status-card { text-align: center; }
|
| 14 |
+
.status-label { font-size: 1.2rem; color: #555; }
|
| 15 |
+
.status-text { font-size: 2rem; font-weight: bold; padding: 10px 20px; border-radius: 8px; display: inline-block; transition: all 0.3s ease; }
|
| 16 |
+
.status-text.on { color: #28a745; background-color: #e9f5e9; border: 2px solid #28a745; }
|
| 17 |
+
.status-text.off { color: #dc3545; background-color: #f8d7da; border: 2px solid #dc3545; }
|
| 18 |
+
.chart-container { width: 100%; height: 400px; }
|
| 19 |
+
</style>
|
| 20 |
+
</head>
|
| 21 |
+
<body>
|
| 22 |
+
<div class="container my-4">
|
| 23 |
+
<div class="card">
|
| 24 |
+
<div class="card-header">Live Irrigation Status</div>
|
| 25 |
+
<div class="card-body">
|
| 26 |
+
<div class="row text-center mb-4">
|
| 27 |
+
<div class="col-md-6">
|
| 28 |
+
<div class="status-label">Current Pump Status</div>
|
| 29 |
+
<div id="pumpStatus" class="status-text off">Loading...</div>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="col-md-6 mt-3 mt-md-0">
|
| 32 |
+
<div class="status-label">Time Elapsed</div>
|
| 33 |
+
<div id="time-counter" class="status-text" style="color: #5a2d0c; background-color: #f0e6e0; border-color: #5a2d0c;">0 seconds</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div class="row">
|
| 38 |
+
<div class="col-lg-6 mb-4">
|
| 39 |
+
<div class="card h-100">
|
| 40 |
+
<div class="card-body">
|
| 41 |
+
<div id="gauge" class="chart-container"></div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="col-lg-6 mb-4">
|
| 46 |
+
<div class="card h-100">
|
| 47 |
+
<div class="card-body">
|
| 48 |
+
<div id="graph1" class="chart-container"></div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="col-lg-6 mb-4">
|
| 53 |
+
<div class="card h-100">
|
| 54 |
+
<div class="card-body">
|
| 55 |
+
<div id="graph2" class="chart-container"></div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="col-lg-6 mb-4">
|
| 60 |
+
<div class="card h-100">
|
| 61 |
+
<div class="card-body">
|
| 62 |
+
<div id="graph3" class="chart-container"></div>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<script>
|
| 72 |
+
let alertSound; // Will be initialized after user interaction
|
| 73 |
+
|
| 74 |
+
function initializeAudio() {
|
| 75 |
+
if (!alertSound) {
|
| 76 |
+
alertSound = new Audio('{{ url_for("static", filename="alarn_tune.mp3") }}');
|
| 77 |
+
console.log("Audio initialized.");
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
document.body.addEventListener('click', initializeAudio, { once: true });
|
| 81 |
+
|
| 82 |
+
function fetchPumpStatus() {
|
| 83 |
+
fetch('/update_pump_status')
|
| 84 |
+
.then(response => response.json())
|
| 85 |
+
.then(data => {
|
| 86 |
+
const statusElement = document.getElementById('pumpStatus');
|
| 87 |
+
const newStatus = data.pump_status;
|
| 88 |
+
const oldStatus = statusElement.innerText;
|
| 89 |
+
|
| 90 |
+
statusElement.innerText = newStatus;
|
| 91 |
+
statusElement.className = 'status-text ' + (newStatus === 'On' ? 'on' : 'off');
|
| 92 |
+
|
| 93 |
+
if (newStatus === 'Off' && oldStatus === 'On' && alertSound) {
|
| 94 |
+
alertSound.play().catch(e => console.error("Audio play failed:", e));
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function fetchGraphData() {
|
| 100 |
+
fetch('/update_graph')
|
| 101 |
+
.then(response => response.json())
|
| 102 |
+
.then(data => {
|
| 103 |
+
if (data.length === 0) return;
|
| 104 |
+
|
| 105 |
+
const time = data.map((_, i) => i * 2); // Assuming 2 second intervals
|
| 106 |
+
const soilMoisture = data.map(entry => entry[0]);
|
| 107 |
+
const pumpStatus = data.map(entry => entry[1]);
|
| 108 |
+
const currentSoilMoisture = soilMoisture[soilMoisture.length - 1];
|
| 109 |
+
|
| 110 |
+
// Responsive layout for all plots
|
| 111 |
+
const responsiveLayout = { margin: { t: 40, b: 50, l: 50, r: 20 }, autosize: true };
|
| 112 |
+
|
| 113 |
+
Plotly.react('gauge', getGaugeData(currentSoilMoisture), { ...responsiveLayout, title: 'Soil Moisture' });
|
| 114 |
+
Plotly.react('graph1', getPumpStatusData(time, pumpStatus), { ...responsiveLayout, title: 'Pump Status vs. Time', yaxis: { tickvals: [-1, 1], ticktext: ['Off', 'On'], range: [-1.5, 1.5] }});
|
| 115 |
+
Plotly.react('graph2', getSoilMoistureData(time, soilMoisture), { ...responsiveLayout, title: 'Soil Moisture vs. Time' });
|
| 116 |
+
Plotly.react('graph3', getMoistureVsStatusData(soilMoisture, pumpStatus), { ...responsiveLayout, title: 'Pump Status vs. Soil Moisture', yaxis: { tickvals: [-1, 1], ticktext: ['Off', 'On'], range: [-1.5, 1.5] }});
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// --- Chart Data Functions ---
|
| 121 |
+
const getGaugeData = value => [{ type: "indicator", mode: "gauge+number", value: value, gauge: { axis: { range: [0, 100] }, steps: [{ range: [0, 30], color: "#ea4335" },{ range: [30, 60], color: "#fbbc05" },{ range: [60, 100], color: "#34a853" }]}}];
|
| 122 |
+
const getPumpStatusData = (x, y) => [{ x, y, mode: 'lines+markers', type: 'scatter', line: { color: '#4285f4' } }];
|
| 123 |
+
const getSoilMoistureData = (x, y) => [{ x, y, mode: 'lines+markers', type: 'scatter', line: { color: '#34a853' } }];
|
| 124 |
+
const getMoistureVsStatusData = (x, y) => [{ x, y, mode: 'lines', type: 'scatter', line: { color: '#ea4335' } }];
|
| 125 |
+
|
| 126 |
+
let timeCounter = 0;
|
| 127 |
+
setInterval(() => { document.getElementById('time-counter').innerText = `${++timeCounter} seconds`; }, 1000);
|
| 128 |
+
setInterval(fetchPumpStatus, 2000);
|
| 129 |
+
setInterval(fetchGraphData, 2000);
|
| 130 |
+
</script>
|
| 131 |
+
</body>
|
| 132 |
+
</html>
|