Mercurial > dive4elements > river
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 :