V2_1.java [src/java/d/soils/basic] 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.soils.basic;

import static adb.DBResources.EROSION_SQLSVR;
import csip.Config;
import csip.ModelDataService;
import csip.api.server.PayloadParameter;
import csip.api.server.PayloadResults;
import csip.api.server.ServiceException;
import csip.api.server.ServiceResources;
import csip.SessionLogger;
import csip.annotations.Resource;
import csip.utils.JSONUtils;
import gisobjects.GISObject;
import static gisobjects.GISObject.GISObjectType.featurecollection;
import static gisobjects.GISObject.GISObjectType.polygon;
import gisobjects.GISObjectException;
import gisobjects.GISObjectFactory;
import static gisobjects.GISObjectFactory.createGISObject;
import static gisobjects.db.GISEngineFactory.createGISEngine;
import gisobjects.vector.GIS_FeatureCollection;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import javax.ws.rs.Path;
import csip.annotations.Description;
import csip.annotations.Name;
import java.util.Map;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import static soils.AoA.CORRECTED_GEOMETRY;
import static soils.AoA.EXCLUDED_LIST;
import soils.Component;
import soils.Horizon;
import soils.MapUnit;
import soils.db.DBResources;
import static soils.db.DBResources.SDM;
import soils.db.SOILS_DATA;
import soils.db.SOILS_DB_Factory;
import soils.db.tables.TableComponent;
import soils.db.tables.TableComponentCalculations;
import soils.db.tables.TableMapUnit;
import soils.db.tables.TableMapUnitCalculations;
import soils.utils.EvalResult;
import static soils.utils.EvalResult.writeDouble;

/**
 * WQM-02: WQMSoilAttributes
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 *
 */
@Name("BasicSoil-02: Water Quality Soil Parameters (wqmsoilparams)")
@Description("Intersects area of analysis (AoA) geometry with NRCS Soil Data Mart (SDM) mapunit geometry, derives a list of distinct soil components for the AoA.")
@Path("d/basicsoil/2.1")

@Resource(from = DBResources.class)
public class V2_1 extends ModelDataService {

  GISObject aoa_geometry;
  private boolean useCsipJSON = true;
  private boolean returnIntersectPolys = true;
  protected double stopDepth = Double.NaN;
  protected final ServiceResources resources = resources();
  String geometry;
  String id;

  //Response
  private JSONArray results;

  @Override
  protected void preProcess() throws ServiceException, JSONException {

    PayloadParameter params = parameter();
    id = parameter().getString("aoa_id");
    
    if (params.has("aoa_geometry")) {
      geometry = params.getString("aoa_geometry");

    } else {
      throw new ServiceException("Service requires an aoa_geometry input.");
    }

    if (params.has("use_csip_json")) {
      useCsipJSON = params.getBoolean("use_csip_json");
    }

    if (params.has("return_intersecting_polygons")) {
      returnIntersectPolys = params.getBoolean("return_intersecting_polygons");
    }

    if (params.has("stop_horizon_depth")) {
      stopDepth = params.getDouble("stop_horizon_depth");
    }
  }

  @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(EROSION_SQLSVR, resources().getResolved(EROSION_SQLSVR));          
        put("fpp.version", "basic 2.1");
      }
    };
  }


    @Override
    protected void doProcess()  throws Exception {
    if (null != geometry) {
      try (SOILS_DATA soilsDb = SOILS_DB_Factory.createEngine(getClass(), LOG, Config.getString("soils.gis.database.source"));
          Connection gisDb = resources.getJDBC(EROSION_SQLSVR);) {
        GISObjectFactory.setFixBadGeometries(true);
        aoa_geometry = createGISObject(geometry, createGISEngine(gisDb));
        V2_1.AoA aoasObject = new V2_1.AoA(soilsDb, gisDb, id, aoa_geometry, LOG);
        aoasObject.getIntersectionsAndComponents();
        results = aoasObject.toJSON();
      }
    }

  }

  @Override
  protected void postProcess() throws Exception {
    PayloadResults result = results();
    if (useCsipJSON) {
      result.put("aoa_list", results);
    } else {
      result.put("basic_soil", results);
    }
  }

