001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.event.ActionEvent; 007import java.awt.event.KeyEvent; 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.Collection; 011import java.util.Collections; 012import java.util.HashMap; 013import java.util.HashSet; 014import java.util.List; 015import java.util.Map; 016import java.util.Map.Entry; 017import java.util.Set; 018import java.util.TreeSet; 019 020import javax.swing.JOptionPane; 021import javax.swing.SwingUtilities; 022 023import org.openstreetmap.josm.Main; 024import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction; 025import org.openstreetmap.josm.command.AddCommand; 026import org.openstreetmap.josm.command.ChangeCommand; 027import org.openstreetmap.josm.command.ChangePropertyCommand; 028import org.openstreetmap.josm.command.Command; 029import org.openstreetmap.josm.command.SequenceCommand; 030import org.openstreetmap.josm.data.osm.MultipolygonBuilder; 031import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; 032import org.openstreetmap.josm.data.osm.OsmPrimitive; 033import org.openstreetmap.josm.data.osm.Relation; 034import org.openstreetmap.josm.data.osm.RelationMember; 035import org.openstreetmap.josm.data.osm.Way; 036import org.openstreetmap.josm.gui.Notification; 037import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationMemberTask; 038import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationTask; 039import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; 040import org.openstreetmap.josm.gui.util.GuiHelper; 041import org.openstreetmap.josm.tools.Pair; 042import org.openstreetmap.josm.tools.Shortcut; 043import org.openstreetmap.josm.tools.Utils; 044 045/** 046 * Create multipolygon from selected ways automatically. 047 * 048 * New relation with type=multipolygon is created. 049 * 050 * If one or more of ways is already in relation with type=multipolygon or the 051 * way is not closed, then error is reported and no relation is created. 052 * 053 * The "inner" and "outer" roles are guessed automatically. First, bbox is 054 * calculated for each way. then the largest area is assumed to be outside and 055 * the rest inside. In cases with one "outside" area and several cut-ins, the 056 * guess should be always good ... In more complex (multiple outer areas) or 057 * buggy (inner and outer ways intersect) scenarios the result is likely to be 058 * wrong. 059 */ 060public class CreateMultipolygonAction extends JosmAction { 061 062 private final boolean update; 063 064 /** 065 * Constructs a new {@code CreateMultipolygonAction}. 066 * @param update {@code true} if the multipolygon must be updated, {@code false} if it must be created 067 */ 068 public CreateMultipolygonAction(final boolean update) { 069 super(getName(update), /* ICON */ "multipoly_create", getName(update), 070 /* atleast three lines for each shortcut or the server extractor fails */ 071 update ? Shortcut.registerShortcut("tools:multipoly_update", 072 tr("Tool: {0}", getName(true)), 073 KeyEvent.VK_B, Shortcut.CTRL_SHIFT) 074 : Shortcut.registerShortcut("tools:multipoly_create", 075 tr("Tool: {0}", getName(false)), 076 KeyEvent.VK_B, Shortcut.CTRL), 077 true, update ? "multipoly_update" : "multipoly_create", true); 078 this.update = update; 079 } 080 081 private static String getName(boolean update) { 082 return update ? tr("Update multipolygon") : tr("Create multipolygon"); 083 } 084 085 private static final class CreateUpdateMultipolygonTask implements Runnable { 086 private final Collection<Way> selectedWays; 087 private final Relation multipolygonRelation; 088 089 private CreateUpdateMultipolygonTask(Collection<Way> selectedWays, Relation multipolygonRelation) { 090 this.selectedWays = selectedWays; 091 this.multipolygonRelation = multipolygonRelation; 092 } 093 094 @Override 095 public void run() { 096 final Pair<SequenceCommand, Relation> commandAndRelation = createMultipolygonCommand(selectedWays, multipolygonRelation); 097 if (commandAndRelation == null) { 098 return; 099 } 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 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 112 // uses 'SwingUtilities.invokeLater' to fire events so we have to do the same.) 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); 120 121 editor.setModal(true); 122 editor.setVisible(true); 123 } 124 } 125 }); 126 } 127 }); 128 } 129 } 130 131 @Override 132 public void actionPerformed(ActionEvent e) { 133 if (!Main.main.hasEditLayer()) { 134 new Notification( 135 tr("No data loaded.")) 136 .setIcon(JOptionPane.WARNING_MESSAGE) 137 .setDuration(Notification.TIME_SHORT) 138 .show(); 139 return; 140 } 141 142 final Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays(); 143 final Collection<Relation> selectedRelations = Main.main.getCurrentDataSet().getSelectedRelations(); 144 145 if (selectedWays.isEmpty()) { 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) 148 new Notification( 149 tr("You must select at least one way.")) 150 .setIcon(JOptionPane.INFORMATION_MESSAGE) 151 .setDuration(Notification.TIME_SHORT) 152 .show(); 153 return; 154 } 155 156 final Relation multipolygonRelation = update 157 ? getSelectedMultipolygonRelation(selectedWays, selectedRelations) 158 : null; 159 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 } 169 } 170 // create/update multipolygon relation 171 Main.worker.submit(new CreateUpdateMultipolygonTask(selectedWays, multipolygonRelation)); 172 } 173 174 private static Relation getSelectedMultipolygonRelation() { 175 return getSelectedMultipolygonRelation(getCurrentDataSet().getSelectedWays(), getCurrentDataSet().getSelectedRelations()); 176 } 177 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 { 182 final Set<Relation> relatedRelations = new HashSet<>(); 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 190 /** 191 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}. 192 * @param selectedWays selected ways 193 * @param selectedMultipolygonRelation selected multipolygon relation 194 * @return pair of old and new multipolygon relation 195 */ 196 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) { 197 198 // add ways of existing relation to include them in polygon analysis 199 Set<Way> ways = new HashSet<>(selectedWays); 200 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class)); 201 202 final MultipolygonBuilder polygon = analyzeWays(ways, true); 203 if (polygon == null) { 204 return null; //could not make multipolygon. 205 } else { 206 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, selectedMultipolygonRelation)); 207 } 208 } 209 210 /** 211 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}. 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 215 */ 216 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) { 217 218 final MultipolygonBuilder polygon = analyzeWays(selectedWays, showNotif); 219 if (polygon == null) { 220 return null; //could not make multipolygon. 221 } else { 222 return Pair.create(null, createRelation(polygon, null)); 223 } 224 } 225 226 /** 227 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}. 228 * @param selectedWays selected ways 229 * @param selectedMultipolygonRelation selected multipolygon relation 230 * @return pair of command and multipolygon relation 231 */ 232 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, 233 Relation selectedMultipolygonRelation) { 234 235 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null 236 ? createMultipolygonRelation(selectedWays, true) 237 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation); 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)); 248 commandName = getName(false); 249 } else { 250 list.add(new ChangeCommand(existingRelation, relation)); 251 commandName = getName(true); 252 } 253 return Pair.create(new SequenceCommand(commandName, list), relation); 254 } 255 256 /** Enable this action only if something is selected */ 257 @Override 258 protected void updateEnabledState() { 259 if (getCurrentDataSet() == null) { 260 setEnabled(false); 261 } else { 262 updateEnabledState(getCurrentDataSet().getSelected()); 263 } 264 } 265 266 /** 267 * Enable this action only if something is selected 268 * 269 * @param selection the current selection, gets tested for emptyness 270 */ 271 @Override 272 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 273 if (getCurrentDataSet() == null) { 274 setEnabled(false); 275 } else if (update) { 276 setEnabled(getSelectedMultipolygonRelation() != null); 277 } else { 278 setEnabled(!getCurrentDataSet().getSelectedWays().isEmpty()); 279 } 280 } 281 282 /** 283 * This method analyzes ways and creates multipolygon. 284 * @param selectedWays list of selected ways 285 * @param showNotif if {@code true}, shows a notification if an error occurs 286 * @return <code>null</code>, if there was a problem with the ways. 287 */ 288 private static MultipolygonBuilder analyzeWays(Collection<Way> selectedWays, boolean showNotif) { 289 290 MultipolygonBuilder pol = new MultipolygonBuilder(); 291 final String error = pol.makeFromWays(selectedWays); 292 293 if (error != null) { 294 if (showNotif) { 295 GuiHelper.runInEDT(new Runnable() { 296 @Override 297 public void run() { 298 new Notification(error) 299 .setIcon(JOptionPane.INFORMATION_MESSAGE) 300 .show(); 301 } 302 }); 303 } 304 return null; 305 } else { 306 return pol; 307 } 308 } 309 310 /** 311 * Builds a relation from polygon ways. 312 * @param pol data storage class containing polygon information 313 * @param clone relation to clone, can be null 314 * @return multipolygon relation 315 */ 316 private static Relation createRelation(MultipolygonBuilder pol, Relation clone) { 317 // Create new relation 318 Relation rel = clone != null ? new Relation(clone) : new Relation(); 319 rel.put("type", "multipolygon"); 320 // Add ways to it 321 for (JoinedPolygon jway:pol.outerWays) { 322 addMembers(jway, rel, "outer"); 323 } 324 325 for (JoinedPolygon jway:pol.innerWays) { 326 addMembers(jway, rel, "inner"); 327 } 328 return rel; 329 } 330 331 private static void addMembers(JoinedPolygon polygon, Relation rel, String role) { 332 final int count = rel.getMembersCount(); 333 final Set<Way> ways = new HashSet<>(polygon.ways); 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 346 private static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "fence_type", "source"); 347 348 /** 349 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary 350 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core 351 * @param relation the multipolygon style relation to process 352 * @return a list of commands to execute 353 */ 354 public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) { 355 Map<String, String> values = new HashMap<>(relation.getKeys()); 356 357 List<Way> innerWays = new ArrayList<>(); 358 List<Way> outerWays = new ArrayList<>(); 359 360 Set<String> conflictingKeys = new TreeSet<>(); 361 362 for (RelationMember m : relation.getMembers()) { 363 364 if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) { 365 innerWays.add(m.getWay()); 366 } 367 368 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) { 369 Way way = m.getWay(); 370 outerWays.add(way); 371 372 for (String key : way.keySet()) { 373 if (!values.containsKey(key)) { //relation values take precedence 374 values.put(key, way.get(key)); 375 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) { 376 conflictingKeys.add(key); 377 } 378 } 379 } 380 } 381 382 // filter out empty key conflicts - we need second iteration 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)) { 388 conflictingKeys.add(key); 389 } 390 } 391 } 392 } 393 } 394 395 for (String key : conflictingKeys) { 396 values.remove(key); 397 } 398 399 for (String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS)) { 400 values.remove(linearTag); 401 } 402 403 if ("coastline".equals(values.get("natural"))) 404 values.remove("natural"); 405 406 values.put("area", "yes"); 407 408 List<Command> commands = new ArrayList<>(); 409 boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true); 410 411 for (Entry<String, String> entry : values.entrySet()) { 412 List<OsmPrimitive> affectedWays = new ArrayList<>(); 413 String key = entry.getKey(); 414 String value = entry.getValue(); 415 416 for (Way way : innerWays) { 417 if (value.equals(way.get(key))) { 418 affectedWays.add(way); 419 } 420 } 421 422 if (moveTags) { 423 // remove duplicated tags from outer ways 424 for (Way way : outerWays) { 425 if (way.hasKey(key)) { 426 affectedWays.add(way); 427 } 428 } 429 } 430 431 if (!affectedWays.isEmpty()) { 432 // reset key tag on affected ways 433 commands.add(new ChangePropertyCommand(affectedWays, key, null)); 434 } 435 } 436 437 if (moveTags) { 438 // add those tag values to the relation 439 boolean fixed = false; 440 Relation r2 = new Relation(relation); 441 for (Entry<String, String> entry : values.entrySet()) { 442 String key = entry.getKey(); 443 if (!r2.hasKey(key) && !"area".equals(key)) { 444 if (relation.isNew()) 445 relation.put(key, entry.getValue()); 446 else 447 r2.put(key, entry.getValue()); 448 fixed = true; 449 } 450 } 451 if (fixed && !relation.isNew()) 452 commands.add(new ChangeCommand(relation, r2)); 453 } 454 455 return commands; 456 } 457}