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

Last change on this file since 13908 was 13497, checked in by Don-vip, 6 years ago

fix #16051, see #8039, see #10456 - more fixes for download/upload policies and locked status (merge of layers)

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