001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.command; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.GridBagLayout; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.EnumSet; 014import java.util.HashMap; 015import java.util.HashSet; 016import java.util.LinkedList; 017import java.util.List; 018import java.util.Map; 019import java.util.Map.Entry; 020import java.util.Objects; 021import java.util.Set; 022 023import javax.swing.Icon; 024import javax.swing.JOptionPane; 025import javax.swing.JPanel; 026 027import org.openstreetmap.josm.Main; 028import org.openstreetmap.josm.actions.SplitWayAction; 029import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; 030import org.openstreetmap.josm.data.osm.DataSet; 031import org.openstreetmap.josm.data.osm.Node; 032import org.openstreetmap.josm.data.osm.OsmPrimitive; 033import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 034import org.openstreetmap.josm.data.osm.PrimitiveData; 035import org.openstreetmap.josm.data.osm.Relation; 036import org.openstreetmap.josm.data.osm.RelationToChildReference; 037import org.openstreetmap.josm.data.osm.Way; 038import org.openstreetmap.josm.data.osm.WaySegment; 039import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 040import org.openstreetmap.josm.gui.DefaultNameFormatter; 041import org.openstreetmap.josm.gui.dialogs.DeleteFromRelationConfirmationDialog; 042import org.openstreetmap.josm.gui.layer.OsmDataLayer; 043import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 044import org.openstreetmap.josm.tools.CheckParameterUtil; 045import org.openstreetmap.josm.tools.ImageProvider; 046import org.openstreetmap.josm.tools.Utils; 047 048/** 049 * A command to delete a number of primitives from the dataset. 050 * @since 23 051 */ 052public class DeleteCommand extends Command { 053 private static final class DeleteChildCommand implements PseudoCommand { 054 private final OsmPrimitive osm; 055 056 private DeleteChildCommand(OsmPrimitive osm) { 057 this.osm = osm; 058 } 059 060 @Override 061 public String getDescriptionText() { 062 return tr("Deleted ''{0}''", osm.getDisplayName(DefaultNameFormatter.getInstance())); 063 } 064 065 @Override 066 public Icon getDescriptionIcon() { 067 return ImageProvider.get(osm.getDisplayType()); 068 } 069 070 @Override 071 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 072 return Collections.singleton(osm); 073 } 074 075 @Override 076 public String toString() { 077 return "DeleteChildCommand [osm=" + osm + "]"; 078 } 079 } 080 081 /** 082 * The primitives that get deleted. 083 */ 084 private final Collection<? extends OsmPrimitive> toDelete; 085 private final Map<OsmPrimitive, PrimitiveData> clonedPrimitives = new HashMap<>(); 086 087 /** 088 * Constructor. Deletes a collection of primitives in the current edit layer. 089 * 090 * @param data the primitives to delete. Must neither be null nor empty. 091 * @throws IllegalArgumentException if data is null or empty 092 */ 093 public DeleteCommand(Collection<? extends OsmPrimitive> data) { 094 CheckParameterUtil.ensureParameterNotNull(data, "data"); 095 this.toDelete = data; 096 checkConsistency(); 097 } 098 099 /** 100 * Constructor. Deletes a single primitive in the current edit layer. 101 * 102 * @param data the primitive to delete. Must not be null. 103 * @throws IllegalArgumentException if data is null 104 */ 105 public DeleteCommand(OsmPrimitive data) { 106 this(Collections.singleton(data)); 107 } 108 109 /** 110 * Constructor for a single data item. Use the collection constructor to delete multiple 111 * objects. 112 * 113 * @param layer the layer context for deleting this primitive. Must not be null. 114 * @param data the primitive to delete. Must not be null. 115 * @throws IllegalArgumentException if data is null 116 * @throws IllegalArgumentException if layer is null 117 */ 118 public DeleteCommand(OsmDataLayer layer, OsmPrimitive data) { 119 this(layer, Collections.singleton(data)); 120 } 121 122 /** 123 * Constructor for a collection of data to be deleted in the context of 124 * a specific layer 125 * 126 * @param layer the layer context for deleting these primitives. Must not be null. 127 * @param data the primitives to delete. Must neither be null nor empty. 128 * @throws IllegalArgumentException if layer is null 129 * @throws IllegalArgumentException if data is null or empty 130 */ 131 public DeleteCommand(OsmDataLayer layer, Collection<? extends OsmPrimitive> data) { 132 super(layer); 133 CheckParameterUtil.ensureParameterNotNull(data, "data"); 134 this.toDelete = data; 135 checkConsistency(); 136 } 137 138 /** 139 * Constructor for a collection of data to be deleted in the context of 140 * a specific data set 141 * 142 * @param dataset the dataset context for deleting these primitives. Must not be null. 143 * @param data the primitives to delete. Must neither be null nor empty. 144 * @throws IllegalArgumentException if dataset is null 145 * @throws IllegalArgumentException if data is null or empty 146 * @since 11240 147 */ 148 public DeleteCommand(DataSet dataset, Collection<? extends OsmPrimitive> data) { 149 super(dataset); 150 CheckParameterUtil.ensureParameterNotNull(data, "data"); 151 this.toDelete = data; 152 checkConsistency(); 153 } 154 155 private void checkConsistency() { 156 if (toDelete.isEmpty()) { 157 throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection")); 158 } 159 for (OsmPrimitive p : toDelete) { 160 if (p == null) { 161 throw new IllegalArgumentException("Primitive to delete must not be null"); 162 } else if (p.getDataSet() == null) { 163 throw new IllegalArgumentException("Primitive to delete must be in a dataset"); 164 } 165 } 166 } 167 168 @Override 169 public boolean executeCommand() { 170 // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed) 171 for (OsmPrimitive osm: toDelete) { 172 if (osm.isDeleted()) 173 throw new IllegalArgumentException(osm + " is already deleted"); 174 clonedPrimitives.put(osm, osm.save()); 175 176 if (osm instanceof Way) { 177 ((Way) osm).setNodes(null); 178 } else if (osm instanceof Relation) { 179 ((Relation) osm).setMembers(null); 180 } 181 } 182 183 for (OsmPrimitive osm: toDelete) { 184 osm.setDeleted(true); 185 } 186 187 return true; 188 } 189 190 @Override 191 public void undoCommand() { 192 for (OsmPrimitive osm: toDelete) { 193 osm.setDeleted(false); 194 } 195 196 for (Entry<OsmPrimitive, PrimitiveData> entry: clonedPrimitives.entrySet()) { 197 entry.getKey().load(entry.getValue()); 198 } 199 } 200 201 @Override 202 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 203 // Do nothing 204 } 205 206 private EnumSet<OsmPrimitiveType> getTypesToDelete() { 207 EnumSet<OsmPrimitiveType> typesToDelete = EnumSet.noneOf(OsmPrimitiveType.class); 208 for (OsmPrimitive osm : toDelete) { 209 typesToDelete.add(OsmPrimitiveType.from(osm)); 210 } 211 return typesToDelete; 212 } 213 214 @Override 215 public String getDescriptionText() { 216 if (toDelete.size() == 1) { 217 OsmPrimitive primitive = toDelete.iterator().next(); 218 String msg; 219 switch(OsmPrimitiveType.from(primitive)) { 220 case NODE: msg = marktr("Delete node {0}"); break; 221 case WAY: msg = marktr("Delete way {0}"); break; 222 case RELATION:msg = marktr("Delete relation {0}"); break; 223 default: throw new AssertionError(); 224 } 225 226 return tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance())); 227 } else { 228 Set<OsmPrimitiveType> typesToDelete = getTypesToDelete(); 229 String msg; 230 if (typesToDelete.size() > 1) { 231 msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size()); 232 } else { 233 OsmPrimitiveType t = typesToDelete.iterator().next(); 234 switch(t) { 235 case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break; 236 case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break; 237 case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break; 238 default: throw new AssertionError(); 239 } 240 } 241 return msg; 242 } 243 } 244 245 @Override 246 public Icon getDescriptionIcon() { 247 if (toDelete.size() == 1) 248 return ImageProvider.get(toDelete.iterator().next().getDisplayType()); 249 Set<OsmPrimitiveType> typesToDelete = getTypesToDelete(); 250 if (typesToDelete.size() > 1) 251 return ImageProvider.get("data", "object"); 252 else 253 return ImageProvider.get(typesToDelete.iterator().next()); 254 } 255 256 @Override public Collection<PseudoCommand> getChildren() { 257 if (toDelete.size() == 1) 258 return null; 259 else { 260 List<PseudoCommand> children = new ArrayList<>(toDelete.size()); 261 for (final OsmPrimitive osm : toDelete) { 262 children.add(new DeleteChildCommand(osm)); 263 } 264 return children; 265 266 } 267 } 268 269 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 270 return toDelete; 271 } 272 273 /** 274 * Delete the primitives and everything they reference. 275 * 276 * If a node is deleted, the node and all ways and relations the node is part of are deleted as well. 277 * If a way is deleted, all relations the way is member of are also deleted. 278 * If a way is deleted, only the way and no nodes are deleted. 279 * 280 * @param layer the {@link OsmDataLayer} in whose context primitives are deleted. Must not be null. 281 * @param selection The list of all object to be deleted. 282 * @param silent Set to true if the user should not be bugged with additional dialogs 283 * @return command A command to perform the deletions, or null of there is nothing to delete. 284 * @throws IllegalArgumentException if layer is null 285 */ 286 public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, boolean silent) { 287 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 288 if (selection == null || selection.isEmpty()) return null; 289 Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection); 290 parents.addAll(selection); 291 292 if (parents.isEmpty()) 293 return null; 294 if (!silent && !checkAndConfirmOutlyingDelete(parents, null)) 295 return null; 296 return new DeleteCommand(layer, parents); 297 } 298 299 /** 300 * Delete the primitives and everything they reference. 301 * 302 * If a node is deleted, the node and all ways and relations the node is part of are deleted as well. 303 * If a way is deleted, all relations the way is member of are also deleted. 304 * If a way is deleted, only the way and no nodes are deleted. 305 * 306 * @param layer the {@link OsmDataLayer} in whose context primitives are deleted. Must not be null. 307 * @param selection The list of all object to be deleted. 308 * @return command A command to perform the deletions, or null of there is nothing to delete. 309 * @throws IllegalArgumentException if layer is null 310 */ 311 public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) { 312 return deleteWithReferences(layer, selection, false); 313 } 314 315 /** 316 * Try to delete all given primitives. 317 * 318 * If a node is used by a way, it's removed from that way. If a node or a way is used by a 319 * relation, inform the user and do not delete. 320 * 321 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If 322 * they are part of a relation, inform the user and do not delete. 323 * 324 * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted 325 * @param selection the objects to delete. 326 * @return command a command to perform the deletions, or null if there is nothing to delete. 327 */ 328 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) { 329 return delete(layer, selection, true, false); 330 } 331 332 /** 333 * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which 334 * can be deleted too. A node can be deleted if 335 * <ul> 336 * <li>it is untagged (see {@link Node#isTagged()}</li> 337 * <li>it is not referred to by other non-deleted primitives outside of <code>primitivesToDelete</code></li> 338 * </ul> 339 * @param primitivesToDelete the primitives to delete 340 * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which 341 * can be deleted too 342 */ 343 protected static Collection<Node> computeNodesToDelete(Collection<OsmPrimitive> primitivesToDelete) { 344 Collection<Node> nodesToDelete = new HashSet<>(); 345 for (Way way : OsmPrimitive.getFilteredList(primitivesToDelete, Way.class)) { 346 for (Node n : way.getNodes()) { 347 if (n.isTagged()) { 348 continue; 349 } 350 Collection<OsmPrimitive> referringPrimitives = n.getReferrers(); 351 referringPrimitives.removeAll(primitivesToDelete); 352 int count = 0; 353 for (OsmPrimitive p : referringPrimitives) { 354 if (!p.isDeleted()) { 355 count++; 356 } 357 } 358 if (count == 0) { 359 nodesToDelete.add(n); 360 } 361 } 362 } 363 return nodesToDelete; 364 } 365 366 /** 367 * Try to delete all given primitives. 368 * 369 * If a node is used by a way, it's removed from that way. If a node or a way is used by a 370 * relation, inform the user and do not delete. 371 * 372 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If 373 * they are part of a relation, inform the user and do not delete. 374 * 375 * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted 376 * @param selection the objects to delete. 377 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well 378 * @return command a command to perform the deletions, or null if there is nothing to delete. 379 */ 380 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, 381 boolean alsoDeleteNodesInWay) { 382 return delete(layer, selection, alsoDeleteNodesInWay, false /* not silent */); 383 } 384 385 /** 386 * Try to delete all given primitives. 387 * 388 * If a node is used by a way, it's removed from that way. If a node or a way is used by a 389 * relation, inform the user and do not delete. 390 * 391 * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If 392 * they are part of a relation, inform the user and do not delete. 393 * 394 * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted 395 * @param selection the objects to delete. 396 * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well 397 * @param silent set to true if the user should not be bugged with additional questions 398 * @return command a command to perform the deletions, or null if there is nothing to delete. 399 */ 400 public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, 401 boolean alsoDeleteNodesInWay, boolean silent) { 402 if (selection == null || selection.isEmpty()) 403 return null; 404 405 // Diamond operator does not work with Java 9 here 406 @SuppressWarnings("unused") 407 Set<OsmPrimitive> primitivesToDelete = new HashSet<OsmPrimitive>(selection); 408 409 Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class); 410 if (!relationsToDelete.isEmpty() && !silent && !confirmRelationDeletion(relationsToDelete)) 411 return null; 412 413 if (alsoDeleteNodesInWay) { 414 // delete untagged nodes only referenced by primitives in primitivesToDelete, too 415 Collection<Node> nodesToDelete = computeNodesToDelete(primitivesToDelete); 416 primitivesToDelete.addAll(nodesToDelete); 417 } 418 419 if (!silent && !checkAndConfirmOutlyingDelete( 420 primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class))) 421 return null; 422 423 Collection<Way> waysToBeChanged = new HashSet<>(OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Way.class)); 424 425 Collection<Command> cmds = new LinkedList<>(); 426 for (Way w : waysToBeChanged) { 427 Way wnew = new Way(w); 428 wnew.removeNodes(OsmPrimitive.getFilteredSet(primitivesToDelete, Node.class)); 429 if (wnew.getNodesCount() < 2) { 430 primitivesToDelete.add(w); 431 } else { 432 cmds.add(new ChangeNodesCommand(w, wnew.getNodes())); 433 } 434 } 435 436 // get a confirmation that the objects to delete can be removed from their parent relations 437 // 438 if (!silent) { 439 Set<RelationToChildReference> references = RelationToChildReference.getRelationToChildReferences(primitivesToDelete); 440 references.removeIf(ref -> ref.getParent().isDeleted()); 441 if (!references.isEmpty()) { 442 DeleteFromRelationConfirmationDialog dialog = DeleteFromRelationConfirmationDialog.getInstance(); 443 dialog.getModel().populate(references); 444 dialog.setVisible(true); 445 if (dialog.isCanceled()) 446 return null; 447 } 448 } 449 450 // remove the objects from their parent relations 451 // 452 for (Relation cur : OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Relation.class)) { 453 Relation rel = new Relation(cur); 454 rel.removeMembersFor(primitivesToDelete); 455 cmds.add(new ChangeCommand(cur, rel)); 456 } 457 458 // build the delete command 459 // 460 if (!primitivesToDelete.isEmpty()) { 461 cmds.add(layer != null ? new DeleteCommand(layer, primitivesToDelete) : 462 new DeleteCommand(primitivesToDelete.iterator().next().getDataSet(), primitivesToDelete)); 463 } 464 465 return new SequenceCommand(tr("Delete"), cmds); 466 } 467 468 /** 469 * Create a command that deletes a single way segment. The way may be split by this. 470 * @param layer The layer the segment is in. 471 * @param ws The way segment that should be deleted 472 * @return A matching command to safely delete that segment. 473 */ 474 public static Command deleteWaySegment(OsmDataLayer layer, WaySegment ws) { 475 if (ws.way.getNodesCount() < 3) 476 return delete(layer, Collections.singleton(ws.way), false); 477 478 if (ws.way.isClosed()) { 479 // If the way is circular (first and last nodes are the same), the way shouldn't be splitted 480 481 List<Node> n = new ArrayList<>(); 482 483 n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1)); 484 n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1)); 485 486 Way wnew = new Way(ws.way); 487 wnew.setNodes(n); 488 489 return new ChangeCommand(ws.way, wnew); 490 } 491 492 List<Node> n1 = new ArrayList<>(); 493 List<Node> n2 = new ArrayList<>(); 494 495 n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1)); 496 n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount())); 497 498 Way wnew = new Way(ws.way); 499 500 if (n1.size() < 2) { 501 wnew.setNodes(n2); 502 return new ChangeCommand(ws.way, wnew); 503 } else if (n2.size() < 2) { 504 wnew.setNodes(n1); 505 return new ChangeCommand(ws.way, wnew); 506 } else { 507 SplitWayResult split = SplitWayAction.splitWay(layer, ws.way, Arrays.asList(n1, n2), Collections.<OsmPrimitive>emptyList()); 508 return split != null ? split.getCommand() : null; 509 } 510 } 511 512 public static boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives, 513 Collection<? extends OsmPrimitive> ignore) { 514 return Command.checkAndConfirmOutlyingOperation("delete", 515 tr("Delete confirmation"), 516 tr("You are about to delete nodes outside of the area you have downloaded." 517 + "<br>" 518 + "This can cause problems because other objects (that you do not see) might use them." 519 + "<br>" 520 + "Do you really want to delete?"), 521 tr("You are about to delete incomplete objects." 522 + "<br>" 523 + "This will cause problems because you don''t see the real object." 524 + "<br>" + "Do you really want to delete?"), 525 primitives, ignore); 526 } 527 528 private static boolean confirmRelationDeletion(Collection<Relation> relations) { 529 JPanel msg = new JPanel(new GridBagLayout()); 530 msg.add(new JMultilineLabel("<html>" + trn( 531 "You are about to delete {0} relation: {1}" 532 + "<br/>" 533 + "This step is rarely necessary and cannot be undone easily after being uploaded to the server." 534 + "<br/>" 535 + "Do you really want to delete?", 536 "You are about to delete {0} relations: {1}" 537 + "<br/>" 538 + "This step is rarely necessary and cannot be undone easily after being uploaded to the server." 539 + "<br/>" 540 + "Do you really want to delete?", 541 relations.size(), relations.size(), DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(relations, 20)) 542 + "</html>")); 543 return ConditionalOptionPaneUtil.showConfirmationDialog( 544 "delete_relations", 545 Main.parent, 546 msg, 547 tr("Delete relation?"), 548 JOptionPane.YES_NO_OPTION, 549 JOptionPane.QUESTION_MESSAGE, 550 JOptionPane.YES_OPTION); 551 } 552 553 @Override 554 public int hashCode() { 555 return Objects.hash(super.hashCode(), toDelete, clonedPrimitives); 556 } 557 558 @Override 559 public boolean equals(Object obj) { 560 if (this == obj) return true; 561 if (obj == null || getClass() != obj.getClass()) return false; 562 if (!super.equals(obj)) return false; 563 DeleteCommand that = (DeleteCommand) obj; 564 return Objects.equals(toDelete, that.toDelete) && 565 Objects.equals(clonedPrimitives, that.clonedPrimitives); 566 } 567}