////////////////////////////////    
//    
//  Inner Classes
//    
//////////////////////////////// 
  /**
   *
   */
  public class AoADetail {

    protected class MapUnitDetail {

      private ArrayList<GISObject> intersectionShapes = new ArrayList<>();
      private double area;
      private double pct_aoa_area;

      public void setIntersectionShapes(ArrayList<GISObject> shapes) {
        if (!intersectionShapes.isEmpty()) {
          intersectionShapes.clear();
        }
        for (GISObject shape : shapes) {
          intersectionShapes.add(shape);
        }
      }

      public ArrayList<GISObject> getIntersectionShapes() {
        return intersectionShapes;
      }

      public void setArea(double value) {
        area = value;
      }

      public double getArea() {
        return area;
      }

      public void setPctAoAArea(double value) {
        pct_aoa_area = value;
      }

      public double getPctAoAArea() {
        return pct_aoa_area;
      }
    }

    String aoa_id;
    GISObject aoaGeometry;
    double area;
    ArrayList<String> map_unit_list = new ArrayList<>();
    String newShapeWKT = "";
    JSONArray excludeds = null;
    LinkedHashMap<String, MapUnitDetail> intersectedMapUnitDetail = new LinkedHashMap<>();

    public AoADetail(String id, GISObject geometry) throws GISObjectException, ServiceException {
      if ((null != id) && (!id.isEmpty()) && (null != geometry)) {
        aoa_id = id;
        aoaGeometry = geometry;
        aoaGeometry.makeValid(GISObject.UsePurpose.all_purposes, GISObject.GISType.all_types);
        area = aoaGeometry.areaInAcres();
      } else {
        throw new ServiceException("Invalid data passed to AoADetail");
      }
    }

    public void addIntersectionShapes(String mukey, ArrayList<GISObject> shapes) {
      MapUnitDetail mapUnitDetail = intersectedMapUnitDetail.get(mukey);
      if (null != mapUnitDetail) {
        mapUnitDetail.setIntersectionShapes(shapes);
      }
    }

    public ArrayList<GISObject> getIntersectionShapes(String mukey) {
      MapUnitDetail mapUnitDetail = intersectedMapUnitDetail.get(mukey);
      if (null != mapUnitDetail) {
        return mapUnitDetail.getIntersectionShapes();
      }
      return null;
    }

    public void addMapUnits(LinkedHashMap<String, MapUnit> newMapUnits) {
      for (String key : newMapUnits.keySet()) {
        map_unit_list.add(key);
        intersectedMapUnitDetail.put(key, new MapUnitDetail());
      }
    }

    public ArrayList<String> getMapUnits() {
      return map_unit_list;
    }

    public void setMapUnitArea(String mukey, double area) {
      MapUnitDetail mapUnitDetail = intersectedMapUnitDetail.get(mukey);
      if (null != mapUnitDetail) {
        mapUnitDetail.setArea(area);
      }
    }

    public double getMapUnitArea(String mukey) {
      MapUnitDetail mapUnitDetail = intersectedMapUnitDetail.get(mukey);
      if (null != mapUnitDetail) {
        return mapUnitDetail.getArea();
      }
      return Double.NaN;
    }

    public void setMapUnitPctAoAArea(String mukey, double area) {
      MapUnitDetail mapUnitDetail = intersectedMapUnitDetail.get(mukey);
      if (null != mapUnitDetail) {
        mapUnitDetail.setPctAoAArea(area);
      }
    }

    public double getMapUnitPctAoAArea(String mukey) {
      MapUnitDetail mapUnitDetail = intersectedMapUnitDetail.get(mukey);
      if (null != mapUnitDetail) {
        return mapUnitDetail.getPctAoAArea();
      }
      return Double.NaN;
    }

    public double getArea() {
      return area;
    }

    public String getId() {
      return aoa_id;
    }

    public GISObject getShape() {
      return aoaGeometry;
    }

    public boolean hasShapeChanged() {
      return (!newShapeWKT.isEmpty());
    }

    public void setNewShape(String shapeWKT) {
      newShapeWKT = shapeWKT;
    }

    public String getNewWKT() {
      return newShapeWKT;
    }

    public JSONArray getExcluded() {
      return excludeds;
    }

    public void setExcluded(JSONArray excluded) {
      excludeds = excluded;
    }

  }

  public class AoA extends soils.AoA {

    ArrayList<String> removedEmptyTaxorders = new ArrayList<>();
    ArrayList<String> removedAreaToSmall = new ArrayList<>();
    ArrayList<AoADetail> aoas = new ArrayList<>();

    AoA(SOILS_DATA soilsDb, Connection gisDb, String id, GISObject geometry, SessionLogger LOG) throws GISObjectException, SQLException, JSONException, IOException, ServiceException {
      super(soilsDb, LOG);
      shape = geometry; //createGISObject(geometry, createGISEngine(gisDb)); //createGISEngine(soilsDb.getConnection()));

      if (shape.getType() != polygon) {//  Also returns this for multipolygons.
        if (shape.getType() != featurecollection) {
          throw new ServiceException("The basicsoil service only accepts single Polygon or MultiPolygon shapes or featurecollections of single Polygon or MultiPolygon shapes");
        } else {
          for (int i = 0; i < ((GIS_FeatureCollection) shape).getFeatureCount(); i++) {
            String aoaId = ((GIS_FeatureCollection) shape).getFeatureAttribute(i, "aoa_id");
            GISObject aoa = ((GIS_FeatureCollection) shape).getGeometry(i);

            if ((null != aoaId) && !aoaId.isEmpty()) {
              AoADetail newAoA = new AoADetail(aoaId, aoa);
              aoas.add(newAoA);
            } else {
              throw new ServiceException("Missing aoa_id feature attribute for the feature at position " + i + 1 + " in the featurecollection specified.");
            }
          }
        }
      } else {
        AoADetail newAoA = new AoADetail((((null != id) && (!id.isEmpty())) ? id : "1"), shape);
        aoas.add(newAoA);
      }
    }

    /**
     *
     * @param component
     */
    public void computeHorizonResults(Component component) {
      double profile_thk = 0.0;
      boolean haveHzDepth = false;
      boolean haveOM_R = false;
      double comp_product = 0.0;
      int counter = 0;

      if (Double.isNaN(stopDepth)) {

      }

      if (component.horizons.size() > 0) {
        double pH = 0.0;
        double pH_thickness = 0.0;
        double currentECEC = 0;
        double currentCEC7 = 0;
        double currentEC = 0;
        double currentDepth = 0;

        for (Horizon horizon : component.horizons.values()) {
          double horizonThickness = 0.0;
          double horizonProduct = 0.0;
          double pH_l = horizon.ph1to1h2o_l();
          double pH_r = horizon.ph1to1h2o_r();
          double pH_h = horizon.ph1to1h2o_h();

          if (Double.isNaN(stopDepth)) {
            currentECEC += (EvalResult.testDefaultDouble(horizon.ecec_r()) ? 0 : horizon.ecec_r()) * horizon.hzthk_r();
            currentCEC7 += (EvalResult.testDefaultDouble(horizon.cec7_r()) ? 0 : horizon.cec7_r()) * horizon.hzthk_r();
            currentEC += (EvalResult.testDefaultDouble(horizon.ec_r()) ? 0 : horizon.ec_r()) * horizon.hzthk_r();
            currentDepth += horizon.hzthk_r();
          } else {
            if (currentDepth < stopDepth) {
              double depthDiff = stopDepth - (currentDepth + horizon.hzthk_r());
              double hzDepth;
              if (depthDiff < 0) {
                hzDepth = horizon.hzthk_r() + depthDiff;
              } else {
                hzDepth = horizon.hzthk_r();
              }
              currentECEC += (EvalResult.testDefaultDouble(horizon.ecec_r()) ? 0 : horizon.ecec_r()) * hzDepth;
              currentCEC7 += (EvalResult.testDefaultDouble(horizon.cec7_r()) ? 0 : horizon.cec7_r()) * hzDepth;
              currentEC += (EvalResult.testDefaultDouble(horizon.ec_r()) ? 0 : horizon.ec_r()) * hzDepth;
              currentDepth += hzDepth;
            }
          }

//                    if (0 == counter) {
//                        component.calculated_om_r(horizon.om_r());
//                    }
          counter++;

          if (EvalResult.testDefaultDouble(horizon.hzthk_r())) {
            horizonThickness = horizon.hzdepb_r() - horizon.hzdept_r();
          } else {
            horizonThickness = horizon.hzthk_r();
          }

          if ((!haveOM_R) && (!haveHzDepth) && (horizon.selected())) {
            component.calculated_om_r(horizon.om_r());
            component.calculated_hzdepb_r(horizonThickness);
            haveHzDepth = true;
            haveOM_R = true;
          }

          if (EvalResult.testDefaultDouble(pH_r)) {
            if (!EvalResult.testDefaultDouble(pH_h) && !EvalResult.testDefaultDouble(pH_l)) {
              pH_r = (pH_h + pH_l) / 2.0;
              pH_thickness += horizonThickness;
              pH += pH_r * horizonThickness;
            }//Else ignore this layer in the calculation...don't need to do anything else in this case...

          } else {
            pH_thickness += horizonThickness;
            pH += pH_r * horizonThickness;
          }

//                    if (!haveHzDepth) {  //We only want to use the first thickness since these are presorted by the SQL statement in soilsdb
//                        component.calculated_hzdepb_r(horizonThickness);
//                        haveHzDepth = true;
//                    }
          component.calculated_coarse_frag_vol_total(component.calculated_coarse_frag_vol_total() + (EvalResult.testDefaultDouble(horizon.fragvol_r()) ? 0 : horizon.fragvol_r()));

          profile_thk += horizonThickness;
          horizonProduct = horizonThickness * (EvalResult.testDefaultDouble(horizon.fragvol_r()) ? 0 : horizon.fragvol_r());
          comp_product += horizonProduct;
        }

        component.calculated_ec(currentEC / currentDepth);
        component.calculated_ecec(currentECEC / currentDepth);
        component.calculated_cec7(currentCEC7 / currentDepth);

        if (profile_thk > 0.0) {
          component.calculated_coarse_frag(comp_product / profile_thk);
        }
        if (pH_thickness > 0.0) {
          component.calculated_pH(pH / pH_thickness);
        }
      }

    }

    /**
     *
     * @throws ServiceException
     * @throws GISObjectException
     */
    public void getIntersectionsAndComponents() throws ServiceException, GISObjectException, JSONException {
      for (AoADetail AoA : aoas) {
        LinkedHashMap<String, MapUnit> saveMapUnits = new LinkedHashMap<>();
        for (String key : this.map_units.keySet()) {
          saveMapUnits.put(key, this.map_units.get(key));
        }
        this.map_units.clear();

        shape = AoA.getShape();

        if (returnIntersectPolys) {
          if (!findIntersectedMapUnitsWithAreasAndShapes()) {
            throw new ServiceException("Cannot find the mapunits intersected by this AoA");
          }
        } else {
          if (!findIntersectedMapUnitsWithAreas()) {
            throw new ServiceException("Cannot find the mapunits intersected by this AoA");
          }
        }

        if (hasGeomChanged()) {
          AoA.setNewShape(shapeWKT());
        }

        //  Must add the mapunit list to the AoA before adding the polygons, areas, etc. below.
        AoA.addMapUnits(this.map_units);
        if (hasGeomChanged()) {
          AoA.newShapeWKT = shapeWKT();
        }

        //  This insures that if any features in the collection overlap that the mapunit area calculations 
        //    and intersection polygons do not get confused with each other since the actual mapunit 
        //    soil data will be the same .
        for (MapUnit mapUnit : map_units.values()) {
          AoA.addIntersectionShapes(mapUnit.mukey(), mapUnit.getIntersectionPolygons());
          AoA.setMapUnitArea(mapUnit.mukey(), mapUnit.area());
          AoA.setMapUnitPctAoAArea(mapUnit.mukey(), mapUnit.area_pct());
        }

        for (String key : saveMapUnits.keySet()) {
          this.map_units.put(key, saveMapUnits.get(key));
        }
      }

      if (soilsDb.findAllBasicComponentHorizonFragTextureData(map_units)) {
        for (MapUnit mapUnit : map_units.values()) {
          if (!mapUnit.isExcluded()) {
            for (Component component : mapUnit.components().values()) {
              if (!component.isExcluded()) {
                computeHorizonResults(component);
              }
            }
          }
        }
      } else {
        throw new ServiceException("Cannot find components for the map units interesected by this " + ((aoas.size() > 1) ? "FeatureCollection" : "AoA"));
      }

      for (AoADetail AoA : aoas) {
        LinkedHashMap<String, MapUnit> saveMapUnits = new LinkedHashMap<>();
        for (String key : this.map_units.keySet()) {
          saveMapUnits.put(key, this.map_units.get(key));
        }
        this.map_units.clear();

        for (String key : AoA.getMapUnits()) {
          map_units.put(key, saveMapUnits.get(key));
        }

        AoA.setExcluded(getExcluded());

        this.map_units.clear();

        for (String key : saveMapUnits.keySet()) {
          this.map_units.put(key, saveMapUnits.get(key));
        }
      }
    }

    protected void setOutputs(MapUnit mapUnit) throws ServiceException {
      mapUnit.outputShapes(returnIntersectPolys);
      mapUnit.setOutputColumnOrdering(new ArrayList<>(Arrays.asList(TableMapUnit.MUKEY, TableMapUnit.MUNAME,
          TableMapUnit.MUSYM, TableMapUnit.AREASYMBOL_NAME, TableMapUnitCalculations.AREA_NAME, TableMapUnitCalculations.AREA_PCT
      )));
      mapUnit.setComponentOutputColumnOrdering(new ArrayList<>(Arrays.asList(TableComponent.COKEY, TableComponent.COMPNAME, TableComponentCalculations.LONG_NAME,
          TableComponent.COMPPCT_R_NAME, TableComponentCalculations.AREA_PCT_NAME, TableComponentCalculations.COMP_AREA_NAME,
          TableComponent.HYDGRP_NAME
      )));
    }

    /**
     *
     * @return @throws JSONException
     */
    public JSONArray toJSON() throws JSONException, ServiceException {
      JSONArray allMapUnitData = new JSONArray();
      JSONObject mapUnitData = null;

      for (AoADetail aoa : aoas) {
        mapUnitData = new JSONObject();
        JSONArray mapUnitArray = new JSONArray();
        double area;
        JSONArray excluded;
        String newWKT = "";

        ArrayList<String> aoaMapUnits = aoa.getMapUnits();

        area = aoa.getArea();
        excluded = aoa.getExcluded();
        if (aoa.hasShapeChanged()) {
          newWKT = aoa.getNewWKT();
        }
        if (useCsipJSON) {
          mapUnitArray.put(JSONUtils.data("aoa_id", aoa.getId()));
          mapUnitArray.put(JSONUtils.data("aoa_area", writeDouble(area, "%.3f"), "acres"));
        } else {
          mapUnitData.put("aoa_id", aoa.getId());
          mapUnitData.put("aoa_area", writeDouble(area, "%.3f") + " acres");
        }

        JSONArray maps = new JSONArray();
        for (String key : aoaMapUnits) {
          MapUnit mapUnit = map_units.get(key);

          //  The order of the three setters below is important.
          mapUnit.area(aoa.getMapUnitArea(key));
          mapUnit.area_pct(aoa.getMapUnitPctAoAArea(key));
          mapUnit.aoaArea(aoa.getArea());

          mapUnit.clearIntersectionPolygonList();
          mapUnit.setIntersectionPolygons(aoa.getIntersectionShapes(key));

          if (!mapUnit.isExcluded()) {
            setOutputs(mapUnit);
            if (useCsipJSON) {
              maps.put(mapUnit.toJSON(false, null));
            } else {
              maps.put(mapUnit.toBasicJSON(false, null));
            }
          }
        }

        if (useCsipJSON) {
          mapUnitArray.put(JSONUtils.data(MAP_UNIT_LIST, maps));
          mapUnitArray.put(JSONUtils.data(EXCLUDED_LIST, excluded));

          if (!newWKT.isEmpty()) {
            mapUnitArray.put(JSONUtils.data(CORRECTED_GEOMETRY, newWKT, "If this section is present, the input geometry was invalid.  This service attempted to correct it, and was successful in creating a new geometry that could be utilized.  Please check this corrected geometry to be sure it represents what you originally intended.  If it does, please contact the source of your original geometry to have it corrected.  You may use this WKT to do so."));
          }
        } else {
          mapUnitData.put(MAP_UNIT_LIST, maps);
          mapUnitData.put(EXCLUDED_LIST, excluded);
          if (!newWKT.isEmpty()) {
            mapUnitData.put(CORRECTED_GEOMETRY, newWKT);
          }
        }
        if (useCsipJSON) {
          allMapUnitData.put(mapUnitArray);
        } else {
          allMapUnitData.put(mapUnitData);
        }
      }

      return allMapUnitData;
    }
  }
}