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

Last change on this file since 12581 was 12581, checked in by bastiK, 7 years ago

see #14794 - javadoc

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