view gwt-client/src/main/java/org/dive4elements/river/client/client/ui/AbstractPairRecommendationPanel.java @ 8870:c26fb37899ca

Introduced groups for modules. Modules marked with the same group-id, will be put together in the ui. Also using now the localization info from the server instead of localizing the modules again on the client side.
author gernotbelger
date Wed, 07 Feb 2018 11:59:13 +0100
parents 8f6d6d26e96f
children 8d1df8639563
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 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() + "]";
    }
}

http://dive4elements.wald.intevation.org