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

import static adb.DBResources.R2GIS_SQLSVR;
import csip.ModelDataService;
import static csip.ModelDataServiceConstants.KEY_DESC;
import static csip.ModelDataServiceConstants.KEY_NAME;
import static csip.ModelDataServiceConstants.KEY_VALUE;
import csip.annotations.Description;
import csip.annotations.Name;
import csip.api.server.PayloadResults;
import csip.api.server.ServiceException;
import csip.utils.JSONUtils;
import d.util.ServiceCall;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.ws.rs.Path;
import javax.ws.rs.core.UriBuilder;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import static soils.AoA.AOA_DRAINED;
import static soils.AoA.CORRECTED_GEOMETRY;
import static soils.AoA.EXCLUDED_LIST;
import soils.SoilsData;
import static soils.db.DBResources.SDM;
import soils.db.tables.TableComponent;
import soils.db.tables.TableComponentCalculations;
import soils.db.tables.TableMapUnit;

/**
 *
 * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
 *
 */
@Name("WQM-21: Nutrient Soil Leaching and Runoff Loss Potentials for an Area of Analysis (NutrientSLP-SRP)")
@Description("This service intersects area of analysis (AoA) and soil mapunit geometries, gets soil parameters, and computes nutrient soil leaching and runoff potentials as an end-to-end process. The services combines WQM-02, WQM-05, and WQM-06 services into a single service. It returns a results payload containing the relevant attributes for each soil component in the AoA, leaching (SLP) and runoff (SRP) potentials for each soil component, and weighted average leaching and runoff loss potential values for the AoA. The services allows for submitting parameter edits. For example, the request payload can contain just the AoA geometry and the services gets soil parameters and computes SLP and SRP and returns the results, including the parameters. If an application user edits the parameters, a subsequent request payload can contain the parameter edits and not the geometry.")
@Path("d/wqmnutrientslpsrp/2.0")
public class V2_0 extends ModelDataService {

  private double minimumPercentage;
  private boolean aoa_comp_drained;
  private String aoa_id = "0";
  private JSONArray soilComponents = null;
  private JSONObject aoaGeometry = null;
  private JSONObject badGeometryNote = null;
  private JSONObject badSoils = null;

  private V2_0.AoA soilData = null;

  private Boolean is_5_6_Only;
  private ServiceCall WQM02;

  protected static final String AOA_COMP_DRAINED = "aoa_comp_drained";
  protected static final String AOA_FILTER_PCT = "aoa_filter_pct";
  private static final String WQM_02_SERVICE_PATH = "/d/wqmsoilparams/2.0";
  private static String WQM_02_Service_URI = "";

  @Override
  protected void preProcess() throws ServiceException {
    WQM_02_Service_URI = UriBuilder.fromUri(request().getURL()).replacePath(request().getContext() + WQM_02_SERVICE_PATH).toString();

    Map<String, JSONObject> inputJSON = getParamMap();

    if (JSONUtils.checkKeyExistsB(inputJSON, soils.AoA.AOA_ID)) {

      aoa_id = parameter().getString(soils.AoA.AOA_ID);

      if (JSONUtils.checkKeyExistsB(inputJSON, soils.AoA.AOA_GEOMETRY)) {
        aoa_comp_drained = parameter().getBoolean(AOA_COMP_DRAINED, false);
        minimumPercentage = parameter().getDouble(AOA_FILTER_PCT, 0.0);
        //Get the entire aoa_geometry group as it matches the input payload exactly for WQM-2
        aoaGeometry = getParameter(soils.AoA.AOA_GEOMETRY);
        this.is_5_6_Only = false;
      } else if (JSONUtils.checkKeyExistsB(inputJSON, soils.SoilsData.MAP_UNIT_LIST)) {
        //  If this key exists then we have it at the top level by design, so keep the object as the payload
        soilComponents = (JSONArray) getParam();
        is_5_6_Only = true;
      } else {
        //  No valid input stream for this service
        throw new ServiceException("No valid input parameters found.");
      }
    } else {
      throw new ServiceException("No AoAId parameter specified.");
    }
  }

