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

Last change on this file since 2940 was 2940, checked in by mjulius, 14 years ago

display list of referrers in ConflictResolutionDialog

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