CligenV1_0.java [src/java/d/weather] Revision: default  Date:
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package d.weather;

import csip.Config;
import csip.ModelDataService;
import csip.api.client.ModelDataServiceCall;
import csip.api.server.ServiceException;
import csip.annotations.Description;
import csip.annotations.Name;
import csip.annotations.Resource;
import csip.utils.Parallel;
import data.interpretors.CligenData;
import static data.interpretors.CligenData.*;
import data.interpretors.WindGenData;
import gisobjects.GISObject;
import gisobjects.GISObjectFactory;
import gisobjects.db.GISEngineFactory;
import java.sql.Connection;
import java.time.LocalDate;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import javax.ws.rs.Path;
import static m.ghg.ApplicationResources.CLIGEN_URL_KEY;
import static m.ghg.ApplicationResources.GISDB_SQLSVR;
import static m.ghg.ApplicationResources.WINDGEN_URL_KEY;
import org.codehaus.jettison.json.JSONObject;

/**
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 */

/*
  WEATHER FILE Specifications:

==============================================================================
Daily Weather Data File:

 1   1  1990    1    7.040  -10.300    0.000  186.425   55.42   10.939
 2   1  1990    2    9.200  -10.530    0.000  158.115   57.42    5.552
 3   1  1990    3   11.840   -7.330    0.000  222.946   42.13    9.165
 4   1  1990    4    1.297  -10.310    0.000  182.844   40.97   12.543
 5   1  1990    5    1.239  -16.010    0.000  213.159   52.25    5.214
 6   1  1990    6    3.745   -9.380    0.000  230.346   41.89   10.603
...
27  12  1992  362   11.320  -12.880    0.000  217.456   35.13    4.429
28  12  1992  363    7.050  -10.180    0.000  216.501   28.25    5.132
29  12  1992  364   -1.095   -8.370    0.000   93.833   53.59    5.264
30  12  1992  365    7.330  -11.490    0.000  152.243   35.88    3.098
31  12  1992  366    7.330  -11.490    0.000  152.243   35.88    3.098

NOTES:
Column  1 - Day of month, 1-31
Column  2 - Month of year, 1-12
Column  3 - Year
Column  4 - Day of the year, 1-366
Column  5 - Maximum temperature for day, degrees C
Column  6 - Minimum temperature for day, degrees C
Column  7 - Precipitation for day, centimeters
Column  8 - Solar radiation, in langleys/day
Column  9 - Relative humidity, percentage, 1-100
Column 10 - Wind speed, miles per hour

Missing weather data values for precipitation, minimum temperature and maximum
temperature are represented by the value -99.9.

The last three columns in the weather data file, solar radiation, relative
humidity, and wind speed, are optional.  When these values are not included in
a weather file PET is computed using the FWLOSS(4) input variable from the
FIX.100 file and the flag for the extra weather drivers in the SITEPAR.IN file
must be set to 0.

==============================================================================

Schedule file notes for running DayCent:

When creating your schedule file for DayCent keep in mind that it operates
using daily weather data and must be able to find and read your daily weather
data file.  The "F" weather option choice is the only valid weather option for
the initial block in a DayCent schedule file.  The weather filename of the
daily weather data file must also appear in your schedule file following the
"F".  If you use a weather option value other than "F" for the initial weather
option in your schedule file the simulation will not run.  Subsequent blocks
may use either the "F", to start reading from the start of a daily weather
file, or "C", to continue reading the current weather file, weather options.

 */
@Name("DayCent Weather")
@Description("Provide Daycent weather input.")
@Path("d/weather/cligen/1.0")
@Resource(from = m.ghg.ApplicationResources.class)
@Resource(from = soils.db.DBResources.class)
public class CligenV1_0 extends ModelDataService {

