EFH2RainfallCoefficients.java [src/java/m/efh2] Revision:   Date:
package m.efh2;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * Runoff coefficients used in calculation of qu (unit peak discharge),
 * per EFH2 Hydrology practice
 */
public class EFH2RainfallCoefficients {

    // NOTE: This is an literal implementation of the lookup table from
    // Basic source code, type.rf (data file), now called RfC.txt for clarity
    //
    private static Map<String, RfCTable> rfcTables = new HashMap<String, RfCTable>(5);

    static {
        // init from default coefficients file, which is a resource in this package
        String resourcePath = "/m/efh2/RfC.txt";
        InputStream istream = EFH2RainfallCoefficients.class.getResourceAsStream(resourcePath);
        loadTables(istream);
    }

    /**
     * private constructor - use the static methods
     */
    private EFH2RainfallCoefficients() {
    }

    /**
     * lookup runoff coeffeicients by storm type and SAPG value
     *
     * @param stormType stormType, from {I, IA, II, III} or other adapted list
     * @param targetSAPG Ia/P value, called SAPG after Basic implementation
     * @return List<RfC> records.  If size 1, use that record only
     * to calc qu; if size 2, caller must use both records & interpolate.
     */
    public static List<RfC> getRainfallCoeff(String stormType, double targetSAPG) {
        RfCTable table = rfcTables.get(stormType);
        if (table == null) {
            System.err.println("Caller requested unknown storm type " + stormType);
            return new ArrayList<RfC>();
        } else {
            return table.getRfC(targetSAPG);
        }
    }

    /**
     * load rainfall coefficient tables
     *
     * @param is input stream containing tables in expected format. See RfC.txt.
     * @return true if successfully read.  If false, this model is unusable.
     */
    public static boolean loadTables(InputStream is) {
        rfcTables.clear();

        byte[] contentBytes = new byte[25000];
        boolean readOK = true;
        try {
            int cc = is.read(contentBytes);
            if (cc < 0) {
                System.err.println("Failed to read RfC data");
                readOK = false;
            }
        } catch (IOException ex) {
            System.err.println("Failed to read RfC data " + ex);
            readOK = false;
        }

        if (readOK) {
            String totalContent = new String(contentBytes);
            String[] lines = totalContent.split("\n");
            int idx = 0;
            while (idx < lines.length) {
                String line = lines[idx++];
                String[] splits = line.split(",");
                if (splits.length == 2) {

                    // get stormType and row count
                    String stormType = splits[0].trim();
                    int nRows = Integer.valueOf(splits[1].trim());

                    RfCTable table = new RfCTable(stormType);
                    for (int i = 0; i < nRows; i++) {
                        line = lines[idx++];
                        splits = line.split(",");
                        if (splits.length == 4) {
                            double SAPG = Double.valueOf(splits[0].trim());
                            double RfC2 = Double.valueOf(splits[1].trim());
                            double RfC3 = Double.valueOf(splits[2].trim());
                            double RfC4 = Double.valueOf(splits[3].trim());
                            RfC rfc = new RfC(SAPG, RfC2, RfC3, RfC4);
                            table.addRfC(rfc);
                        } else {
                            throw new IllegalArgumentException("Improper RfC record: " + line);
                        }
                    }
                    rfcTables.put(stormType, table);
                } else {
                    throw new IllegalArgumentException("Improper start of RfC Table: " + line);
                }
            }
        }
        return readOK;
    }

    /**
     * get the list of known storm types
     */
    public static List<String> getStormTypes() {
        List<String> types = new ArrayList<String>();
        types.addAll(rfcTables.keySet());
        Collections.sort(types);
        return types;
    }

    //---------------------------------------------------------------------
    //
    //  Inner classes to implement the lookup tables
    //
    /**
     * inner class - one table of Rainfall coefficients, for one storm type
     */
    private static class RfCTable {

        private String stormType;
        private List<RfC> rfcList;

        public RfCTable(String stormType) {
            this.stormType = stormType;
            rfcList = new ArrayList<RfC>();
        }

        /* package access */
        void addRfC(RfC rfc) {
            rfcList.add(rfc);
        }

        public String getStormType() {
            return stormType;
        }

        /**
         * get rainfall coefficients for calculating qu
         *
         * @param targetSAPG SAPG (Ia/P) value desired
         * @return List<RfC> records.  If size 1, use that record only
         * to calc qu; if size 2, caller must use both records & interpolate.
         */
        public List<RfC> getRfC(double targetSAPG) {
            List<RfC> rfcs = new ArrayList<RfC>();
            if (targetSAPG <= 0.1) {
                rfcs.add(rfcList.get(0));
            } else if (targetSAPG >= 0.5) {
                rfcs.add(rfcList.get(rfcList.size() - 1));
            } else {

                // first scan for approximate match (binary vs decimal #s)
                for (RfC rfC : rfcList) {
                    if (Math.abs(targetSAPG - rfC.getSAPG()) < 0.001) {
                        // approximately equal
                        rfcs.add(rfC);
                        return Collections.unmodifiableList(rfcs);
                    }
                }

                // then scan table for bracketing value pair
                for (int j = 0, i = 1; i < rfcList.size(); j = i++) {
                    RfC lower = rfcList.get(j);
                    RfC upper = rfcList.get(i);
                    if (lower.getSAPG() < targetSAPG && targetSAPG <= upper.getSAPG()) {
                        rfcs.add(lower);
                        rfcs.add(upper);
                        break;
                    }
                }
            }
            return Collections.unmodifiableList(rfcs);
        }
    }

    /**
     * inner class, representing one row of RFC table
     */
    public static class RfC {

        /** Ia/P value, called SAPG after the Basic source */
        private double SAPG;
        /** rainfall coefficients */
        private double RfC2,  RfC3,  RfC4;
        // NOTE: Sorry, there is no documented explanation of the rainfall
        // coefficients 2, 3, 4

        public RfC(double SAPG, double RfC2, double RfC3, double RfC4) {
            this.SAPG = SAPG;
            this.RfC2 = RfC2;
            this.RfC3 = RfC3;
            this.RfC4 = RfC4;
        }

        public double getSAPG() {
            return SAPG;
        }

        public double getRfC2() {
            return RfC2;
        }

        public double getRfC3() {
            return RfC3;
        }

        public double getRfC4() {
            return RfC4;
        }
    }
}