From 9f55f3ebedc324703a7202e8724aedf3fce8ff5a Mon Sep 17 00:00:00 2001 From: Tor-Einar Skog <tor-einar.skog@nibio.no> Date: Fri, 11 Aug 2023 14:54:53 +0200 Subject: [PATCH] First test version (with mock result data) --- VIPSWeb/settings.py | 1 + VIPSWeb/urls.py | 1 + ipmd/__init__.py | 0 ipmd/admin.py | 3 + ipmd/apps.py | 6 + ipmd/migrations/__init__.py | 0 ipmd/models.py | 3 + ipmd/static/ipmd/js/ipmdlib.js | 96 ++++ ipmd/templates/ipmd/index.html | 32 ++ ipmd/templates/ipmd/saddlegallmidgeform.html | 515 +++++++++++++++++++ ipmd/tests.py | 3 + ipmd/urls.py | 28 + ipmd/views.py | 11 + 13 files changed, 699 insertions(+) create mode 100644 ipmd/__init__.py create mode 100644 ipmd/admin.py create mode 100644 ipmd/apps.py create mode 100644 ipmd/migrations/__init__.py create mode 100644 ipmd/models.py create mode 100644 ipmd/static/ipmd/js/ipmdlib.js create mode 100755 ipmd/templates/ipmd/index.html create mode 100644 ipmd/templates/ipmd/saddlegallmidgeform.html create mode 100644 ipmd/tests.py create mode 100755 ipmd/urls.py create mode 100644 ipmd/views.py diff --git a/VIPSWeb/settings.py b/VIPSWeb/settings.py index 64169738..6d0be063 100755 --- a/VIPSWeb/settings.py +++ b/VIPSWeb/settings.py @@ -143,6 +143,7 @@ INSTALLED_APPS = ( 'observations', 'information', 'cerealblotchmodels', + 'ipmd', 'calculators', 'roughage', 'applefruitmoth', diff --git a/VIPSWeb/urls.py b/VIPSWeb/urls.py index a518e333..d5b0f126 100755 --- a/VIPSWeb/urls.py +++ b/VIPSWeb/urls.py @@ -57,6 +57,7 @@ else: re_path(r'^observations/', include('observations.urls', namespace = "observations")), re_path(r'^information/', include('information.urls', namespace = "information")), re_path(r'^blotch/', include('cerealblotchmodels.urls', namespace = "cerealblotchmodels")), + re_path(r'^ipmd/', include('ipmd.urls', namespace = "ipmd")), re_path(r'^calculators/', include('calculators.urls', namespace = "calculators")), re_path(r'^roughage/', include('roughage.urls', namespace = "roughage")), re_path(r'^security/', include('security.urls', namespace = "security")), diff --git a/ipmd/__init__.py b/ipmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ipmd/admin.py b/ipmd/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/ipmd/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/ipmd/apps.py b/ipmd/apps.py new file mode 100644 index 00000000..a56463fc --- /dev/null +++ b/ipmd/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class IpmdConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'ipmd' diff --git a/ipmd/migrations/__init__.py b/ipmd/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ipmd/models.py b/ipmd/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/ipmd/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/ipmd/static/ipmd/js/ipmdlib.js b/ipmd/static/ipmd/js/ipmdlib.js new file mode 100644 index 00000000..97711188 --- /dev/null +++ b/ipmd/static/ipmd/js/ipmdlib.js @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSWeb. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSWeb. If not, see <http://www.nibio.no/licenses/>. + * + */ + +const ipmdDSSApiURL = "https://platform.ipmdecisions.net/api/dss/"; +const ipmdWeatherApiURL = "https://platform.ipmdecisions.net/api/wx/"; + + +async function getModelMetadata(dssId,modelId) { + const response = await fetch(ipmdDSSApiURL + "rest/dss/" + dssId); + const dss = await response.json(); + for(let i=0; i< dss.models.length;i++) + { + let model = dss.models[i]; + if(model.id == modelId) + { + return model; + } + } + return null; +} + +async function getModelInputSchema(dssId,modelId) { + const response = await fetch(ipmdDSSApiURL + "rest/model/" + dssId + "/" + modelId + "/input_schema/ui_form"); + return await response.json(); +} + +async function getWeatherDatasource(weatherDatasourceId) +{ + const response = await fetch(ipmdWeatherApiURL + "rest/weatherdatasource/" + weatherDatasourceId); + return await response.json(); +} + +function getWeatherStationList(weatherDatasource){ + geoJson = JSON.parse(weatherDatasource.spatial.geoJSON); + let stationList = []; + for(let i=0;i<geoJson.features.length;i++) + { + let feature = geoJson.features[i]; + let station = {"id":feature.id, "name":feature.properties.name} + stationList.push(station); + } + return stationList; +} + +async function getLocationWeatherData(endpoint, weatherStationId, parameters, interval, dateStart, dateEnd){ + const response = await fetch(endpoint + + "?timeStart=" + dateStart + + "&timeEnd=" + dateEnd + + "&interval=" + interval + + "&weatherStationId=" + weatherStationId + + "¶meters=" + parameters.join(",") + ); + return await response.json(); +} + +async function runModel(endpoint, inputData) +{ + const response = await fetch(endpoint, { + method: "POST", + mode: "cors", + cache: "no-cache", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(inputData) + }); + return await response.json(); +} + +function getDateArray(timestart, interval, length) +{ + let dateArray = []; + currentTime = moment(timestart); + for(let i=0;i< length; i++) + { + dateArray.push(currentTime.format("YYYY-MM-DD")); + currentTime.add(interval,'seconds'); + } + return dateArray; +} \ No newline at end of file diff --git a/ipmd/templates/ipmd/index.html b/ipmd/templates/ipmd/index.html new file mode 100755 index 00000000..1e005223 --- /dev/null +++ b/ipmd/templates/ipmd/index.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% load static %} +{% comment %} + +# +# Copyright (c) 2023 NIBIO <http://www.nibio.no/>. +# +# This file is part of VIPSWeb. +# VIPSWeb is free software: you can redistribute it and/or modify +# it under the terms of the NIBIO Open Source License as published by +# NIBIO, either version 1 of the License, or (at your option) any +# later version. +# +# VIPSWeb is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# NIBIO Open Source License for more details. +# +# You should have received a copy of the NIBIO Open Source License +# along with VIPSWeb. If not, see <http://www.nibio.no/licenses/>. +# + +{% endcomment %} +{% load i18n %} +{% block title%}{% trans "IPM Decisions models" %}{%endblock%} +{% block content %} +<h1>{% trans "IPM Decisions models" %}</h1> +<ul> + <li><a href="/ipmd/saddlegallmidge">{% trans "Saddle gall midge" %}</a></li> +</ul> + +{% endblock %} \ No newline at end of file diff --git a/ipmd/templates/ipmd/saddlegallmidgeform.html b/ipmd/templates/ipmd/saddlegallmidgeform.html new file mode 100644 index 00000000..56097a52 --- /dev/null +++ b/ipmd/templates/ipmd/saddlegallmidgeform.html @@ -0,0 +1,515 @@ +{% extends "base_with_date_picker.html" %} +{% load static %} +{% comment %} + +# +# Copyright (c) 2023 NIBIO <http://www.nibio.no/>. +# +# This file is part of VIPSWeb. +# VIPSWeb is free software: you can redistribute it and/or modify +# it under the terms of the NIBIO Open Source License as published by +# NIBIO, either version 1 of the License, or (at your option) any +# later version. +# +# VIPSWeb is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# NIBIO Open Source License for more details. +# +# You should have received a copy of the NIBIO Open Source License +# along with VIPSWeb. If not, see <http://www.nibio.no/licenses/>. +# + +{% endcomment %} +{% load i18n %} +{% block title %}{% trans "Saddle gall midge" %}{% endblock %} +{% block content %} +<div class="singleBlockContainer"> + <h1>{% trans "Saddle gall midge" %}</h1> + <div id="inputForm"></div> + <div id="weatherStations" style="display: none;"> + <h2>Weather stations</h2> + <select class="form-control" name="weatherStationId" id="weatherStationId"></select> + </div> + <button class="btn btn-primary" type="button" onclick="submitData();">Submit</button> + <div> + <canvas id="myChart"></canvas> + </div> + <pre id="modelDescription"></pre> +</div> +{% endblock %} +{% block customJS %} +<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script> +<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> +<script type="text/javascript" src="{% static "js/3rdparty/moment.min.js" %}"></script> +<script type="text/javascript" src="{% static "ipmd/js/ipmdlib.js" %}"></script> +<script type="text/javascript"> + // Page globals + var modelMetaData = undefined; + var weatherDatasource = undefined; + var editor = undefined; + const selectList = document.getElementById("weatherStationId"); + + async function initPage() { + modelMetaData = await getModelMetadata("adas.dss","HAPDMA"); + document.getElementById("modelDescription").innerHTML= await modelMetaData["description"]; + inputFormSchema = await getModelInputSchema("adas.dss","HAPDMA") + console.info(inputFormSchema); + const element = document.getElementById('inputForm'); + editor = new JSONEditor(element, + { + ajax: true, + schema: inputFormSchema, + theme: "bootstrap4" + }); + let fullSchema = JSON.parse(modelMetaData["execution"]["input_schema"]); + if(fullSchema["properties"]["weatherData"] !== undefined) + { + console.info("Adding weather stations"); + // Pull weather stations from web service, render list + weatherDatasource = await getWeatherDatasource("no.nibio.lmt"); + stationList = await getWeatherStationList(weatherDatasource); + stationList.sort((a, b) => { + return (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0; + }); + + for(let i=0;i<stationList.length; i++) + { + selectList.add(new Option(stationList[i].name, stationList[i].id)) + } + document.getElementById("weatherStations").style.display = "block"; + //console.info(weatherDatasource); + } + } + + initPage(); + + + async function submitData(){ + console.info("submitData!"); + let inputData = editor.getValue(); + console.info(inputData); + // Add hidden parameters + let fullSchema = JSON.parse(modelMetaData["execution"]["input_schema"]); + const hiddenParameters = modelMetaData["execution"]["input_schema_categories"]["hidden"]; + console.info(hiddenParameters); + for(let i=0;i<hiddenParameters.length;i++) + { + let hiddenParameter = hiddenParameters[i]; + inputData[hiddenParameter] = fullSchema["properties"][hiddenParameter]["default"]; + } + // Check for weatherData element. Assuming it's at the root node + if(fullSchema["properties"]["weatherData"] !== undefined) + { + console.info("Need to get weather data!"); + let weatherStationId = selectList.options[selectList.selectedIndex].value; + + let weatherData = await getLocationWeatherData( + weatherDatasource.endpoint, + weatherStationId, + (function (){ + let parameterList = [] + modelMetaData.input.weather_parameters.forEach(function(weatherParameter){ + parameterList.push(weatherParameter.parameter_code) + }) + return parameterList; + }()), + 3600, + inputData.optionalData.startDate, + inputData.optionalData.endDate, + ); + inputData["weatherData"] = weatherData; + } + // Ready to call server? + //console.info(JSON.stringify(inputData)); + //let result = await runModel(modelMetaData.execution.endpoint, inputData); + let result = mockResult; + //console.info(result); + displayResult(result); + }; + + function displayResult(result) { + + let chartData = []; + // Generate dates + let dates = getDateArray(result.timeStart, 86400, result.locationResult[0].length); + //console.info(dates); + for(let i=0; i< result.resultParameters.length;i++) + { + let dataset = {label:result.resultParameters[i], data: []} + for(let j=0;j<dates.length;j++) + { + dataset.data.push({x:dates[j],y:result.locationResult[0].data[j][i]}); + } + chartData.push(dataset); + } + + const ctx = document.getElementById('myChart'); + + new Chart(ctx, { + type: "line", + data: { + datasets: chartData + } + }) + } + + // Prototype result!!! + const mockResult={ + "timeStart": "2023-06-30T22:00:00+00:00", + "timeEnd": "2023-08-10T22:00:00+00:00", + "interval": 86400, + "resultParameters": [ + "Cumulative_DayDegrees", + "StartOfEmergenceThreshold", + "FirstThreshold", + "SecondThreshold", + "ThirdThreshold" + ], + "locationResult": [ + { + "longitude": 10.62687, + "latitude": 62.10944, + "altitude": 478.0, + "data": [ + [ + 12.957083333333332, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 26.471249999999998, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 38.164583333333333, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 50.12833333333333, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 63.424166666666665, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 76.315416666666664, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 90.9525, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 106.4175, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 124.07208333333334, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 143.23041666666666, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 158.54916666666665, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 173.02458333333331, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 186.06041666666664, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 198.31124999999997, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 210.43499999999997, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 224.8820833333333, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 239.05541666666665, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 251.71874999999997, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 263.06991666666664, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 274.32391666666666, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 284.68975, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 296.90266666666668, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 309.94766666666669, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 322.764, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 334.34983333333332, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 346.98191666666668, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 359.79066666666665, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 372.52816666666666, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 386.75358333333332, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 402.21066666666667, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 416.82858333333331, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 428.63025, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 442.98983333333331, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 456.554, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 470.31691666666666, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 483.14233333333334, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 497.56275, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 508.33566666666667, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 521.79775, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 533.56191666666666, + 500.0, + 750.47, + 1023.93, + 1500.0 + ], + [ + 545.3566992753623, + 500.0, + 750.47, + 1023.93, + 1500.0 + ] + ], + "warningStatus": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4 + ], + "width": 5, + "length": 41 + } + ], + "message": "Start of cumulative emergence: 07/08/2023 .", + "messageType": 0 + }; + +</script> +{% endblock %} \ No newline at end of file diff --git a/ipmd/tests.py b/ipmd/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/ipmd/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ipmd/urls.py b/ipmd/urls.py new file mode 100755 index 00000000..f076576d --- /dev/null +++ b/ipmd/urls.py @@ -0,0 +1,28 @@ +# +# Copyright (c) 2023 NIBIO <http://www.nibio.no/>. +# +# This file is part of VIPSWeb. +# VIPSWeb is free software: you can redistribute it and/or modify +# it under the terms of the NIBIO Open Source License as published by +# NIBIO, either version 1 of the License, or (at your option) any +# later version. +# +# VIPSWeb is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# NIBIO Open Source License for more details. +# +# You should have received a copy of the NIBIO Open Source License +# along with VIPSWeb. If not, see <http://www.nibio.no/licenses/>. +# + +from django.urls import re_path +from ipmd import views + +app_name = "ipmd" + +urlpatterns = [ + # ex: /forecasts/ + re_path(r'^$', views.index, name='index'), + re_path(r'saddlegallmidge/', views.saddlegallmidgeform, name='saddlegallmidgeform') +] \ No newline at end of file diff --git a/ipmd/views.py b/ipmd/views.py new file mode 100644 index 00000000..8f147afb --- /dev/null +++ b/ipmd/views.py @@ -0,0 +1,11 @@ +from django.shortcuts import render + +# Create your views here. + +def index(request): + context = {} + return render(request, 'ipmd/index.html', context) + +def saddlegallmidgeform(request): + context = {} + return render(request, 'ipmd/saddlegallmidgeform.html', context) \ No newline at end of file -- GitLab