diff --git a/VIPSWeb/settings.py b/VIPSWeb/settings.py
index 56e3e4565407ea1fb8574166ad3c040d4cbe3641..65ef58f06bbf9af829c27d57d530dd276f014a84 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 4aa88ffb63ab9e54675948eaa5ec5515beafc93b..268850ec116eeaca4631b0a52ef472b93e39a552 100755
--- a/VIPSWeb/urls.py
+++ b/VIPSWeb/urls.py
@@ -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")),
diff --git a/docs/new_model_config.md b/docs/new_model_config.md
index 5ca0010f0de2097de6fd61c789f8e5c68e108190..89fbcc4897ab93a11223886cdaeccf8ad6eefac9 100644
--- a/docs/new_model_config.md
+++ b/docs/new_model_config.md
@@ -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)
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..a27adba0d5d8412d21548739972c3dd8012f0d81
--- /dev/null
+++ b/ipmd/static/ipmd/js/ipmdlib.js
@@ -0,0 +1,1121 @@
+/* 
+ * 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
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..ae1b580c43e7f09475b20625cb51e71823924e98
--- /dev/null
+++ b/ipmd/templates/ipmd/saddlegallmidgeform.html
@@ -0,0 +1,891 @@
+{% 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
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