Skip to content
Snippets Groups Projects
Commit 46ba5d23 authored by Lene Wasskog's avatar Lene Wasskog
Browse files

feat: Septoria humidity with point selection, first version

parent 17e201b6
No related branches found
No related tags found
1 merge request!21feat: Septoria humidity with point selection, first version
......@@ -165,6 +165,10 @@ MAP_WARNING_LEGEND_PLACEMENT = 4
# The attribution text that appears in a corner of the map
MAP_ATTRIBUTION = "&copy; <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]
......
......@@ -82,9 +82,6 @@ STATICFILES_FINDERS = (
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......
......@@ -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
}
}
......@@ -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")],
......
......@@ -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" : ""
},
{
......
......@@ -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>&nbsp;&nbsp;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>&nbsp;&nbsp;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";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment