From 1cfaf27e45ed610643260a27849129757e78ba4f Mon Sep 17 00:00:00 2001
From: Bhabesh <bhabesh.mukhopadhyay@nibio.no>
Date: Mon, 19 Oct 2020 13:45:55 +0200
Subject: [PATCH] Basic phenology model (General)

Added required changes to related to phenology
---
 .../PhenologyModelPreprocessor.java           | 348 ++++++++++++++++++
 .../no/nibio/vips/logic/util/Plantation.java  |  92 +++++
 .../resources/dataset/plantationData.json     |  49 +++
 .../vips/logic/i18n/vipslogictexts.properties |   3 +
 .../formdefinitions/models/PHENOLOGYM.json    |  40 ++
 5 files changed, 532 insertions(+)
 create mode 100644 src/main/java/no/nibio/vips/logic/scheduling/model/preprocessor/PhenologyModelPreprocessor.java
 create mode 100644 src/main/java/no/nibio/vips/logic/util/Plantation.java
 create mode 100644 src/main/resources/dataset/plantationData.json
 create mode 100644 src/main/webapp/formdefinitions/models/PHENOLOGYM.json

diff --git a/src/main/java/no/nibio/vips/logic/scheduling/model/preprocessor/PhenologyModelPreprocessor.java b/src/main/java/no/nibio/vips/logic/scheduling/model/preprocessor/PhenologyModelPreprocessor.java
new file mode 100644
index 00000000..78f47281
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/scheduling/model/preprocessor/PhenologyModelPreprocessor.java
@@ -0,0 +1,348 @@
+ 
+package no.nibio.vips.logic.scheduling.model.preprocessor;
+
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.MappingJsonFactory;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import no.nibio.vips.entity.ModelConfiguration;
+import no.nibio.vips.entity.WeatherObservation;
+import no.nibio.vips.logic.entity.ForecastConfiguration;
+import no.nibio.vips.logic.entity.PointOfInterestWeatherStation;
+import no.nibio.vips.logic.scheduling.model.ModelRunPreprocessor;
+import no.nibio.vips.logic.scheduling.model.PreprocessorException;
+import no.nibio.vips.logic.util.Plantation;
+import no.nibio.vips.logic.util.SessionControllerGetter;
+import no.nibio.vips.logic.util.SystemTime;
+import no.nibio.vips.model.ConfigValidationException;
+import no.nibio.vips.util.WeatherElements;
+import no.nibio.vips.util.WeatherObservationListException;
+import no.nibio.vips.util.WeatherUtil;
+import no.nibio.vips.util.weather.WeatherDataSourceException;
+import no.nibio.vips.util.weather.WeatherDataSourceUtil;
+
+/**
+ *
+ * @author wildfly
+ */
+
+
+public class PhenologyModelPreprocessor extends ModelRunPreprocessor{
+     public final           Logger      LOGGER              =   Logger.getLogger(this.getClass().getName());
+    private final           boolean     DEBUG               =   false;
+    private final   static  String      MODEL_ID            =   "PHENOLOGYM";
+    private final   static  String      MAIZE_VARITY        =   "maizeVarity";
+    private final   static  String      SOWING_DATE         =   "sowingDate";
+    
+    private final   static  String      SOWING_DATA         =   "sowingData";
+    private final   static  String      PLANTATION_DATA     =   "plantationData";
+    private final   static  String      FILE_PLANTATION     =   "/dataset/plantationData.json";
+    
+    @Override
+    public ModelConfiguration getModelConfiguration(ForecastConfiguration configuration) throws PreprocessorException {
+        String      PHENOLOGY_MAZE_VARIETY                  = SessionControllerGetter.getForecastBean().getDeCamelizedFieldName(MODEL_ID, MAIZE_VARITY);
+        String      PHENOLOGY_SOWING_DATE                   = SessionControllerGetter.getForecastBean().getDeCamelizedFieldName(MODEL_ID, SOWING_DATE);
+        
+        String      paramMazeVariety                        = null;          
+        Date        paramSowingDate                         = null;
+        
+        
+        String                         paramConfigValue     =   null;
+        
+        ModelConfiguration              retVal              =   new ModelConfiguration();
+        PointOfInterestWeatherStation   weatherStation      =   (PointOfInterestWeatherStation)  configuration.getWeatherStationPointOfInterestId();
+        WeatherDataSourceUtil           wdsUtil             =   new WeatherDataSourceUtil();
+        WeatherUtil                     wUtil               =   new WeatherUtil();
+        TimeZone                        timeZone            =   TimeZone.getTimeZone(weatherStation.getTimeZone());
+        SimpleDateFormat                format              =   new SimpleDateFormat("yyyy-MM-dd");
+        format.setTimeZone(timeZone);
+        // Getting date three days after "today"
+        Calendar cal = Calendar.getInstance(timeZone);
+        cal.setTime(SystemTime.getSystemTime());
+        cal.add(Calendar.DATE, 3);
+        Date endDate = wUtil.normalizeToExactDate(configuration.getDateEnd(), timeZone);
+        cal.set(Calendar.MONTH, Calendar.JANUARY);
+        cal.set(Calendar.DATE, 1);
+        
+        Plantation  sowdata  = new Plantation();
+        
+        try
+        {
+        paramConfigValue    = configuration.getForecastModelConfigurationValue(PHENOLOGY_SOWING_DATE);
+        paramSowingDate     = format.parse(paramConfigValue);
+        }
+        catch(ParseException ex)
+        {
+            throw new PreprocessorException("Could not parse date for Sowing Date for "+MODEL_ID+". Given string was: "+ paramConfigValue);
+        }
+        
+        Date                            startDate       =   wUtil.normalizeToExactDate(
+                                                                        paramSowingDate
+                                                                    ,   timeZone
+                                                            );
+        
+        List<WeatherObservation>        observations    =   null;
+        
+        try {
+            observations    = wdsUtil.getWeatherObservations(
+                                                              weatherStation
+                                                            , WeatherObservation.LOG_INTERVAL_ID_1H
+                                                            , new String[]{
+                                                                            WeatherElements.TEMPERATURE_MEAN,
+                                                                            WeatherElements.PRECIPITATION,
+                                                                            WeatherElements.LEAF_WETNESS_DURATION,
+                                                                            WeatherElements.RELATIVE_HUMIDITY_MEAN
+                                                                        }
+                                                            , startDate
+                                                            , endDate);
+        }
+        catch(WeatherDataSourceException ex)
+        {
+            throw new PreprocessorException(ex.getMessage());
+        }
+        
+
+        
+        
+        paramMazeVariety    = configuration.getForecastModelConfigurationValue(PHENOLOGY_MAZE_VARIETY);
+    
+ 
+        Collections.sort(observations);
+
+        try {
+            List<WeatherObservation>  calcObs  = wdsUtil.getWeatherObservations(
+                                                                                    weatherStation
+                                                                                ,   WeatherObservation.LOG_INTERVAL_ID_1H
+                                                                                ,   new String[] {
+                                                                                                WeatherElements.GLOBAL_RADIATION,
+                                                                                                WeatherElements.WIND_SPEED_2M
+                                                                                                }
+                                                                                , startDate
+                                                                                , endDate
+                                                                                );
+            observations.addAll(calcObs);
+        }catch (WeatherDataSourceException ex)
+        {
+            // TODO nothing reported
+        }
+        
+       if(DEBUG)
+        {
+            LOGGER.log(Level.CONFIG, "Finished getting weather data at  "+new Date().toString());
+        }
+        try {
+            observations = validateAndSanitizeObservations(observations, startDate);
+        } catch (ConfigValidationException | WeatherObservationListException ex) {
+            //ex.printStackTrace();
+            throw new PreprocessorException(ex.getMessage());
+        } 
+        if(DEBUG)
+        {        
+            LOGGER.log(Level.CONFIG, "Observations=" + observations.toString());
+        }
+    
+        sowdata.setVarietyId(paramMazeVariety);
+        sowdata.setStartDate(paramSowingDate);
+        
+        retVal.setModelId(this.getModelId());
+        retVal.setConfigParameter("timeZone", timeZone.getID());
+        retVal.setConfigParameter("observations", observations);
+        retVal.setConfigParameter(SOWING_DATA, sowdata);
+        retVal.setConfigParameter(PLANTATION_DATA, getConfigurationPlantation(FILE_PLANTATION));
+
+        
+        return retVal;
+    }
+
+    
+    
+    
+    @Override
+    public String getModelId() {
+        return MODEL_ID;
+    }
+    
+    
+       private List<WeatherObservation> validateAndSanitizeObservations(List<WeatherObservation> observations, Date firstTimeStamp) throws ConfigValidationException, WeatherObservationListException 
+       {
+       
+         if(DEBUG)
+        {
+            LOGGER.log(Level.CONFIG, "validateAndSanitizeObservations");
+        }
+        
+        WeatherUtil wUtil = new WeatherUtil();
+        
+        // First we remove all duplicates
+        observations = wUtil.removeDuplicateWeatherObservations(observations, null);
+        
+        // Fix weather data
+        List<WeatherObservation> fixedObservations = wUtil.fixHourlyValuesForParameters(
+                observations, 
+                new HashSet(Arrays.asList("TM","RR")), 
+                firstTimeStamp, 
+                null
+        );
+        
+        
+        // Now we need to validate and possibly try to fix the weather data
+        List<WeatherObservation> TM = new ArrayList<>();
+        List<WeatherObservation> RR = new ArrayList<>();
+        
+        for(WeatherObservation o:fixedObservations)
+        {
+            switch(o.getElementMeasurementTypeId())
+            {
+                case WeatherElements.TEMPERATURE_MEAN:
+                    TM.add(o); 
+                    break;
+                case WeatherElements.PRECIPITATION:
+                    RR.add(o); 
+                    break;
+                    
+            }
+        }
+        
+        List<WeatherObservation> BT = new ArrayList<>();
+        List<WeatherObservation> UM = new ArrayList<>();
+        List<WeatherObservation> Q0 = new ArrayList<>();
+        List<WeatherObservation> FM2 = new ArrayList<>();
+        for(WeatherObservation o:observations)
+        {
+            switch(o.getElementMeasurementTypeId())
+            {
+                case WeatherElements.LEAF_WETNESS_DURATION:
+                    BT.add(o); 
+                    break;
+                case WeatherElements.RELATIVE_HUMIDITY_MEAN:
+                    UM.add(o); 
+                    break;
+                case WeatherElements.GLOBAL_RADIATION:
+                    Q0.add(o);
+                    break;
+                case WeatherElements.WIND_SPEED_2M:
+                    FM2.add(o);
+                    break;
+                default:
+                    // Let it pass in silence 
+                    break;
+            }
+        }
+        
+        // Problems with weather observations
+        
+        // Holes in series
+        if(DEBUG)
+        {
+            LOGGER.log(Level.CONFIG, "checkForAndFixHourlyTimeSeriesHoles");
+        }
+
+        
+        
+        // Unequal length of lists
+        if  (        
+                RR.size() != TM.size()
+                || BT.size() != TM.size()
+                || RR.size() != TM.size()
+            )
+        {
+            UM = wUtil.fixHourlyValuesForParameters(
+                    UM, 
+                    new HashSet(Arrays.asList("UM")), 
+                    firstTimeStamp, 
+                    null
+            );
+            
+            
+            
+            // Fallback if missing leaf wetness: If we have relative humidity. leaf wetness may be calculated
+            if((BT.size() != TM.size()) && (UM.size() == TM.size()))
+            {
+                BT = wUtil.calculateLeafWetnessHourSeriesBestEffort(BT,TM, RR, Q0, FM2, UM);
+                
+                if(BT.size() != TM.size())
+                {
+                   throw new ConfigValidationException("Missing leaf wetness data. Also, attempting to calculate leaf wetness from other weather parameters failed.");
+                }
+            }
+            else
+            {
+                LOGGER.log(Level.WARNING, "TM starts " + TM.get(0).getTimeMeasured() + ", ends " + TM.get(TM.size()-1).getTimeMeasured());
+                throw new ConfigValidationException("Incorrect number of weather data. TM.size() = " + TM.size() + ", BT.size()=" + BT.size() +  ", RR.size()=" + RR.size() + ", UM.size()=" + UM.size() );
+            }
+        }
+        List<WeatherObservation> retVal = new ArrayList<>();
+        retVal.addAll(TM);
+        retVal.addAll(RR);
+        retVal.addAll(BT);
+        return retVal;
+    }
+    
+    private List<Plantation> getConfigurationPlantation(String fileName)
+    {
+        List<Plantation> plantations    = new ArrayList<Plantation>();
+        
+        
+        try {
+            BufferedInputStream inputStream = new BufferedInputStream(this.getClass().getResourceAsStream(fileName));
+            JsonFactory f = new MappingJsonFactory();
+            JsonParser jp = f.createParser(inputStream);
+            JsonNode all = jp.readValueAsTree();
+            JsonNode nodePhaseInfo = all.path("phaseInfo");
+            if(nodePhaseInfo.isArray())
+            {
+                for(JsonNode node : nodePhaseInfo)
+                {
+                     Plantation   plantation  =   new Plantation
+                                                (           all.get("typeName").asText()
+                                                        ,   node.get("phaseName").asText()
+                                                       ,    node.get("heatReq").asDouble()
+                                                );
+                     plantations.add(plantation);
+                }
+            }
+            
+            
+            /*
+            if(all.isArray())
+            {
+               for(JsonNode node : all)
+               {
+                   Plantation   plantation  =   new Plantation
+                                                (
+                                                            node.get("maizeVarity").asText()
+                                                       ,    node.get("phaseName").asText()
+                                                       ,    node.get("baseTemp").asText()
+                                                       ,    node.get("heatReq").asDouble()
+                                                );
+                  plantations.add(plantation);
+                  
+                } 
+            }
+            
+            */
+            
+            
+        } catch (IOException ex) {
+            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);
+        }
+        
+        return plantations;
+    }
+       
+}
diff --git a/src/main/java/no/nibio/vips/logic/util/Plantation.java b/src/main/java/no/nibio/vips/logic/util/Plantation.java
new file mode 100644
index 00000000..b50afe70
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/util/Plantation.java
@@ -0,0 +1,92 @@
+
+package no.nibio.vips.logic.util;
+
+import java.util.Date;
+
+/**
+ *
+ * @author wildfly
+ */
+
+
+public class Plantation {
+     private String  varietyId;
+    private String  phaseName;
+    private String  baseTemp;
+    private Double  heatReq;
+    private Date    startDate;
+
+    public Plantation() {
+    }
+
+    
+    
+    public Plantation(String varietyId, String phaseName, String baseTemp, Double heatReq) {
+        this.varietyId = varietyId;
+        this.phaseName = phaseName;
+        this.baseTemp = baseTemp;
+        this.heatReq = heatReq;
+    }
+
+    public Plantation(String varietyId, String phaseName, Double heatReq) {
+        this.varietyId = varietyId;
+        this.phaseName = phaseName;
+        this.heatReq = heatReq;
+        } 
+    
+      public Plantation(String phaseName, Double heatReq) {
+        this.phaseName = phaseName;
+        this.heatReq = heatReq;
+        }  
+    
+    public String getVarietyId() {
+        return varietyId;
+    }
+
+    public void setVarietyId(String varietyId) {
+        this.varietyId = varietyId;
+    }
+
+    public String getPhaseName() {
+        return phaseName;
+    }
+
+    public void setPhaseName(String phaseName) {
+        this.phaseName = phaseName;
+    }
+
+    public String getBaseTemp() {
+        return baseTemp;
+    }
+
+    public void setBaseTemp(String baseTemp) {
+        this.baseTemp = baseTemp;
+    }
+
+    public Double getHeatReq() {
+        return heatReq;
+    }
+
+    public void setHeatReq(Double heatReq) {
+        this.heatReq = heatReq;
+    }
+
+    public Date getStartDate() {
+        return startDate;
+    }
+
+    public void setStartDate(Date startDate) {
+        this.startDate = startDate;
+    }
+
+    @Override
+    public String toString() {
+        return "Plantation{" + "varietyId=" + varietyId + ", phaseName=" + phaseName + ", baseTemp=" + baseTemp + ", heatSum=" + heatReq + ", sowingDate=" + startDate + '}';
+    }
+
+    
+
+    
+    
+
+}
diff --git a/src/main/resources/dataset/plantationData.json b/src/main/resources/dataset/plantationData.json
new file mode 100644
index 00000000..5e041fff
--- /dev/null
+++ b/src/main/resources/dataset/plantationData.json
@@ -0,0 +1,49 @@
+{
+    "speciesLatinName"        : "Zea mays",
+    "typeName"                : "TESTVARIETY 2",
+    "baseTemp"                : "10",
+    "phaseInfo":
+[
+    {
+        "phaseName": "VE",
+        "heatReq"  : "66.67"
+    },
+    {
+        "phaseName": "V2",
+         "heatReq"  : "111.11"
+    },
+     {
+        "phaseName": "V3",
+        "heatReq"  : "194.44"
+    },
+     {
+        "phaseName": "V4-V6",
+        "heatReq"  : "263.89"
+    }, 
+    {
+        "phaseName": "V7-V9",
+        "heatReq"  : "338.89"
+    }, 
+    {
+        "phaseName": "V10",
+        "heatReq"  : "411.11"
+    }, 
+    {
+        "phaseName": "VT",
+        "heatReq"  : "630.55"
+    }, 
+    {
+        "phaseName": "R2",
+        "heatReq"  : "922.22"
+    }, 
+    {
+        "phaseName": "R5",
+        "heatReq"  : "1361.11"
+    }, 
+    {
+        "phaseName": "R6",
+        "heatReq"  : "1500"
+    }
+ 
+]
+}
\ No newline at end of file
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
index 602449bc..d9e661de 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
@@ -494,3 +494,6 @@ sprayingDate01=Spraying Date 01
 sprayingDate02=Spraying Date 02
 sprayingDate03=Spraying Date 03
 sprayingDate04=Spraying Date 04
