source: josm/trunk/src/org/openstreetmap/josm/actions/CreateMultipolygonAction.java@ 7945

Last change on this file since 7945 was 7945, checked in by Don-vip, 9 years ago

see #10953 - fix crash and EDT violation

  • Property svn:eol-style set to native
File size: 17.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.List;
15import java.util.Map;
16import java.util.Map.Entry;
17import java.util.Set;
18import java.util.TreeSet;
19
20import javax.swing.JOptionPane;
21import javax.swing.SwingUtilities;
22
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.command.AddCommand;
25import org.openstreetmap.josm.command.ChangeCommand;
26import org.openstreetmap.josm.command.ChangePropertyCommand;
27import org.openstreetmap.josm.command.Command;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
30import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.Relation;
33import org.openstreetmap.josm.data.osm.RelationMember;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.gui.Notification;
36import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationTask;
37import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
38import org.openstreetmap.josm.gui.util.GuiHelper;
39import org.openstreetmap.josm.tools.Pair;
40import org.openstreetmap.josm.tools.Shortcut;
41import org.openstreetmap.josm.tools.Utils;
42
43/**
44 * Create multipolygon from selected ways automatically.
45 *
46 * New relation with type=multipolygon is created.
47 *
48 * If one or more of ways is already in relation with type=multipolygon or the
49 * way is not closed, then error is reported and no relation is created.
50 *
51 * The "inner" and "outer" roles are guessed automatically. First, bbox is
52 * calculated for each way. then the largest area is assumed to be outside and
53 * the rest inside. In cases with one "outside" area and several cut-ins, the
54 * guess should be always good ... In more complex (multiple outer areas) or
55 * buggy (inner and outer ways intersect) scenarios the result is likely to be
56 * wrong.
57 */
58public class CreateMultipolygonAction extends JosmAction {
59
60 private final boolean update;
61
62 /**
63 * Constructs a new {@code CreateMultipolygonAction}.
64 * @param update {@code true} if the multipolygon must be updated, {@code false} if it must be created
65 */
66 public CreateMultipolygonAction(final boolean update) {
67 super(getName(update), /* ICON */ "multipoly_create", getName(update),
68 /* atleast three lines for each shortcut or the server extractor fails */
69 update ? Shortcut.registerShortcut("tools:multipoly_update",
70 tr("Tool: {0}", getName(true)),
71 KeyEvent.VK_B, Shortcut.CTRL_SHIFT)
72 : Shortcut.registerShortcut("tools:multipoly_create",
73 tr("Tool: {0}", getName(false)),
74 KeyEvent.VK_B, Shortcut.CTRL),
75 true, update ? "multipoly_update" : "multipoly_create", true);
76 this.update = update;
77 }
78
79 private static String getName(boolean update) {
80 return update ? tr("Update multipolygon") : tr("Create multipolygon");
81 }
82
83 private static class CreateUpdateMultipolygonTask implements Runnable {
84 private final Collection<Way> selectedWays;
85 private final Relation multipolygonRelation;
86
87 private CreateUpdateMultipolygonTask(Collection<Way> selectedWays, Relation multipolygonRelation) {
88 this.selectedWays = selectedWays;
89 this.multipolygonRelation = multipolygonRelation;
90 }
91
92 @Override
93 public void run() {
94 final Pair<SequenceCommand, Relation> commandAndRelation = createMultipolygonCommand(selectedWays, multipolygonRelation);
95 if (commandAndRelation == null) {
96 return;
97 }
98 final Command command = commandAndRelation.a;
99 final Relation relation = commandAndRelation.b;
100
101 // to avoid EDT violations
102 SwingUtilities.invokeLater(new Runnable() {
103 @Override
104 public void run() {
105 Main.main.undoRedo.add(command);
106
107 // Use 'SwingUtilities.invokeLater' to make sure the relationListDialog
108 // knows about the new relation before we try to select it.
109 // (Yes, we are already in event dispatch thread. But DatasetEventManager
110 // uses 'SwingUtilities.invokeLater' to fire events so we have to do the same.)
111 SwingUtilities.invokeLater(new Runnable() {
112 @Override
113 public void run() {
114 Main.map.relationListDialog.selectRelation(relation);
115 if (Main.pref.getBoolean("multipoly.show-relation-editor", false)) {
116 //Open relation edit window, if set up in preferences
117 RelationEditor editor = RelationEditor.getEditor(Main.main.getEditLayer(), relation, null);
118
119 editor.setModal(true);
120 editor.setVisible(true);
121 }
122 }
123 });
124 }
125 });
126 }
127 }
128
129 @Override
130 public void actionPerformed(ActionEvent e) {
131 if (!Main.main.hasEditLayer()) {
132 new Notification(
133 tr("No data loaded."))
134 .setIcon(JOptionPane.WARNING_MESSAGE)
135 .setDuration(Notification.TIME_SHORT)
136 .show();
137 return;
138 }
139
140 final Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays();
141 final Collection<Relation> selectedRelations = Main.main.getCurrentDataSet().getSelectedRelations();
142
143 if (selectedWays.isEmpty()) {
144 // Sometimes it make sense creating multipoly of only one way (so it will form outer way)
145 // and then splitting the way later (so there are multiple ways forming outer way)
146 new Notification(
147 tr("You must select at least one way."))
148 .setIcon(JOptionPane.INFORMATION_MESSAGE)
149 .setDuration(Notification.TIME_SHORT)
150 .show();
151 return;
152 }
153
154 final Relation multipolygonRelation = update
155 ? getSelectedMultipolygonRelation(selectedWays, selectedRelations)
156 : null;
157
158 // download incomplete relation if necessary
159 if (multipolygonRelation != null && !multipolygonRelation.isNew()
160 && (multipolygonRelation.isIncomplete() || multipolygonRelation.hasIncompleteMembers())) {
161 Main.worker.submit(new DownloadRelationTask(Collections.singleton(multipolygonRelation), Main.main.getEditLayer()));
162 }
163 // create/update multipolygon relation
164 Main.worker.submit(new CreateUpdateMultipolygonTask(selectedWays, multipolygonRelation));
165 }
166
167 private Relation getSelectedMultipolygonRelation() {
168 return getSelectedMultipolygonRelation(getCurrentDataSet().getSelectedWays(), getCurrentDataSet().getSelectedRelations());
169 }
170
171 private static Relation getSelectedMultipolygonRelation(Collection<Way> selectedWays, Collection<Relation> selectedRelations) {
172 if (selectedRelations.size() == 1 && "multipolygon".equals(selectedRelations.iterator().next().get("type"))) {
173 return selectedRelations.iterator().next();
174 } else {
175 final Set<Relation> relatedRelations = new HashSet<>();
176 for (final Way w : selectedWays) {
177 relatedRelations.addAll(Utils.filteredCollection(w.getReferrers(), Relation.class));
178 }
179 return relatedRelations.size() == 1 ? relatedRelations.iterator().next() : null;
180 }
181 }
182
183 /**
184 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}.
185 */
186 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) {
187
188 // add ways of existing relation to include them in polygon analysis
189 Set<Way> ways = new HashSet<>(selectedWays);
190 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class));
191
192 final MultipolygonBuilder polygon = analyzeWays(ways, true);
193 if (polygon == null) {
194 return null; //could not make multipolygon.
195 } else {
196 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, new Relation(selectedMultipolygonRelation)));
197 }
198 }
199
200 /**
201 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}.
202 */
203 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) {
204
205 final MultipolygonBuilder polygon = analyzeWays(selectedWays, showNotif);
206 if (polygon == null) {
207 return null; //could not make multipolygon.
208 } else {
209 return Pair.create(null, createRelation(polygon, new Relation()));
210 }
211 }
212
213 /**
214 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}.
215 */
216 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) {
217
218 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null
219 ? createMultipolygonRelation(selectedWays, true)
220 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation);
221 if (rr == null) {
222 return null;
223 }
224 final Relation existingRelation = rr.a;
225 final Relation relation = rr.b;
226
227 final List<Command> list = removeTagsFromWaysIfNeeded(relation);
228 final String commandName;
229 if (existingRelation == null) {
230 list.add(new AddCommand(relation));
231 commandName = getName(false);
232 } else {
233 list.add(new ChangeCommand(existingRelation, relation));
234 commandName = getName(true);
235 }
236 return Pair.create(new SequenceCommand(commandName, list), relation);
237 }
238
239 /** Enable this action only if something is selected */
240 @Override
241 protected void updateEnabledState() {
242 if (getCurrentDataSet() == null) {
243 setEnabled(false);
244 } else {
245 updateEnabledState(getCurrentDataSet().getSelected());
246 }
247 }
248
249 /**
250 * Enable this action only if something is selected
251 *
252 * @param selection the current selection, gets tested for emptyness
253 */
254 @Override
255 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
256 if (update) {
257 setEnabled(getSelectedMultipolygonRelation() != null);
258 } else {
259 setEnabled(!getCurrentDataSet().getSelectedWays().isEmpty());
260 }
261 }
262
263 /**
264 * This method analyzes ways and creates multipolygon.
265 * @param selectedWays list of selected ways
266 * @return <code>null</code>, if there was a problem with the ways.
267 */
268 private static MultipolygonBuilder analyzeWays(Collection<Way> selectedWays, boolean showNotif) {
269
270 MultipolygonBuilder pol = new MultipolygonBuilder();
271 final String error = pol.makeFromWays(selectedWays);
272
273 if (error != null) {
274 if (showNotif) {
275 GuiHelper.runInEDT(new Runnable() {
276 @Override
277 public void run() {
278 new Notification(error)
279 .setIcon(JOptionPane.INFORMATION_MESSAGE)
280 .show();
281 }
282 });
283 }
284 return null;
285 } else {
286 return pol;
287 }
288 }
289
290 /**
291 * Builds a relation from polygon ways.
292 * @param pol data storage class containing polygon information
293 * @return multipolygon relation
294 */
295 private static Relation createRelation(MultipolygonBuilder pol, final Relation rel) {
296 // Create new relation
297 rel.put("type", "multipolygon");
298 // Add ways to it
299 for (JoinedPolygon jway:pol.outerWays) {
300 addMembers(jway, rel, "outer");
301 }
302
303 for (JoinedPolygon jway:pol.innerWays) {
304 addMembers(jway, rel, "inner");
305 }
306 return rel;
307 }
308
309 private static void addMembers(JoinedPolygon polygon, Relation rel, String role) {
310 final int count = rel.getMembersCount();
311 final Set<Way> ways = new HashSet<>(polygon.ways);
312 for (int i = 0; i < count; i++) {
313 final RelationMember m = rel.getMember(i);
314 if (ways.contains(m.getMember()) && !role.equals(m.getRole())) {
315 rel.setMember(i, new RelationMember(role, m.getMember()));
316 }
317 }
318 ways.removeAll(rel.getMemberPrimitives());
319 for (final Way way : ways) {
320 rel.addMember(new RelationMember(role, way));
321 }
322 }
323
324 public static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "fence_type", "source");
325
326 /**
327 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary
328 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core
329 * @param relation the multipolygon style relation to process
330 * @return a list of commands to execute
331 */
332 public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) {
333 Map<String, String> values = new HashMap<>(relation.getKeys());
334
335 List<Way> innerWays = new ArrayList<>();
336 List<Way> outerWays = new ArrayList<>();
337
338 Set<String> conflictingKeys = new TreeSet<>();
339
340 for( RelationMember m : relation.getMembers() ) {
341
342 if( m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) {
343 innerWays.add(m.getWay());
344 }
345
346 if( m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) {
347 Way way = m.getWay();
348 outerWays.add(way);
349
350 for (String key : way.keySet()) {
351 if (!values.containsKey(key)) { //relation values take precedence
352 values.put(key, way.get(key));
353 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) {
354 conflictingKeys.add(key);
355 }
356 }
357 }
358 }
359
360 // filter out empty key conflicts - we need second iteration
361 if (!Main.pref.getBoolean("multipoly.alltags", false))
362 for (RelationMember m : relation.getMembers())
363 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay())
364 for (String key : values.keySet())
365 if (!m.getWay().hasKey(key) && !relation.hasKey(key))
366 conflictingKeys.add(key);
367
368 for (String key : conflictingKeys)
369 values.remove(key);
370
371 for (String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS))
372 values.remove(linearTag);
373
374 if ("coastline".equals(values.get("natural")))
375 values.remove("natural");
376
377 values.put("area", "yes");
378
379 List<Command> commands = new ArrayList<>();
380 boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true);
381
382 for (Entry<String, String> entry : values.entrySet()) {
383 List<OsmPrimitive> affectedWays = new ArrayList<>();
384 String key = entry.getKey();
385 String value = entry.getValue();
386
387 for (Way way : innerWays) {
388 if (value.equals(way.get(key))) {
389 affectedWays.add(way);
390 }
391 }
392
393 if (moveTags) {
394 // remove duplicated tags from outer ways
395 for( Way way : outerWays ) {
396 if( way.hasKey(key) ) {
397 affectedWays.add(way);
398 }
399 }
400 }
401
402 if (!affectedWays.isEmpty()) {
403 // reset key tag on affected ways
404 commands.add(new ChangePropertyCommand(affectedWays, key, null));
405 }
406 }
407
408 if (moveTags) {
409 // add those tag values to the relation
410 boolean fixed = false;
411 Relation r2 = new Relation(relation);
412 for (Entry<String, String> entry : values.entrySet()) {
413 String key = entry.getKey();
414 if (!r2.hasKey(key) && !"area".equals(key) ) {
415 if (relation.isNew())
416 relation.put(key, entry.getValue());
417 else
418 r2.put(key, entry.getValue());
419 fixed = true;
420 }
421 }
422 if (fixed && !relation.isNew())
423 commands.add(new ChangeCommand(relation, r2));
424 }
425
426 return commands;
427 }
428}
Note: See TracBrowser for help on using the repository browser.