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

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

Java 8: use Stream

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