source: josm/trunk/src/org/openstreetmap/josm/gui/MapViewState.java@ 11991

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

see #13665 - cleaner error

  • Property svn:eol-style set to native
File size: 24.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import java.awt.Container;
5import java.awt.Point;
6import java.awt.geom.AffineTransform;
7import java.awt.geom.Area;
8import java.awt.geom.Path2D;
9import java.awt.geom.Point2D;
10import java.awt.geom.Point2D.Double;
11import java.awt.geom.Rectangle2D;
12import java.io.Serializable;
13import java.util.Objects;
14import java.util.Optional;
15
16import javax.swing.JComponent;
17
18import org.openstreetmap.josm.Main;
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.ProjectionBounds;
21import org.openstreetmap.josm.data.coor.EastNorth;
22import org.openstreetmap.josm.data.coor.LatLon;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.projection.Projecting;
25import org.openstreetmap.josm.data.projection.Projection;
26import org.openstreetmap.josm.gui.download.DownloadDialog;
27import org.openstreetmap.josm.tools.CheckParameterUtil;
28import org.openstreetmap.josm.tools.Geometry;
29import org.openstreetmap.josm.tools.JosmRuntimeException;
30import org.openstreetmap.josm.tools.bugreport.BugReport;
31
32/**
33 * This class represents a state of the {@link MapView}.
34 * @author Michael Zangl
35 * @since 10343
36 */
37public final class MapViewState implements Serializable {
38
39 private static final long serialVersionUID = 1L;
40
41 /**
42 * A flag indicating that the point is outside to the top of the map view.
43 * @since 10827
44 */
45 public static final int OUTSIDE_TOP = 1;
46
47 /**
48 * A flag indicating that the point is outside to the bottom of the map view.
49 * @since 10827
50 */
51 public static final int OUTSIDE_BOTTOM = 2;
52
53 /**
54 * A flag indicating that the point is outside to the left of the map view.
55 * @since 10827
56 */
57 public static final int OUTSIDE_LEFT = 4;
58
59 /**
60 * A flag indicating that the point is outside to the right of the map view.
61 * @since 10827
62 */
63 public static final int OUTSIDE_RIGHT = 8;
64
65 /**
66 * Additional pixels outside the view for where to start clipping.
67 */
68 private static final int CLIP_BOUNDS = 50;
69
70 private final transient Projecting projecting;
71
72 private final int viewWidth;
73 private final int viewHeight;
74
75 private final double scale;
76
77 /**
78 * Top left {@link EastNorth} coordinate of the view.
79 */
80 private final EastNorth topLeft;
81
82 private final Point topLeftOnScreen;
83 private final Point topLeftInWindow;
84
85 /**
86 * Create a new {@link MapViewState}
87 * @param projection The projection to use.
88 * @param viewWidth The view width
89 * @param viewHeight The view height
90 * @param scale The scale to use
91 * @param topLeft The top left corner in east/north space.
92 * @param topLeftInWindow The top left point in window
93 * @param topLeftOnScreen The top left point on screen
94 */
95 private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft,
96 Point topLeftInWindow, Point topLeftOnScreen) {
97 CheckParameterUtil.ensureParameterNotNull(projection, "projection");
98 CheckParameterUtil.ensureParameterNotNull(topLeft, "topLeft");
99 CheckParameterUtil.ensureParameterNotNull(topLeftInWindow, "topLeftInWindow");
100 CheckParameterUtil.ensureParameterNotNull(topLeftOnScreen, "topLeftOnScreen");
101
102 this.projecting = projection;
103 this.scale = scale;
104 this.topLeft = topLeft;
105
106 this.viewWidth = viewWidth;
107 this.viewHeight = viewHeight;
108 this.topLeftInWindow = topLeftInWindow;
109 this.topLeftOnScreen = topLeftOnScreen;
110 }
111
112 private MapViewState(Projecting projection, int viewWidth, int viewHeight, double scale, EastNorth topLeft) {
113 this(projection, viewWidth, viewHeight, scale, topLeft, new Point(0, 0), new Point(0, 0));
114 }
115
116 private MapViewState(EastNorth topLeft, MapViewState mvs) {
117 this(mvs.projecting, mvs.viewWidth, mvs.viewHeight, mvs.scale, topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen);
118 }
119
120 private MapViewState(double scale, MapViewState mvs) {
121 this(mvs.projecting, mvs.viewWidth, mvs.viewHeight, scale, mvs.topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen);
122 }
123
124 private MapViewState(JComponent position, MapViewState mvs) {
125 this(mvs.projecting, position.getWidth(), position.getHeight(), mvs.scale, mvs.topLeft,
126 findTopLeftInWindow(position), findTopLeftOnScreen(position));
127 }
128
129 private MapViewState(Projecting projecting, MapViewState mvs) {
130 this(projecting, mvs.viewWidth, mvs.viewHeight, mvs.scale, mvs.topLeft, mvs.topLeftInWindow, mvs.topLeftOnScreen);
131 }
132
133 private static Point findTopLeftInWindow(JComponent position) {
134 Point result = new Point();
135 // better than using swing utils, since this allows us to use the method if no screen is present.
136 Container component = position;
137 while (component != null) {
138 result.x += component.getX();
139 result.y += component.getY();
140 component = component.getParent();
141 }
142 return result;
143 }
144
145 private static Point findTopLeftOnScreen(JComponent position) {
146 try {
147 return position.getLocationOnScreen();
148 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
149 throw BugReport.intercept(e).put("position", position).put("parent", position::getParent);
150 }
151 }
152
153 /**
154 * The scale in east/north units per pixel.
155 * @return The scale.
156 */
157 public double getScale() {
158 return scale;
159 }
160
161 /**
162 * Gets the MapViewPoint representation for a position in view coordinates.
163 * @param x The x coordinate inside the view.
164 * @param y The y coordinate inside the view.
165 * @return The MapViewPoint.
166 */
167 public MapViewPoint getForView(double x, double y) {
168 return new MapViewViewPoint(x, y);
169 }
170
171 /**
172 * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate.
173 * @param eastNorth the position.
174 * @return The point for that position.
175 */
176 public MapViewPoint getPointFor(EastNorth eastNorth) {
177 return new MapViewEastNorthPoint(eastNorth);
178 }
179
180 /**
181 * Gets the {@link MapViewPoint} for the given {@link LatLon} coordinate.
182 * @param latlon the position
183 * @return The point for that position.
184 * @since 10651
185 */
186 public MapViewPoint getPointFor(LatLon latlon) {
187 return getPointFor(getProjection().latlon2eastNorth(latlon));
188 }
189
190 /**
191 * Gets the {@link MapViewPoint} for the given node.
192 * This is faster than {@link #getPointFor(LatLon)} because it uses the node east/north cache.
193 * @param node The node
194 * @return The position of that node.
195 * @since 10827
196 */
197 public MapViewPoint getPointFor(Node node) {
198 try {
199 return getPointFor(Optional.ofNullable(node.getEastNorth(getProjection()))
200 .orElseThrow(IllegalArgumentException::new));
201 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
202 throw BugReport.intercept(e).put("node", node);
203 }
204 }
205
206 /**
207 * Gets a rectangle representing the whole view area.
208 * @return The rectangle.
209 */
210 public MapViewRectangle getViewArea() {
211 return getForView(0, 0).rectTo(getForView(viewWidth, viewHeight));
212 }
213
214 /**
215 * Gets a rectangle of the view as map view area.
216 * @param rectangle The rectangle to get.
217 * @return The view area.
218 * @since 10827
219 */
220 public MapViewRectangle getViewArea(Rectangle2D rectangle) {
221 return getForView(rectangle.getMinX(), rectangle.getMinY()).rectTo(getForView(rectangle.getMaxX(), rectangle.getMaxY()));
222 }
223
224 /**
225 * Gets the center of the view.
226 * @return The center position.
227 */
228 public MapViewPoint getCenter() {
229 return getForView(viewWidth / 2.0, viewHeight / 2.0);
230 }
231
232 /**
233 * Gets the center of the view, rounded to a pixel coordinate
234 * @return The center position.
235 * @since 10856
236 */
237 public MapViewPoint getCenterAtPixel() {
238 return getForView(viewWidth / 2, viewHeight / 2);
239 }
240
241 /**
242 * Gets the width of the view on the Screen;
243 * @return The width of the view component in screen pixel.
244 */
245 public double getViewWidth() {
246 return viewWidth;
247 }
248
249 /**
250 * Gets the height of the view on the Screen;
251 * @return The height of the view component in screen pixel.
252 */
253 public double getViewHeight() {
254 return viewHeight;
255 }
256
257 /**
258 * Gets the current projection used for the MapView.
259 * @return The projection.
260 */
261 public Projection getProjection() {
262 return projecting.getBaseProjection();
263 }
264
265 /**
266 * Creates an affine transform that is used to convert the east/north coordinates to view coordinates.
267 * @return The affine transform. It should not be changed.
268 * @since 10375
269 */
270 public AffineTransform getAffineTransform() {
271 return new AffineTransform(1.0 / scale, 0.0, 0.0, -1.0 / scale, -topLeft.east() / scale,
272 topLeft.north() / scale);
273 }
274
275 /**
276 * Gets a rectangle that is several pixel bigger than the view. It is used to define the view clipping.
277 * @return The rectangle.
278 */
279 public MapViewRectangle getViewClipRectangle() {
280 return getForView(-CLIP_BOUNDS, -CLIP_BOUNDS).rectTo(getForView(getViewWidth() + CLIP_BOUNDS, getViewHeight() + CLIP_BOUNDS));
281 }
282
283 /**
284 * Returns the area for the given bounds.
285 * @param bounds bounds
286 * @return the area for the given bounds
287 */
288 public Area getArea(Bounds bounds) {
289 Path2D area = new Path2D.Double();
290 bounds.visitEdge(getProjection(), latlon -> {
291 MapViewPoint point = getPointFor(latlon);
292 if (area.getCurrentPoint() == null) {
293 area.moveTo(point.getInViewX(), point.getInViewY());
294 } else {
295 area.lineTo(point.getInViewX(), point.getInViewY());
296 }
297 });
298 area.closePath();
299 return new Area(area);
300 }
301
302 /**
303 * Creates a new state that is the same as the current state except for that it is using a new center.
304 * @param newCenter The new center coordinate.
305 * @return The new state.
306 * @since 10375
307 */
308 public MapViewState usingCenter(EastNorth newCenter) {
309 return movedTo(getCenter(), newCenter);
310 }
311
312 /**
313 * @param mapViewPoint The reference point.
314 * @param newEastNorthThere The east/north coordinate that should be there.
315 * @return The new state.
316 * @since 10375
317 */
318 public MapViewState movedTo(MapViewPoint mapViewPoint, EastNorth newEastNorthThere) {
319 EastNorth delta = newEastNorthThere.subtract(mapViewPoint.getEastNorth());
320 if (delta.distanceSq(0, 0) < .1e-20) {
321 return this;
322 } else {
323 return new MapViewState(topLeft.add(delta), this);
324 }
325 }
326
327 /**
328 * Creates a new state that is the same as the current state except for that it is using a new scale.
329 * @param newScale The new scale to use.
330 * @return The new state.
331 * @since 10375
332 */
333 public MapViewState usingScale(double newScale) {
334 return new MapViewState(newScale, this);
335 }
336
337 /**
338 * Creates a new state that is the same as the current state except for that it is using the location of the given component.
339 * <p>
340 * The view is moved so that the center is the same as the old center.
341 * @param positon The new location to use.
342 * @return The new state.
343 * @since 10375
344 */
345 public MapViewState usingLocation(JComponent positon) {
346 EastNorth center = this.getCenter().getEastNorth();
347 return new MapViewState(positon, this).usingCenter(center);
348 }
349
350 /**
351 * Creates a state that uses the projection.
352 * @param projection The projection to use.
353 * @return The new state.
354 * @since 10486
355 */
356 public MapViewState usingProjection(Projection projection) {
357 if (projection.equals(this.projecting)) {
358 return this;
359 } else {
360 return new MapViewState(projection, this);
361 }
362 }
363
364 /**
365 * Create the default {@link MapViewState} object for the given map view. The screen position won't be set so that this method can be used
366 * before the view was added to the hirarchy.
367 * @param width The view width
368 * @param height The view height
369 * @return The state
370 * @since 10375
371 */
372 public static MapViewState createDefaultState(int width, int height) {
373 Projection projection = Main.getProjection();
374 double scale = projection.getDefaultZoomInPPD();
375 MapViewState state = new MapViewState(projection, width, height, scale, new EastNorth(0, 0));
376 EastNorth center = calculateDefaultCenter();
377 return state.movedTo(state.getCenter(), center);
378 }
379
380 private static EastNorth calculateDefaultCenter() {
381 Bounds b = Optional.ofNullable(DownloadDialog.getSavedDownloadBounds()).orElseGet(
382 () -> Main.getProjection().getWorldBoundsLatLon());
383 return Main.getProjection().latlon2eastNorth(b.getCenter());
384 }
385
386 /**
387 * Check if this MapViewState equals another one, disregarding the position
388 * of the JOSM window on screen.
389 * @param other the other MapViewState
390 * @return true if the other MapViewState has the same size, scale, position and projection,
391 * false otherwise
392 */
393 public boolean equalsInWindow(MapViewState other) {
394 return other != null &&
395 this.viewWidth == other.viewWidth &&
396 this.viewHeight == other.viewHeight &&
397 this.scale == other.scale &&
398 Objects.equals(this.topLeft, other.topLeft) &&
399 Objects.equals(this.projecting, other.projecting);
400 }
401
402 /**
403 * A class representing a point in the map view. It allows to convert between the different coordinate systems.
404 * @author Michael Zangl
405 */
406 public abstract class MapViewPoint {
407
408 /**
409 * Get this point in view coordinates.
410 * @return The point in view coordinates.
411 */
412 public Point2D getInView() {
413 return new Point2D.Double(getInViewX(), getInViewY());
414 }
415
416 /**
417 * Get the x coordinate in view space without creating an intermediate object.
418 * @return The x coordinate
419 * @since 10827
420 */
421 public abstract double getInViewX();
422
423 /**
424 * Get the y coordinate in view space without creating an intermediate object.
425 * @return The y coordinate
426 * @since 10827
427 */
428 public abstract double getInViewY();
429
430 /**
431 * Convert this point to window coordinates.
432 * @return The point in window coordinates.
433 */
434 public Point2D getInWindow() {
435 return getUsingCorner(topLeftInWindow);
436 }
437
438 /**
439 * Convert this point to screen coordinates.
440 * @return The point in screen coordinates.
441 */
442 public Point2D getOnScreen() {
443 return getUsingCorner(topLeftOnScreen);
444 }
445
446 private Double getUsingCorner(Point corner) {
447 return new Point2D.Double(corner.getX() + getInViewX(), corner.getY() + getInViewY());
448 }
449
450 /**
451 * Gets the {@link EastNorth} coordinate of this point.
452 * @return The east/north coordinate.
453 */
454 public EastNorth getEastNorth() {
455 return new EastNorth(topLeft.east() + getInViewX() * scale, topLeft.north() - getInViewY() * scale);
456 }
457
458 /**
459 * Create a rectangle from this to the other point.
460 * @param other The other point. Needs to be of the same {@link MapViewState}
461 * @return A rectangle.
462 */
463 public MapViewRectangle rectTo(MapViewPoint other) {
464 return new MapViewRectangle(this, other);
465 }
466
467 /**
468 * Gets the current position in LatLon coordinates according to the current projection.
469 * @return The positon as LatLon.
470 * @see #getLatLonClamped()
471 */
472 public LatLon getLatLon() {
473 return projecting.getBaseProjection().eastNorth2latlon(getEastNorth());
474 }
475
476 /**
477 * Gets the latlon coordinate clamped to the current world area.
478 * @return The lat/lon coordinate
479 * @since 10805
480 */
481 public LatLon getLatLonClamped() {
482 return projecting.eastNorth2latlonClamped(getEastNorth());
483 }
484
485 /**
486 * Add the given offset to this point
487 * @param en The offset in east/north space.
488 * @return The new point
489 * @since 10651
490 */
491 public MapViewPoint add(EastNorth en) {
492 return new MapViewEastNorthPoint(getEastNorth().add(en));
493 }
494
495 /**
496 * Check if this point is inside the view bounds.
497 *
498 * This is the case iff <code>getOutsideRectangleFlags(getViewArea())</code> returns no flags
499 * @return true if it is.
500 * @since 10827
501 */
502 public boolean isInView() {
503 return inRange(getInViewX(), 0, getViewWidth()) && inRange(getInViewY(), 0, getViewHeight());
504 }
505
506 private boolean inRange(double val, int min, double max) {
507 return val >= min && val < max;
508 }
509
510 /**
511 * Gets the direction in which this point is outside of the given view rectangle.
512 * @param rect The rectangle to check agains.
513 * @return The direction in which it is outside of the view, as OUTSIDE_... flags.
514 * @since 10827
515 */
516 public int getOutsideRectangleFlags(MapViewRectangle rect) {
517 Rectangle2D bounds = rect.getInView();
518 int flags = 0;
519 if (getInViewX() < bounds.getMinX()) {
520 flags |= OUTSIDE_LEFT;
521 } else if (getInViewX() > bounds.getMaxX()) {
522 flags |= OUTSIDE_RIGHT;
523 }
524 if (getInViewY() < bounds.getMinY()) {
525 flags |= OUTSIDE_TOP;
526 } else if (getInViewY() > bounds.getMaxY()) {
527 flags |= OUTSIDE_BOTTOM;
528 }
529
530 return flags;
531 }
532
533 /**
534 * Gets the sum of the x/y view distances between the points. |x1 - x2| + |y1 - y2|
535 * @param p2 The other point
536 * @return The norm
537 * @since 10827
538 */
539 public double oneNormInView(MapViewPoint p2) {
540 return Math.abs(getInViewX() - p2.getInViewX()) + Math.abs(getInViewY() - p2.getInViewY());
541 }
542
543 /**
544 * Gets the squared distance between this point and an other point.
545 * @param p2 The other point
546 * @return The squared distance.
547 * @since 10827
548 */
549 public double distanceToInViewSq(MapViewPoint p2) {
550 double dx = getInViewX() - p2.getInViewX();
551 double dy = getInViewY() - p2.getInViewY();
552 return dx * dx + dy * dy;
553 }
554
555 /**
556 * Gets the distance between this point and an other point.
557 * @param p2 The other point
558 * @return The distance.
559 * @since 10827
560 */
561 public double distanceToInView(MapViewPoint p2) {
562 return Math.sqrt(distanceToInViewSq(p2));
563 }
564
565 /**
566 * Do a linear interpolation to the other point
567 * @param p1 The other point
568 * @param i The interpolation factor. 0 is at the current point, 1 at the other point.
569 * @return The new point
570 * @since 10874
571 */
572 public MapViewPoint interpolate(MapViewPoint p1, double i) {
573 return new MapViewViewPoint((1 - i) * getInViewX() + i * p1.getInViewX(), (1 - i) * getInViewY() + i * p1.getInViewY());
574 }
575 }
576
577 private class MapViewViewPoint extends MapViewPoint {
578 private final double x;
579 private final double y;
580
581 MapViewViewPoint(double x, double y) {
582 this.x = x;
583 this.y = y;
584 }
585
586 @Override
587 public double getInViewX() {
588 return x;
589 }
590
591 @Override
592 public double getInViewY() {
593 return y;
594 }
595
596 @Override
597 public String toString() {
598 return "MapViewViewPoint [x=" + x + ", y=" + y + ']';
599 }
600 }
601
602 private class MapViewEastNorthPoint extends MapViewPoint {
603
604 private final EastNorth eastNorth;
605
606 MapViewEastNorthPoint(EastNorth eastNorth) {
607 this.eastNorth = Objects.requireNonNull(eastNorth, "eastNorth");
608 }
609
610 @Override
611 public double getInViewX() {
612 return (eastNorth.east() - topLeft.east()) / scale;
613 }
614
615 @Override
616 public double getInViewY() {
617 return (topLeft.north() - eastNorth.north()) / scale;
618 }
619
620 @Override
621 public EastNorth getEastNorth() {
622 return eastNorth;
623 }
624
625 @Override
626 public String toString() {
627 return "MapViewEastNorthPoint [eastNorth=" + eastNorth + ']';
628 }
629 }
630
631 /**
632 * A rectangle on the MapView. It is rectangular in screen / EastNorth space.
633 * @author Michael Zangl
634 */
635 public class MapViewRectangle {
636 private final MapViewPoint p1;
637 private final MapViewPoint p2;
638
639 /**
640 * Create a new MapViewRectangle
641 * @param p1 The first point to use
642 * @param p2 The second point to use.
643 */
644 MapViewRectangle(MapViewPoint p1, MapViewPoint p2) {
645 this.p1 = p1;
646 this.p2 = p2;
647 }
648
649 /**
650 * Gets the projection bounds for this rectangle.
651 * @return The projection bounds.
652 */
653 public ProjectionBounds getProjectionBounds() {
654 ProjectionBounds b = new ProjectionBounds(p1.getEastNorth());
655 b.extend(p2.getEastNorth());
656 return b;
657 }
658
659 /**
660 * Gets a rough estimate of the bounds by assuming lat/lon are parallel to x/y.
661 * @return The bounds computed by converting the corners of this rectangle.
662 * @see #getLatLonBoundsBox()
663 */
664 public Bounds getCornerBounds() {
665 Bounds b = new Bounds(p1.getLatLon());
666 b.extend(p2.getLatLon());
667 return b;
668 }
669
670 /**
671 * Gets the real bounds that enclose this rectangle.
672 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates.
673 * @return The bounds.
674 * @since 10458
675 */
676 public Bounds getLatLonBoundsBox() {
677 // TODO @michael2402: Use hillclimb.
678 return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds());
679 }
680
681 /**
682 * Gets this rectangle on the screen.
683 * @return The rectangle.
684 * @since 10651
685 */
686 public Rectangle2D getInView() {
687 double x1 = p1.getInViewX();
688 double y1 = p1.getInViewY();
689 double x2 = p2.getInViewX();
690 double y2 = p2.getInViewY();
691 return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2));
692 }
693
694 /**
695 * Check if the rectangle intersects the map view area.
696 * @return <code>true</code> if it intersects.
697 * @since 10827
698 */
699 public boolean isInView() {
700 return getInView().intersects(getViewArea().getInView());
701 }
702
703 /**
704 * Gets the entry point at which a line between start and end enters the current view.
705 * @param start The start
706 * @param end The end
707 * @return The entry point or <code>null</code> if the line does not intersect this view.
708 */
709 public MapViewPoint getLineEntry(MapViewPoint start, MapViewPoint end) {
710 ProjectionBounds bounds = getProjectionBounds();
711 if (bounds.contains(start.getEastNorth())) {
712 return start;
713 }
714
715 double dx = end.getEastNorth().east() - start.getEastNorth().east();
716 double boundX = dx > 0 ? bounds.minEast : bounds.maxEast;
717 EastNorth borderIntersection = Geometry.getSegmentSegmentIntersection(start.getEastNorth(), end.getEastNorth(),
718 new EastNorth(boundX, bounds.minNorth),
719 new EastNorth(boundX, bounds.maxNorth));
720 if (borderIntersection != null) {
721 return getPointFor(borderIntersection);
722 }
723
724 double dy = end.getEastNorth().north() - start.getEastNorth().north();
725 double boundY = dy > 0 ? bounds.minNorth : bounds.maxNorth;
726 borderIntersection = Geometry.getSegmentSegmentIntersection(start.getEastNorth(), end.getEastNorth(),
727 new EastNorth(bounds.minEast, boundY),
728 new EastNorth(bounds.maxEast, boundY));
729 if (borderIntersection != null) {
730 return getPointFor(borderIntersection);
731 }
732
733 return null;
734 }
735 }
736
737}
Note: See TracBrowser for help on using the repository browser.