V1_0.java [src/java/d/soils/topthree] Revision: default  Date:
/*
 * $Id$
 *
 * This file is part of the Cloud Services Integration Platform (CSIP),
 * a Model-as-a-Service framework, API, and application suite.
 *
 * 2012-2019, OMSLab, Colorado State University.
 *
 * OMSLab licenses this file to you under the MIT license.
 * See the LICENSE file in the project root for more information.
 */
package d.soils.topthree;

import static adb.DBResources.EROSION_SQLSVR;
import static adb.DBResources.R2GIS_SQLSVR;
import csip.Config;
import csip.ModelDataService;
import csip.SessionLogger;
import csip.annotations.Description;
import csip.annotations.Name;
import csip.annotations.Resource;
import csip.api.server.ServiceException;
import csip.utils.JSONUtils;
import static d.util.WindWaterErosion.WWE_TIFF_FILE;
import gisobjects.GISObject;
import static gisobjects.GISObject.DEFAULT_ASSUMED_SRID;
import gisobjects.GISObjectException;
import gisobjects.GISObjectFactory;
import gisobjects.db.GISEngine;
import gisobjects.db.GISEngineFactory;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.ws.rs.Path;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import soils.Component;
import soils.MapUnit;
import static soils.db.DBResources.SDM;
import soils.db.SOILS_DATA;
import soils.db.SOILS_DB_Factory;
import soils.exceptions.SDMException;
import soils.exceptions.WEPPException;
import soils.exceptions.WEPP_WEPSException;
import soils.exceptions.WEPSException;
import utils.EvalResult;

/**
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 */
@Name("Top Three")
@Description("Returns the the top three dominant soils in an area of interest, which are suitable for WEPP or WEPS modeling.")
@Path("d/topthree/1.0")
@Resource(from = soils.db.DBResources.class)
public class V1_0 extends ModelDataService {

  public static final String PARAM_SCENARIO_ID = "scenario_id";
  public static final String PARAM_SCENARIO_GEOMETRY = "scenario_geometry";
  public static final String PARAM_SHOW_FULL_OUTPUT = "show_full_output";
  public static final String SHOW_DEBUG_OUTPUT = "verbose";
  public static final String POINT_BUFER_SIZE = "point_buffer_radius";
  public static final String BUFFER_SIZE = "buffer_size";
  public static final String BAD_MAPUNIT_DATA = "Could not get component, horizon, fragments data for those mapunits";

  public static final double MIN_ACRES = 0.10;
  public static final double NO_DEM_SLOPE_CALCULATED = -999.0;  //Indicator in the full output that a DEM slope was never calculated for this soil intersection.
  public static final double DEFAULT_POINT_BUFFER_SIZE = 113.5; // 133.5m buffer = 10 acres.
  public static final double AVG_MUPOLYGON_AREA_RADIUS = 287.13;  //  287.13m buffer = about 64 acres.
  public static final double AVG_MUPOLYGON_AREA = 64;
  public static final int MAX_ADJACENT_SOIL_ATTEMPTS = 2;
  public static final double SQ_METERS_TO_ACRES = 4046.86;

  protected JSONObject scenario_geometry;

  protected int mupolygonkey = 0;
  protected double aoiArea = 0.0;

  protected List<SignificantSoil> topThree = new ArrayList<>();
  protected GISObject aoiShape;
  protected JSONArray excludedSoils;
  protected boolean usedAdjacentSoils = false;
  protected ArrayList<String> excludeMapunits = new ArrayList<>();
  protected boolean soilIntersectionsNotLargeEnough = false;

  protected int usedSoilsCount = 0;
  protected double bufferSize;
  protected boolean detailedOutput;

  /**
   *
   * @throws Exception
   */
  @Override
  protected void preProcess() throws Exception {
    bufferSize = parameter().getDouble(POINT_BUFER_SIZE, DEFAULT_POINT_BUFFER_SIZE);
    scenario_geometry = parameter().getParamJSON(PARAM_SCENARIO_GEOMETRY);
    detailedOutput = metainfo().getBoolean(SHOW_DEBUG_OUTPUT, false);
  }

