V1_0.java [src/java/m/weppws] 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 m.weppws;

import csip.utils.Client;
import csip.Config;
import csip.ModelDataService;
import csip.api.server.Executable;
import static csip.ModelDataService.ERROR;
import static csip.ModelDataService.KEY_METAINFO;
import static csip.ModelDataService.KEY_NAME;
import static csip.ModelDataService.KEY_PARAMETER;
import static csip.ModelDataService.KEY_STATUS;
import static csip.ModelDataService.KEY_VALUE;
import csip.annotations.*;
import csip.api.server.ServiceException;
import static csip.annotations.ResourceType.*;
import csip.utils.JSONUtils;
import csip.utils.Validation;

import java.io.*;
import java.util.logging.Level;
import java.util.ArrayList;
import java.util.Set;
import java.net.URI;
import java.util.Map;
import java.util.logging.Logger;
import java.util.HashSet;
import java.util.List;
import java.util.Iterator;
import java.util.Arrays;
import javax.ws.rs.*;
import static m.weppws.ApplicationResources.*;
import org.apache.commons.io.FileUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import util.WEPPHillslopeServiceCall;
import util.Grid;
import static util.WeppConstants.CONFIG_CLIGEN_PRISM_URL;
//import static util.WeppConstants.CONFIG_CONTOURS_URL;
//import static util.WeppConstants.CONFIG_CROPS_URL;
//import static util.WeppConstants.CONFIG_MANAGEMENTS_URL;
//import static util.WeppConstants.CONFIG_OPERATIONS_URL;
//import static util.WeppConstants.CONFIG_RESIDUES_URL;
import static util.WeppConstants.CONFIG_SOIL_URL;
//import static util.WeppConstants.CONFIG_STRIPS_URL;
import static util.WeppConstants.CONFIG_WEPP_HILLSLOPE_URL;
import static util.WeppConstants.NORTH_D8;
import static util.WeppConstants.NORTH_EAST_D8;
import static util.WeppConstants.EAST_D8;
import static util.WeppConstants.SOUTH_EAST_D8;
import static util.WeppConstants.SOUTH_D8;
import static util.WeppConstants.SOUTH_WEST_D8;
import static util.WeppConstants.WEST_D8;
import static util.WeppConstants.NORTH_WEST_D8;
import static util.WeppConstants.buildConfigKey;
import static util.WeppConstants.GridType;
import static util.WeppConstants.CONV_HA_TO_AC;
import static util.WeppConstants.CONV_M_TO_FT;

@Name("weppws")
@Description("WEPPWS - Watershed service for WEPP TauDEM based - 8/14/2023")
@Path("m/weppws/1.0")
@Polling(first = 2000, next = 2000)

@Resource(file = "/bin/${arch}/wepphillslopeservws.exe", id = "weppserv", type = EXECUTABLE)
@Resource(file = "/bin/${arch}/weppws_2020.exe", id = "wepp", type = EXECUTABLE)
@Resource(file = "/bin/${arch}/weppws_2020_64.exe", id = "wepp64", type = EXECUTABLE)

@Resource(file = "/data/channelDatabase.json", id = "channeldatabase")
@Resource(file = "/data/fallow.rot.txt", id = "fallowchannel")
@Resource(file = "/data/grass.rot.txt", id = "grasschannel")

@Resource(file = "/data/impoundmentDatabase.json", id = "impoundmentdatabase")

// This is the main service for runnign a WEPP spatial fiel simulation of 
// small watershed simulation. Each subctachment will be simulated using a 
// sub-service to try to get better response time. 
//
// This service handles reading any data from other databses (soils, climates,
// managements).
//
public class V1_0 extends ModelDataService {

  protected String modelVersion = "4.1";

  static final String WEPP_PRISM = "usePRISM";
  static final String ADJUSTPWW_PWD = "adjustPWW_PWD";
  static final String WEPP_RAND_SEED = "randSeed";
  static final String WEPP_KEY_CLIMATE_DATA_VERSION = "climateDataVersion";
  static final String LATITUDE = "latitude";
  static final String LONGITUDE = "longitude";
  static final String SIMULATION_TYPE = "simulationType";
  static final String SOIL_DICTIONARY = "soilDictionary";
  static final String MANAGE_DICTIONARY = "managementDictionary";
  static final String D8_GRID = "D8Grid";
  static final String FIELD_GRID = "FieldMaskGrid";
  static final String CHANNEL_GRID = "ChannelGrid";
  static final String SUBCATCHMENT_GRID = "SubcatchmentsGrid";
  static final String FLOWPATH_GRID = "LongestFlowpathGrid";
  static final String SLOPE_GRID = "SlopeD8Grid";
  static final String MANAGE_GRID = "ManagementGrid";
  static final String SOIL_GRID = "SoilGrid";
  static final String LINK_TABLE = "ChannelLinks";
  static final String CRLMOD = "crlmod";
  static final String WEPP_KEY_RUN_YEARS = "run_years";
  static final String IET_ROTATIONS = "rotationFiles";
  static final String STATS_ONLY = "noRunsStatsOnly";
  static final String SUBSET = "flowpathSubsetPercentage";
  static final String SPLIT_SUBCATCHMENTS = "splitSubcatchments";
  static final String CHANNEL_PARMS = "channelParameters";
  static final String CHANNEL_ORDERS = "channelOrders";
  static final String USE_CHANNELS = "usechannels";
  static final String RUNOFF_CALC = "runoffCalculation";
  static final String USE_PASSFILES = "usepassfiles";
  static final String DETAIL_SUBCATCHMENTS = "detailsubcatchments";
  static final String USE_LANDUSE_AS_CHANNEL_MAN = "useLanduseManInChannels";
  static final String APPEND_CHANNEL_DB = "appendChannelDatabase";
  static final String APPLY_IMPOUNDMENTS = "impoundments";
  static final String CHAN_EROD_FLAG = "chanErodFlag";

  double longitude;
  double latitude;
  int subsetPercentage;
  boolean usePRISM;
  boolean adjustPWW_PWD;
  int randomSeedVal;
  int run_years;
  int climateDataVersion;
  String simulationType;
  boolean useRepHillslopes;
  boolean statsOnly;
  String d8Grid;
  String fieldMaskGrid;
  String channelGrid;
  String subcatchmentGrid;
  String flowpathGrid;
  String slopeGrid;
  String manageGrid;
  String soilGrid;
  String channelLinks;
  JSONArray managementDict;
  JSONArray managementDictAll;
  JSONArray soilDict;
  JSONArray soilDictAll;
  String[] managementNames;
  JSONArray managementsWithData;
  int defaultSoil;
  int defaultManagement;
  JSONArray channelOrderData;
  JSONArray channelParameterData;
  String rawChannelDatabaseData;
  int numChannelsUsed;
  boolean use64BitWEPP;
  boolean splitSubcatchments;  // True if each TauDEM subcatchment should be
  // split into 3 areas to emuulate TOPAZ
  boolean parallelRuns;
  JSONArray usrChannelOrderData;
  JSONArray usrChannelParameterData;
  boolean clipflag;
  int[] usechannels;
  int runoffCalculation;
  int chanErodFlag;
  boolean usePassFiles;
  int[] detailsubcatchments;
  boolean useLanduseManInChannels;
  JSONArray appendChannelDB;
  JSONArray impoundments;
  JSONArray impoundmentParameterData;

  Grid flow_d8_grid;
  Grid field_grid;
  Grid slope_d8_grid;
  Grid man_grid;
  Grid soil_grid;
  Grid flowpath_grid;
  Grid subcatchment_grid;
  Grid channel_grid;

  Grid outSoilLoss_grid;
  Grid outSedYield_grid;
  Grid outRunoff_grid;
  Grid outFlowpathLoss_grid;
  Grid sedWeight_grid;
  Grid sub3_grid;

  // define how to handle cells in flowpath output where several merge
  static final int weightMethod = 0;  // 0=weighted based on flowpath length, 1=averaged, 2=maximum

  Set<Integer> subs;

  Watershed aoi;

  protected String soilServiceURL = null;
  protected String climateServiceURL;
//  protected String contourServiceURL = null;
//  protected String manServiceURL = null;
//  protected String stripsServiceURL = null;
//  protected String cropsURL = null;
//  protected String operationsURL = null;
//  protected String residueURL = null;
  protected String weppHillslopeURL = null;
  protected String flowpathURL = null;
  protected String queryURL = null;


  protected void setURLs() throws ServiceException {
    soilServiceURL = getConfigString(buildConfigKey(modelVersion, CONFIG_SOIL_URL));
    climateServiceURL = getConfigString(buildConfigKey(modelVersion, CONFIG_CLIGEN_PRISM_URL));
//    contourServiceURL = getConfigString(buildConfigKey(modelVersion, CONFIG_CONTOURS_URL));
//    manServiceURL = getConfigString(buildConfigKey(modelVersion, CONFIG_MANAGEMENTS_URL));
//    stripsServiceURL = getConfigString(buildConfigKey(modelVersion, CONFIG_STRIPS_URL));
//    cropsURL = getConfigString(buildConfigKey(modelVersion, CONFIG_CROPS_URL));
//    operationsURL = getConfigString(buildConfigKey(modelVersion, CONFIG_OPERATIONS_URL));
//    residueURL = getConfigString(buildConfigKey(modelVersion, CONFIG_RESIDUES_URL));
    weppHillslopeURL = getConfigString(buildConfigKey(modelVersion, CONFIG_WEPP_HILLSLOPE_URL));
    flowpathURL = Config.getString("weppws.flowpath", "");
    queryURL = Config.getString("weppws.queryURL", "");
  }


  protected static String getConfigString(String key) throws ServiceException {
    String ret_val = Config.getString(key);
    if (ret_val == null)
      throw new ServiceException("Configuration parameter, '" + key + "', is missing.  Cannot continue.");

    return ret_val;
  }


