diff --git a/VIPSWeb/settings.py b/VIPSWeb/settings.py
index 64169738dfedf68255e289df65013f6f032e9375..6d0be063ff55c89c245dbff1b2982b7ed1449996 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 a518e33344c65225d7a3fd9d71ddeb59822cb690..d5b0f126023fc736f34478a5e74d5c854b218ad7 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ipmd/admin.py b/ipmd/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e
--- /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 0000000000000000000000000000000000000000..a56463fc5eee2c0ef41757642b2e8e989138e104
--- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/ipmd/models.py b/ipmd/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91
--- /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 0000000000000000000000000000000000000000..97711188f44b0e50a1a9006c8e752782bd976409
--- /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 0000000000000000000000000000000000000000..1e005223c037ad4086fd7d253b6c752395b8b515
--- /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 0000000000000000000000000000000000000000..56097a52a33f9e54d5ec4d07723b908dd4cba6c7
--- /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 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6
--- /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 0000000000000000000000000000000000000000..f076576d0af1a217592b2cfe4d0aa935b34e70c2
--- /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 0000000000000000000000000000000000000000..8f147afb1e7c08c34243d1d064549172b45ef64f
--- /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