view flys-artifacts/src/main/java/de/intevation/flys/artifacts/FLYSArtifact.java @ 417:e54053bc0e70

Implemented the input validation of WQ in the adapted WQ panel. flys-artifacts/trunk@1882 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Ingo Weinzierl <ingo.weinzierl@intevation.de>
date Tue, 10 May 2011 15:28:30 +0000
parents 6ab62e5b05b5
children 73bc64c4a7b0
line wrap: on
line source
package de.intevation.flys.artifacts;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.xml.xpath.XPathConstants;

import gnu.trove.TDoubleArrayList;

import org.apache.log4j.Logger;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import de.intevation.artifacts.ArtifactFactory;
import de.intevation.artifacts.CallContext;

import de.intevation.artifacts.common.ArtifactNamespaceContext;
import de.intevation.artifacts.common.utils.XMLUtils;

import de.intevation.artifactdatabase.DefaultArtifact;
import de.intevation.artifactdatabase.data.DefaultStateData;
import de.intevation.artifactdatabase.data.StateData;
import de.intevation.artifactdatabase.state.State;
import de.intevation.artifactdatabase.state.StateEngine;
import de.intevation.artifactdatabase.transition.TransitionEngine;

import de.intevation.flys.model.Gauge;
import de.intevation.flys.model.Range;
import de.intevation.flys.model.River;

import de.intevation.flys.artifacts.context.FLYSContext;
import de.intevation.flys.artifacts.model.DischargeTables;
import de.intevation.flys.artifacts.model.RiverFactory;
import de.intevation.flys.artifacts.states.DefaultState;


