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.dialogs.relation.sort.RelationSorter; 041import org.openstreetmap.josm.gui.util.GuiHelper; 042import org.openstreetmap.josm.tools.Pair; 043import org.openstreetmap.josm.tools.Shortcut; 044import org.openstreetmap.josm.tools.Utils; 045 046/** 047 * Create multipolygon from selected ways automatically. 048 * 049 * New relation with type=multipolygon is created. 050 * 051 * If one or more of ways is already in relation with type=multipolygon or the 052 * way is not closed, then error is reported and no relation is created. 053 * 054 * The "inner" and "outer" roles are guessed automatically. First, bbox is 055 * calculated for each way. then the largest area is assumed to be outside and 056 * the rest inside. In cases with one "outside" area and several cut-ins, the 057 * guess should be always good ... In more complex (multiple outer areas) or 058 * buggy (inner and outer ways intersect) scenarios the result is likely to be 059 * wrong. 060 */ 061public class CreateMultipolygonAction extends JosmAction { 062 063 private final boolean update; 064 065 /** 066 * Constructs a new {@code CreateMultipolygonAction}. 067 * @param update {@code true} if the multipolygon must be updated, {@code false} if it must be created 068 */ 069 public CreateMultipolygonAction(final boolean update) { 070 super(getName(update), /* ICON */ "multipoly_create", getName(update), 071 /* atleast three lines for each shortcut or the server extractor fails */ 072 update ? Shortcut.registerShortcut("tools:multipoly_update", 073 tr("Tool: {0}", getName(true)), 074 KeyEvent.VK_B, Shortcut.CTRL_SHIFT) 075 : Shortcut.registerShortcut("tools:multipoly_create", 076 tr("Tool: {0}", getName(false)), 077 KeyEvent.VK_B, Shortcut.CTRL), 078 true, update ? "multipoly_update" : "multipoly_create", true); 079 this.update = update; 080 } 081 082 private static String getName(boolean update) { 083 return update ? tr("Update multipolygon") : tr("Create multipolygon"); 084 } 085 086 private static final class CreateUpdateMultipolygonTask implements Runnable { 087 private final Collection<Way> selectedWays; 088 private final Relation multipolygonRelation; 089 090 private CreateUpdateMultipolygonTask(Collection<Way> selectedWays, Relation multipolygonRelation) { 091 this.selectedWays = selectedWays; 092 this.multipolygonRelation = multipolygonRelation; 093 } 094 095 @Override 096 public void run() { 097 final Pair<SequenceCommand, Relation> commandAndRelation = createMultipolygonCommand(selectedWays, multipolygonRelation); 098 if (commandAndRelation == null) { 099 return; 100 } 101 final Command command = commandAndRelation.a; 102 final Relation relation = commandAndRelation.b; 103 104 // to avoid EDT violations 105 SwingUtilities.invokeLater(new Runnable() { 106 @Override 107 public void run() { 108 Main.main.undoRedo.add(command); 109 110 // Use 'SwingUtilities.invokeLater' to make sure the relationListDialog 111 // knows about the new relation before we try to select it. 112 // (Yes, we are already in event dispatch thread. But DatasetEventManager 113 // uses 'SwingUtilities.invokeLater' to fire events so we have to do the same.) 114 SwingUtilities.invokeLater(new Runnable() { 115 @Override 116 public void run() { 117 Main.map.relationListDialog.selectRelation(relation); 118 if (Main.pref.getBoolean("multipoly.show-relation-editor", false)) { 119 //Open relation edit window, if set up in preferences 120 RelationEditor editor = RelationEditor.getEditor(Main.main.getEditLayer(), relation, null); 121 122 editor.setModal(true); 123 editor.setVisible(true); 124 } else { 125 Main.main.getEditLayer().setRecentRelation(relation); 126 } 127 } 128 }); 129 } 130 }); 131 } 132 } 133 134 @Override 135 public void actionPerformed(ActionEvent e) { 136 if (!Main.main.hasEditLayer()) { 137 new Notification( 138 tr("No data loaded.")) 139 .setIcon(JOptionPane.WARNING_MESSAGE) 140 .setDuration(Notification.TIME_SHORT) 141 .show(); 142 return; 143 } 144 145 final Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays(); 146 147 if (selectedWays.isEmpty()) { 148 // Sometimes it make sense creating multipoly of only one way (so it will form outer way) 149 // and then splitting the way later (so there are multiple ways forming outer way) 150 new Notification( 151 tr("You must select at least one way.")) 152 .setIcon(JOptionPane.INFORMATION_MESSAGE) 153 .setDuration(Notification.TIME_SHORT) 154 .show(); 155 return; 156 } 157 158 final Collection<Relation> selectedRelations = Main.main.getCurrentDataSet().getSelectedRelations(); 159 final Relation multipolygonRelation = update 160 ? getSelectedMultipolygonRelation(selectedWays, selectedRelations) 161 : null; 162 163 // download incomplete relation or incomplete members if necessary 164 if (multipolygonRelation != null) { 165 if (!multipolygonRelation.isNew() && multipolygonRelation.isIncomplete()) { 166 Main.worker.submit(new DownloadRelationTask(Collections.singleton(multipolygonRelation), Main.main.getEditLayer())); 167 } else if (multipolygonRelation.hasIncompleteMembers()) { 168 Main.worker.submit(new DownloadRelationMemberTask(multipolygonRelation, 169 DownloadSelectedIncompleteMembersAction.buildSetOfIncompleteMembers(Collections.singleton(multipolygonRelation)), 170 Main.main.getEditLayer())); 171 } 172 } 173 // create/update multipolygon relation 174 Main.worker.submit(new CreateUpdateMultipolygonTask(selectedWays, multipolygonRelation)); 175 } 176 177 private static Relation getSelectedMultipolygonRelation() { 178 return getSelectedMultipolygonRelation(getCurrentDataSet().getSelectedWays(), getCurrentDataSet().getSelectedRelations()); 179 } 180 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 { 185 final Set<Relation> relatedRelations = new HashSet<>(); 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 193 /** 194 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}. 195 * @param selectedWays selected ways 196 * @param selectedMultipolygonRelation selected multipolygon relation 197 * @return pair of old and new multipolygon relation 198 */ 199 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) { 200 201 // add ways of existing relation to include them in polygon analysis 202 Set<Way> ways = new HashSet<>(selectedWays); 203 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class)); 204 205 final MultipolygonBuilder polygon = analyzeWays(ways, true); 206 if (polygon == null) { 207 return null; //could not make multipolygon. 208 } else { 209 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, selectedMultipolygonRelation)); 210 } 211 } 212 213 /** 214 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}. 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 218 */ 219 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) { 220 221 final MultipolygonBuilder polygon = analyzeWays(selectedWays, showNotif); 222 if (polygon == null) { 223 return null; //could not make multipolygon. 224 } else { 225 return Pair.create(null, createRelation(polygon, null)); 226 } 227 } 228 229 /** 230 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}. 231 * @param selectedWays selected ways 232 * @param selectedMultipolygonRelation selected multipolygon relation 233 * @return pair of command and multipolygon relation 234 */ 235 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, 236 Relation selectedMultipolygonRelation) { 237 238 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null 239 ? createMultipolygonRelation(selectedWays, true) 240 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation); 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) { 250 list.add(new AddCommand(relation)); 251 commandName = getName(false); 252 } else { 253 list.add(new ChangeCommand(existingRelation, relation)); 254 commandName = getName(true); 255 } 256 return Pair.create(new SequenceCommand(commandName, list), relation); 257 } 258 259 /** Enable this action only if something is selected */ 260 @Override 261 protected void updateEnabledState() { 262 if (getCurrentDataSet() == null) { 263 setEnabled(false); 264 } else { 265 updateEnabledState(getCurrentDataSet().getSelected()); 266 } 267 } 268 269 /** 270 * Enable this action only if something is selected 271 * 272 * @param selection the current selection, gets tested for emptyness 273 */ 274 @Override 275 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 276 if (getCurrentDataSet() == null) { 277 setEnabled(false); 278 } else if (update) { 279 setEnabled(getSelectedMultipolygonRelation() != null); 280 } else { 281 setEnabled(!getCurrentDataSet().getSelectedWays().isEmpty()); 282 } 283 } 284 285 /** 286 * This method analyzes ways and creates multipolygon. 287 * @param selectedWays list of selected ways 288 * @param showNotif if {@code true}, shows a notification if an error occurs 289 * @return <code>null</code>, if there was a problem with the ways. 290 */ 291 private static MultipolygonBuilder analyzeWays(Collection<Way> selectedWays, boolean showNotif) { 292 293 MultipolygonBuilder pol = new MultipolygonBuilder(); 294 final String error = pol.makeFromWays(selectedWays); 295 296 if (error != null) { 297 if (showNotif) { 298 GuiHelper.runInEDT(new Runnable() { 299 @Override 300 public void run() { 301 new Notification(error) 302 .setIcon(JOptionPane.INFORMATION_MESSAGE) 303 .show(); 304 } 305 }); 306 } 307 return null; 308 } else { 309 return pol; 310 } 311 } 312 313 /** 314 * Builds a relation from polygon ways. 315 * @param pol data storage class containing polygon information 316 * @param clone relation to clone, can be null 317 * @return multipolygon relation 318 */ 319 private static Relation createRelation(MultipolygonBuilder pol, Relation clone) { 320 // Create new relation 321 Relation rel = clone != null ? new Relation(clone) : new Relation(); 322 rel.put("type", "multipolygon"); 323 // Add ways to it 324 for (JoinedPolygon jway:pol.outerWays) { 325 addMembers(jway, rel, "outer"); 326 } 327 328 for (JoinedPolygon jway:pol.innerWays) { 329 addMembers(jway, rel, "inner"); 330 } 331 332 if (clone == null) { 333 rel.setMembers(RelationSorter.sortMembersByConnectivity(rel.getMembers())); 334 } 335 336 return rel; 337 } 338 339 private static void addMembers(JoinedPolygon polygon, Relation rel, String role) { 340 final int count = rel.getMembersCount(); 341 final Set<Way> ways = new HashSet<>(polygon.ways); 342 for (int i = 0; i < count; i++) { 343 final RelationMember m = rel.getMember(i); 344 if (ways.contains(m.getMember()) && !role.equals(m.getRole())) { 345 rel.setMember(i, new RelationMember(role, m.getMember())); 346 } 347 } 348 ways.removeAll(rel.getMemberPrimitives()); 349 for (final Way way : ways) { 350 rel.addMember(new RelationMember(role, way)); 351 } 352 } 353 354 private static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "fence_type", "source"); 355 356 /** 357 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary 358 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core 359 * @param relation the multipolygon style relation to process 360 * @return a list of commands to execute 361 */ 362 public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) { 363 Map<String, String> values = new HashMap<>(relation.getKeys()); 364 365 List<Way> innerWays = new ArrayList<>(); 366 List<Way> outerWays = new ArrayList<>(); 367 368 Set<String> conflictingKeys = new TreeSet<>(); 369 370 for (RelationMember m : relation.getMembers()) { 371 372 if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) { 373 innerWays.add(m.getWay()); 374 } 375 376 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) { 377 Way way = m.getWay(); 378 outerWays.add(way); 379 380 for (String key : way.keySet()) { 381 if (!values.containsKey(key)) { //relation values take precedence 382 values.put(key, way.get(key)); 383 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) { 384 conflictingKeys.add(key); 385 } 386 } 387 } 388 } 389 390 // filter out empty key conflicts - we need second iteration 391 if (!Main.pref.getBoolean("multipoly.alltags", false)) { 392 for (RelationMember m : relation.getMembers()) { 393 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) { 394 for (String key : values.keySet()) { 395 if (!m.getWay().hasKey(key) && !relation.hasKey(key)) { 396 conflictingKeys.add(key); 397 } 398 } 399 } 400 } 401 } 402 403 for (String key : conflictingKeys) { 404 values.remove(key); 405 } 406 407 for (String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS)) { 408 values.remove(linearTag); 409 } 410 411 if ("coastline".equals(values.get("natural"))) 412 values.remove("natural"); 413 414 values.put("area", "yes"); 415 416 List<Command> commands = new ArrayList<>(); 417 boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true); 418 419 for (Entry<String, String> entry : values.entrySet()) { 420 List<OsmPrimitive> affectedWays = new ArrayList<>(); 421 String key = entry.getKey(); 422 String value = entry.getValue(); 423 424 for (Way way : innerWays) { 425 if (value.equals(way.get(key))) { 426 affectedWays.add(way); 427 } 428 } 429 430 if (moveTags) { 431 // remove duplicated tags from outer ways 432 for (Way way : outerWays) { 433 if (way.hasKey(key)) { 434 affectedWays.add(way); 435 } 436 } 437 } 438 439 if (!affectedWays.isEmpty()) { 440 // reset key tag on affected ways 441 commands.add(new ChangePropertyCommand(affectedWays, key, null)); 442 } 443 } 444 445 if (moveTags) { 446 // add those tag values to the relation 447 boolean fixed = false; 448 Relation r2 = new Relation(relation); 449 for (Entry<String, String> entry : values.entrySet()) { 450 String key = entry.getKey(); 451 if (!r2.hasKey(key) && !"area".equals(key)) { 452 if (relation.isNew()) 453 relation.put(key, entry.getValue()); 454 else 455 r2.put(key, entry.getValue()); 456 fixed = true; 457 } 458 } 459 if (fixed && !relation.isNew()) 460 commands.add(new ChangeCommand(relation, r2)); 461 } 462 463 return commands; 464 } 465}