  /**
   *
   * @throws Exception
   */
  @Override
  protected Map<String, Object> getConfigInfo() {
    return new LinkedHashMap<String, Object>() {
      {
        put("soils.gis.database.source", resources().getResolved("soils.gis.database.source"));
        put(SDM, resources().getResolved(SDM));
        put(R2GIS_SQLSVR, resources().getResolved(R2GIS_SQLSVR));
        put("fpp.version", "top_three 1.0");
      }
    };
  }

  @Override
  protected void doProcess() throws Exception {
    boolean useSurroundingSoils = true;
    boolean ranAdjacentOnce = false;
    double area;

    try (Connection gisDb = resources().getJDBC(EROSION_SQLSVR); SOILS_DATA soilsDb = SOILS_DB_Factory.createEngine(getClass(), LOG, Config.getString("soils.gis.database.source"))) {

      aoiShape = GISObjectFactory.createGISObject(scenario_geometry, GISEngineFactory.createGISEngine(gisDb), DEFAULT_ASSUMED_SRID);
      aoiShape.makeValid(GISObject.UsePurpose.all_purposes, GISObject.GISType.all_types);
      if (aoiShape.getGeometry().getGeometryType().equals("Point")) {
        aoiShape = aoiShape.buffer(bufferSize);
        aoiShape.hasChanged(true);
      }
      area = aoiShape.areaInAcres();
      getSoilData(soilsDb, aoiShape, LOG);
    }
  }

  /**
   *
   * @throws Exception
   */
  @Override
  protected void postProcess() throws Exception {
    DecimalFormat df = new DecimalFormat("#.##");
    DecimalFormat t_df = new DecimalFormat("#.#");

    results().put("aoi_area", Double.parseDouble(df.format(aoiArea)),
        "Area of the input geometry", "ac");
    results().put("shape_adjusted", aoiShape.hasChanged(),
        "true if the input shape was altered by the service code.");
    results().put("soils_used", usedSoilsCount, "The total number of intersected "
        + "soils used for the weighted averages, (3 or less).");
    results().put("one_or_more_soil_intersections_too_small", soilIntersectionsNotLargeEnough,
        "true if one or more of the soils intersections are less than the required min size " + MIN_ACRES + " ac.");
    JSONArray rComps = new JSONArray();
    for (SignificantSoil tSoil : topThree) {
      JSONArray rComp = new JSONArray();
      rComp.put(JSONUtils.data("soilPtr", tSoil.component.cokey(), "A non-connotative string of characters used to uniquely identify a record in the Component table"));
      rComp.put(JSONUtils.data("soilName", tSoil.component.compname(), "Name assigned to a component based on its range of properties"));
      rComp.put(JSONUtils.data("area_pct", tSoil.component.area_pct(), "The percentage of the component of the mapunit intersection with the CRP Offer Area", "Percent"));
      rComp.put(JSONUtils.data("area", tSoil.component.calculated_area(), "Soil Component Area (Acres) in the CRP Offer area", "Acres"));
      if (detailedOutput) {
        rComp.put(JSONUtils.data("mukey", tSoil.mapUnit.mukey(), "A non-connotative string of characters used to uniquely identify a record in the Mapunit table"));
        rComp.put(JSONUtils.dataUnitDesc("slope_r", tSoil.component.slope_r(), "Percent", "Slope RV: The difference in elevation between two points, expressed as a percentage of the distance between those points. (SSM)"));
        //rComp.put(JSONUtils.dataUnitDesc("length", tSoil.component.slop, "Feet", "Calculated slope length"));
        rComp.put(JSONUtils.data("soilLongName", tSoil.component.comp_long_name(), "A long name assigned to a component based on its range of properties and other factors"));
      }
      rComps.put(rComp);
    }
    results().put("top_3_soils", rComps);

    if (detailedOutput) {
      JSONArray topThreeDetails = new JSONArray();
      for (SignificantSoil tSoil : topThree) {
        topThreeDetails.put(tSoil.toJSON());
      }
      if (aoiShape.hasChanged()) {
        results().put("changed_or_corrected_input_shape", ((aoiShape.hasChanged()) ? aoiShape.toJSON() : "NONE"),
            "Corrected input shape geometry or 'NONE'.");
      }
      results().put("detailed_output", topThreeDetails,
          "Additional information about each soil component.");

      results().put("excluded_soils", excludedSoils,
          "Excluded soils due to missing or invalid SDM.");
    }
  }

