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 } else { 124 Main.main.getEditLayer().setRecentRelation(relation); 125 } 126 } 127 }); 128 } 129 }); 130 } 131 } 132 133 @Override 134 public void actionPerformed(ActionEvent e) { 135 if (!Main.main.hasEditLayer()) { 136 new Notification( 137 tr("No data loaded.")) 138 .setIcon(JOptionPane.WARNING_MESSAGE) 139 .setDuration(Notification.TIME_SHORT) 140 .show(); 141 return; 142 } 143 144 final Collection<Way> selectedWays = Main.main.getCurrentDataSet().getSelectedWays(); 145 146 if (selectedWays.isEmpty()) { 147 // Sometimes it make sense creating multipoly of only one way (so it will form outer way) 148 // and then splitting the way later (so there are multiple ways forming outer way) 149 new Notification( 150 tr("You must select at least one way.")) 151 .setIcon(JOptionPane.INFORMATION_MESSAGE) 152 .setDuration(Notification.TIME_SHORT) 153 .show(); 154 return; 155 } 156 157 final Collection<Relation> selectedRelations = Main.main.getCurrentDataSet().getSelectedRelations(); 158 final Relation multipolygonRelation = update 159 ? getSelectedMultipolygonRelation(selectedWays, selectedRelations) 160 : null; 161 162 // download incomplete relation or incomplete members if necessary 163 if (multipolygonRelation != null) { 164 if (!multipolygonRelation.isNew() && multipolygonRelation.isIncomplete()) { 165 Main.worker.submit(new DownloadRelationTask(Collections.singleton(multipolygonRelation), Main.main.getEditLayer())); 166 } else if (multipolygonRelation.hasIncompleteMembers()) { 167 Main.worker.submit(new DownloadRelationMemberTask(multipolygonRelation, 168 DownloadSelectedIncompleteMembersAction.buildSetOfIncompleteMembers(Collections.singleton(multipolygonRelation)), 169 Main.main.getEditLayer())); 170 } 171 } 172 // create/update multipolygon relation 173 Main.worker.submit(new CreateUpdateMultipolygonTask(selectedWays, multipolygonRelation)); 174 } 175 176 private static Relation getSelectedMultipolygonRelation() { 177 return getSelectedMultipolygonRelation(getCurrentDataSet().getSelectedWays(), getCurrentDataSet().getSelectedRelations()); 178 } 179 180 private static Relation getSelectedMultipolygonRelation(Collection<Way> selectedWays, Collection<Relation> selectedRelations) { 181 if (selectedRelations.size() == 1 && "multipolygon".equals(selectedRelations.iterator().next().get("type"))) { 182 return selectedRelations.iterator().next(); 183 } else { 184 final Set<Relation> relatedRelations = new HashSet<>(); 185 for (final Way w : selectedWays) { 186 relatedRelations.addAll(Utils.filteredCollection(w.getReferrers(), Relation.class)); 187 } 188 return relatedRelations.size() == 1 ? relatedRelations.iterator().next() : null; 189 } 190 } 191 192 /** 193 * Returns a {@link Pair} of the old multipolygon {@link Relation} (or null) and the newly created/modified multipolygon {@link Relation}. 194 * @param selectedWays selected ways 195 * @param selectedMultipolygonRelation selected multipolygon relation 196 * @return pair of old and new multipolygon relation 197 */ 198 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) { 199 200 // add ways of existing relation to include them in polygon analysis 201 Set<Way> ways = new HashSet<>(selectedWays); 202 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class)); 203 204 final MultipolygonBuilder polygon = analyzeWays(ways, true); 205 if (polygon == null) { 206 return null; //could not make multipolygon. 207 } else { 208 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, selectedMultipolygonRelation)); 209 } 210 } 211 212 /** 213 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}. 214 * @param selectedWays selected ways 215 * @param showNotif if {@code true}, shows a notification if an error occurs 216 * @return pair of null and new multipolygon relation 217 */ 218 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) { 219 220 final MultipolygonBuilder polygon = analyzeWays(selectedWays, showNotif); 221 if (polygon == null) { 222 return null; //could not make multipolygon. 223 } else { 224 return Pair.create(null, createRelation(polygon, null)); 225 } 226 } 227 228 /** 229 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}. 230 * @param selectedWays selected ways 231 * @param selectedMultipolygonRelation selected multipolygon relation 232 * @return pair of command and multipolygon relation 233 */ 234 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, 235 Relation selectedMultipolygonRelation) { 236 237 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null 238 ? createMultipolygonRelation(selectedWays, true) 239 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation); 240 if (rr == null) { 241 return null; 242 } 243 final Relation existingRelation = rr.a; 244 final Relation relation = rr.b; 245 246 final List<Command> list = removeTagsFromWaysIfNeeded(relation); 247 final String commandName; 248 if (existingRelation == null) { 249 list.add(new AddCommand(relation)); 250 commandName = getName(false); 251 } else { 252 list.add(new ChangeCommand(existingRelation, relation)); 253 commandName = getName(true); 254 } 255 return Pair.create(new SequenceCommand(commandName, list), relation); 256 } 257 258 /** Enable this action only if something is selected */ 259 @Override 260 protected void updateEnabledState() { 261 if (getCurrentDataSet() == null) { 262 setEnabled(false); 263 } else { 264 updateEnabledState(getCurrentDataSet().getSelected()); 265 } 266 } 267 268 /** 269 * Enable this action only if something is selected 270 * 271 * @param selection the current selection, gets tested for emptyness 272 */ 273 @Override 274 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 275 if (getCurrentDataSet() == null) { 276 setEnabled(false); 277 } else if (update) { 278 setEnabled(getSelectedMultipolygonRelation() != null); 279 } else { 280 setEnabled(!getCurrentDataSet().getSelectedWays().isEmpty()); 281 } 282 } 283 284 /** 285 * This method analyzes ways and creates multipolygon. 286 * @param selectedWays list of selected ways 287 * @param showNotif if {@code true}, shows a notification if an error occurs 288 * @return <code>null</code>, if there was a problem with the ways. 289 */ 290 private static MultipolygonBuilder analyzeWays(Collection<Way> selectedWays, boolean showNotif) { 291 292 MultipolygonBuilder pol = new MultipolygonBuilder(); 293 final String error = pol.makeFromWays(selectedWays); 294 295 if (error != null) { 296 if (showNotif) { 297 GuiHelper.runInEDT(new Runnable() { 298 @Override 299 public void run() { 300 new Notification(error) 301 .setIcon(JOptionPane.INFORMATION_MESSAGE) 302 .show(); 303 } 304 }); 305 } 306 return null; 307 } else { 308 return pol; 309 } 310 } 311 312 /** 313 * Builds a relation from polygon ways. 314 * @param pol data storage class containing polygon information 315 * @param clone relation to clone, can be null 316 * @return multipolygon relation 317 */ 318 private static Relation createRelation(MultipolygonBuilder pol, Relation clone) { 319 // Create new relation 320 Relation rel = clone != null ? new Relation(clone) : new Relation(); 321 rel.put("type", "multipolygon"); 322 // Add ways to it 323 for (JoinedPolygon jway:pol.outerWays) { 324 addMembers(jway, rel, "outer"); 325 } 326 327 for (JoinedPolygon jway:pol.innerWays) { 328 addMembers(jway, rel, "inner"); 329 } 330 return rel; 331 } 332 333 private static void addMembers(JoinedPolygon polygon, Relation rel, String role) { 334 final int count = rel.getMembersCount(); 335 final Set<Way> ways = new HashSet<>(polygon.ways); 336 for (int i = 0; i < count; i++) { 337 final RelationMember m = rel.getMember(i); 338 if (ways.contains(m.getMember()) && !role.equals(m.getRole())) { 339 rel.setMember(i, new RelationMember(role, m.getMember())); 340 } 341 } 342 ways.removeAll(rel.getMemberPrimitives()); 343 for (final Way way : ways) { 344 rel.addMember(new RelationMember(role, way)); 345 } 346 } 347 348 private static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "fence_type", "source"); 349 350 /** 351 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary 352 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core 353 * @param relation the multipolygon style relation to process 354 * @return a list of commands to execute 355 */ 356 public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) { 357 Map<String, String> values = new HashMap<>(relation.getKeys()); 358 359 List<Way> innerWays = new ArrayList<>(); 360 List<Way> outerWays = new ArrayList<>(); 361 362 Set<String> conflictingKeys = new TreeSet<>(); 363 364 for (RelationMember m : relation.getMembers()) { 365 366 if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) { 367 innerWays.add(m.getWay()); 368 } 369 370 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) { 371 Way way = m.getWay(); 372 outerWays.add(way); 373 374 for (String key : way.keySet()) { 375 if (!values.containsKey(key)) { //relation values take precedence 376 values.put(key, way.get(key)); 377 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) { 378 conflictingKeys.add(key); 379 } 380 } 381 } 382 } 383 384 // filter out empty key conflicts - we need second iteration 385 if (!Main.pref.getBoolean("multipoly.alltags", false)) { 386 for (RelationMember m : relation.getMembers()) { 387 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) { 388 for (String key : values.keySet()) { 389 if (!m.getWay().hasKey(key) && !relation.hasKey(key)) { 390 conflictingKeys.add(key); 391 } 392 } 393 } 394 } 395 } 396 397 for (String key : conflictingKeys) { 398 values.remove(key); 399 } 400 401 for (String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS)) { 402 values.remove(linearTag); 403 } 404 405 if ("coastline".equals(values.get("natural"))) 406 values.remove("natural"); 407 408 values.put("area", "yes"); 409 410 List<Command> commands = new ArrayList<>(); 411 boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true); 412 413 for (Entry<String, String> entry : values.entrySet()) { 414 List<OsmPrimitive> affectedWays = new ArrayList<>(); 415 String key = entry.getKey(); 416 String value = entry.getValue(); 417 418 for (Way way : innerWays) { 419 if (value.equals(way.get(key))) { 420 affectedWays.add(way); 421 } 422 } 423 424 if (moveTags) { 425 // remove duplicated tags from outer ways 426 for (Way way : outerWays) { 427 if (way.hasKey(key)) { 428 affectedWays.add(way); 429 } 430 } 431 } 432 433 if (!affectedWays.isEmpty()) { 434 // reset key tag on affected ways 435 commands.add(new ChangePropertyCommand(affectedWays, key, null)); 436 } 437 } 438 439 if (moveTags) { 440 // add those tag values to the relation 441 boolean fixed = false; 442 Relation r2 = new Relation(relation); 443 for (Entry<String, String> entry : values.entrySet()) { 444 String key = entry.getKey(); 445 if (!r2.hasKey(key) && !"area".equals(key)) { 446 if (relation.isNew()) 447 relation.put(key, entry.getValue()); 448 else 449 r2.put(key, entry.getValue()); 450 fixed = true; 451 } 452 } 453 if (fixed && !relation.isNew()) 454 commands.add(new ChangeCommand(relation, r2)); 455 } 456 457 return commands; 458 } 459}