changeset 8852:8f6d6d26e96f

Refaktored the DatacageTwinPanel so it is reusable.
author gernotbelger
date Thu, 18 Jan 2018 18:32:30 +0100
parents 13650d8a2373
children 8c64617a7991
files artifacts/src/main/java/org/dive4elements/river/artifacts/states/WaterlevelPairSelectState.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractUIProvider.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DatacagePairWidget.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DatacageTwinPanel.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DefaultDatacageTwinPanelInfo.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/WaterlevelRecommandationInfo.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/WaterlevelTwinPanel.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/WaterlevelTwinPanelValidator.java gwt-client/src/main/java/org/dive4elements/river/client/client/ui/minfo/BedHeightsDatacagePanel.java
diffstat 10 files changed, 758 insertions(+), 554 deletions(-) [+]
line wrap: on
line diff
--- a/artifacts/src/main/java/org/dive4elements/river/artifacts/states/WaterlevelPairSelectState.java	Thu Jan 18 18:26:30 2018 +0100
+++ b/artifacts/src/main/java/org/dive4elements/river/artifacts/states/WaterlevelPairSelectState.java	Thu Jan 18 18:32:30 2018 +0100
@@ -48,7 +48,7 @@
     /** Specify to display a datacage_twin_panel. */
     @Override
     protected String getUIProvider() {
-        return "datacage_twin_panel";
+        return "waterlevel_twin_panel";
     }
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java	Thu Jan 18 18:32:30 2018 +0100
@@ -0,0 +1,508 @@
+/* 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.client.client.ui;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import com.smartgwt.client.data.Record;
+import com.smartgwt.client.types.ListGridFieldType;
+import com.smartgwt.client.widgets.Canvas;
+import com.smartgwt.client.widgets.events.ClickEvent;
+import com.smartgwt.client.widgets.grid.ListGrid;
+import com.smartgwt.client.widgets.grid.ListGridField;
+import com.smartgwt.client.widgets.grid.ListGridRecord;
+import com.smartgwt.client.widgets.grid.events.RecordClickEvent;
+import com.smartgwt.client.widgets.grid.events.RecordClickHandler;
+import com.smartgwt.client.widgets.layout.VLayout;
+
+import org.dive4elements.river.client.client.Config;
+import org.dive4elements.river.client.client.FLYSConstants;
+import org.dive4elements.river.client.client.event.StepForwardEvent;
+import org.dive4elements.river.client.client.services.LoadArtifactServiceAsync;
+import org.dive4elements.river.client.client.services.RemoveArtifactServiceAsync;
+import org.dive4elements.river.client.shared.model.Artifact;
+import org.dive4elements.river.client.shared.model.Collection;
+import org.dive4elements.river.client.shared.model.Data;
+import org.dive4elements.river.client.shared.model.DataItem;
+import org.dive4elements.river.client.shared.model.DataList;
+import org.dive4elements.river.client.shared.model.DefaultData;
+import org.dive4elements.river.client.shared.model.DefaultDataItem;
+import org.dive4elements.river.client.shared.model.Recommendation;
+import org.dive4elements.river.client.shared.model.Recommendation.Facet;
+import org.dive4elements.river.client.shared.model.Recommendation.Filter;
+import org.dive4elements.river.client.shared.model.User;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+// TODO Probably better to branch off AbstractUIProvider.
+// TODO Merge with other datacage-widget impls.
+/**
+ * Panel containing a Grid and a "next" button. The Grid is fed by a
+ * DatacagePairWidget which is put in the input-helper area.
+ */
+public abstract class AbstractPairRecommendationPanel
+extends      TextProvider {
+
+	/**
+	 * Allows for abstraction on how to handle/serialize the recommendation and the used factories.
+	 * @author Gernot Belger
+	 *
+	 */
+	public static interface IRecommendationInfo	{
+
+		String getFactory();
+
+		/**
+		 * Separate factory for the 'createDataString' method, because in the case of waterlevels. See HOTFIX/FIXME there.
+		 */
+		String getDataStringFactory();
+
+	    /**
+	     * Set factory of recommendation such that the correct artifacts will
+	     * be cloned for difference calculations.
+	     */
+	    void adjustRecommendation(Recommendation recommendation);
+	}
+	
+	public static interface IValidator
+	{
+		List<String> validate(ListGrid differencesList, FLYSConstants msgProvider);
+	}
+
+    private static final long serialVersionUID = 8906629596491827857L;
+
+    // FIXME: why? we hide the field of the super class with exactly the same thing...
+    private static FLYSConstants MSG_PROVIDER = GWT.create(FLYSConstants.class);
+
+    private String dataName;
+
+    private User user;
+
+    /** ListGrid that displays user-selected pairs to build differences with. */
+    private ListGrid differencesList;
+
+    /**
+     * List to track previously selected but now removed pairs. (Needed to
+     * be able to identify artifacts that can be removed from the collection.
+     */
+    private List<RecommendationPairRecord> removedPairs =
+        new ArrayList<RecommendationPairRecord>();
+
+    /** Service handle to clone and add artifacts to collection. */
+    private LoadArtifactServiceAsync loadArtifactService = GWT.create(
+            org.dive4elements.river.client.client.services.LoadArtifactService.class);
+
+    /** Service to remove artifacts from collection. */
+    private RemoveArtifactServiceAsync removeArtifactService = GWT.create(
+            org.dive4elements.river.client.client.services.RemoveArtifactService.class);
+
+	private IRecommendationInfo leftInfo;
+
+	private IRecommendationInfo rightInfo;
+
+	private IValidator validator;
+
+	/**
+	 * @param Validates the content of this form when the user clicks 'apply'
+	 * @param leftInfo Delegate for handling the left part of the recommendation-pair
+	 * @param rightInfo Delegate for handling the right part of the recommendation-pair
+	 */
+    public AbstractPairRecommendationPanel(final User user, IValidator validator, final IRecommendationInfo leftInfo, final IRecommendationInfo rightInfo ) {
+        this.user = user;
+		this.validator = validator;
+		this.leftInfo = leftInfo;
+		this.rightInfo = rightInfo;
+    }
+    
+    // FIXME: better than copy/pasting the MSG field into every sub-class but not really nice yet.
+    protected final static FLYSConstants msg() {
+		return MSG_PROVIDER;
+	}
+
+    /**
+     * Remove first occurrence of "[" and "]" (if both do occur).
+     * @param value String to be stripped of [] (might be null).
+     * @return input string but with [ and ] removed, or input string if no
+     *         brackets were found.
+     * @see StringUtil.unbracket
+     */
+    // FIXME: check if this is the same as STringUItils#unbracket
+    private static final String unbracket(String value) {
+        // null- guard.
+        if (value == null) return value;
+
+        int start = value.indexOf("[");
+        int end   = value.indexOf("]");
+
+        if (start < 0 || end < 0) {
+            return value;
+        }
+
+        return value.substring(start + 1, end);
+    }
+
+    /**
+     * Create a recommendation from a string representation of it.
+     * @param from string in format as shown above.
+     * @param leftInfo2 
+     * @return recommendation from input string.
+     */
+    private Recommendation createRecommendationFromString(final String from, final IRecommendationInfo info) {
+        // TODO Construct "real" filter.
+        String[] parts = unbracket(from).split(";");
+        Recommendation.Filter filter = new Recommendation.Filter();
+        Recommendation.Facet  facet  = new Recommendation.Facet(
+                parts[1],
+                parts[2]);
+
+        List<Recommendation.Facet> facets = new ArrayList<Recommendation.Facet>();
+        facets.add(facet);
+        filter.add("longitudinal_section", facets);
+        
+        final String factory = info.getFactory(  );
+
+        final  Recommendation r = new Recommendation(factory, parts[0], this.artifact.getUuid(), filter);
+        r.setDisplayName(parts[3]);
+        return r;
+    }
+
+
+    /**
+     * Add RecomendationPairRecords from input String to the ListGrid.
+     */
+    private void populateGridFromString(String from){
+        // Split this string.
+        // Create according recommendations and display strings.
+        String[] recs = from.split("#");
+        if (recs.length % 2 != 0) return;
+        for (int i = 0; i < recs.length; i+=2) {
+            Recommendation minuend =
+                createRecommendationFromString(recs[i+0], leftInfo);
+            Recommendation subtrahend =
+                createRecommendationFromString(recs[i+1], rightInfo);
+
+            RecommendationPairRecord pr = new RecommendationPairRecord(
+                minuend, subtrahend);
+            // This Recommendation Pair comes from the data string and was thus
+            // already cloned.
+            pr.setIsAlreadyLoaded(true);
+            this.differencesList.addData(pr);
+        }
+    }
+
+    /**
+     * Creates the graphical representation and interaction widgets for the data.
+     * @param dataList the data.
+     * @return graphical representation and interaction widgets for data.
+     */
+    @Override
+    public final Canvas create(DataList dataList) {
+    	
+        final Canvas widget = createWidget();
+    	
+    	final Canvas canvas = createChooserWidgets(widget, dataList, user, differencesList);
+    	
+        populateGrid(dataList);
+        
+        return canvas;
+    }
+
+    /**
+     * Creates the individual parts of the input-helper area ('Eingabeunterstützung') for choosing the content of this widget.
+     */
+    protected abstract Canvas createChooserWidgets(final Canvas widget, final DataList dataList, final User auser, final ListGrid diffList);
+
+	private void populateGrid(DataList dataList) {
+        Data data     = dataList.get(0);
+        this.dataName = data.getLabel();
+        for (int i = 0; i < dataList.size(); i++) {
+            if (dataList.get(i) != null && dataList.get(i).getItems() != null) {
+                if (dataList.get(i).getItems() != null) {
+                    populateGridFromString(
+                        dataList.get(i).getItems()[0].getStringValue());
+                }
+            }
+        }
+    }
+
+    @Override
+    public final List<String> validate() {
+    	return validator.validate(differencesList, MSG_PROVIDER);
+    }
+
+    /**
+     * Creates layout with grid that displays selection inside.
+     */
+    protected final Canvas createWidget() {
+        VLayout layout  = new VLayout();
+        differencesList = new ListGrid();
+
+        differencesList.setCanEdit(false);
+        differencesList.setCanSort(false);
+        differencesList.setShowHeaderContextMenu(false);
+        differencesList.setHeight(150);
+        differencesList.setShowAllRecords(true);
+
+        ListGridField nameField    = new ListGridField("first",  "Minuend");
+        ListGridField capitalField = new ListGridField("second", "Subtrahend");
+        // Track removed rows, therefore more or less reimplement
+        // setCanRecomeRecords.
+        final ListGridField removeField  =
+            new ListGridField("_removeRecord", "Remove Record"){{
+                setType(ListGridFieldType.ICON);
+                setIcon(GWT.getHostPageBaseURL() + msg().removeFeature());
+                setCanEdit(false);
+                setCanFilter(false);
+                setCanSort(false);
+                setCanGroupBy(false);
+                setCanFreeze(false);
+                setWidth(25);
+        }};
+
+        differencesList.setFields(new ListGridField[] {nameField,
+           capitalField, removeField});
+
+        differencesList.addRecordClickHandler(new RecordClickHandler() {
+                @Override
+                public void onRecordClick(final RecordClickEvent event) {
+                    // Just handle remove-clicks
+                    if(!event.getField().getName().equals(removeField.getName())) {
+                        return;
+                    }
+                    trackRemoved(event.getRecord());
+                    event.getViewer().removeData(event.getRecord());
+                }
+            });
+        layout.addMember(differencesList);
+
+        return layout;
+    }
+
+
+    /**
+     * Add record to list of removed records.
+     */
+    protected final void trackRemoved(Record r) {
+        RecommendationPairRecord pr = (RecommendationPairRecord) r;
+        this.removedPairs.add(pr);
+    }
+
+    /**
+     * Validates data, does nothing if invalid, otherwise clones new selected
+     * waterlevels and add them to collection, forward the artifact.
+     */
+    @Override
+    public void onClick(ClickEvent e) {
+        GWT.log("AbstractPairRecommendationPanel.onClick");
+
+        List<String> errors = validate();
+        if (errors != null && !errors.isEmpty()) {
+            showErrors(errors);
+            return;
+        }
+
+        Config config = Config.getInstance();
+        String locale = config.getLocale();
+
+        ListGridRecord[] records = differencesList.getRecords();
+
+        List<Recommendation> ar  = new ArrayList<Recommendation>();
+        List<Recommendation> all = new ArrayList<Recommendation>();
+
+        for (ListGridRecord record : records) {
+            RecommendationPairRecord r =
+                (RecommendationPairRecord) record;
+            // Do not add "old" recommendations.
+            if (!r.isAlreadyLoaded()) {
+                // Check whether one of those is a dike or similar.
+                // TODO differentiate and merge: new clones, new, old.
+                Recommendation firstR = r.getFirst();
+                leftInfo.adjustRecommendation(firstR);
+
+                Recommendation secondR = r.getSecond();
+                rightInfo.adjustRecommendation(secondR);
+                ar.add(firstR);
+                ar.add(secondR);
+            }
+            else {
+                all.add(r.getFirst());
+                all.add(r.getSecond());
+            }
+        }
+
+        final Recommendation[] toClone = ar.toArray(new Recommendation[ar.size()]);
+        final Recommendation[] toUse   = all.toArray(new Recommendation[all.size()]);
+
+        // Find out whether "old" artifacts have to be removed.
+        List<String> artifactIdsToRemove = new ArrayList<String>();
+        for (RecommendationPairRecord rp: this.removedPairs) {
+            Recommendation first  = rp.getFirst();
+            Recommendation second = rp.getSecond();
+
+            for (Recommendation recommendation: toUse) {
+                if (first != null && first.getIDs().equals(recommendation.getIDs())) {
+                    first = null;
+                }
+                if (second != null && second.getIDs().equals(recommendation.getIDs())) {
+                    second = null;
+                }
+
+                if (first == null && second == null) {
+                    break;
+                }
+            }
+            if (first != null) {
+                artifactIdsToRemove.add(first.getIDs());
+            }
+            if (second != null) {
+                artifactIdsToRemove.add(second.getIDs());
+            }
+        }
+
+        // Remove old artifacts, if any. Do this asychronously without much
+        // feedback.
+        for(final String uuid: artifactIdsToRemove) {
+            removeArtifactService.remove(this.collection,
+                uuid,
+                locale,
+                new AsyncCallback<Collection>() {
+                    @Override
+                    public void onFailure(Throwable caught) {
+                        GWT.log("RemoveArtifact (" + uuid + ") failed.");
+                    }
+                    @Override
+                    public void onSuccess(Collection coll) {
+                        GWT.log("RemoveArtifact succeeded");
+                    }
+                });
+        }
+
+        // Clone new ones (and spawn statics), go forward.
+        parameterList.lockUI();
+        loadArtifactService.loadMany(
+            this.collection,
+            toClone,
+            //"staticwkms" and "waterlevel"
+            null,
+            locale,
+            new AsyncCallback<Artifact[]>() {
+                @Override
+                public void onFailure(Throwable caught) {
+                	caught.printStackTrace();
+                    GWT.log("Failure of cloning with factories!");
+                    parameterList.unlockUI();
+                }
+                @Override
+                public void onSuccess(Artifact[] artifacts) {
+                    GWT.log("Successfully cloned " + toClone.length +
+                        " with factories.");
+
+                    fireStepForwardEvent(new StepForwardEvent(
+                        getData(toClone, artifacts, toUse)));
+                    parameterList.unlockUI();
+                }
+            });
+    }
+    
+    /**
+     * Create Data and DataItem from selection (a long string with identifiers
+     * to construct diff-pairs).
+     *
+     * @param newRecommendations "new" recommendations (did not survive a
+     *        backjump).
+     * @param newArtifacts artifacts cloned from newRecommendations.
+     * @param oldRecommendations old recommendations that survived a backjump.
+     *
+     * @return dataitem with a long string with identifiers to construct
+     *         diff-pairs.
+     */
+    protected final Data[] getData(
+            Recommendation[] newRecommendations,
+            Artifact[] newArtifacts,
+            Recommendation[] oldRecommendations)
+    {
+        // Construct string with info about selections.
+        String dataItemString = "";
+        for (int i = 0; i < newRecommendations.length; i++) {
+            Recommendation r = newRecommendations[i];
+            Artifact newArtifact = newArtifacts[i];
+            String uuid = newArtifact.getUuid();
+            r.setMasterArtifact(uuid);
+
+            if (i>0) 
+            	dataItemString += "#";
+
+            // REMARK: ugly, but we know that the recommandations comes in left/right pairs.
+            final IRecommendationInfo info = i % 2 == 0 ? leftInfo : rightInfo;
+            
+            dataItemString += createDataString(uuid, r, info);
+        }
+
+        for (int i = 0; i < oldRecommendations.length; i++) {
+            Recommendation r = oldRecommendations[i];
+            String uuid = r.getIDs();
+            
+            if (dataItemString.length() > 0) 
+            	dataItemString += "#";
+
+            // REMARK: ugly, but we know that the recommandations comes in left/right pairs.
+            final IRecommendationInfo info = i % 2 == 0 ? leftInfo : rightInfo;
+
+            dataItemString += createDataString(uuid, r, info);
+        }
+
+        // TODO some hassle could be resolved by using multiple DataItems
+        // (e.g. one per pair).
+        DataItem item = new DefaultDataItem(dataName, dataName, dataItemString);
+        return new Data[] { new DefaultData(
+            dataName, null, null, new DataItem[] {item}) };
+    }
+
+    /**
+     * Creates part of the String that encodes minuend or subtrahend.
+     * @param recommendation Recommendation to wrap in string.
+     * @param info Provides the factory to encode.
+     */
+    protected static final String createDataString(final String artifactUuid, final Recommendation recommendation, final IRecommendationInfo info) {
+    	final String factory = info.getDataStringFactory();
+        
+    	Filter filter = recommendation.getFilter();
+		Facet  f      = null;
+		
+		if(filter != null) {
+		    Map<String, List<Facet>>               outs = filter.getOuts();
+		    Set<Map.Entry<String, List<Facet>>> entries = outs.entrySet();
+		
+		    for (Map.Entry<String, List<Facet>> entry: entries) {
+		        List<Facet> fs = entry.getValue();
+		
+		        f = fs.get(0);
+		        if (f != null) {
+		            break;
+		        }
+		    }
+		
+		    return "[" + artifactUuid + ";"
+		        + f.getName()
+		        + ";"
+		        + f.getIndex()
+		        + ";"
+		        + recommendation.getDisplayName() + "]";
+		}
+		
+		return "["
+		    + artifactUuid
+		    + ";" + factory + ";0;"
+		    + recommendation.getDisplayName() + "]";
+    }
+}
\ No newline at end of file
--- a/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractUIProvider.java	Thu Jan 18 18:26:30 2018 +0100
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractUIProvider.java	Thu Jan 18 18:32:30 2018 +0100
@@ -307,12 +307,14 @@
         return null;
     }
 
