changeset 8946:5d5d482da3e9

Implementing SINFO - FlowDepthMinMax calculation
author gernotbelger
date Tue, 13 Mar 2018 18:49:33 +0100
parents 4a6b6a3c279c
children 86650594f051
files artifacts/doc/conf/artifacts/sinfo.xml artifacts/doc/conf/generators/generators.xml artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/SINFOArtifact.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/SInfoI18NStrings.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoCalculationResult.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoExporter.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoLineProcessor.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoResultRow.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractTkhCalculationResult.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractTkhResultRow.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/D50Processor.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/FlowDepthProcessor.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/TauProcessor.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/TkhProcessor.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/VelocityProcessor.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthAccess.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculation.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculationResult.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculator.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthExporter.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthPairSelectState.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthRow.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthUtils.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/UseTransportBodiesChoice.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/WstSoundingIdPair.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxAccess.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxCalculation.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxCalculationResult.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxCalculationResults.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxExporter.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxRow.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxState.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/DischargeValuesFinder.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/TkhCalculator.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/WaterlevelValuesFinder.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/BedHeightsFinder.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhCalculation.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhCalculationResult.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhExporter.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhResultRow.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/util/BedHeightInfo.java artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/util/MetaAndTableJRDataSource.java artifacts/src/main/java/org/dive4elements/river/artifacts/states/WaterlevelFetcher.java artifacts/src/main/resources/messages.properties artifacts/src/main/resources/messages_de.properties
diffstat 45 files changed, 1682 insertions(+), 740 deletions(-) [+]
line wrap: on
line diff
--- a/artifacts/doc/conf/artifacts/sinfo.xml	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/doc/conf/artifacts/sinfo.xml	Tue Mar 13 18:49:33 2018 +0100
@@ -83,6 +83,58 @@
       </outputmodes>
     </state>
 
+    <!-- Calculation Mode: Minimale/maximale Fließtiefe -->
+    <transition transition="org.dive4elements.river.artifacts.transitions.ValueCompareTransition">
+      <from state="state.sinfo.calculation_mode"/>
+      <to state="state.sinfo.distance_only"/>
+      <condition data="calculation_mode" value="sinfo_calc_flow_depth_minmax" operator="equal"/>
+    </transition>
+
+    <transition transition="org.dive4elements.river.artifacts.transitions.ValueCompareTransition">
+      <from state="state.sinfo.distance_only"/>
+      <!-- FIXME: need to distinguish between flow_depth and flow_depth_minmax -->
+      <!-- Insbesondere nur TL/KL -->
+      <to state="state.sinfo.waterlevel_soundings_select"/>
+      <condition data="calculation_mode" value="sinfo_calc_flow_depth_minmax" operator="equal"/>
+    </transition>
+
+    <transition transition="org.dive4elements.river.artifacts.transitions.ValueCompareTransition">
+      <from state="state.sinfo.waterlevel_soundings_select"/>
+      <to state="state.sinfo.flow_depth_minmax"/>
+      <condition data="calculation_mode" value="sinfo_calc_flow_depth_minmax" operator="equal"/>
+    </transition>
+
+    <state id="state.sinfo.flow_depth_minmax" description="state.sinfo.flow_depth_minmax" state="org.dive4elements.river.artifacts.sinfo.flowdepthminmax.FlowDepthMinMaxState" helpText="help.state.sinfo.flow_depth_minmax">
+      <outputmodes>
+      <!-- 
+        <outputmode name="sinfo_flow_depth" description="output.flow_depth" mime-type="image/png" type="chart">
+          <facets>
+            <facet name="sinfo_facet_flow_depth.filtered" description="Facet for mean flow depth, filtered by current zoom state"/>
+            <facet name="sinfo_facet_flow_depth_with_tkh.filtered" description="Facet for mean flow depth including tkh, filtered by current zoom state"/>
+
+            <facet name="sinfo_facet_tkh" description="Facet for tkh"/>
+
+            <facet name="longitudinal_section.annotations" description="facet.longitudinal_section.annotations"/>
+          </facets>
+        </outputmode>
+       -->
+
+        <outputmode name="sinfo_flowdepthminmax_export" description="output.sinfo_flowdepthminmax_export" mime-type="text/plain" type="export">
+          <facets>
+            <facet name="csv" description="facet.sinfo_flowdepthminmax_export.csv"/>
+            <facet name="pdf" description="facet.sinfo_flowdepthminmax_export.pdf"/>
+          </facets>
+        </outputmode>
+
+        <outputmode name="sinfo_flowdepth_report" description="output.sinfo_flowdepth_report" mime-type="text/xml" type="report">
+          <facets>
+            <facet name="report" description="facet.sinfo_flowdepth_report"/>
+          </facets>
+        </outputmode>
+      </outputmodes>
+    </state>
+    
+
     <!-- Calculation Mode: Transportkörperhöhen -->
     <transition transition="org.dive4elements.river.artifacts.transitions.ValueCompareTransition">
       <from state="state.sinfo.calculation_mode"/>
--- a/artifacts/doc/conf/generators/generators.xml	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/doc/conf/generators/generators.xml	Tue Mar 13 18:49:33 2018 +0100
@@ -62,6 +62,8 @@
     <!-- SINFO -->
     <output-generator names="sinfo_flowdepth_export" class="org.dive4elements.river.artifacts.sinfo.flowdepth.FlowDepthExporter"/>
     <output-generator names="sinfo_flowdepth_report" class="org.dive4elements.river.exports.ReportGenerator"/>
+    <output-generator names="sinfo_flowdepthminmax_export" class="org.dive4elements.river.artifacts.sinfo.flowdepthminmax.FlowDepthMinMaxExporter"/>
+    <output-generator names="sinfo_flowdepthminmax_report" class="org.dive4elements.river.exports.ReportGenerator"/>
     <output-generator names="sinfo_tkh_export" class="org.dive4elements.river.artifacts.sinfo.tkhstate.TkhExporter"/>
     <output-generator names="sinfo_tkh_report" class="org.dive4elements.river.exports.ReportGenerator"/>
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/SINFOArtifact.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/SINFOArtifact.java	Tue Mar 13 18:49:33 2018 +0100
@@ -1,6 +1,6 @@
 /* Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
- * Software engineering by 
- *  Björnsen Beratende Ingenieure GmbH 
+ * 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)
@@ -17,26 +17,26 @@
  *
  * @author Gernot Belger
  */
-public class SINFOArtifact
-extends      D4EArtifact
-{
+public class SINFOArtifact extends D4EArtifact {
+
+    private static final long serialVersionUID = 1L;
+
     /** Error message that is thrown if no mode has been chosen. */
-    private static final String ERROR_NO_CALCULATION_MODE =
-        "error_feed_no_calculation_mode";
+    private static final String ERROR_NO_CALCULATION_MODE = "error_feed_no_calculation_mode";
 
-    /** Error message that is thrown if an invalid calculation mode has been
-     * chosen. */
-    private static  final String ERROR_INVALID_CALCULATION_MODE =
-        "error_feed_invalid_calculation_mode";
+    /**
+     * Error message that is thrown if an invalid calculation mode has been
+     * chosen.
+     */
+    private static final String ERROR_INVALID_CALCULATION_MODE = "error_feed_invalid_calculation_mode";
 
-	
     /** The name of the artifact. */
     private static final String ARTIFACT_NAME = "sinfo";
 
     private static final String FIELD_RIVER = "river";
 
     private static final String FIELD_MODE = "calculation_mode";
-    
+
     /**
      * Default constructor, because it's serializable.
      */
@@ -52,22 +52,23 @@
     public String getName() {
         return ARTIFACT_NAME;
     }
-    
+
     public SinfoCalcMode getCalculationMode() {
 
-        final String    calc = getDataAsString(FIELD_MODE);
+        final String calc = getDataAsString(FIELD_MODE);
         if (calc == null) {
-        	throw new IllegalArgumentException(ERROR_NO_CALCULATION_MODE);
+            throw new IllegalArgumentException(ERROR_NO_CALCULATION_MODE);
         }
 
         try {
-			return SinfoCalcMode.valueOf(StringUtils.trimToEmpty(calc).toLowerCase());
-		} catch (Exception e) {
-			throw new IllegalArgumentException(ERROR_INVALID_CALCULATION_MODE, e);
-		}
-	}
-    
+            return SinfoCalcMode.valueOf(StringUtils.trimToEmpty(calc).toLowerCase());
+        }
+        catch (final Exception e) {
+            throw new IllegalArgumentException(ERROR_INVALID_CALCULATION_MODE, e);
+        }
+    }
+
     public String getRiver() {
-    	return getDataAsString(FIELD_RIVER);
-	}
+        return getDataAsString(FIELD_RIVER);
+    }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/SInfoI18NStrings.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/SInfoI18NStrings.java	Tue Mar 13 18:49:33 2018 +0100
@@ -16,57 +16,61 @@
  */
 public interface SInfoI18NStrings {
 
-    public static final String CSV_META_HEADER_RESULT = "sinfo.export.flow_depth.csv.meta.header.result";
-
-    public static final String CSV_META_HEADER_RESULT_LABEL = "sinfo.export.flow_depth.csv.meta.header.result.label";
-
-    public static final String CSV_META_VERSION = "sinfo.export.flow_depth.csv.meta.version";
-
-    public static final String CSV_META_VERSION_LABEL = "sinfo.export.flow_depth.csv.meta.version.label";
-
-    public static final String CSV_META_USER = "sinfo.export.flow_depth.csv.meta.user";
-
-    public static final String CSV_META_USER_LABEL = "sinfo.export.flow_depth.csv.meta.user.label";
-
-    public static final String CSV_META_CREATION = "sinfo.export.flow_depth.csv.meta.creation";
-
-    public static final String CSV_META_CREATION_LABEL = "sinfo.export.flow_depth.csv.meta.creation.label";
-
-    public static final String CSV_META_RIVER = "sinfo.export.flow_depth.csv.meta.river";
-
-    public static final String CSV_META_RIVER_LABEL = "sinfo.export.flow_depth.csv.meta.river.label";
-
-    public static final String CSV_KM_HEADER = "sinfo.export.flow_depth.csv.header.km";
-
-    public static final String CSV_MEAN_BED_HEIGHT_HEADER = "sinfo.export.flow_depth.csv.header.mean_bed_height";
-
-    public static final String CSV_WATERLEVEL_HEADER = "sinfo.export.flow_depth.csv.header.waterlevel";
+    String CSV_META_HEADER_RESULT = "sinfo.export.flow_depth.csv.meta.header.result";
 
-    public static final String CSV_DISCHARGE_HEADER = "sinfo.export.flow_depth.csv.header.discharge";
-
-    public static final String CSV_LABEL_HEADER = "sinfo.export.flow_depth.csv.header.label";
-
-    public static final String CSV_GAUGE_HEADER = "sinfo.export.flow_depth.csv.header.gauge";
-
-    public static final String CSV_LOCATION_HEADER = "sinfo.export.flow_depth.csv.header.location";
-
-    public static final String CSV_META_HEADER_WATERLEVEL = "sinfo.export.flow_depth.csv.meta.header.waterlevel";
-
-    public static final String CSV_META_HEADER_WATERLEVEL_NAME = "sinfo.export.flow_depth.csv.meta.header.waterlevel.name";
-
-    public static final String CSV_META_HEADER_WATERLEVEL_GAUGE = "sinfo.export.flow_depth.csv.meta.header.waterlevel.gauge";
+    String CSV_META_HEADER_RESULT_LABEL = "sinfo.export.flow_depth.csv.meta.header.result.label";
 
-    public static final String CSV_META_HEADER_WATERLEVEL_YEAR = "sinfo.export.flow_depth.csv.meta.header.waterlevel.year";
-
-    public static final String CSV_META_RANGE = "sinfo.export.flow_depth.csv.meta.range";
-
-    public static final String CSV_META_RANGE_LABEL = "sinfo.export.flow_depth.csv.meta.range.label";
+    String CSV_META_VERSION = "sinfo.export.flow_depth.csv.meta.version";
 
-    public static final String CSV_META_HEIGHT_UNIT_RIVER = "sinfo.export.flow_depth.csv.meta.height_unit.river";
-
-    public static final String UNIT_M = "m";
+    String CSV_META_VERSION_LABEL = "sinfo.export.flow_depth.csv.meta.version.label";
 
-    public static final String UNIT_CM = "cm";
+    String CSV_META_USER = "sinfo.export.flow_depth.csv.meta.user";
 
-    public static final String UNIT_CUBIC_M = "m³/s";
+    String CSV_META_USER_LABEL = "sinfo.export.flow_depth.csv.meta.user.label";
+
+    String CSV_META_CREATION = "sinfo.export.flow_depth.csv.meta.creation";
+
+    String CSV_META_CREATION_LABEL = "sinfo.export.flow_depth.csv.meta.creation.label";
+
+    String CSV_META_RIVER = "sinfo.export.flow_depth.csv.meta.river";
+
+    String CSV_META_RIVER_LABEL = "sinfo.export.flow_depth.csv.meta.river.label";
+
+    String CSV_KM_HEADER = "sinfo.export.flow_depth.csv.header.km";
+
+    String CSV_MEAN_BED_HEIGHT_HEADER = "sinfo.export.flow_depth.csv.header.mean_bed_height";
+
+    String CSV_WATERLEVEL_HEADER = "sinfo.export.flow_depth.csv.header.waterlevel";
+
+    String CSV_DISCHARGE_HEADER = "sinfo.export.flow_depth.csv.header.discharge";
+
+    String CSV_LABEL_HEADER = "sinfo.export.flow_depth.csv.header.label";
+
+    String CSV_GAUGE_HEADER = "sinfo.export.flow_depth.csv.header.gauge";
+
+    String CSV_LOCATION_HEADER = "sinfo.export.flow_depth.csv.header.location";
+
+    String CSV_SOUNDING_HEADER = "sinfo.export.flow_depth.csv.header.sounding";
+
+    String CSV_META_HEADER_WATERLEVEL = "sinfo.export.flow_depth.csv.meta.header.waterlevel";
+
+    String CSV_META_HEADER_WATERLEVEL_NAME = "sinfo.export.flow_depth.csv.meta.header.waterlevel.name";
+
+    String CSV_META_HEADER_WATERLEVEL_GAUGE = "sinfo.export.flow_depth.csv.meta.header.waterlevel.gauge";
+
+    String CSV_META_HEADER_WATERLEVEL_YEAR = "sinfo.export.flow_depth.csv.meta.header.waterlevel.year";
+
+    String CSV_META_RANGE = "sinfo.export.flow_depth.csv.meta.range";
+
+    String CSV_META_RANGE_LABEL = "sinfo.export.flow_depth.csv.meta.range.label";
+
+    String CSV_META_HEIGHT_UNIT_RIVER = "sinfo.export.flow_depth.csv.meta.height_unit.river";
+
+    String CSV_MEAN_BED_HEIGHT_HEADER_SHORT = "sinfo.export.flow_depth.csv.header.mean_bed_height.short";
+
+    String UNIT_M = "m";
+
+    String UNIT_CM = "cm";
+
+    String UNIT_CUBIC_M = "m³/s";
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoCalculationResult.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoCalculationResult.java	Tue Mar 13 18:49:33 2018 +0100
@@ -13,13 +13,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 
-import org.dive4elements.river.artifacts.sinfo.tkhcalculation.SoilKind;
 import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
 
-import gnu.trove.TDoubleArrayList;
-
 /**
  * @author Gernot Belger
  */
@@ -31,14 +27,11 @@
 
     private final String label;
 
-    private final boolean hasTkh;
-
     private final WstInfo wst;
 
-    public AbstractSInfoCalculationResult(final String label, final WstInfo wst, final boolean hasTkh, final Collection<ROW> rows) {
+    public AbstractSInfoCalculationResult(final String label, final WstInfo wst, final Collection<ROW> rows) {
         this.label = label;
         this.wst = wst;
-        this.hasTkh = hasTkh;
         this.rows = new ArrayList<>(rows);
     }
 
@@ -46,10 +39,6 @@
         return this.label;
     }
 
-    public final boolean hasTkh() {
-        return this.hasTkh;
-    }
-
     public final WstInfo getWst() {
         return this.wst;
     }
@@ -61,139 +50,4 @@
     public final Collection<ROW> getRows() {
         return Collections.unmodifiableCollection(this.rows);
     }
-
-    public double[][] getFlowDepthPoints() {
-
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getFlowDepth());
-        }
-
-        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
-    }
-
-    public double[][] getFlowDepthTkhPoints() {
-
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getFlowDepthWithTkh());
-        }
-
-        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
-    }
-
-    public final double[][] getTkhUpPoints() {
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-        final List<SoilKind> kinds = new ArrayList<>(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getTkhUp());
-            kinds.add(row.getTkhKind());
-        }
-
-        return adjustTkhVisualization(xPoints, yPoints, kinds);
-    }
-
-    public final double[][] getTkhDownPoints() {
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-        final List<SoilKind> kinds = new ArrayList<>(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getTkhDown());
-            kinds.add(row.getTkhKind());
-        }
-
-        return adjustTkhVisualization(xPoints, yPoints, kinds);
-    }
-
-    public double[][] getVelocityPoints() {
-
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getVelocity());
-        }
-
-        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
-    }
-
-    public double[][] getD50Points() {
-
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getD50());
-        }
-
-        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
-    }
-
-    public double[][] getTauPoints() {
-
-        final TDoubleArrayList xPoints = new TDoubleArrayList(this.rows.size());
-        final TDoubleArrayList yPoints = new TDoubleArrayList(this.rows.size());
-
-        for (final ROW row : this.rows) {
-            xPoints.add(row.getStation());
-            yPoints.add(row.getTau());
-        }
-
-        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
-    }
-
-    /**
-     * the up and down points must be further adjusted for visualization, see Mail Hr. Reiß
-     * basically we need to introduce extra points when the kind changes, so we get vertical lines in that case
-     */
-    private double[][] adjustTkhVisualization(final TDoubleArrayList xPoints, final TDoubleArrayList yPoints, final List<SoilKind> kinds) {
-
-        final TDoubleArrayList adjustedX = new TDoubleArrayList(xPoints.size());
-        final TDoubleArrayList adjustedY = new TDoubleArrayList(yPoints.size());
-
-        adjustedX.add(xPoints.get(0));
-        adjustedY.add(yPoints.get(0));
-
-        for (int i = 1; i < xPoints.size(); i++) {
-
-            final SoilKind kind1 = kinds.get(i - 1);
-            final SoilKind kind2 = kinds.get(i);
-
-            if (kind1 != kind2) {
-                /* introduce two extra points in order to create a vertical line in the middle of the two adjacent points */
-                final double x1 = xPoints.get(i - 1);
-                final double y1 = yPoints.get(i - 1);
-                final double x2 = xPoints.get(i);
-                final double y2 = yPoints.get(i);
-
-                final double middleX = (x1 + x2) / 2;
-
-                // REMARK: we can't produce a 100% vertical line, as the area-renderer will not work correctly
-                adjustedX.add(middleX - 0.0001);
-                adjustedY.add(y1);
-
-                adjustedX.add(middleX + 0.0001);
-                adjustedY.add(y2);
-            }
-
-            /* always add the real point now */
-            adjustedX.add(xPoints.get(i));
-            adjustedY.add(yPoints.get(i));
-        }
-
-        return new double[][] { adjustedX.toNativeArray(), adjustedY.toNativeArray() };
-    }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoExporter.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoExporter.java	Tue Mar 13 18:49:33 2018 +0100
@@ -10,17 +10,26 @@
 package org.dive4elements.river.artifacts.sinfo.common;
 
 import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.NumberFormat;
 import java.util.Collection;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