  // Request JSON name defines
  static final String STREAM_FILE_KEY = "stream_output_file";
  static final String USE_CLIMATE_WIND_KEY = "use_cligen_wind";
  static final String DEFAULT_DAYCENT_CLIMATE_FILE = "dayCentWeather.IN";
  static final String WEATHER_FILE_KEY = "weather_file";
  static final String WEATHER_DURATION_KEY = "weather_duration";
  static final String STARTING_YEAR_KEY = "starting_year";

  boolean fileAsJSON;
  String dayCentClimateFileData;
  int weatherDuration;
  int startingYear;
  JSONObject aoa_geometry;
  boolean useClimateWind;

  WindGenData windData;
  CligenData climateData;

  static final String windgenURL = Config.getString(WINDGEN_URL_KEY);
  static final String cligenURL = Config.getString(CLIGEN_URL_KEY);

  DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");


  @Override
  protected void preProcess() throws Exception {
    aoa_geometry = parameter().getJSON(soils.AoA.AOA_GEOMETRY);
    useClimateWind = parameter().getBoolean(USE_CLIMATE_WIND_KEY, false);
    fileAsJSON = parameter().getBoolean(STREAM_FILE_KEY, false);
//    weatherDuration = parameter().getInt(WEATHER_DURATION_KEY, 50);
//    startingYear = parameter().getInt(STARTING_YEAR_KEY, 2000);

    LocalDate s = LocalDate.parse(parameter().getString("start_date"), formatter);
    LocalDate e = LocalDate.parse(parameter().getString("end_date"), formatter);

    startingYear = s.getYear();
    weatherDuration = (int) s.until(e, ChronoUnit.YEARS);
  }


  @Override
  protected void doProcess() throws Exception {

    double[] latlon = {Double.NaN, Double.NaN};

    //  Get centroid of shape, if it is not a point already.  (If it's a point, the centroid is the point)
    try (Connection conn = resources().getJDBC(GISDB_SQLSVR)) {
      GISObject shape = GISObjectFactory.createGISObject(aoa_geometry, GISEngineFactory.createGISEngine(conn));

      //  getLat/Lon functions get point value or the centroid of the shape.
      latlon[0] = shape.getLatitude();
      latlon[1] = shape.getLongitude();
    }

    if (Double.isNaN(latlon[0]) || Double.isNaN(latlon[1])) {
      throw new ServiceException("Could not get the latitude and longitude of the AoI");
    }

    Parallel.run(
        () -> {
          climateData = getClimateData(cligenURL, latlon[0], latlon[1], weatherDuration);
        },
        () -> {
          if (!useClimateWind) {
            windData = getWindData(windgenURL, latlon[0], latlon[1], weatherDuration);
          }
        });

    //  Use combined Climate/Wind output files class in csip-common(to be written), to merge climate and wind data
    //  Generate combined Climate/Wind file using DayCent specifications above.
    dayCentClimateFileData = toClimate(windData, climateData, useClimateWind, startingYear);
  }


  @Override
  protected void postProcess() throws Exception {
    if (!fileAsJSON) {
      workspace().writeString(DEFAULT_DAYCENT_CLIMATE_FILE, dayCentClimateFileData);
      results().put(workspace().getFile(DEFAULT_DAYCENT_CLIMATE_FILE), "DayCent weather file");
    } else {
      results().put(WEATHER_FILE_KEY, dayCentClimateFileData, "DayCent weather file data.");
    }
  }


  static WindGenData getWindData(String url, double lat, double lon, int years) throws Exception {
    ModelDataServiceCall res = new ModelDataServiceCall()
        .put("latitude", lat)
        .put("longitude", lon)
        .put("duration", years)
        .url(url)
        .withDefaultLogger()
        .call();

    if (res.serviceFinished()) {
      String windString = res.download("win_gen_d.win");
      return new WindGenData(windString, null);
    } else {
      throw new ServiceException("Wind service error: " + res.getError());
    }
  }


