changeset 9263:abf14917be32

Moved stepping behaviour of NaviOutputChart into an exchangeable strategy. Allows for distinct values stepping of sinfo flood duration.
author gernotbelger
date Tue, 17 Jul 2018 19:48:18 +0200
parents fee5fa18361c
children baef46792354
files gwt-client/src/main/java/org/dive4elements/river/client/client/ui/CollectionView.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/DistinctValuesNaviChartStepper.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/INaviChartStepper.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/MinMaxStepNaviChartStepper.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/NaviChartOutputTab.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/NilNaviChartStepper.java gwt-client/src/main/java/org/dive4elements/river/client/shared/model/ChartMode.java gwt-client/src/main/java/org/dive4elements/river/client/shared/model/SINFOArtifact.java
diffstat 8 files changed, 485 insertions(+), 259 deletions(-) [+]
line wrap: on
line diff
--- a/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/CollectionView.java	Tue Jul 17 19:48:09 2018 +0200
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/CollectionView.java	Tue Jul 17 19:48:18 2018 +0200
@@ -28,8 +28,6 @@
 import org.dive4elements.river.client.client.event.ParameterChangeHandler;
 import org.dive4elements.river.client.client.services.AddArtifactService;
 import org.dive4elements.river.client.client.services.AddArtifactServiceAsync;
-import org.dive4elements.river.client.client.services.ArtifactService;
-import org.dive4elements.river.client.client.services.ArtifactServiceAsync;
 import org.dive4elements.river.client.client.services.CollectionAttributeService;
 import org.dive4elements.river.client.client.services.CollectionAttributeServiceAsync;
 import org.dive4elements.river.client.client.services.CreateCollectionService;
