view gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java @ 9416:05405292a7ca

Navigationtheme panel now shows themes of dWt and WQ charts grayed out, if the current station is outside the valid range of the theme.
author gernotbelger
date Thu, 16 Aug 2018 16:28:03 +0200
parents 84397da33d17
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