V2_0.java [src/java/d/soils/wqm02_wqmsoilparams] 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-2017, 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.wqm02_wqmsoilparams;

/**
 *
 * @author Shaun Case
 */
import static adb.DBResources.EROSION_SQLSVR;
import static adb.DBResources.R2GIS_SQLSVR;
import csip.Config;
import csip.ModelDataService;
import csip.api.server.ServiceException;
import csip.SessionLogger;
import csip.annotations.Resource;
import csip.utils.JSONUtils;
import gisobjects.GISObject;
import static gisobjects.GISObject.GISObjectType.polygon;
import gisobjects.GISObjectException;
import static gisobjects.GISObjectFactory.createGISObject;
import static gisobjects.db.GISEngineFactory.createGISEngine;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import javax.ws.rs.Path;
import csip.annotations.Description;
import csip.annotations.Name;
import static d.util.WindWaterErosion.WWE_TIFF_FILE;
import java.util.LinkedHashMap;
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.SoilsData;
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.utils.EvalResult;
import static soils.utils.EvalResult.writeDouble;

/**
 * WQM-02: WQMSoilAttributes
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 *
 */
@Name("WQM-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, and gets parameters from SDM tables for computing nutrient and pesticide loss potentials")
@Path("d/wqmsoilparams/2.0")

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

  //Request
  protected String aoaId;
  protected JSONObject aoa_geometry;
  protected double minimumPercentage = 0.0; // Optional    
  protected String newWKT = "";
  protected double aoa_area;

  //Response
  protected JSONArray results;
  protected JSONArray excludeds;

  @Override
  protected void preProcess() throws ServiceException {
    try {
      JSONArray request = request().getRequest().optJSONArray("parameter");

      if (JSONUtils.checkKeyExistsB(JSONUtils.preprocess(request), "AoAId")) {
        this.aoaId = parameter().getString("AoAId");
        this.minimumPercentage = parameter().getDouble("aoa_filter_pct", 0.0);
        aoa_geometry = parameter().getParamJSON("aoa_geometry");
      } else {
        throw new ServiceException("No AoAId specified.");
      }
    } catch (JSONException se) {
      throw new ServiceException("Could not proceed with preprocessing of the request JSON: " + se.getMessage(), se);
    }
  }

  @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", "wqm-02 2.0");
      }
    };
  }

  @Override
  protected void doProcess() throws ServiceException, SQLException, GISObjectException, JSONException, IOException, Exception {
    if (null != aoa_geometry) {
      try (SOILS_DATA soilsDb = SOILS_DB_Factory.createEngine(getClass(), LOG, Config.getString("soils.gis.database.source")); Connection gisDb = resources().getJDBC(EROSION_SQLSVR);) {

        V2_0.AoA aoaObject = new AoA(soilsDb, gisDb, aoa_geometry, LOG, minimumPercentage);

        aoaObject.getIntersectionsAndComponents();
        aoa_area = aoaObject.getArea();
        results = aoaObject.toJSON();
        excludeds = aoaObject.getExcluded();
        if (aoaObject.hasGeomChanged()) {
          newWKT = aoaObject.shapeWKT();
        }
      }
    }
  }

  @Override
  protected void postProcess() throws Exception {
    results().put("AoAId", aoaId, "Area of Analysis Identifier");
    results().put("AoA Area", writeDouble(aoa_area, "%.3f"), "acres");
    results().put(SoilsData.MAP_UNIT_LIST, results);
    results().put(EXCLUDED_LIST, excludeds);
    if (!newWKT.isEmpty()) {
      results().put(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.");
    }
  }

////////////////////////////////    
//    
//  Inner Classes
//    
//////////////////////////////// 
  /**
   *
   */
  public class AoA extends soils.AoA {

    protected double minimumPercentage = 0.0;
    ArrayList<String> removedEmptyTaxorders = new ArrayList<>();
    ArrayList<String> removedAreaToSmall = new ArrayList<>();

    public AoA(SOILS_DATA soilsDb, Connection gisDb, JSONObject geometry, SessionLogger LOG, double minimumPercentage) throws GISObjectException, SQLException, JSONException, IOException, ServiceException {
      super(soilsDb, LOG);
      shape = createGISObject(geometry, createGISEngine(gisDb)); //createGISEngine(soilsDb.getConnection()));

      if (shape.getType() != polygon) {//  Also returns this for multipolygons.
        throw new ServiceException("The wqmsoilattributes service only accepts Polygon or MultiPolygon shapes");
      }

      shape.makeValid(GISObject.UsePurpose.all_purposes, GISObject.GISType.all_types);

      area = shape.areaInAcres();
      this.minimumPercentage = minimumPercentage;
    }

    /**
     *
     * @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 (component.horizons.size() > 0) {
        double pH = 0.0;
        double pH_thickness = 0.0;

        if (EvalResult.testDefaultDouble(component.calculated_coarse_frag_vol_total())) {
          component.calculated_coarse_frag_vol_total(0.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 (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;
//                    }
          //  TODO:  Is this valid??  What is "coarse" sized fragments??  Shouldn't we be looking at individual fragment records within the horizon for their fragsize_r value ??
          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;
        }

        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 {

      if (findIntersectedMapUnitsWithAreas()) {
        if (findAllComponentsApplyWaterFilter()) {
          removeComponentsByAreaSize();
          findComponentSoilMoistures();
          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 AoA");
        }
      } else {
        throw new ServiceException("Cannot find the mapunits intersected by this AoA");
      }
    }

    protected void removeComponentsByAreaSize() {
      for (String mukey : map_units.keySet()) {
        MapUnit tMapUnit = map_units.get(mukey);
        if (!tMapUnit.isExcluded()) {
          for (String cokey : tMapUnit.components().keySet()) {
            Component tComponent = tMapUnit.components().get(cokey);
            if (!tComponent.isExcluded()) {
              if (tComponent.area_pct() < this.minimumPercentage) {
                removedAreaToSmall.add(cokey);
                //tMapUnit.components().remove(cokey);
                tComponent.setExclude(true, "Component removed because its area percentage of the AoI is less than the minimum percentage required by the input parameters.");
              }
            }
          }
        }
      }
    }

    /**
     *
     * @return @throws JSONException
     */
    public JSONArray toJSON() throws JSONException, ServiceException {
      JSONArray mapunitData = new JSONArray();

      for (MapUnit mapUnit : map_units.values()) {
        if (!mapUnit.isExcluded()) {
          JSONArray mapUnitArray = new JSONArray();

          mapUnit.setOutputColumns(new ArrayList<>(Arrays.asList(TableMapUnit.MUKEY, TableMapUnit.MUNAME)));
          mapUnit.mapUnitJSON(mapUnitArray);

          JSONArray componentArray = new JSONArray();
          for (Component component : mapUnit.components().values()) {
            if (!component.isExcluded()) {
              JSONArray componentObject = new JSONArray();

              component.setOutputColumns(new ArrayList<>(Arrays.asList(
                  TableComponent.COKEY, TableComponent.COMPNAME, TableComponent.HYDGRP_NAME,
                  TableComponent.TAXORDER_NAME, TableComponent.SLOPE_R_NAME, TableComponentCalculations.AREA_PCT_NAME,
                  TableComponentCalculations.KFFACT_NAME, TableComponentCalculations.COMP_AREA_NAME, TableComponentCalculations.COARSE_FRAG_NAME,
                  TableComponentCalculations.OM_R_NAME, TableComponentCalculations.HZDEPTH_NAME, TableComponentCalculations.WTBL_NAME,
                  TableComponentCalculations.CRACKS_GR_24_NAME, TableComponentCalculations.SLOPE_GR_15_NAME, TableComponentCalculations.HWT_LT_24_NAME,
                  TableComponentCalculations.WTBL_TOP_MIN_NAME, TableComponentCalculations.PH
              )));
              component.tableColumnsToJSON(componentObject);
              componentArray.put(componentObject);

            }
          }

          if (componentArray.length() > 0) {
            mapUnitArray.put(JSONUtils.data(SoilsData.MAPUNIT_COMPONENT_LIST_NAME, componentArray));
            mapunitData.put(mapUnitArray);
          }

        }
      }

      return mapunitData;
    }
  }
}