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, 7 years ago

see #15229 - fix deprecations caused by [12840]

  • Property svn:eol-style set to native
File size: 18.7 KB
RevLine 
[11495]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;
[12630]27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.MapView;
[11495]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;
[12620]34import org.openstreetmap.josm.tools.Logging;
[12131]35import org.openstreetmap.josm.tools.Utils;
[11495]36
[12581]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 */
[11495]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
[11502]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
[11495]178 private static final class AnglePopupMenu extends JPopupMenu {
179
180 private AnglePopupMenu(final DrawSnapHelper snapHelper) {
[11534]181 JCheckBoxMenuItem repeatedCb = new JCheckBoxMenuItem(new RepeatedAction(snapHelper));
182 JCheckBoxMenuItem helperCb = new JCheckBoxMenuItem(new HelperAction(snapHelper));
183 JCheckBoxMenuItem projectionCb = new JCheckBoxMenuItem(new ProjectionAction(snapHelper));
[11495]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);
[11502]191 add(new DisableAction(snapHelper));
192 add(new Snap90DegreesAction(snapHelper));
193 add(new Snap45DegreesAction(snapHelper));
194 add(new Snap30DegreesAction(snapHelper));
[11495]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() {
[12841]239 snapAngles = Main.pref.getList(DRAW_ANGLESNAP_ANGLES,
[11495]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) {
[12620]251 Logging.warn("Incorrect number in draw.anglesnap.angles preferences: {0}", string);
[11495]252 return 0;
253 }
254 }
255
256 /**
257 * Save the snap angles
258 * @param angles The angles
259 */
[11747]260 public void saveAngles(String... angles) {
[12841]261 Main.pref.putList(DRAW_ANGLESNAP_ANGLES, Arrays.asList(angles));
[11495]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) {
[12630]339 MapView mapView = MainApplication.getMap().mapView;
[11495]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);
[12630]380 double scale = 20 * mapView.getDist100Pixel();
[11495]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
[12630]389 LatLon mouseLatLon = mapView.getProjection().eastNorth2latlon(snapPoint);
[11495]390 double distance = this.drawAction.getCurrentBaseNode().getCoor().greatCircleDistance(mouseLatLon);
[12131]391 double hdg = Utils.toDegrees(p0.heading(snapPoint));
[11495]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;
[12630]441 double delta = MainApplication.getMap().mapView.getDist100Pixel()/20;
[11495]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);
[12131]499 hdg = Utils.toDegrees(hdg);
[11495]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.