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

Last change on this file since 17379 was 17358, checked in by GerdP, 3 years ago

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

  • (hopefully) fix memory leaks in complex actions
  • handle complex cases with presets and RelationEditor

I hope these changes don't break plugins which extend or overwrite RelationEditor

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