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

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

see #13036 - deprecate Command() default constructor, fix unit tests and java warnings

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