001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.command;
003    
004    import static org.openstreetmap.josm.tools.I18n.marktr;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    import static org.openstreetmap.josm.tools.I18n.trn;
007    
008    import java.awt.GridBagLayout;
009    import java.awt.geom.Area;
010    import java.util.ArrayList;
011    import java.util.Collection;
012    import java.util.Collections;
013    import java.util.HashMap;
014    import java.util.HashSet;
015    import java.util.Iterator;
016    import java.util.LinkedList;
017    import java.util.List;
018    import java.util.Map;
019    import java.util.Set;
020    import java.util.Map.Entry;
021    
022    import javax.swing.Icon;
023    import javax.swing.JLabel;
024    import javax.swing.JOptionPane;
025    import javax.swing.JPanel;
026    
027    import org.openstreetmap.josm.Main;
028    import org.openstreetmap.josm.actions.SplitWayAction;
029    import org.openstreetmap.josm.data.osm.Node;
030    import org.openstreetmap.josm.data.osm.OsmPrimitive;
031    import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
032    import org.openstreetmap.josm.data.osm.PrimitiveData;
033    import org.openstreetmap.josm.data.osm.Relation;
034    import org.openstreetmap.josm.data.osm.RelationToChildReference;
035    import org.openstreetmap.josm.data.osm.Way;
036    import org.openstreetmap.josm.data.osm.WaySegment;
037    import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
038    import org.openstreetmap.josm.gui.DefaultNameFormatter;
039    import org.openstreetmap.josm.gui.actionsupport.DeleteFromRelationConfirmationDialog;
040    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
041    import org.openstreetmap.josm.tools.CheckParameterUtil;
042    import org.openstreetmap.josm.tools.ImageProvider;
043    import org.openstreetmap.josm.tools.Utils;
044    
045    /**
046     * A command to delete a number of primitives from the dataset.
047     *
048     */
049    public class DeleteCommand extends Command {
050        /**
051         * The primitives that get deleted.
052         */
053        private final Collection<? extends OsmPrimitive> toDelete;
054        private final Map<OsmPrimitive, PrimitiveData> clonedPrimitives = new HashMap<OsmPrimitive, PrimitiveData>();
055    
056        /**
057         * Constructor. Deletes a collection of primitives in the current edit layer.
058         *
059         * @param data the primitives to delete. Must neither be null nor empty.
060         * @throws IllegalArgumentException thrown if data is null or empty
061         */
062        public DeleteCommand(Collection<? extends OsmPrimitive> data) throws IllegalArgumentException {
063            if (data == null)
064                throw new IllegalArgumentException("Parameter 'data' must not be empty");
065            if (data.isEmpty())
066                throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
067            this.toDelete = data;
068        }
069    
070        /**
071         * Constructor. Deletes a single primitive in the current edit layer.
072         *
073         * @param data  the primitive to delete. Must not be null.
074         * @throws IllegalArgumentException thrown if data is null
075         */
076        public DeleteCommand(OsmPrimitive data) throws IllegalArgumentException {
077            CheckParameterUtil.ensureParameterNotNull(data, "data");
078            this.toDelete = Collections.singleton(data);
079        }
080    
081        /**
082         * Constructor for a single data item. Use the collection constructor to delete multiple
083         * objects.
084         *
085         * @param layer the layer context for deleting this primitive. Must not be null.
086         * @param data the primitive to delete. Must not be null.
087         * @throws IllegalArgumentException thrown if data is null
088         * @throws IllegalArgumentException thrown if layer is null
089         */
090        public DeleteCommand(OsmDataLayer layer, OsmPrimitive data) throws IllegalArgumentException {
091            super(layer);
092            CheckParameterUtil.ensureParameterNotNull(data, "data");
093            this.toDelete = Collections.singleton(data);
094        }
095    
096        /**
097         * Constructor for a collection of data to be deleted in the context of
098         * a specific layer
099         *
100         * @param layer the layer context for deleting these primitives. Must not be null.
101         * @param data the primitives to delete. Must neither be null nor empty.
102         * @throws IllegalArgumentException thrown if layer is null
103         * @throws IllegalArgumentException thrown if data is null or empty
104         */
105        public DeleteCommand(OsmDataLayer layer, Collection<? extends OsmPrimitive> data) throws IllegalArgumentException{
106            super(layer);
107            if (data == null)
108                throw new IllegalArgumentException("Parameter 'data' must not be empty");
109            if (data.isEmpty())
110                throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
111            this.toDelete = data;
112        }
113    
114        @Override
115        public boolean executeCommand() {
116            // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed)
117            for (OsmPrimitive osm: toDelete) {
118                if (osm.isDeleted())
119                    throw new IllegalArgumentException(osm.toString() + " is already deleted");
120                clonedPrimitives.put(osm, osm.save());
121    
122                if (osm instanceof Way) {
123                    ((Way) osm).setNodes(null);
124                } else if (osm instanceof Relation) {
125                    ((Relation) osm).setMembers(null);
126                }
127            }
128    
129            for (OsmPrimitive osm: toDelete) {
130                osm.setDeleted(true);
131            }
132    
133            return true;
134        }
135    
136        @Override
137        public void undoCommand() {
138            for (OsmPrimitive osm: toDelete) {
139                osm.setDeleted(false);
140            }
141    
142            for (Entry<OsmPrimitive, PrimitiveData> entry: clonedPrimitives.entrySet()) {
143                entry.getKey().load(entry.getValue());
144            }
145        }
146    
147        @Override
148        public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
149                Collection<OsmPrimitive> added) {
150        }
151    
152        private Set<OsmPrimitiveType> getTypesToDelete() {
153            Set<OsmPrimitiveType> typesToDelete = new HashSet<OsmPrimitiveType>();
154            for (OsmPrimitive osm : toDelete) {
155                typesToDelete.add(OsmPrimitiveType.from(osm));
156            }
157            return typesToDelete;
158        }
159    
160        @Override
161        public String getDescriptionText() {
162            if (toDelete.size() == 1) {
163                OsmPrimitive primitive = toDelete.iterator().next();
164                String msg = "";
165                switch(OsmPrimitiveType.from(primitive)) {
166                case NODE: msg = marktr("Delete node {0}"); break;
167                case WAY: msg = marktr("Delete way {0}"); break;
168                case RELATION:msg = marktr("Delete relation {0}"); break;
169                }
170    
171                return tr(msg, primitive.getDisplayName(DefaultNameFormatter.getInstance()));
172            } else {
173                Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
174                String msg = "";
175                if (typesToDelete.size() > 1) {
176                    msg = trn("Delete {0} object", "Delete {0} objects", toDelete.size(), toDelete.size());
177                } else {
178                    OsmPrimitiveType t = typesToDelete.iterator().next();
179                    switch(t) {
180                    case NODE: msg = trn("Delete {0} node", "Delete {0} nodes", toDelete.size(), toDelete.size()); break;
181                    case WAY: msg = trn("Delete {0} way", "Delete {0} ways", toDelete.size(), toDelete.size()); break;
182                    case RELATION: msg = trn("Delete {0} relation", "Delete {0} relations", toDelete.size(), toDelete.size()); break;
183                    }
184                }
185                return msg;
186            }
187        }
188    
189        @Override
190        public Icon getDescriptionIcon() {
191            if (toDelete.size() == 1)
192                return ImageProvider.get(toDelete.iterator().next().getDisplayType());
193            Set<OsmPrimitiveType> typesToDelete = getTypesToDelete();
194            if (typesToDelete.size() > 1)
195                return ImageProvider.get("data", "object");
196            else
197                return ImageProvider.get(typesToDelete.iterator().next());
198        }
199    
200        @Override public Collection<PseudoCommand> getChildren() {
201            if (toDelete.size() == 1)
202                return null;
203            else {
204                List<PseudoCommand> children = new ArrayList<PseudoCommand>();
205                for (final OsmPrimitive osm : toDelete) {
206                    children.add(new PseudoCommand() {
207    
208                        @Override public String getDescriptionText() {
209                            return tr("Deleted ''{0}''", osm.getDisplayName(DefaultNameFormatter.getInstance()));
210                        }
211    
212                        @Override public Icon getDescriptionIcon() {
213                            return ImageProvider.get(osm.getDisplayType());
214                        }
215    
216                        @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
217                            return Collections.singleton(osm);
218                        }
219    
220                    });
221                }
222                return children;
223    
224            }
225        }
226    
227        @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
228            return toDelete;
229        }
230    
231        /**
232         * Delete the primitives and everything they reference.
233         *
234         * If a node is deleted, the node and all ways and relations the node is part of are deleted as
235         * well.
236         *
237         * If a way is deleted, all relations the way is member of are also deleted.
238         *
239         * If a way is deleted, only the way and no nodes are deleted.
240         *
241         * @param layer the {@link OsmDataLayer} in whose context primitives are deleted. Must not be null.
242         * @param selection The list of all object to be deleted.
243         * @param silent  Set to true if the user should not be bugged with additional dialogs
244         * @return command A command to perform the deletions, or null of there is nothing to delete.
245         * @throws IllegalArgumentException thrown if layer is null
246         */
247        public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection, boolean silent) throws IllegalArgumentException {
248            CheckParameterUtil.ensureParameterNotNull(layer, "layer");
249            if (selection == null || selection.isEmpty()) return null;
250            Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection);
251            parents.addAll(selection);
252    
253            if (parents.isEmpty())
254                return null;
255            if (!silent && !checkAndConfirmOutlyingDelete(layer, parents, null))
256                return null;
257            return new DeleteCommand(layer,parents);
258        }
259    
260        public static Command deleteWithReferences(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
261            return deleteWithReferences(layer, selection, false);
262        }
263    
264        public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection) {
265            return delete(layer, selection, true, false);
266        }
267    
268        /**
269         * Replies the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
270         * can be deleted too. A node can be deleted if
271         * <ul>
272         *    <li>it is untagged (see {@link Node#isTagged()}</li>
273         *    <li>it is not referred to by other non-deleted primitives outside of  <code>primitivesToDelete</code></li>
274         * <ul>
275         * @param layer  the layer in whose context primitives are deleted
276         * @param primitivesToDelete  the primitives to delete
277         * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
278         * can be deleted too
279         */
280        protected static Collection<Node> computeNodesToDelete(OsmDataLayer layer, Collection<OsmPrimitive> primitivesToDelete) {
281            Collection<Node> nodesToDelete = new HashSet<Node>();
282            for (Way way : OsmPrimitive.getFilteredList(primitivesToDelete, Way.class)) {
283                for (Node n : way.getNodes()) {
284                    if (n.isTagged()) {
285                        continue;
286                    }
287                    Collection<OsmPrimitive> referringPrimitives = n.getReferrers();
288                    referringPrimitives.removeAll(primitivesToDelete);
289                    int count = 0;
290                    for (OsmPrimitive p : referringPrimitives) {
291                        if (!p.isDeleted()) {
292                            count++;
293                        }
294                    }
295                    if (count == 0) {
296                        nodesToDelete.add(n);
297                    }
298                }
299            }
300            return nodesToDelete;
301        }
302    
303        /**
304         * Try to delete all given primitives.
305         *
306         * If a node is used by a way, it's removed from that way. If a node or a way is used by a
307         * relation, inform the user and do not delete.
308         *
309         * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
310         * they are part of a relation, inform the user and do not delete.
311         *
312         * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted
313         * @param selection the objects to delete.
314         * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
315         * @return command a command to perform the deletions, or null if there is nothing to delete.
316         */
317        public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
318                boolean alsoDeleteNodesInWay) {
319            return delete(layer, selection, alsoDeleteNodesInWay, false /* not silent */);
320        }
321    
322        /**
323         * Try to delete all given primitives.
324         *
325         * If a node is used by a way, it's removed from that way. If a node or a way is used by a
326         * relation, inform the user and do not delete.
327         *
328         * If this would cause ways with less than 2 nodes to be created, delete these ways instead. If
329         * they are part of a relation, inform the user and do not delete.
330         *
331         * @param layer the {@link OsmDataLayer} in whose context the primitives are deleted
332         * @param selection the objects to delete.
333         * @param alsoDeleteNodesInWay <code>true</code> if nodes should be deleted as well
334         * @param silent set to true if the user should not be bugged with additional questions
335         * @return command a command to perform the deletions, or null if there is nothing to delete.
336         */
337        public static Command delete(OsmDataLayer layer, Collection<? extends OsmPrimitive> selection,
338                boolean alsoDeleteNodesInWay, boolean silent) {
339            if (selection == null || selection.isEmpty())
340                return null;
341    
342            Set<OsmPrimitive> primitivesToDelete = new HashSet<OsmPrimitive>(selection);
343    
344            Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class);
345            if (!relationsToDelete.isEmpty() && !silent && !confirmRelationDeletion(relationsToDelete))
346                return null;
347    
348            Collection<Way> waysToBeChanged = new HashSet<Way>();
349    
350            if (alsoDeleteNodesInWay) {
351                // delete untagged nodes only referenced by primitives in primitivesToDelete,
352                // too
353                Collection<Node> nodesToDelete = computeNodesToDelete(layer, primitivesToDelete);
354                primitivesToDelete.addAll(nodesToDelete);
355            }
356    
357            if (!silent && !checkAndConfirmOutlyingDelete(layer,
358                    primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class)))
359                return null;
360    
361            waysToBeChanged.addAll(OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Way.class));
362    
363            Collection<Command> cmds = new LinkedList<Command>();
364            for (Way w : waysToBeChanged) {
365                Way wnew = new Way(w);
366                wnew.removeNodes(OsmPrimitive.getFilteredSet(primitivesToDelete, Node.class));
367                if (wnew.getNodesCount() < 2) {
368                    primitivesToDelete.add(w);
369                } else {
370                    cmds.add(new ChangeCommand(w, wnew));
371                }
372            }
373    
374            // get a confirmation that the objects to delete can be removed from their parent
375            // relations
376            //
377            if (!silent) {
378                Set<RelationToChildReference> references = RelationToChildReference.getRelationToChildReferences(primitivesToDelete);
379                Iterator<RelationToChildReference> it = references.iterator();
380                while(it.hasNext()) {
381                    RelationToChildReference ref = it.next();
382                    if (ref.getParent().isDeleted()) {
383                        it.remove();
384                    }
385                }
386                if (!references.isEmpty()) {
387                    DeleteFromRelationConfirmationDialog dialog = DeleteFromRelationConfirmationDialog.getInstance();
388                    dialog.getModel().populate(references);
389                    dialog.setVisible(true);
390                    if (dialog.isCanceled())
391                        return null;
392                }
393            }
394    
395            // remove the objects from their parent relations
396            //
397            Iterator<Relation> iterator = OsmPrimitive.getFilteredSet(OsmPrimitive.getReferrer(primitivesToDelete), Relation.class).iterator();
398            while (iterator.hasNext()) {
399                Relation cur = iterator.next();
400                Relation rel = new Relation(cur);
401                rel.removeMembersFor(primitivesToDelete);
402                cmds.add(new ChangeCommand(cur, rel));
403            }
404    
405            // build the delete command
406            //
407            if (!primitivesToDelete.isEmpty()) {
408                cmds.add(new DeleteCommand(layer,primitivesToDelete));
409            }
410    
411            return new SequenceCommand(tr("Delete"), cmds);
412        }
413    
414        public static Command deleteWaySegment(OsmDataLayer layer, WaySegment ws) {
415            if (ws.way.getNodesCount() < 3)
416                return delete(layer, Collections.singleton(ws.way), false);
417    
418            if (ws.way.firstNode() == ws.way.lastNode()) {
419                // If the way is circular (first and last nodes are the same),
420                // the way shouldn't be splitted
421    
422                List<Node> n = new ArrayList<Node>();
423    
424                n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
425                n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
426    
427                Way wnew = new Way(ws.way);
428                wnew.setNodes(n);
429    
430                return new ChangeCommand(ws.way, wnew);
431            }
432    
433            List<Node> n1 = new ArrayList<Node>(), n2 = new ArrayList<Node>();
434    
435            n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
436            n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));
437    
438            Way wnew = new Way(ws.way);
439    
440            if (n1.size() < 2) {
441                wnew.setNodes(n2);
442                return new ChangeCommand(ws.way, wnew);
443            } else if (n2.size() < 2) {
444                wnew.setNodes(n1);
445                return new ChangeCommand(ws.way, wnew);
446            } else {
447                List<List<Node>> chunks = new ArrayList<List<Node>>(2);
448                chunks.add(n1);
449                chunks.add(n2);
450                return SplitWayAction.splitWay(layer,ws.way, chunks, Collections.<OsmPrimitive>emptyList()).getCommand();
451            }
452        }
453    
454        public static boolean checkAndConfirmOutlyingDelete(OsmDataLayer layer, Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) {
455            return checkAndConfirmOutlyingDelete(layer.data.getDataSourceArea(), primitives, ignore);
456        }
457    
458        public static boolean checkAndConfirmOutlyingDelete(Area area, Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) {
459            return Command.checkAndConfirmOutlyingOperation("delete",
460                    tr("Delete confirmation"),
461                    tr("You are about to delete nodes outside of the area you have downloaded."
462                            + "<br>"
463                            + "This can cause problems because other objects (that you do not see) might use them."
464                            + "<br>"
465                            + "Do you really want to delete?"),
466                    tr("You are about to delete incomplete objects."
467                            + "<br>"
468                            + "This will cause problems because you don''t see the real object."
469                            + "<br>" + "Do you really want to delete?"),
470                    area, primitives, ignore);
471        }
472    
473        private static boolean confirmRelationDeletion(Collection<Relation> relations) {
474            JPanel msg = new JPanel(new GridBagLayout());
475            msg.add(new JLabel("<html>" + trn(
476                    "You are about to delete {0} relation: {1}"
477                    + "<br/>"
478                    + "This step is rarely necessary and cannot be undone easily after being uploaded to the server."
479                    + "<br/>"
480                    + "Do you really want to delete?",
481                    "You are about to delete {0} relations: {1}"
482                    + "<br/>"
483                    + "This step is rarely necessary and cannot be undone easily after being uploaded to the server."
484                    + "<br/>"
485                    + "Do you really want to delete?",
486                    relations.size(), relations.size(), DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(relations))
487                    + "</html>"));
488            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
489                    "delete_relations",
490                    Main.parent,
491                    msg,
492                    tr("Delete relation?"),
493                    JOptionPane.YES_NO_OPTION,
494                    JOptionPane.QUESTION_MESSAGE,
495                    JOptionPane.YES_OPTION);
496            return answer;
497        }
498    }