view gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java @ 8875:8d1df8639563

Fixed: strange tweak in w-diferences panel always returned staticwkms factory instead of the original one. In sinfo we need the original one though.
author gernotbelger
date Thu, 08 Feb 2018 18:46:34 +0100
parents 8f6d6d26e96f
children 84397da33d17
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;

    // 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 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_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(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_PROVIDER);
    }

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