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

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