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

Last change on this file since 17252 was 17219, checked in by GerdP, 4 years ago

see #19885: memory leak with "temporary" objects in validator and actions

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