diff gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java @ 8852:8f6d6d26e96f

Refaktored the DatacageTwinPanel so it is reusable.
author gernotbelger
date Thu, 18 Jan 2018 18:32:30 +0100
parents
children 8d1df8639563
line wrap: on
line diff
--- /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

http://dive4elements.wald.intevation.org