source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/properties/PropertiesMergeModel.java@ 1811

Last change on this file since 1811 was 1811, checked in by jttt, 15 years ago

PleaseWait refactoring. Progress is now reported using ProgressMonitor interface, that is available through PleaseWaitRunnable.

File size: 21.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.properties;
3
4import static org.openstreetmap.josm.gui.conflict.MergeDecisionType.UNDECIDED;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.beans.PropertyChangeListener;
8import java.beans.PropertyChangeSupport;
9import java.util.ArrayList;
10import java.util.HashMap;
11import java.util.List;
12import java.util.Observable;
13
14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.command.Command;
18import org.openstreetmap.josm.command.CoordinateConflictResolveCommand;
19import org.openstreetmap.josm.command.DeletedStateConflictResolveCommand;
20import org.openstreetmap.josm.command.PurgePrimitivesCommand;
21import org.openstreetmap.josm.command.UndeletePrimitivesCommand;
22import org.openstreetmap.josm.data.coor.LatLon;
23import org.openstreetmap.josm.data.osm.DataSet;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.OsmPrimitive;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.RelationMember;
28import org.openstreetmap.josm.data.osm.Way;
29import org.openstreetmap.josm.gui.conflict.MergeDecisionType;
30import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
31import org.openstreetmap.josm.io.MultiFetchServerObjectReader;
32import org.openstreetmap.josm.io.OsmTransferException;
33
34/**
35 * This is the model for resolving conflicts in the properties of the
36 * {@see OsmPrimitive}s. In particular, it represents conflicts in the coordiates of {@see Node}s and
37 * the deleted or visible state of {@see OsmPrimitive}s.
38 *
39 * This model is an {@see Observable}. It notifies registered {@see Observer}s whenever the
40 * internal state changes.
41 *
42 * This model also emits property changes for {@see #RESOLVED_COMPLETELY_PROP}. Property change
43 * listeners may register themselves using {@see #addPropertyChangeListener(PropertyChangeListener)}.
44 *
45 * @see Node#getCoor()
46 * @see OsmPrimitive#deleted
47 * @see OsmPrimitive#visible
48 *
49 */
50public class PropertiesMergeModel extends Observable {
51
52 static public final String RESOLVED_COMPLETELY_PROP = PropertiesMergeModel.class.getName() + ".resolvedCompletely";
53
54 private OsmPrimitive my;
55
56 private LatLon myCoords;
57 private LatLon theirCoords;
58 private MergeDecisionType coordMergeDecision;
59
60 private boolean myDeletedState;
61 private boolean theirDeletedState;
62 private boolean myVisibleState;
63 private boolean theirVisibleState;
64 private MergeDecisionType deletedMergeDecision;
65 private MergeDecisionType visibleMergeDecision;
66 private final PropertyChangeSupport support;
67 private boolean resolvedCompletely;
68
69 public void addPropertyChangeListener(PropertyChangeListener listener) {
70 support.addPropertyChangeListener(listener);
71 }
72
73 public void removePropertyChangeListener(PropertyChangeListener listener) {
74 support.removePropertyChangeListener(listener);
75 }
76
77 public void fireCompletelyResolved() {
78 boolean oldValue = resolvedCompletely;
79 resolvedCompletely = isResolvedCompletely();
80 support.firePropertyChange(RESOLVED_COMPLETELY_PROP, oldValue, resolvedCompletely);
81 }
82
83 public PropertiesMergeModel() {
84 coordMergeDecision = UNDECIDED;
85 deletedMergeDecision = UNDECIDED;
86 support = new PropertyChangeSupport(this);
87 resolvedCompletely = false;
88 }
89
90 /**
91 * replies true if there is a coordinate conflict and if this conflict is
92 * resolved
93 *
94 * @return true if there is a coordinate conflict and if this conflict is
95 * resolved; false, otherwise
96 */
97 public boolean isDecidedCoord() {
98 return ! coordMergeDecision.equals(UNDECIDED);
99 }
100
101 /**
102 * replies true if there is a conflict in the deleted state and if this conflict is
103 * resolved
104 *
105 * @return true if there is a conflict in the deleted state and if this conflict is
106 * resolved; false, otherwise
107 */
108 public boolean isDecidedDeletedState() {
109 return ! deletedMergeDecision.equals(UNDECIDED);
110 }
111
112 /**
113 * replies true if there is a conflict in the visible state and if this conflict is
114 * resolved
115 *
116 * @return true if there is a conflict in the visible state and if this conflict is
117 * resolved; false, otherwise
118 */
119 public boolean isDecidedVisibleState() {
120 return ! visibleMergeDecision.equals(UNDECIDED);
121 }
122
123 /**
124 * replies true if the current decision for the coordinate conflict is <code>decision</code>
125 *
126 * @return true if the current decision for the coordinate conflict is <code>decision</code>;
127 * false, otherwise
128 */
129 public boolean isCoordMergeDecision(MergeDecisionType decision) {
130 return coordMergeDecision.equals(decision);
131 }
132
133 /**
134 * replies true if the current decision for the deleted state conflict is <code>decision</code>
135 *
136 * @return true if the current decision for the deleted state conflict is <code>decision</code>;
137 * false, otherwise
138 */
139 public boolean isDeletedStateDecision(MergeDecisionType decision) {
140 return deletedMergeDecision.equals(decision);
141 }
142
143 /**
144 * replies true if the current decision for the visible state conflict is <code>decision</code>
145 *
146 * @return true if the current decision for the visible state conflict is <code>decision</code>;
147 * false, otherwise
148 */
149 public boolean isVisibleStateDecision(MergeDecisionType decision) {
150 return visibleMergeDecision.equals(decision);
151 }
152 /**
153 * populates the model with the differences between my and their version
154 *
155 * @param my my version of the primitive
156 * @param their their version of the primitive
157 */
158 public void populate(OsmPrimitive my, OsmPrimitive their) {
159 this.my = my;
160 if (my instanceof Node) {
161 myCoords = ((Node)my).getCoor();
162 theirCoords = ((Node)their).getCoor();
163 } else {
164 myCoords = null;
165 theirCoords = null;
166 }
167
168 myDeletedState = my.deleted;
169 theirDeletedState = their.deleted;
170
171 myVisibleState = my.visible;
172 theirVisibleState = their.visible;
173
174 coordMergeDecision = UNDECIDED;
175 deletedMergeDecision = UNDECIDED;
176 visibleMergeDecision = UNDECIDED;
177 setChanged();
178 notifyObservers();
179 fireCompletelyResolved();
180 }
181
182
183 /**
184 * replies the coordinates of my {@see OsmPrimitive}. null, if my primitive hasn't
185 * coordinates (i.e. because it is a {@see Way}).
186 *
187 * @return the coordinates of my {@see OsmPrimitive}. null, if my primitive hasn't
188 * coordinates (i.e. because it is a {@see Way}).
189 */
190 public LatLon getMyCoords() {
191 return myCoords;
192 }
193
194 /**
195 * replies the coordinates of their {@see OsmPrimitive}. null, if their primitive hasn't
196 * coordinates (i.e. because it is a {@see Way}).
197 *
198 * @return the coordinates of my {@see OsmPrimitive}. null, if my primitive hasn't
199 * coordinates (i.e. because it is a {@see Way}).
200 */
201 public LatLon getTheirCoords() {
202 return theirCoords;
203 }
204
205 /**
206 * replies the coordinates of the merged {@see OsmPrimitive}. null, if the current primitives
207 * have no coordinates or if the conflict is yet {@see MergeDecisionType#UNDECIDED}
208 *
209 * @return the coordinates of the merged {@see OsmPrimitive}. null, if the current primitives
210 * have no coordinates or if the conflict is yet {@see MergeDecisionType#UNDECIDED}
211 */
212 public LatLon getMergedCoords() {
213 switch(coordMergeDecision) {
214 case KEEP_MINE: return myCoords;
215 case KEEP_THEIR: return theirCoords;
216 case UNDECIDED: return null;
217 }
218 // should not happen
219 return null;
220 }
221
222 /**
223 * decides a conflict between my and their coordinates
224 *
225 * @param decision the decision
226 */
227 public void decideCoordsConflict(MergeDecisionType decision) {
228 coordMergeDecision = decision;
229 setChanged();
230 notifyObservers();
231 fireCompletelyResolved();
232 }
233
234 /**
235 * replies my deleted state,
236 * @return
237 */
238 public Boolean getMyDeletedState() {
239 return myDeletedState;
240 }
241
242 public Boolean getTheirDeletedState() {
243 return theirDeletedState;
244 }
245
246 public Boolean getMergedDeletedState() {
247 switch(deletedMergeDecision) {
248 case KEEP_MINE: return myDeletedState;
249 case KEEP_THEIR: return theirDeletedState;
250 case UNDECIDED: return null;
251 }
252 // should not happen
253 return null;
254 }
255
256
257 /**
258 * replies my visible state,
259 * @return my visible state
260 */
261 public Boolean getMyVisibleState() {
262 return myVisibleState;
263 }
264
265 /**
266 * replies their visible state,
267 * @return their visible state
268 */
269 public Boolean getTheirVisibleState() {
270 return theirVisibleState;
271 }
272
273 /**
274 * replies the merged visible state; null, if the merge decision is
275 * {@see MergeDecisionType#UNDECIDED}.
276 *
277 * @return the merged visible state
278 */
279 public Boolean getMergedVisibleState() {
280 switch(visibleMergeDecision) {
281 case KEEP_MINE: return myVisibleState;
282 case KEEP_THEIR: return theirVisibleState;
283 case UNDECIDED: return null;
284 }
285 // should not happen
286 return null;
287 }
288
289 /**
290 * decides the conflict between two deleted states
291 * @param decision the decision (must not be null)
292 *
293 * @throws IllegalArgumentException thrown, if decision is null
294 */
295 public void decideDeletedStateConflict(MergeDecisionType decision) throws IllegalArgumentException{
296 if (decision == null)
297 throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "decision"));
298 this.deletedMergeDecision = decision;
299 setChanged();
300 notifyObservers();
301 fireCompletelyResolved();
302 }
303
304 /**
305 * decides the conflict between two visible states
306 * @param decision the decision (must not be null)
307 *
308 * @throws IllegalArgumentException thrown, if decision is null
309 */
310 public void decideVisibleStateConflict(MergeDecisionType decision) throws IllegalArgumentException {
311 if (decision == null)
312 throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "decision"));
313 this.visibleMergeDecision = decision;
314 setChanged();
315 notifyObservers();
316 fireCompletelyResolved();
317 }
318
319 /**
320 * replies true if my and their primitive have a conflict between
321 * their coordinate values
322 *
323 * @return true if my and their primitive have a conflict between
324 * their coordinate values; false otherwise
325 */
326 public boolean hasCoordConflict() {
327 if (myCoords == null && theirCoords != null) return true;
328 if (myCoords != null && theirCoords == null) return true;
329 if (myCoords == null && theirCoords == null) return false;
330 return !myCoords.equals(theirCoords);
331 }
332
333 /**
334 * replies true if my and their primitive have a conflict between
335 * their deleted states
336 *
337 * @return true if my and their primitive have a conflict between
338 * their deleted states
339 */
340 public boolean hasDeletedStateConflict() {
341 return myDeletedState != theirDeletedState;
342 }
343
344 /**
345 * replies true if my and their primitive have a conflict between
346 * their visible states
347 *
348 * @return true if my and their primitive have a conflict between
349 * their visible states
350 */
351 public boolean hasVisibleStateConflict() {
352 return myVisibleState != theirVisibleState;
353 }
354
355 /**
356 * replies true if all conflict in this model are resolved
357 *
358 * @return true if all conflict in this model are resolved; false otherwise
359 */
360 public boolean isResolvedCompletely() {
361 boolean ret = true;
362 if (hasCoordConflict()) {
363 ret = ret && ! coordMergeDecision.equals(UNDECIDED);
364 }
365 if (hasDeletedStateConflict()) {
366 ret = ret && ! deletedMergeDecision.equals(UNDECIDED);
367 }
368 if (hasVisibleStateConflict()) {
369 ret = ret && ! visibleMergeDecision.equals(UNDECIDED);
370 }
371 return ret;
372 }
373
374 /**
375 * builds the command(s) to apply the conflict resolutions to my primitive
376 *
377 * @param my my primitive
378 * @param their their primitive
379 * @return the list of commands
380 */
381 public List<Command> buildResolveCommand(OsmPrimitive my, OsmPrimitive their) throws OperationCancelledException{
382 ArrayList<Command> cmds = new ArrayList<Command>();
383 if (hasVisibleStateConflict() && isDecidedVisibleState()) {
384 if (isVisibleStateDecision(MergeDecisionType.KEEP_MINE)) {
385 try {
386 UndeletePrimitivesCommand cmd = createUndeletePrimitiveCommand(my);
387 if (cmd == null)
388 throw new OperationCancelledException();
389 cmds.add(cmd);
390 } catch(OsmTransferException e) {
391 handleExceptionWhileBuildingCommand(e);
392 throw new OperationCancelledException(e);
393 }
394 } else if (isVisibleStateDecision(MergeDecisionType.KEEP_THEIR)) {
395 cmds.add(new PurgePrimitivesCommand(my));
396 }
397 }
398 if (hasCoordConflict() && isDecidedCoord()) {
399 cmds.add(new CoordinateConflictResolveCommand((Node)my, (Node)their, coordMergeDecision));
400 }
401 if (hasDeletedStateConflict() && isDecidedDeletedState()) {
402 cmds.add(new DeletedStateConflictResolveCommand(my, their, deletedMergeDecision));
403 }
404 return cmds;
405 }
406
407 public OsmPrimitive getMyPrimitive() {
408 return my;
409 }
410
411 /**
412 *
413 * @param id
414 */
415 protected void handleExceptionWhileBuildingCommand(Exception e) {
416 e.printStackTrace();
417 String msg = e.getMessage() != null ? e.getMessage() : e.toString();
418 msg = msg.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
419 JOptionPane.showMessageDialog(
420 Main.parent,
421 tr("<html>An error occurred while communicating with the server<br>"
422 + "Details: {0}</html>",
423 msg
424 ),
425 tr("Communication with server failed"),
426 JOptionPane.ERROR_MESSAGE
427 );
428 }
429
430 /**
431 * User has decided to keep his local version of a primitive which had been deleted
432 * on the server
433 *
434 * @param id the primitive id
435 */
436 protected UndeletePrimitivesCommand createUndeletePrimitiveCommand(OsmPrimitive my) throws OsmTransferException {
437 if (my instanceof Node)
438 return createUndeleteNodeCommand((Node)my);
439 else if (my instanceof Way)
440 return createUndeleteWayCommand((Way)my);
441 else if (my instanceof Relation)
442 return createUndeleteRelationCommand((Relation)my);
443 return null;
444 }
445 /**
446 * Undelete a node which is already deleted on the server. The API
447 * doesn't offer a call for "undeleting" a node. We therefore create
448 * a clone of the node which we flag as new. On the next upload the
449 * server will assign the node a new id.
450 *
451 * @param node the node to undelete
452 */
453 protected UndeletePrimitivesCommand createUndeleteNodeCommand(Node node) {
454 return new UndeletePrimitivesCommand(node);
455 }
456
457 /**
458 * displays a confirmation message. The user has to confirm that additional dependent
459 * nodes should be undeleted too.
460 *
461 * @param way the way
462 * @param dependent a list of dependent nodes which have to be undelete too
463 * @return true, if the user confirms; false, otherwise
464 */
465 protected boolean confirmUndeleteDependentPrimitives(Way way, ArrayList<OsmPrimitive> dependent) {
466 String [] options = {
467 tr("Yes, undelete them too"),
468 tr("No, cancel operation")
469 };
470 int ret = JOptionPane.showOptionDialog(
471 Main.parent,
472 tr("<html>There are {0} additional nodes used by way {1}<br>"
473 + "which are deleted on the server.<br>"
474 + "<br>"
475 + "Do you want to undelete these nodes too?</html>",
476 Long.toString(dependent.size()), Long.toString(way.id)),
477 tr("Undelete additional nodes?"),
478 JOptionPane.YES_NO_OPTION,
479 JOptionPane.QUESTION_MESSAGE,
480 null,
481 options,
482 options[0]
483 );
484
485 switch(ret) {
486 case JOptionPane.CLOSED_OPTION: return false;
487 case JOptionPane.YES_OPTION: return true;
488 case JOptionPane.NO_OPTION: return false;
489 }
490 return false;
491
492 }
493
494 protected boolean confirmUndeleteDependentPrimitives(Relation r, ArrayList<OsmPrimitive> dependent) {
495 String [] options = {
496 tr("Yes, undelete them too"),
497 tr("No, cancel operation")
498 };
499 int ret = JOptionPane.showOptionDialog(
500 Main.parent,
501 tr("<html>There are {0} additional primitives referred to by relation {1}<br>"
502 + "which are deleted on the server.<br>"
503 + "<br>"
504 + "Do you want to undelete them too?</html>",
505 Long.toString(dependent.size()), Long.toString(r.id)),
506 tr("Undelete dependent primitives?"),
507 JOptionPane.YES_NO_OPTION,
508 JOptionPane.QUESTION_MESSAGE,
509 null,
510 options,
511 options[0]
512 );
513
514 switch(ret) {
515 case JOptionPane.CLOSED_OPTION: return false;
516 case JOptionPane.YES_OPTION: return true;
517 case JOptionPane.NO_OPTION: return false;
518 }
519 return false;
520
521 }
522
523 /**
524 * Creates the undelete command for a way which is already deleted on the server.
525 *
526 * This method also checks whether there are additional nodes referred to by
527 * this way which are deleted on the server too.
528 *
529 * @param way the way to undelete
530 * @return the undelete command
531 * @see #createUndeleteNodeCommand(Node)
532 */
533 protected UndeletePrimitivesCommand createUndeleteWayCommand(final Way way) throws OsmTransferException {
534
535 HashMap<Long,OsmPrimitive> candidates = new HashMap<Long,OsmPrimitive>();
536 for (Node n : way.nodes) {
537 if (n.id > 0 && ! candidates.values().contains(n)) {
538 candidates.put(n.id, n);
539 }
540 }
541 MultiFetchServerObjectReader reader = new MultiFetchServerObjectReader();
542 reader.append(candidates.values());
543 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
544
545 ArrayList<OsmPrimitive> toDelete = new ArrayList<OsmPrimitive>();
546 for (OsmPrimitive their : ds.allPrimitives()) {
547 if (candidates.keySet().contains(their.id) && ! their.visible) {
548 toDelete.add(candidates.get(their.id));
549 }
550 }
551 if (!toDelete.isEmpty()) {
552 if (! confirmUndeleteDependentPrimitives(way, toDelete))
553 // FIXME: throw exception ?
554 return null;
555 }
556 toDelete.add(way);
557 return new UndeletePrimitivesCommand(toDelete);
558 }
559
560 /**
561 * Creates an undelete command for a relation which is already deleted on the server.
562 *
563 * This method checks whether there are additional primitives referred to by
564 * this relation which are already deleted on the server.
565 *
566 * @param r the relation
567 * @return the undelete command
568 * @see #createUndeleteNodeCommand(Node)
569 */
570 protected UndeletePrimitivesCommand createUndeleteRelationCommand(final Relation r) throws OsmTransferException {
571
572 HashMap<Long,OsmPrimitive> candidates = new HashMap<Long, OsmPrimitive>();
573 for (RelationMember m : r.members) {
574 if (m.member.id > 0 && !candidates.values().contains(m.member)) {
575 candidates.put(m.member.id,m.member);
576 }
577 }
578
579 MultiFetchServerObjectReader reader = new MultiFetchServerObjectReader();
580 reader.append(candidates.values());
581 DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
582
583 ArrayList<OsmPrimitive> toDelete = new ArrayList<OsmPrimitive>();
584 for (OsmPrimitive their : ds.allPrimitives()) {
585 if (candidates.keySet().contains(their.id) && ! their.visible) {
586 toDelete.add(candidates.get(their.id));
587 }
588 }
589 if (!toDelete.isEmpty()) {
590 if (! confirmUndeleteDependentPrimitives(r, toDelete))
591 // FIXME: throw exception ?
592 return null;
593 }
594 toDelete.add(r);
595 return new UndeletePrimitivesCommand(toDelete);
596 }
597
598}
Note: See TracBrowser for help on using the repository browser.