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

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

sonar - fb-contrib:FCBL_FIELD_COULD_BE_LOCAL - Correctness - Class defines fields that are used only as locals

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