source: osm/applications/editors/josm/plugins/utilsplugin2/src/utilsplugin2/dumbutils/ReplaceGeometryAction.java@ 27645

Last change on this file since 27645 was 27645, checked in by joshdoe, 12 years ago

utilsplugin2: Replace geometry of node with multipolygon (fixes #7313)

File size: 16.3 KB
Line 
1package utilsplugin2.dumbutils;
2
3import java.awt.event.ActionEvent;
4import java.awt.event.KeyEvent;
5import java.awt.geom.Area;
6import java.awt.geom.Point2D;
7import java.util.*;
8import javax.swing.JOptionPane;
9import org.openstreetmap.josm.Main;
10import org.openstreetmap.josm.actions.JosmAction;
11import org.openstreetmap.josm.command.*;
12import org.openstreetmap.josm.data.osm.Node;
13import org.openstreetmap.josm.data.osm.OsmPrimitive;
14import org.openstreetmap.josm.data.osm.Relation;
15import org.openstreetmap.josm.data.osm.RelationMember;
16import org.openstreetmap.josm.data.osm.RelationToChildReference;
17import org.openstreetmap.josm.data.osm.TagCollection;
18import org.openstreetmap.josm.data.osm.Way;
19import org.openstreetmap.josm.gui.DefaultNameFormatter;
20import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
21import org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil;
22import static org.openstreetmap.josm.tools.I18n.tr;
23import org.openstreetmap.josm.tools.Shortcut;
24
25/**
26 * Replaces already existing object (id>0) with a new object (id<0).
27 *
28 * @author Zverik
29 */
30public class ReplaceGeometryAction extends JosmAction {
31 private static final String TITLE = tr("Replace Geometry");
32
33 public ReplaceGeometryAction() {
34 super(TITLE, "dumbutils/replacegeometry", tr("Replace geometry of selected object with a new one"),
35 Shortcut.registerShortcut("tools:replacegeometry", tr("Tool: {0}", tr("Replace Geometry")), KeyEvent.VK_G, Shortcut.GROUP_HOTKEY, Shortcut.SHIFT_DEFAULT)
36 , true);
37 }
38
39 @Override
40 public void actionPerformed(ActionEvent e) {
41 if (getCurrentDataSet() == null) {
42 return;
43 }
44
45 // There must be two ways selected: one with id > 0 and one new.
46 List<OsmPrimitive> selection = new ArrayList<OsmPrimitive>(getCurrentDataSet().getSelected());
47 if (selection.size() != 2) {
48 JOptionPane.showMessageDialog(Main.parent,
49 tr("This tool replaces geometry of one object with another, and so requires exactly two objects to be selected."),
50 TITLE, JOptionPane.INFORMATION_MESSAGE);
51 return;
52 }
53
54 OsmPrimitive firstObject = selection.get(0);
55 OsmPrimitive secondObject = selection.get(1);
56
57 if (firstObject instanceof Way && secondObject instanceof Way) {
58 replaceWayWithWay(Arrays.asList((Way) firstObject, (Way) secondObject));
59 } else if (firstObject instanceof Node && secondObject instanceof Node) {
60 JOptionPane.showMessageDialog(Main.parent,
61 tr("To replace a node with a node, use the node merge tool."),
62 TITLE, JOptionPane.INFORMATION_MESSAGE);
63 return;
64 } else if (firstObject instanceof Node) {
65 replaceNode((Node) firstObject, secondObject);
66 } else if (secondObject instanceof Node) {
67 replaceNode((Node) secondObject, firstObject);
68 } else {
69 JOptionPane.showMessageDialog(Main.parent,
70 tr("This tool can only replace a node with a way, a node with a multipolygon, or a way with a way."),
71 TITLE, JOptionPane.INFORMATION_MESSAGE);
72 return;
73 }
74 }
75
76 /**
77 * Replace or upgrade a node to a way or multipolygon
78 *
79 * @param node
80 * @param target
81 */
82 public void replaceNode(Node node, OsmPrimitive target) {
83 if (!node.getReferrers().isEmpty()) {
84 JOptionPane.showMessageDialog(Main.parent, tr("Node belongs to way(s) or relation(s), cannot replace."),
85 TITLE, JOptionPane.INFORMATION_MESSAGE);
86 return;
87 }
88
89 if (target instanceof Relation && !((Relation) target).isMultipolygon()) {
90 JOptionPane.showMessageDialog(Main.parent, tr("Relation is not a multipolygon, cannot be used as a replacement."),
91 TITLE, JOptionPane.INFORMATION_MESSAGE);
92 return;
93 }
94
95 Node nodeToReplace = null;
96 // see if we need to replace a node in the replacement way to preserve connection in history
97 if (!node.isNew()) {
98 // Prepare a list of nodes that are not important
99 Collection<Node> nodePool = new HashSet<Node>();
100 if (target instanceof Way) {
101 nodePool.addAll(getUnimportantNodes((Way) target));
102 } else if (target instanceof Relation) {
103 for (RelationMember member : ((Relation) target).getMembers()) {
104 if ((member.getRole().equals("outer") || member.getRole().equals("inner"))
105 && member.isWay()) {
106 // TODO: could consider more nodes, such as nodes that are members of other ways,
107 // just need to replace occurences in all referrers
108 nodePool.addAll(getUnimportantNodes(member.getWay()));
109 }
110 }
111 } else {
112 assert false;
113 }
114 nodeToReplace = findNearestNode(node, nodePool);
115 }
116
117 List<Command> commands = new ArrayList<Command>();
118 AbstractMap<String, String> nodeTags = (AbstractMap<String, String>) node.getKeys();
119
120 // merge tags
121 Collection<Command> tagResolutionCommands = getTagConflictResolutionCommands(node, target);
122 if (tagResolutionCommands == null) {
123 // user canceled tag merge dialog
124 return;
125 }
126 commands.addAll(tagResolutionCommands);
127
128 // replace sacrificial node in way with node that is being upgraded
129 if (nodeToReplace != null) {
130 // node should only have one parent, a way
131 Way parentWay = (Way) nodeToReplace.getReferrers().get(0);
132 List<Node> wayNodes = parentWay.getNodes();
133 int idx = wayNodes.indexOf(nodeToReplace);
134 wayNodes.set(idx, node);
135 if (idx == 0 && parentWay.isClosed()) {
136 // node is at start/end of way
137 wayNodes.set(wayNodes.size() - 1, node);
138 }
139 commands.add(new ChangeNodesCommand(parentWay, wayNodes));
140 commands.add(new MoveCommand(node, nodeToReplace.getCoor()));
141 commands.add(new DeleteCommand(nodeToReplace));
142
143 // delete tags from node
144 if (!nodeTags.isEmpty()) {
145 for (String key : nodeTags.keySet()) {
146 commands.add(new ChangePropertyCommand(node, key, null));
147 }
148
149 }
150 } else {
151 // no node to replace, so just delete the original node
152 commands.add(new DeleteCommand(node));
153 }
154
155 getCurrentDataSet().setSelected(target);
156
157 Main.main.undoRedo.add(new SequenceCommand(
158 tr("Replace geometry for node {0}", node.getDisplayName(DefaultNameFormatter.getInstance())),
159 commands));
160 }
161
162 public void replaceWayWithWay(List<Way> selection) {
163 // determine which way will be replaced and which will provide the geometry
164 boolean overrideNewCheck = false;
165 int idxNew = selection.get(0).isNew() ? 0 : 1;
166 if( selection.get(1-idxNew).isNew() ) {
167 // if both are new, select the one with all the DB nodes
168 boolean areNewNodes = false;
169 for (Node n : selection.get(0).getNodes()) {
170 if (n.isNew()) {
171 areNewNodes = true;
172 }
173 }
174 idxNew = areNewNodes ? 0 : 1;
175 overrideNewCheck = true;
176 for (Node n : selection.get(1 - idxNew).getNodes()) {
177 if (n.isNew()) {
178 overrideNewCheck = false;
179 }
180 }
181 }
182 Way geometry = selection.get(idxNew);
183 Way way = selection.get(1 - idxNew);
184
185 if( !overrideNewCheck && (way.isNew() || !geometry.isNew()) ) {
186 JOptionPane.showMessageDialog(Main.parent,
187 tr("Please select one way that exists in the database and one new way with correct geometry."),
188 TITLE, JOptionPane.WARNING_MESSAGE);
189 return;
190 }
191
192 Area a = getCurrentDataSet().getDataSourceArea();
193 if (!isInArea(way, a) || !isInArea(geometry, a)) {
194 JOptionPane.showMessageDialog(Main.parent,
195 tr("The ways must be entirely within the downloaded area."),
196 TITLE, JOptionPane.WARNING_MESSAGE);
197 return;
198 }
199
200 if (hasImportantNode(way)) {
201 JOptionPane.showMessageDialog(Main.parent,
202 tr("The way to be replaced cannot have any nodes with properties or relation memberships."),
203 TITLE, JOptionPane.WARNING_MESSAGE);
204 return;
205 }
206
207 List<Command> commands = new ArrayList<Command>();
208
209 // merge tags
210 Collection<Command> tagResolutionCommands = getTagConflictResolutionCommands(geometry, way);
211 if (tagResolutionCommands == null) {
212 // user canceled tag merge dialog
213 return;
214 }
215 commands.addAll(tagResolutionCommands);
216
217 // Prepare a list of nodes that are not used anywhere except in the way
218 Collection<Node> nodePool = getUnimportantNodes(way);
219
220 // And the same for geometry, list nodes that can be freely deleted
221 Set<Node> geometryPool = new HashSet<Node>();
222 for( Node node : geometry.getNodes() ) {
223 List<OsmPrimitive> referrers = node.getReferrers();
224 if( node.isNew() && !node.isDeleted() && referrers.size() == 1
225 && referrers.get(0).equals(geometry) && !way.containsNode(node)
226 && !hasInterestingKey(node))
227 geometryPool.add(node);
228 }
229
230 // Find new nodes that are closest to the old ones, remove matching old ones from the pool
231 Map<Node, Node> nodeAssoc = new HashMap<Node, Node>();
232 for( Node n : geometryPool ) {
233 Node nearest = findNearestNode(n, nodePool);
234 if( nearest != null ) {
235 nodeAssoc.put(n, nearest);
236 nodePool.remove(nearest);
237 }
238 }
239
240 // Now that we have replacement list, move all unused new nodes to nodePool (and delete them afterwards)
241 for( Node n : geometryPool )
242 if( nodeAssoc.containsKey(n) )
243 nodePool.add(n);
244
245 // And prepare a list of nodes with all the replacements
246 List<Node> geometryNodes = geometry.getNodes();
247 for( int i = 0; i < geometryNodes.size(); i++ )
248 if( nodeAssoc.containsKey(geometryNodes.get(i)) )
249 geometryNodes.set(i, nodeAssoc.get(geometryNodes.get(i)));
250
251 // Now do the replacement
252 commands.add(new ChangeNodesCommand(way, geometryNodes));
253
254 // Move old nodes to new positions
255 for( Node node : nodeAssoc.keySet() )
256 commands.add(new MoveCommand(nodeAssoc.get(node), node.getCoor()));
257
258 // Remove geometry way from selection
259 getCurrentDataSet().clearSelection(geometry);
260
261 // And delete old geometry way
262 commands.add(new DeleteCommand(geometry));
263
264 // Delete nodes that are not used anymore
265 if( !nodePool.isEmpty() )
266 commands.add(new DeleteCommand(nodePool));
267
268 // Two items in undo stack: change original way and delete geometry way
269 Main.main.undoRedo.add(new SequenceCommand(
270 tr("Replace geometry for way {0}", way.getDisplayName(DefaultNameFormatter.getInstance())),
271 commands));
272 }
273
274 /**
275 * Create a list of nodes that are not used anywhere except in the way.
276 *
277 * @param way
278 * @return
279 */
280 protected Collection<Node> getUnimportantNodes(Way way) {
281 Set<Node> nodePool = new HashSet<Node>();
282 for (Node n : way.getNodes()) {
283 List<OsmPrimitive> referrers = n.getReferrers();
284 if (!n.isDeleted() && referrers.size() == 1 && referrers.get(0).equals(way)
285 && !hasInterestingKey(n)) {
286 nodePool.add(n);
287 }
288 }
289 return nodePool;
290 }
291
292 /**
293 * Checks if a way has at least one important node (e.g. interesting tag,
294 * role membership), and thus cannot be safely modified.
295 *
296 * @param way
297 * @return
298 */
299 protected boolean hasImportantNode(Way way) {
300 for (Node n : way.getNodes()) {
301 //TODO: if way is connected to other ways, warn or disallow?
302 for (OsmPrimitive o : n.getReferrers()) {
303 if (o instanceof Relation) {
304 return true;
305 }
306 }
307 if (hasInterestingKey(n)) {
308 return true;
309 }
310 }
311 return false;
312 }
313
314 protected boolean hasInterestingKey(OsmPrimitive object) {
315 for (String key : object.getKeys().keySet()) {
316 if (!OsmPrimitive.isUninterestingKey(key)) {
317 return true;
318 }
319 }
320 return false;
321 }
322
323 protected static boolean isInArea(Node node, Area area) {
324 if (node.isNewOrUndeleted() || area == null || area.contains(node.getCoor())) {
325 return true;
326 }
327 return false;
328 }
329
330 protected static boolean isInArea(Way way, Area area) {
331 if (area == null) {
332 return true;
333 }
334
335 for (Node n : way.getNodes()) {
336 if (!isInArea(n, area)) {
337 return false;
338 }
339 }
340
341 return true;
342 }
343
344 /**
345 * Merge tags from source to target object, showing resolution dialog if
346 * needed.
347 *
348 * @param source
349 * @param target
350 * @return
351 */
352 public List<Command> getTagConflictResolutionCommands(OsmPrimitive source, OsmPrimitive target) {
353 Collection<OsmPrimitive> primitives = Arrays.asList(source, target);
354
355 Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(primitives);
356
357 // build the tag collection
358 TagCollection tags = TagCollection.unionOfAllPrimitives(primitives);
359 TagConflictResolutionUtil.combineTigerTags(tags);
360 TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(tags, primitives);
361 TagCollection tagsToEdit = new TagCollection(tags);
362 TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit);
363
364 // launch a conflict resolution dialog, if necessary
365 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
366 dialog.getTagConflictResolverModel().populate(tagsToEdit, tags.getKeysWithMultipleValues());
367 dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
368 dialog.setTargetPrimitive(target);
369 dialog.prepareDefaultDecisions();
370
371 // conflict resolution is necessary if there are conflicts in the merged tags
372 // or if at least one of the merged nodes is referred to by a relation
373 if (!tags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
374 dialog.setVisible(true);
375 if (dialog.isCanceled()) {
376 return null;
377 }
378 }
379 return dialog.buildResolutionCommands();
380 }
381
382
383 /**
384 * Find node from the collection which is nearest to <tt>node</tt>. Max distance is taken in consideration.
385 * @return null if there is no such node.
386 */
387 private Node findNearestNode( Node node, Collection<Node> nodes ) {
388 if( nodes.contains(node) )
389 return node;
390
391 Node nearest = null;
392 // TODO: use meters instead of degrees, but do it fast
393 double distance = Double.parseDouble(Main.pref.get("utilsplugin2.replace-geometry.max-distance", "1"));
394 Point2D coor = node.getCoor();
395
396 for( Node n : nodes ) {
397 double d = n.getCoor().distance(coor);
398 if( d < distance ) {
399 distance = d;
400 nearest = n;
401 }
402 }
403 return nearest;
404 }
405
406 @Override
407 protected void updateEnabledState() {
408 if( getCurrentDataSet() == null ) {
409 setEnabled(false);
410 } else
411 updateEnabledState(getCurrentDataSet().getSelected());
412 }
413
414 @Override
415 protected void updateEnabledState( Collection<? extends OsmPrimitive> selection ) {
416 setEnabled(selection != null && selection.size() >= 2 );
417 }
418}
419
Note: See TracBrowser for help on using the repository browser.