  /**
   * init() Initialize WEPP workspace. This requires creating a couple
   * subdirectories. In addition the connections to the sqlite databases are
   * verified.
   *
   */
  private boolean init() throws ServiceException, Exception {
    setURLs();

    aoi = new Watershed(LOG, workspace().getDir(), simulationType.startsWith("Watershed"), this);

    flow_d8_grid = getWSGrid(GridType.INT_GRID, d8Grid, "flowdir_d8.asc");
    field_grid = getWSGrid(GridType.INT_GRID, fieldMaskGrid, "field.asc");
    slope_d8_grid = getWSGrid(GridType.FLOAT_GRID, slopeGrid, "slope_d8.asc");
    flowpath_grid = getWSGrid(GridType.FLOAT_GRID, flowpathGrid, "flowpathlens.asc");
    subcatchment_grid = getWSGrid(GridType.INT_GRID, subcatchmentGrid, "subcatchments.asc");
    channel_grid = getWSGrid(GridType.INT_GRID, channelGrid, "channels.asc");

    man_grid = getWSGrid(GridType.INT_GRID, manageGrid, "managements.asc");
    soil_grid = getWSGrid(GridType.INT_GRID, soilGrid, "soils.asc");

    File linkfilename = workspace().getFile("channel_links.csv");
    if (channelLinks.startsWith("http")) {
      URI fileURI = new URI(channelLinks);
      new Client().doGET(fileURI.toString(), linkfilename);
    } else {
      linkfilename = workspace().getFile(channelLinks);
    }

    readLinksCSV(linkfilename);
    readChannelDatabaseJSON(resources().getFile("channeldatabase"));

    if (impoundments != null)
      readImpoundmentDatabaseJSON(resources().getFile("impoundmentdatabase"));

    applyUsrOrderParameters();
    applyUsrChannelParameters();
    applyAppendChannelDB();

    //buildWEPPFormatChannelData();
    FileUtils.copyFile(resources().getFile("grasschannel"), workspace().getFile("grass.rot"));
    FileUtils.copyFile(resources().getFile("fallowchannel"), workspace().getFile("fallow.rot"));
    //setLocalFile(resources().getFile("grasschannel"),"grass.rot");
    //setLocalFile(resources().getFile("fallowchannel"),"fallow.rot");

    if (slope_d8_grid.verifyFloat(0, 100) == false)
      throw new ServiceException("Slope grid contains bad value.");

    // check that all grids are the same size
    if ((flow_d8_grid.rowCount() != field_grid.rowCount()) || (flow_d8_grid.rowCount() != slope_d8_grid.rowCount())
        || (flow_d8_grid.rowCount() != flowpath_grid.rowCount()) || (flow_d8_grid.rowCount() != subcatchment_grid.rowCount())
        || (flow_d8_grid.rowCount() != channel_grid.rowCount())
        || (flow_d8_grid.rowCount() != man_grid.rowCount()) || (flow_d8_grid.rowCount() != soil_grid.rowCount())
        || (flow_d8_grid.colCount() != field_grid.colCount()) || (flow_d8_grid.colCount() != slope_d8_grid.colCount())
        || (flow_d8_grid.colCount() != flowpath_grid.colCount()) || (flow_d8_grid.colCount() != subcatchment_grid.colCount())
        || (flow_d8_grid.colCount() != channel_grid.colCount())
        || (flow_d8_grid.colCount() != man_grid.colCount()) || (flow_d8_grid.colCount() != soil_grid.colCount())) {
      throw new ServiceException("Error: Input grids do not all have the same number of rows and columns.");
    }

    LOG.info("one col:" + slope_d8_grid.getFloatRowCol(0, 0));
    return true;
  }


  /**
   * preProcess() Get input parameters from WEPP request.
   *
   * @throws Exception
   */
  @Override
  protected void preProcess() throws Exception {
    latitude = parameter().getDouble(LATITUDE);
    longitude = parameter().getDouble(LONGITUDE);

    usePRISM = parameter().getBoolean(WEPP_PRISM, false);
    adjustPWW_PWD = parameter().getBoolean(ADJUSTPWW_PWD, false);
    randomSeedVal = parameter().getInt(WEPP_RAND_SEED, -1);
    run_years = parameter().getInt(WEPP_KEY_RUN_YEARS, 100);
    climateDataVersion = parameter().getInt(WEPP_KEY_CLIMATE_DATA_VERSION, 1992);

    simulationType = parameter().getString(SIMULATION_TYPE);
    Validation.checkString(simulationType, "Watershed", "Field", "Hillslope", "Watershed-Flowpaths");

    statsOnly = parameter().getBoolean(STATS_ONLY, false);

    // management dictionary
    // soils dictionary
    soilDict = parameter().getJSONArray(SOIL_DICTIONARY);
    managementDict = parameter().getJSONArray(MANAGE_DICTIONARY);
    use64BitWEPP = parameter().getBoolean(USE64BITWEPP, false);

    // input grids
    d8Grid = parameter().getString(D8_GRID);
    fieldMaskGrid = parameter().getString(FIELD_GRID);
    channelGrid = parameter().getString(CHANNEL_GRID);
    subcatchmentGrid = parameter().getString(SUBCATCHMENT_GRID);
    flowpathGrid = parameter().getString(FLOWPATH_GRID);
    slopeGrid = parameter().getString(SLOPE_GRID);
    manageGrid = parameter().getString(MANAGE_GRID);
    soilGrid = parameter().getString(SOIL_GRID);
    channelLinks = parameter().getString(LINK_TABLE);
    subsetPercentage = parameter().getInt(SUBSET, 100);
    splitSubcatchments = parameter().getBoolean(SPLIT_SUBCATCHMENTS, false);
//    parallelRuns = parameter().getBoolean(PARALLEL_RUNS, true);
    parallelRuns = Config.getBoolean("weppws.parallel", false);
    usrChannelOrderData = parameter().getJSONArray(CHANNEL_ORDERS, null);
    usrChannelParameterData = parameter().getJSONArray(CHANNEL_PARMS, null);
    usechannels = parameter().getIntArray(USE_CHANNELS, new int[0]);
    detailsubcatchments = parameter().getIntArray(DETAIL_SUBCATCHMENTS, new int[0]);
    usePassFiles = parameter().getBoolean(USE_PASSFILES, false);
    useLanduseManInChannels = parameter().getBoolean(USE_LANDUSE_AS_CHANNEL_MAN, false);
    appendChannelDB = parameter().getJSONArray(APPEND_CHANNEL_DB, null);
    impoundments = parameter().getJSONArray(APPLY_IMPOUNDMENTS, null);
    String runCalc = parameter().getString(RUNOFF_CALC, "Modified EPIC");
    chanErodFlag =  parameter().getInt(CHAN_EROD_FLAG, 0);

    switch (runCalc) {
      case "Modified EPIC":
        runoffCalculation = 1;
        break;
      case "CREAMS":
        runoffCalculation = 2;
        break;
      case "Kinematic Wave":
        runoffCalculation = 3;
        break;
      case "Muskingum-Cunge(constant)":
        runoffCalculation = 4;
        break;
      case "Muskingum-Cunge(variable)":
        runoffCalculation = 5;
        break;
      default:
        throw new IllegalArgumentException("for " + RUNOFF_CALC + ":" + runCalc);
    }

    defaultSoil = 0;
    defaultManagement = 0;

    init();

    //aoi.dumpChannels();
  }


