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