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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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