source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java @ 5241

Revision 4982, 20.6 KB checked in by stoecker, 3 months ago (diff)

see #7226 - patch by akks (fixed a bit) - fix shortcut deprecations

  • Property svn:eol-style set to native
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.AWTEvent;
8import java.awt.BasicStroke;
9import java.awt.Color;
10import java.awt.Cursor;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.Stroke;
14import java.awt.Toolkit;
15import java.awt.event.AWTEventListener;
16import java.awt.event.InputEvent;
17import java.awt.event.KeyEvent;
18import java.awt.event.MouseEvent;
19import java.util.Collection;
20import java.util.LinkedHashSet;
21
22import javax.swing.JOptionPane;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.data.Bounds;
26import org.openstreetmap.josm.data.coor.EastNorth;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.Way;
30import org.openstreetmap.josm.data.osm.WaySegment;
31import org.openstreetmap.josm.gui.MapFrame;
32import org.openstreetmap.josm.gui.MapView;
33import org.openstreetmap.josm.gui.NavigatableComponent;
34import org.openstreetmap.josm.gui.NavigatableComponent.SystemOfMeasurement;
35import org.openstreetmap.josm.gui.layer.Layer;
36import org.openstreetmap.josm.gui.layer.MapViewPaintable;
37import org.openstreetmap.josm.gui.layer.OsmDataLayer;
38import org.openstreetmap.josm.tools.Geometry;
39import org.openstreetmap.josm.tools.ImageProvider;
40import org.openstreetmap.josm.tools.Shortcut;
41
42//// TODO: (list below)
43/* == Functionality ==
44 *
45 * 1. Use selected nodes as split points for the selected ways.
46 *
47 * The ways containing the selected nodes will be split and only the "inner"
48 * parts will be copied
49 *
50 * 2. Enter exact offset
51 *
52 * 3. Improve snapping
53 *
54 * 4. Visual cues could be better
55 *
56 * 5. Cursors (Half-done)
57 *
58 * 6. (long term) Parallelize and adjust offsets of existing ways
59 *
60 * == Code quality ==
61 *
62 * a) The mode, flags, and modifiers might be updated more than necessary.
63 *
64 * Not a performance problem, but better if they where more centralized
65 *
66 * b) Extract generic MapMode services into a super class and/or utility class
67 *
68 * c) Maybe better to simply draw our own source way highlighting?
69 *
70 * Current code doesn't not take into account that ways might been highlighted
71 * by other than us. Don't think that situation should ever happen though.
72 */
73
74/**
75 * MapMode for making parallel ways.
76 *
77 * All calculations are done in projected coordinates.
78 *
79 * @author Ole JÞrgen BrÞnner (olejorgenb)
80 */
81public class ParallelWayAction extends MapMode implements AWTEventListener, MapViewPaintable {
82
83    private static final long serialVersionUID = 1L;
84
85    private enum Mode {
86        dragging, normal
87    }
88
89    //// Preferences and flags
90    // See updateModeLocalPreferences for defaults
91    private Mode mode;
92    private boolean copyTags;
93    private boolean copyTagsDefault;
94
95    private boolean snap;
96    private boolean snapDefault;
97
98    private double snapThreshold; 
99    private double snapDistanceMetric;
100    private double snapDistanceImperial;
101    private double snapDistanceChinese;
102
103    private ModifiersSpec snapModifierCombo;
104    private ModifiersSpec copyTagsModifierCombo;
105    private ModifiersSpec addToSelectionModifierCombo;
106    private ModifiersSpec toggleSelectedModifierCombo;
107    private ModifiersSpec setSelectedModifierCombo;
108
109    private int initialMoveDelay;
110
111    private final MapView mv;
112
113    // Mouse tracking state
114    private Point mousePressedPos;
115    private boolean mouseIsDown;
116    private long mousePressedTime;
117    private boolean mouseHasBeenDragged;
118
119    private WaySegment referenceSegment;
120    private ParallelWays pWays;
121    LinkedHashSet<Way> sourceWays;
122    private EastNorth helperLineStart;
123    private EastNorth helperLineEnd;
124
125    public ParallelWayAction(MapFrame mapFrame) {
126        super(tr("Parallel"), "parallel", tr("Make parallel copies of ways"),
127            Shortcut.registerShortcut("mapmode:parallel", tr("Mode: {0}",
128                tr("Parallel")), KeyEvent.VK_P, Shortcut.SHIFT),
129            mapFrame, ImageProvider.getCursor("normal", "parallel"));
130        putValue("help", ht("/Action/Parallel"));
131        mv = mapFrame.mapView;
132        updateModeLocalPreferences();
133    }
134
135    @Override
136    public void enterMode() {
137        // super.enterMode() updates the status line and cursor so we need our state to be set correctly
138        setMode(Mode.normal);
139        pWays = null;
140        updateAllPreferences(); // All default values should've been set now
141
142        super.enterMode();
143
144        mv.addMouseListener(this);
145        mv.addMouseMotionListener(this);
146        mv.addTemporaryLayer(this);
147
148        //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless
149        try {
150            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK);
151        } catch (SecurityException ex) {
152        }
153        sourceWays = new LinkedHashSet<Way>(getCurrentDataSet().getSelectedWays());
154        for (Way w : sourceWays) {
155            w.setHighlighted(true);
156        }
157        mv.repaint();
158    }
159
160    @Override
161    public void exitMode() {
162        super.exitMode();
163        mv.removeMouseListener(this);
164        mv.removeMouseMotionListener(this);
165        mv.removeTemporaryLayer(this);
166        Main.map.statusLine.setDist(-1);
167        Main.map.statusLine.repaint();
168        try {
169            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
170        } catch (SecurityException ex) {
171        }
172        removeWayHighlighting(sourceWays);
173        pWays = null;
174        sourceWays = null;
175        referenceSegment = null;
176        mv.repaint();
177    }
178
179    @Override
180    public String getModeHelpText() {
181        // TODO: add more detailed feedback based on modifier state.
182        // TODO: dynamic messages based on preferences. (Could be problematic translation wise)
183        switch (mode) {
184        case normal:
185            return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt toggles tag preservation)");
186        case dragging:
187            return tr("Hold Ctrl to toggle snapping");
188        }
189        return ""; // impossible ..
190    }
191
192    // Separated due to "race condition" between default values
193    private void updateAllPreferences() {
194        updateModeLocalPreferences();
195        // @formatter:off
196        // @formatter:on
197    }
198
199    private void updateModeLocalPreferences() {
200        // @formatter:off
201        //snapThreshold        = Main.pref.getDouble (prefKey("snap-threshold"), 0.35); // Old preference was stored in meters, hence the new name (percent)
202        snapThreshold        = Main.pref.getDouble (prefKey("snap-threshold-percent"), 0.70);
203        snapDefault          = Main.pref.getBoolean(prefKey("snap-default"),      true);
204        copyTagsDefault      = Main.pref.getBoolean(prefKey("copy-tags-default"), true);
205        initialMoveDelay     = Main.pref.getInteger(prefKey("initial-move-delay"), 200);
206        snapDistanceMetric   = Main.pref.getDouble(prefKey("snap-distance-metric"), 0.5);
207        snapDistanceImperial = Main.pref.getDouble(prefKey("snap-distance-imperial"), 1);
208        snapDistanceChinese  = Main.pref.getDouble(prefKey("snap-distance-chinese"), 1);
209
210        snapModifierCombo           = new ModifiersSpec(getStringPref("snap-modifier-combo",             "?sC"));
211        copyTagsModifierCombo       = new ModifiersSpec(getStringPref("copy-tags-modifier-combo",        "As?"));
212        addToSelectionModifierCombo = new ModifiersSpec(getStringPref("add-to-selection-modifier-combo", "aSc"));
213        toggleSelectedModifierCombo = new ModifiersSpec(getStringPref("toggle-selection-modifier-combo", "asC"));
214        setSelectedModifierCombo    = new ModifiersSpec(getStringPref("set-selection-modifier-combo",    "asc"));
215        // @formatter:on
216    }
217
218    @Override
219    public boolean layerIsSupported(Layer layer) {
220        return layer instanceof OsmDataLayer;
221    }
222
223    @Override
224    public void eventDispatched(AWTEvent e) {
225        if (Main.map == null || mv == null || !mv.isActiveLayerDrawable())
226            return;
227
228        // Should only get InputEvents due to the mask in enterMode
229        if (updateModifiersState((InputEvent) e)) {
230            updateStatusLine();
231            updateCursor();
232        }
233    }
234
235    private boolean updateModifiersState(InputEvent e) {
236        boolean oldAlt = alt, oldShift = shift, oldCtrl = ctrl;
237        updateKeyModifiers(e);
238        boolean changed = (oldAlt != alt || oldShift != shift || oldCtrl != ctrl);
239        return changed;
240    }
241
242    private void updateCursor() {
243        Cursor newCursor = null;
244        switch (mode) {
245        case normal:
246            if (matchesCurrentModifiers(setSelectedModifierCombo)) {
247                newCursor = ImageProvider.getCursor("normal", "parallel");
248            } else if (matchesCurrentModifiers(addToSelectionModifierCombo)) {
249                newCursor = ImageProvider.getCursor("normal", "parallel"); // FIXME
250            } else if (matchesCurrentModifiers(toggleSelectedModifierCombo)) {
251                newCursor = ImageProvider.getCursor("normal", "parallel"); // FIXME
252            } else {
253                // TODO: set to a cursor indicating an error
254            }
255            break;
256        case dragging:
257            if (snap) {
258                // TODO: snapping cursor?
259                newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
260            } else {
261                newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
262            }
263        }
264        if (newCursor != null) {
265            mv.setNewCursor(newCursor, this);
266        }
267    }
268
269    private void setMode(Mode mode) {
270        this.mode = mode;
271        updateCursor();
272        updateStatusLine();
273    }
274
275    private boolean isValidModifierCombination() {
276        // TODO: implement to give feedback on invalid modifier combination
277        return true;
278    }
279
280    private boolean sanityCheck() {
281        // @formatter:off
282        boolean areWeSane =
283            mv.isActiveLayerVisible() &&
284            mv.isActiveLayerDrawable() &&
285            ((Boolean) this.getValue("active"));
286        // @formatter:on
287        assert (areWeSane); // mad == bad
288        return areWeSane;
289    }
290
291    @Override
292    public void mousePressed(MouseEvent e) {
293        updateModifiersState(e);
294        // Other buttons are off limit, but we still get events.
295        if (e.getButton() != MouseEvent.BUTTON1)
296            return;
297
298        if(sanityCheck() == false)
299            return;
300
301        updateFlagsOnlyChangeableOnPress();
302        updateFlagsChangeableAlways();
303
304        // Since the created way is left selected, we need to unselect again here
305        if (pWays != null && pWays.ways != null) {
306            getCurrentDataSet().clearSelection(pWays.ways);
307            pWays = null;
308        }
309
310        mouseIsDown = true;
311        mousePressedPos = e.getPoint();
312        mousePressedTime = System.currentTimeMillis();
313
314    }
315
316    @Override
317    public void mouseReleased(MouseEvent e) {
318        updateModifiersState(e);
319        // Other buttons are off limit, but we still get events.
320        if (e.getButton() != MouseEvent.BUTTON1)
321            return;
322
323        if (!mouseHasBeenDragged) {
324            // use point from press or click event? (or are these always the same)
325            Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive.isSelectablePredicate);
326            if (nearestWay == null) {
327                if (matchesCurrentModifiers(setSelectedModifierCombo)) {
328                    clearSourceWays();
329                }
330                resetMouseTrackingState();
331                return;
332            }
333            boolean isSelected = nearestWay.isSelected();
334            if (matchesCurrentModifiers(addToSelectionModifierCombo)) {
335                if (!isSelected) {
336                    addSourceWay(nearestWay);
337                }
338            } else if (matchesCurrentModifiers(toggleSelectedModifierCombo)) {
339                if (isSelected) {
340                    removeSourceWay(nearestWay);
341                } else {
342                    addSourceWay(nearestWay);
343                }
344            } else if (matchesCurrentModifiers(setSelectedModifierCombo)) {
345                clearSourceWays();
346                addSourceWay(nearestWay);
347            } // else -> invalid modifier combination
348        } else if (mode == Mode.dragging) {
349            clearSourceWays();
350        }
351
352        setMode(Mode.normal);
353        resetMouseTrackingState();
354        mv.repaint();
355    }
356
357    private void removeWayHighlighting(Collection<Way> ways) {
358        if (ways == null)
359            return;
360        for (Way w : ways) {
361            w.setHighlighted(false);
362        }
363    }
364
365    @Override
366    public void mouseDragged(MouseEvent e) {
367        // WTF.. the event passed here doesn't have button info?
368        // Since we get this event from other buttons too, we must check that
369        // _BUTTON1_ is down.
370        if (!mouseIsDown)
371            return;
372
373        boolean modifiersChanged = updateModifiersState(e);
374        updateFlagsChangeableAlways();
375
376        if (modifiersChanged) {
377            // Since this could be remotely slow, do it conditionally
378            updateStatusLine();
379            updateCursor();
380        }
381
382        if ((System.currentTimeMillis() - mousePressedTime) < initialMoveDelay)
383            return;
384        // Assuming this event only is emitted when the mouse has moved
385        // Setting this after the check above means we tolerate clicks with some movement
386        mouseHasBeenDragged = true;
387
388        Point p = e.getPoint();
389        if (mode == Mode.normal) {
390            // Should we ensure that the copyTags modifiers are still valid?
391
392            // Important to use mouse position from the press, since the drag
393            // event can come quite late
394            if (!isModifiersValidForDragMode())
395                return;
396            if (!initParallelWays(mousePressedPos, copyTags))
397                return;
398            setMode(Mode.dragging);
399        }
400
401        //// Calculate distance to the reference line
402        EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY());
403        EastNorth nearestPointOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(),
404                referenceSegment.getSecondNode().getEastNorth(), enp);
405
406        // Note: d is the distance in _projected units_
407        double d = enp.distance(nearestPointOnRefLine);
408        double realD = mv.getProjection().eastNorth2latlon(enp).greatCircleDistance(mv.getProjection().eastNorth2latlon(nearestPointOnRefLine));
409        double snappedRealD = realD;
410
411        // TODO: abuse of isToTheRightSideOfLine function.
412        boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(),
413                referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp));
414
415        if (snap) {
416            // TODO: Very simple snapping
417            // - Snap steps relative to the distance?
418            double snapDistance;
419            SystemOfMeasurement som = NavigatableComponent.getSystemOfMeasurement();
420            if (som.equals(NavigatableComponent.CHINESE_SOM)) {
421                snapDistance = snapDistanceChinese * NavigatableComponent.CHINESE_SOM.aValue;
422            } else if (som.equals(NavigatableComponent.IMPERIAL_SOM)) {
423                snapDistance = snapDistanceImperial * NavigatableComponent.IMPERIAL_SOM.aValue;
424            } else {
425                snapDistance = snapDistanceMetric; // Metric system by default
426            }
427            double closestWholeUnit;
428            double modulo = realD % snapDistance;
429            if (modulo < snapDistance/2.0) {
430                closestWholeUnit = realD - modulo;
431            } else {
432                closestWholeUnit = realD + (snapDistance-modulo);
433            }
434            if (Math.abs(closestWholeUnit - realD) < (snapThreshold * snapDistance)) {
435                snappedRealD = closestWholeUnit;
436            } else {
437                snappedRealD = closestWholeUnit + Math.signum(realD - closestWholeUnit) * snapDistance;
438            }
439        }
440        d = snappedRealD * (d/realD); // convert back to projected distance. (probably ok on small scales)
441        helperLineStart = nearestPointOnRefLine;
442        helperLineEnd = enp;
443        if (toTheRight) {
444            d = -d;
445        }
446        pWays.changeOffset(d);
447
448        Main.map.statusLine.setDist(Math.abs(snappedRealD));
449        Main.map.statusLine.repaint();
450        mv.repaint();
451    }
452
453    private boolean matchesCurrentModifiers(ModifiersSpec spec) {
454        return spec.matchWithKnown(alt, shift, ctrl);
455    }
456
457    @Override
458    public void paint(Graphics2D g, MapView mv, Bounds bbox) {
459        if (mode == Mode.dragging) {
460            // sanity checks
461            if (mv == null)
462                return;
463
464            // FIXME: should clip the line (gets insanely slow when zoomed in on a very long line
465            Stroke refLineStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[] {
466                    2f, 2f }, 0f);
467            g.setStroke(refLineStroke);
468            g.setColor(Color.RED);
469            Point p1 = mv.getPoint(referenceSegment.getFirstNode().getEastNorth());
470            Point p2 = mv.getPoint(referenceSegment.getSecondNode().getEastNorth());
471            g.drawLine(p1.x, p1.y, p2.x, p2.y);
472
473            Stroke helpLineStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
474            g.setStroke(helpLineStroke);
475            g.setColor(Color.RED);
476            p1 = mv.getPoint(helperLineStart);
477            p2 = mv.getPoint(helperLineEnd);
478            g.drawLine(p1.x, p1.y, p2.x, p2.y);
479        }
480    }
481
482    private boolean isModifiersValidForDragMode() {
483        return (!alt && !shift && !ctrl) || matchesCurrentModifiers(snapModifierCombo)
484                || matchesCurrentModifiers(copyTagsModifierCombo);
485    }
486
487    private void updateFlagsOnlyChangeableOnPress() {
488        copyTags = copyTagsDefault != matchesCurrentModifiers(copyTagsModifierCombo);
489    }
490
491    private void updateFlagsChangeableAlways() {
492        snap = snapDefault != matchesCurrentModifiers(snapModifierCombo);
493    }
494
495    //// We keep the source ways and the selection in sync so the user can see the source way's tags
496    private void addSourceWay(Way w) {
497        assert (sourceWays != null);
498        getCurrentDataSet().addSelected(w);
499        w.setHighlighted(true);
500        sourceWays.add(w);
501    }
502
503    private void removeSourceWay(Way w) {
504        assert (sourceWays != null);
505        getCurrentDataSet().clearSelection(w);
506        w.setHighlighted(false);
507        sourceWays.remove(w);
508    }
509
510    private void clearSourceWays() {
511        assert (sourceWays != null);
512        if (sourceWays == null)
513            return;
514        getCurrentDataSet().clearSelection(sourceWays);
515        for (Way w : sourceWays) {
516            w.setHighlighted(false);
517        }
518        sourceWays.clear();
519    }
520
521    private void resetMouseTrackingState() {
522        mouseIsDown = false;
523        mousePressedPos = null;
524        mouseHasBeenDragged = false;
525    }
526
527    // TODO: rename
528    private boolean initParallelWays(Point p, boolean copyTags) {
529        referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true);
530        if (referenceSegment == null)
531            return false;
532
533        if (!sourceWays.contains(referenceSegment.way)) {
534            clearSourceWays();
535            addSourceWay(referenceSegment.way);
536        }
537
538        try {
539            int referenceWayIndex = -1;
540            int i = 0;
541            for (Way w : sourceWays) {
542                if (w == referenceSegment.way) {
543                    referenceWayIndex = i;
544                    break;
545                }
546                i++;
547            }
548            pWays = new ParallelWays(sourceWays, copyTags, referenceWayIndex);
549            pWays.commit();
550            getCurrentDataSet().setSelected(pWays.ways);
551            return true;
552        } catch (IllegalArgumentException e) {
553            // TODO: Not ideal feedback. Maybe changing the cursor could be a good mechanism?
554            JOptionPane.showMessageDialog(
555                    Main.parent,
556                    tr("ParallelWayAction\n" +
557                            "The ways selected must form a simple branchless path"),
558                    tr("Make parallel way error"),
559                    JOptionPane.INFORMATION_MESSAGE);
560            // The error dialog prevents us from getting the mouseReleased event
561            resetMouseTrackingState();
562            pWays = null;
563            return false;
564        }
565    }
566
567    private String prefKey(String subKey) {
568        return "edit.make-parallel-way-action." + subKey;
569    }
570
571    private String getStringPref(String subKey, String def) {
572        return Main.pref.get(prefKey(subKey), def);
573    }
574
575    private String getStringPref(String subKey) {
576        return getStringPref(subKey, null);
577    }
578}
Note: See TracBrowser for help on using the repository browser.