  /**
   * doProcess()
   *
   * Main process function inherited from ModelDataServices
   *
   *
   * @author <a href="mailto:shaun.case@colostate.edu">Shaun Case</a>
   *
   * @throws org.codehaus.jettison.json.JSONException
   * @throws csip.ServiceException
   */
  @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-21 2.0");
      }
    };
  }

  @Override
  protected void doProcess() throws ServiceException, JSONException {
    //  Call WQM-2 processing member functions, then apply results to WQM-5() and WQM-6() objects inputs   
    JSONObject metaInfo;

    V2_0.AoA.setRequiredInputs(new ArrayList<>(Arrays.asList(AoA.AOA_ID, AoA.MAP_UNIT_LIST)),
        new ArrayList<>(Arrays.asList(TableMapUnit.MUKEY, SoilsData.MAPUNIT_COMPONENT_LIST_NAME)),
        new ArrayList<>(Arrays.asList(TableComponent.COKEY, TableComponent.TAXORDER_NAME, TableComponent.SLOPE_R_NAME,
            TableComponent.HYDGRP_NAME,
            TableComponentCalculations.COMP_AREA_NAME, TableComponentCalculations.COARSE_FRAG_NAME,
            TableComponentCalculations.KFFACT_NAME, TableComponentCalculations.WTBL_TOP_MIN_NAME,
            TableComponentCalculations.HWT_LT_24_NAME, TableComponentCalculations.WTBL_NAME)),
        null);

    if (!this.is_5_6_Only) {
      //This is a full combined request 2/5/6          
      WQM02 = new WQMSoilParams(aoa_id, minimumPercentage, aoa_comp_drained, aoaGeometry, WQM_02_Service_URI);
      WQM02.call();

      soilComponents = WQM02.getResultSection();
      Map<String, JSONObject> wqm02Map = JSONUtils.preprocess(soilComponents);

      if (wqm02Map.containsKey(CORRECTED_GEOMETRY)) {
        badGeometryNote = wqm02Map.get(CORRECTED_GEOMETRY);
      }
      if (wqm02Map.containsKey(EXCLUDED_LIST)) {
        badSoils = wqm02Map.get(EXCLUDED_LIST);
      }

      soilComponents.put(JSONUtils.dataDesc(AOA_DRAINED, aoa_comp_drained, "Is AoA drained?"));
      metaInfo = WQM02.getReturnMetainfo();
      if (JSONUtils.getJSONArrayParam(wqm02Map, SoilsData.MAP_UNIT_LIST).length() <= 0) {
        V2_0.AoA.setRequiredInputs(new ArrayList<>(Arrays.asList(AoA.AOA_ID)), null, null, null);
        soilData = new V2_0.AoA(soilComponents);
      }
    }
    if (null == soilData) {
      soilData = new V2_0.AoA(JSONUtils.preprocess(soilComponents));
    }
    if (soilData.hasData) {
      soilData.computeComponentNSLP();
      soilData.computeAoANSLP();
      soilData.computeComponentSedNutSRP(soilData.isDrained());
      soilData.computeAoASRP();
    }
  }

  @Override
  //writing the results back to JSON
  protected void postProcess() throws Exception {
    //  Return the result arrays produced by WQM-2, WQM-5, and WQM-6     
    JSONArray outArray = soilData.toJSON();
    for (int i = 0; i < outArray.length(); i++) {
      JSONObject outObject = outArray.getJSONObject(i);
      if (outObject.optString(KEY_NAME, "").equalsIgnoreCase(soils.AoA.MAP_UNIT_LIST)) {
        JSONArray outArray2 = outObject.getJSONArray(KEY_VALUE);
        results().put(soils.AoA.MAP_UNIT_LIST, outArray2);
      } else {
        writeResults(results(), outObject);
      }
    }

    if (null != badSoils) {
      results().put(badSoils.getString(KEY_NAME), badSoils.getJSONArray(KEY_VALUE));
    }

    if (null != badGeometryNote) {
      results().put(badGeometryNote.getString(KEY_NAME), badGeometryNote.getString(KEY_VALUE), badGeometryNote.getString(KEY_DESC));
    }
  }

  protected void writeResults(PayloadResults results, JSONObject outObject) throws Exception {
    results.put(outObject.getString(KEY_NAME), outObject.getString(KEY_VALUE), outObject.getString(KEY_DESC));
  }

  /**
   * Get the full JSON parameter record. Avoids the extra step of preprocessing
   * JSONObjects into ArrayLists of Objects, and fixes the problem of invalid
   * GeoJSON that is inherent in the current JSON design in csip-core due to the
   * heave use of the "value" key.
   *
   * @param name
   * @return the JSON record.
   */
  private JSONObject getParameter(String name) throws ServiceException {
    JSONObject p = getParamMap().get(name);
    if (p == null) {
      throw new ServiceException("Parameter not found: '" + name + "'");
    }
    return p;

  }

  private class AoA extends soils.AoA {

    private boolean hasData = false;

    AoA(Map<String, JSONObject> inputJSON) throws ServiceException, JSONException {
      super(inputJSON);
      hasData = true;
    }

    /**
     * Does NOT require the Map Units for this one. Use this ONLY when WQM-02
     * returned NO map units because of exclusions.
     *
     * @param inputJSON
     * @throws ServiceException
     * @throws JSONException
     */
    AoA(JSONArray inputJSON) throws ServiceException, JSONException {
      super();
      tableAoA.setRequiredColumns(aoaRequiredInputs);
      tableAoA.readValuesFromJSON(inputJSON);
    }

    public boolean hasData() {
      return hasData;
    }

    public JSONArray toJSON() throws JSONException, ServiceException {
      JSONArray outArray = new JSONArray();

      tableAoA.setOutputColumns(new ArrayList<>(Arrays.asList(AoA.AOA_ID, AoA.NSLP, AoA.SRP)));
      setMapUnitOutputColumns(new ArrayList<>(Arrays.asList(TableMapUnit.MUKEY)),
          new ArrayList<>(Arrays.asList(TableComponent.COKEY, TableComponentCalculations.NSLP_NAME, TableComponentCalculations.SRP_NAME)));
      toJSON(false, outArray);

      return outArray;

      //Using put result here because need access to "res" in ModelDataServices.  [Perhaps results() should be protected and not private?]
      //  Because the TableColumn class handles all the specifics of how to format information, and if units, descriptions, etc., were required
      //  for the output JSON, it is all contained and abstracted to avoid confusion for programmers writing simple services.  Thus,
      //  direct access to the ModelDataServices.res member would be hugely benefitial here...
//            for (int i = 0; i < outArray.length(); i++) {
//                putResult(outArray.getJSONObject(i));
//            }
//            if (null != badSoils) {
//                putResult(badSoils);
//            }
//            if (null != badGeometryNote) {
//                putResult(badGeometryNote);
//            }
    }
  }

  class WQMSoilParams extends ServiceCall {

    private final String AoAId;
    private final double minimumPercentage;
    private final JSONObject aoaGeometry;

    WQMSoilParams(String AoAId, double minimumPercentage, boolean comp_drained, JSONObject aoaGeometry, String URI) {
      super(URI);
      this.AoAId = AoAId;
      this.minimumPercentage = minimumPercentage;
      this.aoaGeometry = aoaGeometry;
      errorPrefix = "WQMSoilParams";
    }

    @Override
    protected void createRequest() throws ServiceException {
      JSONArray dataArray;

      requestMetainfoObject = new JSONObject();
      request = new JSONObject();

      dataArray = new JSONArray();
      try {
        requestMetainfoObject.put("MultipartRequest", "Bundled Service Request WQM-21 wqmnutrientslpsrp");
        requestMetainfoObject.put("WQMNutrientSLPSRP_Metainfo", new JSONObject(metainfo().toString()));
        request.put(KEY_METAINFO, requestMetainfoObject);

        dataArray.put(JSONUtils.dataDesc(soils.AoA.AOA_ID, AoAId, "Area of Analysis Identifier"));
        dataArray.put(JSONUtils.dataDesc(AOA_FILTER_PCT, minimumPercentage, "Percent AoA Threshold for Including Soil Components in Result Payload; default is no filter value.  Example 0.10 for 10%."));
        dataArray.put(aoaGeometry);
        request.put(KEY_PARAMETER, dataArray);
      } catch (JSONException ex) {
        throwServiceCallException("Cannot create the JSON request.", ex);
      }
    }
  }
}