source: josm/trunk/src/org/openstreetmap/josm/command/DeleteCommand.java @ 12663

Last change on this file since 12663 was 12663, checked in by Don-vip, 4 weeks ago

see #15182 - move NameFormatter* from gui to data.osm

  • Property svn:eol-style set to native
File size: 23.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.GridBagLayout;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.EnumSet;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Objects;
21import java.util.Set;
22
23import javax.swing.Icon;
24import javax.swing.JOptionPane;
25import javax.swing.JPanel;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.actions.SplitWayAction;
29import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult;
30import org.openstreetmap.josm.data.osm.DataSet;
31import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
32import org.openstreetmap.josm.data.osm.Node;
33import org.openstreetmap.josm.data.osm.OsmPrimitive;
34import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
35import org.openstreetmap.josm.data.osm.PrimitiveData;
36import org.openstreetmap.josm.data.osm.Relation;
37import org.openstreetmap.josm.data.osm.RelationToChildReference;
38import org.openstreetmap.josm.data.osm.Way;
39import org.openstreetmap.josm.data.osm.WaySegment;
40import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
41import org.openstreetmap.josm.gui.dialogs.DeleteFromRelationConfirmationDialog;
42import org.openstreetmap.josm.gui.layer.OsmDataLayer;
43import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
44import org.openstreetmap.josm.tools.CheckParameterUtil;
45import org.openstreetmap.josm.tools.ImageProvider;
46import org.openstreetmap.josm.tools.Utils;
47
48/**
49 * A command to delete a number of primitives from the dataset.
50 * @since 23
51 */
52public class DeleteCommand extends Command {
53    private static final class DeleteChildCommand implements PseudoCommand {
54        private final OsmPrimitive osm;
55
56        private DeleteChildCommand(OsmPrimitive osm) {
57            this.osm = osm;
58        }
59
60        @Override
61        public String getDescriptionText() {
62            return tr("Deleted ''{0}''", osm.getDisplayName(DefaultNameFormatter.getInstance()));
63        }
64
65        @Override
66        public Icon getDescriptionIcon() {
67            return ImageProvider.get(osm.getDisplayType());
68        }
69
70        @Override
71        public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
72            return Collections.singleton(osm);
73        }
74
75        @Override
76        public String toString() {
77            return "DeleteChildCommand [osm=" + osm + "]";
78        }
79    }
80
81    /**
82     * The primitives that get deleted.
83     */
84    private final Collection<? extends OsmPrimitive> toDelete;
85    private final Map<OsmPrimitive, PrimitiveData> clonedPrimitives = new HashMap<>();
86
87    /**
88     * Constructor. Deletes a collection of primitives in the current edit layer.
89     *
90     * @param data the primitives to delete. Must neither be null nor empty.
91     * @throws IllegalArgumentException if data is null or empty
92     */
93    public DeleteCommand(Collection<? extends OsmPrimitive> data) {
94        CheckParameterUtil.ensureParameterNotNull(data, "data");
95        this.toDelete = data;
96        checkConsistency();
97    }
98
99    /**
100     * Constructor. Deletes a single primitive in the current edit layer.
101     *
102     * @param data  the primitive to delete. Must not be null.
103     * @throws IllegalArgumentException if data is null
104     */
105    public DeleteCommand(OsmPrimitive data) {
106        this(Collections.singleton(data));
107    }
108
109    /**
110     * Constructor for a single data item. Use the collection constructor to delete multiple
111     * objects.
112     *
113     * @param layer the layer context for deleting this primitive. Must not be null.
114     * @param data the primitive to delete. Must not be null.
115     * @throws IllegalArgumentException if data is null
116     * @throws IllegalArgumentException if layer is null
117     */
118    public DeleteCommand(OsmDataLayer layer, OsmPrimitive data) {
119        this(layer, Collections.singleton(data));
120    }
121
122    /**
123     * Constructor for a collection of data to be deleted in the context of
124     * a specific layer
125     *
126     * @param layer the layer context for deleting these primitives. Must not be null.
127     * @param data the primitives to delete. Must neither be null nor empty.
128     * @throws IllegalArgumentException if layer is null
129     * @throws IllegalArgumentException if data is null or empty
130     */
131    public DeleteCommand(OsmDataLayer layer, Collection<? extends OsmPrimitive> data) {
132        super(layer);
133        CheckParameterUtil.ensureParameterNotNull(data, "data");
134        this.toDelete = data;
135        checkConsistency();
136    }
137
138    /**
139     * Constructor for a collection of data to be deleted in the context of
140     * a specific data set
141     *
142     * @param dataset the dataset context for deleting these primitives. Must not be null.
143     * @param data the primitives to delete. Must neither be null nor empty.
144     * @throws IllegalArgumentException if dataset is null
145     * @throws IllegalArgumentException if data is null or empty
146     * @since 11240
147     */
148    public DeleteCommand(DataSet dataset, Collection<? extends OsmPrimitive> data) {
149        super(dataset);
150        CheckParameterUtil.ensureParameterNotNull(data, "data");
151        this.toDelete = data;
152        checkConsistency();
153    }
154
155    private void checkConsistency() {
156        if (toDelete.isEmpty()) {
157            throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
158        }
159        for (OsmPrimitive p : toDelete) {
160            if (p == null) {
161                throw new IllegalArgumentException("Primitive to delete must not be null");
162            } else if (p.getDataSet() == null) {
163                throw new IllegalArgumentException("Primitive to delete must be in a dataset");
164            }
165        }
166    }
167
168    @Override
169    public boolean executeCommand() {
170        ensurePrimitivesAreInDataset();
171        // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed)
172        for (OsmPrimitive osm: toDelete) {
173            if (osm.isDeleted())
174                throw new IllegalArgumentException(osm + " is already deleted");
175            clonedPrimitives.put(osm, osm.save());
176
177            if (osm instanceof Way) {
178                ((Way) osm).setNodes(null);
179            } else if (osm instanceof Relation) {
180                ((Relation) osm).setMembers(null);
181            }
182        }
183
184        for (OsmPrimitive osm: toDelete) {
185            osm.setDeleted(true);
186        }
187
188        return true;
189    }
190
191    @Override
192    public void undoCommand() {
193        ensurePrimitivesAreInDataset();
194
195        for (OsmPrimitive osm: toDelete) {
196            osm.setDeleted(false);
197        }
198
199        for (Entry<OsmPrimitive, PrimitiveData> entry: clonedPrimitives.entrySet()) {
200            entry.getKey().load(entry.getValue());
201        }
202    }
203
204    @Override
205    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
206        // Do nothing
207    }
208
209    private EnumSet<OsmPrimitiveType> getTypesToDelete() {
210        EnumSet<OsmPrimitiveType> typesToDelete = EnumSet.noneOf(OsmPrimitiveType.class);
211        for (OsmPrimitive osm : toDelete) {
212            typesToDelete.add(OsmPrimitiveType.from(osm));
213        }
214        return typesToDelete;
215    }
216
217    @Override
218    public String getDescriptionText() {
219        if (toDelete.size() == 1) {
220            OsmPrimitive primitive = toDelete.iterator().next();
221            String msg;
222            switch(OsmPrimitiveType.from(primitive)) {
223            case NODE: msg = marktr("Delete node {0}"); break;
224            case WAY: msg = marktr("Delete way {0}"); break;
225            case RELATION:msg = marktr("Delete relation {0}"); break;
226            default: throw new AssertionError();
227            }
228
229            return tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance()));
230        } else {
231            Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
232            String msg;
233            if (typesToDelete.size() > 1) {
234                msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
235            } else {
236                OsmPrimitiveType t = typesToDelete.iterator().next();
237                switch(t) {
238                case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
239                case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
240                case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
241                default: throw new AssertionError();
242                }
243            }
244            return msg;
245        }
246    }
247
248    @Override
249    public Icon getDescriptionIcon() {
250        if (toDelete.size() == 1)
251            return ImageProvider.get(toDelete.iterator().next().getDisplayType());
252        Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
253        if (typesToDelete.size() > 1)
254            return ImageProvider.get("data", "object");
255        else
256            return ImageProvider.get(typesToDelete.iterator().next());
257    }
258
259    @Override public Collection<PseudoCommand> getChildren() {
260        if (toDelete.size() == 1)
261            return null;
262        else {
263            List<PseudoCommand> children = new ArrayList<>(toDelete.size());
264            for (final OsmPrimitive osm : toDelete) {
265                children.add(new DeleteChildCommand(osm));
266            }
267            return children;
268
269        }
270    }
271
272    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
273        return toDelete;
274    }
275
276    /**
277     * Delete the primitives and everything they reference.
278     *
279     * If a node is deleted, the node and all ways and relations the node is part of are deleted as well.
280     * If a way is deleted, all relations the way is member of are also deleted.
281     * If a way is deleted, only the way and no nodes are deleted.
282     *
283     * @param layer the {@link OsmDataLayer} in whose context primitives are deleted. Must not be null.
284     * @param selection The list of all object to be deleted.
285     * @param silent  Set to true if the user should not be bugged with additional dialogs
286     * @return command A command to perform the deletions, or null of there is nothing to delete.
287     * @throws IllegalArgumentException if layer is null
288     */
289    public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, boolean silent) {
290        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
291        if (selection == null || selection.isEmpty()) return null;
292        Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection);
293        parents.addAll(selection);
294
295        if (parents.isEmpty())
296            return null;
297        if (!silent && !checkAndConfirmOutlyingDelete(parents, null))
298            return null;
299        return new DeleteCommand(layer, parents);
300    }
301
302    /**
303     * Delete the primitives and everything they reference.
304     *
305     * If a node is deleted, the node and all ways and relations the node is part of are deleted as well.
306     * If a way is deleted, all relations the way is member of are also deleted.
307     * If a way is deleted, only the way and no nodes are deleted.
308     *
309     * @param layer the {@link OsmDataLayer} in whose context primitives are deleted. Must not be null.
310     * @param selection The list of all object to be deleted.
311     * @return command A command to perform the deletions, or null of there is nothing to delete.
312     * @throws IllegalArgumentException if layer is null
313     */
314    public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
315        return deleteWithReferences(layer, selection, false);
316    }
317
318    /**
319     * Try to delete all given primitives.
320     *
321     * If a node is used by a way, it's removed from that way. If a node or a way is used by a
322     * relation, inform the user and do not delete.
323     *
324     * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
325     * they are part of a relation, inform the user and do not delete.
326     *
327     * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted
328     * @param selection the objects to delete.
329     * @return command a command to perform the deletions, or null if there is nothing to delete.
330     */
331    public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
332        return delete(layer, selection, true, false);
333    }
334
335    /**
336     * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
337     * can be deleted too. A node can be deleted if
338     * <ul>
339     *    <li>it is untagged (see {@link Node#isTagged()}</li>
340     *    <li>it is not referred to by other non-deleted primitives outside of  <code>primitivesToDelete</code></li>
341     * </ul>
342     * @param primitivesToDelete  the primitives to delete
343     * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
344     * can be deleted too
345     */
346    protected static Collection<Node> computeNodesToDelete(Collection<OsmPrimitive> primitivesToDelete) {
347        Collection<Node> nodesToDelete = new HashSet<>();
348        for (Way way : OsmPrimitive.getFilteredList(primitivesToDelete, Way.class)) {
349            for (Node n : way.getNodes()) {
350                if (n.isTagged()) {
351                    continue;
352                }
353                Collection<OsmPrimitive> referringPrimitives = n.getReferrers();
354                referringPrimitives.removeAll(primitivesToDelete);
355                int count = 0;
356                for (OsmPrimitive p : referringPrimitives) {
357                    if (!p.isDeleted()) {
358                        count++;
359                    }
360                }
361                if (count == 0) {
362                    nodesToDelete.add(n);
363                }
364            }
365        }
366        return nodesToDelete;
367    }
368
369    /**
370     * Try to delete all given primitives.
371     *
372     * If a node is used by a way, it's removed from that way. If a node or a way is used by a
373     * relation, inform the user and do not delete.
374     *
375     * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
376     * they are part of a relation, inform the user and do not delete.
377     *
378     * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted
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     */
383    public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
384            boolean alsoDeleteNodesInWay) {
385        return delete(layer, 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 layer the {@link OsmDataLayer} in whose context the primitives are deleted
398     * @param selection the objects to delete.
399     * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
400     * @param silent set to true if the user should not be bugged with additional questions
401     * @return command a command to perform the deletions, or null if there is nothing to delete.
402     */
403    public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
404            boolean alsoDeleteNodesInWay, boolean silent) {
405        if (selection == null || selection.isEmpty())
406            return null;
407
408        Set<OsmPrimitive> primitivesToDelete = new HashSet<>(selection);
409
410        Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class);
411        if (!relationsToDelete.isEmpty() && !silent && !confirmRelationDeletion(relationsToDelete))
412            return null;
413
414        if (alsoDeleteNodesInWay) {
415            // delete untagged nodes only referenced by primitives in primitivesToDelete, too
416            Collection<Node> nodesToDelete = computeNodesToDelete(primitivesToDelete);
417            primitivesToDelete.addAll(nodesToDelete);
418        }
419
420        if (!silent && !checkAndConfirmOutlyingDelete(
421                primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class)))
422            return null;
423
424        Collection<Way> waysToBeChanged = new HashSet<>(OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Way.class));
425
426        Collection<Command> cmds = new LinkedList<>();
427        for (Way w : waysToBeChanged) {
428            Way wnew = new Way(w);
429            wnew.removeNodes(OsmPrimitive.getFilteredSet(primitivesToDelete, Node.class));
430            if (wnew.getNodesCount() < 2) {
431                primitivesToDelete.add(w);
432            } else {
433                cmds.add(new ChangeNodesCommand(w, wnew.getNodes()));
434            }
435        }
436
437        // get a confirmation that the objects to delete can be removed from their parent relations
438        //
439        if (!silent) {
440            Set<RelationToChildReference> references = RelationToChildReference.getRelationToChildReferences(primitivesToDelete);
441            references.removeIf(ref -> ref.getParent().isDeleted());
442            if (!references.isEmpty()) {
443                DeleteFromRelationConfirmationDialog dialog = DeleteFromRelationConfirmationDialog.getInstance();
444                dialog.getModel().populate(references);
445                dialog.setVisible(true);
446                if (dialog.isCanceled())
447                    return null;
448            }
449        }
450
451        // remove the objects from their parent relations
452        //
453        for (Relation cur : OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Relation.class)) {
454            Relation rel = new Relation(cur);
455            rel.removeMembersFor(primitivesToDelete);
456            cmds.add(new ChangeCommand(cur, rel));
457        }
458
459        // build the delete command
460        //
461        if (!primitivesToDelete.isEmpty()) {
462            cmds.add(layer != null ? new DeleteCommand(layer, primitivesToDelete) :
463                new DeleteCommand(primitivesToDelete.iterator().next().getDataSet(), primitivesToDelete));
464        }
465
466        return new SequenceCommand(tr("Delete"), cmds);
467    }
468
469    /**
470     * Create a command that deletes a single way segment. The way may be split by this.
471     * @param layer The layer the segment is in.
472     * @param ws The way segment that should be deleted
473     * @return A matching command to safely delete that segment.
474     */
475    public static Command deleteWaySegment(OsmDataLayer layer, WaySegment ws) {
476        if (ws.way.getNodesCount() < 3)
477            return delete(layer, Collections.singleton(ws.way), false);
478
479        if (ws.way.isClosed()) {
480            // If the way is circular (first and last nodes are the same), the way shouldn't be splitted
481
482            List<Node> n = new ArrayList<>();
483
484            n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
485            n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
486
487            Way wnew = new Way(ws.way);
488            wnew.setNodes(n);
489
490            return new ChangeCommand(ws.way, wnew);
491        }
492
493        List<Node> n1 = new ArrayList<>();
494        List<Node> n2 = new ArrayList<>();
495
496        n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
497        n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
498
499        Way wnew = new Way(ws.way);
500
501        if (n1.size() < 2) {
502            wnew.setNodes(n2);
503            return new ChangeCommand(ws.way, wnew);
504        } else if (n2.size() < 2) {
505            wnew.setNodes(n1);
506            return new ChangeCommand(ws.way, wnew);
507        } else {
508            SplitWayResult split = SplitWayAction.splitWay(layer, ws.way, Arrays.asList(n1, n2), Collections.<OsmPrimitive>emptyList());
509            return split != null ? split.getCommand() : null;
510        }
511    }
512
513    public static boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives,
514            Collection<? extends OsmPrimitive> ignore) {
515        return Command.checkAndConfirmOutlyingOperation("delete",
516                tr("Delete confirmation"),
517                tr("You are about to delete nodes outside of the area you have downloaded."
518                        + "<br>"
519                        + "This can cause problems because other objects (that you do not see) might use them."
520                        + "<br>"
521                        + "Do you really want to delete?"),
522                tr("You are about to delete incomplete objects."
523                        + "<br>"
524                        + "This will cause problems because you don''t see the real object."
525                        + "<br>" + "Do you really want to delete?"),
526                primitives, ignore);
527    }
528
529    private static boolean confirmRelationDeletion(Collection<Relation> relations) {
530        JPanel msg = new JPanel(new GridBagLayout());
531        msg.add(new JMultilineLabel("<html>" + trn(
532                "You are about to delete {0} relation: {1}"
533                + "<br/>"
534                + "This step is rarely necessary and cannot be undone easily after being uploaded to the server."
535                + "<br/>"
536                + "Do you really want to delete?",
537                "You are about to delete {0} relations: {1}"
538                + "<br/>"
539                + "This step is rarely necessary and cannot be undone easily after being uploaded to the server."
540                + "<br/>"
541                + "Do you really want to delete?",
542                relations.size(), relations.size(), DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(relations, 20))
543                + "</html>"));
544        return ConditionalOptionPaneUtil.showConfirmationDialog(
545                "delete_relations",
546                Main.parent,
547                msg,
548                tr("Delete relation?"),
549                JOptionPane.YES_NO_OPTION,
550                JOptionPane.QUESTION_MESSAGE,
551                JOptionPane.YES_OPTION);
552    }
553
554    @Override
555    public int hashCode() {
556        return Objects.hash(super.hashCode(), toDelete, clonedPrimitives);
557    }
558
559    @Override
560    public boolean equals(Object obj) {
561        if (this == obj) return true;
562        if (obj == null || getClass() != obj.getClass()) return false;
563        if (!super.equals(obj)) return false;
564        DeleteCommand that = (DeleteCommand) obj;
565        return Objects.equals(toDelete, that.toDelete) &&
566                Objects.equals(clonedPrimitives, that.clonedPrimitives);
567    }
568}
Note: See TracBrowser for help on using the repository browser.