source: josm/trunk/src/org/openstreetmap/josm/actions/OrthogonalizeAction.java@ 14183

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

see #15229 - deprecate Main.parent and Main itself

  • Property svn:eol-style set to native
File size: 28.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.ArrayList;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.HashMap;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
20
21import javax.swing.JOptionPane;
22
23import org.openstreetmap.josm.command.Command;
24import org.openstreetmap.josm.command.MoveCommand;
25import org.openstreetmap.josm.command.SequenceCommand;
26import org.openstreetmap.josm.data.UndoRedoHandler;
27import org.openstreetmap.josm.data.coor.EastNorth;
28import org.openstreetmap.josm.data.coor.PolarCoor;
29import org.openstreetmap.josm.data.osm.Node;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.data.projection.ProjectionRegistry;
33import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
34import org.openstreetmap.josm.gui.MainApplication;
35import org.openstreetmap.josm.gui.Notification;
36import org.openstreetmap.josm.tools.Geometry;
37import org.openstreetmap.josm.tools.JosmRuntimeException;
38import org.openstreetmap.josm.tools.Logging;
39import org.openstreetmap.josm.tools.Shortcut;
40import org.openstreetmap.josm.tools.Utils;
41
42/**
43 * Tools / Orthogonalize
44 *
45 * Align edges of a way so all angles are angles of 90 or 180 degrees.
46 * See USAGE String below.
47 */
48public final class OrthogonalizeAction extends JosmAction {
49 private static final String USAGE = tr(
50 "<h3>When one or more ways are selected, the shape is adjusted such, that all angles are 90 or 180 degrees.</h3>"+
51 "You can add two nodes to the selection. Then, the direction is fixed by these two reference nodes. "+
52 "(Afterwards, you can undo the movement for certain nodes:<br>"+
53 "Select them and press the shortcut for Orthogonalize / Undo. The default is Shift-Q.)");
54
55 private static final double EPSILON = 1E-6;
56
57 /**
58 * Constructs a new {@code OrthogonalizeAction}.
59 */
60 public OrthogonalizeAction() {
61 super(tr("Orthogonalize Shape"),
62 "ortho",
63 tr("Move nodes so all angles are 90 or 180 degrees"),
64 Shortcut.registerShortcut("tools:orthogonalize", tr("Tool: {0}", tr("Orthogonalize Shape")),
65 KeyEvent.VK_Q,
66 Shortcut.DIRECT), true);
67 putValue("help", ht("/Action/OrthogonalizeShape"));
68 }
69
70 /**
71 * excepted deviation from an angle of 0, 90, 180, 360 degrees
72 * maximum value: 45 degrees
73 *
74 * Current policy is to except just everything, no matter how strange the result would be.
75 */
76 private static final double TOLERANCE1 = Utils.toRadians(45.); // within a way
77 private static final double TOLERANCE2 = Utils.toRadians(45.); // ways relative to each other
78
79 /**
80 * Remember movements, so the user can later undo it for certain nodes
81 */
82 private static final Map<Node, EastNorth> rememberMovements = new HashMap<>();
83
84 /**
85 * Undo the previous orthogonalization for certain nodes.
86 *
87 * This is useful, if the way shares nodes that you don't like to change, e.g. imports or
88 * work of another user.
89 *
90 * This action can be triggered by shortcut only.
91 */
92 public static class Undo extends JosmAction {
93 /**
94 * Constructor
95 */
96 public Undo() {
97 super(tr("Orthogonalize Shape / Undo"), "ortho",
98 tr("Undo orthogonalization for certain nodes"),
99 Shortcut.registerShortcut("tools:orthogonalizeUndo", tr("Tool: {0}", tr("Orthogonalize Shape / Undo")),
100 KeyEvent.VK_Q,
101 Shortcut.SHIFT),
102 true, "action/orthogonalize/undo", true);
103 }
104
105 @Override
106 public void actionPerformed(ActionEvent e) {
107 if (!isEnabled())
108 return;
109 final Collection<Command> commands = new LinkedList<>();
110 final Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
111 try {
112 for (OsmPrimitive p : sel) {
113 if (!(p instanceof Node)) throw new InvalidUserInputException("selected object is not a node");
114 Node n = (Node) p;
115 if (rememberMovements.containsKey(n)) {
116 EastNorth tmp = rememberMovements.get(n);
117 commands.add(new MoveCommand(n, -tmp.east(), -tmp.north()));
118 rememberMovements.remove(n);
119 }
120 }
121 if (!commands.isEmpty()) {
122 UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Orthogonalize / Undo"), commands));
123 } else {
124 throw new InvalidUserInputException("Commands are empty");
125 }
126 } catch (InvalidUserInputException ex) {
127 Logging.debug(ex);
128 new Notification(
129 tr("Orthogonalize Shape / Undo<br>"+
130 "Please select nodes that were moved by the previous Orthogonalize Shape action!"))
131 .setIcon(JOptionPane.INFORMATION_MESSAGE)
132 .show();
133 }
134 }
135
136 @Override
137 protected void updateEnabledState() {
138 updateEnabledStateOnCurrentSelection();
139 }
140
141 @Override
142 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
143 updateEnabledStateOnModifiableSelection(selection);
144 }
145 }
146
147 @Override
148 public void actionPerformed(ActionEvent e) {
149 if (!isEnabled())
150 return;
151 if ("EPSG:4326".equals(ProjectionRegistry.getProjection().toString())) {
152 String msg = tr("<html>You are using the EPSG:4326 projection which might lead<br>" +
153 "to undesirable results when doing rectangular alignments.<br>" +
154 "Change your projection to get rid of this warning.<br>" +
155 "Do you want to continue?</html>");
156 if (!ConditionalOptionPaneUtil.showConfirmationDialog(
157 "align_rectangular_4326",
158 MainApplication.getMainFrame(),
159 msg,
160 tr("Warning"),
161 JOptionPane.YES_NO_OPTION,
162 JOptionPane.QUESTION_MESSAGE,
163 JOptionPane.YES_OPTION))
164 return;
165 }
166
167 final Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
168
169 try {
170 UndoRedoHandler.getInstance().add(orthogonalize(sel));
171 } catch (InvalidUserInputException ex) {
172 Logging.debug(ex);
173 String msg;
174 if ("usage".equals(ex.getMessage())) {
175 msg = "<h2>" + tr("Usage") + "</h2>" + USAGE;
176 } else {
177 msg = ex.getMessage() + "<br><hr><h2>" + tr("Usage") + "</h2>" + USAGE;
178 }
179 new Notification(msg)
180 .setIcon(JOptionPane.INFORMATION_MESSAGE)
181 .setDuration(Notification.TIME_DEFAULT)
182 .show();
183 }
184 }
185
186 /**
187 * Rectifies the selection
188 * @param selection the selection which should be rectified
189 * @return a rectifying command
190 * @throws InvalidUserInputException if the selection is invalid
191 * @since 13670
192 */
193 public static SequenceCommand orthogonalize(Iterable<OsmPrimitive> selection) throws InvalidUserInputException {
194 final List<Node> nodeList = new ArrayList<>();
195 final List<WayData> wayDataList = new ArrayList<>();
196 // collect nodes and ways from the selection
197 for (OsmPrimitive p : selection) {
198 if (p instanceof Node) {
199 nodeList.add((Node) p);
200 } else if (p instanceof Way) {
201 if (!p.isIncomplete()) {
202 wayDataList.add(new WayData(((Way) p).getNodes()));
203 }
204 } else {
205 throw new InvalidUserInputException(tr("Selection must consist only of ways and nodes."));
206 }
207 }
208 final int nodesCount = nodeList.size();
209 if (wayDataList.isEmpty() && nodesCount > 2) {
210 final WayData data = new WayData(nodeList);
211 final Collection<Command> commands = orthogonalize(Collections.singletonList(data), Collections.<Node>emptyList());
212 return new SequenceCommand(tr("Orthogonalize"), commands);
213 } else if (wayDataList.isEmpty()) {
214 throw new InvalidUserInputException("usage");
215 } else {
216 if (nodesCount <= 2) {
217 OrthogonalizeAction.rememberMovements.clear();
218 final Collection<Command> commands = new LinkedList<>();
219
220 if (nodesCount == 2) { // fixed direction, or single node to move
221 commands.addAll(orthogonalize(wayDataList, nodeList));
222 } else if (nodesCount == 1) {
223 commands.add(orthogonalize(wayDataList, nodeList.get(0)));
224 } else if (nodesCount == 0) {
225 for (List<WayData> g : buildGroups(wayDataList)) {
226 commands.addAll(orthogonalize(g, nodeList));
227 }
228 }
229
230 return new SequenceCommand(tr("Orthogonalize"), commands);
231
232 } else {
233 throw new InvalidUserInputException("usage");
234 }
235 }
236 }
237
238 /**
239 * Collect groups of ways with common nodes in order to orthogonalize each group separately.
240 * @param wayDataList list of ways
241 * @return groups of ways with common nodes
242 */
243 private static List<List<WayData>> buildGroups(List<WayData> wayDataList) {
244 List<List<WayData>> groups = new ArrayList<>();
245 Set<WayData> remaining = new HashSet<>(wayDataList);
246 while (!remaining.isEmpty()) {
247 List<WayData> group = new ArrayList<>();
248 groups.add(group);
249 Iterator<WayData> it = remaining.iterator();
250 WayData next = it.next();
251 it.remove();
252 extendGroupRec(group, next, new ArrayList<>(remaining));
253 remaining.removeAll(group);
254 }
255 return groups;
256 }
257
258 private static void extendGroupRec(List<WayData> group, WayData newGroupMember, List<WayData> remaining) {
259 group.add(newGroupMember);
260 for (int i = 0; i < remaining.size(); ++i) {
261 WayData candidate = remaining.get(i);
262 if (candidate == null) continue;
263 if (!Collections.disjoint(candidate.wayNodes, newGroupMember.wayNodes)) {
264 remaining.set(i, null);
265 extendGroupRec(group, candidate, remaining);
266 }
267 }
268 }
269
270 /**
271 * Try to orthogonalize the given ways by moving only a single given node
272 * @param wayDataList list of ways
273 * @param singleNode common node to ways to orthogonalize. Only this one will be moved
274 * @return the command to move the node
275 * @throws InvalidUserInputException if the command cannot be computed
276 */
277 private static Command orthogonalize(List<WayData> wayDataList, Node singleNode) throws InvalidUserInputException {
278 List<EastNorth> rightAnglePositions = new ArrayList<>();
279 int wayCount = wayDataList.size();
280 for (WayData wd : wayDataList) {
281 int n = wd.wayNodes.size();
282 int i = wd.wayNodes.indexOf(singleNode);
283 Node n0, n2;
284 if (i == 0 && n >= 3 && singleNode.equals(wd.wayNodes.get(n-1))) {
285 n0 = wd.wayNodes.get(n-2);
286 n2 = wd.wayNodes.get(1);
287 } else if (i > 0 && i < n-1) {
288 n0 = wd.wayNodes.get(i-1);
289 n2 = wd.wayNodes.get(i+1);
290 } else {
291 continue;
292 }
293 EastNorth n0en = n0.getEastNorth();
294 EastNorth n1en = singleNode.getEastNorth();
295 EastNorth n2en = n2.getEastNorth();
296 double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(n0en, n1en, n2en));
297 if (wayCount == 1 || (80 <= angle && angle <= 100)) {
298 EastNorth c = n0en.getCenter(n2en);
299 double r = n0en.distance(n2en) / 2d;
300 double vX = n1en.east() - c.east();
301 double vY = n1en.north() - c.north();
302 double magV = Math.sqrt(vX*vX + vY*vY);
303 rightAnglePositions.add(new EastNorth(c.east() + vX / magV * r,
304 c.north() + vY / magV * r));
305 }
306 }
307 if (rightAnglePositions.isEmpty()) {
308 throw new InvalidUserInputException("Unable to orthogonalize " + singleNode);
309 }
310 return new MoveCommand(singleNode, ProjectionRegistry.getProjection().eastNorth2latlon(Geometry.getCentroidEN(rightAnglePositions)));
311 }
312
313 /**
314 *
315 * Outline:
316 * 1. Find direction of all segments
317 * - direction = 0..3 (right,up,left,down)
318 * - right is not really right, you may have to turn your screen
319 * 2. Find average heading of all segments
320 * - heading = angle of a vector in polar coordinates
321 * - sum up horizontal segments (those with direction 0 or 2)
322 * - sum up vertical segments
323 * - turn the vertical sum by 90 degrees and add it to the horizontal sum
324 * - get the average heading from this total sum
325 * 3. Rotate all nodes by the average heading so that right is really right
326 * and all segments are approximately NS or EW.
327 * 4. If nodes are connected by a horizontal segment: Replace their y-Coordinate by
328 * the mean value of their y-Coordinates.
329 * - The same for vertical segments.
330 * 5. Rotate back.
331 * @param wayDataList list of ways
332 * @param headingNodes list of heading nodes
333 * @return list of commands to perform
334 * @throws InvalidUserInputException if selected ways have an angle different from 90 or 180 degrees
335 **/
336 private static Collection<Command> orthogonalize(List<WayData> wayDataList, List<Node> headingNodes) throws InvalidUserInputException {
337 // find average heading
338 double headingAll;
339 try {
340 if (headingNodes.isEmpty()) {
341 // find directions of the segments and make them consistent between different ways
342 wayDataList.get(0).calcDirections(Direction.RIGHT);
343 double refHeading = wayDataList.get(0).heading;
344 EastNorth totSum = new EastNorth(0., 0.);
345 for (WayData w : wayDataList) {
346 w.calcDirections(Direction.RIGHT);
347 int directionOffset = angleToDirectionChange(w.heading - refHeading, TOLERANCE2);
348 w.calcDirections(Direction.RIGHT.changeBy(directionOffset));
349 if (angleToDirectionChange(refHeading - w.heading, TOLERANCE2) != 0)
350 throw new JosmRuntimeException("orthogonalize error");
351 totSum = EN.sum(totSum, w.segSum);
352 }
353 headingAll = EN.polar(EastNorth.ZERO, totSum);
354 } else {
355 headingAll = EN.polar(headingNodes.get(0).getEastNorth(), headingNodes.get(1).getEastNorth());
356 for (WayData w : wayDataList) {
357 w.calcDirections(Direction.RIGHT);
358 int directionOffset = angleToDirectionChange(w.heading - headingAll, TOLERANCE2);
359 w.calcDirections(Direction.RIGHT.changeBy(directionOffset));
360 }
361 }
362 } catch (RejectedAngleException ex) {
363 throw new InvalidUserInputException(
364 tr("<html>Please make sure all selected ways head in a similar direction<br>"+
365 "or orthogonalize them one by one.</html>"), ex);
366 }
367
368 // put the nodes of all ways in a set
369 final Set<Node> allNodes = new HashSet<>();
370 for (WayData w : wayDataList) {
371 allNodes.addAll(w.wayNodes);
372 }
373
374 // the new x and y value for each node
375 final Map<Node, Double> nX = new HashMap<>();
376 final Map<Node, Double> nY = new HashMap<>();
377
378 // calculate the centroid of all nodes
379 // it is used as rotation center
380 EastNorth pivot = EastNorth.ZERO;
381 for (Node n : allNodes) {
382 pivot = EN.sum(pivot, n.getEastNorth());
383 }
384 pivot = new EastNorth(pivot.east() / allNodes.size(), pivot.north() / allNodes.size());
385
386 // rotate
387 for (Node n: allNodes) {
388 EastNorth tmp = EN.rotateCC(pivot, n.getEastNorth(), -headingAll);
389 nX.put(n, tmp.east());
390 nY.put(n, tmp.north());
391 }
392
393 // orthogonalize
394 final Direction[] horizontal = {Direction.RIGHT, Direction.LEFT};
395 final Direction[] vertical = {Direction.UP, Direction.DOWN};
396 final Direction[][] orientations = {horizontal, vertical};
397 for (Direction[] orientation : orientations) {
398 final Set<Node> s = new HashSet<>(allNodes);
399 int size = s.size();
400 for (int dummy = 0; dummy < size; ++dummy) {
401 if (s.isEmpty()) {
402 break;
403 }
404 final Node dummyN = s.iterator().next(); // pick arbitrary element of s
405
406 final Set<Node> cs = new HashSet<>(); // will contain each node that can be reached from dummyN
407 cs.add(dummyN); // walking only on horizontal / vertical segments
408
409 boolean somethingHappened = true;
410 while (somethingHappened) {
411 somethingHappened = false;
412 for (WayData w : wayDataList) {
413 for (int i = 0; i < w.nSeg; ++i) {
414 Node n1 = w.wayNodes.get(i);
415 Node n2 = w.wayNodes.get(i+1);
416 if (Arrays.asList(orientation).contains(w.segDirections[i])) {
417 if (cs.contains(n1) && !cs.contains(n2)) {
418 cs.add(n2);
419 somethingHappened = true;
420 }
421 if (cs.contains(n2) && !cs.contains(n1)) {
422 cs.add(n1);
423 somethingHappened = true;
424 }
425 }
426 }
427 }
428 }
429
430 final Map<Node, Double> nC = (orientation == horizontal) ? nY : nX;
431
432 double average = 0;
433 for (Node n : cs) {
434 s.remove(n);
435 average += nC.get(n).doubleValue();
436 }
437 average = average / cs.size();
438
439 // if one of the nodes is a heading node, forget about the average and use its value
440 for (Node fn : headingNodes) {
441 if (cs.contains(fn)) {
442 average = nC.get(fn);
443 }
444 }
445
446 // At this point, the two heading nodes (if any) are horizontally aligned, i.e. they
447 // have the same y coordinate. So in general we shouldn't find them in a vertical string
448 // of segments. This can still happen in some pathological cases (see #7889). To avoid
449 // both heading nodes collapsing to one point, we simply skip this segment string and
450 // don't touch the node coordinates.
451 if (orientation == vertical && headingNodes.size() == 2 && cs.containsAll(headingNodes)) {
452 continue;
453 }
454
455 for (Node n : cs) {
456 nC.put(n, average);
457 }
458 }
459 if (!s.isEmpty()) throw new JosmRuntimeException("orthogonalize error");
460 }
461
462 // rotate back and log the change
463 final Collection<Command> commands = new LinkedList<>();
464 for (Node n: allNodes) {
465 EastNorth tmp = new EastNorth(nX.get(n), nY.get(n));
466 tmp = EN.rotateCC(pivot, tmp, headingAll);
467 final double dx = tmp.east() - n.getEastNorth().east();
468 final double dy = tmp.north() - n.getEastNorth().north();
469 if (headingNodes.contains(n)) { // The heading nodes should not have changed
470 if (Math.abs(dx) > Math.abs(EPSILON * tmp.east()) ||
471 Math.abs(dy) > Math.abs(EPSILON * tmp.east()))
472 throw new AssertionError("heading node has changed");
473 } else {
474 OrthogonalizeAction.rememberMovements.put(n, new EastNorth(dx, dy));
475 commands.add(new MoveCommand(n, dx, dy));
476 }
477 }
478 return commands;
479 }
480
481 /**
482 * Class contains everything we need to know about a single way.
483 */
484 private static class WayData {
485 /** The assigned way */
486 public final List<Node> wayNodes;
487 /** Number of Segments of the Way */
488 public final int nSeg;
489 /** Number of Nodes of the Way */
490 public final int nNode;
491 /** Direction of the segments */
492 public final Direction[] segDirections;
493 // segment i goes from node i to node (i+1)
494 /** (Vector-)sum of all horizontal segments plus the sum of all vertical */
495 public EastNorth segSum;
496 // segments turned by 90 degrees
497 /** heading of segSum == approximate heading of the way */
498 public double heading;
499
500 WayData(List<Node> wayNodes) {
501 this.wayNodes = wayNodes;
502 this.nNode = wayNodes.size();
503 this.nSeg = nNode - 1;
504 this.segDirections = new Direction[nSeg];
505 }
506
507 /**
508 * Estimate the direction of the segments, given the first segment points in the
509 * direction <code>pInitialDirection</code>.
510 * Then sum up all horizontal / vertical segments to have a good guess for the
511 * heading of the entire way.
512 * @param pInitialDirection initial direction
513 * @throws InvalidUserInputException if selected ways have an angle different from 90 or 180 degrees
514 */
515 public void calcDirections(Direction pInitialDirection) throws InvalidUserInputException {
516 final EastNorth[] en = new EastNorth[nNode]; // alias: wayNodes.get(i).getEastNorth() ---> en[i]
517 for (int i = 0; i < nNode; i++) {
518 en[i] = wayNodes.get(i).getEastNorth();
519 }
520 Direction direction = pInitialDirection;
521 segDirections[0] = direction;
522 for (int i = 0; i < nSeg - 1; i++) {
523 double h1 = EN.polar(en[i], en[i+1]);
524 double h2 = EN.polar(en[i+1], en[i+2]);
525 try {
526 direction = direction.changeBy(angleToDirectionChange(h2 - h1, TOLERANCE1));
527 } catch (RejectedAngleException ex) {
528 throw new InvalidUserInputException(tr("Please select ways with angles of approximately 90 or 180 degrees."), ex);
529 }
530 segDirections[i+1] = direction;
531 }
532
533 // sum up segments
534 EastNorth h = new EastNorth(0., 0.);
535 EastNorth v = new EastNorth(0., 0.);
536 for (int i = 0; i < nSeg; ++i) {
537 EastNorth segment = EN.diff(en[i+1], en[i]);
538 if (segDirections[i] == Direction.RIGHT) {
539 h = EN.sum(h, segment);
540 } else if (segDirections[i] == Direction.UP) {
541 v = EN.sum(v, segment);
542 } else if (segDirections[i] == Direction.LEFT) {
543 h = EN.diff(h, segment);
544 } else if (segDirections[i] == Direction.DOWN) {
545 v = EN.diff(v, segment);
546 } else throw new IllegalStateException();
547 }
548 // rotate the vertical vector by 90 degrees (clockwise) and add it to the horizontal vector
549 segSum = EN.sum(h, new EastNorth(v.north(), -v.east()));
550 this.heading = EN.polar(new EastNorth(0., 0.), segSum);
551 }
552 }
553
554 enum Direction {
555 RIGHT, UP, LEFT, DOWN;
556 public Direction changeBy(int directionChange) {
557 int tmp = (this.ordinal() + directionChange) % 4;
558 if (tmp < 0) {
559 tmp += 4; // the % operator can return negative value
560 }
561 return Direction.values()[tmp];
562 }
563 }
564
565 /**
566 * Make sure angle (up to 2*Pi) is in interval [ 0, 2*Pi ).
567 * @param a angle
568 * @return correct angle
569 */
570 private static double standardAngle0to2PI(double a) {
571 while (a >= 2 * Math.PI) {
572 a -= 2 * Math.PI;
573 }
574 while (a < 0) {
575 a += 2 * Math.PI;
576 }
577 return a;
578 }
579
580 /**
581 * Make sure angle (up to 2*Pi) is in interval ( -Pi, Pi ].
582 * @param a angle
583 * @return correct angle
584 */
585 private static double standardAngleMPItoPI(double a) {
586 while (a > Math.PI) {
587 a -= 2 * Math.PI;
588 }
589 while (a <= -Math.PI) {
590 a += 2 * Math.PI;
591 }
592 return a;
593 }
594
595 /**
596 * Class contains some auxiliary functions
597 */
598 static final class EN {
599 private EN() {
600 // Hide implicit public constructor for utility class
601 }
602
603 /**
604 * Rotate counter-clock-wise.
605 * @param pivot pivot
606 * @param en original east/north
607 * @param angle angle, in radians
608 * @return new east/north
609 */
610 public static EastNorth rotateCC(EastNorth pivot, EastNorth en, double angle) {
611 double cosPhi = Math.cos(angle);
612 double sinPhi = Math.sin(angle);
613 double x = en.east() - pivot.east();
614 double y = en.north() - pivot.north();
615 double nx = cosPhi * x - sinPhi * y + pivot.east();
616 double ny = sinPhi * x + cosPhi * y + pivot.north();
617 return new EastNorth(nx, ny);
618 }
619
620 public static EastNorth sum(EastNorth en1, EastNorth en2) {
621 return new EastNorth(en1.east() + en2.east(), en1.north() + en2.north());
622 }
623
624 public static EastNorth diff(EastNorth en1, EastNorth en2) {
625 return new EastNorth(en1.east() - en2.east(), en1.north() - en2.north());
626 }
627
628 public static double polar(EastNorth en1, EastNorth en2) {
629 return PolarCoor.computeAngle(en2, en1);
630 }
631 }
632
633 /**
634 * Recognize angle to be approximately 0, 90, 180 or 270 degrees.
635 * returns an integral value, corresponding to a counter clockwise turn.
636 * @param a angle, in radians
637 * @param deltaMax maximum tolerance, in radians
638 * @return an integral value, corresponding to a counter clockwise turn
639 * @throws RejectedAngleException in case of invalid angle
640 */
641 private static int angleToDirectionChange(double a, double deltaMax) throws RejectedAngleException {
642 a = standardAngleMPItoPI(a);
643 double d0 = Math.abs(a);
644 double d90 = Math.abs(a - Math.PI / 2);
645 double dm90 = Math.abs(a + Math.PI / 2);
646 int dirChange;
647 if (d0 < deltaMax) {
648 dirChange = 0;
649 } else if (d90 < deltaMax) {
650 dirChange = 1;
651 } else if (dm90 < deltaMax) {
652 dirChange = -1;
653 } else {
654 a = standardAngle0to2PI(a);
655 double d180 = Math.abs(a - Math.PI);
656 if (d180 < deltaMax) {
657 dirChange = 2;
658 } else
659 throw new RejectedAngleException();
660 }
661 return dirChange;
662 }
663
664 /**
665 * Exception: unsuited user input
666 * @since 13670
667 */
668 public static final class InvalidUserInputException extends Exception {
669 InvalidUserInputException(String message) {
670 super(message);
671 }
672
673 InvalidUserInputException(String message, Throwable cause) {
674 super(message, cause);
675 }
676 }
677
678 /**
679 * Exception: angle cannot be recognized as 0, 90, 180 or 270 degrees
680 */
681 protected static class RejectedAngleException extends Exception {
682 RejectedAngleException() {
683 super();
684 }
685 }
686
687 @Override
688 protected void updateEnabledState() {
689 updateEnabledStateOnCurrentSelection();
690 }
691
692 @Override
693 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
694 updateEnabledStateOnModifiableSelection(selection);
695 }
696}
Note: See TracBrowser for help on using the repository browser.