-
+    /**
+     * Validates the selection.
+     * @return List of internationalized errror messages (if any).
+     */
     public List<String> validate() {
         return new ArrayList<String>(); // FIXME: What's this?
     }
 
-
     /** Create simple DefaultData with single DataItem inside. */
     public static DefaultData createDataArray(String name, String value) {
         DataItem item = new DefaultDataItem(
--- a/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DatacagePairWidget.java	Thu Jan 18 18:26:30 2018 +0100
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DatacagePairWidget.java	Thu Jan 18 18:32:30 2018 +0100
@@ -49,12 +49,14 @@
      *
      * @param artifact Artifact to query datacage with.
      * @param user     User to query datacage with.
-     * @param outs     outs to query datacage with.
+     * @param leftOuts     outs to query the left datacage with.
+     * @param rightOuts     outs to query the right datacage with.
      * @param grid     Grid into which to insert selection of pairs.
      */
     public DatacagePairWidget(Artifact artifact,
          User user,
-         String outs,
+         String leftOuts,
+         String rightOuts,
          ListGrid grid) {
         this.grid = grid;
 
@@ -62,13 +64,13 @@
         firstDatacageWidget  = new DatacageWidget(
             artifact,
             user,
-            outs,
+            leftOuts,
             "load-system:true",
             false);
         secondDatacageWidget = new DatacageWidget(
             artifact,
             user,
-            outs,
+            rightOuts,
             "load-system:true",
             false);
         firstDatacageWidget.setIsMutliSelectable(false);
--- a/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DatacageTwinPanel.java	Thu Jan 18 18:26:30 2018 +0100
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DatacageTwinPanel.java	Thu Jan 18 18:32:30 2018 +0100
@@ -8,511 +8,56 @@
 
 package org.dive4elements.river.client.client.ui;
 
+import org.dive4elements.river.client.shared.model.DataList;
+import org.dive4elements.river.client.shared.model.User;
+
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-import com.smartgwt.client.data.Record;
-import com.smartgwt.client.types.ListGridFieldType;
 import com.smartgwt.client.widgets.Canvas;
-import com.smartgwt.client.widgets.events.ClickEvent;
 import com.smartgwt.client.widgets.grid.ListGrid;
-import com.smartgwt.client.widgets.grid.ListGridField;
-import com.smartgwt.client.widgets.grid.ListGridRecord;
-import com.smartgwt.client.widgets.grid.events.RecordClickEvent;
-import com.smartgwt.client.widgets.grid.events.RecordClickHandler;
 import com.smartgwt.client.widgets.layout.HLayout;
 import com.smartgwt.client.widgets.layout.VLayout;
 
-import org.dive4elements.river.client.client.Config;
-import org.dive4elements.river.client.client.FLYSConstants;
-import org.dive4elements.river.client.client.event.StepForwardEvent;
-import org.dive4elements.river.client.client.services.LoadArtifactServiceAsync;
-import org.dive4elements.river.client.client.services.RemoveArtifactServiceAsync;
-import org.dive4elements.river.client.shared.model.Artifact;
-import org.dive4elements.river.client.shared.model.Collection;
-import org.dive4elements.river.client.shared.model.Data;
-import org.dive4elements.river.client.shared.model.DataItem;
-import org.dive4elements.river.client.shared.model.DataList;
-import org.dive4elements.river.client.shared.model.DefaultData;
-import org.dive4elements.river.client.shared.model.DefaultDataItem;
-import org.dive4elements.river.client.shared.model.Recommendation;
-import org.dive4elements.river.client.shared.model.Recommendation.Facet;
-import org.dive4elements.river.client.shared.model.Recommendation.Filter;
-import org.dive4elements.river.client.shared.model.User;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-// TODO Probably better to branch off AbstractUIProvider.
-// TODO Merge with other datacage-widget impls.
 /**
- * Panel containing a Grid and a "next" button. The Grid is fed by a
+ * A {@link AbstractPairRecommendationPanel} that uses a 'TwinDatacage' in the help-input area.
  * DatacagePairWidget which is put in the input-helper area.
  */
-public class DatacageTwinPanel
-extends      TextProvider {
-
-    private static final long serialVersionUID = 8906629596491827857L;
-
-    protected static FLYSConstants MSG = GWT.create(FLYSConstants.class);
-
-    protected String dataName;
-
-    protected User user;
-
-    /** ListGrid that displays user-selected pairs to build differences with. */
-    protected ListGrid differencesList;
-
-    /**
-     * List to track previously selected but now removed pairs. (Needed to
-     * be able to identify artifacts that can be removed from the collection.
-     */
-    protected List<RecommendationPairRecord> removedPairs =
-        new ArrayList<RecommendationPairRecord>();
-
-    /** Service handle to clone and add artifacts to collection. */
-    LoadArtifactServiceAsync loadArtifactService = GWT.create(
-            org.dive4elements.river.client.client.services.LoadArtifactService.class);
-
-    /** Service to remove artifacts from collection. */
-    RemoveArtifactServiceAsync removeArtifactService = GWT.create(
-            org.dive4elements.river.client.client.services.RemoveArtifactService.class);
-
-
-    public DatacageTwinPanel(User user) {
-        super();
-        this.user = user;
-    }
-
-
-    /**
-     * Remove first occurrence of "[" and "]" (if both do occur).
-     * @param value String to be stripped of [] (might be null).
-     * @return input string but with [ and ] removed, or input string if no
-     *         brackets were found.
-     * @see StringUtil.unbracket
-     */
-    public static final String unbracket(String value) {
-        // null- guard.
-        if (value == null) return value;
-
-        int start = value.indexOf("[");
-        int end   = value.indexOf("]");
-
-        if (start < 0 || end < 0) {
-            return value;
-        }
-
-        value = value.substring(start + 1, end);
+public abstract class DatacageTwinPanel
+extends      AbstractPairRecommendationPanel {
 
-        return value;
-    }
-
-
-    /**
-     * Create a recommendation from a string representation of it.
-     * @param from string in format as shown above.
-     * @return recommendation from input string.
-     */
-    public Recommendation createRecommendationFromString(String from, String factory) {
-        // TODO Construct "real" filter.
-        String[] parts = unbracket(from).split(";");
-        Recommendation.Filter filter = new Recommendation.Filter();
-        Recommendation.Facet  facet  = new Recommendation.Facet(
-                parts[1],
-                parts[2]);
-
-        List<Recommendation.Facet> facets = new ArrayList<Recommendation.Facet>
-            ();
-        facets.add(facet);
-        filter.add("longitudinal_section", facets);
-        Recommendation r = new Recommendation(factory, parts[0],
-            this.artifact.getUuid(), filter);
-        r.setDisplayName(parts[3]);
-        return r;
-    }
-
+	private IDatacageTwinPanelInfo leftInfo;
+	private IDatacageTwinPanelInfo rightInfo;
 
-    /**
-     * Add RecomendationPairRecords from input String to the ListGrid.
-     */
-    public void populateGridFromString(String from, String factory){
-        // Split this string.
-        // Create according recommendations and display strings.
-        String[] recs = from.split("#");
-        if (recs.length % 2 != 0) return;
-        for (int i = 0; i < recs.length; i+=2) {
-            Recommendation minuend =
-                createRecommendationFromString(recs[i+0], factory);
-            Recommendation subtrahend =
-                createRecommendationFromString(recs[i+1], factory);
+	public static interface IDatacageTwinPanelInfo extends IRecommendationInfo
+	{
+		String getOuts();
+	}
+	
+	public DatacageTwinPanel(final User user, IValidator validator, final IDatacageTwinPanelInfo leftInfo, final IDatacageTwinPanelInfo rightInfo ) {
+		super(user, validator, leftInfo, rightInfo);
 
-            RecommendationPairRecord pr = new RecommendationPairRecord(
-                minuend, subtrahend);
-            // This Recommendation Pair comes from the data string and was thus
-            // already cloned.
-            pr.setIsAlreadyLoaded(true);
-            this.differencesList.addData(pr);
-        }
-    }
-
-
-    /**
-     * Creates the graphical representation and interaction widgets for the data.
-     * @param dataList the data.
-     * @return graphical representation and interaction widgets for data.
-     */
+		this.leftInfo = leftInfo;
+		this.rightInfo = rightInfo;
+	}
+	
     @Override
-    public Canvas create(DataList dataList) {
+    protected final Canvas createChooserWidgets(final Canvas widget, final DataList dataList, final User user, final ListGrid differencesList) {
         GWT.log("createData()");
 
-        Canvas widget = createWidget();
         Canvas submit = getNextButton();
 
         VLayout layout       = new VLayout();
         HLayout helperLayout = new HLayout();
-        helperLayout.addMember(new DatacagePairWidget(this.artifact,
-            user, "winfo_diff_twin_panel", differencesList));
+        
+        final String leftOuts = leftInfo.getOuts();
+        final String rightOuts = rightInfo.getOuts();
+        
+        helperLayout.addMember(new DatacagePairWidget(this.artifact, user, leftOuts, rightOuts, differencesList));
 
         layout.addMember(widget);
         layout.addMember(submit);
         layout.setMembersMargin(10);
         this.helperContainer.addMember(helperLayout);
 
-        populateGrid(dataList, "waterlevel");
-        return layout;
-    }
-
-    protected void populateGrid(DataList dataList, String factory) {
-        Data data     = dataList.get(0);
-        this.dataName = data.getLabel();
-        for (int i = 0; i < dataList.size(); i++) {
-            if (dataList.get(i) != null && dataList.get(i).getItems() != null) {
-                if (dataList.get(i).getItems() != null) {
-                    populateGridFromString(
-                        dataList.get(i).getItems()[0].getStringValue(),
-                        factory);
-                }
-            }
-        }
-    }
-
-
-    /**
-     * Validates the selection.
-     * @return List of internationalized errror messages (if any).
-     */
-    @Override
-    public List<String> validate() {
-        List<String> errors = new ArrayList<String>();
-        if (differencesList.getRecords().length == 0) {
-            errors.add(MSG.error_no_waterlevel_pair_selected());
-        }
-        // Check whether minuend and subtrahend are equal.
-        for (ListGridRecord record: differencesList.getRecords()) {
-            RecommendationPairRecord r = (RecommendationPairRecord) record;
-            if (r.getFirst().equals(r.getSecond())) {
-                errors.add(MSG.error_same_waterlevels_in_pair());
-            }
-        }
-
-        return errors;
-    }
-
-
-    /**
-     * Creates layout with grid that displays selection inside.
-     */
-    public Canvas createWidget() {
-        VLayout layout  = new VLayout();
-        differencesList = new ListGrid();
-
-        differencesList.setCanEdit(false);
-        differencesList.setCanSort(false);
-        differencesList.setShowHeaderContextMenu(false);
-        differencesList.setHeight(150);
-        differencesList.setShowAllRecords(true);
-
-        ListGridField nameField    = new ListGridField("first",  "Minuend");
-        ListGridField capitalField = new ListGridField("second", "Subtrahend");
-        // Track removed rows, therefore more or less reimplement
-        // setCanRecomeRecords.
-        final ListGridField removeField  =
-            new ListGridField("_removeRecord", "Remove Record"){{
-                setType(ListGridFieldType.ICON);
-                setIcon(GWT.getHostPageBaseURL() + MSG.removeFeature());
-                setCanEdit(false);
-                setCanFilter(false);
-                setCanSort(false);
-                setCanGroupBy(false);
-                setCanFreeze(false);
-                setWidth(25);
-        }};
-
-        differencesList.setFields(new ListGridField[] {nameField,
-           capitalField, removeField});
-
-        differencesList.addRecordClickHandler(new RecordClickHandler() {
-                @Override
-                public void onRecordClick(final RecordClickEvent event) {
-                    // Just handle remove-clicks
-                    if(!event.getField().getName().equals(removeField.getName())) {
-                        return;
-                    }
-                    trackRemoved(event.getRecord());
-                    event.getViewer().removeData(event.getRecord());
-                }
-            });
-        layout.addMember(differencesList);
-
         return layout;
     }
-
-
-    /**
-     * Add record to list of removed records.
-     */
-    public void trackRemoved(Record r) {
-        RecommendationPairRecord pr = (RecommendationPairRecord) r;
-        this.removedPairs.add(pr);
-    }
-
-    /**
-     * Set factory of recommendation such that the correct artifacts will
-     * be cloned for difference calculations.
-     */
-    public void adjustRecommendation(Recommendation recommendation) {
-        // XXX: THIS IS AN EVIL HACK TO MAKE W-DIFFERENCES WORK AGAIN!
-        // TODO: Throw all this code away and do it with server side recommendations!
-        recommendation.setTargetOut("w_differences");
-
-        if (recommendation.getIDs() != null) {
-            GWT.log("Setting staticwkms factory for rec with ID "
-                + recommendation.getIDs());
-            recommendation.setFactory("staticwkms");
-        }
-        /*
-        // So far, we do not need to rewrite the factory anymore,
-        // except for staticwkms; probably other cases will pop up later.
-        else if (recommendation.getFactory().equals("winfo")) {
-            GWT.log("Setting waterlevel factory for a winfo rec.");
-            recommendation.setFactory("waterlevel");
-        }
-        */
-        else {
-           GWT.log("Leave rec. id " + recommendation.getIDs() + ", factory "
-               + recommendation.getFactory() + " untouched.");
-        }
-    }
-
-    /**
-     * Validates data, does nothing if invalid, otherwise clones new selected
-     * waterlevels and add them to collection, forward the artifact.
-     */
-    @Override
-    public void onClick(ClickEvent e) {
-        GWT.log("DatacageTwinPanel.onClick");
-
-        List<String> errors = validate();
-        if (errors != null && !errors.isEmpty()) {
-            showErrors(errors);
-            return;
-        }
-
-        Config config = Config.getInstance();
-        String locale = config.getLocale();
-
-        ListGridRecord[] records = differencesList.getRecords();
-
-        List<Recommendation> ar  = new ArrayList<Recommendation>();
-        List<Recommendation> all = new ArrayList<Recommendation>();
-
-        for (ListGridRecord record : records) {
-            RecommendationPairRecord r =
-                (RecommendationPairRecord) record;
-            // Do not add "old" recommendations.
-            if (!r.isAlreadyLoaded()) {
-                // Check whether one of those is a dike or similar.
-                // TODO differentiate and merge: new clones, new, old.
-                Recommendation firstR = r.getFirst();
-                adjustRecommendation(firstR);
-
-                Recommendation secondR = r.getSecond();
-                adjustRecommendation(secondR);
-                ar.add(firstR);
-                ar.add(secondR);
-            }
-            else {
-                all.add(r.getFirst());
-                all.add(r.getSecond());
-            }
-        }
-
-        final Recommendation[] toClone = ar.toArray(new Recommendation[ar.size()]);
-        final Recommendation[] toUse   = all.toArray(new Recommendation[all.size()]);
-
-        // Find out whether "old" artifacts have to be removed.
-        List<String> artifactIdsToRemove = new ArrayList<String>();
-        for (RecommendationPairRecord rp: this.removedPairs) {
-            Recommendation first  = rp.getFirst();
-            Recommendation second = rp.getSecond();
-
-            for (Recommendation recommendation: toUse) {
-                if (first != null && first.getIDs().equals(recommendation.getIDs())) {
-                    first = null;
-                }
-                if (second != null && second.getIDs().equals(recommendation.getIDs())) {
-                    second = null;
-                }
-
-                if (first == null && second == null) {
-                    break;
-                }
-            }
-            if (first != null) {
-                artifactIdsToRemove.add(first.getIDs());
-            }
-            if (second != null) {
-                artifactIdsToRemove.add(second.getIDs());
-            }
-        }
-
-        // Remove old artifacts, if any. Do this asychronously without much
-        // feedback.
-        for(final String uuid: artifactIdsToRemove) {
-            removeArtifactService.remove(this.collection,
-                uuid,
-                locale,
-                new AsyncCallback<Collection>() {
-                    @Override
-                    public void onFailure(Throwable caught) {
-                        GWT.log("RemoveArtifact (" + uuid + ") failed.");
-                    }
-                    @Override
-                    public void onSuccess(Collection collection) {
-                        GWT.log("RemoveArtifact succeeded");
-                    }
-                });
-        }
-
-        // Clone new ones (and spawn statics), go forward.
-        parameterList.lockUI();
-        loadArtifactService.loadMany(
-            this.collection,
-            toClone,
-            //"staticwkms" and "waterlevel"
-            null,
-            locale,
-            new AsyncCallback<Artifact[]>() {
-                @Override
-                public void onFailure(Throwable caught) {
-                    GWT.log("Failure of cloning with factories!");
-                    parameterList.unlockUI();
-                }
-                @Override
-                public void onSuccess(Artifact[] artifacts) {
-                    GWT.log("Successfully cloned " + toClone.length +
-                        " with factories.");
-
-                    fireStepForwardEvent(new StepForwardEvent(
-                        getData(toClone, artifacts, toUse)));
-                    parameterList.unlockUI();
-                }
-            });
-    }
-
-
-    /**
-     * Create Data and DataItem from selection (a long string with identifiers
-     * to construct diff-pairs).
-     *
-     * @param newRecommendations "new" recommendations (did not survive a
-     *        backjump).
-     * @param newArtifacts artifacts cloned from newRecommendations.
-     * @param oldRecommendations old recommendations that survived a backjump.
-     *
-     * @return dataitem with a long string with identifiers to construct
-     *         diff-pairs.
-     */
-    protected Data[] getData(
-            Recommendation[] newRecommendations,
-            Artifact[] newArtifacts,
-            Recommendation[] oldRecommendations)
-    {
-        // Construct string with info about selections.
-        String dataItemString = "";
-        for (int i = 0; i < newRecommendations.length; i++) {
-            Recommendation r = newRecommendations[i];
-            Artifact newArtifact = newArtifacts[i];
-            String uuid = newArtifact.getUuid();
-            r.setMasterArtifact(uuid);
-            if (i>0) dataItemString += "#";
-
-            dataItemString += createDataString(uuid, r);
-        }
-
-        for (int i = 0; i < oldRecommendations.length; i++) {
-            Recommendation r = oldRecommendations[i];
-            String uuid = r.getIDs();
-            if (dataItemString.length() > 0) dataItemString += "#";
-
-            dataItemString += createDataString(uuid, r);
-        }
-
-        // TODO some hassle could be resolved by using multiple DataItems
-        // (e.g. one per pair).
-        DataItem item = new DefaultDataItem(dataName, dataName, dataItemString);
-        return new Data[] { new DefaultData(
-            dataName, null, null, new DataItem[] {item}) };
-    }
-
-
-    protected String createDataString(String artifact, Recommendation recommendation) {
-        return createDataString(artifact, recommendation, "staticwkms");
-    }
-
-    /**
-     * Creates part of the String that encodes minuend or subtrahend.
-     * @param artifact Artifacts UUID.
-     * @param recommendation Recommendation to wrap in string.
-     * @param factory The factory to encode.
-     */
-    protected String createDataString(
-        String artifact,
-        Recommendation recommendation,
-        String factory)
-    {
-        Filter filter = recommendation.getFilter();
-        Facet  f      = null;
-
-        if(filter != null) {
-            Map<String, List<Facet>>               outs = filter.getOuts();
-            Set<Map.Entry<String, List<Facet>>> entries = outs.entrySet();
-
-            for (Map.Entry<String, List<Facet>> entry: entries) {
-                List<Facet> fs = entry.getValue();
-
-                f = fs.get(0);
-                if (f != null) {
-                    break;
-                }
-            }
-
-            return "[" + artifact + ";"
-                + f.getName()
-                + ";"
-                + f.getIndex()
-                + ";"
-                + recommendation.getDisplayName() + "]";
-        }
-        else {
-            return "["
-                + artifact
-                + ";" + factory + ";0;"
-                + recommendation.getDisplayName() + "]";
-        }
-    }
-}
-// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/DefaultDatacageTwinPanelInfo.java	Thu Jan 18 18:32:30 2018 +0100
@@ -0,0 +1,47 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by 
+ *  Björnsen Beratende Ingenieure GmbH 
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui;
+
+import org.dive4elements.river.client.client.ui.DatacageTwinPanel.IDatacageTwinPanelInfo;
+import org.dive4elements.river.client.shared.model.Recommendation;
+
+/**
+ * @author Gernot Belger
+ */
+public final class DefaultDatacageTwinPanelInfo implements IDatacageTwinPanelInfo {
+
+	private String factory;
+	private String outs;
+
+	public DefaultDatacageTwinPanelInfo(final String factory, final String outs) {
+		this.factory = factory;
+		this.outs = outs;
+	}
+	
+	@Override
+	public String getFactory() {
+		return factory;
+	}
+	
+	@Override
+	public String getDataStringFactory() {
+		return factory;
+	}
+
+    @Override
+	public void adjustRecommendation(Recommendation recommendation) {
+        recommendation.setFactory(factory);
+    }
+
+	@Override
+	public String getOuts() {
+		return outs;
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/WaterlevelRecommandationInfo.java	Thu Jan 18 18:32:30 2018 +0100
@@ -0,0 +1,69 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by 
+ *  Björnsen Beratende Ingenieure GmbH 
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui;
+
+import org.dive4elements.river.client.client.ui.DatacageTwinPanel.IDatacageTwinPanelInfo;
+import org.dive4elements.river.client.shared.model.Recommendation;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * @author Gernot Belger
+ */
+public final class WaterlevelRecommandationInfo implements IDatacageTwinPanelInfo {
+
+	private String outs;
+
+	public WaterlevelRecommandationInfo(String outs ) {
+		this.outs = outs;
+	}
+	
+	@Override
+	public String getFactory() {
+		// FIXME: why are the factory here and the one used in createDataString different?
+		// Probably also because of the 'throw all this code away comment'
+		return "waterlevel";
+	}
+	
+	@Override
+	public String getDataStringFactory() {
+		return "staticwkms";
+	}
+	
+    @Override
+	public void adjustRecommendation(Recommendation recommendation) {
+        // XXX: THIS IS AN EVIL HACK TO MAKE W-DIFFERENCES WORK AGAIN!
+        // TODO: Throw all this code away and do it with server side recommendations!
+        recommendation.setTargetOut("w_differences");
+
+        if (recommendation.getIDs() != null) {
+            GWT.log("Setting staticwkms factory for rec with ID "
+                + recommendation.getIDs());
+            recommendation.setFactory("staticwkms");
+        }
+        /*
+        // So far, we do not need to rewrite the factory anymore,
+        // except for staticwkms; probably other cases will pop up later.
+        else if (recommendation.getFactory().equals("winfo")) {
+            GWT.log("Setting waterlevel factory for a winfo rec.");
+            recommendation.setFactory("waterlevel");
+        }
+        */
+        else {
+           GWT.log("Leave rec. id " + recommendation.getIDs() + ", factory "
+               + recommendation.getFactory() + " untouched.");
+        }
+    }
+
+	@Override
+	public String getOuts() {
+		return outs;
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/WaterlevelTwinPanel.java	Thu Jan 18 18:32:30 2018 +0100
@@ -0,0 +1,25 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by 
+ *  Björnsen Beratende Ingenieure GmbH 
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui;
+
+import org.dive4elements.river.client.shared.model.User;
+
+/**
+ * A DatacageTwinPanel implementation for W-INFO Differences: choose two waterlevels
+ * 
+ * @author Gernot Belger
+ */
+public class WaterlevelTwinPanel
+extends DatacageTwinPanel {
+
+	public WaterlevelTwinPanel(final User user) {
+		super(user, new WaterlevelTwinPanelValidator(), new WaterlevelRecommandationInfo("winfo_diff_twin_panel"), new WaterlevelRecommandationInfo("winfo_diff_twin_panel") );
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/WaterlevelTwinPanelValidator.java	Thu Jan 18 18:32:30 2018 +0100
@@ -0,0 +1,48 @@
+/** Copyright (C) 2017 by Bundesanstalt für Gewässerkunde
+ * Software engineering by 
+ *  Björnsen Beratende Ingenieure GmbH 
+ *  Dr. Schumacher Ingenieurbüro für Wasser und Umwelt
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+package org.dive4elements.river.client.client.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.dive4elements.river.client.client.FLYSConstants;
+import org.dive4elements.river.client.client.ui.AbstractPairRecommendationPanel.IValidator;
+
+import com.smartgwt.client.widgets.grid.ListGrid;
+import com.smartgwt.client.widgets.grid.ListGridRecord;
+
+/**
+ * Contains the old code from the validate-method of the DatacageTwinPanel.
+ *  
+ * @author Gernot Belger
+ */
+public final class WaterlevelTwinPanelValidator implements IValidator {
+
+	@Override
+	public List<String> validate(final ListGrid differencesList, final FLYSConstants msgProvider) {
+		
+        final List<String> errors = new ArrayList<String>();
+        if (differencesList.getRecords().length == 0) {
+        	// FIXME: waterlevel dependent! This will lead to a bad error message in English, for M-Info/Bed-Differences calculation
+            errors.add(msgProvider.error_no_waterlevel_pair_selected());
+        }
+        // Check whether minuend and subtrahend are equal.
+        for (ListGridRecord record: differencesList.getRecords()) {
+            RecommendationPairRecord r = (RecommendationPairRecord) record;
+            if (r.getFirst().equals(r.getSecond())) {
+            	// FIXME: this is still waterlevel specific!
+            	// TODO: delegate validation to specific implementations
+                errors.add(msgProvider.error_same_waterlevels_in_pair());
+            }
+        }
+
+        return errors;
+	}
+}
\ No newline at end of file
--- a/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/minfo/BedHeightsDatacagePanel.java	Thu Jan 18 18:26:30 2018 +0100
+++ b/gwt-client/src/main/java/org/dive4elements/river/client/client/ui/minfo/BedHeightsDatacagePanel.java	Thu Jan 18 18:32:30 2018 +0100
@@ -8,83 +8,51 @@
 
 package org.dive4elements.river.client.client.ui.minfo;
 
+import java.util.List;
+
+import org.dive4elements.river.client.client.ui.AbstractPairRecommendationPanel;
+import org.dive4elements.river.client.client.ui.DatacageWidget;
+import org.dive4elements.river.client.client.ui.DefaultDatacageTwinPanelInfo;
+import org.dive4elements.river.client.client.ui.RecommendationPairRecord;
+import org.dive4elements.river.client.client.ui.WaterlevelTwinPanelValidator;
+import org.dive4elements.river.client.shared.model.DataList;
+import org.dive4elements.river.client.shared.model.ToLoad;
+import org.dive4elements.river.client.shared.model.User;
+
 import com.google.gwt.core.client.GWT;
-
 import com.smartgwt.client.util.SC;
 import com.smartgwt.client.widgets.Button;
 import com.smartgwt.client.widgets.Canvas;
-
 import com.smartgwt.client.widgets.events.ClickEvent;
 import com.smartgwt.client.widgets.events.ClickHandler;
-
+import com.smartgwt.client.widgets.grid.ListGrid;
 import com.smartgwt.client.widgets.layout.VLayout;
 import com.smartgwt.client.widgets.tree.TreeNode;
 
-import org.dive4elements.river.client.client.FLYSConstants;
-
-import org.dive4elements.river.client.client.services.LoadArtifactServiceAsync;
-import org.dive4elements.river.client.client.services.RemoveArtifactServiceAsync;
-
-import org.dive4elements.river.client.client.ui.DatacageTwinPanel;
-import org.dive4elements.river.client.client.ui.DatacageWidget;
-import org.dive4elements.river.client.client.ui.RecommendationPairRecord;
-
-import org.dive4elements.river.client.shared.model.DataList;
-import org.dive4elements.river.client.shared.model.ToLoad;
-
-import org.dive4elements.river.client.shared.model.Recommendation;
-import org.dive4elements.river.client.shared.model.User;
-
-import java.util.ArrayList;
-import java.util.List;
-
-// TODO Probably better to branch off AbstractUIProvider.
 public class BedHeightsDatacagePanel
-extends      DatacageTwinPanel {
-
-    protected static FLYSConstants MSG = GWT.create(FLYSConstants.class);
-
-    /**
-     * List to track previously selected but now removed pairs. (Needed to
-     * be able to identify artifacts that can be removed from the collection.
-     */
-    protected List<RecommendationPairRecord> removedPairs =
-        new ArrayList<RecommendationPairRecord>();
-
-    /** Service handle to clone and add artifacts to collection. */
-    LoadArtifactServiceAsync loadArtifactService = GWT.create(
-            org.dive4elements.river.client.client.services.LoadArtifactService.class);
-
-    /** Service to remove artifacts from collection. */
-    RemoveArtifactServiceAsync removeArtifactService = GWT.create(
-            org.dive4elements.river.client.client.services.RemoveArtifactService.class);
-
-    protected DatacageWidget datacage;
+extends      AbstractPairRecommendationPanel {
 
     public BedHeightsDatacagePanel(User user) {
-        super(user);
+    	// FIXME: This will lead to a bad error message in English (i.e. contains something about waterlevels), for M-Info/Bed-Differences calculation
+    	// BUT: this is the behavior of 3.2.1 (because of sloppy derivation), so we do not change it now
+        super(user, new WaterlevelTwinPanelValidator(), new DefaultDatacageTwinPanelInfo("bedheight", null), new DefaultDatacageTwinPanelInfo("bedheight", null) );
     }
 
-    /**
-     * Creates the graphical representation and interaction widgets for the data.
-     * @param dataList the data.
-     * @return graphical representation and interaction widgets for data.
-     */
     @Override
-    public Canvas create(DataList dataList) {
+    protected Canvas createChooserWidgets(final Canvas widget, final DataList dataList, final User user, final ListGrid differencesList) {
         GWT.log("createData()");
 
-        Canvas widget = createWidget();
         Canvas submit = getNextButton();
-        datacage = new DatacageWidget(
+
+        final DatacageWidget datacage = new DatacageWidget(
             this.artifact, user, "minfo_diff_panel", "load-system:true", false);
 
-        Button plusBtn = new Button(MSG.datacage_add_pair());
+        Button plusBtn = new Button(msg().datacage_add_pair());
         plusBtn.setAutoFit(true);
         plusBtn.addClickHandler(new ClickHandler() {
             @Override
             public void onClick(ClickEvent event) {
-                plusClicked();
+                plusClicked(datacage, differencesList);
             }
         });
 
@@ -98,29 +66,19 @@
         layout.setMembersMargin(10);
         this.helperContainer.addMember(helperLayout);
 
-        populateGrid(dataList, "bedheight");
-
         return layout;
     }
 
-    public void adjustRecommendation(Recommendation recommendation) {
-        recommendation.setFactory("bedheight");
-    }
-
-    @Override
-    protected String createDataString(String artifact, Recommendation recommendation) {
-        return createDataString(artifact, recommendation, "bedheight");
-    }
-
     /**
      * Callback for add-button.
      * Fires to load for every selected element and handler.
+     * @param differencesList 
      */
-    public void plusClicked() {
+    protected final static void plusClicked( final DatacageWidget datacage, ListGrid differencesList ) {
         List<TreeNode> selection = datacage.getPlainSelection();
 
         if (selection == null || selection.isEmpty()) {
-            SC.say(MSG.warning());
+            SC.say(msg().warning());
             return;
         }
 

http://dive4elements.wald.intevation.org