  static CligenData getClimateData(String url, double lat, double lon, int years) throws Exception {
    ModelDataServiceCall r = new ModelDataServiceCall()
        .put("outputFile", "cligen.cli")
        .put("returnParFiles", "true")
        //.put("adjustPWW_PWD", adjustPWW_PWD)
        .put("usePRISM", true)
        .put("dataVersion", 2015)
        .put("duration", years)
        .put("input_zone_features", new JSONObject(
            "{  'type': 'FeatureCollection',"
            + "   'features': [{"
            + "      'type': 'Feature',"
            + "      'properties': {"
            + "        'name': 'pt one',"
            + "        'gid': 1"
            + "       },"
            + "      'geometry': {"
            + "        'type': 'Point',"
            + "        'coordinates': ["
            + "          " + lon + ","
            + "          " + lat
            + "         ]"
            + "       }"
            + "   }]"
            + "}"))
        .withDefaultLogger()
        .url(url)
        .call();

    if (r.serviceFinished()) {
      String climateString = r.download("cligen.cli");
      return new CligenData(climateString, null);
    } else {
      throw new ServiceException("Climate service error: " + r.getError());
    }
  }


  static String toClimate(WindGenData windData,
      CligenData climateData, boolean useCligenWind, int startingYear)
      throws ServiceException {

    if (!((windData != null) || useCligenWind) && (climateData != null)) {
      throw new ServiceException("requires a valid WindGenData and CligenData input.");
    }

    int climateYears = climateData.yearsInFile();
    int windYears = useCligenWind ? climateYears : windData.yearsInFile();
    int combinedYears = (windYears < climateYears) ? windYears : climateYears;

    if (combinedYears < 1) {
      throw new ServiceException("combined climate years < 1");
    }

    StringBuilder fs = new StringBuilder("");
    for (int year = 0; year < combinedYears; year++) {
      int currentYear = year + startingYear;
      boolean leapYear = Year.of(currentYear).isLeap();
      int yearDay = 0;

      double[][] windEnergy = useCligenWind ? null : windData.dailyData(year);
      double[][][] climateValues = climateData.dailyData(year);

      for (int month = 0; month < 12; month++) {
        //Use WindGenData months...skip leap years. (windgen and cligen don't count Feb the same)
        for (int day = 0; day < WindGenData.monthDays[month]; day++) {
          double tMax = climateValues[month][day][TMAX_INDEX];
          double tMin = climateValues[month][day][TMIN_INDEX];
          double dewPoint = climateValues[month][day][TDEW_INDEX];

          //cligen stores precip in mm, we need cm
          double precip = climateValues[month][day][PRCP_INDEX] / 10.0;
          double rad = climateValues[month][day][RAD_INDEX];

          //Wind speeds are stored in cligen and windgen as meters per second, we need mph.
          double windSpeed = (useCligenWind ? climateValues[month][day][W_VL_INDEX] : windEnergy[month][day]) * 2.236936;

          double avgT = (tMax + tMin) / 2.0;

          //Standard RH calc based on temps and dew point in Celcius.
          double rHumidity = 100 * (Math.exp((17.625 * dewPoint) / (243.04 + dewPoint)) / Math.exp((17.625 * avgT) / (243.04 + avgT)));

          fs.append(day + 1).append('\t').append(month + 1).append('\t').append(currentYear).append('\t')
              .append(yearDay + 1).append('\t').append(tMax).append('\t').append(tMin).append('\t').append(precip).append('\t')
              .append(rad).append('\t').append(rHumidity).append('\t').append(windSpeed).append('\n');

          yearDay++;

          if (leapYear && (month == 1) && (day == 27)) {
            //  If this is a leapyear and Feb 28th, duplicate Feb 28th as also Feb 29th.
            fs.append(day + 2).append('\t').append(month + 1).append('\t').append(currentYear).append('\t')
                .append(yearDay + 1).append('\t').append(tMax).append('\t').append(tMin).append('\t').append(precip).append('\t')
                .append(rad).append('\t').append(rHumidity).append('\t').append(windSpeed).append('\n');

            yearDay++;
          }
        }
      }
    }
    return fs.toString();
  }

}