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

Last change on this file since 2181 was 2181, checked in by stoecker, 15 years ago

lots of i18n fixes

File size: 21.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;
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.pair.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.isDeleted();
169 theirDeletedState = their.isDeleted();
170
171 myVisibleState = my.isVisible();
172 theirVisibleState = their.isVisible();
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.getId())),
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.getId())),
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.getNodes()) {
537 if (n.getId() > 0 && ! candidates.values().contains(n)) {
538 candidates.put(n.getId(), 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.getId()) && ! their.isVisible()) {
548 toDelete.add(candidates.get(their.getId()));
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.getMembers()) {
574 if (m.getMember().getId() > 0 && !candidates.values().contains(m.getMember())) {
575 candidates.put(m.getMember().getId(), m.getMember());
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.getId()) && ! their.isVisible()) {
586 toDelete.add(candidates.get(their.getId()));
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.