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

Last change on this file since 2563 was 2563, checked in by Gubaer, 14 years ago

fixed #3400: relation editor: improvement to highlight an element
fixed #3873: Feature request: download selected elements in relation editor
New: Dbl-Click in member table to set the map selection to this member
New: Ctrl-Dbl-Clik in member table to add the member to the the map selection
New: Download selected incomplete members only

  • Property svn:eol-style set to native
File size: 17.3 KB
Line 
1package org.openstreetmap.josm.data.osm;
2
3import static org.openstreetmap.josm.tools.I18n.tr;
4
5import java.util.Collection;
6import java.util.HashMap;
7import java.util.HashSet;
8import java.util.LinkedList;
9import java.util.List;
10import java.util.Map;
11import java.util.Set;
12import java.util.logging.Logger;
13
14import 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 */
21public 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}
Note: See TracBrowser for help on using the repository browser.