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