From c38735b76aba4e2e160626684aea238fbd5ab2a2 Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Mon, 9 Oct 2017 09:41:32 +0200
Subject: [PATCH] First version of the parser service for the Norwegian
 Meteorological service's Thredds server

---
 nbactions.xml                                 |   1 +
 pom.xml                                       |  10 +
 .../logic/service/WeatherProxyService.java    |  85 ++++
 .../metnothredds/MetNoThreddsDataParser.java  | 455 ++++++++++++++++++
 .../metnothredds/parametersources.properties  |  22 +
 .../MetNoThreddsDataParserTest.java           | 166 +++++++
 6 files changed, 739 insertions(+)
 create mode 100644 src/main/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParser.java
 create mode 100644 src/main/resources/no/nibio/vips/util/weather/metnothredds/parametersources.properties
 create mode 100644 src/test/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParserTest.java

diff --git a/nbactions.xml b/nbactions.xml
index cd76a198..b86e6dae 100755
--- a/nbactions.xml
+++ b/nbactions.xml
@@ -44,6 +44,7 @@
                 <no.nibio.vips.logic.weather.FIELDCLIMATE_API_PASSWORD>q22bspFVPwkaohImV21m</no.nibio.vips.logic.weather.FIELDCLIMATE_API_PASSWORD>
                 <no.nibio.vips.logic.weather.FIELDCLIMATE_API_CLIENT_ID>MetosDemo</no.nibio.vips.logic.weather.FIELDCLIMATE_API_CLIENT_ID>
                 <no.nibio.vips.logic.weather.FIELDCLIMATE_API_CLIENT_SECRET>aa8f4b62b72986bac7c84be78836c2c6</no.nibio.vips.logic.weather.FIELDCLIMATE_API_CLIENT_SECRET>
+                <no.nibio.vips.logic.weather.METNOTHREDDS_TMP_FILE_PATH>/home/treinar/prosjekter/vips/projects/2017_SpotIT/Task 3.2/</no.nibio.vips.logic.weather.METNOTHREDDS_TMP_FILE_PATH>
                 
                 
                 
diff --git a/pom.xml b/pom.xml
index 7ee404c2..696f8bee 100755
--- a/pom.xml
+++ b/pom.xml
@@ -19,8 +19,18 @@
         <id>jitpack.io</id>
         <url>https://jitpack.io</url>
     </repository>
+    <repository>
+        <id>unidata-releases</id>
+        <name>Unidata Releases</name>
+        <url>https://artifacts.unidata.ucar.edu/content/repositories/unidata-releases/</url>
+    </repository>
 </repositories>
   <dependencies>
+      <dependency>
+    <groupId>edu.ucar</groupId>
+    <artifactId>cdm</artifactId>
+    <version>4.6.10</version>
+  </dependency>
       <dependency>
         <groupId>com.github.bjornharrtell</groupId>
           <!--groupId>org.wololo</groupId-->
diff --git a/src/main/java/no/nibio/vips/logic/service/WeatherProxyService.java b/src/main/java/no/nibio/vips/logic/service/WeatherProxyService.java
index 6803e60c..7c9fdcb1 100755
--- a/src/main/java/no/nibio/vips/logic/service/WeatherProxyService.java
+++ b/src/main/java/no/nibio/vips/logic/service/WeatherProxyService.java
@@ -19,11 +19,15 @@
 
 package no.nibio.vips.logic.service;
 
+import com.vividsolutions.jts.geom.Envelope;
+import com.vividsolutions.jts.geom.GeometryFactory;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
@@ -35,6 +39,7 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
+import no.nibio.vips.entity.PointWeatherObservationList;
 import no.nibio.vips.entity.WeatherObservation;
 import no.nibio.vips.logic.util.SessionControllerGetter;
 import no.nibio.vips.logic.util.SystemTime;