+import org.apache.commons.lang.math.DoubleRange;
 import org.apache.log4j.Logger;
 import org.dive4elements.artifacts.CallMeta;
 import org.dive4elements.artifacts.common.utils.Config;
+import org.dive4elements.river.FLYS;
 import org.dive4elements.river.artifacts.model.CalculationResult;
 import org.dive4elements.river.artifacts.resources.Resources;
+import org.dive4elements.river.artifacts.sinfo.SInfoI18NStrings;
+import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
 import org.dive4elements.river.artifacts.sinfo.util.MetaAndTableJRDataSource;
 import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
+import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
 import org.dive4elements.river.exports.AbstractExporter;
 
 import au.com.bytecode.opencsv.CSVWriter;
@@ -35,6 +44,20 @@
  */
 public abstract class AbstractSInfoExporter<ROW extends AbstractSInfoResultRow, RESULT extends AbstractSInfoCalculationResult<ROW>, RESULTS extends AbstractSInfoCalculationResults<ROW, RESULT>> extends AbstractExporter {
 
+    private static final String CSV_META_HEADER_SOUNDING = "sinfo.export.flow_depth.csv.meta.header.sounding";
+
+    private static final String CSV_META_HEADER_SOUNDING_YEAR = "sinfo.export.flow_depth.csv.meta.header.sounding.year";
+
+    private static final String CSV_META_HEADER_SOUNDING_TYPE = "sinfo.export.flow_depth.csv.meta.header.sounding.type";
+
+    private static final String CSV_META_HEADER_SOUNDING_EVALUATOR = "sinfo.export.flow_depth.csv.meta.header.sounding.evaluator";
+
+    private static final String CSV_META_HEADER_SOUNDING_PRJ = "sinfo.export.flow_depth.csv.meta.header.sounding.prj";
+
+    private static final String CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL = "sinfo.export.flow_depth.csv.meta.header.sounding.elevationmodel";
+
+    private static final String CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL_ORIGINAL = "sinfo.export.flow_depth.csv.meta.header.sounding.elevationmodel.original";
+
     /** The storage that contains the current calculation result. */
     private RESULTS data = null;
 
@@ -173,4 +196,98 @@
     }
 
     protected abstract String[] formatPDFRow(RESULTS results, final ROW row);
+
+    protected final void writeCSVGlobalMetadataDefaults(final CSVWriter writer, final AbstractSInfoCalculationResults<?, ?> results) {
+
+        final String calcModeLabel = results.getCalcModeLabel();
+        final RiverInfo river = results.getRiver();
+        final DoubleRange calcRange = results.getCalcRange();
+
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_RESULT, msg(SInfoI18NStrings.CSV_META_HEADER_RESULT_LABEL), river.getName(), calcModeLabel);
+
+        // "# FLYS-Version: "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_VERSION, msg(SInfoI18NStrings.CSV_META_VERSION_LABEL), FLYS.VERSION);
+
+        // "# Bearbeiter: "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_USER, msg(SInfoI18NStrings.CSV_META_USER_LABEL), results.getUser());
+
+        // "# Datum der Erstellung: "
+        final Locale locale = Resources.getLocale(this.context.getMeta());
+        final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_CREATION, msg(SInfoI18NStrings.CSV_META_CREATION_LABEL), df.format(new Date()));
+
+        // "# Gewässer: "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_RIVER, msg(SInfoI18NStrings.CSV_META_RIVER_LABEL), river.getName());
+
+        // "# Höhensystem des Flusses: "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEIGHT_UNIT_RIVER, river.getWstUnit());
+
+        // "# Ort/Bereich (km): "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_RANGE, msg(SInfoI18NStrings.CSV_META_RANGE_LABEL),
+                getKmFormatter().format(calcRange.getMinimumDouble()), getKmFormatter().format(calcRange.getMaximumDouble()));
+    }
+
+    protected final void writeCSVSoundingMetadata(final CSVWriter writer, final BedHeightInfo sounding) {
+        // "##METADATEN PEILUNG"
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING);
+
+        // "# Jahr der Peilung: "
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_YEAR, Integer.toString(sounding.getYear()));
+        // "# Aufnahmeart: "
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_TYPE, sounding.getType());
+        // "# Auswerter: "
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_EVALUATOR, sounding.getEvaluationBy());
+        // "# Lagesystem: "
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_PRJ, sounding.getLocationSystem());
+        // "# Höhensystem: "
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL, sounding.getCurElevationModelUnit());
+        // "# ursprüngliches Höhensystem: "
+        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL_ORIGINAL, sounding.getOldElevationModelUnit());
+    }
+
+    protected final void writeCSVWaterlevelMetadata(final CSVWriter writer, final WstInfo wst) {
+        // "##METADATEN WASSERSPIEGELLAGE"
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL);
+
+        // "# Bezeichnung der Wasserspiegellage: "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_NAME, wst.getLabel());
+
+        // "# Bezugspegel: "
+        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_GAUGE, wst.getGauge());
+
+        // "# Jahr/Zeitraum der Wasserspiegellage: "
+        final int year = wst.getYear();
+        if (year > 0)
+            writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_YEAR, Integer.toString(year));
+    }
+
+    protected final void addJRMetaDataDefaults(final MetaAndTableJRDataSource source, final AbstractSInfoCalculationResults<?, ?> results) {
+
+        final RiverInfo river = results.getRiver();
+        final String wstUnitName = river.getWstUnit();
+
+        source.addMetaData("header", msg(SInfoI18NStrings.CSV_META_HEADER_RESULT_LABEL));
+        source.addMetaData("calcMode", results.getCalcModeLabel());
+
+        source.addMetaData("version_label", msg(SInfoI18NStrings.CSV_META_VERSION_LABEL));
+        source.addMetaData("version", FLYS.VERSION);
+
+        source.addMetaData("user_label", msg(SInfoI18NStrings.CSV_META_USER_LABEL));
+        source.addMetaData("user", results.getUser());
+
+        final Locale locale = Resources.getLocale(this.context.getMeta());
+        final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
+        source.addMetaData("date_label", msg(SInfoI18NStrings.CSV_META_CREATION_LABEL));
+        source.addMetaData("date", df.format(new Date()));
+
+        source.addMetaData("river_label", msg(SInfoI18NStrings.CSV_META_RIVER_LABEL));
+        source.addMetaData("river", river.getName());
+        source.addMetaData("river_unit", wstUnitName);
+
+        final DoubleRange calcRange = results.getCalcRange();
+        final NumberFormat kmFormatter = getKmFormatter();
+        final String rangeValue = String.format("%s - %s", kmFormatter.format(calcRange.getMinimumDouble()), kmFormatter.format(calcRange.getMaximumDouble()));
+        source.addMetaData("range_label", msg(SInfoI18NStrings.CSV_META_RANGE_LABEL));
+        source.addMetaData("range", rangeValue);
+    }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoLineProcessor.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoLineProcessor.java	Tue Mar 13 18:49:33 2018 +0100
@@ -26,7 +26,7 @@
 import org.dive4elements.river.jfree.StyledXYSeries;
 import org.dive4elements.river.themes.ThemeDocument;
 