+PHENOLOGYM=Phenology Model
+maizeVarity=Maize Variety
+sowingDate=Sowing Date
diff --git a/src/main/webapp/formdefinitions/models/PHENOLOGYM.json b/src/main/webapp/formdefinitions/models/PHENOLOGYM.json
new file mode 100644
index 00000000..23ffe221
--- /dev/null
+++ b/src/main/webapp/formdefinitions/models/PHENOLOGYM.json
@@ -0,0 +1,40 @@
+{
+    "_licenseNote": [
+        "Copyright (c) 2014 NIBIO <http://www.nibio.no/>. ",
+        "",
+        "This file is part of VIPSLogic. ",
+        "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. ",
+        "",
+        "VIPSLogic 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 VIPSLogic.  If not, see <http://www.nibio.no/licenses/>. "
+    ],
+    "_comment" : "Structure of the specific fields for FAWPHENOLO",
+        "fields": [
+            {
+                "name" : "maizeVarity",
+                "dataType" : "STRING",
+                "fieldType" : "SELECT_SINGLE",
+                "required" : false,
+                "required" : true,
+                            "selectOptions": [
+                                    {"value":"SOTUBAKA", "label":{"en":"SOTUBAKA Variety","nb":"SOTUBAKA Variety"}, "selected":"true"},
+                                    {"value":"TESTVARIETY 2", "label":{"en":"TESTVARIETY 2","nb":"TESTVARIETY 2"}},
+                                    {"value":"NIELENI", "label":{"en" : "NIELENI Variety", "nb":"NIELENI Variety"}}
+                            ]
+            },
+            {
+                "name" : "sowingDate",
+                "dataType" : "DATE",
+                "dateFormat" : "yyyy-MM-dd",
+                "required" : false
+            }
+       ]
+}
-- 
GitLab