/**
 * The defaul FLYS artifact.
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public abstract class FLYSArtifact extends DefaultArtifact {

    /** The logger that is used in this artifact.*/
    private static Logger logger = Logger.getLogger(FLYSArtifact.class);


    /** The XPath that points to the input data elements of the FEED document.*/
    public static final String XPATH_FEED_INPUT =
        "/art:action/art:data/art:input";

    /** The XPath that points to the name of the target state of ADVANCE.*/
    public static final String XPATH_ADVANCE_TARGET =
        "/art:action/art:target/@art:name";

    /** The constant string that shows that an operation was successful.*/
    public static final String OPERATION_SUCCESSFUL = "SUCCESS";

    /** The constant string that shows that an operation failed.*/
    public static final String OPERATION_FAILED = "FAILURE";

    /** The default number of steps between the start end end of a selected Q
     * range.*/
    public static final int DEFAULT_Q_STEPS = 30;

    /** The default step width between the start end end kilometer.*/
    public static final double DEFAULT_KM_STEPS = 0.1;


    /** The identifier of the current state. */
    protected String currentStateId;

    /** The identifiers of previous states on a stack.*/
    protected Vector<String> previousStateIds;

    /** The name of the artifact.*/
    protected String name;

    /** The data that have been inserted into this artifact.*/
    protected Map<String, StateData> data;


    /**
     * The default constructor that creates an empty FLYSArtifact.
     */
    public FLYSArtifact() {
        data             = new HashMap<String, StateData>();
        previousStateIds = new Vector<String>();
    }


    /**
     * Returns the name of the concrete artifact.
     *
     * @return the name of the concrete artifact.
     */
    public abstract String getName();


    /**
     * Returns the FLYSContext from context object.
     *
     * @param context The CallContext or the FLYSContext.
     *
     * @return the FLYSContext.
     */
    protected FLYSContext getFlysContext(Object context) {
        return context instanceof FLYSContext
            ? (FLYSContext) context
            : (FLYSContext) ((CallContext) context).globalContext();
    }


    /**
     * Initialize the artifact and insert new data if <code>data</code> contains
     * information necessary for this artifact.
     *
     * @param identifier The UUID.
     * @param factory The factory that is used to create this artifact.
     * @param context The CallContext.
     * @param data Some optional data.
     */
    @Override
    public void setup(
        String          identifier,
        ArtifactFactory factory,
        Object          context,
        Document        data)
    {
        logger.debug("Setup this artifact with the uuid: " + identifier);

        super.setup(identifier, factory, context, data);

        FLYSContext flysContext = (FLYSContext) context;
        StateEngine engine      = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        String name = getName();

        logger.debug("Set initial state for artifact '" + name + "'");
        List<State> states = engine.getStates(name);

        setCurrentState(states.get(0));
    }


    /**
     * Insert new data included in <code>input</code> into the current state.
     *
     * @param target XML document that contains new data.
     * @param context The CallContext.
     *
     * @return a document that contains a SUCCESS or FAILURE message.
     */
    @Override
    public Document feed(Document target, CallContext context) {
        logger.info("FLYSArtifact.feed()");

        Document doc = XMLUtils.newDocument();

        XMLUtils.ElementCreator creator = new XMLUtils.ElementCreator(
            doc,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element result = creator.create("result");
        doc.appendChild(result);

        try {
            saveData(target, XPATH_FEED_INPUT, context);
            return describe(target, context);
        }
        catch (IllegalArgumentException iae) {
            creator.addAttr(result, "type", OPERATION_FAILED, true);

            result.setTextContent(iae.getMessage());
        }

        return doc;
    }


    /**
     * This method handles request for changing the current state of an
     * artifact. It is possible to step forward or backward.
     *
     * @param target The incoming ADVANCE document.
     * @param context The CallContext.
     *
     * @return a document that contains a SUCCESS or FAILURE message.
     */
    public Document advance(Document target, CallContext context) {
        Document doc = XMLUtils.newDocument();

        XMLUtils.ElementCreator ec = new XMLUtils.ElementCreator(
            doc,
            ArtifactNamespaceContext.NAMESPACE_URI,
            ArtifactNamespaceContext.NAMESPACE_PREFIX);

        Element result = ec.create("result");

        String targetState = XMLUtils.xpathString(
            target, XPATH_ADVANCE_TARGET, ArtifactNamespaceContext.INSTANCE);

        logger.info("FLYSArtifact.advance() to '" + targetState + "'");

        if (isStateReachable(targetState, context)) {
            logger.info("Advance: Step forward");

            Vector<String> prev = getPreviousStateIds();
            prev.add(getCurrentStateId());

            setCurrentStateId(targetState);

            return describe(target, context);
        }
        else if (isPreviousState(targetState, context)) {
            logger.info("Advance: Step back to");

            Vector<String> prevs = getPreviousStateIds();
            int targetIdx        = prevs.indexOf(targetState);
            int start            = prevs.size() - 1;

            for (int i = start; i >= targetIdx; i--) {
                String prev = prevs.get(i);
                logger.debug("Remove state id '" + prev + "'");
                prevs.remove(prev);
            }

            setCurrentStateId(targetState);

            return describe(target, context);
        }

        logger.warn("Advance: Cannot advance to '" + targetState + "'");
        ec.addAttr(result, "type", OPERATION_FAILED, true);

        doc.appendChild(result);

        return doc;
    }


    /**
     * Returns the identifier of the current state.
     *
     * @return the identifier of the current state.
     */
    protected String getCurrentStateId() {
        return currentStateId;
    }


    /**
     * Sets the identifier of the current state.
     *
     * @param id the identifier of a state.
     */
    protected void setCurrentStateId(String id) {
        currentStateId = id;
    }


    /**
     * Set the current state of this artifact. <b>NOTE</b>We don't store the
     * State object itself - which is not necessary - but its identifier. So
     * this method will just call the setCurrentStateId() method with the
     * identifier of <i>state</i>.
     *
     * @param state The new current state.
     */
    protected void setCurrentState(State state) {
        setCurrentStateId(state.getID());
    }


    /**
     * Returns the current state of the artifact.
     *
     * @return the current State of the artifact.
     */
    protected State getCurrentState(Object context) {
        FLYSContext flysContext = getFlysContext(context);
        StateEngine engine      = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        return engine.getState(getCurrentStateId());
    }


    /**
     * Returns the vector of previous state identifiers.
     *
     * @return the vector of previous state identifiers.
     */
    protected Vector<String> getPreviousStateIds() {
        return previousStateIds;
    }


    /**
     * Adds a new StateData item to the data pool of this artifact.
     *
     * @param name the name of the data object.
     * @param data the data object itself.
     */
    protected void addData(String name, StateData data) {
        this.data.put(name, data);
    }


    /**
     * This method returns a specific StateData object that is stored in the
     * data pool of this artifact.
     *
     * @param name The name of the data object.
     *
     * @return the StateData object if existing, otherwise null.
     */
    public StateData getData(String name) {
        return data.get(name);
    }


    /**
     * This method stores the data that is contained in the FEED document.
     *
     * @param feed The FEED document.
     * @param xpath The XPath that points to the data nodes.
     */
    public void saveData(Document feed, String xpath, CallContext context)
    throws IllegalArgumentException
    {
        if (feed == null || xpath == null || xpath.length() == 0) {
            throw new IllegalArgumentException("error_feed_no_data");
        }

        NodeList nodes = (NodeList) XMLUtils.xpath(
            feed,
            xpath,
            XPathConstants.NODESET,
            ArtifactNamespaceContext.INSTANCE);

        if (nodes == null || nodes.getLength() == 0) {
            throw new IllegalArgumentException("error_feed_no_data");
        }

        int count = nodes.getLength();
        logger.debug("Try to save " + count + " data items.");

        for (int i = 0; i < count; i++) {
            Node node = nodes.item(i);

            String name = XMLUtils.xpathString(
                node, "@art:name", ArtifactNamespaceContext.INSTANCE);
            String value = XMLUtils.xpathString(
                node, "@art:value", ArtifactNamespaceContext.INSTANCE);

            if (name != null && value != null) {
                logger.debug("Save data item for '" + name + "' : " + value);

                addData(name, new DefaultStateData(name, null, null, value));
            }
        }

        State current           = getCurrentState(context);
        DefaultState toValidate = (DefaultState) fillState(current);

        toValidate.validate(this, context);
    }


    /**
     * This method fills a state object with the data that have been inserted to
     * this artifact. This is necessary to use the isStateReachable() method,
     * because the Transitions need to know about the inserted data.
     *
     * @param state The state that needs to be filled with data.
     *
     * @return the filled state.
     */
    protected State fillState(State state) {
        Map<String, StateData> stateData = state.getData();

        if (stateData == null) {
            return state;
        }

        Set<String>                 keys = stateData.keySet();

        for (String key: keys) {
            StateData tmp = getData(key);

            if (tmp != null) {
                StateData data = stateData.get(key);
                data.setValue(tmp.getValue());
            }
        }

        return state;
    }


    /**
     * Determines if the state with the identifier <i>stateId</i> is reachable
     * from the current state. The determination itself takes place in the
     * TransitionEngine.
     *
     * @param stateId The identifier of a state.
     * @param context The context object.
     *
     * @return true, if the state specified by <i>stateId</i> is reacahble,
     * otherwise false.
     */
    protected boolean isStateReachable(String stateId, Object context) {
        logger.debug("Determine if the state '" + stateId + "' is reachable.");

        FLYSContext flysContext = getFlysContext(context);

        State currentState  = fillState(getCurrentState(context));
        StateEngine sEngine = (StateEngine) flysContext.get(
            FLYSContext.STATE_ENGINE_KEY);

        TransitionEngine tEngine = (TransitionEngine) flysContext.get(
            FLYSContext.TRANSITION_ENGINE_KEY);

        return tEngine.isStateReachable(this, stateId, currentState, sEngine);
    }


    /**
     * Determines if the state with the identifier <i>stateId</i> is a previous
     * state of the current state.
     *
     * @param stateId The target state identifier.
     * @param context The context object.
     */
    protected boolean isPreviousState(String stateId, Object context) {
        logger.debug("Determine if the state '" + stateId + "' is old.");

        Vector<String> prevs = getPreviousStateIds();
        if (prevs.contains(stateId)) {
            return true;
        }

        return false;
    }


    /**
     * Returns the selected River object based on the 'river' data that might
     * have been inserted by the user.
     *
     * @return the selected River or null if no river has been chosen yet.
     */
    public River getRiver() {
        StateData dRiver = getData("river");

        return dRiver != null
            ? RiverFactory.getRiver((String) dRiver.getValue())
            : null;
    }


    /**
     * Returns the selected distance of points.
     *
     * @return the selected distance or points.
     */
    public double[] getDistance() {
        StateData dFrom      = getData("ld_from");
        StateData dTo        = getData("ld_to");
        StateData dLocations = getData("ld_locations");

        if (dFrom != null && dTo != null) {
            return getDistanceByRange(dFrom, dTo);
        }
        else if (dLocations != null) {
            double[] locations = getLocations();
            return new double[] { locations[0], locations[locations.length-1] };
        }

        logger.warn("No data found for distance determination!");

        return null;
    }


    /**
     * This method returns the given distance 
     *
     * @return an array with lower and upper kilometer range for each
     * intersected gauge.
     */
    public double[][] getSplittedDistance() {
        double[]    dist   = getDistance();
        List<Gauge> gauges = getGauges();

        int num = gauges != null ? gauges.size() : 0;

        double[][] res = new double[num][2];

        for (int i = 0; i < num; i++) {
            Range range = gauges.get(i).getRange();

            double lower = range.getA().doubleValue();
            double upper = range.getB().doubleValue();

            res[i][0] = dist[0] < lower ? lower : dist[0];
            res[i][1] = dist[1] > upper ? upper : dist[1];
        }

        return res;
    }


    /**
     * Returns the selected locations based on a given array of locations.
     *
     * @param locations The StateData that contains the locations.
     *
     * @return the selected locations.
     */
    public double[] getLocations() {
        StateData dLocations  = getData("ld_locations");
        String    locationStr = dLocations != null
            ? (String) dLocations.getValue()
            : "";

        if (locationStr == null || locationStr.length() == 0) {
            logger.warn("No valid location string found!");
            return null;
        }

        String[] tmp               = locationStr.split(" ");
        TDoubleArrayList locations = new TDoubleArrayList();

        for (String l: tmp) {
            try {
                locations.add(Double.parseDouble(l));
            }
            catch (NumberFormatException nfe) {
                logger.warn(nfe, nfe);
            }
        }

        locations.sort();

        return locations.toNativeArray();
    }


    /**
     * Returns the selected distance based on a given range (from, to).
     *
     * @param dFrom The StateData that contains the lower value.
     * @param dTo The StateData that contains the upper value.
     *
     * @return the selected distance.
     */
    protected double[] getDistanceByRange(StateData dFrom, StateData dTo) {
        double from = Double.parseDouble((String) dFrom.getValue());
        double to   = Double.parseDouble((String) dTo.getValue());

        return new double[] { from, to };
    }


    /**
     * Returns the selected Kms.
     *
     * @param distance An 2dim array with [lower, upper] values.
     *
     * @return the selected Kms.
     */
    public double[] getKms(double[] distance) {
        StateData dStep = getData("ld_step");

        if (dStep == null) {
            logger.warn("No step width given. Cannot compute Kms.");
            return null;
        }

        double step = Double.parseDouble((String) dStep.getValue());

        // transform step from 'm' into 'km'
        step = step / 1000;

        if (step == 0d) {
            step = DEFAULT_KM_STEPS;
        }

        return getExplodedValues(distance[0], distance[1], step);
    }


    /**
     * Returns the selected Kms.
     *
     * @return the selected kms.
     */
    public double[] getKms() {
        double[] distance = getDistance();
        return getKms(distance);
    }


    /**
     * Returns the gauge based on the current distance and river.
     *
     * @return the gauge.
     */
    public Gauge getGauge() {
        River    river = getRiver();
        double[] dist  = getDistance();

        if (logger.isDebugEnabled()) {
            logger.debug("Determine gauge for:");
            logger.debug("... river: " + river.getName());
            logger.debug("... distance: " + dist[0] + " - " + dist[1]);
        }

        Gauge gauge = river.determineGauge(dist[0], dist[1]);

        String name = gauge != null ? gauge.getName() : "'n/a";
        logger.debug("Found gauge: " + name);

        return gauge;
    }


    /**
     * Returns the gauges that match the selected kilometer range.
     *
     * @return the gauges based on the selected kilometer range.
     */
    public List<Gauge> getGauges() {
        River    river = getRiver();
        double[] dist  = getDistance();

        return river.determineGauges(dist[0], dist[1]);
    }


    /**
     * This method returns the Q values.
     *
     * @return the selected Q values or null, if no Q values are selected.
     */
    public double[] getQs() {
        StateData dMode   = getData("wq_mode");
        StateData dSingle = getData("wq_single");

        String mode = dMode != null ? (String) dMode.getValue() : "";

        if (mode.equals("Q")) {
            if (dSingle != null) {
                return getSingleWQValues();
            }
            else {
                return getWQTriple();
            }
        }
        else {
            logger.warn("You try to get Qs, but W has been inserted.");
            return null;
        }
    }


    /**
     * Returns the Q values based on a specified kilometer range.
     *
     * @param range A 2dim array with lower and upper kilometer range.
     *
     * @return an array of Q values.
     */
    public double[] getQs(double[] range) {
        StateData dMode   = getData("wq_mode");
        StateData dValues = getData("wq_values");

        String mode = dMode != null ? (String) dMode.getValue() : "";

        if (mode.equals("Q")) {
            return getWQForDist(range);
        }

        logger.warn("You try to get Qs, but Ws has been inserted.");
        return null;
    }


    /**
     * Returns the W values based on a specified kilometer range.
     *
     * @param range A 2dim array with lower and upper kilometer range.
     *
     * @return an array of W values.
     */
    public double[] getWs(double[] range) {
        StateData dMode   = getData("wq_mode");
        StateData dValues = getData("wq_values");

        String mode = dMode != null ? (String) dMode.getValue() : "";

        if (mode.equals("W")) {
            return getWQForDist(range);
        }

        logger.warn("You try to get Ws, but Qs has been inserted.");
        return null;
    }


    /**
     * This method returns the W values.
     *
     * @return the selected W values or null, if no W values are selected.
     */
    public double[] getWs() {
        StateData dMode   = getData("wq_mode");
        StateData dSingle = getData("wq_single");

        String mode = dMode != null ? (String) dMode.getValue() : "";

        if (mode.equals("W")) {
            if (dSingle != null) {
                return getSingleWQValues();
            }
            else {
                return getWQTriple();
            }
        }
        else {
            logger.warn("You try to get Qs, but W has been inserted.");
            return null;
        }
    }


    /**
     * Returns the Qs for a number of Ws. This method makes use of
     * DischargeTables.getQForW().
     *
     * @param ws An array of W values.
     *
     * @return an array of Q values.
     */
    public double[] getQsForWs(double[] ws) {
        logger.debug("FLYSArtifact.getQsForWs");

        River r = getRiver();
        Gauge g = getGauge();

        DischargeTables dt = new DischargeTables(r.getName(), g.getName());
        Map<String, double [][]>  tmp = dt.getValues();

        double[][] values = tmp.get(g.getName());
        double[]   qs     = new double[ws.length];

        for (int i = 0; i < ws.length; i++) {
            qs[i] = dt.getQForW(values, ws[i]);
        }

        return qs;
    }


    /**
     * This method returns the given W or Q values for a specific range
     * (inserted in the WQ input panel for discharge longitudinal sections).
     *
     * @param dist A 2dim array with lower und upper kilometer values.
     *
     * @return an array of W or Q values.
     */
    protected double[] getWQForDist(double[] dist) {
        logger.debug("Search wq values for range: " + dist[0] + " - " + dist[1]);
        StateData data = getData("wq_values");

        if (data == null) {
            logger.warn("Missing wq values!");
            return null;
        }

        String dataString = (String) data.getValue();
        String[]   ranges = dataString.split(":");

        for (String range: ranges) {
            String[] parts = range.split(";");

            double lower = Double.parseDouble(parts[0]);
            double upper = Double.parseDouble(parts[1]);

            if (lower <= dist[0] && upper >= dist[1]) {
                String[] values = parts[2].split(",");

                int      num = values.length;
                double[] res = new double[num];

                for (int i = 0; i < num; i++) {
                    try {
                        res[i] = Double.parseDouble(values[i]);
                    }
                    catch (NumberFormatException nfe) {
                        logger.warn(nfe, nfe);
                    }
                }

                return res;
            }
        }

        logger.warn("Specified range for WQ not found!");

        return null;
    }


    /**
     * This method returns an array of inserted WQ triples that consist of from,
     * to and the step width.
     *
     * @return an array of from, to and step width.
     */
    protected double[] getWQTriple() {
        StateData dFrom = getData("wq_from");
        StateData dTo   = getData("wq_to");

        if (dFrom == null || dTo == null) {
            logger.warn("Missing start or end value for range.");
            return null;
        }

        double from = Double.parseDouble((String) dFrom.getValue());
        double to   = Double.parseDouble((String) dTo.getValue());

        StateData dStep = getData("wq_step");

        if (dStep == null) {
            logger.warn("No step width given. Cannot compute Qs.");
            return null;
        }

        double step  = Double.parseDouble((String) dStep.getValue());

        // if no width is given, the DEFAULT_Q_STEPS is used to compute the step
        // width. Maybe, we should round the value to a number of digits.
        if (step == 0d) {
            double diff = to - from;
            step = diff / DEFAULT_Q_STEPS;
        }

        return getExplodedValues(from, to, step);
    }


    /**
     * Returns an array of inserted WQ double values stored as whitespace
     * separated list.
     *
     * @return an array of W or Q values.
     */
    protected double[] getSingleWQValues() {
        StateData dSingle = getData("wq_single");

        if (dSingle == null) {
            logger.warn("Cannot determine single WQ values. No data given.");
            return null;
        }

        String   tmp       = (String) dSingle.getValue();
        String[] strValues = tmp.split(" ");

        TDoubleArrayList values = new TDoubleArrayList();

        for (String strValue: strValues) {
            try {
                values.add(Double.parseDouble(strValue));
            }
            catch (NumberFormatException nfe) {
                logger.warn(nfe, nfe);
            }
        }

        values.sort();

        return values.toNativeArray();
    }


    /**
     * Returns an array of double values. The values contained in this array
     * begin with the value <i>from</i> and end with the value <i>to</i>. The
     * number of values in the result array depends on the <i>step</i> width.
     *
     * @param from The lower value.
     * @param to The upper value.
     * @param step The step width between two values in the result array.
     *
     * @return an array of double values.
     */
    public double[] getExplodedValues(double from, double to, double step) {
        double lower = from;

        double diff = to - from;
        double tmp  = diff / step;
        int    num = (int) Math.ceil(tmp) ;

        double[] values = new double[num];

        for (int idx = 0; idx < num; idx++) {
            values[idx] = lower;
            lower      += step;
        }

        return values;
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org