From 56e87f393615640c59cae4f93f75bbae4df33e77 Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Wed, 6 Nov 2024 12:24:22 +0100
Subject: [PATCH] feat: add map and weather data source logic

---
 .../fusarium/js/oatFloweringModelForm.js      | 103 ++++----
 .../templates/fusarium/oat_flowering.html     | 235 ++++++++++++++++--
 fusarium/views.py                             |   7 +-
 3 files changed, 273 insertions(+), 72 deletions(-)

diff --git a/fusarium/static/fusarium/js/oatFloweringModelForm.js b/fusarium/static/fusarium/js/oatFloweringModelForm.js
index dd1848dd..19201d2d 100755
--- a/fusarium/static/fusarium/js/oatFloweringModelForm.js
+++ b/fusarium/static/fusarium/js/oatFloweringModelForm.js
@@ -49,7 +49,8 @@ var corePass = "";
 // Location of weather data source
 var weatherDataBaseUri = "https://lmt.nibio.no/agrometbase/export/getSeasonDailyTemperaturesJSON.php";
 //var weatherDataBaseUri = "http://agrometbase-local/agrometbase/export/getSeasonDailyTemperaturesJSON.php";
-
+const LMTServicesBaseUri = "http://localhost:8081/";
+const OpenMeteoBaseUri = "http://localhost:8082/rest/grid/openmeteo/"
 // Setting current time
 var now = moment();
 //var now = moment("2014-07-04");
@@ -76,7 +77,6 @@ var oatForm = function(options)
 	var height = workspace.offsetHeight;
 	var width = workspace.offsetWidth;
 	DEBUG = options.debug != null ? options.debug : false;
-	workspace.innerHTML = getStartHTML();
 	
 	var theForm = document.getElementById(theFormId);
 	
@@ -105,13 +105,13 @@ var submitForm = function()
 	var dateOfSowingLimit = moment(thisYear + "-07-01","YYYY-MM-DD");
 	if(getDateOfSowing().isAfter(dateOfSowingLimit))
 	{
-		alert("Saadato er etter 1. juli. Dette stemmer antakelig ikke. Ingen beregninger er foretatt.");
+		alert("Sådato er etter 1. juli. Dette stemmer antakelig ikke. Ingen beregninger er foretatt.");
 		return;
 	}
 	var theForm = document.getElementById(theFormId);
 	if(theForm.weatherStationId.options[theForm.weatherStationId.selectedIndex].value <= 0)
 	{
-		alert("Vennligst velg klimastasjon");
+		alert("Vennligst velg værstasjon");
 		return;
 	}
 	// See if test input now is set
@@ -131,15 +131,15 @@ var submitForm = function()
 /**
  * Creates the configuration for weather data. Moves on to running model after data returns
  */
