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

Last change on this file since 3715 was 3426, checked in by stoecker, 14 years ago

see #5303 - some cleanup patches from matthew Bell - partially applied only

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