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}