  /**
   * Access SDM and Erosion databases directly. This function doesn't make any
   * external service calls, so it does not necessarily need to be run in
   * parallel, but if other services can run at the same time that do not need
   * the results of this, then it speeds things up if this is run in parallel
   * too.
   *
   * @param soilsDb
   * @param aoaShape
   * @param LOG
   * @throws Exception
   */
  protected void getSoilData(SOILS_DATA soilsDb, GISObject aoaShape, SessionLogger LOG) throws Exception {

    //  Initialize soilsdb connection with shape data to get soils information
    //   Will update the call to getParamMap() to use the PayloadParameter object instead after updating soilsDb.
    ErosionAoA aoa = new ErosionAoA(soilsDb, aoaShape, LOG);

    //  Get database erosion values (Will overwrite water erosion subsequently, and keep wind)
    //    getErosion(erosionConn, aoa.shape());  ///SAVE This until we know that we'll never use the erosion database for any subsequent versions of this service.
    //    sortByMukeyArea();
    //  Find top three soils that meet WEPP/WEPS filtering.
    aoa.findSoils();
    List<Component> tTopThree = aoa.getTopThree();
    for (Component comp : tTopThree) {
      SignificantSoil tSoil = new SignificantSoil();
      tSoil.component = comp;
      tSoil.mapUnit = aoa.getMapUnits().get(comp.mukey());
      tSoil.area = comp.calculated_area();

      if (!tSoil.hasGoodArea()) {
        metainfo().setWarning("One or more of the soil intersection polygons was less than the minimum acre threshold, " + MIN_ACRES + ".");
        soilIntersectionsNotLargeEnough = true;
      }
      topThree.add(tSoil);
    }
    aoiArea = aoa.getArea();
    excludedSoils = aoa.getExcluded();
  }

  public class ErosionAoA extends soils.AoA {

    private HashMap<String, MapUnit> weppwepsMapUnits = new LinkedHashMap<>();
    private Map<Double, Component> componentOrderList = new TreeMap<>(Collections.reverseOrder());

    static final double MINIMUM_PERCENTAGE_2 = 20.0;
    static final int ALLOWED_SRID = DEFAULT_ASSUMED_SRID;
    static final double DEFAULT_WEI = 134.0;

    protected String mupolygonkey = "";  //  This is held over from the original, in case we want to search by mupolygon in the future.
    protected GISEngine gisEngine = null;

    /**
     *
     * @param soilsDb
     * @param aoa_geometry
     * @param Log
     * @throws GISObjectException
     * @throws SQLException
     * @throws JSONException
     * @throws IOException
     * @throws ServiceException
     */
    public ErosionAoA(SOILS_DATA soilsDb, GISObject aoa_geometry, SessionLogger Log) throws GISObjectException, SQLException, JSONException, IOException, ServiceException {
      super(soilsDb, aoa_geometry, Log);

      if (ALLOWED_SRID != getSRID()) {
        throw new ServiceException("RUSLE2 Erosion, currently, only supports an input CRS of WGS-84 (4326 SRID).  You requested service for a geometry that was SRID: " + getSRID());
      }

      if (area > 10000.0) {
        throw new ServiceException("Feature specified has an area greater than 10,000 acres.  Area is too large.");
      }

    }

