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

Last change on this file since 17008 was 17008, checked in by simon04, 4 years ago

Checkstyle

  • Property svn:eol-style set to native
File size: 21.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.LinkedHashSet;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.Set;
20import java.util.TreeSet;
21import java.util.stream.Collectors;
22
23import javax.swing.JOptionPane;
24import javax.swing.SwingUtilities;
25
26import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
27import org.openstreetmap.josm.command.AddCommand;
28import org.openstreetmap.josm.command.ChangeCommand;
29import org.openstreetmap.josm.command.ChangePropertyCommand;
30import org.openstreetmap.josm.command.Command;
31import org.openstreetmap.josm.command.SequenceCommand;
32import org.openstreetmap.josm.data.UndoRedoHandler;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.osm.IPrimitive;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.data.osm.OsmUtils;
37import org.openstreetmap.josm.data.osm.Relation;
38import org.openstreetmap.josm.data.osm.RelationMember;
39import org.openstreetmap.josm.data.osm.Way;
40import org.openstreetmap.josm.data.validation.TestError;
41import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
42import org.openstreetmap.josm.gui.MainApplication;
43import org.openstreetmap.josm.gui.Notification;
44import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationMemberTask;
45import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationTask;
46import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
47import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
48import org.openstreetmap.josm.gui.layer.OsmDataLayer;
49import org.openstreetmap.josm.gui.util.GuiHelper;
50import org.openstreetmap.josm.spi.preferences.Config;
51import org.openstreetmap.josm.tools.Pair;
52import org.openstreetmap.josm.tools.Shortcut;
53import org.openstreetmap.josm.tools.SubclassFilteredCollection;
54import org.openstreetmap.josm.tools.Utils;
55
56/**
57 * Create multipolygon from selected ways automatically.
58 *
59 * New relation with type=multipolygon is created.
60 *
61 * If one or more of ways is already in relation with type=multipolygon or the
62 * way is not closed, then error is reported and no relation is created.
63 *
64 * The "inner" and "outer" roles are guessed automatically. First, bbox is
65 * calculated for each way. then the largest area is assumed to be outside and
66 * the rest inside. In cases with one "outside" area and several cut-ins, the
67 * guess should be always good ... In more complex (multiple outer areas) or
68 * buggy (inner and outer ways intersect) scenarios the result is likely to be
69 * wrong.
70 */
71public class CreateMultipolygonAction extends JosmAction {
72
73 private final boolean update;
74 private static final int MAX_MEMBERS_TO_DOWNLOAD = 100;
75
76 /**
77 * Constructs a new {@code CreateMultipolygonAction}.
78 * @param update {@code true} if the multipolygon must be updated, {@code false} if it must be created
79 */
80 public CreateMultipolygonAction(final boolean update) {
81 super(getName(update), /* ICON */ "multipoly_create", getName(update),
82 /* at least three lines for each shortcut or the server extractor fails */
83 update ? Shortcut.registerShortcut("tools:multipoly_update",
84 tr("Tool: {0}", getName(true)),
85 KeyEvent.VK_B, Shortcut.CTRL_SHIFT)
86 : Shortcut.registerShortcut("tools:multipoly_create",
87 tr("Tool: {0}", getName(false)),
88 KeyEvent.VK_B, Shortcut.CTRL),
89 true, update ? "multipoly_update" : "multipoly_create", true);
90 this.update = update;
91 }
92
93 private static String getName(boolean update) {
94 return update ? tr("Update multipolygon") : tr("Create multipolygon");
95 }
96
97 private static final class CreateUpdateMultipolygonTask implements Runnable {
98 private final Collection<Way> selectedWays;
99 private final Relation multipolygonRelation;
100
101 private CreateUpdateMultipolygonTask(Collection<Way> selectedWays, Relation multipolygonRelation) {
102 this.selectedWays = selectedWays;
103 this.multipolygonRelation = multipolygonRelation;
104 }
105
106 @Override
107 public void run() {
108 final Pair<SequenceCommand, Relation> commandAndRelation = createMultipolygonCommand(selectedWays, multipolygonRelation);
109 if (commandAndRelation == null) {
110 return;
111 }
112 final Command command = commandAndRelation.a;
113
114 // to avoid EDT violations
115 SwingUtilities.invokeLater(() -> {
116 UndoRedoHandler.getInstance().add(command);
117 final Relation relation = (Relation) MainApplication.getLayerManager().getEditDataSet()
118 .getPrimitiveById(commandAndRelation.b);
119 if (relation == null || relation.getDataSet() == null)
120 return; // should not happen
121
122 // Use 'SwingUtilities.invokeLater' to make sure the relationListDialog
123 // knows about the new relation before we try to select it.
124 // (Yes, we are already in event dispatch thread. But DatasetEventManager
125 // uses 'SwingUtilities.invokeLater' to fire events so we have to do the same.)
126 SwingUtilities.invokeLater(() -> {
127 MainApplication.getMap().relationListDialog.selectRelation(relation);
128 if (Config.getPref().getBoolean("multipoly.show-relation-editor", false)) {
129 //Open relation edit window, if set up in preferences
130 // see #19346 un-select updated multipolygon
131 MainApplication.getLayerManager().getEditDataSet().clearSelection(relation);
132 RelationEditor editor = RelationEditor
133 .getEditor(MainApplication.getLayerManager().getEditLayer(), relation, null);
134 editor.setModal(true);
135 editor.setVisible(true);
136 } else {
137 MainApplication.getLayerManager().getEditLayer().setRecentRelation(relation);
138 if (multipolygonRelation == null) {
139 // see #19346 select new multipolygon
140 MainApplication.getLayerManager().getEditDataSet().setSelected(relation);
141 }
142 }
143 });
144 });
145 }
146 }
147
148 @Override
149 public void actionPerformed(ActionEvent e) {
150 DataSet dataSet = getLayerManager().getEditDataSet();
151 if (dataSet == null) {
152 new Notification(
153 tr("No data loaded."))
154 .setIcon(JOptionPane.WARNING_MESSAGE)
155 .setDuration(Notification.TIME_SHORT)
156 .show();
157 return;
158 }
159
160 final Collection<Way> selectedWays = dataSet.getSelectedWays();
161
162 if (selectedWays.isEmpty()) {
163 // Sometimes it make sense creating multipoly of only one way (so it will form outer way)
164 // and then splitting the way later (so there are multiple ways forming outer way)
165 new Notification(
166 tr("You must select at least one way."))
167 .setIcon(JOptionPane.INFORMATION_MESSAGE)
168 .setDuration(Notification.TIME_SHORT)
169 .show();
170 return;
171 }
172
173 final Collection<Relation> selectedRelations = dataSet.getSelectedRelations();
174 final Relation multipolygonRelation = update
175 ? getSelectedMultipolygonRelation(selectedWays, selectedRelations)
176 : null;
177
178 if (update && multipolygonRelation == null)
179 return;
180 // download incomplete relation or incomplete members if necessary
181 OsmDataLayer editLayer = getLayerManager().getEditLayer();
182 if (multipolygonRelation != null && editLayer != null && editLayer.isDownloadable()) {
183 if (!multipolygonRelation.isNew() && multipolygonRelation.isIncomplete()) {
184 MainApplication.worker
185 .submit(new DownloadRelationTask(Collections.singleton(multipolygonRelation), editLayer));
186 } else if (multipolygonRelation.hasIncompleteMembers()) {
187 // with complex relations the download of the full relation is much faster than download of almost all members, see #18341
188 SubclassFilteredCollection<IPrimitive, OsmPrimitive> incompleteMembers = Utils
189 .filteredCollection(DownloadSelectedIncompleteMembersAction.buildSetOfIncompleteMembers(
190 Collections.singleton(multipolygonRelation)), OsmPrimitive.class);
191
192 if (incompleteMembers.size() <= MAX_MEMBERS_TO_DOWNLOAD) {
193 MainApplication.worker
194 .submit(new DownloadRelationMemberTask(multipolygonRelation, incompleteMembers, editLayer));
195 } else {
196 MainApplication.worker
197 .submit(new DownloadRelationTask(Collections.singleton(multipolygonRelation), editLayer));
198
199 }
200 }
201 }
202 // create/update multipolygon relation
203 MainApplication.worker.submit(new CreateUpdateMultipolygonTask(selectedWays, multipolygonRelation));
204 }
205
206 private static Relation getSelectedMultipolygonRelation(Collection<Way> selectedWays, Collection<Relation> selectedRelations) {
207 Relation candidate = null;
208 if (selectedRelations.size() == 1) {
209 candidate = selectedRelations.iterator().next();
210 if (!candidate.hasTag("type", "multipolygon"))
211 candidate = null;
212 } else if (!selectedWays.isEmpty()) {
213 for (final Way w : selectedWays) {
214 for (OsmPrimitive r : w.getReferrers()) {
215 if (r != candidate && !r.isDisabled() && r instanceof Relation && r.hasTag("type", "multipolygon")) {
216 if (candidate != null)
217 return null; // found another multipolygon relation
218 candidate = (Relation) r;
219 }
220 }
221 }
222 }
223 return candidate;
224 }
225
226 /**
227 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}.
228 * @param selectedWays selected ways
229 * @param selectedMultipolygonRelation selected multipolygon relation
230 * @return pair of old and new multipolygon relation if a difference was found, else the pair contains the old relation twice
231 */
232 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) {
233
234 // add ways of existing relation to include them in polygon analysis
235 Set<Way> ways = new HashSet<>(selectedWays);
236 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class));
237
238 // even if no way was added the inner/outer roles might be different
239 MultipolygonTest mpTest = new MultipolygonTest();
240 Relation calculated = mpTest.makeFromWays(ways);
241 if (mpTest.getErrors().isEmpty()) {
242 return mergeRelationsMembers(selectedMultipolygonRelation, calculated);
243 }
244 showErrors(mpTest.getErrors());
245 return null; //could not make multipolygon.
246 }
247
248 /**
249 * Merge members of multipolygon relation. Maintains the order of the old relation. May change roles,
250 * removes duplicate and non-way members and adds new members found in {@code calculated}.
251 * @param old old multipolygon relation
252 * @param calculated calculated multipolygon relation
253 * @return pair of old and new multipolygon relation if a difference was found, else the pair contains the old relation twice
254 */
255 private static Pair<Relation, Relation> mergeRelationsMembers(Relation old, Relation calculated) {
256 Set<RelationMember> merged = new LinkedHashSet<>();
257 boolean foundDiff = false;
258 int nonWayMember = 0;
259 // maintain order of members in updated relation
260 for (RelationMember oldMem :old.getMembers()) {
261 if (oldMem.isNode() || oldMem.isRelation()) {
262 nonWayMember++;
263 continue;
264 }
265 for (RelationMember newMem : calculated.getMembers()) {
266 if (newMem.getMember().equals(oldMem.getMember())) {
267 if (!newMem.getRole().equals(oldMem.getRole())) {
268 foundDiff = true;
269 }
270 foundDiff |= !merged.add(newMem); // detect duplicate members in old relation
271 break;
272 }
273 }
274 }
275 if (nonWayMember > 0) {
276 foundDiff = true;
277 String msg = trn("Non-Way member removed from multipolygon", "Non-Way members removed from multipolygon", nonWayMember);
278 GuiHelper.runInEDT(() -> new Notification(msg).setIcon(JOptionPane.WARNING_MESSAGE).show());
279 }
280 foundDiff |= merged.addAll(calculated.getMembers());
281 if (!foundDiff) {
282 return Pair.create(old, old); // unchanged
283 }
284 Relation toModify = new Relation(old);
285 toModify.setMembers(new ArrayList<>(merged));
286 return Pair.create(old, toModify);
287 }
288
289 /**
290 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}.
291 * @param selectedWays selected ways
292 * @param showNotif if {@code true}, shows a notification if an error occurs
293 * @return pair of null and new multipolygon relation
294 */
295 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) {
296 MultipolygonTest mpTest = new MultipolygonTest();
297 Relation calculated = mpTest.makeFromWays(selectedWays);
298 calculated.setMembers(RelationSorter.sortMembersByConnectivity(calculated.getMembers()));
299 if (mpTest.getErrors().isEmpty())
300 return Pair.create(null, calculated);
301 if (showNotif) {
302 showErrors(mpTest.getErrors());
303 }
304 return null; //could not make multipolygon.
305 }
306
307 private static void showErrors(List<TestError> errors) {
308 if (!errors.isEmpty()) {
309 String errorMessages = errors.stream()
310 .map(TestError::getMessage)
311 .distinct()
312 .collect(Collectors.joining("\n"));
313 GuiHelper.runInEDT(() -> new Notification(errorMessages).setIcon(JOptionPane.INFORMATION_MESSAGE).show());
314 }
315 }
316
317 /**
318 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}.
319 * @param selectedWays selected ways
320 * @param selectedMultipolygonRelation selected multipolygon relation
321 * @return pair of command and multipolygon relation
322 */
323 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays,
324 Relation selectedMultipolygonRelation) {
325
326 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null
327 ? createMultipolygonRelation(selectedWays, true)
328 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation);
329 if (rr == null) {
330 return null;
331 }
332 boolean unchanged = rr.a == rr.b;
333 final Relation existingRelation = rr.a;
334 final Relation relation = rr.b;
335
336 final List<Command> list = removeTagsFromWaysIfNeeded(relation);
337 final String commandName;
338 if (existingRelation == null) {
339 list.add(new AddCommand(selectedWays.iterator().next().getDataSet(), relation));
340 commandName = getName(false);
341 } else {
342 if (!unchanged) {
343 list.add(new ChangeCommand(existingRelation, relation));
344 }
345 if (list.isEmpty()) {
346 if (unchanged) {
347 MultipolygonTest mpTest = new MultipolygonTest();
348 mpTest.visit(existingRelation);
349 if (!mpTest.getErrors().isEmpty()) {
350 showErrors(mpTest.getErrors());
351 return null;
352 }
353 }
354
355 GuiHelper.runInEDT(() -> new Notification(tr("Nothing changed")).setDuration(Notification.TIME_SHORT)
356 .setIcon(JOptionPane.INFORMATION_MESSAGE).show());
357 return null;
358 }
359 commandName = getName(true);
360 }
361 return Pair.create(new SequenceCommand(commandName, list), relation);
362 }
363
364 /** Enable this action only if something is selected */
365 @Override
366 protected void updateEnabledState() {
367 updateEnabledStateOnCurrentSelection();
368 }
369
370 /**
371 * Enable this action only if something is selected
372 *
373 * @param selection the current selection, gets tested for emptiness
374 */
375 @Override
376 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
377 DataSet ds = getLayerManager().getEditDataSet();
378 if (ds == null || selection.isEmpty()) {
379 setEnabled(false);
380 } else if (update) {
381 setEnabled(getSelectedMultipolygonRelation(ds.getSelectedWays(), ds.getSelectedRelations()) != null);
382 } else {
383 setEnabled(!ds.getSelectedWays().isEmpty());
384 }
385 }
386
387 private static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "fence_type", "source");
388
389 /**
390 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary
391 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core
392 * @param relation the multipolygon style relation to process
393 * @return a list of commands to execute
394 */
395 public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) {
396 Map<String, String> values = new HashMap<>(relation.getKeys());
397
398 List<Way> innerWays = new ArrayList<>();
399 List<Way> outerWays = new ArrayList<>();
400
401 Set<String> conflictingKeys = new TreeSet<>();
402
403 for (RelationMember m : relation.getMembers()) {
404
405 if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
406 innerWays.add(m.getWay());
407 }
408
409 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
410 Way way = m.getWay();
411 outerWays.add(way);
412
413 for (String key : way.keySet()) {
414 if (!values.containsKey(key)) { //relation values take precedence
415 values.put(key, way.get(key));
416 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) {
417 conflictingKeys.add(key);
418 }
419 }
420 }
421 }
422
423 // filter out empty key conflicts - we need second iteration
424 if (!Config.getPref().getBoolean("multipoly.alltags", false)) {
425 for (RelationMember m : relation.getMembers()) {
426 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) {
427 for (String key : values.keySet()) {
428 if (!m.getWay().hasKey(key) && !relation.hasKey(key)) {
429 conflictingKeys.add(key);
430 }
431 }
432 }
433 }
434 }
435
436 for (String key : conflictingKeys) {
437 values.remove(key);
438 }
439
440 for (String linearTag : Config.getPref().getList("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS)) {
441 values.remove(linearTag);
442 }
443
444 if ("coastline".equals(values.get("natural")))
445 values.remove("natural");
446
447 values.put("area", OsmUtils.TRUE_VALUE);
448
449 List<Command> commands = new ArrayList<>();
450 boolean moveTags = Config.getPref().getBoolean("multipoly.movetags", true);
451
452 for (Entry<String, String> entry : values.entrySet()) {
453 String key = entry.getKey();
454 String value = entry.getValue();
455 List<OsmPrimitive> affectedWays = innerWays.stream().filter(way -> value.equals(way.get(key))).collect(Collectors.toList());
456
457 if (moveTags) {
458 // remove duplicated tags from outer ways
459 for (Way way : outerWays) {
460 if (way.hasKey(key)) {
461 affectedWays.add(way);
462 }
463 }
464 }
465
466 if (!affectedWays.isEmpty()) {
467 // reset key tag on affected ways
468 commands.add(new ChangePropertyCommand(affectedWays, key, null));
469 }
470 }
471
472 if (moveTags) {
473 // add those tag values to the relation
474 boolean fixed = false;
475 Relation r2 = new Relation(relation);
476 for (Entry<String, String> entry : values.entrySet()) {
477 String key = entry.getKey();
478 if (!r2.hasKey(key) && !"area".equals(key)) {
479 if (relation.isNew())
480 relation.put(key, entry.getValue());
481 else
482 r2.put(key, entry.getValue());
483 fixed = true;
484 }
485 }
486 if (fixed && !relation.isNew()) {
487 DataSet ds = relation.getDataSet();
488 if (ds == null) {
489 ds = MainApplication.getLayerManager().getEditDataSet();
490 }
491 commands.add(new ChangeCommand(ds, relation, r2));
492 }
493 }
494
495 return commands;
496 }
497}
Note: See TracBrowser for help on using the repository browser.