@@ -46,6 +51,7 @@ import no.nibio.vips.util.weather.ParseWeatherDataException;
 import no.nibio.vips.util.weather.USPestDataParser;
 import no.nibio.vips.util.weather.YrWeatherForecastProvider;
 import no.nibio.vips.util.weather.dnmipointweb.DMIPointWebDataParser;
+import no.nibio.vips.util.weather.metnothredds.MetNoThreddsDataParser;
 import org.jboss.resteasy.annotations.GZIP;
 
 /**
@@ -287,4 +293,83 @@ public class WeatherProxyService {
             return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ex.getMessage()).build();
         }
     }
+    
+    @GET
+    @Path("metno/thredds/point/")
+    @GZIP
+    @Produces("application/json;charset=UTF-8")
+    public Response getMetNoThreddsPointData(
+            @QueryParam("longitude") Double longitude,
+            @QueryParam("latitude") Double latitude,
+            @QueryParam("timeZone") String timeZoneStr,
+            @QueryParam("startDate") String startDateStr,
+            @QueryParam("startTime") String startTimeStr,
+            @QueryParam("endDate") String endDateStr,
+            @QueryParam("endTime") String endTimeStr,
+            @QueryParam("elementMeasurementTypes") String[] elementMeasurementTypes 
+            
+    )
+    {
+        try
+        {
+            TimeZone timeZone = TimeZone.getTimeZone(timeZoneStr);
+            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH");
+            format.setTimeZone(timeZone);
+            startTimeStr = startTimeStr != null ? startTimeStr : "00";
+            endTimeStr = endTimeStr != null ? endTimeStr : "23";
+
+            Date startDate = format.parse(startDateStr + " " + startTimeStr);
+            Date endDate = format.parse(endDateStr + " " + endTimeStr);
+            MetNoThreddsDataParser dp = new MetNoThreddsDataParser();
+            List<WeatherObservation> retVal = dp.getPointData(longitude, latitude, startDate, endDate, Arrays.asList(elementMeasurementTypes));
+            Collections.sort(retVal);
+            return Response.ok().entity(retVal).build();
+        }
+        catch(ParseException pe)
+        {
+            return Response.status(Response.Status.BAD_REQUEST).entity(pe.getMessage()).build();
+        }
+    }
+    
+    @GET
+    @Path("metno/thredds/grid/")
+    @GZIP
+    @Produces("application/json;charset=UTF-8")
+    public Response getMetNoThreddsGridData(
+            @QueryParam("north") Double north,
+            @QueryParam("south") Double south,
+            @QueryParam("east") Double east,
+            @QueryParam("west") Double west,
+            @QueryParam("timeZone") String timeZoneStr,
+            @QueryParam("startDate") String startDateStr,
+            @QueryParam("startTime") String startTimeStr,
+            @QueryParam("endDate") String endDateStr,
+            @QueryParam("endTime") String endTimeStr,
+            @QueryParam("elementMeasurementTypes") String[] elementMeasurementTypes 
+            
+    )
+    {
+        try
+        {
+            TimeZone timeZone = TimeZone.getTimeZone(timeZoneStr);
+            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH");
+            format.setTimeZone(timeZone);
+            startTimeStr = startTimeStr != null ? startTimeStr : "00";
+            endTimeStr = endTimeStr != null ? endTimeStr : "23";
+
+            Date startDate = format.parse(startDateStr + " " + startTimeStr);
+            Date endDate = format.parse(endDateStr + " " + endTimeStr);
+            MetNoThreddsDataParser dp = new MetNoThreddsDataParser();
+            
+            // Creating an envelope with the given bounds
+            Envelope envelope = new Envelope(west, east, south, north);
+            GeometryFactory gf = new GeometryFactory();
+            List<PointWeatherObservationList> retVal = dp.getGridData(gf.toGeometry(envelope), startDate, endDate, Arrays.asList(elementMeasurementTypes));
+            return Response.ok().entity(retVal).build();
+        }
+        catch(ParseException pe)
+        {
+            return Response.status(Response.Status.BAD_REQUEST).entity(pe.getMessage()).build();
+        }
+    }
 }
diff --git a/src/main/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParser.java b/src/main/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParser.java
new file mode 100644
index 00000000..609f06f3
--- /dev/null
+++ b/src/main/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParser.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (c) 2017 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/>.
+ * 
+ */
+
+package no.nibio.vips.util.weather.metnothredds;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import com.vividsolutions.jts.geom.Geometry;
+import com.vividsolutions.jts.geom.Polygon;
+import java.util.TimeZone;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import no.nibio.vips.entity.PointWeatherObservationList;
+import no.nibio.vips.entity.WeatherObservation;
+import no.nibio.vips.gis.SimpleWGS84Coordinate;
+import no.nibio.vips.util.WeatherElements;
+import no.nibio.vips.util.WeatherUtil;
+import org.apache.commons.io.FileUtils;
+import ucar.ma2.Array;
+import ucar.ma2.ArrayDouble;
+import ucar.ma2.ArrayFloat;
+import ucar.nc2.NetcdfFile;
+import ucar.nc2.Variable;
+
+/**
+ * Digging into the Thredds archive of the Norwegian Meteorological Institute,
+ * collecting 
+ * @copyright 2017 <a href="http://www.nibio.no/">NIBIO</a>
+ * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
+ */
+public class MetNoThreddsDataParser {
+    
+    private final String BASE_URL = "http://thredds.met.no/thredds/";
+    private final String NCSS_URL = BASE_URL + "ncss/";
+    private final String MEPS25_PATH = "meps25detarchive/";
+    private final String MEPS25_FILEBASENAME = "meps_mbr0_extracted_2_5km_";
+    private final String NETCDF_EXT = ".nc";
+    private final String STANDARD_PARAMS = "?accept=netcdf&temporal=all&&horizStride=1&disableLLSubset=on&timeStride=1";
+    private Properties paramInfo;
+    private final String TMP_FILE_PATH;
+    private final List<String> accumulatedParams = Arrays.asList("Q0", "RR");
+    private final SimpleDateFormat pathFormat;
+    private final SimpleDateFormat fileTimeStampFormat;
+    
+    
+    private final WeatherUtil weatherUtil;
+    
+    public MetNoThreddsDataParser()
+    {
+        paramInfo = new Properties();
+        try (InputStream in = this.getClass().getResourceAsStream("parametersources.properties")) {
+            paramInfo.load(in);
+        }
+        catch(IOException ex)
+        {
+        }
+        this.TMP_FILE_PATH = System.getProperty("no.nibio.vips.logic.weather.METNOTHREDDS_TMP_FILE_PATH");
+        this.weatherUtil = new WeatherUtil();
+        this.pathFormat = new SimpleDateFormat("yyyy/MM/dd/");
+        this.pathFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+        this.fileTimeStampFormat = new SimpleDateFormat("yyyyMMdd'T'00'Z'");
+        this.fileTimeStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+    
+    public List<PointWeatherObservationList> getGridData(Geometry geometry, Date dateFrom, Date dateTo, List<String> VIPSWeatherParameters)
+    {
+        Set<String> metParamNames = VIPSWeatherParameters.stream().map(
+                param -> paramInfo.getProperty(param)
+        ).collect(Collectors.toSet());
+        
+        
+        List<Date> archiveDates = getArchiveDates(dateFrom, dateTo);
+        Collections.sort(archiveDates);
+        //new GeometryFactory().
+        Polygon gridBounds =  (Polygon) geometry.getEnvelope();
+        System.out.println("geometryType=" + gridBounds.getGeometryType());
+        System.out.println("gridBounds=" + gridBounds);
+         
+        Map<Coordinate, Map<Long, WeatherObservation>> tempRetVal = new HashMap<>();
+        archiveDates.stream().forEach(archiveDate -> {
+            File tmp = null;
+            NetcdfFile ncFile = null;
+            try
+            {
+                URL threddsURL = new URL(NCSS_URL + MEPS25_PATH + 
+                        this.pathFormat.format(archiveDate) + 
+                        MEPS25_FILEBASENAME + 
+                        this.fileTimeStampFormat.format(archiveDate) +
+                        NETCDF_EXT + STANDARD_PARAMS +
+                        "&north=" + this.getNorth(gridBounds) + "&west=" + this.getWest(gridBounds) + "&east=" + this.getEast(gridBounds) + "&south=" + this.getSouth(gridBounds) +
+                        "&var=" + String.join(",", metParamNames) +
+                        "&addLatLon=true");
+                System.out.println("URL: " + threddsURL.toString());
+                Long timestamp = new Date().getTime();
+                tmp = new File(this.TMP_FILE_PATH + timestamp + ".nc");
+                FileUtils.copyURLToFile(threddsURL, tmp);
+                ncFile = NetcdfFile.open(tmp.getAbsolutePath());
+                
+                Variable time = ncFile.findVariable("time");
+                Array times = time.read();
+                Date[] timeMeasured = new Date[times.getShape()[0]];
+                for(int i=0; i<timeMeasured.length;i++)
+                {
+                    timeMeasured[i] = new Date(times.getLong(i) * 1000);
+                }
+                // The points are in a matrix. Get the dims first
+                ArrayDouble.D2 longitude = (ArrayDouble.D2) ncFile.findVariable("lon").read();
+                ArrayDouble.D2 latitude = (ArrayDouble.D2) ncFile.findVariable("lat").read();
+                int[] latLonShape = longitude.getShape(); // Assuming they're identical!
+                Coordinate[][] coords = new Coordinate[latLonShape[0]][latLonShape[1]];
+                for(int y=0;y<latLonShape[0];y++)
+                {
+                    for(int x=0;x<latLonShape[1];x++)
+                    {
+                        double lat = latitude.get(y, x);
+                        double lon = longitude.get(y,x);
+                        Coordinate c = new Coordinate(lon, lat);
+                        coords[y][x] = c;
+                        //System.out.println("lat/lon=" + lat + "/" + lon);
+                    }
+                }
+                for(String metParamName:metParamNames)
+                {
+                    String VIPSParamName = this.findVIPSParamNameFromMetParamName(metParamName);
+
+                    Variable var = ncFile.findVariable(metParamName);
+                    // All variables have the dims [time, height, y, x]. All 
+                    // variables represent only one height, and can therefore be reduced 
+                    // from 4 to three dimensions
+                    // Assuming all weather parameters are of type "float"
+
+                    ArrayFloat.D3 varArray = (ArrayFloat.D3) var.read().reduce();
+                    // We always skip the two first timestamps, since the model needs 
+                    // a couple of hours to spin up
+                    for(int i=2;i<timeMeasured.length;i++)
+                    {
+                        if(timeMeasured[i].compareTo(dateFrom) < 0 || timeMeasured[i].after(dateTo))
+                        {
+                            continue;
+                        }
+                        for(int y=0;y<latLonShape[0];y++)
+                        {
+                            for(int x=0;x<latLonShape[1];x++)
+                            {
+                                Map<Long, WeatherObservation> obsMapForPoint = tempRetVal.get(coords[y][x]);
+                                if(obsMapForPoint == null)
+                                {
+                                    obsMapForPoint = new HashMap<>();
+                                    tempRetVal.put(coords[y][x], obsMapForPoint);
+                                }
+                                Double value = new Double(varArray.get(i, y, x));
+                                // For accumulated parameters, substract previous value in array
+                                if(i>0 && this.accumulatedParams.contains(VIPSParamName))
+                                {
+                                    value = value - new Double(varArray.get(i-1, y, x));
+                                }
+                                WeatherObservation newObs = new WeatherObservation(
+                                        timeMeasured[i],
+                                        VIPSParamName,
+                                        WeatherObservation.LOG_INTERVAL_ID_1H,
+                                        this.getConvertedValue(value, VIPSParamName)
+                                );
+
+                                obsMapForPoint.put(newObs.getValiditySignature(), newObs);
+                            }   
+                        }
+
+                    }
+                }
+                
+            }
+            catch(IOException ioe)
+            {
+                
+            }
+            finally {
+                if(ncFile != null)
+                {
+                    try
+                    {
+                        ncFile.close();   
+                    }catch(IOException ex) {}
+                }
+                if(tmp != null)
+                {
+                    tmp.delete();
+                }
+            }
+        });
+        List<PointWeatherObservationList> retVal = new ArrayList<>();
+        for(Coordinate c : tempRetVal.keySet())
+        {
+            Map<Long, WeatherObservation> hashedObsForPoint = tempRetVal.get(c);
+            List<WeatherObservation> obsList = new ArrayList(hashedObsForPoint.values());
+            Collections.sort(obsList);
+            PointWeatherObservationList obsForPoint = new PointWeatherObservationList(c,obsList);
+            retVal.add(obsForPoint);
+        }
+        return retVal;
+    }
+    
+    public List<WeatherObservation> getPointData(Double longitude, Double latitude, Date dateFrom, Date dateTo, List<String> VIPSWeatherParameters)
+    {
+        Set<String> metParamNames = VIPSWeatherParameters.stream().map(
+                param -> paramInfo.getProperty(param)
+        ).collect(Collectors.toSet());
+        
+        List<Date> archiveDates = getArchiveDates(dateFrom, dateTo);
+        Collections.sort(archiveDates);
+        
+        Map<Long, WeatherObservation> retVal = new HashMap<>();
+        archiveDates.stream().forEach(archiveDate -> {
+            File tmp = null;
+            NetcdfFile ncFile = null;
+            try
+            {
+                URL threddsURL = new URL(NCSS_URL + MEPS25_PATH + 
+                        this.pathFormat.format(archiveDate) + 
+                        MEPS25_FILEBASENAME + 
+                        this.fileTimeStampFormat.format(archiveDate) +
+                        NETCDF_EXT + STANDARD_PARAMS + "&longitude=" + longitude + "&latitude=" + latitude
+                        + "&var=" + String.join(",", metParamNames));
+                System.out.println("URL: " + threddsURL.toString());
+                Long timestamp = new Date().getTime();
+                tmp = new File(this.TMP_FILE_PATH + timestamp + ".nc");
+                FileUtils.copyURLToFile(threddsURL, tmp);
+                ncFile = NetcdfFile.open(tmp.getAbsolutePath());
+
+                // Set up the array of timestamps
+                // Unfortunately, the time value is a Double
+                // Dimensions of the time array are [station, timestamp]. Since theres 
+                // Only one station, we can reduce the array dims. The reduce() method
+                // Simply removes all dims with a length of 1. Handy!
+                ArrayDouble.D1 time = (ArrayDouble.D1) ncFile.findVariable("time").read().reduce();
+                int[] timeDims = time.getShape();
+                // Get the length of the timestamp dimension
+                Date[] timeMeasured = new Date[timeDims[0]];
+                // Iterate, create parallel array with Dates
+                for(int i=0;i<timeDims[0];i++)
+                {
+                    timeMeasured[i] = new Date(Math.round(time.get(i) * 1000));
+                }
+
+                // The position variables have 1 dimension and one element. Simple!
+                SimpleWGS84Coordinate coordinate = new SimpleWGS84Coordinate(
+                        ((ArrayDouble.D1) ncFile.findVariable("latitude").read()).get(0), 
+                        ((ArrayDouble.D1) ncFile.findVariable("longitude").read()).get(0)
+                );
+                // Can't use stream API here because the ncfile would need to be 
+                // final and therefore not closeable in a try-finally statement
+                for(String metParamName:metParamNames)
+                {
+                    String VIPSParamName = this.findVIPSParamNameFromMetParamName(metParamName);
+
+                    Variable var = ncFile.findVariable(metParamName);
+
+                    // The variables have two different dims. The variables that
+                    // are height dependent (e.g. temperature) have [station, value, height] 
+                    // and the height independent (e.g. precipitation_amount) have 
+                    // [station, value]. Either way, all dims except value have a length
+                    // of 1 and can be reduced.
+                    // Assuming all weather parameters are of type "float"
+                    try
+                    {
+                        ArrayFloat.D1 varArray = (ArrayFloat.D1) var.read().reduce();
+                        // We always skip the two first timestamps, since the model needs 
+                        // a couple of hours to spin up
+                        for(int i=2;i<timeMeasured.length;i++)
+                        {
+                            if(timeMeasured[i].compareTo(dateFrom) < 0 || timeMeasured[i].after(dateTo))
+                            {
+                                continue;
+                            }
+                            Double value = new Double(varArray.get(i));
+                            // For accumulated parameters, substract previous value in array
+                            if(i>0 && this.accumulatedParams.contains(VIPSParamName))
+                            {
+                                value = value - new Double(varArray.get(i-1));
+                            }
+                            WeatherObservation newObs = new WeatherObservation(
+                                    timeMeasured[i],
+                                    VIPSParamName,
+                                    WeatherObservation.LOG_INTERVAL_ID_1H,
+                                    this.getConvertedValue(value, VIPSParamName)
+                            );
+                            retVal.put(newObs.getValiditySignature(), newObs);
+                        }
+                    }catch(IOException ioe)
+                    {
+                        ioe.printStackTrace();
+                    }
+                }
+                
+                
+            }
+            catch(IOException ioe)
+            {
+                
+            }
+            finally {
+                if(ncFile != null)
+                {
+                    try
+                    {
+                        ncFile.close();   
+                    }catch(IOException ex) {}
+                }
+                if(tmp != null)
+                {
+                    tmp.delete();
+                }
+            }
+        });
+        return new ArrayList(retVal.values());
+    }
+    
+    public String findVIPSParamNameFromMetParamName(String metParamName)
+    {
+        // Need to treat the Properties object as a map and search for a key that
+        // maps to the given met param name
+        Optional<Entry<Object,Object>> opt = this.paramInfo.entrySet().stream().filter(propEntry -> propEntry.getValue().equals(metParamName)).findFirst();
+        return opt.isPresent() ? (String) opt.get().getKey() : null;
+    }
+    
+    /**
+     * Performing the correct transformations between Met params and VIPS params
+     * @param value
+     * @param VIPSParamName
+     * @return 
+     */
+    public double getConvertedValue(Double value, String VIPSParamName)
+    {
+        switch(VIPSParamName){
+            case WeatherElements.TEMPERATURE_MEAN:
+            case WeatherElements.TEMPERATURE_MAXIMUM:
+            case WeatherElements.TEMPERATURE_MINIMUM:
+                return this.weatherUtil.getCelciusFromKelvin(value);
+            case WeatherElements.RELATIVE_HUMIDITY_MEAN:
+            case WeatherElements.RELATIVE_HUMIDITY_INSTANTANEOUS:
+                return this.getPercentFromFraction(value);
+            case WeatherElements.PRECIPITATION:
+                return roundToDecimals(value, 1);
+            case WeatherElements.GLOBAL_RADIATION:
+                return Math.round(value/3600);
+            default:
+                return value;
+        }
+    }
+    
+    
+    
+    public double getPercentFromFraction(Double value)
+    {
+        return value * 100;
+    }
+    
+    public double roundToDecimals(Double value, Integer decimals)
+    {
+        return Math.round(value * Math.pow(10.0, decimals)) / Math.pow(10.0, decimals);
+    }
+
+    /**
+     * Provide the list of date paths in which we can find files that contain
+     * data for the given period
+     * @param dateFrom
+     * @param dateTo
+     * @return 
+     */
+    public List<Date> getArchiveDates(Date dateFrom, Date dateTo) {
+        Date today = new Date();
+        dateTo = dateTo.after(today) ? today : dateTo;
+        if(dateFrom.after(dateTo))
+        {
+            return null;
+        }
+        TimeZone UTC = TimeZone.getTimeZone("UTC");
+        
+        // If the date is set at earlier than xx:02:00 UTC
+        // Going one day back in time from dateFrom, since we skip the two 
+        // first runs of each model
+        Calendar cal = Calendar.getInstance(UTC);
+        cal.setTime(dateFrom);
+        if(cal.get(Calendar.HOUR_OF_DAY) < 2)
+        {
+            cal.add(Calendar.DATE, -1);
+            dateFrom = cal.getTime();
+        }
+        
+        // Adjusting to UTC midnight so that the last date (dateTo) is not
+        // overstepped
+        dateFrom = this.weatherUtil.normalizeToExactDate(dateFrom, UTC);
+        List<Date> retVal = new ArrayList<>();
+        while(dateFrom.before(dateTo))
+        {
+            retVal.add(dateFrom);
+            cal.setTime(dateFrom);
+            cal.set(Calendar.HOUR_OF_DAY, 0);
+            cal.add(Calendar.DATE, 1);
+            dateFrom = cal.getTime();
+        }
+        
+        return retVal;
+    }
+    
+    public double getNorth(Polygon envelope)
+    {
+        return envelope.getCoordinates()[2].y;
+    }
+    
+    public double getSouth(Polygon envelope)
+    {
+        return envelope.getCoordinates()[0].y;
+    }
+    
+    public double getWest(Polygon envelope)
+    {
+        return envelope.getCoordinates()[0].x;
+    }
+    
+    public double getEast(Polygon envelope)
+    {
+        return envelope.getCoordinates()[2].x;
+    }
+}
diff --git a/src/main/resources/no/nibio/vips/util/weather/metnothredds/parametersources.properties b/src/main/resources/no/nibio/vips/util/weather/metnothredds/parametersources.properties
new file mode 100644
index 00000000..5707d828
--- /dev/null
+++ b/src/main/resources/no/nibio/vips/util/weather/metnothredds/parametersources.properties
@@ -0,0 +1,22 @@
+# Copyright (c) 2017 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/>.
+# 
+TM=air_temperature_2m
+TX=air_temperature_max
+TN=air_temperature_min
+UM=relative_humidity_2m
+RR=precipitation_amount_acc
+Q0=integral_of_surface_net_downward_shortwave_flux_wrt_time
\ No newline at end of file
diff --git a/src/test/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParserTest.java b/src/test/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParserTest.java
new file mode 100644
index 00000000..a44f8045
--- /dev/null
+++ b/src/test/java/no/nibio/vips/util/weather/metnothredds/MetNoThreddsDataParserTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2017 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/>.
+ * 
+ */
+package no.nibio.vips.util.weather.metnothredds;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import com.vividsolutions.jts.geom.GeometryFactory;
+import com.vividsolutions.jts.geom.Polygon;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+import no.nibio.vips.entity.PointWeatherObservationList;
+import no.nibio.vips.entity.WeatherObservation;
+import no.nibio.vips.util.WeatherUtil;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author treinar
+ */
+public class MetNoThreddsDataParserTest {
+    
+    public MetNoThreddsDataParserTest() {
+    }
+    
+    @BeforeClass
+    public static void setUpClass() {
+    }
+    
+    @AfterClass
+    public static void tearDownClass() {
+    }
+    
+    @Before
+    public void setUp() {
+    }
+    
+    @After
+    public void tearDown() {
+    }
+
+    //@Test
+    public void testGetGridData()
+    {
+        System.out.println("testGetGridData");
+        Coordinate[] coords = new Coordinate[5];
+        coords[0] = new Coordinate(10.31,59.91);
+        coords[1] = new Coordinate(11.07,59.91);
+        coords[2] = new Coordinate(11.07,59.52);
+        coords[3] = new Coordinate(10.31,59.52);
+        coords[4] = new Coordinate(10.31,59.91);
+        GeometryFactory gFac = new GeometryFactory();
+        Polygon pol = gFac.createPolygon(coords);
+        
+        List<String> weatherParameters = Arrays.asList("TM","RR", "Q0", "TX","TN","UM");
+        MetNoThreddsDataParser instance = new MetNoThreddsDataParser();
+        
+        Date start = new Date();
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Europe/Oslo"));
+        cal.setTime(new Date());
+        //cal.add(Calendar.DATE, -2);
+        cal.set(Calendar.HOUR_OF_DAY, 5);
+        Date dateFrom = new WeatherUtil().normalizeToExactHour(cal.getTime(), TimeZone.getDefault());
+        cal.add(Calendar.DATE, 5);
+        Date dateTo = cal.getTime();
+        
+        
+        List<PointWeatherObservationList> result = instance.getGridData(pol, dateFrom, dateTo, weatherParameters);
+        assertNotNull(result);
+        Long timeSpent = new Date().getTime() - start.getTime();
+        System.out.println("Time spent=" + (new Double(timeSpent)/1000) + " seconds");
+        result.stream().forEach(mp->System.out.println(mp));
+    }
+    
+    /**
+     * Test of getPointData method, of class MetNoThreddsDataParser.
+     */
+    @Test
+    public void testGetPointData() {
+        System.out.println("getPointData");
+        
+        Double longitude = 10.7946;
+        Double latitude = 59.6652;
+
+        List<String> weatherParameters = Arrays.asList("TM","RR", "Q0", "TX","TN","UM");
+        MetNoThreddsDataParser instance = new MetNoThreddsDataParser();
+        
+        Date start = new Date();
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Europe/Oslo"));
+        cal.setTime(new Date());
+        cal.add(Calendar.DATE, -20);
+        cal.set(Calendar.HOUR_OF_DAY, 1);
+        Date dateFrom = new WeatherUtil().normalizeToExactHour(cal.getTime(), TimeZone.getDefault());
+        cal.add(Calendar.DATE, 20);
+        Date dateTo = cal.getTime();
+        
+        
+        List<WeatherObservation> result = instance.getPointData(longitude, latitude, dateFrom, dateTo, weatherParameters);
+        Long timeSpent = new Date().getTime() - start.getTime();
+        System.out.println("Time spent=" + (new Double(timeSpent)/1000) + " seconds");
+        
+        Collections.sort(result);
+        Long hoursIncluded = 1 + ((result.get(result.size()-1).getTimeMeasured().getTime() - result.get(0).getTimeMeasured().getTime()) / 3600000);
+        long expResult = weatherParameters.size() * hoursIncluded;
+        result.stream().forEach(obs->System.out.println(obs));
+        assertEquals(expResult, result.size());
+        assertTrue(result.size() > 10);
+        
+    }
+    
+    @Test
+    public void testFindVIPSParamNameFromMetParamName()
+    {
+        System.out.println("testFindVIPSParamNameFromMetParamName");
+        MetNoThreddsDataParser instance = new MetNoThreddsDataParser();
+        String expResult = "TM";
+        String result = instance.findVIPSParamNameFromMetParamName("air_temperature_2m");
+        assertEquals(expResult, result);
+        result = instance.findVIPSParamNameFromMetParamName("foo_bar_1000m");
+        assertNull(result);
+    }
+    
+    @Test
+    public void testGetArchiveDates()
+    {
+        System.out.println("testGetArchiveDates");
+        MetNoThreddsDataParser instance = new MetNoThreddsDataParser();
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Europe/Oslo"));
+        cal.setTime(new Date());
+        cal.add(Calendar.DATE, -2);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        Date dateFrom = new WeatherUtil().normalizeToExactHour(cal.getTime(), TimeZone.getDefault());
+        cal.add(Calendar.DATE, 5);
+        List<Date> result = instance.getArchiveDates(dateFrom, cal.getTime());
+        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HHZ");
+        f.setTimeZone(TimeZone.getTimeZone("UTC"));
+        result.stream().forEach(r->System.out.println(f.format(r)));
+        assertEquals(4,result.size());
+        
+    }
+    
+}
-- 
GitLab