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();
}
}
}