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

Last change on this file since 12346 was 12131, checked in by Don-vip, 7 years ago

see #11889, see #11924, see #13387 - use backported versions of Math.toDegrees/toRadians (more accurate and faster) - to revert when migrating to Java 9

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