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}