diff --git a/VIPSWeb/local_settings_sample.py b/VIPSWeb/local_settings_sample.py index 466a2c166494e8bb09326682fc199d5ef5e3a4f2..58088731eddb2e4f917e142243ad5af2547b9db4 100755 --- a/VIPSWeb/local_settings_sample.py +++ b/VIPSWeb/local_settings_sample.py @@ -165,6 +165,10 @@ MAP_WARNING_LEGEND_PLACEMENT = 4 # The attribution text that appears in a corner of the map MAP_ATTRIBUTION = "© <a href='http://www.openstreetmap.org'>OpenStreetMap</a> contributors" +# Geoapify is used for getting information (timezone etc) about a given point (latitude, longitude) +# Register at https://www.geoapify.com, and create an API key in order to be able to use the API +GEOAPIFY_API_KEY='api-key' + # The message tags to use on the front page FRONTPAGE_MESSAGE_TAG_IDS = [1,2,3] diff --git a/VIPSWeb/settings.py b/VIPSWeb/settings.py index ee36cbb49cc99254eea6d7fdf9d709ead9fd2e80..d91aa417e6de0beefa02df84048172d5db67784e 100755 --- a/VIPSWeb/settings.py +++ b/VIPSWeb/settings.py @@ -82,9 +82,6 @@ STATICFILES_FINDERS = ( # 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) - - - MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/VIPSWeb/static/js/util.js b/VIPSWeb/static/js/util.js index 633a3ccc8fe992d4d4ecaba00e48cc040852838f..6443151f15c40e34c50106bc84ce7d1503b42a3c 100755 --- a/VIPSWeb/static/js/util.js +++ b/VIPSWeb/static/js/util.js @@ -421,3 +421,28 @@ function getLocalizedCropCategoryName(cropCategory) // Then we give up return gettext("Unnamed"); } + +async function getLocationInformation(latitude, longitude) { + let location = "Unknown" + let timezone = "Europe/Oslo" + + try { + const response = await fetch(`https://api.geoapify.com/v1/geocode/reverse?lat=${latitude}&lon=${longitude}&format=json&apiKey=${settings.geoapifyApiKey}`); + const respJson = await response.json() + if(respJson.results && respJson.results.length > 0) { + location = respJson.results[0].city ? respJson.results[0].city : respJson.results[0].formatted; + timezone = respJson.results[0].timezone.name; + } else { + console.error("Unable to get location information for for lat=" + latitude + " lon=" + longitude) + } + } catch (error) { + console.error("Error fetching location information", error) + } + return { + latitude: latitude, + longitude: longitude, + location: location, + timezone: timezone + } +} + diff --git a/VIPSWeb/templates/settings.js b/VIPSWeb/templates/settings.js index 4659ca6ce179995c8f5ceb19660448d02956439b..afe28c9b8d9552e17d2edaf383904255f0d337e9 100755 --- a/VIPSWeb/templates/settings.js +++ b/VIPSWeb/templates/settings.js @@ -43,6 +43,8 @@ var settings = { userIsIE: {{user_is_ie|yesno:"true,false"}}, + geoapifyApiKey: "{{settings.GEOAPIFY_API_KEY}}", + highchartsGlobalOptions: { lang: { shortMonths: [gettext("Jan"),gettext("Feb"),gettext("Mar"),gettext("Apr"),gettext("May"),gettext("Jun"),gettext("Jul"),gettext("Aug"),gettext("Sep"),gettext("Oct"),gettext("Nov"),gettext("Dec")], diff --git a/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/septoriaHumidityForm.json b/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/septoriaHumidityForm.json index 6005dbd07334b626f39a822c20284f1178f8c84d..915954d62d382f9efdaa1bc8fe73ab21c2c74e96 100755 --- a/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/septoriaHumidityForm.json +++ b/cerealblotchmodels/static/cerealblotchmodels/formdefinitions/septoriaHumidityForm.json @@ -18,18 +18,28 @@ ], "_comment" : "Structure of the septoriaHumidityForm and how to validate it", "fields": [ + { + "name" : "latitude", + "dataType" : "DOUBLE", + "required" : false + }, + { + "name" : "longitude", + "dataType" : "DOUBLE", + "required" : false + }, { "name" : "organizationId_countryCode", "dataType" : "STRING", "fieldType" : "SELECT_SINGLE", - "required" : true, + "required" : false, "nullValue" : "None" }, { "name" : "weatherStationId", "dataType" : "STRING", "fieldType" : "SELECT_SINGLE", - "required" : true, + "required" : false, "nullValue" : "" }, { diff --git a/cerealblotchmodels/templates/cerealblotchmodels/septoriahumiditymodelform.html b/cerealblotchmodels/templates/cerealblotchmodels/septoriahumiditymodelform.html index 65632da22e6ec598c51675b410e280c45bef4cba..dd0cd96ea7432b82b7c38623046f28c8101e3343 100644 --- a/cerealblotchmodels/templates/cerealblotchmodels/septoriahumiditymodelform.html +++ b/cerealblotchmodels/templates/cerealblotchmodels/septoriahumiditymodelform.html @@ -23,32 +23,82 @@ {% endcomment %} {% load i18n %} {% block title %}{% trans "Septoria humidity model" %}{% endblock %} + +{% block customCSS %} +<link type="text/css" rel="stylesheet" href="https://logic.testvips.nibio.no/css/3rdparty/leaflet.css" /> +<link type="text/css" rel="stylesheet" href="https://logic.testvips.nibio.no/css/mapModal.css" /> + +<style> + /* Added when integrating map, should perhaps be moved to main css file. */ + input#latitude, input#longitude { + margin: 10px 10px 10px 0; + } + select#organizationId_countryCode, select#weatherStationId { + margin: 5px; + } + .main-label { + font-size: 1.8rem; + font-weight: 500 !important; + } + .space { + margin-top: 40px; + } + .radio { + margin-top: 0; + margin-bottom: 20px; + } + #gridPointInfo { + margin: 10px 0; + } +</style> +{% endblock %} + {% block content %} <div class="singleBlockContainer"> <h1>{% trans "Septoria humidity model" %}</h1> - <p>{% trans "Fuktmodellen er et beslutningsstøtteverktøy, utviklet av <a href='https://www.seges.dk/'>SEGES</a>, Danmark, for å kunne vurdere risiko for angrep av hvetebladprikk i høsthvete under danske forhold. <a href='/forecasts/models/SEPTORIAHU/' target='new'>Les mer</a>, og se <a href='https://vimeo.com/818734601' target='new'>informasjonsvideo</a>" %}</p> + <p class="lead">{% trans "Fuktmodellen er et beslutningsstøtteverktøy, utviklet av <a href='https://www.seges.dk/'>SEGES</a>, Danmark, for å kunne vurdere risiko for angrep av hvetebladprikk i høsthvete under danske forhold. <a href='/forecasts/models/SEPTORIAHU/' target='new'>Les mer</a>, og se <a href='https://vimeo.com/818734601' target='new'>informasjonsvideo</a>" %}</p> <form role="form" id="{{ form_id }}"> - <div class="row"> - <div class="col-md-12"> - <h2>{% trans "Background data" %}</h2> - </div> - </div> <div class="row"> <div class="col-md-3"> - <div class="form-group"> - <label for="organizationId_countryCode">{% trans "Country" %}</label> - <select name="organizationId_countryCode" id="organizationId_countryCode" class="form-control" onchange="updateWeatherDataSources(this.options[this.options.selectedIndex].value);"> - <option value="None">{% trans "Please select" %}</option> - </select> - <span class="help-block" id="{{ form_id }}_organizationId_countryCode_validation"></span> - </div> - <div class="form-group"> - <label for="weatherStationId">{% trans "Weather station" %}</label> - <select name="weatherStationId" id="weatherStationId" class="form-control"> - <option value="">{% trans "Please select" %}</option> - </select> - <span class="help-block" id="{{ form_id }}_weatherStationId_validation"></span> - </div> + <fieldset> + <legend>Værdata</legend> + <div class="form-group"> + <div class="radio"> + <label> + <input type="radio" name="weatherDataSourceType" id="grid" value="grid" checked onchange="displayCoordinatesInput()"> + for et punkt i kartet + </label> + <div id="input-coordinates"> + <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()"><i class="fa fa-map-marker fa-lg"></i> Velg i kart</button> + </div> + <div id="coordinates-map" class="map-modal"></div> + </div> + <div class="radio"> + <label> + <input type="radio" name="weatherDataSourceType" id="weatherstation" value="weatherstation" onchange="displayWeatherstationInput()"> + fra en værstasjon + </label> + <div id="input-weatherstation" style="display: none;"> + <select name="organizationId_countryCode" id="organizationId_countryCode" class="form-control" onchange="updateWeatherDataSources(this.options[this.options.selectedIndex].value);"> + <option value="None">Velg land</option> + </select> + <select name="weatherStationId" id="weatherStationId" class="form-control" disabled> + <option value="">Velg værstasjon</option> + </select> + <button disabled type="button" id="poi-map-button" class="btn btn-primary" onclick="openPoiMap()"><i class="fa fa-map-marker fa-lg"></i> Velg i kart</button> + </div> + <div id="poi-map" class="map-modal"></div> + </div> + <span class="help-block" id="{{ form_id }}_organizationId_countryCode_validation"></span> + <span class="help-block" id="{{ form_id }}_weatherStationId_validation"></span> + <span class="help-block" id="{{ form_id }}_latitude_validation"></span> + <span class="help-block" id="{{ form_id }}_longitude_validation"></span> + </div> + </fieldset> <fieldset> <legend>{% trans "Sprayings" %}</legend> <div class="form-group"> @@ -62,8 +112,6 @@ <span class="help-block" id="{{ form_id }}_dateSpraying2_validation"></span> </div> </fieldset> - - </div> <div class="col-md-3"> <fieldset> @@ -94,7 +142,9 @@ <span class="help-block" id="{{ form_id }}_dateGs75_validation"></span> </div> </fieldset> - {% trans "Show advanced settings" %} <input type="checkbox" onclick="toggleAdvancedColumns(this);" autocomplete="off"/> + <div class="pull-right"> + {% trans "Show advanced settings" %} <input type="checkbox" onclick="toggleAdvancedColumns(this);" autocomplete="off"/> + </div> </div> <div class="col-md-3"> @@ -149,8 +199,8 @@ </div> </div> <div class="row"> - <div class="col-md-12 form-group"> - <button type="button" class="btn btn-primary" onclick="if(validateForm(document.getElementById('{{ form_id }}'))){storeUserSettings();runModel();}">{% trans "Run model" %}</button> + <div class="col-md-6 form-group"> + <button type="button" class="btn btn-primary pull-right" onclick="if(validateForm(document.getElementById('{{ form_id }}')) & validateFormExtra()){storeUserSettings();runModel();}">{% trans "Run model" %}</button> </div> </div> </form> @@ -179,7 +229,120 @@ <script type="text/javascript" src="{% static "js/util.js" %}"></script> <script type="text/javascript" src="{% static "js/validateForm.js" %}"></script> <script type="text/javascript" src="{% static "forecasts/js/forecasts.js" %}"></script> -<script type="text/javascript"> +<script type="module"> + import MapModal from 'https://logic.testvips.nibio.no/js/mapModal.js'; + //import MapModal from settings.vipslogicProtocol + "://" + settings.vipslogicServerName + "/js/mapModal.js" + + const theForm = document.getElementById("{{ form_id }}"); + const inputLatitudeElement = document.getElementById("latitude"); + const inputLongitudeElement = document.getElementById("longitude"); + const selectWeatherstationElement = document.getElementById("weatherStationId"); + const openPoiMapButton = document.getElementById("poi-map-button"); + + let poiIdList = [] + + let selectedPoint = null; + let selectedFeature = undefined; + + function getSelectedPoiId() { + const value = selectWeatherstationElement.value; + const parsedValue = parseInt(value, 10); + return (!isNaN(parsedValue) && parsedValue > 0) ? parsedValue : undefined; + } + + function selectCoordinates(coordinatesData) { + const selectedLatitude = coordinatesData ? coordinatesData.latitude : undefined; + const selectedLongitude = coordinatesData ? coordinatesData.longitude : undefined; + + if(selectedLatitude && selectedLongitude) { + console.error("Coordinates selected!") + inputLatitudeElement.value = selectedLatitude; + inputLongitudeElement.value = selectedLongitude; + getTimezone(selectedLatitude, selectedLongitude); + } + } + + 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.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, settings.currentLanguage, isPoiMap, allowNewPoints, selectCoordinates); + coordinatesMapInstance.openModal(selectedPoint); + } + + window.openPoiMap = () => { + fetch(settings.vipslogicProtocol + "://" + settings.vipslogicServerName + "/rest/poi/geojson", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(poiIdList) + }) + .then(response => response.json()) + .then(geoJson => { + 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, settings.currentLanguage, isPoiMap, allowNewPoints, selectPoi); + const selectedPoiId = getSelectedPoiId(); + poiMapInstance.openModal(selectedPoiId); + }) + .catch(error => { + console.error('Unable to retrieve weatherstation geojson', error); + }); + } + + window.displayWeatherstationInput = () => { + document.getElementById("weatherstation").checked = true; + document.getElementById('input-weatherstation').style.display="block"; + document.getElementById('input-coordinates').style.display="none"; + } + + window.displayCoordinatesInput = (id) => { + document.getElementById("grid").checked = true; + document.getElementById('input-weatherstation').style.display="none"; + document.getElementById('input-coordinates').style.display="block"; + } + + const getTimezone = (latitude, longitude) => { + getLocationInformation(latitude, longitude).then(locationInfo => { + theForm["timezone"].value = locationInfo.timezone; + document.getElementById("gridPointInfo").innerHTML = `<b>Sted</b> ${locationInfo.location}<br> + <b>Breddegrad</b> ${locationInfo.latitude}<br> + <b>Lengdegrad</b> ${locationInfo.longitude}<br> + <b>Tidssone</b> ${locationInfo.timezone}` + }); + } + var danishPostCodesUTM; var organizations; var allowedCountryCodes = ["NO","DK","SE","FI","LT"]; @@ -225,10 +388,30 @@ }); }); - - }); - + + window.validateFormExtra = () => { + // Location: Either weatherStationId or latitude/longitude must be set + const selectedWeatherdataType = theForm.querySelector('input[name="weatherDataSourceType"]:checked').value; + if( selectedWeatherdataType === "grid") { + const trimmedLat = theForm["latitude"].value.trim() + const trimmedLon = theForm["longitude"].value.trim() + if(trimmedLat === "" || trimmedLon === "" || isNaN(Number(trimmedLat)) || isNaN(Number(trimmedLon))) { + alert("Mangler gyldig punkt"); + return false; + } + } + else if (selectedWeatherdataType === "weatherstation") { + if(theForm["weatherStationId"].options[theForm["weatherStationId"].selectedIndex].value == "-1") { + alert("Mangler værstasjon"); + return false; + } + } else { + alert("Mangler type værdatakilde") + } + return true; + } + var initDanishPostCodesUTM = function(callback){ $.ajax({ type:"GET", @@ -279,13 +462,13 @@ } }); }; - - var updateWeatherDataSources = function(organizationId_countryCode){ - var selectList = document.getElementById("weatherStationId"); - selectList.options.length = 1; // Erase all former options + + window.updateWeatherDataSources = (organizationId_countryCode) => { + selectWeatherstationElement.options.length = 1; // Erase all former options - if(organizationId_countryCode === "None") - { + if(organizationId_countryCode === "None"){ + selectWeatherstationElement.disabled = true; + openPoiMapButton.disabled = true; return; } @@ -301,12 +484,13 @@ var city = postCodeEl.getElementsByTagName("CityName")[0].firstChild.nodeValue; var opt = new Option(city,UTM32v); //console.info(opt); - selectList.options[selectList.options.length] = opt; + selectWeatherstationElement.options[selectWeatherstationElement.options.length] = opt; } - renderUserSetting(selectList); + renderUserSetting(selectWeatherstationElement); } else { + poiIdList = [] $.ajax({ type:"GET", url: settings.vipslogicProtocol + "://" + settings.vipslogicServerName + "/rest/poi/organization/" + organizationId, @@ -318,6 +502,7 @@ for(var i in data) { var ws = data[i]; + poiIdList.push(ws["pointOfInterestId"]); wsHTML.push("<option value=\"" + ws["pointOfInterestId"] + "\">" + ws["name"] + "</option>"); } var wsSelect = document.getElementById("weatherStationId"); @@ -331,9 +516,11 @@ } }); } + selectWeatherstationElement.disabled = false; + openPoiMapButton.disabled = false; }; - var updateGSDates = function(){ + window.updateGSDates = () => { var dateGs31 = document.getElementById("dateGs31"); var date3rdUpperLeafEmergingdateGs31 = document.getElementById("date3rdUpperLeafEmerging"); var date2ndUpperLeafEmerging = document.getElementById("date2ndUpperLeafEmerging"); @@ -353,7 +540,7 @@ dateGs75.value = currentDate.format("YYYY-MM-DD"); } - var runModel = function(){ + window.runModel = () => { document.getElementById("chartContainer").style.display="none"; // Hide chart document.getElementById("warningStatusInterpretation").style.display="none"; // Insert please wait message @@ -385,8 +572,7 @@ "SEPTORIAHU.HPHPP" : "{% trans "Humid period hour outside protection period" %}" }; - var renderResults = function(data,textStatus, jqXHR) - { + window.renderResults = (data,textStatus, jqXHR) => { data.sort(compareForecastResults).reverse(); // First attempt: A table! var headingLine = "<tr><td style=\"font-weight: bold;\">{% trans "Time" %}</td>"; @@ -449,11 +635,12 @@ renderForecastChart("chartContainer", "{% trans "Septoria humidity model" %}", warningStatusPlotBandData, data); } - var handleAjaxError = function(jqXHR,textStatus,errorThrown){ + window.handleAjaxError = (jqXHR,textStatus,errorThrown) => { + document.getElementById("resultsTable").innerHTML="" alert(textStatus); }; - var renderUserSettings = function(userSettings){ + window.renderUserSettings = (userSettings) => { // Strip namespace from form field var theForm = document.getElementById('{{ form_id }}'); for(var i in userSettings){ @@ -464,7 +651,7 @@ } }; - var renderUserSetting = function(formField) + window.renderUserSetting = (formField) => { var localStorageKey = "{{form_id}}." + formField.name; @@ -475,7 +662,7 @@ } } - var getNameSpaced = function(nameSpace, anArray){ + window.getNameSpaced = (nameSpace, anArray) => { var retVal = []; for(var i = 0; i<anArray.length;i++) { @@ -484,7 +671,7 @@ return retVal; }; - var storeUserSettings = function(){ + window.storeUserSettings = () => { var theForm = document.getElementById('{{ form_id }}'); var settingsDict = {} for(var i in formFields) @@ -495,7 +682,7 @@ storeLocalSettings(settingsDict); }; - var toggleAdvancedColumns = function(theCheckBox){ + window.toggleAdvancedColumns = (theCheckBox) => { var col1 = document.getElementById("septoriaHumidityAdvancedColumn1"); var col2 = document.getElementById("septoriaHumidityAdvancedColumn2"); col1.style.visibility = theCheckBox.checked ? "visible" : "hidden";