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

Last change on this file since 4077 was 3965, checked in by mjulius, 13 years ago

fix #6074 - Update failure
It is not an error if a primitive on the server is invisible while the local copy is not if the latter one is modified.
Catch DataIntegrityProblemException when merging.

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