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