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

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

Sonar - squid:S1941 - Variables should not be declared before they are relevant

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