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 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 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 */ 193 public static Pair<Relation, Relation> updateMultipolygonRelation(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) { 194 195 // add ways of existing relation to include them in polygon analysis 196 Set<Way> ways = new HashSet<>(selectedWays); 197 ways.addAll(selectedMultipolygonRelation.getMemberPrimitives(Way.class)); 198 199 final MultipolygonBuilder polygon = analyzeWays(ways, true); 200 if (polygon == null) { 201 return null; //could not make multipolygon. 202 } else { 203 return Pair.create(selectedMultipolygonRelation, createRelation(polygon, new Relation(selectedMultipolygonRelation))); 204 } 205 } 206 207 /** 208 * Returns a {@link Pair} null and the newly created/modified multipolygon {@link Relation}. 209 */ 210 public static Pair<Relation, Relation> createMultipolygonRelation(Collection<Way> selectedWays, boolean showNotif) { 211 212 final MultipolygonBuilder polygon = analyzeWays(selectedWays, showNotif); 213 if (polygon == null) { 214 return null; //could not make multipolygon. 215 } else { 216 return Pair.create(null, createRelation(polygon, new Relation())); 217 } 218 } 219 220 /** 221 * Returns a {@link Pair} of a multipolygon creating/modifying {@link Command} as well as the multipolygon {@link Relation}. 222 */ 223 public static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> selectedWays, Relation selectedMultipolygonRelation) { 224 225 final Pair<Relation, Relation> rr = selectedMultipolygonRelation == null 226 ? createMultipolygonRelation(selectedWays, true) 227 : updateMultipolygonRelation(selectedWays, selectedMultipolygonRelation); 228 if (rr == null) { 229 return null; 230 } 231 final Relation existingRelation = rr.a; 232 final Relation relation = rr.b; 233 234 final List<Command> list = removeTagsFromWaysIfNeeded(relation); 235 final String commandName; 236 if (existingRelation == null) { 237 list.add(new AddCommand(relation)); 238 commandName = getName(false); 239 } else { 240 list.add(new ChangeCommand(existingRelation, relation)); 241 commandName = getName(true); 242 } 243 return Pair.create(new SequenceCommand(commandName, list), relation); 244 } 245 246 /** Enable this action only if something is selected */ 247 @Override 248 protected void updateEnabledState() { 249 if (getCurrentDataSet() == null) { 250 setEnabled(false); 251 } else { 252 updateEnabledState(getCurrentDataSet().getSelected()); 253 } 254 } 255 256 /** 257 * Enable this action only if something is selected 258 * 259 * @param selection the current selection, gets tested for emptyness 260 */ 261 @Override 262 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 263 if (update) { 264 setEnabled(getSelectedMultipolygonRelation() != null); 265 } else { 266 setEnabled(!getCurrentDataSet().getSelectedWays().isEmpty()); 267 } 268 } 269 270 /** 271 * This method analyzes ways and creates multipolygon. 272 * @param selectedWays list of selected ways 273 * @return <code>null</code>, if there was a problem with the ways. 274 */ 275 private static MultipolygonBuilder analyzeWays(Collection<Way> selectedWays, boolean showNotif) { 276 277 MultipolygonBuilder pol = new MultipolygonBuilder(); 278 final String error = pol.makeFromWays(selectedWays); 279 280 if (error != null) { 281 if (showNotif) { 282 GuiHelper.runInEDT(new Runnable() { 283 @Override 284 public void run() { 285 new Notification(error) 286 .setIcon(JOptionPane.INFORMATION_MESSAGE) 287 .show(); 288 } 289 }); 290 } 291 return null; 292 } else { 293 return pol; 294 } 295 } 296 297 /** 298 * Builds a relation from polygon ways. 299 * @param pol data storage class containing polygon information 300 * @return multipolygon relation 301 */ 302 private static Relation createRelation(MultipolygonBuilder pol, final Relation rel) { 303 // Create new relation 304 rel.put("type", "multipolygon"); 305 // Add ways to it 306 for (JoinedPolygon jway:pol.outerWays) { 307 addMembers(jway, rel, "outer"); 308 } 309 310 for (JoinedPolygon jway:pol.innerWays) { 311 addMembers(jway, rel, "inner"); 312 } 313 return rel; 314 } 315 316 private static void addMembers(JoinedPolygon polygon, Relation rel, String role) { 317 final int count = rel.getMembersCount(); 318 final Set<Way> ways = new HashSet<>(polygon.ways); 319 for (int i = 0; i < count; i++) { 320 final RelationMember m = rel.getMember(i); 321 if (ways.contains(m.getMember()) && !role.equals(m.getRole())) { 322 rel.setMember(i, new RelationMember(role, m.getMember())); 323 } 324 } 325 ways.removeAll(rel.getMemberPrimitives()); 326 for (final Way way : ways) { 327 rel.addMember(new RelationMember(role, way)); 328 } 329 } 330 331 public static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "fence_type", "source"); 332 333 /** 334 * This method removes tags/value pairs from inner and outer ways and put them on relation if necessary 335 * Function was extended in reltoolbox plugin by Zverikk and copied back to the core 336 * @param relation the multipolygon style relation to process 337 * @return a list of commands to execute 338 */ 339 public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) { 340 Map<String, String> values = new HashMap<>(relation.getKeys()); 341 342 List<Way> innerWays = new ArrayList<>(); 343 List<Way> outerWays = new ArrayList<>(); 344 345 Set<String> conflictingKeys = new TreeSet<>(); 346 347 for( RelationMember m : relation.getMembers() ) { 348 349 if( m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) { 350 innerWays.add(m.getWay()); 351 } 352 353 if( m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys() ) { 354 Way way = m.getWay(); 355 outerWays.add(way); 356 357 for (String key : way.keySet()) { 358 if (!values.containsKey(key)) { //relation values take precedence 359 values.put(key, way.get(key)); 360 } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) { 361 conflictingKeys.add(key); 362 } 363 } 364 } 365 } 366 367 // filter out empty key conflicts - we need second iteration 368 if (!Main.pref.getBoolean("multipoly.alltags", false)) 369 for (RelationMember m : relation.getMembers()) 370 if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) 371 for (String key : values.keySet()) 372 if (!m.getWay().hasKey(key) && !relation.hasKey(key)) 373 conflictingKeys.add(key); 374 375 for (String key : conflictingKeys) 376 values.remove(key); 377 378 for (String linearTag : Main.pref.getCollection("multipoly.lineartagstokeep", DEFAULT_LINEAR_TAGS)) 379 values.remove(linearTag); 380 381 if ("coastline".equals(values.get("natural"))) 382 values.remove("natural"); 383 384 values.put("area", "yes"); 385 386 List<Command> commands = new ArrayList<>(); 387 boolean moveTags = Main.pref.getBoolean("multipoly.movetags", true); 388 389 for (Entry<String, String> entry : values.entrySet()) { 390 List<OsmPrimitive> affectedWays = new ArrayList<>(); 391 String key = entry.getKey(); 392 String value = entry.getValue(); 393 394 for (Way way : innerWays) { 395 if (value.equals(way.get(key))) { 396 affectedWays.add(way); 397 } 398 } 399 400 if (moveTags) { 401 // remove duplicated tags from outer ways 402 for( Way way : outerWays ) { 403 if( way.hasKey(key) ) { 404 affectedWays.add(way); 405 } 406 } 407 } 408 409 if (!affectedWays.isEmpty()) { 410 // reset key tag on affected ways 411 commands.add(new ChangePropertyCommand(affectedWays, key, null)); 412 } 413 } 414 415 if (moveTags) { 416 // add those tag values to the relation 417 boolean fixed = false; 418 Relation r2 = new Relation(relation); 419 for (Entry<String, String> entry : values.entrySet()) { 420 String key = entry.getKey(); 421 if (!r2.hasKey(key) && !"area".equals(key) ) { 422 if (relation.isNew()) 423 relation.put(key, entry.getValue()); 424 else 425 r2.put(key, entry.getValue()); 426 fixed = true; 427 } 428 } 429 if (fixed && !relation.isNew()) 430 commands.add(new ChangeCommand(relation, r2)); 431 } 432 433 return commands; 434 } 435}