Mercurial > dive4elements > river
comparison 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 |
comparison
equal
deleted
inserted
replaced
8851:13650d8a2373 | 8852:8f6d6d26e96f |
---|---|
1 /* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde | |
2 * Software engineering by Intevation GmbH | |
3 * | |
4 * This file is Free Software under the GNU AGPL (>=v3) | |
5 * and comes with ABSOLUTELY NO WARRANTY! Check out the | |
6 * documentation coming with Dive4Elements River for details. | |
7 */ | |
8 | |
9 package org.dive4elements.river.client.client.ui; | |
10 | |
11 import com.google.gwt.core.client.GWT; | |
12 import com.google.gwt.user.client.rpc.AsyncCallback; | |
13 | |
14 import com.smartgwt.client.data.Record; | |
15 import com.smartgwt.client.types.ListGridFieldType; | |
16 import com.smartgwt.client.widgets.Canvas; | |
17 import com.smartgwt.client.widgets.events.ClickEvent; | |
18 import com.smartgwt.client.widgets.grid.ListGrid; | |
19 import com.smartgwt.client.widgets.grid.ListGridField; | |
20 import com.smartgwt.client.widgets.grid.ListGridRecord; | |
21 import com.smartgwt.client.widgets.grid.events.RecordClickEvent; | |
22 import com.smartgwt.client.widgets.grid.events.RecordClickHandler; | |
23 import com.smartgwt.client.widgets.layout.VLayout; | |
24 | |
25 import org.dive4elements.river.client.client.Config; | |
26 import org.dive4elements.river.client.client.FLYSConstants; | |
27 import org.dive4elements.river.client.client.event.StepForwardEvent; | |
28 import org.dive4elements.river.client.client.services.LoadArtifactServiceAsync; | |
29 import org.dive4elements.river.client.client.services.RemoveArtifactServiceAsync; | |
30 import org.dive4elements.river.client.shared.model.Artifact; | |
31 import org.dive4elements.river.client.shared.model.Collection; | |
32 import org.dive4elements.river.client.shared.model.Data; | |
33 import org.dive4elements.river.client.shared.model.DataItem; | |
34 import org.dive4elements.river.client.shared.model.DataList; | |
35 import org.dive4elements.river.client.shared.model.DefaultData; | |
36 import org.dive4elements.river.client.shared.model.DefaultDataItem; | |
37 import org.dive4elements.river.client.shared.model.Recommendation; | |
38 import org.dive4elements.river.client.shared.model.Recommendation.Facet; | |
39 import org.dive4elements.river.client.shared.model.Recommendation.Filter; | |
40 import org.dive4elements.river.client.shared.model.User; | |
41 | |
42 import java.util.ArrayList; | |
43 import java.util.List; | |
44 import java.util.Map; | |
45 import java.util.Set; | |
46 | |
47 // TODO Probably better to branch off AbstractUIProvider. | |
48 // TODO Merge with other datacage-widget impls. | |
49 /** | |
50 * Panel containing a Grid and a "next" button. The Grid is fed by a | |
51 * DatacagePairWidget which is put in the input-helper area. | |
52 */ | |
53 public abstract class AbstractPairRecommendationPanel | |
54 extends TextProvider { | |
55 | |
56 /** | |
57 * Allows for abstraction on how to handle/serialize the recommendation and the used factories. | |
58 * @author Gernot Belger | |
59 * | |
60 */ | |
61 public static interface IRecommendationInfo { | |
62 | |
63 String getFactory(); | |
64 | |
65 /** | |
66 * Separate factory for the 'createDataString' method, because in the case of waterlevels. See HOTFIX/FIXME there. | |
67 */ | |
68 String getDataStringFactory(); | |
69 | |
70 /** | |
71 * Set factory of recommendation such that the correct artifacts will | |
72 * be cloned for difference calculations. | |
73 */ | |
74 void adjustRecommendation(Recommendation recommendation); | |
75 } | |
76 | |
77 public static interface IValidator | |
78 { | |
79 List<String> validate(ListGrid differencesList, FLYSConstants msgProvider); | |
80 } | |
81 | |
82 private static final long serialVersionUID = 8906629596491827857L; | |
83 | |
84 // FIXME: why? we hide the field of the super class with exactly the same thing... | |
85 private static FLYSConstants MSG_PROVIDER = GWT.create(FLYSConstants.class); | |
86 | |
87 private String dataName; | |
88 | |
89 private User user; | |
90 | |
91 /** ListGrid that displays user-selected pairs to build differences with. */ | |
92 private ListGrid differencesList; | |
93 | |
94 /** | |
95 * List to track previously selected but now removed pairs. (Needed to | |
96 * be able to identify artifacts that can be removed from the collection. | |
97 */ | |
98 private List<RecommendationPairRecord> removedPairs = | |
99 new ArrayList<RecommendationPairRecord>(); | |
100 | |
101 /** Service handle to clone and add artifacts to collection. */ | |
102 private LoadArtifactServiceAsync loadArtifactService = GWT.create( | |
103 org.dive4elements.river.client.client.services.LoadArtifactService.class); | |
104 | |
105 /** Service to remove artifacts from collection. */ | |
106 private RemoveArtifactServiceAsync removeArtifactService = GWT.create( | |
107 org.dive4elements.river.client.client.services.RemoveArtifactService.class); | |
108 | |
109 private IRecommendationInfo leftInfo; | |
110 | |
111 private IRecommendationInfo rightInfo; | |
112 | |
113 private IValidator validator; | |
114 | |
115 /** | |
116 * @param Validates the content of this form when the user clicks 'apply' | |
117 * @param leftInfo Delegate for handling the left part of the recommendation-pair | |
118 * @param rightInfo Delegate for handling the right part of the recommendation-pair | |
119 */ | |
120 public AbstractPairRecommendationPanel(final User user, IValidator validator, final IRecommendationInfo leftInfo, final IRecommendationInfo rightInfo ) { | |
121 this.user = user; | |
122 this.validator = validator; | |
123 this.leftInfo = leftInfo; | |
124 this.rightInfo = rightInfo; | |
125 } | |
126 | |
127 // FIXME: better than copy/pasting the MSG field into every sub-class but not really nice yet. | |
128 protected final static FLYSConstants msg() { | |
129 return MSG_PROVIDER; | |
130 } | |
131 | |
132 /** | |
133 * Remove first occurrence of "[" and "]" (if both do occur). | |
134 * @param value String to be stripped of [] (might be null). | |
135 * @return input string but with [ and ] removed, or input string if no | |
136 * brackets were found. | |
137 * @see StringUtil.unbracket | |
138 */ | |
139 // FIXME: check if this is the same as STringUItils#unbracket | |
140 private static final String unbracket(String value) { | |
141 // null- guard. | |
142 if (value == null) return value; | |
143 | |
144 int start = value.indexOf("["); | |
145 int end = value.indexOf("]"); | |
146 | |
147 if (start < 0 || end < 0) { | |
148 return value; | |
149 } | |
150 | |
151 return value.substring(start + 1, end); | |
152 } | |
153 | |
154 /** | |
155 * Create a recommendation from a string representation of it. | |
156 * @param from string in format as shown above. | |
157 * @param leftInfo2 | |
158 * @return recommendation from input string. | |
159 */ | |
160 private Recommendation createRecommendationFromString(final String from, final IRecommendationInfo info) { | |
161 // TODO Construct "real" filter. | |
162 String[] parts = unbracket(from).split(";"); | |
163 Recommendation.Filter filter = new Recommendation.Filter(); | |
164 Recommendation.Facet facet = new Recommendation.Facet( | |
165 parts[1], | |
166 parts[2]); | |
167 | |
168 List<Recommendation.Facet> facets = new ArrayList<Recommendation.Facet>(); | |
169 facets.add(facet); | |
170 filter.add("longitudinal_section", facets); | |
171 | |
172 final String factory = info.getFactory( ); | |
173 | |
174 final Recommendation r = new Recommendation(factory, parts[0], this.artifact.getUuid(), filter); | |
175 r.setDisplayName(parts[3]); | |
176 return r; | |
177 } | |
178 | |
179 | |
180 /** | |
181 * Add RecomendationPairRecords from input String to the ListGrid. | |
182 */ | |
183 private void populateGridFromString(String from){ | |
184 // Split this string. | |
185 // Create according recommendations and display strings. | |
186 String[] recs = from.split("#"); | |
187 if (recs.length % 2 != 0) return; | |
188 for (int i = 0; i < recs.length; i+=2) { | |
189 Recommendation minuend = | |
190 createRecommendationFromString(recs[i+0], leftInfo); | |
191 Recommendation subtrahend = | |
192 createRecommendationFromString(recs[i+1], rightInfo); | |
193 | |
194 RecommendationPairRecord pr = new RecommendationPairRecord( | |
195 minuend, subtrahend); | |
196 // This Recommendation Pair comes from the data string and was thus | |
197 // already cloned. | |
198 pr.setIsAlreadyLoaded(true); | |
199 this.differencesList.addData(pr); | |
200 } | |
201 } | |
202 | |
203 /** | |
204 * Creates the graphical representation and interaction widgets for the data. | |
205 * @param dataList the data. | |
206 * @return graphical representation and interaction widgets for data. | |
207 */ | |
208 @Override | |
209 public final Canvas create(DataList dataList) { | |
210 | |
211 final Canvas widget = createWidget(); | |
212 | |
213 final Canvas canvas = createChooserWidgets(widget, dataList, user, differencesList); | |
214 | |
215 populateGrid(dataList); | |
216 | |
217 return canvas; | |
218 } | |
219 | |
220 /** | |
221 * Creates the individual parts of the input-helper area ('Eingabeunterstützung') for choosing the content of this widget. | |
222 */ | |
223 protected abstract Canvas createChooserWidgets(final Canvas widget, final DataList dataList, final User auser, final ListGrid diffList); | |
224 | |
225 private void populateGrid(DataList dataList) { | |
226 Data data = dataList.get(0); | |
227 this.dataName = data.getLabel(); | |
228 for (int i = 0; i < dataList.size(); i++) { | |
229 if (dataList.get(i) != null && dataList.get(i).getItems() != null) { | |
230 if (dataList.get(i).getItems() != null) { | |
231 populateGridFromString( | |
232 dataList.get(i).getItems()[0].getStringValue()); | |
233 } | |
234 } | |
235 } | |
236 } | |
237 | |
238 @Override | |
239 public final List<String> validate() { | |
240 return validator.validate(differencesList, MSG_PROVIDER); | |
241 } | |
242 | |
243 /** | |
244 * Creates layout with grid that displays selection inside. | |
245 */ | |
246 protected final Canvas createWidget() { | |
247 VLayout layout = new VLayout(); | |
248 differencesList = new ListGrid(); | |
249 | |
250 differencesList.setCanEdit(false); | |
251 differencesList.setCanSort(false); | |
252 differencesList.setShowHeaderContextMenu(false); | |
253 differencesList.setHeight(150); | |
254 differencesList.setShowAllRecords(true); | |
255 | |
256 ListGridField nameField = new ListGridField("first", "Minuend"); | |
257 ListGridField capitalField = new ListGridField("second", "Subtrahend"); | |
258 // Track removed rows, therefore more or less reimplement | |
259 // setCanRecomeRecords. | |
260 final ListGridField removeField = | |
261 new ListGridField("_removeRecord", "Remove Record"){{ | |
262 setType(ListGridFieldType.ICON); | |
263 setIcon(GWT.getHostPageBaseURL() + msg().removeFeature()); | |
264 setCanEdit(false); | |
265 setCanFilter(false); | |
266 setCanSort(false); | |
267 setCanGroupBy(false); | |
268 setCanFreeze(false); | |
269 setWidth(25); | |
270 }}; | |
271 | |
272 differencesList.setFields(new ListGridField[] {nameField, | |
273 capitalField, removeField}); | |
274 | |
275 differencesList.addRecordClickHandler(new RecordClickHandler() { | |
276 @Override | |
277 public void onRecordClick(final RecordClickEvent event) { | |
278 // Just handle remove-clicks | |
279 if(!event.getField().getName().equals(removeField.getName())) { | |
280 return; | |
281 } | |
282 trackRemoved(event.getRecord()); | |
283 event.getViewer().removeData(event.getRecord()); | |
284 } | |
285 }); | |
286 layout.addMember(differencesList); | |
287 | |
288 return layout; | |
289 } | |
290 | |
291 | |
292 /** | |
293 * Add record to list of removed records. | |
294 */ | |
295 protected final void trackRemoved(Record r) { | |
296 RecommendationPairRecord pr = (RecommendationPairRecord) r; | |
297 this.removedPairs.add(pr); | |
298 } | |
299 | |
300 /** | |
301 * Validates data, does nothing if invalid, otherwise clones new selected | |
302 * waterlevels and add them to collection, forward the artifact. | |
303 */ | |
304 @Override | |
305 public void onClick(ClickEvent e) { | |
306 GWT.log("AbstractPairRecommendationPanel.onClick"); | |
307 | |
308 List<String> errors = validate(); | |
309 if (errors != null && !errors.isEmpty()) { | |
310 showErrors(errors); | |
311 return; | |
312 } | |
313 | |
314 Config config = Config.getInstance(); | |
315 String locale = config.getLocale(); | |
316 | |
317 ListGridRecord[] records = differencesList.getRecords(); | |
318 | |
319 List<Recommendation> ar = new ArrayList<Recommendation>(); | |
320 List<Recommendation> all = new ArrayList<Recommendation>(); | |
321 | |
322 for (ListGridRecord record : records) { | |
323 RecommendationPairRecord r = | |
324 (RecommendationPairRecord) record; | |
325 // Do not add "old" recommendations. | |
326 if (!r.isAlreadyLoaded()) { | |
327 // Check whether one of those is a dike or similar. | |
328 // TODO differentiate and merge: new clones, new, old. | |
329 Recommendation firstR = r.getFirst(); | |
330 leftInfo.adjustRecommendation(firstR); | |
331 | |
332 Recommendation secondR = r.getSecond(); | |
333 rightInfo.adjustRecommendation(secondR); | |
334 ar.add(firstR); | |
335 ar.add(secondR); | |
336 } | |
337 else { | |
338 all.add(r.getFirst()); | |
339 all.add(r.getSecond()); | |
340 } | |
341 } | |
342 | |
343 final Recommendation[] toClone = ar.toArray(new Recommendation[ar.size()]); | |
344 final Recommendation[] toUse = all.toArray(new Recommendation[all.size()]); | |
345 | |
346 // Find out whether "old" artifacts have to be removed. | |
347 List<String> artifactIdsToRemove = new ArrayList<String>(); | |
348 for (RecommendationPairRecord rp: this.removedPairs) { | |
349 Recommendation first = rp.getFirst(); | |
350 Recommendation second = rp.getSecond(); | |
351 | |
352 for (Recommendation recommendation: toUse) { | |
353 if (first != null && first.getIDs().equals(recommendation.getIDs())) { | |
354 first = null; | |
355 } | |
356 if (second != null && second.getIDs().equals(recommendation.getIDs())) { | |
357 second = null; | |
358 } | |
359 | |
360 if (first == null && second == null) { | |
361 break; | |
362 } | |
363 } | |
364 if (first != null) { | |
365 artifactIdsToRemove.add(first.getIDs()); | |
366 } | |
367 if (second != null) { | |
368 artifactIdsToRemove.add(second.getIDs()); | |
369 } | |
370 } | |
371 | |
372 // Remove old artifacts, if any. Do this asychronously without much | |
373 // feedback. | |
374 for(final String uuid: artifactIdsToRemove) { | |
375 removeArtifactService.remove(this.collection, | |
376 uuid, | |
377 locale, | |
378 new AsyncCallback<Collection>() { | |
379 @Override | |
380 public void onFailure(Throwable caught) { | |
381 GWT.log("RemoveArtifact (" + uuid + ") failed."); | |
382 } | |
383 @Override | |
384 public void onSuccess(Collection coll) { | |
385 GWT.log("RemoveArtifact succeeded"); | |
386 } | |
387 }); | |
388 } | |
389 | |
390 // Clone new ones (and spawn statics), go forward. | |
391 parameterList.lockUI(); | |
392 loadArtifactService.loadMany( | |
393 this.collection, | |
394 toClone, | |
395 //"staticwkms" and "waterlevel" | |
396 null, | |
397 locale, | |
398 new AsyncCallback<Artifact[]>() { | |
399 @Override | |
400 public void onFailure(Throwable caught) { | |
401 caught.printStackTrace(); | |
402 GWT.log("Failure of cloning with factories!"); | |
403 parameterList.unlockUI(); | |
404 } | |
405 @Override | |
406 public void onSuccess(Artifact[] artifacts) { | |
407 GWT.log("Successfully cloned " + toClone.length + | |
408 " with factories."); | |
409 | |
410 fireStepForwardEvent(new StepForwardEvent( | |
411 getData(toClone, artifacts, toUse))); | |
412 parameterList.unlockUI(); | |
413 } | |
414 }); | |
415 } | |
416 | |
417 /** | |
418 * Create Data and DataItem from selection (a long string with identifiers | |
419 * to construct diff-pairs). | |
420 * | |
421 * @param newRecommendations "new" recommendations (did not survive a | |
422 * backjump). | |
423 * @param newArtifacts artifacts cloned from newRecommendations. | |
424 * @param oldRecommendations old recommendations that survived a backjump. | |
425 * | |
426 * @return dataitem with a long string with identifiers to construct | |
427 * diff-pairs. | |
428 */ | |
429 protected final Data[] getData( | |
430 Recommendation[] newRecommendations, | |
431 Artifact[] newArtifacts, | |
432 Recommendation[] oldRecommendations) | |
433 { | |
434 // Construct string with info about selections. | |
435 String dataItemString = ""; | |
436 for (int i = 0; i < newRecommendations.length; i++) { | |
437 Recommendation r = newRecommendations[i]; | |
438 Artifact newArtifact = newArtifacts[i]; | |
439 String uuid = newArtifact.getUuid(); | |
440 r.setMasterArtifact(uuid); | |
441 | |
442 if (i>0) | |
443 dataItemString += "#"; | |
444 | |
445 // REMARK: ugly, but we know that the recommandations comes in left/right pairs. | |
446 final IRecommendationInfo info = i % 2 == 0 ? leftInfo : rightInfo; | |
447 | |
448 dataItemString += createDataString(uuid, r, info); | |
449 } | |
450 | |
451 for (int i = 0; i < oldRecommendations.length; i++) { | |
452 Recommendation r = oldRecommendations[i]; | |
453 String uuid = r.getIDs(); | |
454 | |
455 if (dataItemString.length() > 0) | |
456 dataItemString += "#"; | |
457 | |
458 // REMARK: ugly, but we know that the recommandations comes in left/right pairs. | |
459 final IRecommendationInfo info = i % 2 == 0 ? leftInfo : rightInfo; | |
460 | |
461 dataItemString += createDataString(uuid, r, info); | |
462 } | |
463 | |
464 // TODO some hassle could be resolved by using multiple DataItems | |
465 // (e.g. one per pair). | |
466 DataItem item = new DefaultDataItem(dataName, dataName, dataItemString); | |
467 return new Data[] { new DefaultData( | |
468 dataName, null, null, new DataItem[] {item}) }; | |
469 } | |
470 | |
471 /** | |
472 * Creates part of the String that encodes minuend or subtrahend. | |
473 * @param recommendation Recommendation to wrap in string. | |
474 * @param info Provides the factory to encode. | |
475 */ | |
476 protected static final String createDataString(final String artifactUuid, final Recommendation recommendation, final IRecommendationInfo info) { | |
477 final String factory = info.getDataStringFactory(); | |
478 | |
479 Filter filter = recommendation.getFilter(); | |
480 Facet f = null; | |
481 | |
482 if(filter != null) { | |
483 Map<String, List<Facet>> outs = filter.getOuts(); | |
484 Set<Map.Entry<String, List<Facet>>> entries = outs.entrySet(); | |
485 | |
486 for (Map.Entry<String, List<Facet>> entry: entries) { | |
487 List<Facet> fs = entry.getValue(); | |
488 | |
489 f = fs.get(0); | |
490 if (f != null) { | |
491 break; | |
492 } | |
493 } | |
494 | |
495 return "[" + artifactUuid + ";" | |
496 + f.getName() | |
497 + ";" | |
498 + f.getIndex() | |
499 + ";" | |
500 + recommendation.getDisplayName() + "]"; | |
501 } | |
502 | |
503 return "[" | |
504 + artifactUuid | |
505 + ";" + factory + ";0;" | |
506 + recommendation.getDisplayName() + "]"; | |
507 } | |
508 } |