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