-abstract class AbstractSInfoLineProcessor extends AbstractSInfoProcessor {
+abstract class AbstractSInfoLineProcessor<RESULT extends AbstractSInfoCalculationResult<?>> extends AbstractSInfoProcessor {
 
     public AbstractSInfoLineProcessor(final String i18nAxisLabel, final Set<String> handledFacetType) {
         super(i18nAxisLabel, handledFacetType);
@@ -44,7 +44,8 @@
         series.putMetaData(metaData, artifact, context);
 
         final String facetName = bundle.getFacetName();
-        final AbstractSInfoCalculationResult<?> data = (AbstractSInfoCalculationResult<?>) bundle.getData(context);
+        @SuppressWarnings("unchecked")
+        final RESULT data = (RESULT) bundle.getData(context);
         if (data == null) {
             // Check has been here before so we keep it for security reasons
             // this should never happen though.
@@ -74,7 +75,7 @@
         return scales.getRadius(river, start, end);
     }
 
-    private double[][] generatePoints(final CallContext context, final Artifact artifact, final AbstractSInfoCalculationResult<?> data,
+    private double[][] generatePoints(final CallContext context, final Artifact artifact, final RESULT data,
             final String facetName) {
 
         final double[][] points = doGetPoints(data, facetName);
@@ -87,7 +88,7 @@
         return points;
     }
 
-    protected abstract double[][] doGetPoints(AbstractSInfoCalculationResult<?> data, String facetName);
+    protected abstract double[][] doGetPoints(RESULT data, String facetName);
 
     private double[][] movingAverage(final Double radius, final double[][] points) {
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoResultRow.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractSInfoResultRow.java	Tue Mar 13 18:49:33 2018 +0100
@@ -11,9 +11,6 @@
 
 import java.io.Serializable;
 
-import org.dive4elements.river.artifacts.sinfo.tkhcalculation.SoilKind;
-import org.dive4elements.river.artifacts.sinfo.tkhcalculation.Tkh;
-
 /**
  * Contains common result data of flow-depth- and tkh-calculations.
  *
@@ -22,49 +19,18 @@
 public abstract class AbstractSInfoResultRow implements Serializable {
     private static final long serialVersionUID = 1L;
 
-    private final Tkh tkh;
-
     private final String waterlevelLabel;
 
     private final String gauge;
 
     private final String location;
 
-    public AbstractSInfoResultRow(final Tkh tkh, final String waterlevelLabel, final String gauge, final String location) {
-        this.tkh = tkh;
+    public AbstractSInfoResultRow(final String waterlevelLabel, final String gauge, final String location) {
         this.waterlevelLabel = waterlevelLabel;
         this.gauge = gauge;
         this.location = location;
     }
 
-    public final double getStation() {
-        return this.tkh.getStation();
-    }
-
-    public final SoilKind getTkhKind() {
-        return this.tkh.getKind();
-    }
-
-    public final double getTkh() {
-        return this.tkh.getTkh();
-    }
-
-    public final double getTkhUp() {
-        return this.tkh.getUp();
-    }
-
-    public final double getTkhDown() {
-        return this.tkh.getDown();
-    }
-
-    public final double getWaterlevel() {
-        return this.tkh.getWaterlevel();
-    }
-
-    public final double getDischarge() {
-        return this.tkh.getDischarge();
-    }
-
     public final String getWaterlevelLabel() {
         return this.waterlevelLabel;
     }
@@ -73,30 +39,6 @@
         return this.gauge;
     }
 
-    public final double getMeanBedHeight() {
-        return this.tkh.getMeanBedHeight();
-    }
-
-    public final double getFlowDepth() {
-        return this.tkh.getFlowDepth();
-    }
-
-    public double getFlowDepthWithTkh() {
-        return this.tkh.getFlowDepthTkh();
-    }
-
-    public double getVelocity() {
-        return this.tkh.getVelocity();
-    }
-
-    public double getD50() {
-        return this.tkh.getD50();
-    }
-
-    public double getTau() {
-        return this.tkh.getTau();
-    }
-
     public final String getLocation() {
         return this.location;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractTkhCalculationResult.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,189 @@
+/** 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.artifacts.sinfo.common;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.SoilKind;
+import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
+
+import gnu.trove.TDoubleArrayList;
+
+/**
+ * @author Gernot Belger
+ */
+public abstract class AbstractTkhCalculationResult<ROW extends AbstractTkhResultRow> extends AbstractSInfoCalculationResult<ROW> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final boolean hasTkh;
+
+    public AbstractTkhCalculationResult(final String label, final WstInfo wst, final boolean hasTkh, final Collection<ROW> rows) {
+        super(label, wst, rows);
+        this.hasTkh = hasTkh;
+    }
+
+    public final boolean hasTkh() {
+        return this.hasTkh;
+    }
+
+    public double[][] getFlowDepthPoints() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getFlowDepth());
+        }
+
+        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
+    }
+
+    public double[][] getFlowDepthTkhPoints() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getFlowDepthWithTkh());
+        }
+
+        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
+    }
+
+    public final double[][] getTkhUpPoints() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+        final List<SoilKind> kinds = new ArrayList<>(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getTkhUp());
+            kinds.add(row.getTkhKind());
+        }
+
+        return adjustTkhVisualization(xPoints, yPoints, kinds);
+    }
+
+    public final double[][] getTkhDownPoints() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+        final List<SoilKind> kinds = new ArrayList<>(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getTkhDown());
+            kinds.add(row.getTkhKind());
+        }
+
+        return adjustTkhVisualization(xPoints, yPoints, kinds);
+    }
+
+    public double[][] getVelocityPoints() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getVelocity());
+        }
+
+        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
+    }
+
+    public double[][] getD50Points() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getD50());
+        }
+
+        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
+    }
+
+    public double[][] getTauPoints() {
+
+        final Collection<ROW> rows = getRows();
+
+        final TDoubleArrayList xPoints = new TDoubleArrayList(rows.size());
+        final TDoubleArrayList yPoints = new TDoubleArrayList(rows.size());
+
+        for (final ROW row : rows) {
+            xPoints.add(row.getStation());
+            yPoints.add(row.getTau());
+        }
+
+        return new double[][] { xPoints.toNativeArray(), yPoints.toNativeArray() };
+    }
+
+    /**
+     * the up and down points must be further adjusted for visualization, see Mail Hr. Reiß
+     * basically we need to introduce extra points when the kind changes, so we get vertical lines in that case
+     */
+    private double[][] adjustTkhVisualization(final TDoubleArrayList xPoints, final TDoubleArrayList yPoints, final List<SoilKind> kinds) {
+
+        final TDoubleArrayList adjustedX = new TDoubleArrayList(xPoints.size());
+        final TDoubleArrayList adjustedY = new TDoubleArrayList(yPoints.size());
+
+        adjustedX.add(xPoints.get(0));
+        adjustedY.add(yPoints.get(0));
+
+        for (int i = 1; i < xPoints.size(); i++) {
+
+            final SoilKind kind1 = kinds.get(i - 1);
+            final SoilKind kind2 = kinds.get(i);
+
+            if (kind1 != kind2) {
+                /* introduce two extra points in order to create a vertical line in the middle of the two adjacent points */
+                final double x1 = xPoints.get(i - 1);
+                final double y1 = yPoints.get(i - 1);
+                final double x2 = xPoints.get(i);
+                final double y2 = yPoints.get(i);
+
+                final double middleX = (x1 + x2) / 2;
+
+                // REMARK: we can't produce a 100% vertical line, as the area-renderer will not work correctly
+                adjustedX.add(middleX - 0.0001);
+                adjustedY.add(y1);
+
+                adjustedX.add(middleX + 0.0001);
+                adjustedY.add(y2);
+            }
+
+            /* always add the real point now */
+            adjustedX.add(xPoints.get(i));
+            adjustedY.add(yPoints.get(i));
+        }
+
+        return new double[][] { adjustedX.toNativeArray(), adjustedY.toNativeArray() };
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/AbstractTkhResultRow.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,81 @@
+/** 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.artifacts.sinfo.common;
+
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.SoilKind;
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.Tkh;
+
+/**
+ * Contains common result data of flow-depth- and tkh-calculations.
+ *
+ * @author Gernot Belger
+ */
+public abstract class AbstractTkhResultRow extends AbstractSInfoResultRow {
+    private static final long serialVersionUID = 1L;
+
+    private final Tkh tkh;
+
+    public AbstractTkhResultRow(final Tkh tkh, final String waterlevelLabel, final String gauge, final String location) {
+        super(waterlevelLabel, gauge, location);
+        this.tkh = tkh;
+    }
+
+    public final double getStation() {
+        return this.tkh.getStation();
+    }
+
+    public final SoilKind getTkhKind() {
+        return this.tkh.getKind();
+    }
+
+    public final double getTkh() {
+        return this.tkh.getTkh();
+    }
+
+    public final double getTkhUp() {
+        return this.tkh.getUp();
+    }
+
+    public final double getTkhDown() {
+        return this.tkh.getDown();
+    }
+
+    public final double getWaterlevel() {
+        return this.tkh.getWaterlevel();
+    }
+
+    public final double getDischarge() {
+        return this.tkh.getDischarge();
+    }
+
+    public final double getMeanBedHeight() {
+        return this.tkh.getMeanBedHeight();
+    }
+
+    public final double getFlowDepth() {
+        return this.tkh.getFlowDepth();
+    }
+
+    public double getFlowDepthWithTkh() {
+        return this.tkh.getFlowDepthTkh();
+    }
+
+    public double getVelocity() {
+        return this.tkh.getVelocity();
+    }
+
+    public double getD50() {
+        return this.tkh.getD50();
+    }
+
+    public double getTau() {
+        return this.tkh.getTau();
+    }
+}
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/D50Processor.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/D50Processor.java	Tue Mar 13 18:49:33 2018 +0100
@@ -18,7 +18,7 @@
 import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.states.DefaultState.ComputeType;
 
-public final class D50Processor extends AbstractSInfoLineProcessor {
+public final class D50Processor extends AbstractSInfoLineProcessor<AbstractTkhCalculationResult<?>> {
 
     // FIXME: check: filtered or not?
     public static final String FACET_TKH_D50_FILTERED = "sinfo_facet_d50.filtered";
@@ -40,7 +40,7 @@
     }
 
     @Override
-    protected double[][] doGetPoints(final AbstractSInfoCalculationResult<?> data, final String facetName) {
+    protected double[][] doGetPoints(final AbstractTkhCalculationResult<?> data, final String facetName) {
 
         if (FACET_TKH_D50_FILTERED.contentEquals(facetName))
             return data.getD50Points();
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/FlowDepthProcessor.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/FlowDepthProcessor.java	Tue Mar 13 18:49:33 2018 +0100
@@ -18,7 +18,7 @@
 import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.states.DefaultState.ComputeType;
 
-public final class FlowDepthProcessor extends AbstractSInfoLineProcessor {
+public final class FlowDepthProcessor extends AbstractSInfoLineProcessor<AbstractTkhCalculationResult<?>> {
 
     private static final String I18N_AXIS_LABEL = "sinfo.chart.flow_depth.section.yaxis.label";
 
@@ -47,7 +47,7 @@
     }
 
     @Override
-    protected double[][] doGetPoints(final AbstractSInfoCalculationResult<?> data, final String facetName) {
+    protected double[][] doGetPoints(final AbstractTkhCalculationResult<?> data, final String facetName) {
 
         if (FACET_FLOW_DEPTH_FILTERED.contentEquals(facetName))
             return data.getFlowDepthPoints();
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/TauProcessor.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/TauProcessor.java	Tue Mar 13 18:49:33 2018 +0100
@@ -18,7 +18,7 @@
 import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.states.DefaultState.ComputeType;
 
-public final class TauProcessor extends AbstractSInfoLineProcessor {
+public final class TauProcessor extends AbstractSInfoLineProcessor<AbstractTkhCalculationResult<?>> {
 
     // FIXME: check: filtered or not?
     public static final String FACET_TKH_TAU_FILTERED = "sinfo_facet_tau.filtered";
@@ -40,7 +40,7 @@
     }
 
     @Override
-    protected double[][] doGetPoints(final AbstractSInfoCalculationResult<?> data, final String facetName) {
+    protected double[][] doGetPoints(final AbstractTkhCalculationResult<?> data, final String facetName) {
 
         if (FACET_TKH_TAU_FILTERED.contentEquals(facetName))
             return data.getTauPoints();
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/TkhProcessor.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/TkhProcessor.java	Tue Mar 13 18:49:33 2018 +0100
@@ -49,7 +49,7 @@
         final CallContext context = generator.getCallContext();
 
         final String facetName = bundle.getFacetName();
-        final AbstractSInfoCalculationResult<?> data = (AbstractSInfoCalculationResult<?>) bundle.getData(context);
+        final AbstractTkhCalculationResult<?> data = (AbstractTkhCalculationResult<?>) bundle.getData(context);
         if (data == null) {
             // Check has been here before so we keep it for security reasons
             // this should never happen though.
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/VelocityProcessor.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/common/VelocityProcessor.java	Tue Mar 13 18:49:33 2018 +0100
@@ -18,7 +18,7 @@
 import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.states.DefaultState.ComputeType;
 
-public final class VelocityProcessor extends AbstractSInfoLineProcessor {
+public final class VelocityProcessor extends AbstractSInfoLineProcessor<AbstractTkhCalculationResult<?>> {
 
     // FIXME: check: filtered or not?
     public static final String FACET_TKH_VELOCITY_FILTERED = "sinfo_facet_velocity.filtered";
@@ -40,7 +40,7 @@
     }
 
     @Override
-    protected double[][] doGetPoints(final AbstractSInfoCalculationResult<?> data, final String facetName) {
+    protected double[][] doGetPoints(final AbstractTkhCalculationResult<?> data, final String facetName) {
 
         if (FACET_TKH_VELOCITY_FILTERED.contentEquals(facetName))
             return data.getVelocityPoints();
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthAccess.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthAccess.java	Tue Mar 13 18:49:33 2018 +0100
@@ -10,7 +10,6 @@
 
 package org.dive4elements.river.artifacts.sinfo.flowdepth;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -18,7 +17,6 @@
 import org.dive4elements.river.artifacts.access.RangeAccess;
 import org.dive4elements.river.artifacts.sinfo.SINFOArtifact;
 import org.dive4elements.river.artifacts.sinfo.SinfoCalcMode;
-import org.dive4elements.river.backend.utils.StringUtil;
 
 /**
  * Access to the flow depth calculation type specific SInfo artifact data.
@@ -29,23 +27,8 @@
  * @author Gernot Belger
  */
 final class FlowDepthAccess extends RangeAccess {
-    public static class DifferencesPair {
-        private final String wstId;
-        private final String soundingId;
 
-        public DifferencesPair(final String wstId, final String soundingId) {
-            this.wstId = wstId;
-            this.soundingId = soundingId;
-        }
-
-        public String getWstId() {
-            return this.wstId;
-        }
-
-        public String getSoundingId() {
-            return this.soundingId;
-        }
-    }
+    private static final String FIELD_DIFFIDS = "diffids";
 
     private static final String FIELD_USE_TKH = "use_transport_bodies"; //$NON-NLS-1$
 
@@ -68,25 +51,15 @@
         return useTkh == null ? false : useTkh;
     }
 
-    public Collection<DifferencesPair> getDifferencePairs() {
+    public Collection<WstSoundingIdPair> getDifferencePairs() {
 
-        final Collection<DifferencesPair> diffPairs = new ArrayList<>();
-
-        final String diffids = super.getString("diffids");
+        final String diffids = getString(FIELD_DIFFIDS);
         if (diffids == null) {
             // Should never happen as this is handled by the ui
             return Collections.emptyList();
         }
 
-        // FIXME: this way of parsing the datacage-ids is repeated all over flys!
-        final String datas[] = diffids.split("#");
-        for (int i = 0; i < datas.length; i += 2) {
-            final String leftId = StringUtil.unbracket(datas[i]);
-            final String rightId = StringUtil.unbracket(datas[i + 1]);
-
-            diffPairs.add(new DifferencesPair(leftId, rightId));
-        }
-
-        return Collections.unmodifiableCollection(diffPairs);
+        final Collection<WstSoundingIdPair> pairs = WstSoundingIdPair.parsePairs(diffids);
+        return pairs;
     }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculation.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculation.java	Tue Mar 13 18:49:33 2018 +0100
@@ -13,16 +13,15 @@
 
 import org.apache.commons.lang.math.DoubleRange;
 import org.dive4elements.artifacts.CallContext;
-import org.dive4elements.river.artifacts.BedHeightsArtifact;
 import org.dive4elements.river.artifacts.model.Calculation;
 import org.dive4elements.river.artifacts.model.CalculationResult;
 import org.dive4elements.river.artifacts.model.WKms;
 import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.sinfo.SINFOArtifact;
 import org.dive4elements.river.artifacts.sinfo.common.RiverInfoProvider;
-import org.dive4elements.river.artifacts.sinfo.flowdepth.FlowDepthAccess.DifferencesPair;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.DischargeValuesFinder;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.TkhCalculator;
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.WaterlevelValuesFinder;
 import org.dive4elements.river.artifacts.sinfo.tkhstate.BedHeightsFinder;
 import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils;
 import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
@@ -30,7 +29,6 @@
 import org.dive4elements.river.artifacts.states.WaterlevelData;
 import org.dive4elements.river.artifacts.states.WaterlevelFetcher;
 import org.dive4elements.river.model.River;
-import org.dive4elements.river.utils.RiverUtils;
 
 class FlowDepthCalculation {
 
@@ -51,7 +49,7 @@
         final River river = access.getRiver();
         final RiverInfo riverInfo = new RiverInfo(river);
 
-        final Collection<DifferencesPair> diffPairs = access.getDifferencePairs();
+        final Collection<WstSoundingIdPair> diffPairs = access.getDifferencePairs();
 
         final DoubleRange calcRange = access.getRange();
 
@@ -66,7 +64,7 @@
 
         final FlowDepthCalculationResults results = new FlowDepthCalculationResults(calcModeLabel, user, riverInfo, calcRange, useTkh);
 
-        for (final DifferencesPair diffPair : diffPairs) {
+        for (final WstSoundingIdPair diffPair : diffPairs) {
             final FlowDepthCalculationResult result = calculateResult(calcRange, diffPair, problems, infoProvider, useTkh);
             if (result != null)
                 results.addResult(result);
@@ -80,34 +78,29 @@
      *
      * @param infoProvider
      */
-    private FlowDepthCalculationResult calculateResult(final DoubleRange calcRange, final DifferencesPair diffPair,
-            final Calculation problems, final RiverInfoProvider infoProvider, final boolean useTkh) {
+    private FlowDepthCalculationResult calculateResult(final DoubleRange calcRange, final WstSoundingIdPair diffPair, final Calculation problems,
+            final RiverInfoProvider infoProvider, final boolean useTkh) {
 
         /* access real input data from database */
         final String soundingId = diffPair.getSoundingId();
         final String wstId = diffPair.getWstId();
 
-        final BedHeightsFinder bedHeight = loadBedHeight(soundingId, calcRange);
-        if (bedHeight == null) {
-            final String message = Resources.format(this.context.getMeta(), "Failed to access sounding with id '{0}'", soundingId);
-            problems.addProblem(message);
+        final BedHeightsFinder bedHeight = BedHeightsFinder.forId(this.context, soundingId, calcRange, problems);
+        if (bedHeight == null)
             return null;
-        }
 
         /* REMARK: fetch ALL wst kms, because we want to determine the original reference gauge */
-        final WaterlevelData waterlevel = new WaterlevelFetcher().findWaterlevel(this.context, wstId, Double.NaN, Double.NaN);
-        if (waterlevel == null) {
-            final String message = Resources.format(this.context.getMeta(), "Failed to access waterlevel with id '{0}'", wstId);
-            problems.addProblem(message);
+        final WaterlevelData waterlevel = new WaterlevelFetcher().findWaterlevel(this.context, wstId, Double.NaN, Double.NaN, problems);
+        if (waterlevel == null)
             return null;
-        }
+
         final WKms wstKms = waterlevel.getWkms();
 
         final String wspLabel = wstKms.getName();
         final String soundingLabel = bedHeight.getInfo().getDescription();
         final String label = String.format("%s - %s", wspLabel, soundingLabel);
 
-        checkYearDifference(label, waterlevel, bedHeight.getInfo().getYear(), problems);
+        FlowDepthUtils.checkYearDifference(label, waterlevel, bedHeight.getInfo().getYear(), problems);
         checkWaterlevelDiscretisation(wstKms, calcRange, problems);
         // TODO: prüfen, ob sohlhöhen die calcRange abdecken/überschneiden
 
@@ -117,61 +110,19 @@
         final int wspYear = waterlevel.getYear();
         final WstInfo wstInfo = new WstInfo(wspLabel, wspYear, riverInfoProvider.getReferenceGauge());
 
+        final WaterlevelValuesFinder waterlevelProvider = WaterlevelValuesFinder.fromKms(wstKms);
         final DischargeValuesFinder dischargeProvider = DischargeValuesFinder.fromKms(wstKms);
 
         final River river = riverInfoProvider.getRiver();
-        final TkhCalculator tkhCalculator = TkhCalculator.buildTkhCalculator(useTkh, this.context, problems, label, river, calcRange, dischargeProvider,
+        final TkhCalculator tkhCalculator = TkhCalculator.buildTkhCalculator(useTkh, this.context, problems, label, river, calcRange, waterlevelProvider,
+                dischargeProvider,
                 bedHeight);
 
-        final FlowDepthCalculator calculator = new FlowDepthCalculator(riverInfoProvider, wstKms, dischargeProvider, bedHeight, tkhCalculator);
+        final FlowDepthCalculator calculator = new FlowDepthCalculator(riverInfoProvider, wspLabel, bedHeight, tkhCalculator);
         return calculator.execute(label, wstInfo, calcRange);
     }
 
-
-    /**
-     * Checks the year difference between waterlevels and sounding, and issues a warning if too big.
-     *
-     * Zeitraum Zeitliche Differenz [a]
-     * X ≥ 1998 ± 3
-     * 1958 ≤ X < 1998 ± 6
-     * 1918 ≤ X < 1958 ± 12
-     * X < 1918 ± 25
-     */
-    private void checkYearDifference(final String label, final WaterlevelData waterlevel, final Integer soundingYear, final Calculation problems) {
-        if (soundingYear == null)
-            return;
-
-        final int wstYear = waterlevel.getYear();
-        if (wstYear < 0)
-            return;
-
-        final int maxDifference = getMaxDifferenceYears(soundingYear);
-
-        final int difference = Math.abs(soundingYear - wstYear);
-        if (difference > maxDifference) {
-            final String message = Resources.getMsg(this.context.getMeta(), "sinfo_calc_flow_depth.warning.year_difference", null, label, wstYear,
-                    soundingYear);
-            problems.addProblem(message);
-        }
-    }
-
-    private int getMaxDifferenceYears(final int year) {
-
-        if (year < 1918)
-            return 25;
-
-        if (1918 <= year && year < 1958)
-            return 12;
-
-        if (1958 <= year && year < 1998)
-            return 6;
-
-        /* >= 1998 */
-        return 3;
-    }
-
     /* Checks if the discretisation of the waterlevel exceeds 1000m */
-
     private void checkWaterlevelDiscretisation(final WKms wstKms, final DoubleRange calcRange, final Calculation problems) {
 
         final int size = wstKms.size();
@@ -190,37 +141,4 @@
             }
         }
     }
-
-    private BedHeightsFinder loadBedHeight(final String soundingId, final DoubleRange calcRange) {
-
-        // REMARK: absolutely unbelievable....
-        // The way how bed-heights (and other data too) is accessed is different for nearly every calculation-type
-        // throughout flys.
-        // The knowledge on how to parse the datacage-ids is spread through the complete code-base...
-
-        // We use here the way on how bed-heights are accessed by the BedDifferenceAccess/BedDifferenceCalculation, but
-        // this is plain random
-        final String[] parts = soundingId.split(";");
-
-        final BedHeightsArtifact artifact = (BedHeightsArtifact) RiverUtils.getArtifact(parts[0], this.context);
-
-        final Integer bedheightId = artifact.getDataAsInteger("height_id");
-        if (bedheightId == null) {
-            // FIXME: error message!
-            return null;
-        }
-
-        // REMARK: this only works with type 'single'; unclear on how to distinguish from epoch data (or whatever the
-        // other type means)
-        // Luckily, the requirement is to only access 'single' data here.
-        // final String bedheightType = artifact.getDataAsString("type");
-
-        // REMARK: BedDifferences uses this, but we also need the metadata of the BedHeight
-        // REMARK: second absolutely awful thing: BedHeight is a hibernate binding class, accessing the database via
-        // hibernate stuff
-        // BedHeightFactory uses its own (direct) way of accessing the data, with its own implemented data classes.
-        // return BedHeightFactory.getHeight(bedheightType, bedheightId, from, to);
-
-        return BedHeightsFinder.forId(bedheightId, calcRange);
-    }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculationResult.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculationResult.java	Tue Mar 13 18:49:33 2018 +0100
@@ -11,7 +11,7 @@
 
 import java.util.Collection;
 
-import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoCalculationResult;
+import org.dive4elements.river.artifacts.sinfo.common.AbstractTkhCalculationResult;
 import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
 import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
 
@@ -20,7 +20,7 @@
  *
  * @author Gernot Belger
  */
-final class FlowDepthCalculationResult extends AbstractSInfoCalculationResult<FlowDepthRow> {
+final class FlowDepthCalculationResult extends AbstractTkhCalculationResult<FlowDepthRow> {
 
     private static final long serialVersionUID = 1L;
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculator.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthCalculator.java	Tue Mar 13 18:49:33 2018 +0100
@@ -13,16 +13,11 @@
 import java.util.Collection;
 
 import org.apache.commons.lang.math.DoubleRange;
-import org.apache.commons.math.FunctionEvaluationException;
-import org.apache.commons.math.analysis.polynomials.PolynomialSplineFunction;
-import org.dive4elements.river.artifacts.model.WKms;
 import org.dive4elements.river.artifacts.sinfo.common.RiverInfoProvider;
-import org.dive4elements.river.artifacts.sinfo.tkhcalculation.DischargeValuesFinder;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.Tkh;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.TkhCalculator;
 import org.dive4elements.river.artifacts.sinfo.tkhstate.BedHeightsFinder;
 import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
-import org.dive4elements.river.utils.DoubleUtil;
 
 /**
  * @author Gernot Belger
@@ -31,33 +26,26 @@
 
     private final Collection<FlowDepthRow> rows = new ArrayList<>();
 
-    private final DischargeValuesFinder dischargeProvider;
-
     private final BedHeightsFinder bedHeight;
 
     private final TkhCalculator tkhCalculator;
 
-    private final PolynomialSplineFunction wstInterpolator;
-
     private final RiverInfoProvider riverInfoProvider;
 
     private final String bedHeightLabel;
 
     private final String wstLabel;
 
-    public FlowDepthCalculator(final RiverInfoProvider riverInfoProvider, final WKms wstKms,
-            final DischargeValuesFinder dischargeProvider, final BedHeightsFinder bedHeight, final TkhCalculator tkhCalculator) {
+    public FlowDepthCalculator(final RiverInfoProvider riverInfoProvider, final String wstLabel, final BedHeightsFinder bedHeight,
+            final TkhCalculator tkhCalculator) {
 
         this.riverInfoProvider = riverInfoProvider;
+        this.wstLabel = wstLabel;
 
-        this.dischargeProvider = dischargeProvider;
         this.bedHeight = bedHeight;
         this.tkhCalculator = tkhCalculator;
 
-        this.wstInterpolator = DoubleUtil.getLinearInterpolator(wstKms.allKms(), wstKms.allWs());
-
         this.bedHeightLabel = bedHeight.getInfo().getDescription();
-        this.wstLabel = wstKms.getName();
     }
 
     public FlowDepthCalculationResult execute(final String label, final WstInfo wstInfo, final DoubleRange calcRange) {
@@ -68,40 +56,21 @@
                 calculateResultRow(station);
         }
 
-        return new FlowDepthCalculationResult(label, wstInfo, this.bedHeight.getInfo(), this.tkhCalculator != null, this.rows);
+        final boolean hasTkh = this.tkhCalculator.hasTkh();
+
+        return new FlowDepthCalculationResult(label, wstInfo, this.bedHeight.getInfo(), hasTkh, this.rows);
     }
 
     private void calculateResultRow(final double station) {
 
-        try {
-            // FIXME: check out of range of waterlevel?
-            final double wst = this.wstInterpolator.value(station);
-
-            final Tkh tkh = calculateTkh(station, wst);
-
-            // REMARK: access the location once only during calculation
-            final String location = this.riverInfoProvider.getLocation(station);
-
-            // REMARK: access the gauge once only during calculation
-            final String gaugeLabel = this.riverInfoProvider.findGauge(station);
+        final Tkh tkh = this.tkhCalculator.getTkh(station);
 
-            this.rows.add(new FlowDepthRow(tkh, this.wstLabel, gaugeLabel, this.bedHeightLabel, location));
-        }
-        catch (final FunctionEvaluationException e) {
-            /* should only happen if out of range */
-            e.printStackTrace();
-            /* simply ignore */
-        }
-    }
+        // REMARK: access the location once only during calculation
+        final String location = this.riverInfoProvider.getLocation(station);
 
-    private Tkh calculateTkh(final double station, final double wst) throws FunctionEvaluationException {
-        if (this.tkhCalculator == null) {
-            final double discharge = this.dischargeProvider.getDischarge(station);
-            final double meanBedHeight = this.bedHeight.getMeanBedHeight(station);
-            final double flowDepth = wst - meanBedHeight;
-            return new Tkh(station, wst, meanBedHeight, flowDepth, discharge);
-        }
+        // REMARK: access the gauge once only during calculation
+        final String gaugeLabel = this.riverInfoProvider.findGauge(station);
 
-        return this.tkhCalculator.getTkh(station, wst);
+        this.rows.add(new FlowDepthRow(tkh, this.wstLabel, gaugeLabel, this.bedHeightLabel, location));
     }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthExporter.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthExporter.java	Tue Mar 13 18:49:33 2018 +0100
@@ -8,18 +8,11 @@
 
 package org.dive4elements.river.artifacts.sinfo.flowdepth;
 
-import java.text.DateFormat;
-import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
-import java.util.Locale;
 
 import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang.math.DoubleRange;
 import org.apache.log4j.Logger;
-import org.dive4elements.river.FLYS;
-import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.sinfo.SInfoI18NStrings;
 import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoExporter;
 import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
@@ -31,7 +24,7 @@
 import au.com.bytecode.opencsv.CSVWriter;
 
 /**
- * Generates different output formats (csv, pdf) of data that resulted from a flow depths computation.
+ * Generates different output formats (csv, pdf) of data that resulted from a flow depths min/max computation.
  *
  * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
  * @author Gernot Belger
@@ -42,28 +35,13 @@
     /** The log used in this exporter. */
     private static Logger log = Logger.getLogger(FlowDepthExporter.class);
 
-    private static final String CSV_FLOWDEPTH_HEADER = "sinfo.export.flow_depth.csv.header.flowdepth";
-    private static final String CSV_FLOWDEPTHTKH_HEADER = "sinfo.export.flow_depth.csv.header.flowdepthTkh";
+    private static final String CSV_FLOWDEPTHMINMAX_HEADER = "sinfo.export.flow_depth_minmax.csv.header.flowdepthminmax";
+
+    private static final String CSV_FLOWDEPTHTKHMINMAX_HEADER = "sinfo.export.flow_depth_minmax.csv.header.flowdepthTkh";
+
     private static final String CSV_TKH_HEADER = "sinfo.export.flow_depth.csv.header.tkh";
 
-    private static final String CSV_MEAN_BED_HEIGHT_HEADER_SHORT = "sinfo.export.flow_depth.csv.header.mean_bed_height.short";
-    private static final String CSV_SOUNDING_HEADER = "sinfo.export.flow_depth.csv.header.sounding";
-
-    private static final String CSV_META_HEADER_SOUNDING = "sinfo.export.flow_depth.csv.meta.header.sounding";
-
-    private static final String CSV_META_HEADER_SOUNDING_YEAR = "sinfo.export.flow_depth.csv.meta.header.sounding.year";
-
-    private static final String CSV_META_HEADER_SOUNDING_TYPE = "sinfo.export.flow_depth.csv.meta.header.sounding.type";
-
-    private static final String CSV_META_HEADER_SOUNDING_EVALUATOR = "sinfo.export.flow_depth.csv.meta.header.sounding.evaluator";
-
-    private static final String CSV_META_HEADER_SOUNDING_PRJ = "sinfo.export.flow_depth.csv.meta.header.sounding.prj";
-
-    private static final String CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL = "sinfo.export.flow_depth.csv.meta.header.sounding.elevationmodel";
-
-    private static final String CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL_ORIGINAL = "sinfo.export.flow_depth.csv.meta.header.sounding.elevationmodel.original";
-
-    private static final String JASPER_FILE = "/jasper/sinfo.flowdepth.jasper";
+    private static final String JASPER_FILE = "/jasper/sinfo.flowdepthminmax.jasper";
 
     @Override
     protected Logger getLog() {
@@ -71,70 +49,22 @@
     }
 
     @Override
-    protected void writeCSVResultMetadata(final CSVWriter writer, final FlowDepthCalculationResults results, final FlowDepthCalculationResult result) {
-
-        /* first some specific metadata */
-        final BedHeightInfo sounding = result.getSounding();
-        final WstInfo wst = result.getWst();
-
-        // "##METADATEN PEILUNG"
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING);
+    protected void writeCSVGlobalMetadata(final CSVWriter writer, final FlowDepthCalculationResults results) {
+        log.info("FlowDepthExporter.writeCSVMeta");
 
-        // "# Jahr der Peilung: "
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_YEAR, Integer.toString(sounding.getYear()));
-        // "# Aufnahmeart: "
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_TYPE, sounding.getType());
-        // "# Auswerter: "
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_EVALUATOR, sounding.getEvaluationBy());
-        // "# Lagesystem: "
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_PRJ, sounding.getLocationSystem());
-        // "# Höhensystem: "
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL, sounding.getCurElevationModelUnit());
-        // "# ursprüngliches Höhensystem: "
-        writeCSVMetaEntry(writer, CSV_META_HEADER_SOUNDING_ELEVATIOIN_MODEL_ORIGINAL, sounding.getOldElevationModelUnit());
+        super.writeCSVGlobalMetadataDefaults(writer, results);
 
-        // "##METADATEN WASSERSPIEGELLAGE"
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL);
-        // "# Bezeichnung der Wasserspiegellage: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_NAME, wst.getLabel());
-        // "# Bezugspegel: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_GAUGE, wst.getGauge());
-        // "# Jahr/Zeitraum der Wasserspiegellage: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_YEAR, Integer.toString(wst.getYear()));
+        writer.writeNext(new String[] { "" });
     }
 
     @Override
-    protected void writeCSVGlobalMetadata(final CSVWriter writer, final FlowDepthCalculationResults results) {
-        log.info("FlowDepthExporter.writeCSVMeta");
-
-        final String calcModeLabel = results.getCalcModeLabel();
-        final RiverInfo river = results.getRiver();
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_RESULT, msg(SInfoI18NStrings.CSV_META_HEADER_RESULT_LABEL), river.getName(), calcModeLabel);
-
-        // "# FLYS-Version: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_VERSION, msg(SInfoI18NStrings.CSV_META_VERSION_LABEL), FLYS.VERSION);
-
-        // "# Bearbeiter: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_USER, msg(SInfoI18NStrings.CSV_META_USER_LABEL), results.getUser());
+    protected void writeCSVResultMetadata(final CSVWriter writer, final FlowDepthCalculationResults results, final FlowDepthCalculationResult result) {
 
-        // "# Datum der Erstellung: "
-        final Locale locale = Resources.getLocale(this.context.getMeta());
-        final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_CREATION, msg(SInfoI18NStrings.CSV_META_CREATION_LABEL), df.format(new Date()));
-
-        // "# Gewässer: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_RIVER, msg(SInfoI18NStrings.CSV_META_RIVER_LABEL), river.getName());
+        final BedHeightInfo sounding = result.getSounding();
+        super.writeCSVSoundingMetadata(writer, sounding);
 
-        // "# Höhensystem des Flusses: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEIGHT_UNIT_RIVER, river.getWstUnit());
-
-        // "# Ort/Bereich (km): "
-        final DoubleRange calcRange = results.getCalcRange();
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_RANGE, msg(SInfoI18NStrings.CSV_META_RANGE_LABEL),
-                getKmFormatter().format(calcRange.getMinimumDouble()),
-                getKmFormatter().format(calcRange.getMaximumDouble()));
-
-        writer.writeNext(new String[] { "" });
+        final WstInfo wst = result.getWst();
+        super.writeCSVWaterlevelMetadata(writer, wst);
     }
 
     /**
@@ -152,9 +82,9 @@
         final Collection<String> header = new ArrayList<>(11);
 
         header.add(msg(SInfoI18NStrings.CSV_KM_HEADER));
-        header.add(msgUnit(CSV_FLOWDEPTH_HEADER, SInfoI18NStrings.UNIT_M));
+        header.add(msgUnit(CSV_FLOWDEPTHMINMAX_HEADER, SInfoI18NStrings.UNIT_M));
         if (getData().isUseTkh()) {
-            header.add(msgUnit(CSV_FLOWDEPTHTKH_HEADER, SInfoI18NStrings.UNIT_M));
+            header.add(msgUnit(CSV_FLOWDEPTHTKHMINMAX_HEADER, SInfoI18NStrings.UNIT_M));
             header.add(msgUnit(CSV_TKH_HEADER, SInfoI18NStrings.UNIT_CM));
         }
 
@@ -163,7 +93,7 @@
         header.add(msg(SInfoI18NStrings.CSV_LABEL_HEADER));
         header.add(msg(SInfoI18NStrings.CSV_GAUGE_HEADER));
         header.add(msgUnit(SInfoI18NStrings.CSV_MEAN_BED_HEIGHT_HEADER, river.getWstUnit()));
-        header.add(msg(CSV_SOUNDING_HEADER));
+        header.add(msg(SInfoI18NStrings.CSV_SOUNDING_HEADER));
         header.add(msg(SInfoI18NStrings.CSV_LOCATION_HEADER));
 
         writer.writeNext(header.toArray(new String[header.size()]));
@@ -235,45 +165,20 @@
     @Override
     protected final void addJRMetaData(final MetaAndTableJRDataSource source, final FlowDepthCalculationResults results) {
 
-        final RiverInfo river = results.getRiver();
-        final String wstUnitName = river.getWstUnit();
-
         /* general metadata */
-        source.addMetaData("header", msg(SInfoI18NStrings.CSV_META_HEADER_RESULT_LABEL));
-        source.addMetaData("calcMode", results.getCalcModeLabel());
-
-        source.addMetaData("version_label", msg(SInfoI18NStrings.CSV_META_VERSION_LABEL));
-        source.addMetaData("version", FLYS.VERSION);
-
-        source.addMetaData("user_label", msg(SInfoI18NStrings.CSV_META_USER_LABEL));
-        source.addMetaData("user", results.getUser());
-
-        final Locale locale = Resources.getLocale(this.context.getMeta());
-        final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
-        source.addMetaData("date_label", msg(SInfoI18NStrings.CSV_META_CREATION_LABEL));
-        source.addMetaData("date", df.format(new Date()));
-
-        source.addMetaData("river_label", msg(SInfoI18NStrings.CSV_META_RIVER_LABEL));
-        source.addMetaData("river", river.getName());
-
-        final DoubleRange calcRange = results.getCalcRange();
-        final NumberFormat kmFormatter = getKmFormatter();
-        final String rangeValue = String.format("%s - %s", kmFormatter.format(calcRange.getMinimumDouble()), kmFormatter.format(calcRange.getMaximumDouble()));
-        source.addMetaData("range_label", msg(SInfoI18NStrings.CSV_META_RANGE_LABEL));
-        source.addMetaData("range", rangeValue);
+        super.addJRMetaDataDefaults(source, results);
 
         /* column headings */
         source.addMetaData("station_header", msg(SInfoI18NStrings.CSV_KM_HEADER));
-        source.addMetaData("flowdepth_header", msg(CSV_FLOWDEPTH_HEADER));
-        source.addMetaData("flowdepth_tkh_header", msg(CSV_FLOWDEPTHTKH_HEADER));
+        source.addMetaData("flowdepth_header", msg(CSV_FLOWDEPTHMINMAX_HEADER));
+        source.addMetaData("flowdepth_tkh_header", msg(CSV_FLOWDEPTHTKHMINMAX_HEADER));
         source.addMetaData("tkh_header", msg(CSV_TKH_HEADER));
         source.addMetaData("waterlevel_header", msg(SInfoI18NStrings.CSV_WATERLEVEL_HEADER));
-        source.addMetaData("river_unit", wstUnitName);
         source.addMetaData("discharge_header", msg(SInfoI18NStrings.CSV_DISCHARGE_HEADER));
         source.addMetaData("waterlevel_name_header", msg(SInfoI18NStrings.CSV_LABEL_HEADER));
         source.addMetaData("gauge_header", msg(SInfoI18NStrings.CSV_GAUGE_HEADER));
-        source.addMetaData("bedheight_header", msg(CSV_MEAN_BED_HEIGHT_HEADER_SHORT));
-        source.addMetaData("sounding_name_header", msg(CSV_SOUNDING_HEADER));
+        source.addMetaData("bedheight_header", msg(SInfoI18NStrings.CSV_MEAN_BED_HEIGHT_HEADER_SHORT));
+        source.addMetaData("sounding_name_header", msg(SInfoI18NStrings.CSV_SOUNDING_HEADER));
         source.addMetaData("location_header", msg(SInfoI18NStrings.CSV_LOCATION_HEADER));
     }
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthPairSelectState.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthPairSelectState.java	Tue Mar 13 18:49:33 2018 +0100
@@ -1,6 +1,6 @@
 /** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
- * Software engineering by 
- *  Björnsen Beratende Ingenieure GmbH 
+ * 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)
@@ -15,13 +15,15 @@
  * @author Gernot Belger
  *
  */
-public class FlowDepthPairSelectState
-// FIXME: very ugly; but probably we will break the serialization of WaterlevelPairSelectState if we introduce an abstraction 
-extends WaterlevelPairSelectState {
+// FIXME: very ugly; but probably we will break the serialization of WaterlevelPairSelectState if we introduce an
+// abstraction
+public final class FlowDepthPairSelectState extends WaterlevelPairSelectState {
+
+    private static final long serialVersionUID = 1L;
 
     /** Specify to display a datacage_twin_panel. */
     @Override
     protected String getUIProvider() {
         return "sinfo_flowdepth_twin_panel";
     }
-}
+}
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthRow.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthRow.java	Tue Mar 13 18:49:33 2018 +0100
@@ -9,7 +9,7 @@
  */
 package org.dive4elements.river.artifacts.sinfo.flowdepth;
 
-import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoResultRow;
+import org.dive4elements.river.artifacts.sinfo.common.AbstractTkhResultRow;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.Tkh;
 
 /**
@@ -17,7 +17,7 @@
  *
  * @author Gernot Belger
  */
-final class FlowDepthRow extends AbstractSInfoResultRow {
+final class FlowDepthRow extends AbstractTkhResultRow {
     private static final long serialVersionUID = 1L;
 
     private final String soundingLabel;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/FlowDepthUtils.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,61 @@
+/** 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.artifacts.sinfo.flowdepth;
+
+import org.dive4elements.river.artifacts.model.Calculation;
+import org.dive4elements.river.artifacts.states.WaterlevelData;
+
+/**
+ * @author Gernot Belger
+ */
+public final class FlowDepthUtils {
+
+    private FlowDepthUtils() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Checks the year difference between waterlevels and sounding, and issues a warning if too big.
+     *
+     * Zeitraum Zeitliche Differenz [a]
+     * X ≥ 1998 ± 3
+     * 1958 ≤ X < 1998 ± 6
+     * 1918 ≤ X < 1958 ± 12
+     * X < 1918 ± 25
+     */
+    public static void checkYearDifference(final String label, final WaterlevelData waterlevel, final int soundingYear, final Calculation problems) {
+
+        final int wstYear = waterlevel.getYear();
+        if (wstYear < 0)
+            return;
+
+        final int maxDifference = getMaxDifferenceYears(soundingYear);
+
+        final int difference = Math.abs(soundingYear - wstYear);
+        if (difference > maxDifference) {
+            problems.addProblem("sinfo_calc_flow_depth.warning.year_difference", label, wstYear, soundingYear);
+        }
+    }
+
+    public static int getMaxDifferenceYears(final int year) {
+
+        if (year < 1918)
+            return 25;
+
+        if (1918 <= year && year < 1958)
+            return 12;
+
+        if (1958 <= year && year < 1998)
+            return 6;
+
+        /* >= 1998 */
+        return 3;
+    }
+}
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/UseTransportBodiesChoice.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/UseTransportBodiesChoice.java	Tue Mar 13 18:49:33 2018 +0100
@@ -15,7 +15,9 @@
  */
 public class UseTransportBodiesChoice extends BooleanChoiceState {
 
+    private static final long serialVersionUID = 1L;
+
     public UseTransportBodiesChoice() {
-    	super( "useTransportBodies.option", "useTransportBodies.active", "useTransportBodies.inactive" );
-	}
+        super( "useTransportBodies.option", "useTransportBodies.active", "useTransportBodies.inactive" );
+    }
 }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepth/WstSoundingIdPair.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,56 @@
+/** 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.artifacts.sinfo.flowdepth;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.dive4elements.river.backend.utils.StringUtil;
+
+/**
+ * @author Gernot Belger
+ */
+public final class WstSoundingIdPair {
+
+    private final String wstId;
+
+    private final String soundingId;
+
+    public WstSoundingIdPair(final String wstId, final String soundingId) {
+        this.wstId = wstId;
+        this.soundingId = soundingId;
+    }
+
+    public String getWstId() {
+        return this.wstId;
+    }
+
+    public String getSoundingId() {
+        return this.soundingId;
+    }
+
+    public static List<WstSoundingIdPair> parsePairs(final String diffids) {
+
+        // FIXME: this way of parsing the datacage-ids is repeated all over flys!
+        final String datas[] = diffids.split("#");
+
+        final List<WstSoundingIdPair> diffPairs = new ArrayList<>(datas.length);
+
+        for (int i = 0; i < datas.length; i += 2) {
+            final String leftId = StringUtil.unbracket(datas[i]);
+            final String rightId = StringUtil.unbracket(datas[i + 1]);
+
+            diffPairs.add(new WstSoundingIdPair(leftId, rightId));
+        }
+
+        return Collections.unmodifiableList(diffPairs);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxAccess.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,105 @@
+/* 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.artifacts.sinfo.flowdepthminmax;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang.math.DoubleRange;
+import org.dive4elements.river.artifacts.access.RangeAccess;
+import org.dive4elements.river.artifacts.sinfo.SINFOArtifact;
+import org.dive4elements.river.artifacts.sinfo.SinfoCalcMode;
+import org.dive4elements.river.artifacts.sinfo.flowdepth.WstSoundingIdPair;
+
+/**
+ * Access to the flow depth calculation type specific SInfo artifact data.
+ * REMARK: this class is NOT intended to be hold in the results (or anywhere else), in order to avoid a permanent
+ * reference to the artifact instance.
+ * Hence we do NOT cache any data.
+ *
+ * @author Gernot Belger
+ */
+final class FlowDepthMinMaxAccess extends RangeAccess {
+
+    public static class MinMaxIdPair {
+
+        private final String wstId;
+
+        private final String minSoundingId;
+
+        private final String maxSoundingId;
+
+        public MinMaxIdPair(final String wstId, final String minSoundingId, final String maxSoundingId) {
+            this.wstId = wstId;
+            this.minSoundingId = minSoundingId;
+            this.maxSoundingId = maxSoundingId;
+        }
+
+        public String getWstId() {
+            return this.wstId;
+        }
+
+        public String getMinSoundingId() {
+            return this.minSoundingId;
+        }
+
+        public String getMaxSoundingId() {
+            return this.maxSoundingId;
+        }
+    }
+
+    private static final String FIELD_DIFFIDS = "diffids";
+
+    public FlowDepthMinMaxAccess(final SINFOArtifact artifact) {
+        super(artifact);
+
+        /* assert calculation mode */
+        final SinfoCalcMode calculationMode = artifact.getCalculationMode();
+        assert (calculationMode == SinfoCalcMode.sinfo_calc_flow_depth_minmax);
+    }
+
+    public DoubleRange getRange() {
+        final double from = getFrom();
+        final double to = getTo();
+        return new DoubleRange(from, to);
+    }
+
+    public Collection<MinMaxIdPair> getMinMaxPairs() {
+
+        final String diffids = getString(FIELD_DIFFIDS);
+
+        /* fetch the raw configured pairs */
+        final List<WstSoundingIdPair> diffPairs = WstSoundingIdPair.parsePairs(diffids);
+
+        /* now sort eleemnts into pairs of TL/KL */
+        // FIXME: use sounding-ids to determine how pairs fit together
+        // or, let the ui already enforce it somehow
+
+        final List<MinMaxIdPair> minMaxPairs = new ArrayList<>(diffPairs.size());
+        // FIXME: at the moment, we simply pair by order
+        for (int i = 0; i < diffPairs.size(); i++) {
+
+            final WstSoundingIdPair minPair = diffPairs.get(i);
+
+            if (i < diffPairs.size() - 1) {
+                final WstSoundingIdPair maxPair = diffPairs.get(i + 1);
+                minMaxPairs.add(new MinMaxIdPair(minPair.getWstId(), minPair.getSoundingId(), maxPair.getSoundingId()));
+                i++;
+            } else {
+                minMaxPairs.add(new MinMaxIdPair(minPair.getWstId(), minPair.getSoundingId(), null));
+            }
+        }
+
+        return Collections.unmodifiableCollection(minMaxPairs);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxCalculation.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,201 @@
+/** 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.artifacts.sinfo.flowdepthminmax;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.apache.commons.lang.math.DoubleRange;
+import org.dive4elements.artifacts.CallContext;
+import org.dive4elements.river.artifacts.model.Calculation;
+import org.dive4elements.river.artifacts.model.CalculationResult;
+import org.dive4elements.river.artifacts.model.WKms;
+import org.dive4elements.river.artifacts.resources.Resources;
+import org.dive4elements.river.artifacts.sinfo.SINFOArtifact;
+import org.dive4elements.river.artifacts.sinfo.common.RiverInfoProvider;
+import org.dive4elements.river.artifacts.sinfo.flowdepth.FlowDepthUtils;
+import org.dive4elements.river.artifacts.sinfo.flowdepthminmax.FlowDepthMinMaxAccess.MinMaxIdPair;
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.DischargeValuesFinder;
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.WaterlevelValuesFinder;
+import org.dive4elements.river.artifacts.sinfo.tkhstate.BedHeightsFinder;
+import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
+import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils;
+import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
+import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
+import org.dive4elements.river.artifacts.states.WaterlevelData;
+import org.dive4elements.river.artifacts.states.WaterlevelFetcher;
+import org.dive4elements.river.model.River;
+
+/**
+ * @author Gernot Belger
+ */
+final class FlowDepthMinMaxCalculation {
+
+    private final CallContext context;
+
+    public FlowDepthMinMaxCalculation(final CallContext context) {
+        this.context = context;
+    }
+
+    public CalculationResult calculate(final SINFOArtifact sinfo) {
+
+        final String user = CalculationUtils.findArtifactUser(this.context, sinfo);
+
+        /* access input data */
+        final FlowDepthMinMaxAccess access = new FlowDepthMinMaxAccess(sinfo);
+        final River river = access.getRiver();
+        final RiverInfo riverInfo = new RiverInfo(river);
+
+        final Collection<MinMaxIdPair> minMaxPairs = access.getMinMaxPairs();
+
+        final DoubleRange calcRange = access.getRange();
+
+        /* calculate results for each diff pair */
+        final Calculation problems = new Calculation();
+
+        final RiverInfoProvider infoProvider = RiverInfoProvider.forRange(this.context, river, calcRange);
+
+        final String calcModeLabel = Resources.getMsg(this.context.getMeta(), sinfo.getCalculationMode().name());
+
+        final FlowDepthMinMaxCalculationResults results = new FlowDepthMinMaxCalculationResults(calcModeLabel, user, riverInfo, calcRange);
+
+        for (final MinMaxIdPair minMaxPair : minMaxPairs) {
+            final FlowDepthMinMaxCalculationResult result = calculateResult(calcRange, minMaxPair, problems, infoProvider);
+            if (result != null)
+                results.addResult(result);
+        }
+
+        return new CalculationResult(results, problems);
+    }
+
+    /**
+     * Calculates one W-MSH differences pair.
+     *
+     * @param infoProvider
+     */
+    private FlowDepthMinMaxCalculationResult calculateResult(final DoubleRange calcRange, final MinMaxIdPair minMaxPair, final Calculation problems,
+            final RiverInfoProvider infoProvider) {
+
+        /* access real input data from database */
+        final String wstId = minMaxPair.getWstId();
+        final String minSoundingId = minMaxPair.getMinSoundingId();
+        final String maxSoundingId = minMaxPair.getMinSoundingId();
+
+        final BedHeightsFinder minBedHeight = minSoundingId == null ? null : BedHeightsFinder.forId(this.context, minSoundingId, calcRange, problems);
+        final BedHeightsFinder maxBedHeight = maxSoundingId == null ? null : BedHeightsFinder.forId(this.context, maxSoundingId, calcRange, problems);
+        if (minBedHeight == null && maxBedHeight == null)
+            return null;
+
+        /* REMARK: fetch ALL wst kms, because we want to determine the original reference gauge */
+        final WaterlevelData waterlevel = new WaterlevelFetcher().findWaterlevel(this.context, wstId, Double.NaN, Double.NaN, problems);
+        if (waterlevel == null)
+            return null;
+
+        final String label = createLabel(waterlevel, minBedHeight, maxBedHeight);
+
+        final WKms wstKms = waterlevel.getWkms();
+
+        final int soundingYear = checkSoundingYear(minBedHeight, maxBedHeight, problems);
+        FlowDepthUtils.checkYearDifference(label, waterlevel, soundingYear, problems);
+        // FIXME
+        // checkWaterlevelDiscretisation(wstKms, calcRange, problems);
+        // TODO: prüfen, ob sohlhöhen die calcRange abdecken/überschneiden
+
+        /* re-determine the reference gauge, in the same way as the WaterlevelArtifact would do it */
+        final RiverInfoProvider riverInfoProvider = infoProvider.forWaterlevel(waterlevel);
+
+        final int wspYear = waterlevel.getYear();
+        final WstInfo wstInfo = new WstInfo(waterlevel.getName(), wspYear, riverInfoProvider.getReferenceGauge());
+
+        final WaterlevelValuesFinder waterlevelProvider = WaterlevelValuesFinder.fromKms(wstKms);
+        final DischargeValuesFinder dischargeProvider = DischargeValuesFinder.fromKms(wstKms);
+
+        final String waterlevelLabel = waterlevel.getName();
+        final String soundingLabel = buildSoundingLabel(minBedHeight, maxBedHeight);
+
+        /* real calculation loop */
+        final Collection<FlowDepthMinMaxRow> rows = new ArrayList<>();
+
+        // FIXME: determine what is the spatial discretisation that we will use...
+        final double[] allKms = wstKms.allKms().toNativeArray();
+        for (final double station : allKms) {
+            if (calcRange.containsDouble(station)) {
+
+                final double wst = waterlevelProvider.getWaterlevel(station);
+                final double discharge = dischargeProvider.getDischarge(station);
+
+                final double minBedHeightValue = minBedHeight == null ? Double.NaN : minBedHeight.getMeanBedHeight(station);
+                final double maxBedHeightValue = maxBedHeight == null ? Double.NaN : maxBedHeight.getMeanBedHeight(station);
+
+                final double minFlowDepth = wst - minBedHeightValue;
+                final double maxFlowDepth = wst - maxBedHeightValue;
+
+                // FIXME: unclear what is meant here...
+                final double meanBedHeight = Double.NaN;
+
+                // REMARK: access the location once only during calculation
+                final String location = riverInfoProvider.getLocation(station);
+
+                // REMARK: access the gauge once only during calculation
+                final String gaugeLabel = riverInfoProvider.findGauge(station);
+
+                rows.add(new FlowDepthMinMaxRow(station, minFlowDepth, maxFlowDepth, wst, discharge, waterlevelLabel, gaugeLabel, meanBedHeight, soundingLabel,
+                        location));
+            }
+        }
+
+        final BedHeightInfo minBedHeightInfo = minBedHeight == null ? null : minBedHeight.getInfo();
+        final BedHeightInfo maxBedHeightInfo = maxBedHeight == null ? null : maxBedHeight.getInfo();
+        return new FlowDepthMinMaxCalculationResult(label, wstInfo, minBedHeightInfo, maxBedHeightInfo, rows);
+    }
+
+    private String buildSoundingLabel(final BedHeightsFinder minBedHeight, final BedHeightsFinder maxBedHeight) {
+
+        if (minBedHeight == null)
+            return maxBedHeight.getInfo().getDescription();
+
+        if (maxBedHeight == null)
+            return minBedHeight.getInfo().getDescription();
+
+        return String.format("%s / %s", minBedHeight.getInfo().getDescription(), maxBedHeight.getInfo().getDescription());
+    }
+
+    private String createLabel(final WaterlevelData waterlevel, final BedHeightsFinder minBedHeight, final BedHeightsFinder maxBedHeight) {
+
+        final StringBuilder buffer = new StringBuilder(waterlevel.getName());
+
+        if (minBedHeight != null)
+            buffer.append(" - "). //
+            append(minBedHeight.getInfo().getDescription());
+
+        if (maxBedHeight != null)
+            buffer.append(" - "). //
+            append(maxBedHeight.getInfo().getDescription());
+
+        return buffer.toString();
+    }
+
+    private int checkSoundingYear(final BedHeightsFinder minBedHeight, final BedHeightsFinder maxBedHeight, final Calculation problems) {
+
+        if (maxBedHeight == null)
+            return minBedHeight.getInfo().getYear();
+
+        if (minBedHeight == null)
+            return maxBedHeight.getInfo().getYear();
+
+        final int minYear = minBedHeight.getInfo().getYear();
+        final int maxYear = minBedHeight.getInfo().getYear();
+
+        if (minYear != maxYear)
+            problems.addProblem("sinfo.flowdepthminmaxcalculation.soundingyear.different");
+
+        return minYear;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxCalculationResult.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,53 @@
+/* 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.artifacts.sinfo.flowdepthminmax;
+
+import java.util.Collection;
+
+import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoCalculationResult;
+import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
+import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
+
+/**
+ * Contains the results of a {@link FlowDepthCalculation}.
+ *
+ * @author Gernot Belger
+ */
+final class FlowDepthMinMaxCalculationResult extends AbstractSInfoCalculationResult<FlowDepthMinMaxRow> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final BedHeightInfo minSounding;
+
+    private final BedHeightInfo maxSounding;
+
+    public FlowDepthMinMaxCalculationResult(final String label, final WstInfo wst, final BedHeightInfo minSounding, final BedHeightInfo maxSounding,
+            final Collection<FlowDepthMinMaxRow> rows) {
+        super(label, wst, rows);
+
+        this.minSounding = minSounding;
+        this.maxSounding = maxSounding;
+    }
+
+    public BedHeightInfo getMinSounding() {
+        return this.minSounding;
+    }
+
+    public BedHeightInfo getMaxSounding() {
+        return this.maxSounding;
+    }
+
+    public BedHeightInfo getAnySounding() {
+        if (this.minSounding != null)
+            return this.minSounding;
+
+        return this.maxSounding;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxCalculationResults.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,26 @@
+/** 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.artifacts.sinfo.flowdepthminmax;
+
+import org.apache.commons.lang.math.DoubleRange;
+import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoCalculationResults;
+import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
+
+/**
+ * @author Gernot Belger
+ */
+final class FlowDepthMinMaxCalculationResults extends AbstractSInfoCalculationResults<FlowDepthMinMaxRow, FlowDepthMinMaxCalculationResult> {
+
+    private static final long serialVersionUID = 1L;
+
+    public FlowDepthMinMaxCalculationResults(final String calcModeLabel, final String user, final RiverInfo river, final DoubleRange calcRange) {
+        super(calcModeLabel, user, river, calcRange);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxExporter.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,179 @@
+/* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde
+ * Software engineering by Intevation GmbH
+ *
+ * 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.artifacts.sinfo.flowdepthminmax;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.dive4elements.river.artifacts.sinfo.SInfoI18NStrings;
+import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoExporter;
+import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
+import org.dive4elements.river.artifacts.sinfo.util.MetaAndTableJRDataSource;
+import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
+import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
+import org.dive4elements.river.utils.RiverUtils;
+
+import au.com.bytecode.opencsv.CSVWriter;
+
+/**
+ * Generates different output formats (csv, pdf) of data that resulted from a flow depths computation.
+ *
+ * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
+ * @author Gernot Belger
+ */
+// REMARK: must be public because its registered in generators.xml
+public class FlowDepthMinMaxExporter extends AbstractSInfoExporter<FlowDepthMinMaxRow, FlowDepthMinMaxCalculationResult, FlowDepthMinMaxCalculationResults> {
+
+    /** The log used in this exporter. */
+    private static Logger log = Logger.getLogger(FlowDepthMinMaxExporter.class);
+
+    private static final String CSV_FLOWDEPTH_MIN_HEADER = "sinfo.export.flow_depth_minmax.csv.header.min";
+
+    private static final String CSV_FLOWDEPTH_MAX_HEADER = "sinfo.export.flow_depth_minmax.csv.header.max";
+
+    private static final String JASPER_FILE = "/jasper/sinfo.flowdepth.jasper";
+
+    @Override
+    protected Logger getLog() {
+        return log;
+    }
+
+    @Override
+    protected void writeCSVResultMetadata(final CSVWriter writer, final FlowDepthMinMaxCalculationResults results,
+            final FlowDepthMinMaxCalculationResult result) {
+
+        final BedHeightInfo sounding = result.getAnySounding();
+        super.writeCSVSoundingMetadata(writer, sounding);
+
+        final WstInfo wst = result.getWst();
+        writeCSVWaterlevelMetadata(writer, wst);
+    }
+
+    @Override
+    protected void writeCSVGlobalMetadata(final CSVWriter writer, final FlowDepthMinMaxCalculationResults results) {
+
+        super.writeCSVGlobalMetadataDefaults(writer, results);
+
+        writer.writeNext(new String[] { "" });
+    }
+
+    /**
+     * Write the header, with different headings depending on whether at a
+     * gauge or at a location.
+     *
+     * @param river
+     * @param useTkh
+     */
+    @Override
+    protected void writeCSVHeader(final CSVWriter writer, final FlowDepthMinMaxCalculationResults results, final RiverInfo river) {
+        log.info("FlowDepthExporter.writeCSVHeader");
+
+        final Collection<String> header = new ArrayList<>(11);
+
+        header.add(msg(SInfoI18NStrings.CSV_KM_HEADER));
+
+        header.add(msgUnit(CSV_FLOWDEPTH_MIN_HEADER, SInfoI18NStrings.UNIT_M));
+        header.add(msgUnit(CSV_FLOWDEPTH_MAX_HEADER, SInfoI18NStrings.UNIT_M));
+
+        header.add(msgUnit(SInfoI18NStrings.CSV_WATERLEVEL_HEADER, river.getWstUnit()));
+        header.add(msgUnit(SInfoI18NStrings.CSV_DISCHARGE_HEADER, SInfoI18NStrings.UNIT_CUBIC_M));
+        header.add(msg(SInfoI18NStrings.CSV_LABEL_HEADER));
+        header.add(msg(SInfoI18NStrings.CSV_GAUGE_HEADER));
+        header.add(msgUnit(SInfoI18NStrings.CSV_MEAN_BED_HEIGHT_HEADER, river.getWstUnit()));
+        header.add(msg(SInfoI18NStrings.CSV_SOUNDING_HEADER));
+        header.add(msg(SInfoI18NStrings.CSV_LOCATION_HEADER));
+
+        writer.writeNext(header.toArray(new String[header.size()]));
+    }
+
+    @Override
+    protected String[] formatCSVRow(final FlowDepthMinMaxCalculationResults results, final FlowDepthMinMaxRow row) {
+        return formatFlowDepthRow(row);
+    }
+
+    /**
+     * Format a row of a flow depth result into an array of string, both used by csv and pdf
+     *
+     * @param useTkh
+     */
+    private String[] formatFlowDepthRow(final FlowDepthMinMaxRow row) {
+
+        final Collection<String> lines = new ArrayList<>(11);
+
+        // Fluss-km
+        lines.add(getKmFormatter().format(row.getStation()));
+
+        // FIXME: spalten weglassen, wenn min oder max fehlt
+
+        // Minimale Fließtiefe [m]
+        lines.add(getFlowDepthFormatter().format(row.getMinFlowDepth()));
+        // Maximale Fließtiefe [m]
+        lines.add(getFlowDepthFormatter().format(row.getMaxFlowDepth()));
+
+        // Wasserstand [NN + m]
+        lines.add(getW2Formatter().format(row.getWaterlevel()));
+
+        // Q [m³/s]
+        final double discharge = row.getDischarge();
+        if (Double.isNaN(discharge))
+            lines.add(StringUtils.EMPTY);
+        else {
+            final double roundedDischarge = RiverUtils.roundQ(discharge);
+            lines.add(getQFormatter().format(roundedDischarge));
+        }
+
+        // Bezeichnung
+        lines.add(row.getWaterlevelLabel());
+
+        // Bezugspegel
+        lines.add(row.getGauge());
+
+        // Mittlere Sohlhöhe [NN + m]
+        lines.add(getMeanBedHeighFormatter().format(row.getMeanBedHeight()));
+
+        // Peilung/Epoche
+        lines.add(row.getSoundageLabel());
+
+        // Lage
+        lines.add(row.getLocation());
+
+        return lines.toArray(new String[lines.size()]);
+    }
+
+    @Override
+    protected final String getJasperFile() {
+        return JASPER_FILE;
+    }
+
+    @Override
+    protected final void addJRMetaData(final MetaAndTableJRDataSource source, final FlowDepthMinMaxCalculationResults results) {
+
+        /* general metadata */
+        super.addJRMetaDataDefaults(source, results);
+
+        /* column headings */
+        source.addMetaData("station_header", msg(SInfoI18NStrings.CSV_KM_HEADER));
+        source.addMetaData("flowdepthmin_header", msg(CSV_FLOWDEPTH_MIN_HEADER));
+        source.addMetaData("flowdepthmax_header", msg(CSV_FLOWDEPTH_MAX_HEADER));
+        source.addMetaData("waterlevel_header", msg(SInfoI18NStrings.CSV_WATERLEVEL_HEADER));
+        source.addMetaData("discharge_header", msg(SInfoI18NStrings.CSV_DISCHARGE_HEADER));
+        source.addMetaData("waterlevel_name_header", msg(SInfoI18NStrings.CSV_LABEL_HEADER));
+        source.addMetaData("gauge_header", msg(SInfoI18NStrings.CSV_GAUGE_HEADER));
+        source.addMetaData("bedheight_header", msg(SInfoI18NStrings.CSV_MEAN_BED_HEIGHT_HEADER_SHORT));
+        source.addMetaData("sounding_name_header", msg(SInfoI18NStrings.CSV_SOUNDING_HEADER));
+        source.addMetaData("location_header", msg(SInfoI18NStrings.CSV_LOCATION_HEADER));
+    }
+
+    @Override
+    protected String[] formatPDFRow(final FlowDepthMinMaxCalculationResults results, final FlowDepthMinMaxRow row) {
+        return formatFlowDepthRow(row);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxRow.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,81 @@
+/* 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.artifacts.sinfo.flowdepthminmax;
+
+import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoResultRow;
+
+/**
+ * Part of {@link FlowDepthMinMaxCalculationResult} which represents one calculated row of flow depth data.
+ *
+ * @author Gernot Belger
+ */
+final class FlowDepthMinMaxRow extends AbstractSInfoResultRow {
+    private static final long serialVersionUID = 1L;
+
+    private final double station;
+
+    private final double minFlowDepth;
+
+    private final double maxFlowDepth;
+
+    private final double waterlevel;
+
+    private final double discharge;
+
+    private final double meanBedHeight;
+
+    private final String soundingLabel;
+
+    public FlowDepthMinMaxRow(final double station, final double minFlowDepth, final double maxFlowDepth, final double waterlevel, final double discharge,
+            final String waterlevelLabel, final String gauge, final double meanBedHeight, final String soundingLabel, final String location) {
+
+        super(waterlevelLabel, gauge, location);
+
+        this.station = station;
+        this.minFlowDepth = minFlowDepth;
+        this.maxFlowDepth = maxFlowDepth;
+        this.waterlevel = waterlevel;
+        this.discharge = discharge;
+        this.meanBedHeight = meanBedHeight;
+        this.soundingLabel = soundingLabel;
+    }
+
+    public String getSoundageLabel() {
+        return this.soundingLabel;
+    }
+
+    public double getStation() {
+        return this.station;
+    }
+
+    public double getMinFlowDepth() {
+        return this.minFlowDepth;
+    }
+
+    public double getMaxFlowDepth() {
+        return this.maxFlowDepth;
+    }
+
+    public double getWaterlevel() {
+        return this.waterlevel;
+    }
+
+    public double getDischarge() {
+        return this.discharge;
+    }
+
+    public double getMeanBedHeight() {
+        return this.meanBedHeight;
+    }
+
+    public String getSoundingLabel() {
+        return this.soundingLabel;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/flowdepthminmax/FlowDepthMinMaxState.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,109 @@
+/* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde
+ * Software engineering by Intevation GmbH
+ *
+ * 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.artifacts.sinfo.flowdepthminmax;
+
+import java.util.List;
+
+import org.dive4elements.artifactdatabase.state.Facet;
+import org.dive4elements.artifacts.CallContext;
+import org.dive4elements.river.artifacts.ChartArtifact;
+import org.dive4elements.river.artifacts.D4EArtifact;
+import org.dive4elements.river.artifacts.model.Calculation;
+import org.dive4elements.river.artifacts.model.CalculationResult;
+import org.dive4elements.river.artifacts.model.DataFacet;
+import org.dive4elements.river.artifacts.model.EmptyFacet;
+import org.dive4elements.river.artifacts.model.FacetTypes;
+import org.dive4elements.river.artifacts.model.ReportFacet;
+import org.dive4elements.river.artifacts.sinfo.SINFOArtifact;
+import org.dive4elements.river.artifacts.states.DefaultState;
+
+/** State in which a waterlevel has been calculated. */
+public class FlowDepthMinMaxState extends DefaultState {
+
+    /// ** The log that is used in this state. */
+    // private static Logger log = Logger.getLogger(FlowDepthState.class);
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * From this state can only be continued trivially.
+     */
+    @Override
+    protected String getUIProvider() {
+        return "continue";
+    }
+
+    @Override
+    public Object computeFeed(final D4EArtifact artifact, final String hash, final CallContext context, final List<Facet> facets, final Object old) {
+        // FIXME: why is this necessary?
+        if (artifact instanceof ChartArtifact) {
+            facets.add(new EmptyFacet());
+            return null;
+        }
+
+        return compute((SINFOArtifact) artifact, context, hash, facets, old);
+    }
+
+    @Override
+    public Object computeAdvance(final D4EArtifact artifact, final String hash, final CallContext context, final List<Facet> facets, final Object old) {
+        if (artifact instanceof ChartArtifact) {
+            facets.add(new EmptyFacet());
+            return null;
+        }
+        return compute((SINFOArtifact) artifact, context, hash, facets, old);
+    }
+
+    /**
+     * Compute result or returned object from cache, create facets.
+     *
+     * @param old
+     *            Object that was cached.
+     */
+    private Object compute(final SINFOArtifact sinfo, final CallContext context, final String hash, final List<Facet> facets, final Object old) {
+
+        final CalculationResult res = doCompute(sinfo, context, old);
+
+        if (facets == null)
+            return res;
+
+        final FlowDepthMinMaxCalculationResults results = (FlowDepthMinMaxCalculationResults) res.getData();
+
+        /* add themes for chart, for each result */
+        final List<FlowDepthMinMaxCalculationResult> resultList = results.getResults();
+        for (int index = 0; index < resultList.size(); index++) {
+
+            final FlowDepthMinMaxCalculationResult result = resultList.get(index);
+
+            /* filtered (zoom dependent mean) flow depth */
+            // facets.add(FlowDepthProcessor.createFlowDepthFacet(context, hash, this.id, result, index));
+
+        }
+
+        if (!resultList.isEmpty()) {
+            final Facet csv = new DataFacet(FacetTypes.CSV, "CSV data", ComputeType.ADVANCE, hash, this.id);
+            final Facet pdf = new DataFacet(FacetTypes.PDF, "PDF data", ComputeType.ADVANCE, hash, this.id);
+
+            facets.add(csv);
+            facets.add(pdf);
+        }
+
+        final Calculation report = res.getReport();
+        if (report.hasProblems())
+            facets.add(new ReportFacet(ComputeType.ADVANCE, hash, this.id));
+
+        return res;
+    }
+
+    private CalculationResult doCompute(final SINFOArtifact sinfo, final CallContext context, final Object old) {
+        if (old instanceof CalculationResult)
+            return (CalculationResult) old;
+
+        return new FlowDepthMinMaxCalculation(context).calculate(sinfo);
+    }
+}
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/DischargeValuesFinder.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/DischargeValuesFinder.java	Tue Mar 13 18:49:33 2018 +0100
@@ -46,14 +46,20 @@
 
     public DischargeValuesFinder(final QKms qKms) {
         this.qKms = qKms;
-        this.qInterpolator = qKms == null ? null : DoubleUtil.getLinearInterpolator(qKms.allKms(), qKms.allQs());
-
-        this.exactValues = new TDoubleDoubleHashMap(qKms.size());
 
-        for (int i = 0; i < qKms.size(); i++) {
-            final double station = qKms.getKm(i);
-            final double discharge = qKms.getQ(i);
-            this.exactValues.put(station, discharge);
+        if (qKms == null) {
+            this.qInterpolator = null;
+            this.exactValues = null;
+        } else {
+            this.qInterpolator = DoubleUtil.getLinearInterpolator(qKms.allKms(), qKms.allQs());
+
+            this.exactValues = new TDoubleDoubleHashMap(qKms.size());
+
+            for (int i = 0; i < qKms.size(); i++) {
+                final double station = qKms.getKm(i);
+                final double discharge = qKms.getQ(i);
+                this.exactValues.put(station, discharge);
+            }
         }
     }
 
@@ -68,14 +74,23 @@
         return new DoubleRange(this.qKms.allQs().min(), this.qKms.allQs().max());
     }
 
-    public double getDischarge(final double station) throws FunctionEvaluationException {
+    public double getDischarge(final double station) {
 
-        // IMPORTANT: we first try to retreive the exact value if it is present, to avoid rounding changes due to interpolation.
-        // This is important because in the WaterlevelExporter code, these values are double-compared (with '==' ...) in order
-        // to find the corresponding main-value.
-        if (this.exactValues.contains(station))
-            return this.exactValues.get(station);
+        try {
+            // IMPORTANT: we first try to retrieve the exact value if it is present, to avoid rounding changes due to interpolation.
+            // This is important because in the WaterlevelExporter code, these values are double-compared (with '==' ...) in order
+            // to find the corresponding main-value.
+            if (this.exactValues != null && this.exactValues.contains(station))
+                return this.exactValues.get(station);
 
-        return this.qInterpolator.value(station);
+            if (this.qInterpolator == null)
+                return Double.NaN;
+
+            return this.qInterpolator.value(station);
+        }
+        catch (final FunctionEvaluationException e) {
+            e.printStackTrace();
+            return Double.NaN;
+        }
     }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/TkhCalculator.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/TkhCalculator.java	Tue Mar 13 18:49:33 2018 +0100
@@ -11,7 +11,6 @@
 
 import org.apache.commons.lang.math.DoubleRange;
 import org.apache.commons.math.ArgumentOutsideDomainException;
-import org.apache.commons.math.FunctionEvaluationException;
 import org.dive4elements.artifacts.CallContext;
 import org.dive4elements.river.artifacts.model.Calculation;
 import org.dive4elements.river.artifacts.resources.Resources;
@@ -37,30 +36,33 @@
 
     private final BedHeightsFinder bedHeightsProvider;
 
+    private final WaterlevelValuesFinder waterlevelProvider;
+
     private final DischargeValuesFinder dischargeProvider;
 
     private final FlowVelocityModelKmValueFinder flowVelocitiesFinder;
 
     public static TkhCalculator buildTkhCalculator(final boolean useTkh, final CallContext context, final Calculation problems, final String label,
-            final River river, final DoubleRange calcRange, final DischargeValuesFinder dischargeProvider, final BedHeightsFinder bedHeightsProvider) {
+            final River river, final DoubleRange calcRange, final WaterlevelValuesFinder waterlevelProvider, final DischargeValuesFinder dischargeProvider,
+            final BedHeightsFinder bedHeightsProvider) {
 
         if (!useTkh)
-            return null;
+            return new TkhCalculator(problems, label, context, null, waterlevelProvider, dischargeProvider, bedHeightsProvider, null, null);
 
         if (!dischargeProvider.isValid()) {
             final String message = Resources.getMsg(context.getMeta(), "sinfo_calc_flow_depth.warning.missingQ", null, label);
             problems.addProblem(message);
-            return null;
+            return new TkhCalculator(problems, label, context, null, waterlevelProvider, dischargeProvider, bedHeightsProvider, null, null);
         }
 
-        final Integer soundingYear = bedHeightsProvider.getInfo().getYear();
+        final int soundingYear = bedHeightsProvider.getInfo().getYear();
         final BedQualityD50KmValueFinder bedMeasurementsFinder = BedQualityD50KmValueFinder.loadBedMeasurements(river, calcRange, soundingYear,
                 VALID_BED_MEASUREMENT_YEARS);
 
         if (bedMeasurementsFinder == null) {
             final String message = Resources.getMsg(context.getMeta(), "sinfo_calc_flow_depth.warning.missingD50", null, label);
             problems.addProblem(message);
-            return null;
+            return new TkhCalculator(problems, label, context, null, waterlevelProvider, dischargeProvider, bedHeightsProvider, null, null);
         }
 
         // FIXME: wie wird ggf. interpoliert? prüfung ob werte vorhanden?
@@ -68,7 +70,7 @@
         if (soilKindFinder == null) {
             final String message = Resources.getMsg(context.getMeta(), "sinfo_calc_flow_depth.warning.missingSoilKind", null, label);
             problems.addProblem(message);
-            return null;
+            return new TkhCalculator(problems, label, context, null, waterlevelProvider, dischargeProvider, bedHeightsProvider, null, null);
         }
 
         final DoubleRange qRange = dischargeProvider.getRange();
@@ -76,35 +78,43 @@
         if (flowVelocitiesFinder == null) {
             final String message = Resources.getMsg(context.getMeta(), "sinfo_calc_flow_depth.warning.missingVelocity", null, label);
             problems.addProblem(message);
-            return null;
+            return new TkhCalculator(problems, label, context, null, waterlevelProvider, dischargeProvider, bedHeightsProvider, null, null);
         }
 
-        return new TkhCalculator(problems, label, context, bedMeasurementsFinder, dischargeProvider, bedHeightsProvider, soilKindFinder, flowVelocitiesFinder);
+        return new TkhCalculator(problems, label, context, bedMeasurementsFinder, waterlevelProvider, dischargeProvider, bedHeightsProvider, soilKindFinder,
+                flowVelocitiesFinder);
     }
 
     private TkhCalculator(final Calculation problems, final String problemLabel, final CallContext context,
-            final BedQualityD50KmValueFinder bedMeasurementsFinder, final DischargeValuesFinder dischargeProvider, final BedHeightsFinder bedHeightsProvider,
-            final SoilKindKmValueFinder soilKindFinder,
+            final BedQualityD50KmValueFinder bedMeasurementsFinder, final WaterlevelValuesFinder waterlevelProvider,
+            final DischargeValuesFinder dischargeProvider, final BedHeightsFinder bedHeightsProvider, final SoilKindKmValueFinder soilKindFinder,
             final FlowVelocityModelKmValueFinder flowVelocitiesFinder) {
         this.problems = problems;
         this.problemLabel = problemLabel;
         this.context = context;
         this.bedMeasurementsFinder = bedMeasurementsFinder;
+        this.waterlevelProvider = waterlevelProvider;
         this.dischargeProvider = dischargeProvider;
         this.bedHeightsProvider = bedHeightsProvider;
         this.soilKindFinder = soilKindFinder;
         this.flowVelocitiesFinder = flowVelocitiesFinder;
     }
 
-    private double getDischarge(final double km) {
+    public boolean hasTkh() {
 
-        try {
-            return this.dischargeProvider.getDischarge(km);
-        }
-        catch (final FunctionEvaluationException e) {
-            // TODO: exceptions nicht komplett schlucken? evtl. mit log.debug(e) ausgeben
-            return Double.NaN;
-        }
+        if (this.dischargeProvider == null || !this.dischargeProvider.isValid())
+            return false;
+
+        if (this.bedMeasurementsFinder == null)
+            return false;
+
+        if (this.soilKindFinder == null)
+            return false;
+
+        if (this.flowVelocitiesFinder == null)
+            return false;
+
+        return true;
     }
 
     private SoilKind getSoilKind(final double km) {
@@ -134,26 +144,22 @@
         }
     }
 
-    public Tkh getTkh(final double km, final double wst) {
+    public Tkh getTkh(final double km) {
 
         final SoilKind kind = getSoilKind(km);
 
+        final double wst = this.waterlevelProvider.getWaterlevel(km);
+
         final double meanBedHeight = this.bedHeightsProvider.getMeanBedHeight(km);
 
         final double flowDepth = wst - meanBedHeight;
 
-        final double discharge = getDischarge(km);
-        if (Double.isNaN(discharge)) {
+        final double discharge = this.dischargeProvider.getDischarge(km);
+        if (Double.isNaN(discharge))
+            return new Tkh(km, wst, meanBedHeight, flowDepth, Double.NaN, kind);
 
-            // final String message = Resources.getMsg(this.context.getMeta(), "sinfo_calc_flow_depth.warning.missingQ", null,
-            // this.problemLabel);
-            // this.problems.addProblem(km, message);
-
-            // TODO: nochmal gemeinsam überlegen welche probleme wir loggen, an dieser stelle müsste man ggf. die station
-            // mitausgeben
-
+        if (!this.hasTkh())
             return new Tkh(km, wst, meanBedHeight, flowDepth, Double.NaN, kind);
-        }
 
         final double d50 = getBedMeasurement(km);
         if (Double.isNaN(d50))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhcalculation/WaterlevelValuesFinder.java	Tue Mar 13 18:49:33 2018 +0100
@@ -0,0 +1,43 @@
+/** 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.artifacts.sinfo.tkhcalculation;
+
+import org.apache.commons.math.ArgumentOutsideDomainException;
+import org.apache.commons.math.analysis.polynomials.PolynomialSplineFunction;
+import org.dive4elements.river.artifacts.model.WKms;
+import org.dive4elements.river.utils.DoubleUtil;
+
+/**
+ * Abstraction for access to waterlevels by station.
+ *
+ * @author Gernot Belger
+ */
+public class WaterlevelValuesFinder {
+
+    public static WaterlevelValuesFinder fromKms(final WKms wkms) {
+        return new WaterlevelValuesFinder(wkms);
+    }
+
+    private final PolynomialSplineFunction wstInterpolator;
+
+    private WaterlevelValuesFinder(final WKms wkms) {
+        this.wstInterpolator = DoubleUtil.getLinearInterpolator(wkms.allKms(), wkms.allWs());
+    }
+
+    public double getWaterlevel(final double km) {
+        try {
+            return this.wstInterpolator.value(km);
+        }
+        catch (final ArgumentOutsideDomainException e) {
+            e.printStackTrace();
+            return Double.NaN;
+        }
+    }
+}
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/BedHeightsFinder.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/BedHeightsFinder.java	Tue Mar 13 18:49:33 2018 +0100
@@ -17,10 +17,14 @@
 import java.util.TreeMap;
 
 import org.apache.commons.lang.math.DoubleRange;
+import org.dive4elements.artifacts.CallContext;
+import org.dive4elements.river.artifacts.BedHeightsArtifact;
 import org.dive4elements.river.artifacts.math.Linear;
+import org.dive4elements.river.artifacts.model.Calculation;
 import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
 import org.dive4elements.river.model.BedHeight;
 import org.dive4elements.river.model.BedHeightValue;
+import org.dive4elements.river.utils.RiverUtils;
 
 /**
  * Provides bed heigts for vcarious calculations.
@@ -47,12 +51,47 @@
         return result;
     }
 
+    public static BedHeightsFinder forId(final CallContext context, final String soundingId, final DoubleRange calcRange, final Calculation problems) {
+
+        // REMARK: absolutely unbelievable....
+        // The way how bed-heights (and other data too) is accessed is different for nearly every calculation-type
+        // throughout flys.
+        // The knowledge on how to parse the datacage-ids is spread through the complete code-base...
+
+        // We use here the way on how bed-heights are accessed by the BedDifferenceAccess/BedDifferenceCalculation, but
+        // this is plain random
+        final String[] parts = soundingId.split(";");
+
+        final BedHeightsArtifact artifact = (BedHeightsArtifact) RiverUtils.getArtifact(parts[0], context);
+
+        final Integer bedheightId = artifact.getDataAsInteger("height_id");
+
+        // REMARK: this only works with type 'single'; unclear on how to distinguish from epoch data (or whatever the
+        // other type means)
+        // Luckily, the requirement is to only access 'single' data here.
+        // final String bedheightType = artifact.getDataAsString("type");
+
+        // REMARK: BedDifferences uses this, but we also need the metadata of the BedHeight
+        // REMARK: second absolutely awful thing: BedHeight is a hibernate binding class, accessing the database via
+        // hibernate stuff
+        // BedHeightFactory uses its own (direct) way of accessing the data, with its own implemented data classes.
+        // return BedHeightFactory.getHeight(bedheightType, bedheightId, from, to);
+
+        final BedHeightsFinder bedHeight = bedheightId == null ? null : BedHeightsFinder.forId(bedheightId, calcRange);
+        if (bedHeight != null)
+            return bedHeight;
+
+        // FIXME: 10n
+        problems.addProblem("Failed to access sounding with id '{0}'", soundingId);
+        return null;
+    }
+
     /**
      * Creates a {@link BedHeightsFinder} for a dataset from the database, specified by its id.
      *
      * @return <code>null</code> if no bed height with the given id exists.
      */
-    public static BedHeightsFinder forId(final int id, final DoubleRange range) {
+    private static BedHeightsFinder forId(final int id, final DoubleRange range) {
 
         final BedHeight bedHeight = BedHeight.getBedHeightById(id);
         if (bedHeight == null)
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhCalculation.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhCalculation.java	Tue Mar 13 18:49:33 2018 +0100
@@ -31,6 +31,7 @@
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.DischargeValuesFinder;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.Tkh;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.TkhCalculator;
+import org.dive4elements.river.artifacts.sinfo.tkhcalculation.WaterlevelValuesFinder;
 import org.dive4elements.river.artifacts.sinfo.util.BedHeightInfo;
 import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils;
 import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
@@ -143,14 +144,13 @@
         for (int i = 0; i < size; i++) {
 
             final double station = wkms.getKm(i);
-            final double wst = wkms.getW(i);
 
             /* find the right calculator (i.e. bedheigh) depending on station, there should only be one maximal */
             final TkhCalculator tkhCalculator = findCalculator(calculatorsByRanges, station);
             if (tkhCalculator == null)
                 continue;
 
-            final Tkh tkh = tkhCalculator.getTkh(station, wst);
+            final Tkh tkh = tkhCalculator.getTkh(station);
 
             final String description = descBuilder.getDesc(wkms);
             final String gaugeLabel = riverInfoProvider.findGauge(station);
@@ -185,11 +185,12 @@
 
             final NumberRange range = new NumberRange(info.getFrom(), info.getTo());
 
+            final WaterlevelValuesFinder waterlevelProvider = WaterlevelValuesFinder.fromKms(wkms);
             final DischargeValuesFinder dischargeProvider = DischargeValuesFinder.fromKms(wkms);
 
             /* initialize tkh calculator */
             final TkhCalculator tkhCalculator = TkhCalculator.buildTkhCalculator(true, this.context, problems, wstLabel, riverInfoProvider.getRiver(),
-                    calcRange, dischargeProvider, bedHeightsProvider);
+                    calcRange, waterlevelProvider, dischargeProvider, bedHeightsProvider);
 
             if (tkhCalculator != null) {
                 /* just ignore null ones, problems have already been updated by buildTkhCalculator() */
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhCalculationResult.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhCalculationResult.java	Tue Mar 13 18:49:33 2018 +0100
@@ -11,7 +11,7 @@
 
 import java.util.Collection;
 
-import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoCalculationResult;
+import org.dive4elements.river.artifacts.sinfo.common.AbstractTkhCalculationResult;
 import org.dive4elements.river.artifacts.sinfo.util.WstInfo;
 
 /**
@@ -19,7 +19,7 @@
  *
  * @author Gernot Belger
  */
-final class TkhCalculationResult extends AbstractSInfoCalculationResult<TkhResultRow> {
+final class TkhCalculationResult extends AbstractTkhCalculationResult<TkhResultRow> {
 
     private static final long serialVersionUID = 1L;
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhExporter.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhExporter.java	Tue Mar 13 18:49:33 2018 +0100
@@ -8,17 +8,10 @@
 
 package org.dive4elements.river.artifacts.sinfo.tkhstate;
 
-import java.text.DateFormat;
-import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
-import java.util.Locale;
 
-import org.apache.commons.lang.math.DoubleRange;
 import org.apache.log4j.Logger;
-import org.dive4elements.river.FLYS;
-import org.dive4elements.river.artifacts.resources.Resources;
 import org.dive4elements.river.artifacts.sinfo.SInfoI18NStrings;
 import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoExporter;
 import org.dive4elements.river.artifacts.sinfo.util.MetaAndTableJRDataSource;
@@ -64,31 +57,7 @@
     protected void writeCSVGlobalMetadata(final CSVWriter writer, final TkhCalculationResults results) {
         log.info("TkhExporter.writeCSVMeta");
 
-        final String calcModeLabel = results.getCalcModeLabel();
-        final RiverInfo river = results.getRiver();
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_RESULT, msg(SInfoI18NStrings.CSV_META_HEADER_RESULT_LABEL), river.getName(), calcModeLabel);
-
-        // "# FLYS-Version: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_VERSION, msg(SInfoI18NStrings.CSV_META_VERSION_LABEL), FLYS.VERSION);
-
-        // "# Bearbeiter: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_USER, msg(SInfoI18NStrings.CSV_META_USER_LABEL), results.getUser());
-
-        // "# Datum der Erstellung: "
-        final Locale locale = Resources.getLocale(this.context.getMeta());
-        final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_CREATION, msg(SInfoI18NStrings.CSV_META_CREATION_LABEL), df.format(new Date()));
-
-        // "# Gewässer: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_RIVER, msg(SInfoI18NStrings.CSV_META_RIVER_LABEL), river.getName());
-
-        // "# Höhensystem des Flusses: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEIGHT_UNIT_RIVER, river.getWstUnit());
-
-        // "# Ort/Bereich (km): "
-        final DoubleRange calcRange = results.getCalcRange();
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_RANGE, msg(SInfoI18NStrings.CSV_META_RANGE_LABEL),
-                getKmFormatter().format(calcRange.getMinimumDouble()), getKmFormatter().format(calcRange.getMaximumDouble()));
+        super.writeCSVGlobalMetadataDefaults(writer, results);
 
         // "# Berechnungsgrundlage: Gleichung nach GILL (1971)"
         writeCSVMetaEntry(writer, CSV_META_CALCULATION_FORMULA);
@@ -128,17 +97,12 @@
     // FIXME: rename
     protected void writeCSVResultMetadata(final CSVWriter writer, final TkhCalculationResults results, final TkhCalculationResult result) {
 
-        /* first some specific metadata */
         final WstInfo wst = result.getWst();
+        super.writeCSVWaterlevelMetadata(writer, wst);
 
-        // "##METADATEN WASSERSPIEGELLAGE"
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL);
-        // "# Bezeichnung der Wasserspiegellage: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_NAME, wst.getLabel());
-        // "# Bezugspegel: "
-        writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_GAUGE, wst.getGauge());
-        // // "# Jahr/Zeitraum der Wasserspiegellage: "
-        // writeCSVMetaEntry(writer, SInfoI18NStrings.CSV_META_HEADER_WATERLEVEL_YEAR, Integer.toString(wst.getYear()));
+        // TODO:
+        // "# W/Pegel [cm]: " (nur bei Eingabe des Wasserstands am Pegel)
+        // "# Q (m³/s): " (nur bei Eingabe des Durchflusses)
     }
 
     @Override
@@ -199,33 +163,8 @@
     @Override
     protected final void addJRMetaData(final MetaAndTableJRDataSource source, final TkhCalculationResults results) {
 
-        final RiverInfo river = results.getRiver();
-        final String wstUnitName = river.getWstUnit();
-
         /* general metadata */
-        source.addMetaData("header", msg(SInfoI18NStrings.CSV_META_HEADER_RESULT_LABEL));
-        source.addMetaData("calcMode", results.getCalcModeLabel());
-
-        source.addMetaData("version_label", msg(SInfoI18NStrings.CSV_META_VERSION_LABEL));
-        source.addMetaData("version", FLYS.VERSION);
-
-        source.addMetaData("user_label", msg(SInfoI18NStrings.CSV_META_USER_LABEL));
-        source.addMetaData("user", results.getUser());
-
-        final Locale locale = Resources.getLocale(this.context.getMeta());
-        final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
-        source.addMetaData("date_label", msg(SInfoI18NStrings.CSV_META_CREATION_LABEL));
-        source.addMetaData("date", df.format(new Date()));
-
-        source.addMetaData("river_label", msg(SInfoI18NStrings.CSV_META_RIVER_LABEL));
-        source.addMetaData("river", river.getName());
-        source.addMetaData("river_unit", wstUnitName);
-
-        final DoubleRange calcRange = results.getCalcRange();
-        final NumberFormat kmFormatter = getKmFormatter();
-        final String rangeValue = String.format("%s - %s", kmFormatter.format(calcRange.getMinimumDouble()), kmFormatter.format(calcRange.getMaximumDouble()));
-        source.addMetaData("range_label", msg(SInfoI18NStrings.CSV_META_RANGE_LABEL));
-        source.addMetaData("range", rangeValue);
+        super.addJRMetaDataDefaults(source, results);
 
         /* column headings */
         source.addMetaData("station_header", msg(SInfoI18NStrings.CSV_KM_HEADER));
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhResultRow.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/tkhstate/TkhResultRow.java	Tue Mar 13 18:49:33 2018 +0100
@@ -9,13 +9,13 @@
  */
 package org.dive4elements.river.artifacts.sinfo.tkhstate;
 
-import org.dive4elements.river.artifacts.sinfo.common.AbstractSInfoResultRow;
+import org.dive4elements.river.artifacts.sinfo.common.AbstractTkhResultRow;
 import org.dive4elements.river.artifacts.sinfo.tkhcalculation.Tkh;
 
 /**
  * @author Gernot Belger
  */
-final class TkhResultRow extends AbstractSInfoResultRow {
+final class TkhResultRow extends AbstractTkhResultRow {
 
     private static final long serialVersionUID = 1L;
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/util/BedHeightInfo.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/util/BedHeightInfo.java	Tue Mar 13 18:49:33 2018 +0100
@@ -23,7 +23,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    private final Integer year;
+    private final int year;
 
     private final String description;
 
@@ -58,7 +58,7 @@
         this.to = bedHeight.getRange().getB();
     }
 
-    public Integer getYear() {
+    public int getYear() {
         return this.year;
     }
 
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/util/MetaAndTableJRDataSource.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/sinfo/util/MetaAndTableJRDataSource.java	Tue Mar 13 18:49:33 2018 +0100
@@ -1,6 +1,6 @@
 /* Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
- * Software engineering by 
- *  Björnsen Beratende Ingenieure GmbH 
+ * 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)
@@ -21,16 +21,16 @@
 /**
  * @author <a href="mailto:raimund.renkert@intevation.de">Raimund Renkert</a>
  */
-public final class MetaAndTableJRDataSource implements JRDataSource
-{
-    private List<String[]> data = new ArrayList<>();
+public final class MetaAndTableJRDataSource implements JRDataSource {
 
-    private Map<String, String> metaData = new HashMap<>();
+    private final List<String[]> data = new ArrayList<>();
+
+    private final Map<String, String> metaData = new HashMap<>();
 
     private int index = -1;
 
-    public void addData(final String[] data) {
-        this.data.add(data);
+    public void addData(final String[] row) {
+        this.data.add(row);
     }
 
     public void addMetaData(final String key, final String value) {
@@ -38,27 +38,24 @@
     }
 
     @Override
-	public boolean next() throws JRException
-    {
-        index++;
+    public boolean next() throws JRException {
+        this.index++;
 
-        return index < data.size();
+        return this.index < this.data.size();
     }
 
     @Override
-	public Object getFieldValue(final JRField field) throws JRException
-    {
+    public Object getFieldValue(final JRField field) throws JRException {
         final String fieldName = field.getName();
-        
-        if( fieldName.startsWith("meta:"))
-        	return metaData.get(fieldName.substring("meta:".length()));
 
-        if( fieldName.startsWith("data:"))
-        {
-        	int column = Integer.valueOf(fieldName.substring("data:".length()));
-        	return data.get(index)[column];
+        if (fieldName.startsWith("meta:"))
+            return this.metaData.get(fieldName.substring("meta:".length()));
+
+        if (fieldName.startsWith("data:")) {
+            final int column = Integer.valueOf(fieldName.substring("data:".length()));
+            return this.data.get(this.index)[column];
         }
-        
+
         return null;
     }
 }
\ No newline at end of file
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/states/WaterlevelFetcher.java	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/states/WaterlevelFetcher.java	Tue Mar 13 18:49:33 2018 +0100
@@ -19,6 +19,7 @@
 import org.dive4elements.river.artifacts.StaticWQKmsArtifact;
 import org.dive4elements.river.artifacts.WINFOArtifact;
 import org.dive4elements.river.artifacts.access.FixRealizingAccess;
+import org.dive4elements.river.artifacts.model.Calculation;
 import org.dive4elements.river.artifacts.model.CalculationResult;
 import org.dive4elements.river.artifacts.model.Segment;
 import org.dive4elements.river.artifacts.model.WKms;
@@ -39,7 +40,7 @@
     private static Logger log = Logger.getLogger(WaterlevelFetcher.class);
 
     public WaterlevelData findWaterlevel(final CallContext context, final String mingle, final double from,
-            final double to) {
+            final double to, final Calculation problems) {
 
         final String[] def = mingle.split(";");
         final String uuid = def[0];
@@ -48,7 +49,11 @@
         final D4EArtifact d4eArtifact = RiverUtils.getArtifact(uuid, context);
 
         final WaterlevelData data = fetchWaterlevelFromArtifact(context, d4eArtifact, idx, from, to);
-        return data.withName(name);
+        if (data != null)
+            return data.withName(name);
+
+        problems.addProblem("waterlevelfetcher.missing'", mingle);
+        return null;
     }
 
     private WaterlevelData fetchWaterlevelFromArtifact(final CallContext context, final D4EArtifact d4eArtifact,
--- a/artifacts/src/main/resources/messages.properties	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/resources/messages.properties	Tue Mar 13 18:49:33 2018 +0100
@@ -886,4 +886,11 @@
 
 sinfo.chart.tkh_d50.section.yaxis.label = Sohlbeschaffenheit D50 [mm]
 sinfo.chart.tkh_d50.yaxis.label = Sohlbeschaffenheit D50 [mm]
-sinfo.facet.tkh_d50.filtered.description = Sohlbeschaffenheit D50 ({0})
\ No newline at end of file
+sinfo.facet.tkh_d50.filtered.description = Sohlbeschaffenheit D50 ({0})
+
+sinfo.flowdepthminmaxcalculation.soundingyear.different = Die Peiljahre von Talweg und Kammlage untersheiden sich
+
+sinfo.export.flow_depth_minmax.csv.header.min = Minimale Flie\u00dftiefe
+sinfo.export.flow_depth_minmax.csv.header.max = Maximale Flie\u00dftiefe
+
+waterlevelfetcher.missing = Failed to access waterlevel with id '{0}'
\ No newline at end of file
--- a/artifacts/src/main/resources/messages_de.properties	Tue Mar 13 09:55:53 2018 +0100
+++ b/artifacts/src/main/resources/messages_de.properties	Tue Mar 13 18:49:33 2018 +0100
@@ -886,4 +886,11 @@
 
 sinfo.chart.tkh_d50.section.yaxis.label = Sohlbeschaffenheit D50 [mm]
 sinfo.chart.tkh_d50.yaxis.label = Sohlbeschaffenheit D50 [mm]
-sinfo.facet.tkh_d50.filtered.description = Sohlbeschaffenheit D50 ({0})
\ No newline at end of file
+sinfo.facet.tkh_d50.filtered.description = Sohlbeschaffenheit D50 ({0})
+
+sinfo.flowdepthminmaxcalculation.soundingyear.different = Die Peiljahre von Talweg und Kammlage untersheiden sich
+
+sinfo.export.flow_depth_minmax.csv.header.min = Minimale Flie\u00dftiefe
+sinfo.export.flow_depth_minmax.csv.header.max = Maximale Flie\u00dftiefe
+
+waterlevelfetcher.missing = Fehler beim Zugriff auf Wasserspiegel mit id '{0}'
\ No newline at end of file

http://dive4elements.wald.intevation.org