  void progress(String msg) {
    synchronized (this) {
      try {
        setProgress(msg);
      } catch (ServiceException ex) {
        Logger.getLogger(V1_0.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
  }


  /**
   * process() Calls functions to build the WEPP input files and then run WEPP
   *
   * @throws Exception
   */
  @Override
  protected void doProcess() throws Exception {
    
    getClimateFile();
    getSoilFiles();
    getManagementFiles();
    
    findSubcatchmentsInAOI();
    findFlowpaths();
    findChannels();

    aoi.setChannelFlowpaths();
    aoi.connectChannels();

    if (usechannels.length > 0) {
      // only a portion of the field/watershed area is being used so the
      // field mask needs to be updated. First determine the ids to include 
      // in the mask.
      aoi.labelWEPPChannelsSubarea(usechannels[0]);

      // update the mask to only include the areas identified.
      field_grid = aoi.createSubareaMask(field_grid, subcatchment_grid);
    } else {
      if (simulationType.startsWith("Watershed"))
        aoi.labelWEPPChannels();
    }
    
    aoi.findMajorComponents(subcatchment_grid, channel_grid, soil_grid, man_grid, field_grid);

    // set to include all subcatchments
    if (usechannels.length == 0)
      aoi.setInuseSubcatchments();

    aoi.calcArea(field_grid);
 
    buildWEPPFormatChannelData();

    if (splitSubcatchments) {
      aoi.setTopazIDs();
      sub3_grid = new Grid(subcatchment_grid, GridType.INT_GRID, 0);
      aoi.classifyFlowpaths();
      LOG.info("Creating sub3 grid.");
      aoi.createsub3grid(sub3_grid);
      sub3_grid.writeGrid(workspace().getFile("topazSubcatchments.asc"));
      aoi.findMajorComponentsTopaz(sub3_grid, soil_grid, man_grid, field_grid);
    }

    applyImpoundments();

    Executable weppserv = resources().getExe("weppserv");

    // Run an external program to create all the model inputs and run wepp
    // turn on calibration flag
    if (simulationType.startsWith("Watershed") || (usechannels.length > 0)) {
      weppserv.setArguments(workspace().getDir(),
          workspace().getFile("wepp.prw"),
          workspace().getFile("output/weppout.txt"),
          resources().getFile(use64BitWEPP ? "wepp64" : "wepp"), "0", 0, 0, 1);
    } else {
      weppserv.setArguments(workspace().getDir(),
          workspace().getFile("wepp.prj"),
          workspace().getFile("output/weppout.txt"),
          resources().getFile(use64BitWEPP ? "wepp64" : "wepp"), "0", 0, 0, 1);
    }

    switch (simulationType) {
      case "Field":
        int totalRuns;
        if (detailsubcatchments.length == 0) {
          totalRuns = aoi.runFlowpaths(weppserv, field_grid, statsOnly, subsetPercentage, parallelRuns);
        } else {
          totalRuns = aoi.runFlowpathsSubset(weppserv, field_grid, statsOnly, subsetPercentage, parallelRuns, detailsubcatchments);
        }
        LOG.info("***** Total number of flowpath runs" + totalRuns);
        totalRuns = aoi.runRepresentativeHillslopes(weppserv, field_grid, statsOnly, parallelRuns, splitSubcatchments, false);
        LOG.info("***** Total number of representative hillslope runs (field simulation): " + totalRuns);
        break;
      case "Hillslope":
        if (usechannels.length == 0) {
          totalRuns = aoi.runRepresentativeHillslopes(weppserv, field_grid, statsOnly, parallelRuns, splitSubcatchments, false);
        } else {
          totalRuns = aoi.runRepresentativeHillslopes(weppserv, field_grid, true, parallelRuns, splitSubcatchments, usePassFiles);
          aoi.runWatershedSubarea(weppserv, statsOnly, usechannels[0], usePassFiles);
        }
        LOG.info("***** Total number of representative hillslope runs (hillslope simulation): " + totalRuns);
        break;
      case "Watershed":
        totalRuns = aoi.runRepresentativeHillslopes(weppserv, field_grid, true, parallelRuns, splitSubcatchments, usePassFiles);
        if (usechannels.length == 0) {
          aoi.runWatershed(weppserv, statsOnly, usePassFiles);
        } else {
          aoi.runWatershedSubarea(weppserv, statsOnly, usechannels[0], usePassFiles);
        }
        LOG.info("***** Total number of representative hillslope runs: " + totalRuns);
        break;
      case "Watershed-Flowpaths":
        // setup watershed inputs for WEPP      
        totalRuns = aoi.runRepresentativeHillslopes(weppserv, field_grid, true, parallelRuns, splitSubcatchments, usePassFiles);
        LOG.info("***** Total number of representative hillslope runs: " + totalRuns);

        if (usechannels.length == 0) {
          aoi.runWatershed(weppserv, statsOnly, usePassFiles);
        } else {
          aoi.runWatershedSubarea(weppserv, statsOnly, usechannels[0], usePassFiles);
        }
        // For flowpaths setup hillslope inputs for WEPP
        weppserv.setArguments(workspace().getDir(),
            workspace().getFile("wepp.prj"),
            workspace().getFile("output/weppout.txt"),
            resources().getFile(use64BitWEPP ? "wepp64" : "wepp"), "0", 0, 0, 1);
        
        if (detailsubcatchments.length == 0) {
          totalRuns = aoi.runFlowpaths(weppserv, field_grid, statsOnly, subsetPercentage, parallelRuns);
        } else {
          totalRuns = aoi.runFlowpathsSubset(weppserv, field_grid, statsOnly, subsetPercentage, parallelRuns, detailsubcatchments);
        }
        LOG.info("***** Total number of watershed flowpath runs: " + totalRuns);
        break;
      default:
        throw new IllegalArgumentException("simulationType: " + simulationType);
    }

    if (usechannels.length > 0)
      aoi.filterSubarea();

    if (!statsOnly) {
      createOutputGrids();

      outSoilLoss_grid.writeGrid(workspace().getFile("rephillslope.asc"));
      outSedYield_grid.writeGrid(workspace().getFile("repsedleaving.asc"));
      outRunoff_grid.writeGrid(workspace().getFile("reprunoff.asc"));
      if (weightMethod == 2) {
        // no weighting, just the maximum loss at each cell
        outFlowpathLoss_grid.writeGrid(workspace().getFile("flowpathloss.asc"));
      } else {
        // need to first apply the weight grid to get the actual loss values
        outFlowpathLoss_grid.writeWeightedGrid(sedWeight_grid, workspace().getFile("flowpathloss.asc"));
      }
    }

    aoi.writeOutputSummary(workspace().getFile("outputsummary.json"));
    aoi.writeInputSummary(workspace().getFile("inputsummary.json"));
    aoi.dumpSubcatchments();
    removeFiles();
  }


  /**
   * postProcess() Extract some WEPP outputs and include links to model input
   * and output files.
   *
   * @throws Exception
   */
  @Override
  protected void postProcess() throws Exception {
    metainfo().put("WEPPVersion", aoi.weppVersion);
    metainfo().put("PreprocessorVersion", aoi.preVersion);

    results().put("Precipitation", aoi.precip, "Average Annual Preciptation", "in");

    if (simulationType.equals("Hillslope")) {
      results().put("Field Irrigation", aoi.irrigation, "Average annual irrigation from all representative hillslopes in field", "ac-in");
      results().put("Field Hillslope Erosion", aoi.fieldErosion, "Average annual soil loss from all representative hillslopes in field", "ton/ac/yr");
      results().put("Field NRCS Soil Loss", aoi.fieldNRCSErosion, "NRCS Planning soil loss from all representative hillslopes in field", "ton/ac/yr");
      results().put("Field Sediment Yield", aoi.fieldSedYield, "Average annual sediment yield from all representative hillslopes in field", "ton/ac/yr");
    }

    if (simulationType.startsWith("Watershed")) {
      results().put("Precipitation Volume", aoi.precipVol, "Average Annual Preciptation Volume", "ft^3");
      results().put("Discharge", aoi.discharge, "Average Annual Discharge at Outlet", "in");
      results().put("Discharge Volume", aoi.dischargeVol, "Average Annual Discharge Volume at Outlet", "ft^3");
      results().put("Sediment Yield", aoi.sedimentYld, "Average Annual Sediment Yield", "ton");
      results().put("Sediment Yield Per Acre", aoi.sedimentYldArea, "Average Annual Sediment Yield per Area", "ton/ac");
      results().put("Delivery Ratio", aoi.delRatio, "Sediment Delivery Ratio", "");
    }
    results().put("Area", aoi.area * CONV_HA_TO_AC, "Watershed Area", "ac");

    if (simulationType.startsWith("Watershed")) {
      JSONArray chans = new JSONArray();
      for (int i = 0; i < aoi.channels.size(); i++) {
        if (aoi.channels.get(i).weppID > 0) {
          JSONObject jo = new JSONObject();
          jo.put("TAUDEM_ID", aoi.channels.get(i).id);
          jo.put("WEPP_ID", aoi.channels.get(i).weppID);
          jo.put("LENGTH_FT", aoi.channels.get(i).length);
          jo.put("WIDTH_FT", aoi.channels.get(i).width);
          jo.put("SOIL_LOSS_TON", aoi.channels.get(i).loss);
          jo.put("SED_YIELD_TON", aoi.channels.get(i).sedYield);
          jo.put("DISCHARGE_FT3", aoi.channels.get(i).discharge);
          jo.put("INTERSECTS_FIELD", 0);
          chans.put(jo);
        }
      }
      results().put("Channels", chans, "Average Annual Channel Sediment Yields and Discharges", "ft,ton,ft^3");
    }

    JSONArray hills = new JSONArray();
    for (int i = 0; i < aoi.subcatchments.size(); i++) {
      Subcatchment mysub = aoi.subcatchments.get(i);
      if (!splitSubcatchments) {
        if (mysub.weppid > 0) {
          JSONObject jo = new JSONObject();
          jo.put("TAUDEM_ID", mysub.id);
          jo.put("WEPP_ID", mysub.weppid);
          if (mysub.repHillslopeFlowpath != null) {
            jo.put("LENGTH_FT", mysub.repHillslopeFlowpath.totalLengthMeters * CONV_M_TO_FT);
          } else {
            jo.put("LENGTH_FT", 0);
          }
          jo.put("WIDTH_FT", mysub.repHillslopeWidth * CONV_M_TO_FT);
          jo.put("RUNOFF_IN", mysub.avgRunoff);
          jo.put("SOIL_LOSS_TONAC", mysub.avgSoilLoss);
          jo.put("SED_YIELD_TONAC", mysub.avgSedYield);

          jo.put("NRCSSOILLOSS_TONAC", mysub.nrcssoilloss);
          jo.put("IRRIGATION_IN", mysub.irrigation);
          jo.put("IRRIGATION_FT3", mysub.irrigationVol);
          jo.put("INTERSECTS_FIELD", 0);
          hills.put(jo);
        }
      } else {
        SubcatchmentSubarea mysubsub;
        for (int j = 0; j < 3; j++) {
          if (j == 0) {
            mysubsub = mysub.leftsub;
          } else if (j == 1) {
            mysubsub = mysub.rightsub;
          } else {
            mysubsub = mysub.topsub;
          }
          if (mysubsub != null) {
            JSONObject jo = new JSONObject();
            jo.put("TAUDEM_ID", mysub.id);
            jo.put("WEPP_ID", mysubsub.weppid);
            jo.put("WEPPALTID", mysubsub.weppAltID);
            jo.put("ORIENTATION", mysubsub.getOrientation());
            if (mysubsub.repHillslopeFlowpath != null) {
              jo.put("LENGTH_FT", mysubsub.repHillslopeFlowpath.totalLengthMeters * CONV_M_TO_FT);
            } else {
              jo.put("LENGTH_FT", 0);
            }
            jo.put("WIDTH_FT", mysubsub.repHillslopeWidth * CONV_M_TO_FT);
            jo.put("RUNOFF_IN", mysubsub.avgRunoff);
            jo.put("SOIL_LOSS_TONAC", mysubsub.avgSoilLoss);
            jo.put("SED_YIELD_TONAC", mysubsub.avgSedYield);

            jo.put("NRCSSOILLOSS_TONAC", mysub.nrcssoilloss);
            jo.put("IRRIGATION_IN", mysub.irrigation);
            jo.put("IRRIGATION_FT3", mysub.irrigationVol);
            jo.put("INTERSECTS_FIELD", 0);

            jo.put("FLOWPATHS_FOUND", mysubsub.numFlowpaths);
            jo.put("GRID_CELLS", mysubsub.cells);
            hills.put(jo);
          }
        }
      }
    }
    results().put("Hillslopes", hills, "Average Annual Representative Hillslopes Runoff, Soil Loss and Sediment Yields", "in, ton/ac, ton/ac");

    putResultIfFileExist("inputsummary.json", "Watershed/Field Inputs summary");
    putResultIfFileExist("outputsummary.json", "Watershed/Field Output summary");

    // flowpathloss grid is only available for flowpath runs
    if (simulationType.equals("Watershed-Flowpaths") || simulationType.equals("Field"))
      putResultIfFileExist("flowpathloss.asc", "Watershed/Field flowpath soil loss results (ton/ac/yr)");

    // These grids are available from all simulations
    putResultIfFileExist("rephillslope.asc", "Watershed/Field representative hillslope soil loss results (ton/ac/yr)");
    putResultIfFileExist("repsedleaving.asc", "Watershed/Field representative hillslope sediment leaving (ton/ac/yr)");
    putResultIfFileExist("reprunoff.asc", "Watershed/Field representative hillslope runoff (in/yr)");
    
    if (simulationType.equals("Watershed-Flowpaths") || simulationType.equals("Watershed")) {
        putResultIfFileExist("output/loss_pw0.txt", "WEPP model watershed main text output");
    }

    if (splitSubcatchments)
      putResultIfFileExist("topazSubcatchments.asc", "Subcatchments split, TOPAZ emulation");

  }


  private void putResultIfFileExist(String file, String description) {
    File f = workspace().getFile(file);
    if (f.exists())
      results().put(f, description);
    else
      LOG.info("Missing: " + file);
  }


  void removeFiles() {
    // only keep files really needed, archive can get too large otherwise
    aoi.removeFiles();
  }


  void createOutputGrids() throws ServiceException {
    // create empty grids the same size as the subcatchment grid
    outSoilLoss_grid = new Grid(subcatchment_grid, GridType.FLOAT_GRID, -1);
    outSedYield_grid = new Grid(subcatchment_grid, GridType.FLOAT_GRID, -1);
    outRunoff_grid = new Grid(subcatchment_grid, GridType.FLOAT_GRID, -1);
    outFlowpathLoss_grid = new Grid(subcatchment_grid, GridType.FLOAT_GRID, -999);
    sedWeight_grid = new Grid(subcatchment_grid, GridType.FLOAT_GRID, 0);

    if (!splitSubcatchments) {
      aoi.createSoilLossGrid(outSoilLoss_grid, subcatchment_grid);
      aoi.createSedYieldGrid(outSedYield_grid, subcatchment_grid);
      aoi.createRunoffGrid(outRunoff_grid, subcatchment_grid);
    } else {
      aoi.createSoilLossGridSubareas(outSoilLoss_grid, sub3_grid);
      aoi.createSedYieldGridSubareas(outSedYield_grid, sub3_grid);
      aoi.createRunoffGridSubareas(outRunoff_grid, sub3_grid);
    }
    aoi.createFlowpathLossGrid(outFlowpathLoss_grid, sedWeight_grid);
    channel_grid.maskChannels(outSoilLoss_grid, outSedYield_grid, outRunoff_grid, -1);
  }


  void readLinksCSV(File csvfile) throws ServiceException {
    String row;
    int line = 0;
    int chanIDCol = -1;
    int downCol = -1;
    int up1Col = -1;
    int up2Col = -1;
    int lastCol = -1;
    int orderCol = -1;

    try (BufferedReader csvReader = new BufferedReader(new FileReader(csvfile))) {
      while ((row = csvReader.readLine()) != null) {
        String[] data = row.split(",");
        if (line == 0) {
          // header
          for (int i = 0; i < data.length; i++) {
            if (data[i].equals("LINKNO")) {
              chanIDCol = i;
              if (i > lastCol) {
                lastCol = i;
              }

            } else if (data[i].equals("DSLINKNO")) {
              downCol = i;
              if (i > lastCol) {
                lastCol = i;
              }

            } else if (data[i].equals("USLINKNO1")) {
              up1Col = i;
              if (i > lastCol) {
                lastCol = i;
              }

            } else if (data[i].equals("USLINKNO2")) {
              up2Col = i;
              if (i > lastCol) {
                lastCol = i;
              }

            } else if (data[i].equals("strmOrder")) {
              orderCol = i;
              if (i > lastCol) {
                lastCol = i;
              }

            }
          }
          if ((chanIDCol < 0) || (downCol < 0) || (up1Col < 0) || (up2Col < 0)) {
            throw new ServiceException("Channel link file missing one or more required columns: LINKNO,DSLINKNO,USLINKNO1,USLINKNO2");
          }
        } else {
          // regular line of data
          if (data.length > (lastCol)) {
            int id = Integer.parseInt(data[chanIDCol]);
            int downs = Integer.parseInt(data[downCol]);
            int up1 = Integer.parseInt(data[up1Col]);
            int up2 = Integer.parseInt(data[up2Col]);
            // may not have stream order column
            int order = (orderCol > 0) ? Integer.parseInt(data[orderCol]) : 1;
            Channel ch = new Channel(id, downs, up1, up2, order, this);
            aoi.addChannel(ch);

          } else {
            throw new ServiceException("Missing columns in channel link file.");
          }
        }
        line++;
      }
    } catch (Exception e) {
      throw new ServiceException("Error reading file " + csvfile.getAbsolutePath());
    }
  }


  Grid getWSGrid(GridType type, String gridURL, String localFilename) throws Exception {
    File gridfilename;
    if (gridURL.startsWith("http")) {
      gridfilename = workspace().getFile(localFilename);
      URI gridURI = new URI(gridURL);
      new Client().doGET(gridURI.toString(), gridfilename);
    } else {
      gridfilename = workspace().getFile(gridURL);
    }
    Grid g = new Grid(type, gridfilename.getAbsolutePath());
    return g;
  }


  Set<Integer> findSubcatchmentsInAOI() throws ServiceException {
    // need to look in all cells enclosed in field mask and get unique
    // values from subcatchments grid.
    subs = field_grid.intersect(subcatchment_grid);
    int idx = 1;
    for (Integer sub : subs) {
      Subcatchment s = new Subcatchment(sub, this, idx++);
      aoi.addSubcatchment(s);
    }
    LOG.info("SUBCATCHMENTS>> " + subs.toString());
    return subs;
  }


  int findFlowpaths() throws ServiceException {
    int num = 0;
    float fdata[][] = flowpath_grid.getFloatData();
    int subdata[][] = subcatchment_grid.getIntData();

    for (int i = 0; i < flowpath_grid.rowCount(); i++) {
      for (int j = 0; j < flowpath_grid.colCount(); j++) {
        if (fdata[i][j] == 0) {  // start of a flowpath
          int whatsub = subdata[i][j];
          if (subs.contains(whatsub)) {
            // is within area of interest
            Subcatchment s = aoi.getSubcatchment(whatsub);
            walkFlowpath(s, i, j);
            num++;
          }
        }
      }
    }
    return num;
  }


  int findChannels() throws ServiceException {
    int cdata[][] = channel_grid.getIntData();
    int nodata = channel_grid.getNoData();
    Set<Integer> hitChans = new HashSet<>();
    int num = 0;

    for (int i = 0; i < channel_grid.rowCount(); i++) {
      for (int j = 0; j < channel_grid.colCount(); j++) {
        if (cdata[i][j] != nodata) {  // part of a channel 
          // determine if this channel has already been processed
          if (!hitChans.contains(cdata[i][j])) {
            // hit a channel cell, find start
            findChannelHead(i, j, cdata[i][j]);
            // mark this channel as complete
            hitChans.add(cdata[i][j]);
            num++;
          }
        }
      }
    }
    
    return num;
  }


  void findChannelHead(int r, int c, int id) throws ServiceException {
    int cdata[][] = channel_grid.getIntData();
    int d8[][] = flow_d8_grid.getIntData();
    int row = r;
    int col = c;
    int lastRow, lastCol;
    boolean moved;
    boolean addThisChannel;
    boolean d8neighbors = true;

    int maxrows = flow_d8_grid.rowCount();
    int maxcols = flow_d8_grid.colCount();
    int d8nodata = flow_d8_grid.getNoData();

    // Look at the neighboring cells to see which ones are potential upstream from 
    // this cell.
    while (d8neighbors) {
      lastRow = row;
      lastCol = col;
      moved = false;
      if (((row - 1) >= 0) && ((col - 1) >= 0)) {
        if (cdata[row - 1][col - 1] == id) {
          if (d8[row - 1][col - 1] == SOUTH_EAST_D8) {
            row = row - 1;
            col = col - 1;
            moved = true;
          }
        }
      }
      if (moved == false) {
        if ((row - 1) >= 0) {
          if (cdata[row - 1][col] == id) {
            if (d8[row - 1][col] == SOUTH_D8) {
              row = row - 1;
              moved = true;
            }
          }
        }
      }
      if (moved == false) {
        if (((row - 1) >= 0) && ((col + 1) < maxcols)) {
          if (cdata[row - 1][col + 1] == id) {
            if (d8[row - 1][col + 1] == SOUTH_WEST_D8) {
              row = row - 1;
              col = col + 1;
              moved = true;
            }
          }
        }
      }
      if (moved == false) {
        if ((col + 1) < maxcols) {
          if (cdata[row][col + 1] == id) {
            if (d8[row][col + 1] == WEST_D8) {
              col = col + 1;
              moved = true;
            }
          }
        }
      }
      if (moved == false) {
        if (((row + 1) < maxrows) && ((col + 1) < maxcols)) {
          if (cdata[row + 1][col + 1] == id) {
            if (d8[row + 1][col + 1] == NORTH_WEST_D8) {
              row = row + 1;
              col = col + 1;
              moved = true;
            }
          }
        }
      }
      if (moved == false) {
        if ((row + 1) < maxrows) {
          if (cdata[row + 1][col] == id) {
            if (d8[row + 1][col] == NORTH_D8) {
              row = row + 1;
              moved = true;
            }
          }
        }
      }
      if (moved == false) {
        if (((row + 1) < maxrows) && ((col - 1) >= 0)) {
          if (cdata[row + 1][col - 1] == id) {
            if (d8[row + 1][col - 1] == NORTH_EAST_D8) {
              row = row + 1;
              col = col - 1;
              moved = true;
            }
          }
        }
      }
      if (moved == false) {
        if ((col - 1) >= 0) {
          if (cdata[row][col - 1] == id) {
            if (d8[row][col - 1] == EAST_D8) {
              col = col - 1;
              moved = true;
            }
          }
        }
      }

      addThisChannel = false;
      if (moved == false) {
        // reached starting/head cell, none above this cell
        addThisChannel = true;
      } else {
        if ((col < 0) || (row < 0) || (col >= maxcols) || (row >= maxrows)) {
          // ran off grid, use last position
          addThisChannel = true;
        } else {
          if (d8[row][col] == d8nodata) {
            // ran off edge of field mask, use last position
            addThisChannel = true;
          }
        }
      }
      if (addThisChannel) {
        d8neighbors = false;
        // add start position for channel
        if ((lastRow == 0) && (lastCol == 0)) {
          aoi.setChannelStart(id, -1, -1);
        }
        aoi.setChannelStart(id, lastRow, lastCol);
      }
    }
  }


  void walkFlowpath(Subcatchment s, int row, int col) throws ServiceException {
    int cgrid[][] = channel_grid.getIntData();
    int d8[][] = flow_d8_grid.getIntData();
    float slpgrid[][] = slope_d8_grid.getFloatData();
    int sgrid[][] = soil_grid.getIntData();
    int mgrid[][] = man_grid.getIntData();

    List<Integer> rows = new ArrayList<>();
    List<Integer> cols = new ArrayList<>();
    List<Float> dist = new ArrayList<>();
    List<Float> slp = new ArrayList<>();
    List<Integer> man = new ArrayList<>();
    List<Integer> soil = new ArrayList<>();

    float cellsize = (float) channel_grid.getCellsize();

    float cellsizediag = (float) Math.sqrt((double) ((cellsize * cellsize) + (cellsize * cellsize)));
    int nodata = channel_grid.getNoData();
    int d8nodata = flow_d8_grid.getNoData();
    int soilnodata = soil_grid.getNoData();
    int mannodata = man_grid.getNoData();
    int maxrows = flow_d8_grid.rowCount();
    int maxcols = flow_d8_grid.colCount();

    boolean walking = true;
    while (walking) {
      rows.add(row);
      cols.add(col);
      slp.add(slpgrid[row][col]);
      if (sgrid[row][col] == soilnodata) {
        soil.add(defaultSoil);
      } else {
        soil.add(sgrid[row][col]);
      }
      if (mgrid[row][col] == mannodata) {
        man.add(defaultManagement);
      } else {
        man.add(mgrid[row][col]);
      }

      switch (d8[row][col]) {
        case NORTH_D8:
          dist.add(cellsize);
          row--;
          break;
        case NORTH_EAST_D8:
          dist.add(cellsizediag);
          row--;
          col++;
          break;
        case EAST_D8:
          dist.add(cellsize);
          col++;
          break;
        case SOUTH_EAST_D8:
          dist.add(cellsizediag);
          row++;
          col++;
          break;
        case SOUTH_D8:
          dist.add(cellsize);
          row++;
          break;
        case SOUTH_WEST_D8:
          dist.add(cellsizediag);
          row++;
          col--;
          break;
        case WEST_D8:
          dist.add(cellsize);
          col--;
          break;
        case NORTH_WEST_D8:
          dist.add(cellsizediag);
          row--;
          col--;
          break;
      }
      // check if we hit a channel or went out of bounds
      if ((row < 0) || (col < 0) || (row >= maxrows) || (col >= maxcols)) {
        s.addFlowpath(rows, cols, slp, man, soil, dist, -1, -1, -1);
        walking = false;
      } else {
        if ((cgrid[row][col] != nodata) || (d8[row][col] == d8nodata)) {
          s.addFlowpath(rows, cols, slp, man, soil, dist, row, col, cgrid[row][col]);
          walking = false;
        }
      }
    }
  }


  void walkChannelFlowpath(Channel s) throws ServiceException {
    int cgrid[][] = channel_grid.getIntData();
    int d8[][] = flow_d8_grid.getIntData();
    float slpgrid[][] = slope_d8_grid.getFloatData();
    int sgrid[][] = soil_grid.getIntData();
    int mgrid[][] = man_grid.getIntData();

    List<Integer> rows = new ArrayList<>();
    List<Integer> cols = new ArrayList<>();
    List<Float> dist = new ArrayList<>();
    List<Float> slp = new ArrayList<>();
    List<Integer> man = new ArrayList<>();
    List<Integer> soil = new ArrayList<>();

    float cellsize = (float) channel_grid.getCellsize();
    float cellsizediag;
    int row, col;
    int maxrows, maxcols;
    int MAX_CELLS_IN_FLOWPATH = 1000;

    row = s.startRow;
    col = s.startCol;
    if ((row == -1) && (col == -1)) {
        LOG.info("Channel listed in links.csv not found in channel grid, looking in subcatchment grid");
        // see if it listed in the subcatchment grid.
        int subgrid[][] = subcatchment_grid.getIntData();
        for (int i = 0; i < subcatchment_grid.rowCount(); i++) {
            for (int j = 0; j < subcatchment_grid.colCount(); j++) {
                 if (subgrid[i][j] == s.id) {  // part of a channel 
                     s.startRow = i;
                     s.startCol = j;
                     row = i;
                     col = j;
                 }
            }
        }
    }

    if ((row == -1) && (col == -1)) {
       LOG.info("Channel Flowpath inconsistent for subcatchment: " + s.id + " links.csv lists channel but none found.");
       rows.add(0);
       cols.add(0);
       slp.add((float)0.001);
       soil.add(defaultSoil);
       man.add(defaultManagement);
       dist.add(cellsize);
       s.addFlowpath(rows, cols, slp, man, soil, dist);
       LOG.info("Added pseudo channel soil: " + defaultSoil + " " + defaultManagement);
       return;
    }
    
    maxrows = flow_d8_grid.rowCount();
    maxcols = flow_d8_grid.colCount();

    boolean walking = true;

    cellsizediag = (float) Math.sqrt((double) ((cellsize * cellsize) + (cellsize * cellsize)));
    int nodata = channel_grid.getNoData();
    int d8nodata = flow_d8_grid.getNoData();
    int soilnodata = soil_grid.getNoData();
    int mannodata = man_grid.getNoData();
    
    s.addFlowpath(rows, cols, slp, man, soil, dist);

    while (walking) {
      if (rows.size() > MAX_CELLS_IN_FLOWPATH) {
        throw new ServiceException("Channel Flowpath inconsistent for subcatchment: " + s.id + " starting from row: " + s.startRow + " and column: " + s.startCol);
      }
      rows.add(row);
      cols.add(col);
      slp.add(slpgrid[row][col]);

      if (sgrid[row][col] == soilnodata) {
        if (defaultSoil == soilnodata) {
          throw new ServiceException("Channel Flowpath soil inconsistent for subcatchment: " + s.id + " starting from row: " + s.startRow + " and column: " + s.startCol);
        }
        soil.add(defaultSoil);
      } else {
        soil.add(sgrid[row][col]);
      }

      if (mgrid[row][col] == mannodata) {
        if (defaultManagement == mannodata) {
          throw new ServiceException("Channel Flowpath management inconsistent for subcatchment: " + s.id + " starting from row: " + s.startRow + " and column: " + s.startCol);
        }
        man.add(defaultManagement);
      } else {
        man.add(mgrid[row][col]);
      }

      switch (d8[row][col]) {
        case NORTH_D8:
          dist.add(cellsize);
          row--;
          break;
        case NORTH_EAST_D8:
          dist.add(cellsizediag);
          row--;
          col++;
          break;
        case EAST_D8:
          dist.add(cellsize);
          col++;
          break;
        case SOUTH_EAST_D8:
          dist.add(cellsizediag);
          row++;
          col++;
          break;
        case SOUTH_D8:
          dist.add(cellsize);
          row++;
          break;
        case SOUTH_WEST_D8:
          dist.add(cellsizediag);
          row++;
          col--;
          break;
        case WEST_D8:
          dist.add(cellsize);
          col--;
          break;
        case NORTH_WEST_D8:
          dist.add(cellsizediag);
          row--;
          col--;
          break;
      }
      // check if we hit the end of this channel
      if ((row < 0) || (col < 0) || (row >= maxrows) || (col >= maxcols)) {
        s.addFlowpath(rows, cols, slp, man, soil, dist);
        walking = false;
      } else {
        if ((cgrid[row][col] != s.id) || (d8[row][col] == d8nodata)) {
          s.addFlowpath(rows, cols, slp, man, soil, dist);
          walking = false;
        }
      }
    }
  }


  /**
   * Calls the CSIP service to run CLIGEN and return a climate file with
   * optional PRISM adjustments.
   *
   * @throws JSONException parsing JSON errors
   * @throws ServiceException CSIP message to return if failed.
   * @throws Exception file writing errors
   */
  protected void getClimateFile() throws JSONException, ServiceException, Exception {
    JSONObject climateRequest = new JSONObject();
    JSONObject climateResponse;
    JSONObject meta = new JSONObject();
    JSONArray paramObj = new JSONArray();
    JSONObject inputFeature = new JSONObject();
    JSONObject collection = new JSONObject();
    JSONArray features = new JSONArray();
    JSONObject feature = new JSONObject();
    JSONObject properties = new JSONObject();
    JSONObject geometry = new JSONObject();
    JSONArray coordinates = new JSONArray();
    JSONObject prism = new JSONObject();
    JSONObject dataVersion = new JSONObject();
    JSONObject runYears = new JSONObject();
    JSONObject fileName = new JSONObject();
    JSONObject jparFile = new JSONObject();
    JSONObject adjustProb = new JSONObject();
    JSONObject randomSeed = new JSONObject();

    URI parUri;
    File parFile;

    Map<String, JSONObject> resultMap;

    meta.put("mode", "sync");

    climateRequest.put(KEY_METAINFO, meta);
    climateRequest.put(KEY_PARAMETER, paramObj);

    paramObj.put(inputFeature);
    inputFeature.put(KEY_NAME, "input_zone_features");
    inputFeature.put(KEY_VALUE, collection);
    collection.put("type", "FeatureCollection");
    collection.put("features", features);
    features.put(feature);
    feature.put("type", "Feature");
    feature.put("properties", properties);
    properties.put(KEY_NAME, "pt one");
    properties.put("gid", 1);
    feature.put("geometry", geometry);
    geometry.put("type", "Point");
    geometry.put("coordinates", coordinates);
    coordinates.put(longitude);
    coordinates.put(latitude);

    if (randomSeedVal > -1) {
      randomSeed.put(KEY_NAME, "randSeed");
      randomSeed.put(KEY_VALUE, randomSeedVal);
      paramObj.put(randomSeed);
    }

    paramObj.put(prism);
    prism.put(KEY_NAME, "usePRISM");
    prism.put(KEY_VALUE, usePRISM);

    paramObj.put(adjustProb);
    adjustProb.put(KEY_NAME, "adjustPWW_PWD");
    adjustProb.put(KEY_VALUE, adjustPWW_PWD);

    paramObj.put(dataVersion);

    // set correct parameter, this chnaged from v1 to v2
    if (climateServiceURL.endsWith("cligen_prism/wepp/1.0")) {
      dataVersion.put(KEY_NAME, "climateDataVersion");
      runYears.put(KEY_NAME, WEPP_KEY_RUN_YEARS);
    } else {
      dataVersion.put(KEY_NAME, "dataVersion");
      runYears.put(KEY_NAME, "duration");
    }

    dataVersion.put(KEY_VALUE, climateDataVersion);

    paramObj.put(runYears);
    runYears.put(KEY_VALUE, run_years);

    paramObj.put(fileName);
    fileName.put(KEY_NAME, "outputFile");
    fileName.put(KEY_VALUE, "wepp.cli");

    paramObj.put(jparFile);
    jparFile.put(KEY_NAME, "returnParFiles");
    jparFile.put(KEY_VALUE, "true");

    LOG.info("Request URL: " + climateServiceURL);
    LOG.info("CLIMATE Request: " + climateRequest.toString());

    climateResponse = new Client().doPOST(climateServiceURL, climateRequest);
    //LOG.info("CLIMATE Response: " + climateResponse.toString());
    JSONObject responseMeta = climateResponse.getJSONObject(KEY_METAINFO);
    if (!responseMeta.getString(KEY_STATUS).equalsIgnoreCase("Failed")) {
      resultMap = JSONUtils.getResults(climateResponse);
      URI climateURI = new URI(JSONUtils.getStringParam(resultMap, "wepp.cli", ""));
      String climateFileName = climateURI.getPath().substring(climateURI.getPath().lastIndexOf("/") + 1, climateURI.getPath().length());
      File climateFile = workspace().getFile(climateFileName);

      new Client().doGET(climateURI.toString(), climateFile);

      if (usePRISM) {
        parUri = new URI(JSONUtils.getStringParam(resultMap, "cligenRecordPrism.par", ""));
        parFile = workspace().getFile("wepp.par");

        if (parUri.getHost() == null) {
          LOG.severe("CligenRecordPrism.par missing from cligen response.");
          parUri = new URI(JSONUtils.getStringParam(resultMap, "cligenRecord.par", ""));
        }
        if (parUri.getHost() != null) {
          new Client().doGET(parUri.toString(), parFile);
        } else {
          LOG.severe("CligenRecord.par missing from cligen response.");
        }
      } else {
        // get correct result, this name changed from v1 to v2
        if (climateServiceURL.endsWith("cligen_prism/wepp/1.0")) {
          parUri = new URI(JSONUtils.getStringParam(resultMap, "base.par", ""));
        } else {
          parUri = new URI(JSONUtils.getStringParam(resultMap, "cligenRecord.par", ""));
        }
        parFile = workspace().getFile("wepp.par");
        new Client().doGET(parUri.toString(), parFile);
      }
    } else {
      String error = responseMeta.getString(ERROR);
      throw new ServiceException("Climate service error: " + error);
    }
  }


  protected boolean getManagementFiles() throws Exception {
    JSONArray allparms;
    JSONObject myRequest;
    String manfileURL;
    String manfileName;
    JSONObject oneRot;
    String name;
    int id;

    JSONObject crlmodobj = parameter().getJSON(CRLMOD);

    managementDictAll = new JSONArray();

    allparms = new JSONArray();
    myRequest = new JSONObject();
    JSONArray temp = new JSONArray();
    temp.put("0");

    allparms = allparms.put(JSONUtils.data("onlyConvertManagements", true));
    allparms = allparms.put(JSONUtils.data("latitude", 0));
    allparms = allparms.put(JSONUtils.data("longitude", 0));
    allparms = allparms.put(JSONUtils.data("length", 100));
    allparms = allparms.put(JSONUtils.data("soilPtr", temp));
    allparms = allparms.put(JSONUtils.data("crlmod", crlmodobj));

    String metainfo = "{\"request-results\" : [ \"SLOPE_DELIVERY\",\"SLOPE_T_VALUE\",\"SLOPE_DEGRAD\"],\"mode\" : \"sync\"}";
    JSONObject m = new JSONObject(metainfo);

    myRequest = myRequest.put("metainfo", m);
    myRequest = myRequest.put("parameter", allparms);

    LOG.info("WEPP-HILL REQUEST URL: " + weppHillslopeURL);
    LOG.info("WEPP-HILL REQUEST:" + myRequest.toString());

    // call the wepp hillslope service to do the management conversion
    WEPPHillslopeServiceCall wwe04 = new WEPPHillslopeServiceCall(weppHillslopeURL, m, allparms);
    try {
      wwe04.call();
    } catch (ServiceException ex) {
      String metaError = wwe04.getMetaError();
      Logger.getLogger(WeppSoil.class.getName()).log(Level.SEVERE, null, ex);
      if (!metaError.isEmpty())
        throw new ServiceException("Error getting WEPP hillslope management.");

    }

    JSONArray results = wwe04.getResultSection();

    if (results == null)
      throw new ServiceException("Error getting WEPP hillslope management. - Results are null.");

    LOG.info(results.toString());

    for (int i = 0; i < results.length(); i++) {
      try {
        manfileName = results.getJSONObject(i).getString("name");
        manfileURL = results.getJSONObject(i).getString("value");
      } catch (Exception ex) {
        throw new ServiceException("Error getting WEPP hillslope management.");
      }

      boolean rotInDict = false;
      String mparts[];
      for (int j = 0; j < managementDict.length(); j++) {
        oneRot = managementDict.getJSONObject(j);
        name = oneRot.getString("name") + ".rot";
        id = oneRot.getInt("id");
        if (j == 0) {
          defaultManagement = id;
          LOG.info("Setting default management: " + id);
        }

        mparts = manfileName.split("_", 2);
        if (name.equals(mparts[1])) {
          // make sure the name corresponds to the index that will be 
          // in the grid file.
          manfileName = Integer.toString(id) + "_" + name;
          rotInDict = true;
          int cellCount = man_grid.cellCountWithMask(id, field_grid);
          oneRot.put("cells", cellCount);
          managementDictAll.put(oneRot);
          break;
        }
      }
      // use the dictionary to define what managements are needed
      if (rotInDict) {
        File manFile = workspace().getFile(manfileName);
        try {
          new Client().doGET(manfileURL, manFile);
        } catch (IOException ex) {
          throw new ServiceException("Cannot create the management files locally with the returned data from WEPP Hillslope: " + ex.getMessage(), ex);
        }
      }
    }
    return true;
  }


  protected void getSoilFiles() throws JSONException, ServiceException, Exception {
    soilDictAll = new JSONArray();
    for (int i = 0; i < soilDict.length(); i++) {
      JSONObject soil = soilDict.getJSONObject(i);
      String onecokey = soil.getString("cokey");
      if (!onecokey.isEmpty()) {
        int id = soil.getInt("id");
        String idxFilename = workspace().getFile(Integer.toString(id) + "_" + onecokey + ".sol").toString();
        File idxFilenameFile = workspace().getFile(Integer.toString(id) + "_" + onecokey + ".sol");
        File onecokeyFile = workspace().getFile(onecokey + ".sol");
        if (onecokeyFile.exists()) {
          // Already got this soil, copy to table specific name
          FileUtils.copyFile(onecokeyFile, idxFilenameFile);
        } else {
          LOG.info("Request saving soil to: " + workspace().getDir().toString() + "/" + onecokey + ".sol");
          buildSoilInput(idxFilename, onecokey);
          FileUtils.copyFile(idxFilenameFile, onecokeyFile);
        }
        int cellCount = soil_grid.cellCountWithMask(id, field_grid);
        soil.put("cells", cellCount);
        soilDictAll.put(soil);
        if (defaultSoil == 0) {
          defaultSoil = id;
          LOG.info("Setting default soil: " + id);
        }
      } else {
         throw new ServiceException("Cannot create the soil file locally for soil, missing COKEY: " + soil.getInt("id") + " named: " + soil.getString("name"));
      }
    } 
  }


  /**
   * This calls functions to build the WEPP soil files.
   *
   * @param file Name of soil file where output is written
   * @param soilid SSURGO soil ID in the format mukey:cokey or cokey
   * @throws JSONException JSON
   * @throws Exception any errors parsing numbers.
   *
   */
  protected void buildSoilInput(String file, String soilid) throws JSONException, Exception {
    // Need to use the soil id from the request to create a soil file with
    // WeppSoil
    int cokey = Integer.parseInt(soilid);
    LOG.info("SOIL SERVICE CALL: " + soilServiceURL + " " + soilid);
    WeppSoil theSoil = new WeppSoil(LOG, soilServiceURL);
    if (theSoil.getSoilCSIP(metainfo().toString(), cokey, workspace().getDir(), file) == false) {
      // Updated - push the error up by throwing an exception, let the calling code
      // or user interface decide how to handle this condition.
      String msg = "The cokey " + soilid + " provided does not exist in the Soils Data Mart.<br>"
          + "Soil identifier information in project may not be up to date.<br><br>"
          + "Data returned from the WEPPSoilInput service did not contain the expected results. ";
      throw new ServiceException(msg);
    }
  }


  public String soilIndexToName(int idx) throws JSONException {
    for (int i = 0; i < soilDict.length(); i++) {
      JSONObject soil = soilDict.getJSONObject(i);
      String onecokey = soil.getString("cokey");
      int id = soil.getInt("id");
      if (id == idx)
        return Integer.toString(id) + "_" + onecokey + ".sol";
    }
    return "";
  }


  public String soilIndexToShortName(int idx) throws JSONException {
    for (int i = 0; i < soilDict.length(); i++) {
      JSONObject soil = soilDict.getJSONObject(i);
      int id = soil.getInt("id");
      if (id == idx)
        return soil.getString("name");
    }
    return "???";
  }


  public String manIndexToName(int idx) throws JSONException {
    for (int i = 0; i < managementDict.length(); i++) {
      JSONObject oneRot = managementDict.getJSONObject(i);
      int id = oneRot.getInt("id");
      if (id == idx)
        return Integer.toString(id) + "_" + oneRot.getString("name") + ".rot";
    }
    return "";
  }


  public String manIndexToShortName(int idx) throws JSONException {
    for (int i = 0; i < managementDict.length(); i++) {
      JSONObject oneRot = managementDict.getJSONObject(i);
      int id = oneRot.getInt("id");
      if (id == idx)
        return oneRot.getString("name");
    }
    return "???";
  }


  public String manNameToRot(String name) throws JSONException {
    for (int i = 0; i < managementDict.length(); i++) {
      JSONObject oneRot = managementDict.getJSONObject(i);
      if (name.equals(oneRot.getString("name")))
        return Integer.toString(oneRot.getInt("id")) + "_" + name + ".rot";
    }
    // check if it is one of built-in managements fallow or grass
    if (name.equals("fallow") || (name.equals("grass"))) {
        return name + ".rot";
    }
    return "ERROR missing management: " + name;
  }


  void buildWEPPFormatChannelData() throws JSONException {
    // from the channel parameter JSON refromat to what WEPP expects.
    StringBuilder chandbdata = new StringBuilder();
    numChannelsUsed = 0;
    for (int i = 0; i < channelParameterData.length(); i++) {
      JSONObject chan = channelParameterData.getJSONObject(i);
      if (aoi.usesChannelParms(chan.getString("id"))) {
        chandbdata.append(chan.getString("id")).append(" 0\n");
        chandbdata.append(chan.getString("name")).append("\n");
        chandbdata.append(chan.getString("Comment")).append("\n");
        chandbdata.append("comment line 2\n");
        chandbdata.append("comment line 3\n");

        chandbdata.append(chan.getInt("ishape")).append(" ");
        chandbdata.append(chan.getInt("icntrl")).append(" ");
        chandbdata.append(chan.getInt("ienslp")).append(" ");
        chandbdata.append(chan.getInt("flgout")).append("\n");

        chandbdata.append(chan.getDouble("chnz")).append(" ");
        chandbdata.append(chan.getDouble("chnnbr")).append("\n");

        chandbdata.append(chan.getDouble("chnn")).append(" ");
        chandbdata.append(chan.getDouble("chnk")).append(" ");
        chandbdata.append(chan.getDouble("chntcr")).append(" ");
        chandbdata.append(chan.getDouble("chnedm")).append(" ");
        chandbdata.append(chan.getDouble("chneds")).append("\n");

        chandbdata.append(chan.getDouble("ctlslp")).append(" ");
        chandbdata.append(chan.getDouble("ctlz")).append(" ");
        chandbdata.append(chan.getDouble("ctln")).append("\n");

        chandbdata.append(chan.getDouble("rccoeff")).append(" ");
        chandbdata.append(chan.getDouble("rcexp")).append(" ");
        chandbdata.append(chan.getDouble("rcoset")).append("\n");

        chandbdata.append("\"").append(chan.getString("management")).append(".rot\"\n\n");
        numChannelsUsed++;
      }
    }
    chandbdata.insert(0, "99.1\n" + numChannelsUsed + "\n");
    rawChannelDatabaseData = chandbdata.toString();

  }


  void writeWEPPFormatChannelData(String filename) throws IOException {
    File chanFile = workspace().getFile(filename);

    FileUtils.writeStringToFile(chanFile, rawChannelDatabaseData, "UTF-8");
  }


  void buildWEPPFormatImpoundmentData(String filename) throws JSONException, IOException {
    File impFile = workspace().getFile(filename);
    StringBuilder impbdata = new StringBuilder();
    int count;

    count = 0;
    for (int i = 0; i < impoundmentParameterData.length(); i++) {
      JSONObject imp = impoundmentParameterData.getJSONObject(i);
      LOG.info("--->Checking impoundment " + imp.getString("id"));
      if (aoi.usesImpoundmentParms(imp.getString("id"))) {
        impbdata.append(buildWEPPFormatSingleImpoundment(imp, count));
        count++;
      } else {
        LOG.info("Not using impoundment " + imp.getString("id"));
      }
    }

    String header = "99.1\n" + String.valueOf(count) + "\n";
    impbdata.insert(0, header);

    FileUtils.writeStringToFile(impFile, impbdata.toString(), "UTF-8");

  }


  String buildWEPPFormatSingleImpoundment(JSONObject imp, int idx) throws JSONException {

    StringBuilder myimp = new StringBuilder();
    String thisimp;

    thisimp = "# impoundment " + String.valueOf(idx + 1) + "\n";
    myimp.append(thisimp);
    myimp.append(imp.getString("id")).append(" 0\n");
    myimp.append(imp.getString("name")).append("\n");
    myimp.append(imp.getString("comments")).append("\n");
    myimp.append("created by weppws service\n");
    myimp.append("----\n");

    // output the type line
    if (imp.has("dropspillway")) {
      myimp.append("1\n");
    } else if (imp.has("culverts")) {
      myimp.append("2\n");
    } else if (imp.has("rockfilldam")) {
      myimp.append("3\n");
    } else if (imp.has("emergencyspillway")) {
      myimp.append("4\n");
    } else if (imp.has("filterfence")) {
      myimp.append("5\n");
    } else if (imp.has("perforatedriser")) {
      myimp.append("6\n");
    } else {
      myimp.append("0\n");
    }
    // output drop spillway section
    if (imp.has("dropspillway")) {
      JSONObject sw = imp.getJSONObject("dropspillway");
      int stype = sw.getInt("type");

      myimp.append(stype).append("\n");
      switch (stype) {
        case 1:
          myimp.append(String.format("%f %f %f %f\n", sw.getDouble("diars"), sw.getDouble("hrs"), sw.getDouble("coefw"), sw.getDouble("coefo")));
          myimp.append(String.format("%f %f %f %f %f\n", sw.getDouble("diabl"), sw.getDouble("hrh"), sw.getDouble("lbl"), sw.getDouble("sbl"), sw.getDouble("hblot")));
          break;
        case 2:
          myimp.append(String.format("%f %f %f %f %f\n", sw.getDouble("lenrs"), sw.getDouble("widrs"), sw.getDouble("hrs"), sw.getDouble("coefw"), sw.getDouble("coefo")));
          myimp.append(String.format("%f %f %f %f %f\n", sw.getDouble("diabl"), sw.getDouble("hrh"), sw.getDouble("lbl"), sw.getDouble("sbl"), sw.getDouble("hblot")));
          break;
        case 3:
          myimp.append(String.format("%f %f %f %f %f\n", sw.getDouble("lenrs"), sw.getDouble("widrs"), sw.getDouble("hrs"), sw.getDouble("coefw"), sw.getDouble("coefo")));
          myimp.append(String.format("%f %f %f %f %f %f\n", sw.getDouble("hitbl"), sw.getDouble("widbl"), sw.getDouble("hrh"), sw.getDouble("lbl"), sw.getDouble("sbl"), sw.getDouble("hblot")));
          break;
      }
      myimp.append(String.format("%f %f %f\n", sw.getDouble("ke"), sw.getDouble("kb"), sw.getDouble("kc")));
    } else {
      myimp.append("0\n");
    }

    // output culvert section
    if (imp.has("culverts")) {
      JSONArray culs = imp.getJSONArray("culverts");
      for (int i = 0; i < culs.length(); i++) {
        JSONObject cul = culs.getJSONObject(i);
        myimp.append(String.format("%d\n%d\n", i + 1, i + 1));  // culvert is present
        myimp.append(String.format("%f %f %f %f %f %f\n", cul.getDouble("arcv"), cul.getDouble("hitcv"), cul.getDouble("hcv"), cul.getDouble("lcv"), cul.getDouble("scv"), cul.getDouble("hcvot")));
        myimp.append(String.format("%f %f %f\n", cul.getDouble("ke"), cul.getDouble("kb"), cul.getDouble("kc")));
      }
      if (culs.length() < 2) {
        myimp.append("0\n");   // only 1 culvert, 0 for second
      }

    } else {
      myimp.append("0\n0\n");    // there are options for 2 culverts, 0 indicates no culverts for both
    }

    // output rock fill checkdam section
    if (imp.has("rockfilldam")) {
      myimp.append("1\n"); // rockfill checkdam is present
      JSONObject rd = imp.getJSONObject("rockfilldam");
      myimp.append(String.format("%f %f %f %f %f\n", rd.getDouble("lnrf"), rd.getDouble("hrf"), rd.getDouble("hotrf"), rd.getDouble("wdrf"), rd.getDouble("diarf")));
    } else {
      myimp.append("0\n");
    }

    // output emergency spillway section
    if (imp.has("emergencyspillway")) {
      JSONObject es = imp.getJSONObject("emergencyspillway");
      int stype = es.getInt("type");

      myimp.append(stype).append("\n");
      switch (stype) {
        case 1:
          myimp.append(String.format("%f %f %f %f %f\n", es.getDouble("bwes"), es.getDouble("sses"), es.getDouble("nes"), es.getDouble("hes"), es.getDouble("hmxes")));
          myimp.append(String.format("%f %f %f %f %f\n", es.getDouble("ses1"), es.getDouble("les1"), es.getDouble("ses2"), es.getDouble("les2"), es.getDouble("ses3")));
          break;
        case 2:
          JSONArray hest = imp.getJSONArray("hest");
          myimp.append(String.format("%d\n%f\n", hest.length(), es.getDouble("hes")));
          for (int i = 0; i < hest.length(); i++) {
            myimp.append(String.format("%f\n", hest.getDouble(i)));
          }
          JSONArray qes = imp.getJSONArray("qes");
          for (int i = 0; i < qes.length(); i++) {
            myimp.append(String.format("%f\n", qes.getDouble(i)));
          }
          break;
      }
    } else {
      myimp.append("0\n");
    }

    // output filter fence section
    if (imp.has("filterfence")) {
      JSONObject ff = imp.getJSONObject("filterfence");
      int stype = ff.getInt("type");
      myimp.append(stype).append("\n");
      myimp.append(String.format("%f %f %f %f\n", ff.getDouble("vsl"), ff.getDouble("wdff"), ff.getDouble("hff"), ff.getDouble("hotff")));
    } else {
      myimp.append("0\n");
    }

    // output perorated riser section
    if (imp.has("perforatedriser")) {
      myimp.append("1\n");
      JSONObject pr = imp.getJSONObject("perforatedriser");
      myimp.append(String.format("%f %f %f %f %f %f %f\n", pr.getDouble("hr"), pr.getDouble("hb"), pr.getDouble("hs"), pr.getDouble("hd"), pr.getDouble("diar"), pr.getDouble("as"), pr.getDouble("diab")));
      myimp.append(String.format("%f %f %f %f %f %f %f %f\n", pr.getDouble("hrh"), pr.getDouble("lbl"), pr.getDouble("sbl"), pr.getDouble("diabl"), pr.getDouble("cb"), pr.getDouble("coefw"), pr.getDouble("coefo"), pr.getDouble("cs")));
      myimp.append(String.format("%f %f %f\n", pr.getDouble("ke"), pr.getDouble("kb"), pr.getDouble("kc")));
    } else {
      myimp.append("0\n");
    }

    // output miscellaneous and stage-area-length data that all types share
    JSONObject misc = imp.getJSONObject("misc");

    myimp.append(String.format("%f %f %f %f %f\n", misc.getDouble("htop"), misc.getDouble("hfull"), misc.getDouble("h"), misc.getDouble("deltat"), misc.getDouble("qinf")));
    myimp.append(String.format("%d %d\n", misc.getInt("isize"), misc.getInt("ndiv")));

    JSONArray hal = misc.getJSONArray("hal");

    myimp.append(String.format("%d\n", hal.length()));
    myimp.append(String.format("%f %f %f\n", misc.getDouble("hmin"), misc.getDouble("a0"), misc.getDouble("l0")));

    for (int i = 0; i < hal.length(); i++) {
      myimp.append(String.format("%f\n", hal.getDouble(i)));
    }
    JSONArray area = misc.getJSONArray("area");
    for (int i = 0; i < area.length(); i++) {
      myimp.append(String.format("%f\n", area.getDouble(i)));
    }
    JSONArray length = misc.getJSONArray("length");
    for (int i = 0; i < length.length(); i++) {
      myimp.append(String.format("%f\n", length.getDouble(i)));
    }

    return myimp.toString();
  }


  void setAllChannelOrderNames(String name) throws JSONException {
    for (int j = 0; j < channelOrderData.length(); j++) {
      JSONObject ch = channelOrderData.getJSONObject(j);
      ch.put("name", name);
    }
  }


  void setAllChannelOrderWidths(int width) throws JSONException {
    for (int j = 0; j < channelOrderData.length(); j++) {
      JSONObject ch = channelOrderData.getJSONObject(j);
      ch.put("width", width);
    }
  }

  static final List<String> ckeysStrs = Arrays.asList("comment", "management");
  static final List<String> ckeysInts = Arrays.asList("ishape", "icntrl", "ienslp", "flgout", "width");
  static final List<String> ckeysDbl = Arrays.asList("chnz", "chnnbr", "chnn", "chnk", "chntcr",
      "chnedm", "chneds", "ctlslp", "ctlz", "ctln", "rccoeff", "rcexp", "rcoset");


  void modifyChannelRecord(String id, JSONObject chdata) throws JSONException {
    for (int j = 0; j < channelParameterData.length(); j++) {
      JSONObject ch = channelParameterData.getJSONObject(j);
      if (ch.getString("id").equals(id)) {
        Iterator it = chdata.keys();
        while (it.hasNext()) {
          String pname = it.next().toString();
          if (ckeysStrs.contains(pname)) {
            ch.put(pname, chdata.getString(pname));
          } else if (ckeysInts.contains(pname)) {
            ch.put(pname, chdata.getInt(pname));
          } else if (ckeysDbl.contains(pname)) {
            ch.put(pname, chdata.getDouble(pname));
          }
        }
      }
    }
  }


  boolean findChannelSet(String id) throws JSONException {
    for (int j = 0; j < channelParameterData.length(); j++) {
      JSONObject ch = channelParameterData.getJSONObject(j);
      if (ch.getString("id").equals(id))
        return true;
    }
    return false;
  }


  void applyUsrChannelParameters() throws JSONException {
    // merge data for channel parameters contained in the request into 
    // the channel parameter JSON bundled with the service.
    if (usrChannelParameterData != null) {
      for (int i = 0; i < usrChannelParameterData.length(); i++) {
        JSONObject uch = usrChannelParameterData.getJSONObject(i);
        if (uch.has("id")) {
          // this applies to a channel data record which will 
          // modify all channels that reference this id
          if (findChannelSet(uch.getString("id"))) {
            // this applies to a single channel data record which will 
            // modify all channels that reference this id
            modifyChannelRecord(uch.getString("id"), uch);
          } else {
            // this applies to s single channel with TauDEM identifier specified
            // by "id". Currently only the width and parameter set can be 
            // changed for indivisual channels. TBD - need to add the 
            // ability to modify other parameters
            aoi.modifySingleChannel(uch.getString("id"), uch);
          }
        }
      }
    }
  }


  void applyUsrOrderParameters() throws JSONException {
    // merge data for channel orders contained in the request into 
    // the channel order JSON bundled with the service.
    if (usrChannelOrderData != null) {
      for (int i = 0; i < usrChannelOrderData.length(); i++) {
        JSONObject uch = usrChannelOrderData.getJSONObject(i);
        int order = uch.getInt("order");
        // find this entry in the loaded channel order database
        for (int j = 0; j < channelOrderData.length(); j++) {
          JSONObject ch = channelOrderData.getJSONObject(j);
          if (order == ch.getInt("order")) {
            // found entry
            if (uch.has("id"))
              ch.put("id", uch.getString("id"));

            if (uch.has("width"))
              ch.put("width", uch.getInt("width"));

          }
        }
      }
    }
  }


  void applyAppendChannelDB() throws JSONException {
    // create new channel parameter sets based on existing names
    if (appendChannelDB != null) {
      for (int i = 0; i < appendChannelDB.length(); i++) {
        JSONObject uch = appendChannelDB.getJSONObject(i);
        String def = uch.getString("def");
        String base = uch.getString("base");
        // find this entry in the loaded channel order database
        for (int j = 0; j < channelParameterData.length(); j++) {
          JSONObject ch = channelParameterData.getJSONObject(j);
          if (base.equals(ch.getString("id"))) {
            // found entry
            JSONObject newobj = new JSONObject(ch.toString());
            newobj.put("id", def);
            channelParameterData.put(newobj);
            modifyChannelRecord(def, uch);
          }
        }
      }
    }
  }


  void readChannelDatabaseJSON(File dbFile) throws ServiceException {
    try {
      String chandata = FileUtils.readFileToString(dbFile, "UTF-8");
      JSONObject st = new JSONObject(chandata);
      JSONObject chdb = st.getJSONObject("channelDatabase");
      channelOrderData = chdb.getJSONArray("channelOrders");
      channelParameterData = chdb.getJSONArray("channels");
    } catch (IOException | JSONException e) {
      throw new ServiceException("Could not read channel database file: " + dbFile.getName());
    }
  }


  void readImpoundmentDatabaseJSON(File dbFile) throws ServiceException {
    try {
      // read in the JSON data for channel orders
      String impounddata = FileUtils.readFileToString(dbFile, "UTF-8");
      JSONObject st = new JSONObject(impounddata);
      impoundmentParameterData = st.getJSONArray("impoundments");
    } catch (IOException | JSONException e) {
      throw new ServiceException("Could not read impoundment database file: " + dbFile.getName());
    }

  }


  String getChannelName(int order) {
    try {
      for (int i = 0; i < channelOrderData.length(); i++) {
        JSONObject chan = channelOrderData.optJSONObject(i);
        if (chan.getInt("order") == order)
          return chan.getString("id");

      }
    } catch (JSONException ex) {
      System.out.println("Error " + ex);
    }
    return null;
  }


  int getChannelWidth(int order) {
    try {
      for (int i = 0; i < channelOrderData.length(); i++) {
        JSONObject chan = channelOrderData.optJSONObject(i);
        if (chan.getInt("order") == order)
          return chan.getInt("width");
      }
    } catch (JSONException ex) {
      System.out.println("Error " + ex);
    }
    return -1;
  }


  void applyImpoundments() throws ServiceException {
    if (impoundments == null) {
      return;
    }
    try {
      for (int i = 0; i < impoundments.length(); i++) {
        JSONObject imp = impoundments.optJSONObject(i);
        int chanid = imp.getInt("id");
        Impoundment istruct = new Impoundment(i + 1, chanid, imp.getString("def"), imp.getString("position"), aoi);
        aoi.addImpoundment(istruct);
      }
    } catch (JSONException ex) {
      System.out.println("Error " + ex);
    }

  }
}