Mercurial > dive4elements > river
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