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

Last change on this file since 13434 was 13434, checked in by Don-vip, 6 years ago

see #8039, see #10456 - support read-only data layers

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