    /**
     *
     * @throws SQLException
     * @throws ServiceException
     * @throws GISObjectException
     */
    public void findSoils() throws SQLException, ServiceException, GISObjectException {

      // Test these soil components for their abililty to be used in WEPP or WEPS
      //  TODO:  Add calls to the WEPP and WEPS filters and collect exclusion reasons if they are missing data, but do not error-out.
      if ((null == shape) && (!mupolygonkey.isEmpty())) {
        if (null != gisEngine) {
          findMapUnitsWithAreasAndShapesByMuPolygonKey(mupolygonkey, gisEngine);
          this.area = 0.0;
          for (MapUnit mapUnit : map_units.values()) {
            this.area += mapUnit.area();
          }
        } else {
          throw new ServiceException("No GISEngine was created to manage this mupolygonkey's result data.");
        }
      } else {
        if (null != shape) {
          findIntersectedMapUnitsWithAreasAndShapes();
        } else {
          throw new ServiceException("No AoI location was speified.  Please specify an AoI location as a geometry or as a mapunit mukey vaue");
        }
      }

      if (soilsDb.findAllBasicComponentHorizonFragTextureData(map_units)) {
        //findAllTextureData();
        for (MapUnit mapunit : map_units.values()) {
          for (Component tComponent : mapunit.components().values()) {
            tComponent.applyHorizonFilter("WIND");
            tComponent.applyHorizonFilter("WATER");
            tComponent.updateCalculations();
          }
        }

        //  Copy data and test for WEPP usability
        for (MapUnit mapunit : map_units.values()) {
          MapUnit newMapUnit = mapunit.deepCopy();
          weppwepsMapUnits.put(newMapUnit.mukey(), newMapUnit);

          for (Component tComponent : newMapUnit.components().values()) {
            try {
              if (!tComponent.isExcluded()) {
                if (!EvalResult.testDefaultDouble(tComponent.tfact())) {
                  tComponent.adjustForWEPP_WEPS("all");
                  componentOrderList.put(tComponent.calculated_area(), tComponent);
                } else {
                  tComponent.setExclude(true, "Soil is missing t_factor value in SDM.");
                  map_units.get(newMapUnit.mukey()).components().get(tComponent.cokey()).setExclude(true, tComponent.getExcludedReason());
                }
              }
            } catch (WEPPException ex) {
              tComponent.setExclude(true, "WEPP Exclusion: " + ex.getMessage());
              map_units.get(newMapUnit.mukey()).components().get(tComponent.cokey()).setExclude(true, "WEPP Exclusion: " + ex.getMessage());
            } catch (SDMException ex) {
              tComponent.setExclude(true, "SDM Exclusion: " + ex.getMessage());
              map_units.get(newMapUnit.mukey()).components().get(tComponent.cokey()).setExclude(true, "SDM Exclusion: " + ex.getMessage());
            } catch (WEPP_WEPSException ex) {
              tComponent.setExclude(true, "Generic WEPP/WEPS Exclusion: " + ex.getMessage());
              map_units.get(newMapUnit.mukey()).components().get(tComponent.cokey()).setExclude(true, "Generic WEPP/WEPS Exclusion: " + ex.getMessage());
            } catch (WEPSException ex) {
              tComponent.setExclude(true, "WEPS Exclusion: " + ex.getMessage());
              map_units.get(newMapUnit.mukey()).components().get(tComponent.cokey()).setExclude(true, "WEPS Exclusion: " + ex.getMessage());
            }
          }
        }
      }
    }

    /**
     *
     * @return @throws ServiceException
     * @throws JSONException
     */
    public List<Component> getTopThree() throws ServiceException, JSONException {
      List<Component> ret_val = new ArrayList<>();
      if (!componentOrderList.isEmpty()) {
        int count = 0;

        Set s = componentOrderList.entrySet();
        Iterator i = s.iterator();

        while (i.hasNext() && (count < 3)) {
          Map.Entry entry = (Map.Entry) i.next();
          Component tComponent = (Component) entry.getValue();
          if (!tComponent.isExcluded()) {
            if (!excludeMapunits.contains(tComponent.mukey())) {
              ret_val.add((Component) entry.getValue());
              count++;
            }
          }
        }
      } else {
        throw new ServiceException("There were no returned components for the AoA provided to the CRP Service.");
      }
      return ret_val;
    }

    /**
     *
     * @return
     */
    public final int getSRID() {
      return shape.getGeometry().getSRID();
    }
  }
}