-var createConfig = function(){
+const createConfig = async function(){
 	
 	
-	var theForm = document.getElementById(theFormId);
-	var weatherStationId = theForm.weatherStationId.options[theForm.weatherStationId.selectedIndex].value;
-	var dateOfSowing = getDateOfSowing();
-	// Challenge: Get weather data asynchronously first....
-	//Tip: Use jQuery.stringify
-	//var theConfig = 
+	const theForm = document.getElementById(theFormId);
+	const weatherStationId = theForm.weatherStationId.options[theForm.weatherStationId.selectedIndex].value;
+	const dateOfSowing = getDateOfSowing();
+	const endDateNormalData = moment(dateOfSowing).month(9).date(1);
+	
+
 	var weatherDataUri = [weatherDataBaseUri,
 	                      "?startDate=", dateOfSowing.format("YYYY-MM-DD"),
 	                      (DEBUG ? "&today=" + now.format("YYYY-MM-DD") : ""),
@@ -147,25 +147,46 @@ var createConfig = function(){
 	                      //"&callback=?"
 	                      ].join("");
 	
+	const normalDataRequestUri = LMTServicesBaseUri + "rest/vips/getdata/grovfornormal?" + 
+		"elementMeasurementTypes[]=TM&timeZone=Europe/Oslo" + 
+		"&weatherStationId=" + weatherStationId +
+		"&startDate=" + now.format("YYYY-MM-DD") +
+		"&endDate=" + now.year() + "-12-31"
 	
-	var jqxhr = $.ajax( {
-		url:weatherDataUri,
-		dataType: "json"
-		})
-	.done(function(data, textStatus, jqXHR) {
-		runModel(data);
-	})
-	.fail(function( jqXHR, textStatus,errorThrown ) {
-		alert( "Request failed: " + errorThrown );
-	})
-	.always(function() {
-	//alert( "complete" );
-	})
-	;
 	
-		
+	const normalDataResponse = await fetch(normalDataRequestUri);
+	const normalData = await normalDataResponse.json();
+	normalData.sort(sortWeatherData)
+
+	const measuredDataRequestUri = document.getElementById("coordinates").checked? 
+	OpenMeteoBaseUri + "?longitude=" + document.getElementById("longitude").value +
+		"&elementMeasurementTypes[]=TM&logIntervalId=2" +
+		"&latitude=" + document.getElementById("latitude").value + 
+		"&timeZone=Europe/Oslo" + 
+		"&startDate=" + dateOfSowing.format("YYYY-MM-DD") +
+		"&endDate=" + moment(now).add(10, "days").format("YYYY-MM-DD") 
+	: LMTServicesBaseUri + "rest/vips/getdata/forecastfallback?"+
+		"elementMeasurementTypes[]=TM&logInterval=1d" +
+		"&weatherStationId=" + weatherStationId +
+		"&timeZone=Europe/Oslo" + 
+		"&startDate=" + dateOfSowing.format("YYYY-MM-DD") + "&startTime=00" +
+		"&endDate=" + moment(now).add(10, "days").format("YYYY-MM-DD") + "&endTime=00";
+
+	const measuredDataResponse = await fetch(measuredDataRequestUri);
+	const measuredData = await measuredDataResponse.json();
+	// Combine data. Normal data has lowest priority
+	measuredData.sort(sortWeatherData);
+	const maxMeasuredDataTimestamp = moment(measuredData[measuredData.length-1].timeMeasured);
+	const mergedData = measuredData.concat(normalData.filter((obs) => moment(obs.timeMeasured) > maxMeasuredDataTimestamp));
+	console.info(mergedData);
+	runModel(mergedData);		
 };
 
+function sortWeatherData(a,b)
+{
+	return  moment(a.timeMeasured).unix()- moment(b.timeMeasured).unix() ;
+}
+
 /**
  * Calls the oatFlowering model in the VIPS forecasting system
  * Displays data when results are returned
@@ -231,6 +252,7 @@ var displayResults = function(data)
 		console.log("z62.5: " + getDateForZ(data,62.5).tz("Europe/Oslo").format());
 		console.log("z69: " + dateZ69.tz("Europe/Oslo").format());
 	*/
+	
 	// Scenario 1: Today is before date of sowing
 	if(now.isBefore(dateOfSowing))
 	{
@@ -312,6 +334,7 @@ var getRawResults = function(data)
  */
 var displayResultHTML = function(text)
 {
+	document.getElementById("oatFloweringModelResults").style.display="block";
 	document.getElementById("oatFloweringModelResults").innerHTML= "<h2>Resultater</h2>" + text;
 }
 
@@ -372,34 +395,6 @@ function getNow()
 	else return now;
 }
 
-/**
- * @return the HTML for the form
- */
-var getStartHTML = function()
-{
-	return [
-	        	 
-                 "<form id='",theFormId,"' role='form'>",
-                 "<div class='form-group'>",
-                 "<label for='dateOfSowing'>S&aring;dato</label> ",
-                 "<input class='form-control' type='date' name='dateOfSowing', value='",now.format("YYYY-MM-DD"),"'/><br/>",
-                 "</div>",
-                 (DEBUG ? "<div class='form-group'>" :""),
-                 (DEBUG ? "<label for='now'>N&aring;dato </label>" : ""),
-                 (DEBUG ? ["<input type='date' class='form-control' name='now' value='", now.format("YYYY-MM-DD"), "'/><br/>"].join("") : ""),
-                 (DEBUG ? "</div>" :""),
-                 "<div class='form-group'>",
-                 "<label for='weatherStationId'>Målestasjon</label>",
-                 "<select name='weatherStationId' class='form-control'>",
-                 "<option value='-1'>-- Vennligst velg værstasjon --</option>",
-                 "</select><br/>",
-                 "<button type='button' class='btn btn-default' onclick='submitForm();'>Beregn</button>",
-                 "</form>",
-                 "</div>",
-                 "<div id='resultsTable'></div>"
-                 ].join("");
-}
-
 var getAllValues = function(allValuesStr)
 {
 	return JSON.parse(allValuesStr);
diff --git a/fusarium/templates/fusarium/oat_flowering.html b/fusarium/templates/fusarium/oat_flowering.html
index 7bdd13a8..bad1d539 100755
--- a/fusarium/templates/fusarium/oat_flowering.html
+++ b/fusarium/templates/fusarium/oat_flowering.html
@@ -3,17 +3,72 @@
 {% block title%}{% trans "Oat flowering model" %}{%endblock%}
 {% block extendCSS %}
 
+{% endblock %}
+{% block customCSS %}
+<link type="text/css" rel="stylesheet" href="{{VIPSLOGIC_URL}}/css/3rdparty/leaflet.css" />
+<link type="text/css" rel="stylesheet" href="{{VIPSLOGIC_URL}}/css/mapModal.css" />
 {% endblock %}
 {% block content %}
 <div class="singleBlockContainer">
-	<h1>{% trans "Oat flowering model" %}</h1>
-	<p>
-        Her kan du beregne tidspunkt for n&aring;r havren er i blomst og dermed n&aring;r 
-        en eventuell behandling med soppmiddel mot Fusarium m&aring; utf&oslash;res. 
-        <a href='/forecasts/models/OATFLOWERM/' target='new'>Les mer</a>
-    </p>
-	<div id="oatFloweringModelForm" style="width:100%"></div>
-    <div id="oatFloweringModelResults" style="width:100%;"></div>
+	<div class="row">
+		<div class="col-md-12">
+			<h1>{% trans "Oat flowering model" %}</h1>
+			<p>
+				Her kan du beregne tidspunkt for n&aring;r havren er i blomst og dermed n&aring;r 
+				en eventuell behandling med soppmiddel mot Fusarium m&aring; utf&oslash;res. 
+				<a href='/forecasts/models/OATFLOWERM/' target='new'>Les mer</a>
+			</p>
+		</div>
+	</div>
+	<div class="row">
+		<div class="col-md-6">
+			<div id="oatFloweringModelForm" style="width:100%">
+				<form id="_oatFloweringModelForm" role="form">
+					<div class="form-group">
+						<label for="dateOfSowing">Sådato</label> 
+						<input class="form-control" type="date" name="dateOfSowing"  value=""><br>
+					</div>
+					<div class="form-group">
+						<h4>Værdata og prognoser</h4>
+						<p>For værdata fram til nå og prognoser de nærmeste dagene fram i tid vil jeg bruke</p>
+						<div class="radio">
+							<div class="radio">
+								<label>
+								<input type="radio" name="weatherdataType" id="weatherstation" value="weatherstation" onchange="hideCoordinatesInput();storeUserSettings();">
+								data fra værstasjonen valgt for normaldata (se nedenfor)
+								</label>
+							</div>
+							<label>
+								<input type="radio" name="weatherdataType" id="coordinates" value="coordinates" onchange="displayCoordinatesInput();storeUserSettings();">
+								data for et spesifikt punkt (koordinater)
+							</label>
+							<div id="input-coordinates" class="form-inline" style="margin-top: 10px; display: none;">
+								<input type="hidden" class="form-control" name="latitude" id="latitude" placeholder="Breddegrad" aria-label="Breddegrad">
+								<input type="hidden" class="form-control" name="longitude" id="longitude" placeholder="Lengdegrad" aria-label="Lengdegrad">
+								<input type="hidden" class="form-control" name="timezone" id="timezone" placeholder="Tidssone" aria-label="Tidssone">
+								<div id="gridPointInfo"></div>
+								<button type="button" class="btn btn-primary" onclick="openCoordinatesMap();" style="margin-left: 5px;"><i class="fa fa-map-marker fa-lg"></i>&nbsp;&nbsp;{% trans "Select in map" %}</button>
+							</div>
+							<div id="coordinates-map" class="map-modal"></div>
+						</div>
+					</div>
+					<div class="form-group form-inline">
+						<h4>Normaldata</h4>
+						<p>Brukes for beregning når tidspunkt for blomstring er senere enn 10 dager fram i tid</p>
+						<select name="weatherStationId" id="weatherStationId" class="form-control">
+							<option value="-1">-- Vennligst velg værstasjon --</option>
+						</select><button type="button" class="btn btn-primary" onclick="openPoiMap()" style="margin-left: 5px;"><i class="fa fa-map-marker fa-lg"></i>&nbsp;&nbsp;{% trans "Select in map" %}</button>
+						
+					</div>
+					<div class="form-group">
+						<button type="button" class="btn btn-primary" onclick="submitForm();">Beregn</button>
+					</div>
+					<div id="poi-map" class="map-modal"></div>
+				</form>
+			</div>
+		</div>
+	</div>
+    <div id="oatFloweringModelResults" class="alert alert-info" role="alert" style="width:100%; display: none;"></div>
 </div>
 {% endblock %}
 {% block extendJS%}
@@ -25,18 +80,47 @@
 <script type="text/javascript" src="{% static "js/3rdparty/modernizr_custom.js"%}"></script>
 <script type="text/javascript" src="{% url "javascript-catalog" %}"></script>
 <script type="text/javascript" src="{% url "views.settings_js" %}"></script>
+<script type="text/javascript" src="{% static "js/util.js" %}"></script>
+
 <script src="{% static "fusarium/js/oatFloweringModelForm.js" %}"></script>
-<script type="text/javascript">
-	
-	var weatherStations = [];
-	$(document).ready(function() {
-		$.getJSON("https://lmt.nibio.no/agrometbase/export/getNormalDataStationsJSON.php", function( json ) {
+<script type="module">
+	import MapModal from "{{VIPSLOGIC_URL}}/js/mapModal.js";
+	const userSettingsFields = ["latitude","longitude","timezone","weatherStationId","heatSumStartDate"];
+    const userSettingsRadios = ["weatherdataType"];
+	window.weatherStations = [];
+	window.weatherStationIds = [];
+
+	const inputLatitudeElement = document.getElementById("latitude");
+    const inputLongitudeElement = document.getElementById("longitude");
+    const selectWeatherstationElement = document.getElementById("weatherStationId");
+	window.onload = (event) => {
+
+		let userSettings = getLocalSettings(getNameSpaced("{{ form_id }}",userSettingsFields.concat(userSettingsRadios)), false);
+
+        
+        // Settings found, render form and run model
+        if(Object.keys(userSettings).length > 0)
+        {
+			console.info("User settings found, configuring form!");
+		}
+
+		fetch("https://lmt.nibio.no/agrometbase/export/getNormalDataStationsJSON.php")
+		.then(response => {
+			if(response.ok)
+			{
+				return response.json();		
+			}
+			else
+			{
+				throw new Error("Network response was not OK." + response.text);
+			}
+		})
+		.then(json => {
 			weatherStations = json;
+			weatherStationIds = weatherStations.map(ws=>ws.weatherstation_id);
 			var formApp = new oatForm({
 		  		target:"oatFloweringModelForm",
 		  		weatherStations:weatherStations,
-		  		//coreUsername:"gamlevips",
-		  		//corePass:"gamlevips123"
 		  		coreUsername:"testuser",
 		  		corePass:"testpass",
 	            debug : false
@@ -47,6 +131,125 @@
 			    $('input[type=date]').datepicker({ dateFormat: 'yy-mm-dd' });
 			}
 		});
-	});
+	};
+
+	window.displayCoordinatesInput = function () {
+        document.getElementById("coordinates").checked = true;
+		document.getElementById('input-coordinates').style.display="block";
+    }
+
+	window.hideCoordinatesInput = function () {
+        document.getElementById("weatherstation").checked = true;
+		document.getElementById('input-coordinates').style.display="none";
+    }
+
+	// Callback for coordinates map
+    function selectCoordinates(coordinatesData) {
+		const selectedLatitude = coordinatesData ? coordinatesData.latitude : undefined;
+		const selectedLongitude = coordinatesData ? coordinatesData.longitude : undefined;
+
+		if(selectedLatitude && selectedLongitude) {
+			inputLatitudeElement.value = selectedLatitude;
+			inputLongitudeElement.value = selectedLongitude;
+		}
+        getTimezoneForPoint(selectedLatitude, selectedLongitude);
+        storeUserSettings();
+	}
+
+    const getTimezoneForPoint = (latitude, longitude) => {
+		getLocationInformation(latitude, longitude).then(locationInfo => {
+			document.getElementById("timezone").value = locationInfo.timezone;
+			document.getElementById("gridPointInfo").innerHTML = `<b>{% trans "Location name" %}</b> ${locationInfo.location}<br>
+																	<b>{% trans "Latitude" %}</b> ${locationInfo.latitude}<br>	
+																	<b>{% trans "Longitude" %}</b> ${locationInfo.longitude}<br>
+																	<b>{% trans "Timezone" %}</b> ${locationInfo.timezone}`
+		});
+	}
+
+	let selectedPoint = null;
+    let selectedFeature = undefined;
+    window.openCoordinatesMap = () => {
+		if (inputLatitudeElement.value && inputLongitudeElement.value) {
+			selectedPoint = 1;
+			selectedFeature = { 
+				"type": "FeatureCollection", "features": [
+					{
+						"type": "Feature",
+						"geometry": {
+							"type": "Point",
+							"coordinates": [parseFloat(inputLongitudeElement.value), parseFloat(inputLatitudeElement.value)]
+						},
+						"properties": {
+							"pointOfInterestId": selectedPoint,
+						}
+					}]
+			};
+		} else {
+			selectedPoint = undefined;
+			selectedFeature = undefined;
+		}
+
+
+		const isPoiMap = false; // Map should enable selection of coordinates (not pois)
+        const allowNewPoints = true; // User should be able to select new pois
+        const coordinatesMapInstance = new MapModal('coordinates-map', selectedFeature, 'nb', isPoiMap, allowNewPoints, selectCoordinates);
+		coordinatesMapInstance.openModal(selectedPoint);
+	}
+
+	function selectPoi(poiData)
+    {
+        const selectedId = poiData ? poiData.pointOfInterestId : undefined;
+		if (selectedId) {
+			const optionIndex = Array.from(selectWeatherstationElement.options).findIndex(option => option.value == selectedId);
+			if (optionIndex !== -1) {
+				selectWeatherstationElement.selectedIndex = optionIndex;
+			} 
+		}
+    }
+
+    window.openPoiMap = () => {
+		fetch("https://lmt.nibio.no/services/rest/weatherstation/ipmdecisions", {
+                method: 'GET'
+            })
+            .then(response => response.json())
+            .then(geoJson => {
+                // FILTER with only Ids from the Cydia station list
+                let filteredFeatures = geoJson["features"]
+                .filter(feature => window.weatherStationIds.indexOf(feature.id) >= 0)
+                .map(feature => {
+                    feature["properties"]["pointOfInterestName"] = feature["properties"]["name"];
+                    feature["properties"]["pointOfInterestId"] = feature["id"];
+                    feature["properties"]["pointOfInterestTypeId"] = 1; // Type = Weather station
+                    return feature;
+                });
+                geoJson["features"] = filteredFeatures;
+				const isPoiMap = true; // Map should enable selection of pois
+		        const allowNewPoints = false; // User should not be able to create new pois
+				const poiMapInstance = new MapModal('poi-map', geoJson, 'nb', isPoiMap, allowNewPoints, selectPoi);
+                const selectedPoiId = parseInt(selectWeatherstationElement.options[selectWeatherstationElement.selectedIndex].value);
+                poiMapInstance.openModal(selectedPoiId >= 0 ? selectedPoiId : null);
+            })
+            .catch(error => {
+                console.error('Unable to retrieve weatherstation geojson', error);
+            });
+	}
+
+	window.storeUserSettings = function() {
+		return;
+        let userSettings = {};
+        userSettingsFields.forEach((fieldId) => {
+            userSettings[`{{ form_id }}.${fieldId}`] = document.getElementById(fieldId).value;
+        });
+        userSettingsRadios.forEach((radioName) => {
+            document.getElementsByName(radioName).forEach((radioElement) => {
+                if(radioElement.checked)
+                {
+                    userSettings[`{{ form_id }}.${radioName}`] = radioElement.value;
+                }
+            });
+
+        });
+        storeLocalSettings(userSettings);
+    }
 </script>
 {% endblock %}
diff --git a/fusarium/views.py b/fusarium/views.py
index 2b828d27..b4391103 100755
--- a/fusarium/views.py
+++ b/fusarium/views.py
@@ -19,10 +19,13 @@
 #
 #
 from django.shortcuts import render
+from django.conf import settings
 
 # Create your views here.
 
-
 def index(request):
-    context = {}
+    context = {
+        "VIPSLOGIC_URL" : f"{settings.VIPSLOGIC_PROTOCOL}://{settings.VIPSLOGIC_SERVER_NAME}",
+        "form_id": "oatFloweringForm"
+        }
     return render(request, 'fusarium/oat_flowering.html', context)
\ No newline at end of file
-- 
GitLab