source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/DrawSnapHelper.java @ 12841

Last change on this file since 12841 was 12841, checked in by bastiK, 5 weeks ago

see #15229 - fix deprecations caused by [12840]

  • Property svn:eol-style set to native
File size: 18.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Graphics2D;
7import java.awt.event.ActionEvent;
8import java.awt.event.MouseEvent;
9import java.awt.event.MouseListener;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Comparator;
14import java.util.stream.DoubleStream;
15
16import javax.swing.AbstractAction;
17import javax.swing.JCheckBoxMenuItem;
18import javax.swing.JPopupMenu;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.data.coor.EastNorth;
22import org.openstreetmap.josm.data.coor.LatLon;
23import org.openstreetmap.josm.data.osm.DataSet;
24import org.openstreetmap.josm.data.osm.Node;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.data.osm.WaySegment;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.MapView;
29import org.openstreetmap.josm.gui.MapViewState;
30import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
31import org.openstreetmap.josm.gui.draw.MapViewPath;
32import org.openstreetmap.josm.gui.draw.SymbolShape;
33import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
34import org.openstreetmap.josm.tools.Logging;
35import org.openstreetmap.josm.tools.Utils;
36
37/**
38 * Class that enables the user to draw way segments in angles of exactly 30, 45,
39 * 60, 90 degrees.
40 *
41 * With enabled snapping, the new way node will be projected onto the helper line
42 * that indicates a certain fixed angle relative to the previous segment.
43 */
44class DrawSnapHelper {
45
46    private final DrawAction drawAction;
47
48    /**
49     * Constructs a new {@code SnapHelper}.
50     * @param drawAction enclosing DrawAction
51     */
52    DrawSnapHelper(DrawAction drawAction) {
53        this.drawAction = drawAction;
54        this.anglePopupListener = new PopupMenuLauncher(new AnglePopupMenu(this)) {
55            @Override
56            public void mouseClicked(MouseEvent e) {
57                super.mouseClicked(e);
58                if (e.getButton() == MouseEvent.BUTTON1) {
59                    toggleSnapping();
60                    drawAction.updateStatusLine();
61                }
62            }
63        };
64    }
65
66    private static final String DRAW_ANGLESNAP_ANGLES = "draw.anglesnap.angles";
67
68    private static final class RepeatedAction extends AbstractAction {
69        RepeatedAction(DrawSnapHelper snapHelper) {
70            super(tr("Toggle snapping by {0}", snapHelper.drawAction.getShortcut().getKeyText()));
71        }
72
73        @Override
74        public void actionPerformed(ActionEvent e) {
75            boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
76            DrawAction.USE_REPEATED_SHORTCUT.put(sel);
77        }
78    }
79
80    private static final class HelperAction extends AbstractAction {
81        private final transient DrawSnapHelper snapHelper;
82
83        HelperAction(DrawSnapHelper snapHelper) {
84            super(tr("Show helper geometry"));
85            this.snapHelper = snapHelper;
86        }
87
88        @Override
89        public void actionPerformed(ActionEvent e) {
90            boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
91            DrawAction.DRAW_CONSTRUCTION_GEOMETRY.put(sel);
92            DrawAction.SHOW_PROJECTED_POINT.put(sel);
93            DrawAction.SHOW_ANGLE.put(sel);
94            snapHelper.enableSnapping();
95        }
96    }
97
98    private static final class ProjectionAction extends AbstractAction {
99        private final transient DrawSnapHelper snapHelper;
100
101        ProjectionAction(DrawSnapHelper snapHelper) {
102            super(tr("Snap to node projections"));
103            this.snapHelper = snapHelper;
104        }
105
106        @Override
107        public void actionPerformed(ActionEvent e) {
108            boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
109            DrawAction.SNAP_TO_PROJECTIONS.put(sel);
110            snapHelper.enableSnapping();
111        }
112    }
113
114    private static final class DisableAction extends AbstractAction {
115        private final transient DrawSnapHelper snapHelper;
116
117        DisableAction(DrawSnapHelper snapHelper) {
118            super(tr("Disable"));
119            this.snapHelper = snapHelper;
120        }
121
122        @Override
123        public void actionPerformed(ActionEvent e) {
124            snapHelper.saveAngles("180");
125            snapHelper.init();
126            snapHelper.enableSnapping();
127        }
128    }
129
130    private static final class Snap90DegreesAction extends AbstractAction {
131        private final transient DrawSnapHelper snapHelper;
132
133        Snap90DegreesAction(DrawSnapHelper snapHelper) {
134            super(tr("0,90,..."));
135            this.snapHelper = snapHelper;
136        }
137
138        @Override
139        public void actionPerformed(ActionEvent e) {
140            snapHelper.saveAngles("0", "90", "180");
141            snapHelper.init();
142            snapHelper.enableSnapping();
143        }
144    }
145
146    private static final class Snap45DegreesAction extends AbstractAction {
147        private final transient DrawSnapHelper snapHelper;
148
149        Snap45DegreesAction(DrawSnapHelper snapHelper) {
150            super(tr("0,45,90,..."));
151            this.snapHelper = snapHelper;
152        }
153
154        @Override
155        public void actionPerformed(ActionEvent e) {
156            snapHelper.saveAngles("0", "45", "90", "135", "180");
157            snapHelper.init();
158            snapHelper.enableSnapping();
159        }
160    }
161
162    private static final class Snap30DegreesAction extends AbstractAction {
163        private final transient DrawSnapHelper snapHelper;
164
165        Snap30DegreesAction(DrawSnapHelper snapHelper) {
166            super(tr("0,30,45,60,90,..."));
167            this.snapHelper = snapHelper;
168        }
169
170        @Override
171        public void actionPerformed(ActionEvent e) {
172            snapHelper.saveAngles("0", "30", "45", "60", "90", "120", "135", "150", "180");
173            snapHelper.init();
174            snapHelper.enableSnapping();
175        }
176    }
177
178    private static final class AnglePopupMenu extends JPopupMenu {
179
180        private AnglePopupMenu(final DrawSnapHelper snapHelper) {
181            JCheckBoxMenuItem repeatedCb = new JCheckBoxMenuItem(new RepeatedAction(snapHelper));
182            JCheckBoxMenuItem helperCb = new JCheckBoxMenuItem(new HelperAction(snapHelper));
183            JCheckBoxMenuItem projectionCb = new JCheckBoxMenuItem(new ProjectionAction(snapHelper));
184
185            helperCb.setState(DrawAction.DRAW_CONSTRUCTION_GEOMETRY.get());
186            projectionCb.setState(DrawAction.SNAP_TO_PROJECTIONS.get());
187            repeatedCb.setState(DrawAction.USE_REPEATED_SHORTCUT.get());
188            add(repeatedCb);
189            add(helperCb);
190            add(projectionCb);
191            add(new DisableAction(snapHelper));
192            add(new Snap90DegreesAction(snapHelper));
193            add(new Snap45DegreesAction(snapHelper));
194            add(new Snap30DegreesAction(snapHelper));
195        }
196    }
197
198    private boolean snapOn; // snapping is turned on
199
200    private boolean active; // snapping is active for current mouse position
201    private boolean fixed; // snap angle is fixed
202    private boolean absoluteFix; // snap angle is absolute
203
204    EastNorth dir2;
205    private EastNorth projected;
206    private String labelText;
207    private double lastAngle;
208
209    private double customBaseHeading = -1; // angle of base line, if not last segment)
210    private EastNorth segmentPoint1; // remembered first point of base segment
211    private EastNorth segmentPoint2; // remembered second point of base segment
212    private EastNorth projectionSource; // point that we are projecting to the line
213
214    private double[] snapAngles;
215
216    private double pe, pn; // (pe, pn) - direction of snapping line
217    private double e0, n0; // (e0, n0) - origin of snapping line
218
219    private final String fixFmt = "%d "+tr("FIX");
220
221    private JCheckBoxMenuItem checkBox;
222
223    final MouseListener anglePopupListener;
224
225    /**
226     * Set the initial state
227     */
228    public void init() {
229        snapOn = false;
230        checkBox.setState(snapOn);
231        fixed = false;
232        absoluteFix = false;
233
234        computeSnapAngles();
235        Main.pref.addWeakKeyPreferenceChangeListener(DRAW_ANGLESNAP_ANGLES, e -> this.computeSnapAngles());
236    }
237
238    private void computeSnapAngles() {
239        snapAngles = Main.pref.getList(DRAW_ANGLESNAP_ANGLES,
240                Arrays.asList("0", "30", "45", "60", "90", "120", "135", "150", "180"))
241                .stream()
242                .mapToDouble(DrawSnapHelper::parseSnapAngle)
243                .flatMap(s -> DoubleStream.of(s, 360-s))
244                .toArray();
245    }
246
247    private static double parseSnapAngle(String string) {
248        try {
249            return Double.parseDouble(string);
250        } catch (NumberFormatException e) {
251            Logging.warn("Incorrect number in draw.anglesnap.angles preferences: {0}", string);
252            return 0;
253        }
254    }
255
256    /**
257     * Save the snap angles
258     * @param angles The angles
259     */
260    public void saveAngles(String... angles) {
261        Main.pref.putList(DRAW_ANGLESNAP_ANGLES, Arrays.asList(angles));
262    }
263
264    /**
265     * Sets the menu checkbox.
266     * @param checkBox menu checkbox
267     */
268    public void setMenuCheckBox(JCheckBoxMenuItem checkBox) {
269        this.checkBox = checkBox;
270    }
271
272    /**
273     * Draw the snap hint line.
274     * @param g2 graphics
275     * @param mv MapView state
276     * @since 10874
277     */
278    public void drawIfNeeded(Graphics2D g2, MapViewState mv) {
279        if (!snapOn || !active)
280            return;
281        MapViewPoint p1 = mv.getPointFor(drawAction.getCurrentBaseNode());
282        MapViewPoint p2 = mv.getPointFor(dir2);
283        MapViewPoint p3 = mv.getPointFor(projected);
284        if (DrawAction.DRAW_CONSTRUCTION_GEOMETRY.get()) {
285            g2.setColor(DrawAction.SNAP_HELPER_COLOR.get());
286            g2.setStroke(DrawAction.HELPER_STROKE.get());
287
288            MapViewPath b = new MapViewPath(mv);
289            b.moveTo(p2);
290            if (absoluteFix) {
291                b.lineTo(p2.interpolate(p1, 2)); // bi-directional line
292            } else {
293                b.lineTo(p3);
294            }
295            g2.draw(b);
296        }
297        if (projectionSource != null) {
298            g2.setColor(DrawAction.SNAP_HELPER_COLOR.get());
299            g2.setStroke(DrawAction.HELPER_STROKE.get());
300            MapViewPath b = new MapViewPath(mv);
301            b.moveTo(p3);
302            b.lineTo(projectionSource);
303            g2.draw(b);
304        }
305
306        if (customBaseHeading >= 0) {
307            g2.setColor(DrawAction.HIGHLIGHT_COLOR.get());
308            g2.setStroke(DrawAction.HIGHLIGHT_STROKE.get());
309            MapViewPath b = new MapViewPath(mv);
310            b.moveTo(segmentPoint1);
311            b.lineTo(segmentPoint2);
312            g2.draw(b);
313        }
314
315        g2.setColor(DrawAction.RUBBER_LINE_COLOR.get());
316        g2.setStroke(DrawAction.RUBBER_LINE_STROKE.get());
317        MapViewPath b = new MapViewPath(mv);
318        b.moveTo(p1);
319        b.lineTo(p3);
320        g2.draw(b);
321
322        g2.drawString(labelText, (int) p3.getInViewX()-5, (int) p3.getInViewY()+20);
323        if (DrawAction.SHOW_PROJECTED_POINT.get()) {
324            g2.setStroke(DrawAction.RUBBER_LINE_STROKE.get());
325            g2.draw(new MapViewPath(mv).shapeAround(p3, SymbolShape.CIRCLE, 10)); // projected point
326        }
327
328        g2.setColor(DrawAction.SNAP_HELPER_COLOR.get());
329        g2.setStroke(DrawAction.HELPER_STROKE.get());
330    }
331
332    /**
333     * If mouse position is close to line at 15-30-45-... angle, remembers this direction
334     * @param currentEN Current position
335     * @param baseHeading The heading
336     * @param curHeading The current mouse heading
337     */
338    public void checkAngleSnapping(EastNorth currentEN, double baseHeading, double curHeading) {
339        MapView mapView = MainApplication.getMap().mapView;
340        EastNorth p0 = drawAction.getCurrentBaseNode().getEastNorth();
341        EastNorth snapPoint = currentEN;
342        double angle = -1;
343
344        double activeBaseHeading = (customBaseHeading >= 0) ? customBaseHeading : baseHeading;
345
346        if (snapOn && (activeBaseHeading >= 0)) {
347            angle = curHeading - activeBaseHeading;
348            if (angle < 0) {
349                angle += 360;
350            }
351            if (angle > 360) {
352                angle = 0;
353            }
354
355            double nearestAngle;
356            if (fixed) {
357                nearestAngle = lastAngle; // if direction is fixed use previous angle
358                active = true;
359            } else {
360                nearestAngle = getNearestAngle(angle);
361                if (getAngleDelta(nearestAngle, angle) < DrawAction.SNAP_ANGLE_TOLERANCE.get()) {
362                    active = customBaseHeading >= 0 || Math.abs(nearestAngle - 180) > 1e-3;
363                    // if angle is to previous segment, exclude 180 degrees
364                    lastAngle = nearestAngle;
365                } else {
366                    active = false;
367                }
368            }
369
370            if (active) {
371                double phi;
372                e0 = p0.east();
373                n0 = p0.north();
374                buildLabelText((nearestAngle <= 180) ? nearestAngle : (nearestAngle-360));
375
376                phi = (nearestAngle + activeBaseHeading) * Math.PI / 180;
377                // (pe,pn) - direction of snapping line
378                pe = Math.sin(phi);
379                pn = Math.cos(phi);
380                double scale = 20 * mapView.getDist100Pixel();
381                dir2 = new EastNorth(e0 + scale * pe, n0 + scale * pn);
382                snapPoint = getSnapPoint(currentEN);
383            } else {
384                noSnapNow();
385            }
386        }
387
388        // find out the distance, in metres, between the base point and projected point
389        LatLon mouseLatLon = mapView.getProjection().eastNorth2latlon(snapPoint);
390        double distance = this.drawAction.getCurrentBaseNode().getCoor().greatCircleDistance(mouseLatLon);
391        double hdg = Utils.toDegrees(p0.heading(snapPoint));
392        // heading of segment from current to calculated point, not to mouse position
393
394        if (baseHeading >= 0) { // there is previous line segment with some heading
395            angle = hdg - baseHeading;
396            if (angle < 0) {
397                angle += 360;
398            }
399            if (angle > 360) {
400                angle = 0;
401            }
402        }
403        DrawAction.showStatusInfo(angle, hdg, distance, isSnapOn());
404    }
405
406    private void buildLabelText(double nearestAngle) {
407        if (DrawAction.SHOW_ANGLE.get()) {
408            if (fixed) {
409                if (absoluteFix) {
410                    labelText = "=";
411                } else {
412                    labelText = String.format(fixFmt, (int) nearestAngle);
413                }
414            } else {
415                labelText = String.format("%d", (int) nearestAngle);
416            }
417        } else {
418            if (fixed) {
419                if (absoluteFix) {
420                    labelText = "=";
421                } else {
422                    labelText = String.format(tr("FIX"), 0);
423                }
424            } else {
425                labelText = "";
426            }
427        }
428    }
429
430    /**
431     * Gets a snap point close to p. Stores the result for display.
432     * @param p The point
433     * @return The snap point close to p.
434     */
435    public EastNorth getSnapPoint(EastNorth p) {
436        if (!active)
437            return p;
438        double de = p.east()-e0;
439        double dn = p.north()-n0;
440        double l = de*pe+dn*pn;
441        double delta = MainApplication.getMap().mapView.getDist100Pixel()/20;
442        if (!absoluteFix && l < delta) {
443            active = false;
444            return p;
445        } //  do not go backward!
446
447        projectionSource = null;
448        if (DrawAction.SNAP_TO_PROJECTIONS.get()) {
449            DataSet ds = drawAction.getLayerManager().getEditDataSet();
450            Collection<Way> selectedWays = ds.getSelectedWays();
451            if (selectedWays.size() == 1) {
452                Way w = selectedWays.iterator().next();
453                Collection<EastNorth> pointsToProject = new ArrayList<>();
454                if (w.getNodesCount() < 1000) {
455                    for (Node n: w.getNodes()) {
456                        pointsToProject.add(n.getEastNorth());
457                    }
458                }
459                if (customBaseHeading >= 0) {
460                    pointsToProject.add(segmentPoint1);
461                    pointsToProject.add(segmentPoint2);
462                }
463                EastNorth enOpt = null;
464                double dOpt = 1e5;
465                for (EastNorth en: pointsToProject) { // searching for besht projection
466                    double l1 = (en.east()-e0)*pe+(en.north()-n0)*pn;
467                    double d1 = Math.abs(l1-l);
468                    if (d1 < delta && d1 < dOpt) {
469                        l = l1;
470                        enOpt = en;
471                        dOpt = d1;
472                    }
473                }
474                if (enOpt != null) {
475                    projectionSource = enOpt;
476                }
477            }
478        }
479        projected = new EastNorth(e0+l*pe, n0+l*pn);
480        return projected;
481    }
482
483    /**
484     * Disables snapping
485     */
486    void noSnapNow() {
487        active = false;
488        dir2 = null;
489        projected = null;
490        labelText = null;
491    }
492
493    void setBaseSegment(WaySegment seg) {
494        if (seg == null) return;
495        segmentPoint1 = seg.getFirstNode().getEastNorth();
496        segmentPoint2 = seg.getSecondNode().getEastNorth();
497
498        double hdg = segmentPoint1.heading(segmentPoint2);
499        hdg = Utils.toDegrees(hdg);
500        if (hdg < 0) {
501            hdg += 360;
502        }
503        if (hdg > 360) {
504            hdg -= 360;
505        }
506        customBaseHeading = hdg;
507    }
508
509    /**
510     * Enable snapping.
511     */
512    void enableSnapping() {
513        snapOn = true;
514        checkBox.setState(snapOn);
515        customBaseHeading = -1;
516        unsetFixedMode();
517    }
518
519    void toggleSnapping() {
520        snapOn = !snapOn;
521        checkBox.setState(snapOn);
522        customBaseHeading = -1;
523        unsetFixedMode();
524    }
525
526    void setFixedMode() {
527        if (active) {
528            fixed = true;
529        }
530    }
531
532    void unsetFixedMode() {
533        fixed = false;
534        absoluteFix = false;
535        lastAngle = 0;
536        active = false;
537    }
538
539    boolean isActive() {
540        return active;
541    }
542
543    boolean isSnapOn() {
544        return snapOn;
545    }
546
547    private double getNearestAngle(double angle) {
548        double bestAngle = DoubleStream.of(snapAngles).boxed()
549                .min(Comparator.comparing(snapAngle -> getAngleDelta(angle, snapAngle))).orElse(0.0);
550        if (Math.abs(bestAngle-360) < 1e-3) {
551            bestAngle = 0;
552        }
553        return bestAngle;
554    }
555
556    private static double getAngleDelta(double a, double b) {
557        double delta = Math.abs(a-b);
558        if (delta > 180)
559            return 360-delta;
560        else
561            return delta;
562    }
563
564    void unFixOrTurnOff() {
565        if (absoluteFix) {
566            unsetFixedMode();
567        } else {
568            toggleSnapping();
569        }
570    }
571}
Note: See TracBrowser for help on using the repository browser.