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}