001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.actions.mapmode;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.AWTEvent;
007    import java.awt.Cursor;
008    import java.awt.Toolkit;
009    import java.awt.event.AWTEventListener;
010    import java.awt.event.ActionEvent;
011    import java.awt.event.InputEvent;
012    import java.awt.event.KeyEvent;
013    import java.awt.event.MouseEvent;
014    import java.util.Collections;
015    import java.util.HashSet;
016    import java.util.Set;
017    
018    import org.openstreetmap.josm.Main;
019    import org.openstreetmap.josm.command.Command;
020    import org.openstreetmap.josm.command.DeleteCommand;
021    import org.openstreetmap.josm.data.osm.DataSet;
022    import org.openstreetmap.josm.data.osm.Node;
023    import org.openstreetmap.josm.data.osm.OsmPrimitive;
024    import org.openstreetmap.josm.data.osm.Relation;
025    import org.openstreetmap.josm.data.osm.WaySegment;
026    import org.openstreetmap.josm.gui.MapFrame;
027    import org.openstreetmap.josm.gui.dialogs.relation.RelationDialogManager;
028    import org.openstreetmap.josm.gui.layer.Layer;
029    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
030    import org.openstreetmap.josm.tools.CheckParameterUtil;
031    import org.openstreetmap.josm.tools.ImageProvider;
032    import org.openstreetmap.josm.tools.Shortcut;
033    
034    /**
035     * A map mode that enables the user to delete nodes and other objects.
036     *
037     * The user can click on an object, which gets deleted if possible. When Ctrl is
038     * pressed when releasing the button, the objects and all its references are
039     * deleted.
040     *
041     * If the user did not press Ctrl and the object has any references, the user
042     * is informed and nothing is deleted.
043     *
044     * If the user enters the mapmode and any object is selected, all selected
045     * objects are deleted, if possible.
046     *
047     * @author imi
048     */
049    public class DeleteAction extends MapMode implements AWTEventListener {
050        // Cache previous mouse event (needed when only the modifier keys are
051        // pressed but the mouse isn't moved)
052        private MouseEvent oldEvent = null;
053    
054        /**
055         * elements that have been highlighted in the previous iteration. Used
056         * to remove the highlight from them again as otherwise the whole data
057         * set would have to be checked.
058         */
059        private Set<OsmPrimitive> oldHighlights = new HashSet<OsmPrimitive>();
060        private WaySegment oldHighlightedWaySegment = null;
061    
062        private boolean drawTargetHighlight;
063    
064        private enum DeleteMode {
065            none("delete"),
066            segment("delete_segment"),
067            node("delete_node"),
068            node_with_references("delete_node"),
069            way("delete_way_only"),
070            way_with_references("delete_way_normal"),
071            way_with_nodes("delete_way_node_only");
072    
073            private final Cursor c;
074    
075            private DeleteMode(String cursorName) {
076                c = ImageProvider.getCursor("normal", cursorName);
077            }
078    
079            public Cursor cursor() {
080                return c;
081            }
082        }
083    
084        private static class DeleteParameters {
085            DeleteMode mode;
086            Node nearestNode;
087            WaySegment nearestSegment;
088        }
089    
090        /**
091         * Construct a new DeleteAction. Mnemonic is the delete - key.
092         * @param mapFrame The frame this action belongs to.
093         */
094        public DeleteAction(MapFrame mapFrame) {
095            super(tr("Delete Mode"),
096                    "delete",
097                    tr("Delete nodes or ways."),
098                    Shortcut.registerShortcut("mapmode:delete", tr("Mode: {0}",tr("Delete")),
099                    KeyEvent.VK_DELETE, Shortcut.CTRL),
100                    mapFrame,
101                    ImageProvider.getCursor("normal", "delete"));
102        }
103    
104        @Override public void enterMode() {
105            super.enterMode();
106            if (!isEnabled())
107                return;
108    
109            drawTargetHighlight = Main.pref.getBoolean("draw.target-highlight", true);
110    
111            Main.map.mapView.addMouseListener(this);
112            Main.map.mapView.addMouseMotionListener(this);
113            // This is required to update the cursors when ctrl/shift/alt is pressed
114            try {
115                Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
116            } catch (SecurityException ex) {
117                System.out.println(ex);
118            }
119        }
120    
121        @Override public void exitMode() {
122            super.exitMode();
123            Main.map.mapView.removeMouseListener(this);
124            Main.map.mapView.removeMouseMotionListener(this);
125            try {
126                Toolkit.getDefaultToolkit().removeAWTEventListener(this);
127            } catch (SecurityException ex) {
128                System.out.println(ex);
129            }
130            removeHighlighting();
131        }
132    
133        @Override public void actionPerformed(ActionEvent e) {
134            super.actionPerformed(e);
135            doActionPerformed(e);
136        }
137    
138        static public void doActionPerformed(ActionEvent e) {
139            if(!Main.map.mapView.isActiveLayerDrawable())
140                return;
141            boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
142            boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0;
143    
144            Command c;
145            if (ctrl) {
146                c = DeleteCommand.deleteWithReferences(getEditLayer(),getCurrentDataSet().getSelected());
147            } else {
148                c = DeleteCommand.delete(getEditLayer(),getCurrentDataSet().getSelected(), !alt /* also delete nodes in way */);
149            }
150            // if c is null, an error occurred or the user aborted. Don't do anything in that case.
151            if (c != null) {
152                Main.main.undoRedo.add(c);
153                getCurrentDataSet().setSelected();
154                Main.map.repaint();
155            }
156        }
157    
158        @Override public void mouseDragged(MouseEvent e) {
159            mouseMoved(e);
160        }
161    
162        /**
163         * Listen to mouse move to be able to update the cursor (and highlights)
164         * @param e The mouse event that has been captured
165         */
166        @Override public void mouseMoved(MouseEvent e) {
167            oldEvent = e;
168            giveUserFeedback(e);
169        }
170    
171        /**
172         * removes any highlighting that may have been set beforehand.
173         */
174        private void removeHighlighting() {
175            for(OsmPrimitive prim : oldHighlights) {
176                prim.setHighlighted(false);
177            }
178            oldHighlights = new HashSet<OsmPrimitive>();
179            DataSet ds = getCurrentDataSet();
180            if(ds != null) {
181                ds.clearHighlightedWaySegments();
182            }
183        }
184    
185        /**
186         * handles everything related to highlighting primitives and way
187         * segments for the given pointer position (via MouseEvent) and
188         * modifiers.
189         * @param e
190         * @param modifiers
191         */
192        private void addHighlighting(MouseEvent e, int modifiers) {
193            if(!drawTargetHighlight)
194                return;
195    
196            Set<OsmPrimitive> newHighlights = new HashSet<OsmPrimitive>();
197            DeleteParameters parameters = getDeleteParameters(e, modifiers);
198    
199            if(parameters.mode == DeleteMode.segment) {
200                // deleting segments is the only action not working on OsmPrimitives
201                // so we have to handle them separately.
202                repaintIfRequired(newHighlights, parameters.nearestSegment);
203            } else {
204                // don't call buildDeleteCommands for DeleteMode.segment because it doesn't support
205                // silent operation and SplitWayAction will show dialogs. A lot.
206                Command delCmd = buildDeleteCommands(e, modifiers, true);
207                if(delCmd != null) {
208                    // all other cases delete OsmPrimitives directly, so we can
209                    // safely do the following
210                    for(OsmPrimitive osm : delCmd.getParticipatingPrimitives()) {
211                        newHighlights.add(osm);
212                    }
213                }
214                repaintIfRequired(newHighlights, null);
215            }
216        }
217    
218        private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
219            boolean needsRepaint = false;
220            DataSet ds = getCurrentDataSet();
221    
222            if(newHighlightedWaySegment == null && oldHighlightedWaySegment != null) {
223                if(ds != null) {
224                    ds.clearHighlightedWaySegments();
225                    needsRepaint = true;
226                }
227                oldHighlightedWaySegment = null;
228            } else if(newHighlightedWaySegment != null && !newHighlightedWaySegment.equals(oldHighlightedWaySegment)) {
229                if(ds != null) {
230                    ds.setHighlightedWaySegments(Collections.singleton(newHighlightedWaySegment));
231                    needsRepaint = true;
232                }
233                oldHighlightedWaySegment = newHighlightedWaySegment;
234            }
235    
236            for(OsmPrimitive x : newHighlights) {
237                if(oldHighlights.contains(x)) {
238                    continue;
239                }
240                needsRepaint = true;
241                x.setHighlighted(true);
242            }
243            oldHighlights.removeAll(newHighlights);
244            for(OsmPrimitive x : oldHighlights) {
245                x.setHighlighted(false);
246                needsRepaint = true;
247            }
248            oldHighlights = newHighlights;
249            if(needsRepaint) {
250                Main.map.mapView.repaint();
251            }
252        }
253    
254        /**
255         * This function handles all work related to updating the cursor and
256         * highlights
257         *
258         * @param e
259         * @param modifiers
260         */
261        private void updateCursor(MouseEvent e, int modifiers) {
262            if (!Main.isDisplayingMapView())
263                return;
264            if(!Main.map.mapView.isActiveLayerVisible() || e == null)
265                return;
266    
267            DeleteParameters parameters = getDeleteParameters(e, modifiers);
268            Main.map.mapView.setNewCursor(parameters.mode.cursor(), this);
269        }
270        /**
271         * Gives the user feedback for the action he/she is about to do. Currently
272         * calls the cursor and target highlighting routines. Allows for modifiers
273         * not taken from the given mouse event.
274         * 
275         * Normally the mouse event also contains the modifiers. However, when the
276         * mouse is not moved and only modifier keys are pressed, no mouse event
277         * occurs. We can use AWTEvent to catch those but still lack a proper
278         * mouseevent. Instead we copy the previous event and only update the
279         * modifiers.
280         */
281        private void giveUserFeedback(MouseEvent e, int modifiers) {
282            updateCursor(e, modifiers);
283            addHighlighting(e, modifiers);
284        }
285    
286        /**
287         * Gives the user feedback for the action he/she is about to do. Currently
288         * calls the cursor and target highlighting routines. Extracts modifiers
289         * from mouse event.
290         */
291        private void giveUserFeedback(MouseEvent e) {
292            giveUserFeedback(e, e.getModifiers());
293        }
294    
295        /**
296         * If user clicked with the left button, delete the nearest object.
297         * position.
298         */
299        @Override public void mouseReleased(MouseEvent e) {
300            if (e.getButton() != MouseEvent.BUTTON1)
301                return;
302            if(!Main.map.mapView.isActiveLayerVisible())
303                return;
304    
305            // request focus in order to enable the expected keyboard shortcuts
306            //
307            Main.map.mapView.requestFocus();
308    
309            Command c = buildDeleteCommands(e, e.getModifiers(), false);
310            if (c != null) {
311                Main.main.undoRedo.add(c);
312            }
313    
314            getCurrentDataSet().setSelected();
315            giveUserFeedback(e);
316        }
317    
318        @Override public String getModeHelpText() {
319            return tr("Click to delete. Shift: delete way segment. Alt: do not delete unused nodes when deleting a way. Ctrl: delete referring objects.");
320        }
321    
322        @Override public boolean layerIsSupported(Layer l) {
323            return l instanceof OsmDataLayer;
324        }
325    
326        @Override
327        protected void updateEnabledState() {
328            setEnabled(Main.isDisplayingMapView() && Main.map.mapView.isActiveLayerDrawable());
329        }
330    
331        /**
332         * Deletes the relation in the context of the given layer.
333         *
334         * @param layer the layer in whose context the relation is deleted. Must not be null.
335         * @param toDelete  the relation to be deleted. Must  not be null.
336         * @exception IllegalArgumentException thrown if layer is null
337         * @exception IllegalArgumentException thrown if toDelete is nul
338         */
339        public static void deleteRelation(OsmDataLayer layer, Relation toDelete) {
340            CheckParameterUtil.ensureParameterNotNull(layer, "layer");
341            CheckParameterUtil.ensureParameterNotNull(toDelete, "toDelete");
342    
343            Command cmd = DeleteCommand.delete(layer, Collections.singleton(toDelete));
344            if (cmd != null) {
345                // cmd can be null if the user cancels dialogs DialogCommand displays
346                Main.main.undoRedo.add(cmd);
347                if (getCurrentDataSet().getSelectedRelations().contains(toDelete)) {
348                    getCurrentDataSet().toggleSelected(toDelete);
349                }
350                RelationDialogManager.getRelationDialogManager().close(layer, toDelete);
351            }
352        }
353    
354        private DeleteParameters getDeleteParameters(MouseEvent e, int modifiers) {
355            updateKeyModifiers(modifiers);
356    
357            DeleteParameters result = new DeleteParameters();
358    
359            result.nearestNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate);
360            if (result.nearestNode == null) {
361                result.nearestSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate);
362                if (result.nearestSegment != null) {
363                    if (shift) {
364                        result.mode = DeleteMode.segment;
365                    } else if (ctrl) {
366                        result.mode = DeleteMode.way_with_references;
367                    } else {
368                        result.mode = alt?DeleteMode.way:DeleteMode.way_with_nodes;
369                    }
370                } else {
371                    result.mode = DeleteMode.none;
372                }
373            } else if (ctrl) {
374                result.mode = DeleteMode.node_with_references;
375            } else {
376                result.mode = DeleteMode.node;
377            }
378    
379            return result;
380        }
381    
382        /**
383         * This function takes any mouse event argument and builds the list of elements
384         * that should be deleted but does not actually delete them.
385         * @param e MouseEvent from which modifiers and position are taken
386         * @param modifiers For explanation: @see updateCursor
387         * @param silet Set to true if the user should not be bugged with additional
388         *        dialogs
389         * @return
390         */
391        private Command buildDeleteCommands(MouseEvent e, int modifiers, boolean silent) {
392            DeleteParameters parameters = getDeleteParameters(e, modifiers);
393            switch (parameters.mode) {
394            case node:
395                return DeleteCommand.delete(getEditLayer(),Collections.singleton(parameters.nearestNode), false, silent);
396            case node_with_references:
397                return DeleteCommand.deleteWithReferences(getEditLayer(),Collections.singleton(parameters.nearestNode), silent);
398            case segment:
399                return DeleteCommand.deleteWaySegment(getEditLayer(), parameters.nearestSegment);
400            case way:
401                return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), false, silent);
402            case way_with_nodes:
403                return DeleteCommand.delete(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true, silent);
404            case way_with_references:
405                return DeleteCommand.deleteWithReferences(getEditLayer(), Collections.singleton(parameters.nearestSegment.way), true);
406            default:
407                return null;
408            }
409        }
410    
411        /**
412         * This is required to update the cursors when ctrl/shift/alt is pressed
413         */
414        public void eventDispatched(AWTEvent e) {
415            if(oldEvent == null)
416                return;
417            // We don't have a mouse event, so we pass the old mouse event but the
418            // new modifiers.
419            giveUserFeedback(oldEvent, ((InputEvent) e).getModifiers());
420        }
421    }