@@ -62,64 +60,58 @@
  * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
  */
 public class CollectionView extends Window implements CollectionChangeHandler, HasCollectionChangeHandlers, OutputModesChangeHandler, HasOutputModesChangeHandlers, ParameterChangeHandler, CloseClickHandler {
-    /** The ArtifactService used to communicate with the Artifact server. */
-    protected CreateCollectionServiceAsync createCollectionService = GWT.create(CreateCollectionService.class);
+
+    /** The message class that provides i18n strings. */
+    private static final FLYSConstants messages = GWT.create(FLYSConstants.class);
 
     /** The ArtifactService used to communicate with the Artifact server. */
-    protected ArtifactServiceAsync createArtifactService = GWT.create(ArtifactService.class);
+    private final CreateCollectionServiceAsync createCollectionService = GWT.create(CreateCollectionService.class);
 
     /** The AddArtifactService used to add an artifact to a collection. */
-    protected AddArtifactServiceAsync addArtifactService = GWT.create(AddArtifactService.class);
+    private final AddArtifactServiceAsync addArtifactService = GWT.create(AddArtifactService.class);
 
     /** The DescribeCollectionService used to update the existing collection. */
-    protected DescribeCollectionServiceAsync describeCollectionService = GWT.create(DescribeCollectionService.class);
+    private final DescribeCollectionServiceAsync describeCollectionService = GWT.create(DescribeCollectionService.class);
 
-    protected CollectionAttributeServiceAsync updater = GWT.create(CollectionAttributeService.class);
+    private final CollectionAttributeServiceAsync updater = GWT.create(CollectionAttributeService.class);
 
     /** The LoadArtifactService used to load recommendations */
-    protected LoadArtifactServiceAsync loadArtifactService = GWT.create(LoadArtifactService.class);
-
-    /** The message class that provides i18n strings. */
-    protected FLYSConstants messages = GWT.create(FLYSConstants.class);
+    private final LoadArtifactServiceAsync loadArtifactService = GWT.create(LoadArtifactService.class);
 
     /** The FLYS instance used to call services. */
-    protected FLYS flys;
+    private final FLYS flys;
 
     /** The ParameterList. */
-    protected ParameterList parameterList;
+    private ParameterList parameterList;
 
     /** The list of CollectionChangeHandlers. */
-    protected List<CollectionChangeHandler> handlers;
+    private final List<CollectionChangeHandler> handlers;
 
     /** The list of OutputModesChangeHandlers. */
-    protected List<OutputModesChangeHandler> outHandlers;
+    private final List<OutputModesChangeHandler> outHandlers;
 
     /** The collection to be displayed. */
-    protected Collection collection;
+    private Collection collection;
 
     /** The artifact that handles the parameterization. */
-    protected Artifact artifact;
+    private Artifact artifact;
 
-    protected TabSet tabs;
+    private final TabSet tabs;
 
     /** The output tab. */
-    protected Map<String, OutputTab> outputTabs;
+    private final Map<String, OutputTab> outputTabs;
 
     /** The layout. */
-    protected Layout layout;
+    private final Layout layout;
 
     /** Layout to show spinning wheel of joy. */
-    protected VLayout lockScreen;
+    private VLayout lockScreen;
 
-    protected int artifactsQueue;
-    protected int recommendationQueue;
-    protected Stack<Recommendation> newRecommendations;
+    private final int artifactsQueue;
+    private final Stack<Recommendation> newRecommendations;
 
     /** Values for fix analysis charts */
-    protected double currentKm;
-    protected double minKm;
-    protected double maxKm;
-    protected double steps;
+    private double currentKm;
 
     /**
      * This constructor creates a new CollectionView that is used to display the
@@ -132,15 +124,11 @@
         this.handlers = new ArrayList<CollectionChangeHandler>();
         this.outHandlers = new ArrayList<OutputModesChangeHandler>();
         this.layout = new VLayout();
-        this.parameterList = new ParameterList(flys, this, this.messages.new_project());
+        this.parameterList = new ParameterList(flys, this, CollectionView.messages.new_project());
         this.artifactsQueue = 0;
-        this.recommendationQueue = 0;
         this.newRecommendations = new Stack<Recommendation>();
 
         this.currentKm = -1d;
-        this.minKm = -1d;
-        this.maxKm = -1d;
-        this.steps = -1d;
 
         addCollectionChangeHandler(this);
         addCollectionChangeHandler(this.parameterList);
@@ -169,9 +157,6 @@
         this.layout = new VLayout();
 
         this.currentKm = -1d;
-        this.minKm = -1d;
-        this.maxKm = -1d;
-        this.steps = -1d;
 
         if (artifact != null) {
             this.parameterList = new ParameterList(flys, this,
@@ -179,9 +164,9 @@
                     // but... the international name is resolved client-side.... Instead also transport the description of the artifact and
                     // use it!
                     // FIXME: the same holds for a very few other international strings (e.g. names of facets used in Tabs)
-                    this.messages.getString(artifact.getName()), artifact);
+                    CollectionView.messages.getString(artifact.getName()), artifact);
         } else {
-            this.parameterList = new ParameterList(flys, this, this.messages.new_project());
+            this.parameterList = new ParameterList(flys, this, CollectionView.messages.new_project());
         }
 
         this.artifactsQueue = 0;
@@ -208,7 +193,7 @@
     /**
      * This method handles the initial layout stuff.
      */
-    protected void init() {
+    private void init() {
         setWidth(1010);
         setHeight(700);
 
@@ -229,7 +214,7 @@
         this.tabs.addTab(this.parameterList);
     }
 
-    protected FLYS getFlys() {
+    private FLYS getFlys() {
         return this.flys;
     }
 
@@ -263,13 +248,13 @@
      * This method calls the <code>onValueChange()</code> method of all
      * registered ValueChangeHanders.
      */
-    protected void fireCollectionChangeEvent(final Collection old, final Collection newCol) {
+    private void fireCollectionChangeEvent(final Collection old, final Collection newCol) {
         for (final CollectionChangeHandler handler : this.handlers) {
             handler.onCollectionChange(new CollectionChangeEvent(old, newCol));
         }
     }
 
-    protected void fireOutputModesChangeEvent(final OutputMode[] outputs) {
+    private void fireOutputModesChangeEvent(final OutputMode[] outputs) {
         if (this.collection == null) {
             return;
         }
@@ -290,16 +275,6 @@
     }
 
     /**
-     * This method returns true, if the Collection is new and no plugins has
-     * been chosen.
-     *
-     * @return true, if the Collection is new.
-     */
-    public boolean isNew() {
-        return this.collection.hasItems();
-    }
-
-    /**
      * Returns the artifact that is used for the parameterization.
      *
      * @return the artifact that is used for the parameterization.
@@ -321,10 +296,6 @@
     public void setArtifact(final Artifact artifact) {
         this.artifact = artifact;
 
-        onArtifactChanged(artifact);
-    }
-
-    public void onArtifactChanged(final Artifact artifact) {
         artifactChanged();
 
         if (artifact.isInBackground()) {
@@ -346,7 +317,7 @@
         setArtifact(event.getNewValue());
     }
 
-    protected void artifactChanged() {
+    private void artifactChanged() {
         final Collection c = getCollection();
 
         if (c != null) {
@@ -359,15 +330,14 @@
     /**
      * Loads all information of a collection.
      * If 'recommendations' present, load these.
-     * 
+     *
      * @param c
      *            the Collection
      */
     private void loadCollection(final Collection c) {
         final ArtifactDescription desc = getArtifact().getArtifactDescription();
         final Recommendation[] recom = desc.getRecommendations();
-        final Config config = Config.getInstance();
-        final String locale = config.getLocale();
+        final String locale = Config.getInstance().getLocale();
 
         this.describeCollectionService.describe(c.identifier(), locale, new AsyncCallback<Collection>() {
             @Override
@@ -403,7 +373,7 @@
         return this.collection;
     }
 
-    protected void setCollection(final Collection collection) {
+    private void setCollection(final Collection collection) {
         setCollection(collection, false);
     }
 
@@ -415,7 +385,7 @@
      * @param suppress
      *            Whether to fire a collectionchangeevent.
      */
-    protected void setCollection(final Collection collection, final boolean suppress) {
+    public void setCollection(final Collection collection, final boolean suppress) {
         if (collection != null && this.collection == null) {
             this.flys.getWorkspace().addView(collection.identifier(), this);
         }
@@ -423,11 +393,11 @@
         final Collection tmp = this.collection;
         this.collection = collection;
 
-        setTitle(collection.getDisplayName());
+        if (collection != null)
+            setTitle(collection.getDisplayName());
 
-        if (!suppress) {
+        if (!suppress)
             fireCollectionChangeEvent(tmp, this.collection);
-        }
     }
 
     @Override
@@ -488,7 +458,7 @@
      * @param name
      *            The name and title of the output.
      */
-    protected void addOutputTab(final String name, final OutputMode out) {
+    private void addOutputTab(final String name, final OutputMode out) {
         if (out instanceof ExportMode) {
             final ExportMode export = (ExportMode) out;
 
@@ -509,18 +479,17 @@
 
         GWT.log("Add new output tab for '" + name + "'");
 
-        final String title = this.messages.getString(name);
+        final String title = CollectionView.messages.getString(name);
         final OutputTab tab = out.createOutputTab(title, getCollection(), this);
 
-        if (tab != null) {
+        if (tab != null)
             this.outputTabs.put(name, tab);
-        }
     }
 
     /**
      * Removes all output mode tabs from tab bar.
      */
-    protected void clearOutputTabs() {
+    private void clearOutputTabs() {
         GWT.log("Clear OutputTabs.");
 
         final int num = this.tabs.getNumTabs();
@@ -535,7 +504,7 @@
     /**
      * Update the view (refresh the list of old and current data).
      */
-    protected void updateView() {
+    private void updateView() {
         GWT.log("CollectionView.updateView()");
         updateOutputTabs();
     }
@@ -543,7 +512,7 @@
     /**
      * This method is used to update the tabs to show specific output modes.
      */
-    protected void updateOutputTabs() {
+    private void updateOutputTabs() {
         GWT.log("Update output tabs.");
         if (this.outputTabs != null) {
             final Set<String> keys = this.outputTabs.keySet();
@@ -569,14 +538,12 @@
     }
 
     public void addArtifactToCollection(final Artifact artifact) {
-        final Config config = Config.getInstance();
-        final String locale = config.getLocale();
-        final Collection collection = getCollection();
+        final String locale = Config.getInstance().getLocale();
 
-        GWT.log("CollectionView.addArtifactToCollection " + collection);
+        GWT.log("CollectionView.addArtifactToCollection " + this.collection);
 
-        if (collection != null) {
-            this.addArtifactService.add(collection, artifact, locale, new AsyncCallback<Collection>() {
+        if (this.collection != null) {
+            this.addArtifactService.add(this.collection, artifact, locale, new AsyncCallback<Collection>() {
                 @Override
                 public void onFailure(final Throwable caught) {
                     GWT.log("An error occured while adding artifact.");
@@ -596,7 +563,7 @@
                 @Override
                 public void onFailure(final Throwable caught) {
                     GWT.log("Could not create the new collection.");
-                    SC.warn(FLYS.getExceptionString(CollectionView.this.messages, caught));
+                    SC.warn(FLYS.getExceptionString(CollectionView.messages, caught));
                 }
 
                 @Override
@@ -620,18 +587,16 @@
         }
     }
 
-    protected void addRecommendationsToCollection() {
-        final Config config = Config.getInstance();
-        final String locale = config.getLocale();
-        final Collection collection = getCollection();
+    private void addRecommendationsToCollection() {
+        final String locale = Config.getInstance().getLocale();
 
-        collection.addRecommendations(this.newRecommendations);
+        this.collection.addRecommendations(this.newRecommendations);
 
-        this.updater.update(collection, locale, new AsyncCallback<Collection>() {
+        this.updater.update(this.collection, locale, new AsyncCallback<Collection>() {
             @Override
             public void onFailure(final Throwable caught) {
                 CollectionView.this.newRecommendations.removeAllElements();
-                setCollection(collection);
+                setCollection(CollectionView.this.collection);
 
                 GWT.log("An error occured while saving recommendations.");
                 SC.warn(FLYS.getExceptionString(CollectionView.this.messages, caught));
@@ -646,10 +611,8 @@
         });
     }
 
-    protected void loadRecommendedArtifacts(final Recommendation[] recommendations) {
-        final Config config = Config.getInstance();
-        final String locale = config.getLocale();
-        final Collection collection = getCollection();
+    private void loadRecommendedArtifacts(final Recommendation[] recommendations) {
+        final String locale = Config.getInstance().getLocale();
 
         final Artifact masterArtifact = getArtifact();
 
@@ -659,7 +622,7 @@
         }
 
         for (final Recommendation recommendation : recommendations) {
-            if (collection.loadedRecommendation(recommendation)) {
+            if (this.collection.loadedRecommendation(recommendation)) {
                 continue;
             }
             this.newRecommendations.push(recommendation);
@@ -672,11 +635,11 @@
 
         }
 
-        this.loadArtifactService.loadMany(collection, recommendations, null, locale, new AsyncCallback<Artifact[]>() {
+        this.loadArtifactService.loadMany(this.collection, recommendations, null, locale, new AsyncCallback<Artifact[]>() {
             @Override
             public void onFailure(final Throwable caught) {
                 GWT.log("Error loading recommendations: " + caught.getMessage());
-                SC.warn(FLYS.getExceptionString(CollectionView.this.messages, caught));
+                SC.warn(FLYS.getExceptionString(CollectionView.messages, caught));
             }
 
             @Override
@@ -698,29 +661,4 @@
     public double getCurrentKm() {
         return this.currentKm;
     }
-
-    public void setMinKm(final double km) {
-        this.minKm = km;
-    }
-
-    public double getMinKm() {
-        return this.minKm;
-    }
-
-    public void setMaxKm(final double km) {
-        this.maxKm = km;
-    }
-
-    public double getMaxKm() {
-        return this.maxKm;
-    }
-
-    public void setSteps(final double step) {
-        this.steps = step;
-    }
-
-    public double getSteps() {
-        return this.steps;
-    }
-}
-// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/DistinctValuesNaviChartStepper.java	Tue Jul 17 19:48:18 2018 +0200
@@ -0,0 +1,126 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by
+ *  Björnsen Beratende Ingenieure GmbH
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui.chart;
+
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Allows stepping through a list of distinct (known) values.
+ *
+ * @author Gernot Belger
+ */
+public class DistinctValuesNaviChartStepper implements INaviChartStepper {
+
+    private final TreeSet<Double> validSteps = new TreeSet<Double>();
+
+    private double currentKm;
+
+    public DistinctValuesNaviChartStepper(final Set<Double> validKms) {
+        this.validSteps.addAll(validKms);
+
+        if (this.validSteps.isEmpty())
+            this.validSteps.add(-1d);
+
+        this.currentKm = this.validSteps.first();
+    }
+
+    @Override
+    public double getCurrentKm() {
+        return this.currentKm;
+    }
+
+    @Override
+    public double stepForward() {
+        this.currentKm = calculateStepFormward();
+        return this.currentKm;
+    }
+
+    private double calculateStepFormward() {
+        // REMARK: can't use higher due to gwt bug
+        // final Double next = this.validSteps.higher(this.currentKm);
+
+        // REMAREK: add a bit, because tailSet is inclusive
+        final SortedSet<Double> tailSet = this.validSteps.tailSet(this.currentKm + 1e-6);
+        final Double next = tailSet.isEmpty() ? null : tailSet.first();
+
+        if (next != null)
+            return next;
+
+        return this.validSteps.last();
+    }
+
+    @Override
+    public double stepBackward() {
+        this.currentKm = calculateStepBackward();
+        return this.currentKm;
+    }
+
+    private double calculateStepBackward() {
+        // REMARK: can't use lower due to gwt bug
+        // final Double prev = this.validSteps.lower(this.currentKm);
+
+        final SortedSet<Double> headSet = this.validSteps.headSet(this.currentKm);
+        final Double prev = headSet.isEmpty() ? null : headSet.last();
+
+        if (prev != null)
+            return prev;
+
+        return this.validSteps.first();
+    }
+
+    @Override
+    public double setValidCurrentKm(final double currentKm) {
+        this.currentKm = calculateValidCurrentKm(currentKm);
+        return this.currentKm;
+    }
+
+    private double calculateValidCurrentKm(final double newKm) {
+
+        if (this.validSteps.contains(newKm)) {
+            /* special case, and because headSet() is not inclusive */
+            return newKm;
+        }
+
+        final SortedSet<Double> headSet = this.validSteps.headSet(newKm);
+        final SortedSet<Double> tailSet = this.validSteps.tailSet(newKm);
+
+        // REMARK: can't use floor/ceiling because of gwt bug
+        // final Double floor = this.validSteps.floor(currentKm);
+        // final Double ceiling = this.validSteps.ceiling(currentKm);
+        final Double floor = headSet.isEmpty() ? null : headSet.last();
+        final Double ceiling = tailSet.isEmpty() ? null : tailSet.first();
+
+        if (floor != null && ceiling == null)
+            return floor;
+
+        if (floor == null && ceiling != null)
+            return ceiling;
+
+        if (floor == null && ceiling == null) {
+            /* should never happen as validKms is never empty */
+            return this.currentKm;
+        }
+
+        if (floor == null || ceiling == null) {
+            /* will never happen, but makes the NullPointer access checker happy, else we get warnings in the folowing code */
+            return this.currentKm;
+        }
+
+        /* both not null; find nearest */
+        final double floorDiff = Math.abs(newKm - floor);
+        final double ceilingDiff = Math.abs(newKm - ceiling);
+        if (floorDiff < ceilingDiff)
+            return floor;
+
+        return ceiling;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/INaviChartStepper.java	Tue Jul 17 19:48:18 2018 +0200
@@ -0,0 +1,25 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by
+ *  Björnsen Beratende Ingenieure GmbH
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui.chart;
+
+/**
+ * Abstraction for stepping through the navi chart.
+ *
+ * @author Gernot Belger
+ */
+public interface INaviChartStepper {
+    double getCurrentKm();
+
+    double stepForward();
+
+    double stepBackward();
+
+    double setValidCurrentKm(double userInput);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/MinMaxStepNaviChartStepper.java	Tue Jul 17 19:48:18 2018 +0200
@@ -0,0 +1,80 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by
+ *  Björnsen Beratende Ingenieure GmbH
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui.chart;
+
+/**
+ * 'Old' stepping behaviour of the navi chart, as used by WINFO and Fixierungsanalyse.
+ *
+ * @author Gernot Belger
+ */
+public class MinMaxStepNaviChartStepper implements INaviChartStepper {
+
+    private double currentKm;
+    private final double minKm;
+    private final double maxKm;
+    private final double step;
+
+    public MinMaxStepNaviChartStepper(final double minKm, final double maxKm, final double step) {
+        this.minKm = minKm;
+        this.maxKm = maxKm;
+        this.step = step;
+
+        this.currentKm = minKm;
+    }
+
+    @Override
+    public double getCurrentKm() {
+        return this.currentKm;
+    }
+
+    @Override
+    public double stepForward() {
+
+        if (this.currentKm >= this.maxKm)
+            this.currentKm = this.maxKm;
+        else {
+            // Why this math?
+            double newVal = this.currentKm * 100;
+            newVal += this.step / 10;
+            this.currentKm = (double) Math.round(newVal) / 100;
+        }
+
+        return this.currentKm;
+    }
+
+    @Override
+    public double stepBackward() {
+
+        if (this.currentKm <= this.minKm)
+            this.currentKm = this.minKm;
+        else {
+            // Why this math?
+            double newVal = this.currentKm * 100;
+            newVal -= this.step / 10;
+
+            this.currentKm = ((double) Math.round(newVal) / 100);
+        }
+
+        return this.currentKm;
+    }
+
+    @Override
+    public double setValidCurrentKm(final double userInput) {
+
+        if (userInput > this.maxKm)
+            this.currentKm = this.maxKm;
+        else if (userInput < this.minKm)
+            this.currentKm = this.minKm;
+        else
+            this.currentKm = userInput;
+
+        return this.currentKm;
+    }
+}
\ No newline at end of file
--- a/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/NaviChartOutputTab.java	Tue Jul 17 19:48:09 2018 +0200
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/NaviChartOutputTab.java	Tue Jul 17 19:48:18 2018 +0200
@@ -11,18 +11,22 @@
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import org.dive4elements.river.client.client.Config;
 import org.dive4elements.river.client.client.ui.CollectionView;
 import org.dive4elements.river.client.shared.model.AbstractFixBunduArtifact;
 import org.dive4elements.river.client.shared.model.Artifact;
 import org.dive4elements.river.client.shared.model.Collection;
+import org.dive4elements.river.client.shared.model.CollectionItem;
 import org.dive4elements.river.client.shared.model.FixFilter;
 import org.dive4elements.river.client.shared.model.OutputMode;
+import org.dive4elements.river.client.shared.model.SINFOArtifact;
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.i18n.client.NumberFormat;
 import com.smartgwt.client.types.Alignment;
+import com.smartgwt.client.util.SC;
 import com.smartgwt.client.widgets.Button;
 import com.smartgwt.client.widgets.Canvas;
 import com.smartgwt.client.widgets.events.ClickEvent;
@@ -42,10 +46,17 @@
  * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
  */
 public class NaviChartOutputTab extends ChartOutputTab implements TabSelectedHandler {
-    protected TextItem currentkm;
+    private TextItem currentkm;
+
+    private final NumberFormat kmFormat = NumberFormat.getDecimalFormat();
+
+    private INaviChartStepper stepper;
 
     public NaviChartOutputTab(final String title, final Collection collection, final OutputMode mode, final CollectionView collectionView) {
         super(title, collection, mode, collectionView);
+
+        this.stepper = new NilNaviChartStepper();
+
         this.right.removeChild(this.chart);
         this.right.addChild(createNaviChart());
         collectionView.registerTabHandler(this);
@@ -72,121 +83,37 @@
         form.setFields(this.currentkm);
         form.setWidth(60);
 
-        double fromKm;
-        double toKm;
-
-        if (art instanceof AbstractFixBunduArtifact) {
-            final AbstractFixBunduArtifact fix = (AbstractFixBunduArtifact) art;
-            final FixFilter fixFilter = fix.getFilter();
-            final String s = fix.getArtifactDescription().getDataValueAsString("ld_step");
-            try {
-                final double ds = Double.parseDouble(s);
-                this.collectionView.setSteps(ds);
-            }
-            catch (final NumberFormatException nfe) {
-                this.collectionView.setSteps(100d);
-            }
-            fromKm = fixFilter.getLowerKm();
-            toKm = fixFilter.getUpperKm();
-        } else {
-            // Probably WINFOArtifact kind of artifact.
-            final String ld_step = art.getArtifactDescription().getDataValueAsString("ld_step");
-            try {
-                this.collectionView.setSteps(Double.valueOf(ld_step));
-            }
-            catch (final Exception e) {
-                GWT.log("No ld_steps data or not parsable.");
-                return root;
-            }
-
-            final double[] kmRange = art.getArtifactDescription().getKMRange();
-            if (kmRange == null || kmRange.length == 2) {
-                fromKm = kmRange[0];
-                toKm = kmRange[1];
-            } else {
-                GWT.log("No KM range in description found.");
-                return root;
-            }
-        }
-
-        this.collectionView.setMinKm(fromKm);
-        this.collectionView.setMaxKm(toKm);
-
-        final NumberFormat nf = NumberFormat.getDecimalFormat();
+        this.stepper = createStepper(art);
 
         // Always jump to the from km when initialized.
-        try {
-            final double d = Double.valueOf(fromKm);
-            this.currentkm.setValue(nf.format(d));
-        }
-        catch (final NumberFormatException e) {
-            this.currentkm.setValue(fromKm);
-        }
-        this.collectionView.setCurrentKm(fromKm);
+        final double currentKm = this.stepper.getCurrentKm();
+        this.collectionView.setCurrentKm(currentKm);
+        this.currentkm.setValue(this.kmFormat.format(currentKm));
 
         lower.addClickHandler(new ClickHandler() {
             @Override
             public void onClick(final ClickEvent ce) {
-                NaviChartOutputTab.this.tbarPanel.deselectControls();
                 updateChartDown();
-                try {
-                    final double d = Double.valueOf(NaviChartOutputTab.this.collectionView.getCurrentKm());
-                    NaviChartOutputTab.this.currentkm.setValue(nf.format(d));
-                    NaviChartOutputTab.this.tbarPanel.onZoom(null);
-                }
-                catch (final NumberFormatException e) {
-                    NaviChartOutputTab.this.currentkm.setValue(NaviChartOutputTab.this.collectionView.getCurrentKm());
-                }
             }
         });
 
         upper.addClickHandler(new ClickHandler() {
             @Override
             public void onClick(final ClickEvent ce) {
-                NaviChartOutputTab.this.tbarPanel.deselectControls();
                 updateChartUp();
-                try {
-                    final double d = Double.valueOf(NaviChartOutputTab.this.collectionView.getCurrentKm());
-                    NaviChartOutputTab.this.currentkm.setValue(nf.format(d));
-                    NaviChartOutputTab.this.tbarPanel.onZoom(null);
-                }
-                catch (final NumberFormatException e) {
-                    NaviChartOutputTab.this.currentkm.setValue(NaviChartOutputTab.this.collectionView.getCurrentKm());
-                }
             }
         });
 
         this.currentkm.addKeyPressHandler(new KeyPressHandler() {
             @Override
             public void onKeyPress(final KeyPressEvent kpe) {
-                if (!kpe.getKeyName().equals("Enter")) {
+
+                if (!kpe.getKeyName().equals("Enter"))
                     return;
-                }
+
                 if (kpe.getItem().getValue() != null) {
-                    NaviChartOutputTab.this.tbarPanel.deselectControls();
-                    try {
-                        final String s = kpe.getItem().getValue().toString();
-                        double d;
-                        try {
-                            d = nf.parse(s);
-                            NaviChartOutputTab.this.currentkm.setValue(nf.format(d));
-                        }
-                        catch (final NumberFormatException e) {
-                            d = -1d;
-                        }
-                        if (d <= NaviChartOutputTab.this.collectionView.getMaxKm() && d >= NaviChartOutputTab.this.collectionView.getMinKm()) {
-                            NaviChartOutputTab.this.collectionView.setCurrentKm(d);
-                            NaviChartOutputTab.this.tbarPanel.updateLinks();
-                            NaviChartOutputTab.this.tbarPanel.onZoom(null);
-                            if (NaviChartOutputTab.this.right != null) {
-                                updateChartPanel();
-                                updateChartInfo();
-                            }
-                        }
-                    }
-                    catch (final NumberFormatException nfe) {
-                        // Do nothing.
-                    }
+                    final String kmText = kpe.getItem().getValue().toString();
+                    updateChartKm(kmText);
                 }
             }
         });
@@ -199,21 +126,84 @@
         return root;
     }
 
+    private INaviChartStepper createStepper(final Artifact art) {
+
+        if (art instanceof AbstractFixBunduArtifact) {
+            final AbstractFixBunduArtifact fix = (AbstractFixBunduArtifact) art;
+            final FixFilter fixFilter = fix.getFilter();
+
+            final double fromKm = fixFilter.getLowerKm();
+            final double toKm = fixFilter.getUpperKm();
+
+            final String s = fix.getArtifactDescription().getDataValueAsString("ld_step");
+            try {
+                final double ds = Double.parseDouble(s);
+                return new MinMaxStepNaviChartStepper(fromKm, toKm, ds);
+            }
+            catch (final NumberFormatException nfe) {
+                return new MinMaxStepNaviChartStepper(fromKm, toKm, 100d);
+            }
+        } else if (art instanceof SINFOArtifact) {
+            /* special case for SINFO-Flood-Duration */
+            final SINFOArtifact sinfo = (SINFOArtifact) art;
+
+            final CollectionItem item = this.collection.getItem(sinfo.getUuid());
+
+            final Set<Double> validKms = sinfo.getValidDurationChartKms(item);
+            return new DistinctValuesNaviChartStepper(validKms);
+        } else {
+            // Probably WINFOArtifact kind of artifact.
+
+            double fromKm;
+            double toKm;
+
+            final double[] kmRange = art.getArtifactDescription().getKMRange();
+            if (kmRange != null && kmRange.length == 2) {
+                fromKm = kmRange[0];
+                toKm = kmRange[1];
+            } else {
+                GWT.log("No KM range in description found.");
+                return new NilNaviChartStepper();
+            }
+
+            final String ld_step = art.getArtifactDescription().getDataValueAsString("ld_step");
+            try {
+                final Double step = Double.valueOf(ld_step);
+                return new MinMaxStepNaviChartStepper(fromKm, toKm, step);
+            }
+            catch (final Exception e) {
+                GWT.log("No ld_steps data or not parsable.", e);
+                return new MinMaxStepNaviChartStepper(fromKm, toKm, 100d);
+            }
+        }
+    }
+
+    protected void updateChartKm(final String kmText) {
+
+        NaviChartOutputTab.this.tbarPanel.deselectControls();
+
+        try {
+            final double d = this.kmFormat.parse(kmText);
+
+            final double validCurrentKm = this.stepper.setValidCurrentKm(d);
+            updateCurrentKm(validCurrentKm);
+        }
+        catch (final NumberFormatException e) {
+            SC.warn("Invalid value: " + kmText);
+            // do nothing, but an error message would be nice
+        }
+    }
+
     /**
      * Callback when km-up-button is clicked.
      * Increases collectionViews KM and refreshes view.
      */
     protected void updateChartUp() {
-        final double currentKm = this.collectionView.getCurrentKm();
-        if (currentKm < this.collectionView.getMaxKm()) {
-            // Why this math?
-            double newVal = currentKm * 100;
-            newVal += (this.collectionView.getSteps() / 10);
-            this.collectionView.setCurrentKm((double) Math.round(newVal) / 100);
-            this.tbarPanel.updateLinks();
-            updateChartPanel();
-            updateChartInfo();
-        }
+
+        this.tbarPanel.deselectControls();
+
+        final double nextKm = this.stepper.stepForward();
+        updateCurrentKm(nextKm);
     }
 
     /**
@@ -221,16 +211,24 @@
      * Decreases collectionViews KM and refreshes view.
      */
     protected void updateChartDown() {
-        final double currentKm = this.collectionView.getCurrentKm();
-        if (currentKm > this.collectionView.getMinKm()) {
-            // Why this math?
-            double newVal = currentKm * 100;
-            newVal -= (this.collectionView.getSteps() / 10);
-            this.collectionView.setCurrentKm((double) Math.round(newVal) / 100);
-            this.tbarPanel.updateLinks();
-            updateChartPanel();
-            updateChartInfo();
-        }
+
+        this.tbarPanel.deselectControls();
+
+        final double prevKm = this.stepper.stepBackward();
+        updateCurrentKm(prevKm);
+    }
+
+    private void updateCurrentKm(final double currentKm) {
+
+        this.collectionView.setCurrentKm(currentKm);
+
+        this.tbarPanel.updateLinks();
+
+        updateChartPanel();
+        updateChartInfo();
+
+        this.currentkm.setValue(this.kmFormat.format(currentKm));
+        this.tbarPanel.onZoom(null);
 
     }
 
@@ -289,14 +287,17 @@
             }
         }
 
-        if (this.collectionView.getArtifact() instanceof AbstractFixBunduArtifact) {
-            if (this.collectionView.getCurrentKm() == -1) {
+        if (this.collectionView.getCurrentKm() == -1) {
+            // REMARK: this happens, because we get called from the constructor of our super class
+
+            if (this.collectionView.getArtifact() instanceof AbstractFixBunduArtifact) {
                 final AbstractFixBunduArtifact fix = (AbstractFixBunduArtifact) this.collectionView.getArtifact();
                 this.collectionView.setCurrentKm(fix.getFilter().getLowerKm());
             }
-        } else if (this.collectionView.getCurrentKm() == -1) {
-            this.collectionView.setCurrentKm(this.collectionView.getArtifact().getArtifactDescription().getKMRange()[0]);
+            else
+                this.collectionView.setCurrentKm(this.collectionView.getArtifact().getArtifactDescription().getKMRange()[0]);
         }
+
         if (this.collectionView.getCurrentKm() != -1) {
             imgUrl += "&currentKm=" + this.collectionView.getCurrentKm();
         }
@@ -307,9 +308,11 @@
     @Override
     public void onTabSelected(final TabSelectedEvent tse) {
         if (this.equals(tse.getTab())) {
-            updateChartPanel();
-            updateChartInfo();
-            this.currentkm.setValue(this.collectionView.getCurrentKm());
+
+            final double currentKm = this.collectionView.getCurrentKm();
+
+            final double validCurrentKm = this.stepper.setValidCurrentKm(currentKm);
+            updateCurrentKm(validCurrentKm);
         }
     }
 
@@ -335,5 +338,4 @@
         }
         return url;
     }
-}
-// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/chart/NilNaviChartStepper.java	Tue Jul 17 19:48:18 2018 +0200
@@ -0,0 +1,38 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by
+ *  Björnsen Beratende Ingenieure GmbH
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui.chart;
+
+/**
+ * Nil implementation that always return 0 as current station.
+ *
+ * @author Gernot Belger
+ */
+public class NilNaviChartStepper implements INaviChartStepper {
+
+    @Override
+    public double getCurrentKm() {
+        return 0;
+    }
+
+    @Override
+    public double stepForward() {
+        return 0;
+    }
+
+    @Override
+    public double stepBackward() {
+        return 0;
+    }
+
+    @Override
+    public double setValidCurrentKm(final double userInput) {
+        return 0;
+    }
+}
\ No newline at end of file
--- a/gwt-client/src/main/java/org/dive4elements/river/client/shared/model/ChartMode.java	Tue Jul 17 19:48:09 2018 +0200
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/shared/model/ChartMode.java	Tue Jul 17 19:48:18 2018 +0200
@@ -49,7 +49,8 @@
             this.getName().equals("extreme_wq_curve") ||
             this.getName().equals("fix_deltawt_curve") ||
             this.getName().equals("fix_derivate_curve") ||
-            this.getName().equals("fix_vollmer_wq_curve")){
+            this.getName().equals("fix_vollmer_wq_curve") ||
+            this.getName().equals("sinfo_floodduration_curve")){
             return new NaviChartOutputTab(t, c, this, p);
         }
         return new ChartOutputTab(t, c, this, p);
--- a/gwt-client/src/main/java/org/dive4elements/river/client/shared/model/SINFOArtifact.java	Tue Jul 17 19:48:09 2018 +0200
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/shared/model/SINFOArtifact.java	Tue Jul 17 19:48:18 2018 +0200
@@ -8,8 +8,10 @@
 
 package org.dive4elements.river.client.shared.model;
 
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
-
+import java.util.Set;
 
 /**
  * The SINFO implementation of an Artifact.
@@ -18,29 +20,43 @@
  */
 public class SINFOArtifact extends DefaultArtifact {
 
-    /** The name of this artifact: 'sinfo'.*/
+    /** The name of this artifact: 'sinfo'. */
     private static final String NAME = "sinfo";
 
     /** Necessary for serialization */
     public SINFOArtifact() {
     }
 
-//    public  SINFOArtifact(String uuid, String hash) {
-//        super(uuid, hash);
-//    }
-
-    public SINFOArtifact(
-        String                   uuid,
-        String                   hash,
-        boolean                  inBackground,
-        List<CalculationMessage> messages
-    ) {
+    public SINFOArtifact(final String uuid, final String hash, final boolean inBackground, final List<CalculationMessage> messages) {
         super(uuid, hash, inBackground, messages);
     }
 
-
+    @Override
     public String getName() {
         return NAME;
     }
-}
-// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
+
+    public Set<Double> getValidDurationChartKms(final CollectionItem item) {
+
+        final String validKmsText = item.getData().get("validStations");
+        if (validKmsText == null || validKmsText.trim().isEmpty())
+            return Collections.emptySet();
+
+        final String[] split = validKmsText.split(",");
+
+        final Set<Double> validKms = new HashSet<Double>(split.length);
+
+        for (final String stationText : split) {
+
+            try {
+                final double station = Double.parseDouble(stationText);
+                validKms.add(station);
+            }
+            catch (final NumberFormatException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return validKms;
+    }
+}
\ No newline at end of file

http://dive4elements.wald.intevation.org