source: josm/trunk/src/org/openstreetmap/josm/data/osm/DataSetMerger.java@ 3033

Last change on this file since 3033 was 3033, checked in by jttt, 14 years ago

Fix #4572 after update data - way contains deleted member

  • Property svn:eol-style set to native
File size: 18.2 KB
Line 
1package org.openstreetmap.josm.data.osm;
2
3import static org.openstreetmap.josm.tools.I18n.tr;
4
5import java.util.ArrayList;
6import java.util.Collection;
7import java.util.HashMap;
8import java.util.HashSet;
9import java.util.LinkedList;
10import java.util.List;
11import java.util.Map;
12import java.util.Set;
13import java.util.logging.Logger;
14
15import org.openstreetmap.josm.data.conflict.ConflictCollection;
16import org.openstreetmap.josm.tools.CheckParameterUtil;
17
18/**
19 * A dataset merger which takes a target and a source dataset and merges the source data set
20 * onto the target dataset.
21 *
22 */
23public class DataSetMerger {
24 private static Logger logger = Logger.getLogger(DataSetMerger.class.getName());
25
26 /** the collection of conflicts created during merging */
27 private final ConflictCollection conflicts;
28
29 /** the target dataset for merging */
30 private final DataSet targetDataSet;
31 /** the source dataset where primitives are merged from */
32 private final DataSet sourceDataSet;
33
34 /**
35 * A map of all primitives that got replaced with other primitives.
36 * Key is the primitive id in their dataset, the value is the id in my dataset
37 */
38 private final Map<Long, Long> mergedMap;
39 /** a set of primitive ids for which we have to fix references (to nodes and
40 * to relation members) after the first phase of merging
41 */
42 private final Set<PrimitiveId> objectsWithChildrenToMerge;
43 private final Set<OsmPrimitive> deletedObjectsToUnlink;
44
45 /**
46 * constructor
47 *
48 * The visitor will merge <code>theirDataSet</code> onto <code>myDataSet</code>
49 *
50 * @param targetDataSet dataset with my primitives. Must not be null.
51 * @param sourceDataSet dataset with their primitives. Ignored, if null.
52 * @throws IllegalArgumentException thrown if myDataSet is null
53 */
54 public DataSetMerger(DataSet targetDataSet, DataSet sourceDataSet) throws IllegalArgumentException {
55 CheckParameterUtil.ensureParameterNotNull(targetDataSet, "targetDataSet");
56 this.targetDataSet = targetDataSet;
57 this.sourceDataSet = sourceDataSet;
58 conflicts = new ConflictCollection();
59 mergedMap = new HashMap<Long, Long>();
60 objectsWithChildrenToMerge = new HashSet<PrimitiveId>();
61 deletedObjectsToUnlink = new HashSet<OsmPrimitive>();
62 }
63
64 /**
65 * Merges a primitive <code>other</code> of type <P> onto my primitives.
66 *
67 * If other.id != 0 it tries to merge it with an corresponding primitive from
68 * my dataset with the same id. If this is not possible a conflict is remembered
69 * in {@see #conflicts}.
70 *
71 * If other.id == 0 it tries to find a primitive in my dataset with id == 0 which
72 * is semantically equal. If it finds one it merges its technical attributes onto
73 * my primitive.
74 *
75 * @param <P> the type of the other primitive
76 * @param source the other primitive
77 */
78 protected void mergePrimitive(OsmPrimitive source) {
79 if (!source.isNew() ) {
80 // try to merge onto a matching primitive with the same
81 // defined id
82 //
83 if (mergeById(source))
84 return;
85 //if (!source.isVisible())
86 // ignore it
87 // return;
88 } else {
89 // try to merge onto a primitive which has no id assigned
90 // yet but which is equal in its semantic attributes
91 //
92 Collection<? extends OsmPrimitive> candidates = null;
93 switch(source.getType()) {
94 case NODE: candidates = targetDataSet.getNodes(); break;
95 case WAY: candidates =targetDataSet.getWays(); break;
96 case RELATION: candidates = targetDataSet.getRelations(); break;
97 default: throw new AssertionError();
98 }
99 for (OsmPrimitive target : candidates) {
100 if (!target.isNew()) {
101 continue;
102 }
103 if (target.hasEqualSemanticAttributes(source)) {
104 mergedMap.put(source.getUniqueId(), target.getUniqueId());
105 if (target.isDeleted() != source.isDeleted()) {
106 // differences in deleted state have to be merged manually
107 //
108 conflicts.add(target, source);
109 } else {
110 // copy the technical attributes from other
111 // version
112 target.setVisible(source.isVisible());
113 target.setUser(source.getUser());
114 target.setTimestamp(source.getTimestamp());
115 target.setModified(source.isModified());
116 objectsWithChildrenToMerge.add(source.getPrimitiveId());
117 }
118 return;
119 }
120 }
121 }
122
123 // If we get here we didn't find a suitable primitive in
124 // the target dataset. Create a clone and add it to the target dataset.
125 //
126 OsmPrimitive target = null;
127 switch(source.getType()) {
128 case NODE: target = source.isNew() ? new Node() : new Node(source.getId()); break;
129 case WAY: target = source.isNew() ? new Way() : new Way(source.getId()); break;
130 case RELATION: target = source.isNew() ? new Relation() : new Relation(source.getId()); break;
131 default: throw new AssertionError();
132 }
133 target.mergeFrom(source);
134 targetDataSet.addPrimitive(target);
135 mergedMap.put(source.getUniqueId(), target.getUniqueId());
136 objectsWithChildrenToMerge.add(source.getPrimitiveId());
137 }
138
139 protected OsmPrimitive getMergeTarget(OsmPrimitive mergeSource) throws IllegalStateException{
140 Long targetId = mergedMap.get(mergeSource.getUniqueId());
141 if (targetId == null)
142 return null;
143 return targetDataSet.getPrimitiveById(targetId, mergeSource.getType());
144 }
145
146 protected void fixIncomplete(Way other) {
147 Way myWay = (Way)getMergeTarget(other);
148 if (myWay == null)
149 throw new RuntimeException(tr("Missing merge target for way with id {0}", other.getUniqueId()));
150 }
151
152 /**
153 * A way in the target dataset might be incomplete because at least one of its nodes is incomplete.
154 * The nodes might have become complete because a complete node was merged into in the
155 * merge operation.
156 *
157 * This method loops over all parent ways of such nodes and turns them into complete ways
158 * if necessary.
159 *
160 * @param other
161 */
162 protected void fixIncompleteParentWays(Node other) {
163 Node myNode = (Node)getMergeTarget(other);
164 if (myNode == null)
165 throw new RuntimeException(tr("Missing merge target for node with id {0}", other.getUniqueId()));
166 if (myNode.isIncomplete() || myNode.isDeleted() || !myNode.isVisible()) return;
167 }
168
169 /**
170 * Postprocess the dataset and fix all merged references to point to the actual
171 * data.
172 */
173 public void fixReferences() {
174 for (Way w : sourceDataSet.getWays()) {
175 if (!conflicts.hasConflictForTheir(w) && objectsWithChildrenToMerge.contains(w.getPrimitiveId())) {
176 mergeNodeList(w);
177 fixIncomplete(w);
178 }
179 }
180 for (Relation r : sourceDataSet.getRelations()) {
181 if (!conflicts.hasConflictForTheir(r) && objectsWithChildrenToMerge.contains(r.getPrimitiveId())) {
182 mergeRelationMembers(r);
183 }
184 }
185 for (OsmPrimitive source: deletedObjectsToUnlink) {
186 OsmPrimitive target = getMergeTarget(source);
187 if (target == null)
188 throw new RuntimeException(tr("Missing merge target for object with id {0}", source.getUniqueId()));
189 targetDataSet.unlinkReferencesToPrimitive(target);
190 }
191 // objectsWithChildrenToMerge also includes complete nodes which have
192 // been merged into their incomplete equivalents.
193 //
194 for (PrimitiveId id: objectsWithChildrenToMerge) {
195 if (!id.getType().equals(OsmPrimitiveType.NODE)) {
196 continue;
197 }
198 Node n = (Node)sourceDataSet.getPrimitiveById(id);
199 if (!conflicts.hasConflictForTheir(n)) {
200 fixIncompleteParentWays(n);
201 }
202 }
203
204 }
205
206 /**
207 * Merges the node list of a source way onto its target way.
208 *
209 * @param source the source way
210 * @throws IllegalStateException thrown if no target way can be found for the source way
211 * @throws IllegalStateException thrown if there isn't a target node for one of the nodes in the source way
212 *
213 */
214 private void mergeNodeList(Way source) throws IllegalStateException {
215 Way target = (Way)getMergeTarget(source);
216 if (target == null)
217 throw new IllegalStateException(tr("Missing merge target for way with id {0}", source.getUniqueId()));
218
219 List<Node> newNodes = new ArrayList<Node>(source.getNodesCount());
220 for (Node sourceNode : source.getNodes()) {
221 Node targetNode = (Node)getMergeTarget(sourceNode);
222 if (targetNode != null) {
223 if (targetNode.isVisible()) {
224 newNodes.add(targetNode);
225 if (targetNode.isDeleted() && !conflicts.hasConflictForMy(targetNode)) {
226 conflicts.add(targetNode, sourceNode);
227 }
228 } else {
229 target.setModified(true);
230 }
231 } else
232 throw new IllegalStateException(tr("Missing merge target for node with id {0}", sourceNode.getUniqueId()));
233 }
234 target.setNodes(newNodes);
235 }
236
237 /**
238 * Merges the relation members of a source relation onto the corresponding target relation.
239 * @param source the source relation
240 * @throws IllegalStateException thrown if there is no corresponding target relation
241 * @throws IllegalStateException thrown if there isn't a corresponding target object for one of the relation
242 * members in source
243 */
244 private void mergeRelationMembers(Relation source) throws IllegalStateException {
245 Relation target = (Relation) getMergeTarget(source);
246 if (target == null)
247 throw new IllegalStateException(tr("Missing merge target for relation with id {0}", source.getUniqueId()));
248 LinkedList<RelationMember> newMembers = new LinkedList<RelationMember>();
249 for (RelationMember sourceMember : source.getMembers()) {
250 OsmPrimitive targetMember = getMergeTarget(sourceMember.getMember());
251 if (targetMember == null)
252 throw new IllegalStateException(tr("Missing merge target of type {0} with id {1}", sourceMember.getType(), sourceMember.getUniqueId()));
253 if (targetMember.isVisible()) {
254 RelationMember newMember = new RelationMember(sourceMember.getRole(), targetMember);
255 newMembers.add(newMember);
256 if (targetMember.isDeleted() && !conflicts.hasConflictForMy(targetMember)) {
257 conflicts.add(targetMember, sourceMember.getMember());
258 }
259 } else {
260 target.setModified(true);
261 }
262 }
263 target.setMembers(newMembers);
264 }
265
266 /**
267 * Tries to merge a primitive <code>source</code> into an existing primitive with the same id.
268 *
269 * @param source the source primitive which is to be merged into a target primitive
270 * @return true, if this method was able to merge <code>source</code> into a target object; false, otherwise
271 */
272 private boolean mergeById(OsmPrimitive source) {
273 OsmPrimitive target = targetDataSet.getPrimitiveById(source.getId(), source.getType());
274 // merge other into an existing primitive with the same id, if possible
275 //
276 if (target == null)
277 return false;
278 // found a corresponding target, remember it
279 mergedMap.put(source.getUniqueId(), target.getUniqueId());
280
281 if (target.getVersion() > source.getVersion())
282 // target.version > source.version => keep target version
283 return true;
284 if (! target.isVisible() && source.isVisible()) {
285 // should not happen
286 // FIXME: this message does not make sense, source version can not be lower than
287 // target version at this point
288 logger.warning(tr("Target object with id {0} and version {1} is visible although "
289 + "source object with lower version {2} is not visible. "
290 + "Cannot deal with this inconsistency. Keeping target object. ",
291 Long.toString(target.getId()),Long.toString(target.getVersion()), Long.toString(source.getVersion())
292 ));
293 } else if (target.isVisible() && ! source.isVisible()) {
294 // this is always a conflict because the user has to decide whether
295 // he wants to create a clone of its target primitive or whether he
296 // wants to purge the target from the local dataset. He can't keep it unchanged
297 // because it was deleted on the server.
298 //
299 conflicts.add(target,source);
300 } else if (target.isIncomplete() && !source.isIncomplete()) {
301 // target is incomplete, source completes it
302 // => merge source into target
303 //
304 target.mergeFrom(source);
305 objectsWithChildrenToMerge.add(source.getPrimitiveId());
306 } else if (!target.isIncomplete() && source.isIncomplete()) {
307 // target is complete and source is incomplete
308 // => keep target, it has more information already
309 //
310 } else if (target.isIncomplete() && source.isIncomplete()) {
311 // target and source are incomplete. Doesn't matter which one to
312 // take. We take target.
313 //
314 } else if (target.isDeleted() && ! source.isDeleted() && target.getVersion() == source.getVersion()) {
315 // same version, but target is deleted. Assume target takes precedence
316 // otherwise too many conflicts when refreshing from the server
317 // but, if source has a referrer that is not in the target dataset there is a conflict
318 // If target dataset refers to the deleted primitive, conflict will be added in fixReferences method
319 for (OsmPrimitive referrer: source.getReferrers()) {
320 if (targetDataSet.getPrimitiveById(referrer.getPrimitiveId()) == null) {
321 conflicts.add(target, source);
322 break;
323 }
324 }
325 } else if (target.isDeleted() != source.isDeleted()) {
326 // differences in deleted state have to be resolved manually. This can
327 // happen if one layer is merged onto another layer
328 //
329 conflicts.add(target,source);
330 } else if (! target.isModified() && source.isModified()) {
331 // target not modified. We can assume that source is the most recent version.
332 // clone it into target. But check first, whether source is deleted. if so,
333 // make sure that target is not referenced any more in myDataSet. If it is there
334 // is a conflict
335 if (source.isDeleted()) {
336 if (!target.getReferrers().isEmpty()) {
337 conflicts.add(target, source);
338 }
339 } else {
340 target.mergeFrom(source);
341 objectsWithChildrenToMerge.add(source.getPrimitiveId());
342 }
343 } else if (! target.isModified() && !source.isModified() && target.getVersion() == source.getVersion()) {
344 // both not modified. Merge nevertheless.
345 // This helps when updating "empty" relations, see #4295
346 target.mergeFrom(source);
347 objectsWithChildrenToMerge.add(source.getPrimitiveId());
348 } else if (! target.isModified() && !source.isModified() && target.getVersion() < source.getVersion()) {
349 // my not modified but other is newer. clone other onto mine.
350 //
351 target.mergeFrom(source);
352 objectsWithChildrenToMerge.add(source.getPrimitiveId());
353 } else if (target.isModified() && ! source.isModified() && target.getVersion() == source.getVersion()) {
354 // target is same as source but target is modified
355 // => keep target and reset modified flag if target and source are semantically equal
356 if (target.hasEqualSemanticAttributes(source)) {
357 target.setModified(false);
358 }
359 } else if (! target.hasEqualSemanticAttributes(source)) {
360 // target is modified and is not semantically equal with source. Can't automatically
361 // resolve the differences
362 // => create a conflict
363 conflicts.add(target,source);
364 } else {
365 // clone from other, but keep the modified flag. mergeFrom will mainly copy
366 // technical attributes like timestamp or user information. Semantic
367 // attributes should already be equal if we get here.
368 //
369 target.mergeFrom(source);
370 target.setModified(true);
371 objectsWithChildrenToMerge.add(source.getPrimitiveId());
372 }
373 return true;
374 }
375
376 /**
377 * Runs the merge operation. Successfully merged {@see OsmPrimitive}s are in
378 * {@see #getMyDataSet()}.
379 *
380 * See {@see #getConflicts()} for a map of conflicts after the merge operation.
381 */
382 public void merge() {
383 if (sourceDataSet == null)
384 return;
385 targetDataSet.beginUpdate();
386 try {
387 for (Node node: sourceDataSet.getNodes()) {
388 mergePrimitive(node);
389 }
390 for (Way way: sourceDataSet.getWays()) {
391 mergePrimitive(way);
392 }
393 for (Relation relation: sourceDataSet.getRelations()) {
394 mergePrimitive(relation);
395 }
396 fixReferences();
397 } finally {
398 targetDataSet.endUpdate();
399 }
400 }
401
402 /**
403 * replies my dataset
404 *
405 * @return
406 */
407 public DataSet getTargetDataSet() {
408 return targetDataSet;
409 }
410
411 /**
412 * replies the map of conflicts
413 *
414 * @return the map of conflicts
415 */
416 public ConflictCollection getConflicts() {
417 return conflicts;
418 }
419}
Note: See TracBrowser for help on using the repository browser.