view gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java @ 9227:84397da33d17

Allow to control specific behaviour in TwinDatacagePanel Implemented client logic of 'intelligent datacage filtering' for SINFO
author gernotbelger
date Wed, 04 Jul 2018 18:28:08 +0200
parents 8d1df8639563
children 839b2aa84dd0
line wrap: on
line source
/* 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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

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 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;

// 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.
     * Basically this allows to tweak the factory that is delivered from the datacage to be replaced by a specific one...
     *
     * @author Gernot Belger
     */
    public static interface IRecommendationInfo	{

        String getFactory(String originalFactory);

        /**
         * Separate factory for the 'createDataString' method, because in the case of waterlevels. See HOTFIX/FIXME there.
         * @param recommendation
         */
        String getDataStringFactory(Recommendation recommendation);

        /**
         * 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;

    private String dataName;

    private final 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 final List<RecommendationPairRecord> removedPairs =
            new ArrayList<RecommendationPairRecord>();

    /** Service handle to clone and add artifacts to collection. */
    private final LoadArtifactServiceAsync loadArtifactService = GWT.create(
            org.dive4elements.river.client.client.services.LoadArtifactService.class);

    /** Service to remove artifacts from collection. */
    private final RemoveArtifactServiceAsync removeArtifactService = GWT.create(
            org.dive4elements.river.client.client.services.RemoveArtifactService.class);

    private final IRecommendationInfo leftInfo;

    private final IRecommendationInfo rightInfo;

    private final 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, final 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;
    }

    /**
     * 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(final String value) {
        // null- guard.
        if (value == null) return value;

        final int start = value.indexOf("[");
        final 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.
        final String[] parts = unbracket(from).split(";");
        final Recommendation.Filter filter = new Recommendation.Filter();
        final Recommendation.Facet  facet  = new Recommendation.Facet(
                parts[1],
                parts[2]);

        final List<Recommendation.Facet> facets = new ArrayList<Recommendation.Facet>();
        facets.add(facet);
        filter.add("longitudinal_section", facets);

        final String factory = info.getFactory( parts[1] );

        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(final String from){
        // Split this string.
        // Create according recommendations and display strings.
        final String[] recs = from.split("#");
        if (recs.length % 2 != 0) return;
        for (int i = 0; i < recs.length; i+=2) {
            final Recommendation minuend =
                    createRecommendationFromString(recs[i+0], this.leftInfo);
            final Recommendation subtrahend =
                    createRecommendationFromString(recs[i+1], this.rightInfo);

            final 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(final DataList dataList) {

        final Canvas widget = createWidget();

        final Canvas canvas = createChooserWidgets(widget, dataList, this.user, this.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(final DataList dataList) {
        final 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 this.validator.validate(this.differencesList, msg());
    }

    /**
     * Creates layout with grid that displays selection inside.
     */
    protected final Canvas createWidget() {
        final VLayout layout  = new VLayout();
        this.differencesList = new ListGrid();

        this.differencesList.setCanEdit(false);
        this.differencesList.setCanSort(false);
        this.differencesList.setShowHeaderContextMenu(false);
        this.differencesList.setHeight(150);
        this.differencesList.setShowAllRecords(true);

        final ListGridField nameField    = new ListGridField("first",  "Minuend");
        final 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);
                }};

                this.differencesList.setFields(new ListGridField[] {nameField,
                        capitalField, removeField});

                this.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(this.differencesList);

                return layout;
    }


    /**
     * Add record to list of removed records.
     */
    protected final void trackRemoved(final Record r) {
        final 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(final ClickEvent e) {
        GWT.log("AbstractPairRecommendationPanel.onClick");

        final List<String> errors = validate();
        if (errors != null && !errors.isEmpty()) {
            showErrors(errors);
            return;
        }

        final Config config = Config.getInstance();
        final String locale = config.getLocale();

        final ListGridRecord[] records = this.differencesList.getRecords();

        final List<Recommendation> ar  = new ArrayList<Recommendation>();
        final List<Recommendation> all = new ArrayList<Recommendation>();

        for (final ListGridRecord record : records) {
            final 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.
                final Recommendation firstR = r.getFirst();
                this.leftInfo.adjustRecommendation(firstR);

                final Recommendation secondR = r.getSecond();
                this.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.
        final List<String> artifactIdsToRemove = new ArrayList<String>();
        for (final RecommendationPairRecord rp: this.removedPairs) {
            Recommendation first  = rp.getFirst();
            Recommendation second = rp.getSecond();

            for (final 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) {
            this.removeArtifactService.remove(this.collection,
                    uuid,
                    locale,
                    new AsyncCallback<Collection>() {
                @Override
                public void onFailure(final Throwable caught) {
                    GWT.log("RemoveArtifact (" + uuid + ") failed.");
                }
                @Override
                public void onSuccess(final Collection coll) {
                    GWT.log("RemoveArtifact succeeded");
                }
            });
        }

        // Clone new ones (and spawn statics), go forward.
        this.parameterList.lockUI();
        this.loadArtifactService.loadMany(
                this.collection,
                toClone,
                //"staticwkms" and "waterlevel"
                null,
                locale,
                new AsyncCallback<Artifact[]>() {
                    @Override
                    public void onFailure(final Throwable caught) {
                        caught.printStackTrace();
                        GWT.log("Failure of cloning with factories!");
                        AbstractPairRecommendationPanel.this.parameterList.unlockUI();
                    }
                    @Override
                    public void onSuccess(final Artifact[] artifacts) {
                        GWT.log("Successfully cloned " + toClone.length +
                                " with factories.");

                        fireStepForwardEvent(new StepForwardEvent(
                                getData(toClone, artifacts, toUse)));
                        AbstractPairRecommendationPanel.this.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(
            final Recommendation[] newRecommendations,
            final Artifact[] newArtifacts,
            final Recommendation[] oldRecommendations)
    {
        // Construct string with info about selections.
        String dataItemString = "";
        for (int i = 0; i < newRecommendations.length; i++) {
            final Recommendation r = newRecommendations[i];
            final Artifact newArtifact = newArtifacts[i];
            final 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 ? this.leftInfo : this.rightInfo;

            dataItemString += createDataString(uuid, r, info);
        }

        for (int i = 0; i < oldRecommendations.length; i++) {
            final Recommendation r = oldRecommendations[i];
            final 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 ? this.leftInfo : this.rightInfo;

            dataItemString += createDataString(uuid, r, info);
        }

        // TODO some hassle could be resolved by using multiple DataItems
        // (e.g. one per pair).
        final DataItem item = new DefaultDataItem(this.dataName, this.dataName, dataItemString);
        return new Data[] { new DefaultData(
                this.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( recommendation );

        final Filter filter = recommendation.getFilter();
        Facet  f      = null;

        if(filter != null) {
            final Map<String, List<Facet>>               outs = filter.getOuts();
            final Set<Map.Entry<String, List<Facet>>> entries = outs.entrySet();

            for (final Map.Entry<String, List<Facet>> entry: entries) {
                final 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() + "]";
    }
}

http://dive4elements.wald.intevation.org