Skip to content
Snippets Groups Projects
Commit 4997f228 authored by Tor-Einar Skog's avatar Tor-Einar Skog
Browse files

Merge branch 'saddlegallmidge-form-idec-372' into 'feature/gridv-6-mapserver-layer'

Saddlegallmidge form idec 372

See merge request !13
parents e011cbdc 53c4993a
No related branches found
No related tags found
2 merge requests!13Saddlegallmidge form idec 372,!12feat: Add test page (spatial) with mapserver layer in openlayers map
......@@ -143,6 +143,7 @@ INSTALLED_APPS = (
'observations',
'information',
'cerealblotchmodels',
'ipmd',
'calculators',
'roughage',
'applefruitmoth',
......
......@@ -58,6 +58,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")),
......
......@@ -13,7 +13,7 @@ Add a result parameter by clicking as shown below
![Click Add to add a new result parameter](./illustrations/add_result_param_1.png "Click Add to add a new result parameter")
The result parameter form is shown below.
* Namespace = The model ID (dictated by the model)
* Namespace = The model ID (dictated by the model) or WEATHER (Weather parameters are most times using the WEATHER namespace)
* Key = The parameter ID (dictated by the model)
* Name = The parameter name as displayed to users
* Description = Further description of the parameter (currently not in use)
......
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class IpmdConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ipmd'
from django.db import models
# Create your models here.
/*
* 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/>.
*
*/
/**
* Utilities and methods for using the IPM Decisions DSS and Weather APIs
*
* Dependencies:
* momentJS
* turf 6
* OpenLayers 4
*/
// TODO Refactor these to be set by the user of this library
const ipmdDSSApiURL = "https://platform.ipmdecisions.net/api/dss/";
//const ipmdWeatherApiURL = "https://platform.ipmdecisions.net/api/wx/";
const ipmdWeatherApiURL = "http://ipmdlocal/api/wx/";
/**
*
* @param {String} dssId The id of the DSS
* @param {String} modelId The id of the DSS model
* @returns {Json} metadata for the requested DSS model
*/
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;
}
/**
*
* @param {String} dssId The id of the DSS
* @param {String} modelId The id of the DSS model
* @returns the UI form components of the Json schema for input data for the requested model. Hidden fields are excluded.
*/
async function getModelInputSchema(dssId,modelId) {
const response = await fetch(ipmdDSSApiURL + "rest/model/" + dssId + "/" + modelId + "/input_schema/ui_form");
return await response.json();
}
/**
*
* @param {Json} weatherDatasource
* @returns The web service endpoint for the weather data source
*/
function getWeatherDatasourceEndpoint(weatherDatasource)
{
return weatherDatasource.endpoint.replace("{WEATHER_API_URL}",ipmdWeatherApiURL);
}
/**
*
* @param {Json} weatherDatasources The list of weather data sources
* @param {String} weatherDatasourceId the id of the requested weather data source
* @returns {Json} Metadata for the requested weather datasource
*/
function getWeatherDatasource(weatherDatasources, weatherDatasourceId)
{
for(let i=0;i<weatherDatasources.length;i++)
{
if(weatherDatasources[i].id == weatherDatasourceId)
{
return weatherDatasources[i];
}
}
return null;
}
/**
*
* @returns {Json} list of all available weather datasources
*/
async function getWeatherDatasources()
{
const response = await fetch(ipmdWeatherApiURL + "rest/weatherdatasource/");
return await response.json();
}
/**
*
* @param {String} weatherDatasource
* @returns {Array} Weather stations listed as part of this weather data source. Example list item: {"id":"5", "name":"Merrapanna"}
*/
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);
}
stationList.sort((a, b) => {
return (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0;
});
return stationList;
}
/**
* List all weather stations in list in the provided HTML Form select list
* @param {Array} stationList
* @param {HTMLElement} selectList
*/
function renderWeatherStationSelectList(stationList, selectList){
selectList.options.length = 0;
selectList.add(new Option("-- Please select weather station --", -1));
for(let i=0;i<stationList.length; i++)
{
selectList.add(new Option(stationList[i].name, stationList[i].id));
}
}
/**
*
* @param {String} endpoint URL to web service endpoint
* @param {String} weatherStationId Id of the weather station
* @param {Array} parameters List of weather parameters (IPM Decisions codes)
* @param {Int} interval log interval for the weather data, provided in seconds. E.g. 3600 = hourly, 86400 = daily
* @param {String} dateStart (YYYY-MM-DD)
* @param {String} dateEnd (YYYY-MM-DD)
* @returns weather data from a station based weather data source
*/
async function getStationWeatherData(endpoint, weatherStationId, parameters, interval, dateStart, dateEnd){
const response = await fetch(endpoint
+ "?timeStart=" + dateStart
+ "&timeEnd=" + dateEnd
+ "&interval=" + interval
+ "&weatherStationId=" + weatherStationId
+ "&parameters=" + parameters.join(",")
);
return await response.json();
}
/**
*
* @param {String} endpoint URL to web service endpoint
* @param {Float} longitude Decimal degrees (WGS84) for the location
* @param {Float} latitude Decimal degrees (WGS84) for the location
* @param {Array} parameters List of weather parameters (IPM Decisions codes)
* @param {Int} interval log interval for the weather data, provided in seconds. E.g. 3600 = hourly, 86400 = daily
* @param {String} dateStart (YYYY-MM-DD)
* @param {String} dateEnd (YYYY-MM-DD)
* @returns weather data from a location based weather data source
*/
async function getLocationWeatherData(endpoint, longitude, latitude, parameters, interval, dateStart, dateEnd){
const response = await fetch(endpoint
+ "?timeStart=" + dateStart
+ "&timeEnd=" + dateEnd
+ "&interval=" + interval
+ "&longitude=" + longitude
+ "&latitude=" + latitude
+ "&parameters=" + parameters.join(",")
);
return (response.status == 200) ? await response.json() : null;
}
/**
* Run and get results back from a DSS model
* @param {String} endpoint The web service endpont for that DSS model
* @param {Json} inputData as defined by the Json schema in the DSS model metadata property input_data
* @returns {Json} model results in IPM Decisions format
*/
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();
}
/**
* Util model for getting an array of consecutive dates
* @param {Integer} timestart UNIX epoch seconds
* @param {Integer} interval log interval in seconds. E.g. 3600 = hourly, 86400 = daily
* @param {Integer} length How many dates you want in the array
* @returns
*/
function getDateArray(timestart, interval, length)
{
let dateArray = [];
currentTime = moment(timestart);
for(let i=0;i< length; i++)
{
dateArray.push(currentTime.valueOf());
currentTime.add(interval,'seconds');
}
return dateArray;
}
/**
*
* @param {Integer} weatherParameterId the parameter id
* @returns {Json} parameter metadata (name, description, unit, aggregation type (MIN/MAX/AVG))
*/
function getWeatherParameter(weatherParameterId){
for(let i=0;i<weatherParameterList.length;i++)
{
if(weatherParameterList[i].id == weatherParameterId)
{
return weatherParameterList[i];
}
}
return null;
}
/**
*
* @param {Json} weatherDatasource Metadata for the weather datasource
* @param {String} weatherStationId the requested weather station
* @returns {Array} coordinate (Decimal degrees/WGS84) of the weather station.
*/
function getWeatherStationCoordinate(weatherDatasource, weatherStationId)
{
geoJson = JSON.parse(weatherDatasource.spatial.geoJSON);
let stationList = [];
for(let i=0;i<geoJson.features.length;i++)
{
let feature = geoJson.features[i];
if(feature.id == weatherStationId)
{
// TODO: Don't assume feature is a point
return feature.geometry.coordinates;
}
}
return null;
}
/**
* Models may require weather parameters that the requested weather datasource
* can't provide. The difference between a requested parameter and available parameter
* may be insignificant. For instance, a model may require average temperature for an hour,
* and the data source may provide instantaneous temperature for that hour.
*
* This methods figures out which parameters are considered fallback parameters for each other,
* and returns the intersection of the requested parameters (including fallbacks) and the available
* parameters
*
* @param {Array} requestedParameters the parameters that the model requires
* @param {Array} availableParameters the parameters that the weather datasource provides
* @returns
*/
function getPragmaticWeatherParameterList(requestedParameters, availableParameters)
{
return getParametersWithFallbacks(requestedParameters).filter(param => availableParameters.includes(param));
}
function getParametersWithFallbacks(parameterList)
{
let completeList = [];
for(let i=0;i<parameterList.length;i++)
{
completeList = completeList.concat(parameterList[i], fallbackParams[parameterList[i]]);
}
return completeList;
}
function arePragmaticWeatherParametersInList(requestedParameters, availableParameters)
{
for(let i=0; i<requestedParameters.length;i++)
{
let requestedParameter = requestedParameters[i];
// 1. Is the requested parameter in the availableParameters list?
if(! availableParameters.includes(requestedParameter))
{
if(isEmpty(fallbackParams[requestedParameter]))
{
return false;
}
// If no, check if any fallback parameters are available
let match = false;
for(let j=0;j<fallbackParams[requestedParameter].length; j++)
{
if(availableParameters.includes(fallbackParams[requestedParameter][j]))
{
match = true;
}
}
if(!match)
{
return false;
}
}
}
return true;
}
function getRequiredParameters(modelMetaData)
{
let parameterList = []
modelMetaData.input.weather_parameters.forEach(function(weatherParameter){
parameterList.push(weatherParameter.parameter_code)
})
return parameterList;
}
/**
*
* @param {Array} coordinate [longitude,latitude] decimal degrees (WGS84)
* @param {Json} datasource metadata
* @returns
*/
async function isPointCoveredByDatasource(coordinate, datasource)
{
// If the geoJson is {"type":"Sphere"}, return true
if(datasource.spatial.geoJSON != null && JSON.parse(datasource.spatial.geoJSON).type.toLowerCase() == "sphere")
{
return true;
}
let geoJson = await getDatasourceFeatures(JSON.parse(datasource.spatial.geoJSON), datasource.spatial.countries);
let retVal = false;
for(let i=0; i<geoJson.features.length;i++)
{
if(turf.booleanPointInPolygon(coordinate, geoJson.features[i]))
{
retVal = true;
}
}
return retVal;
}
/**
* Displays the weather data in a table
* @param {Object} weatherData the weatherData in IPM Decisions format
* @param {HTMLElement} container the element to render to
* @param {String} tableClass for styling the table, e.g. using Bootstrap
*/
function renderWeatherData(weatherData, container, tableClass){
let dates = getDateArray(weatherData.timeStart, weatherData.interval, weatherData.locationWeatherData[0].length);
let html = "<table" + (tableClass != undefined ? " class=\"" + tableClass + "\"" : "") + "><thead><tr><th>Time</th>";
weatherData.weatherParameters.forEach(function(weatherParameterId){
html +="<th>" + getWeatherParameter(weatherParameterId).name + "</th>";
});
html += "</tr></thead>"
html += "<tbody>";
for(let i=0;i< weatherData.locationWeatherData[0].data.length;i++)
{
html += "<tr><td>" + moment(dates[i]).format("YYYY-MM-DD HH:mm:ss") + "</td>";
weatherData.locationWeatherData[0].data[i].forEach(function(val){html+="<td>" + val + "</td>";})
html += "</tr>";
}
html += "</tbody>";
html += "</table>";
container.innerHTML = html;
}
/**
* Merges the two datasets. Keeping the primaryData if both sets have data for same time and parameter
* If a merge operation is not possible, primaryData is returned unchanged
* @param {Object} primaryData
* @param {Object} secondaryData
*/
function mergeWeatherData(primaryData, secondaryData)
{
// Input check
// If one set is null, return the non-null one unchanged
// If both sets are null, return null
if(primaryData == null || secondaryData == null)
{
return primaryData == null ? secondaryData : primaryData;
}
// If the log interval differs, return primaryData
if(primaryData.interval != secondaryData.interval)
{
return primaryData;
}
// Initialize the result dataset
let mergedData = {
timeStart: moment(primaryData.timeStart <= secondaryData.timeStart ? primaryData.timeStart : secondaryData.timeStart),
timeEnd: moment(primaryData.timeEnd >= secondaryData.timeEnd ? primaryData.timeEnd : secondaryData.timeEnd),
interval: primaryData.interval,
weatherParameters: function() {
// Copy primaryData's weather parameters
var mergedParams = [];
for(let j=0;j<primaryData.weatherParameters.length;j++)
{
mergedParams.push(primaryData.weatherParameters[j]);
};
return mergedParams;
}(),
locationWeatherData: [
{
longitude: primaryData.locationWeatherData[0].longitude,
latitude: primaryData.locationWeatherData[0].latitude
}
]
};
// The parameter ordering may differ between primaryData and secondaryData
// We solve this by mapping the parameter in secondaryData to an index in primaryData
paramIndexes = {};
secondaryData.weatherParameters.forEach(element => {
if(mergedData.weatherParameters.includes(element))
{
paramIndexes[element] = mergedData.weatherParameters.indexOf(element);
}
// Checking for fallback parameters
else if(Object.keys(fallbackParams).includes(element.toString()))
{
fallbackParams[element.toString()].forEach(fbElement=>{
if(mergedData.weatherParameters.includes(fbElement))
{
paramIndexes[element] = mergedData.weatherParameters.indexOf(fbElement);
}
});
}
});
// Calculate dimensions of the merged data set
let length = (getUnix(mergedData.timeEnd) - getUnix(mergedData.timeStart)) / mergedData.interval;
let width = mergedData.weatherParameters.length;
//console.info(length + "*" + width);
let dataSet = Array.from(Array(length), () => new Array(width));
// Keeping track of offsets (if dataset does not cover the entire combined period of the merging datasets)
let primaryOffset = (getUnix(primaryData.timeStart) - getUnix(mergedData.timeStart)) / mergedData.interval;
let secondaryOffset = (getUnix(secondaryData.timeStart) - getUnix(mergedData.timeStart)) / mergedData.interval;
let primaryDataSet = primaryData.locationWeatherData[0].data;
let secondaryDataSet = secondaryData.locationWeatherData[0].data;
// Finally: Merge the data!
for(let i=0;i<dataSet.length;i++)
{
if(secondaryOffset <= i && i-secondaryOffset < secondaryDataSet.length)
{
for(let j=0; j<secondaryData.weatherParameters.length;j++)
{
if(paramIndexes[secondaryData.weatherParameters[j]] != undefined && !isEmpty(secondaryDataSet[i-secondaryOffset][j]))
{
dataSet[i][paramIndexes[secondaryData.weatherParameters[j]]] = secondaryDataSet[i-secondaryOffset][j];
}
}
}
if(primaryOffset <= i && i-primaryOffset < primaryDataSet.length )
{
for(let j=0; j<primaryData.weatherParameters.length;j++)
{
if(!isEmpty(primaryDataSet[i-primaryOffset][j]))
{
dataSet[i][j] = primaryDataSet[i-primaryOffset][j];
}
}
}
}
mergedData.locationWeatherData[0].data = dataSet;
return mergedData;
}
/**
* Util method if you don't know the type of object you're trying to get a Unix timestamp from
* @param {Object} aDate
* @returns {BigInt} the Unix timestamp
*/
function getUnix(aDate)
{
if(typeof aDate == "string")
{
aDate = moment(aDate);
}
return aDate.unix();
}
/**
* Saves a bit of typing. Currently checks if the val is either undefined or null
* @param {Object} val
* @returns {Boolean} true if the value is considered empty
*/
function isEmpty(val)
{
return val === undefined || val === null;
}
// Keeping track of maps
var maps = {};
/**
*
* @param {String} containerId the container where the map to destroy is anchored
*/
function destroyDataSourceMap(containerId)
{
let map = maps[containerId];
// Delete whatever was there
if(map != undefined)
{
map.setTarget(undefined);
map = null;
}
}
/**
* Requires OpenLayers v. 4
* @param {String} containerId
* @param {JSON} geoJson
* @param {Array} countryCodeList
* @param {Function} featureClickedCallback Callback function taking parameters ({String} id, {Array<Float>} coordinate)
*/
async function initDataSourceMap(containerId, geoJson, countryCodeList, featureClickedCallback)
{
let map = maps[containerId];
// Delete whatever was there
if(map != undefined)
{
map.setTarget(undefined);
map = null;
}
// If no data, no map
if((isEmpty(geoJson) || isEmpty(geoJson.features) || geoJson.features.length == 0) && (countryCodeList == null || countryCodeList.length == 0))
{
document.getElementById(containerId).innerHTML = "NO GEODATA PROVIDED BY WEATHER DATA SOURCE";
return;
}
else
{
document.getElementById(containerId).innerHTML = "";
}
// Using OpenStreetMap as default layer
var backgroundLayer = new ol.layer.Tile({
source: new ol.source.OSM()
});
// Creating the map
map = new ol.Map({
target: containerId,
layers: [backgroundLayer],
renderer: 'canvas'
});
// Adding it to list of maps, for bookkeeping
maps[containerId] = map;
// Setting zoom and center for the map (need to do this after creating map. so that we can transform our
// center to correct map projection)
var view = new ol.View({
center: ol.proj.transform([10,65], 'EPSG:4326', map.getView().getProjection().getCode()),
zoom: 7
});
map.setView(view);
// Read the datasource's geoJson features
let features = new ol.Collection();
let format = new ol.format.GeoJSON();
let drawnFeatures = await format.readFeatures(
await getDatasourceFeatures(geoJson, countryCodeList),
{
dataProjection: 'EPSG:4326',
featureProjection: map.getView().getProjection().getCode()
}
);
// Add any features to the featureOverlay
let featureOverlay = undefined;
if(drawnFeatures != undefined)
{
// Create an empty layer
featureOverlay = new ol.layer.Vector({
source: new ol.source.Vector({
features: features
}),
style: styleUnselected
});
// Add the stations or area
featureOverlay.getSource().addFeatures(drawnFeatures);
featureOverlay.setMap(map);
// Fit the features to the extent of the map
extent = featureOverlay.getSource().getExtent();
map.getView().fit(extent, map.getSize());
}
/*
* A little bit of this and that to make feature selection work the way we want
* - Highlight a selected station (but only one station, multiple select is not allowed)
* - If user clicks on multiple stations, zoom in to make it easy to pick the right one
*/
let selectInteraction = new ol.interaction.Select({
toggleCondition: ol.events.condition.never // Only one can be selected at a time
});
map.addInteraction(selectInteraction);
selectInteraction.on("select", function(e){
// If it's an area (id == undefined), don't mark it as selected
if(e.target.getFeatures().getLength() > 0 && e.target.getFeatures().item(0).getId() == undefined)
{
e.target.getFeatures().clear();
}
else if(e.target.getFeatures().getLength() == 1)
{
let feature = e.target.getFeatures().item(0);
featureClickedCallback(feature.getId(), undefined);
}
});
// We add this in order to detect if multiple features were clicked on, since the selectInteraction
// does not catch that
map.on('singleclick', function(evt) {
var pixel = map.getEventPixel(evt.originalEvent);
var coordinate = map.getEventCoordinate(evt.originalEvent);
handleMapClicked(map, featureOverlay, pixel, coordinate, featureClickedCallback);
});
}
/**
* Get "complete" spatial info from weather datasource, using either provided GeoJson or
* inferred from list of countries
* @param {JSON} geoJson
* @param {Array} countryCodeList
* @returns {Json} GeoJson
*/
async function getDatasourceFeatures(geoJson, countryCodeList)
{
// If we have geoJson available, we display that
if (geoJson != null && geoJson.features !== undefined && geoJson.features.length > 0) {
return geoJson;
}
return await getCountryBoundaries(countryCodeList);
}
// Default style for weather stations and areas
const styleUnselected = new ol.style.Style({
fill: new ol.style.Fill({
color: 'rgba(173, 173, 173, 0.5)'
}),
stroke: new ol.style.Stroke({
color: '#000000',
width: 1
}),
image: new ol.style.Circle({
radius: 6,
fill: new ol.style.Fill({
color: '#adadad'
}),
stroke: new ol.style.Stroke({
color: '#000000',
width: 1
})
})
});
/*
* Singleton pattern works. Used in function below
*/
/**
* When a user clicks on the map, handles whatever was clicked (station, point in area, point outside any feature/area)
* @param {ol.map} map
* @param {ol.layer} layer
* @param {ol.Pixel} pixel
* @param {ol.coordinate} coordinate
* @param {Function} featureClickedCallback Callback function taking parameters ({String} id, {Array<Float>} coordinate)
*/
function handleMapClicked(map, layer, pixel, coordinate, featureClickedCallback) {
var features = [];
map.forEachFeatureAtPixel(pixel, function(feature){
features.push(feature);
});
if (features.length == 1) {
// Area
if(features[0].getId() === undefined)
{
// We must place a marker where the user clicked
let marker = map.getOverlayById("marker");
// If first time click, create a new marker
if(marker == undefined)
{
map.addOverlay(new ol.Overlay({
id: "marker",
element: function() {
let markerElement = document.createElement("div");
markerElement.setAttribute(
"style",
"width: 20px; height: 20px; border: 1px solid #088; border-radius: 10px; background-color: #0FF; opacity: 0.5;"
);
return markerElement
}(),
position: coordinate,
positioning: "center-center"
})
);
}
// Otherwise, just move the current marker
else
{
marker.setPosition(coordinate);
}
featureClickedCallback(null, getDecimalDegrees(map, coordinate));
}
// Click on a station is handled in the select interaction (see initDataSourceMap)
}
// The select interaction (see initDataSourceMap) highlights one and only one
// selected feature. If the clicked pixel has more than one feature,
// zoom in to let the user be able to separate the features
else if(features.length > 1)
{
map.getView().setCenter(coordinate);
map.getView().setZoom(map.getView().getZoom() + 2);
}
};
function createMarkerOverlay(coordinate)
{
}
/**
*
* @param {ol.Map} map
* @param {Array<Float>} coordinate
* @return {ol.Coordinate} the pixel coordinate from a map converted to WGS84 Decimal degrees
*/
function getDecimalDegrees(map, coordinate)
{
return ol.proj.transform(coordinate, map.getView().getProjection().getCode(), 'EPSG:4326');
}
async function getCountryBoundaries(countryCodeList)
{
if(countryCodeList == undefined || countryCodeList == null)
{
return {};
}
const response = await fetch(ipmdWeatherApiURL + "rest/country/" + countryCodeList.join(","));
return await response.json();
}
/**
* List of fallback parameters to use if the weather data source does not provide the
* initially requested weather parameter
*/
const fallbackParams = {
1001: [1002],
1002: [1001],
3001: [3002],
3002: [3001],
4002: [4003,4012,4013],
4003: [4002,4013,4012],
4012: [4013,4002,4003],
4013: [4012,4003,4002]
}
/**
* Util method for programmatically selecting an option in a list
* @param {HTMLElement} selectList
* @param {String} optionValue
*/
function setSelection(selectList, optionValue)
{
for(let i=0;i<selectList.options.length; i++)
{
if(selectList.options[i].value == optionValue)
{
selectList.options[i].selected = true;
}
else
{
selectList.options[i].selected = false;
}
}
}
/**
* All the available weather parameters
*/
const weatherParameterList = [
{
"id": 1001,
"name": "Instantaneous temperature at 2m",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1002,
"name": "Mean air temperature at 2m",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1003,
"name": "Minimum air temperature at 2m",
"description": null,
"unit": "Celcius",
"aggregationType": "MIN"
},
{
"id": 1004,
"name": "Maximum air temperature at 2m",
"description": null,
"unit": "Celcius",
"aggregationType": "MAX"
},
{
"id": 1021,
"name": "Instantaneous temperature in canopy",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1022,
"name": "Mean air temperature in canopy",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1023,
"name": "Minimum air temperature in canopy",
"description": null,
"unit": "Celcius",
"aggregationType": "MIN"
},
{
"id": 1024,
"name": "Maximum air temperature in canopy",
"description": null,
"unit": "Celcius",
"aggregationType": "MAX"
},
{
"id": 1101,
"name": "Instantaneous temperature at -5cm (Celcius)",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1102,
"name": "Mean temperature at -5cm",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1111,
"name": "Instantaneous temperature at -10cm (Celcius)",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1112,
"name": "Mean temperature at -10cm",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1121,
"name": "Instantaneous temperature at -20cm (Celcius)",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1122,
"name": "Mean temperature at -20cm",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1131,
"name": "Instantaneous temperature at -30cm (Celcius)",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1132,
"name": "Mean temperature at -30cm",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1141,
"name": "Instantaneous temperature at -40cm (Celcius)",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1142,
"name": "Mean temperature at -40cm",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1151,
"name": "Instantaneous temperature at -50cm (Celcius)",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1152,
"name": "Mean temperature at -50cm",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 1901,
"name": "Dew point temperature",
"description": null,
"unit": "Celcius",
"aggregationType": "AVG"
},
{
"id": 2001,
"name": "Precipitation",
"description": null,
"unit": "mm",
"aggregationType": "SUM"
},
{
"id": 3001,
"name": "Instantaneous RH at 2m (%)",
"description": null,
"unit": "%",
"aggregationType": "AVG"
},
{
"id": 3002,
"name": "Mean RH at 2m",
"description": null,
"unit": "%",
"aggregationType": "AVG"
},
{
"id": 3003,
"name": "Minimum RH at 2m",
"description": null,
"unit": "%",
"aggregationType": "MIN"
},
{
"id": 3004,
"name": "Maximum RH at 2m",
"description": null,
"unit": "%",
"aggregationType": "MAX"
},
{
"id": 3021,
"name": "Instantaneous RH in canopy",
"description": null,
"unit": "%",
"aggregationType": "AVG"
},
{
"id": 3022,
"name": "Mean RH in canopy",
"description": null,
"unit": "%",
"aggregationType": "AVG"
},
{
"id": 3023,
"name": "Minimum RH in canopy",
"description": null,
"unit": "%",
"aggregationType": "MIN"
},
{
"id": 3024,
"name": "Maximum RH in canopy",
"description": null,
"unit": "%",
"aggregationType": "MAX"
},
{
"id": 3101,
"name": "Leaf wetness in 2m (minutes/hour)",
"description": null,
"unit": "minutes/hour",
"aggregationType": "SUM"
},
{
"id": 3102,
"name": "Leaf wetness in canopy",
"description": null,
"unit": "minutes/hour",
"aggregationType": "SUM"
},
{
"id": 3103,
"name": "Leaf wetness in grass",
"description": null,
"unit": "minutes/hour",
"aggregationType": "SUM"
},
{
"id": 4001,
"name": "Wind direction at 2m (degrees 0-360)",
"description": null,
"unit": "degrees",
"aggregationType": "AVG"
},
{
"id": 4002,
"name": "Instantaneous wind speed at 2m",
"description": null,
"unit": "m/s",
"aggregationType": "AVG"
},
{
"id": 4003,
"name": "Mean wind speed at 2m",
"description": null,
"unit": "m/s",
"aggregationType": "AVG"
},
{
"id": 4004,
"name": "Max wind speed at 2m",
"description": null,
"unit": "m/s",
"aggregationType": "MAX"
},
{
"id": 4005,
"name": "Min wind speed at 2m",
"description": null,
"unit": "m/s",
"aggregationType": "MIN"
},
{
"id": 4011,
"name": "Wind direction at 10m (degrees 0-360)",
"description": null,
"unit": "degrees",
"aggregationType": "AVG"
},
{
"id": 4012,
"name": "Instantaneous wind speed at 10m",
"description": null,
"unit": "m/s",
"aggregationType": "AVG"
},
{
"id": 4013,
"name": "Mean wind speed at 10m",
"description": null,
"unit": "m/s",
"aggregationType": "AVG"
},
{
"id": 4014,
"name": "Max wind speed at 10m",
"description": null,
"unit": "m/s",
"aggregationType": "MAX"
},
{
"id": 4015,
"name": "Min wind speed at 10m",
"description": null,
"unit": "m/s",
"aggregationType": "MIN"
},
{
"id": 5001,
"name": "Solar radiation (Q0) (W/sqm)",
"description": null,
"unit": "W/sqm",
"aggregationType": "SUM"
}
];
\ No newline at end of file
{% 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
{% 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="weatherDataForm" style="display: none;">
<h2>Weather data</h2>
<div class="row">
<div class="col-md-6">
<div id="parameterInfo" style="display: none;"></div>
<fieldset id="historicDatasourceFields">
<legend>Weather datasource (historic)</legend>
<select class="form-control" name="weatherDatasourceId" id="weatherDatasourceList" onchange="handleWeatherDatasourceSelected(this);"></select>
<div id="historicSourceInfoPanel" class="panel panel-default" style="display: none;">
<div class="panel-body" id="historicSourceInfo"></div>
</div>
</fieldset>
<fieldset id="stationFields" style="display: none;">
<select class="form-control" name="weatherStationId" id="weatherStationId" onchange="handleWeatherStationSelected(this);" ></select>
</fieldset>
<fieldset id="locationFields">
<legend>Location (decimal degrees)</legend>
<div class="form-group">
<label for="latitude">Latitude</label>
<input type="number" class="form-control" id="latitude" name="latitude" placeholder="Latitude">
</div>
<div class="form-group">
<label for="longitude">Longitude</label>
<input type="number" class="form-control" id="longitude" name="longitude" placeholder="Longitude">
</div>
</fieldset>
</div>
<div class="col-md-6"><div id="historicDatasourceMap"></div></div>
</div>
<div class="row">
<div class="col-md-6">
<fieldset id="forecastDatasourceFields">
<legend>Weather forecasts datasource</legend>
<select class="form-control" name="forecastWeatherDatasourceId" id="forecastWeatherDatasourceList" onchange="handleForecastSourceSelected(this);"></select>
<div id="forecastSourceInfoPanel" class="panel panel-default" style="display: none;">
<div class="panel-body" id="forecastSourceInfo"></div>
</div>
</fieldset>
</div>
<div class="col-md-6"><div id="forecastDatasourceMap"></div></div>
</div>
<button class="btn btn-primary" type="button" onclick="submitData();">Submit</button>
<div style="aspect-ratio: 2;">
<canvas id="resultChart"></canvas>
</div>
<div id="weatherData" class="table-responsive"></div>
<pre id="modelDescription"></pre>
</div>
{% endblock %}
{% block extendCSS %}
<link rel="stylesheet" href="{% static "css/3rdparty/ol.css" %}" type="text/css">
{% 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 src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
<script type="text/javascript" src="{% static "js/3rdparty/moment.min.js" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment"></script>
<script type="text/javascript" src="{% static "js/3rdparty/ol-debug.js" %}"></script>
<script type="text/javascript" src="{% static "ipmd/js/ipmdlib.js" %}"></script>
<script type="text/javascript">
// Page globals
var currentModelMetaData = undefined;
var currentWeatherDatasource = undefined;
var currentForecastWeatherDatasource = undefined;
var weatherDatasources = undefined;
var editor = undefined;
var selectList = document.getElementById("weatherStationId");
var weatherData = undefined;
async function initPage() {
currentModelMetaData = await getModelMetadata("adas.dss","HAPDMA");
document.getElementById("modelDescription").innerHTML= await currentModelMetaData["description"];
inputFormSchema = await getModelInputSchema("adas.dss","HAPDMA")
inputFormSchema.title ="Model input data";
const element = document.getElementById('inputForm');
// See https://github.com/json-editor/json-editor#options
editor = new JSONEditor(element,
{
ajax: true,
schema: inputFormSchema,
theme: "bootstrap4",
iconlib: "fontawesome4",
disable_edit_json: true,
disable_properties: true
});
let fullSchema = JSON.parse(currentModelMetaData["execution"]["input_schema"]);
if(fullSchema["properties"]["weatherData"] !== undefined)
{
// Display the required weather parameters
let requiredParameters = getRequiredParameters(currentModelMetaData);
let paramHTML = "<h4>Parameters required by the model</h4><ul>";
for(let i=0;i<requiredParameters.length;i++)
{
let param = getWeatherParameter(requiredParameters[i]);
paramHTML += "<li>" + param.name + "</li>";
}
paramHTML += "</ul>"
let parameterInfo = document.getElementById("parameterInfo");
parameterInfo.innerHTML = paramHTML;
parameterInfo.style.display = "block";
// Pull weather data sources from web service, render lists (historic and forecast sources, some are both)
weatherDatasources = await getWeatherDatasources();
//console.info(weatherDatasources);
weatherDatasources.sort((a, b) => {
return (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0;
});
let weatherDatasourceList = document.getElementById("weatherDatasourceList");
weatherDatasourceList.add(new Option("Please select a weather datasource", "-1"));
for(let i=0;i<weatherDatasources.length; i++)
{
if(weatherDatasources[i].temporal.historic != null && weatherDatasources[i].temporal.historic.start != null)
{
weatherDatasourceList.add(new Option(weatherDatasources[i].name, weatherDatasources[i].id));
}
}
let forecastWeatherDatasourceList = document.getElementById("forecastWeatherDatasourceList");
forecastWeatherDatasourceList.add(new Option("Please select a weather forecast datasource", "-1"));
for(let i=0;i<weatherDatasources.length; i++)
{
if(weatherDatasources[i].temporal.forecast != null && weatherDatasources[i].temporal.forecast > 0)
{
forecastWeatherDatasourceList.add(new Option(weatherDatasources[i].name, weatherDatasources[i].id));
}
}
document.getElementById("weatherDataForm").style.display = "block";
}
// TODO: Remove this auto-mock!!
/*editor.getValue().optionalData.startDate="2023-08-01";
editor.getValue().optionalData.endDate="2023-08-19";
document.getElementById("longitude").value="11.781989";
document.getElementById("latitude").value="59.680468";*/
//submitData();
}
initPage();
/**
*
* When the user selects a weather datasource:
* - Displays the description from the weather datasources's metadata
* - Displays a map showing the stations or area OR nothing - depending on the metadata's "spatial" property
*
*
* @param {HTMLElement} weatherDatasourcelist - the select list
*/
function handleWeatherDatasourceSelected(weatherDatasourceList){
currentWeatherDatasource = getWeatherDatasource(
weatherDatasources,
weatherDatasourceList.options[weatherDatasourceList.selectedIndex].value
);
let sourceInfoPanel = document.getElementById("historicSourceInfoPanel");
if(currentWeatherDatasource == null)
{
sourceInfoPanel.style.display="none";
document.getElementById("stationFields").style.display = "none";
destroyDataSourceMap("historicDatasourceMap");
return;
}
// Display map
initDataSourceMap(
"historicDatasourceMap",
JSON.parse(currentWeatherDatasource.spatial.geoJSON),
currentWeatherDatasource.spatial.countries,
handleHistoricDatasourceMapClicked
);
let sourceInfo = document.getElementById("historicSourceInfo");
let sourceHTML = currentWeatherDatasource.description;
sourceHTML += "<h4>Provided weather parameters</h4><ul>";
for(let i=0;i<currentWeatherDatasource.parameters.common.length;i++)
{
sourceHTML += "<li>" + getWeatherParameter(currentWeatherDatasource.parameters.common[i]).name + "</li>";
}
sourceHTML += "</ul>";
if(!isEmpty(currentWeatherDatasource.parameters.optional) && currentWeatherDatasource.parameters.optional.length > 0)
{
sourceHTML += "<h5>Optional parameters (some weather stations)</h5><ul>";
for(let i=0;i<currentWeatherDatasource.parameters.optional.length;i++)
{
sourceHTML += "<li>" + getWeatherParameter(currentWeatherDatasource.parameters.optional[i]).name + "</li>";
}
sourceHTML += "</ul>";
}
sourceInfo.innerHTML = sourceHTML;
sourceInfoPanel.style.display="block";
if(currentWeatherDatasource.access_type == "stations")
{
// Display the weather stations
renderWeatherStationSelectList(getWeatherStationList(currentWeatherDatasource), selectList);
document.getElementById("stationFields").style.display = "block";
}
else
{
// Location based API
// Display lat/lon input fields
document.getElementById("stationFields").style.display = "none";
}
// Check that this datasource can deliver the model's requested parameters
if(!arePragmaticWeatherParametersInList(getRequiredParameters(currentModelMetaData), currentWeatherDatasource.parameters.common))
{
alert("WARNING: The selected datasource can't deliver all weather parameters required by the DSS");
}
}
/**
* This is a callback method passed on to impdlib.initDataSourceMap. When the user clicks on
* - For station based datasources: a weather station
* -> the correct station is selected in the station list
* -> The station's location is set in the latitude/longitude form fields
*
* - For gridded datasources: Anywhere inside the grid area
* -> The location selected by the user is set in the latitude/longitude form fields
*
* @param {String} id the weatherstation id NULL => gridded weather datasource
* @param {Array} [lon,lat] (decimal degrees) of the clicked location. NULL => station based weather datasource
*/
function handleHistoricDatasourceMapClicked(id, coordinate)
{
// id != null => station. Pull station coordinates from source list to avoid inacurracies in user's map click coordinate
if(id !== null)
{
// Set the selected station
setSelection(selectList, id);
handleWeatherStationSelected(selectList);
}
// id == null => area. Use the provided coordinate
else
{
setLatLon(coordinate);
}
}
/**
* Shows datasource info and displays map
* Alerts if the selected forecast data source does not cover the selected location
*/
async function handleForecastSourceSelected(forecastSourceSelectList)
{
currentForecastWeatherDatasource = await getWeatherDatasource(
weatherDatasources,
forecastWeatherDatasourceList.options[forecastWeatherDatasourceList.selectedIndex].value
);
let sourceInfoPanel = document.getElementById("forecastSourceInfoPanel");
// No datasource selected? Hide info panel and map and return
if(currentForecastWeatherDatasource == null)
{
sourceInfoPanel.style.display="none";
destroyDataSourceMap("forecastDatasourceMap");
return;
}
let geoJson = JSON.parse(currentForecastWeatherDatasource.spatial.geoJSON);
// Display map
initDataSourceMap(
"forecastDatasourceMap",
geoJson,
currentForecastWeatherDatasource.spatial.countries,
function(){} // We don't do nothing, right?
);
let sourceInfo = document.getElementById("forecastSourceInfo");
let sourceHTML = currentForecastWeatherDatasource.description;
sourceHTML += "<h4>Provided weather parameters</h4><ul>";
for(let i=0;i<currentForecastWeatherDatasource.parameters.common.length;i++)
{
sourceHTML += "<li>" + getWeatherParameter(currentForecastWeatherDatasource.parameters.common[i]).name + "</li>";
}
sourceHTML += "</ul>";
sourceInfo.innerHTML = sourceHTML;
sourceInfoPanel.style.display="block";
// Does the forecast data source cover the point in question?
if(! await isPointCoveredByDatasource(getLatLon(), currentForecastWeatherDatasource))
{
alert("The selected forecast datasource does not cover the selected location.");
}
// Check that this datasource can deliver the model's requested parameters
if(!arePragmaticWeatherParametersInList(getRequiredParameters(currentModelMetaData), currentForecastWeatherDatasource.parameters.common))
{
alert("WARNING: The selected forecast datasource can't deliver all weather parameters required by the DSS");
}
}
/**
* Pulls the coordinates of the selected weather station from the metadata,
* enters it in the form fields
*/
function handleWeatherStationSelected(weatherStationSelectList)
{
let weatherStationId = selectList.options[selectList.selectedIndex].value;
let stationCoordinate = getWeatherStationCoordinate(currentWeatherDatasource, weatherStationId);
setLatLon(stationCoordinate);
}
/**
* Populate the form fields with the given coordinate
*/
function setLatLon(coordinate)
{
document.getElementById("longitude").value = coordinate[0];
document.getElementById("latitude").value = coordinate[1];
}
/**
* @returns {Array} [longitude,latitude] from the form fields
*/
function getLatLon()
{
return [document.getElementById("longitude").value, document.getElementById("latitude").value];
}
/**
* Collects the input data into Json that conforms to the DSS model's Json schema
* Also collects weather data (which goes into the input data Json)
*/
async function submitData(){
let inputData = editor.getValue();
// Add hidden parameters
let fullSchema = JSON.parse(currentModelMetaData["execution"]["input_schema"]);
const hiddenParameters = currentModelMetaData["execution"]["input_schema_categories"]["hidden"];
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)
{
let forecastData = undefined;
// 1. Historic weather data
if(currentWeatherDatasource != undefined)
{
if(currentWeatherDatasource.access_type == "stations")
{
let weatherStationId = selectList.options[selectList.selectedIndex].value;
weatherData = await getStationWeatherData(
getWeatherDatasourceEndpoint(currentWeatherDatasource),
weatherStationId,
getPragmaticWeatherParameterList(
function (){
let parameterList = []
currentModelMetaData.input.weather_parameters.forEach(function(weatherParameter){
parameterList.push(weatherParameter.parameter_code)
})
return parameterList;
}(),
currentWeatherDatasource.parameters.common
),
3600,
inputData.optionalData.startDate,
inputData.optionalData.endDate,
);
}
else
{
coordinate = getLatLon();
// Need to check that the selected datasource covers the requested location
if(! await isPointCoveredByDatasource(coordinate, currentWeatherDatasource))
{
alert("ERROR: The selected historic datasource does not cover your selected location. Please select an appropriate location or datasource.");
return;
}
weatherData = await getLocationWeatherData(
getWeatherDatasourceEndpoint(currentWeatherDatasource),
coordinate[0],
coordinate[1],
getPragmaticWeatherParameterList(
function (){
let parameterList = []
currentModelMetaData.input.weather_parameters.forEach(function(weatherParameter){
parameterList.push(weatherParameter.parameter_code)
})
return parameterList;
}(),
currentWeatherDatasource.parameters.common
),
3600,
inputData.optionalData.startDate,
inputData.optionalData.endDate,
);
}
}
// 2. Forecast weather data
if(currentForecastWeatherDatasource != undefined)
{
// Need to check that the selected datasource covers the requested location
if(! await isPointCoveredByDatasource(getLatLon(), currentForecastWeatherDatasource))
{
alert("ERROR: The selected forecast datasource does not cover your selected location. Please select an appropriate location or datasource.");
return;
}
forecastData = await getLocationWeatherData(
getWeatherDatasourceEndpoint(currentForecastWeatherDatasource),
document.getElementById("longitude").value,
document.getElementById("latitude").value,
getPragmaticWeatherParameterList(
function (){
let parameterList = []
currentModelMetaData.input.weather_parameters.forEach(function(weatherParameter){
parameterList.push(weatherParameter.parameter_code)
})
return parameterList;
}(),
currentForecastWeatherDatasource.parameters.common
),
3600,
inputData.optionalData.startDate,
inputData.optionalData.endDate,
);
// Merge if both historic and forecast data have been collected
if(weatherData == undefined)
{
weatherData = forecastData;
}
else
{
// Is useless until method is implemented
weatherData = mergeWeatherData(weatherData, forecastData);
}
}
if(weatherData != null)
{
inputData["weatherData"] = weatherData;
}
else
{
alert("ERROR: Could not get weather data. Please check if your datasource covers your location.");
return;
}
}
// Ready to call server?
//let result = mockResult;
let result = await runModel(currentModelMetaData.execution.endpoint, inputData)
displayResult(result);
};
/**
* Render results in a chart
*/
function displayResult(result) {
let chartData = [];
// Generate dates
let dates = getDateArray(result.timeStart, 86400, result.locationResult[0].length);
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('resultChart');
new Chart(ctx, {
type: "line",
options: {
responsive: true,
scales: {
x: {
type: "time",
time: {
//unit: "day",
diplayFormats: {
day: "YYYY-MM-DD" // This doesn't seem to have any effect. Why??
}
}
}
}
},
data: {
datasets: chartData
}
})
renderWeatherData(weatherData, document.getElementById("weatherData"), "table table-striped")
}
// Mock result!!! Waiting for ADAS to fix CORS issue
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
from django.test import TestCase
# Create your tests here.
#
# 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
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment