source: josm/trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java @ 5241

Revision 5226, 46.7 KB checked in by bastiK, 13 days ago (diff)

improvements for custom projection

  • Property svn:eol-style set to native
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.gui;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5
6import java.awt.Cursor;
7import java.awt.Point;
8import java.awt.Rectangle;
9import java.awt.geom.AffineTransform;
10import java.awt.geom.Point2D;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.Date;
15import java.util.HashSet;
16import java.util.LinkedHashMap;
17import java.util.LinkedList;
18import java.util.List;
19import java.util.Locale;
20import java.util.Map;
21import java.util.Set;
22import java.util.Stack;
23import java.util.TreeMap;
24import java.util.concurrent.CopyOnWriteArrayList;
25
26import javax.swing.JComponent;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.data.Bounds;
30import org.openstreetmap.josm.data.ProjectionBounds;
31import org.openstreetmap.josm.data.coor.CachedLatLon;
32import org.openstreetmap.josm.data.coor.EastNorth;
33import org.openstreetmap.josm.data.coor.LatLon;
34import org.openstreetmap.josm.data.osm.BBox;
35import org.openstreetmap.josm.data.osm.DataSet;
36import org.openstreetmap.josm.data.osm.Node;
37import org.openstreetmap.josm.data.osm.OsmPrimitive;
38import org.openstreetmap.josm.data.osm.Relation;
39import org.openstreetmap.josm.data.osm.Way;
40import org.openstreetmap.josm.data.osm.WaySegment;
41import org.openstreetmap.josm.data.preferences.IntegerProperty;
42import org.openstreetmap.josm.data.projection.Projection;
43import org.openstreetmap.josm.data.projection.Projections;
44import org.openstreetmap.josm.gui.help.Helpful;
45import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
46import org.openstreetmap.josm.tools.Predicate;
47
48/**
49 * An component that can be navigated by a mapmover. Used as map view and for the
50 * zoomer in the download dialog.
51 *
52 * @author imi
53 */
54public class NavigatableComponent extends JComponent implements Helpful {
55
56    /**
57     * Interface to notify listeners of the change of the zoom area.
58     */
59    public interface ZoomChangeListener {
60        void zoomChanged();
61    }
62
63    public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10);
64
65    public static final String PROPNAME_CENTER = "center";
66    public static final String PROPNAME_SCALE  = "scale";
67
68    /**
69     * the zoom listeners
70     */
71    private static final CopyOnWriteArrayList<ZoomChangeListener> zoomChangeListeners = new CopyOnWriteArrayList<ZoomChangeListener>();
72
73    /**
74     * Removes a zoom change listener
75     *
76     * @param listener the listener. Ignored if null or already absent
77     */
78    public static void removeZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
79        zoomChangeListeners.remove(listener);
80    }
81
82    /**
83     * Adds a zoom change listener
84     *
85     * @param listener the listener. Ignored if null or already registered.
86     */
87    public static void addZoomChangeListener(NavigatableComponent.ZoomChangeListener listener) {
88        if (listener != null) {
89            zoomChangeListeners.addIfAbsent(listener);
90        }
91    }
92
93    protected static void fireZoomChanged() {
94        for (ZoomChangeListener l : zoomChangeListeners) {
95            l.zoomChanged();
96        }
97    }
98
99    /**
100     * The scale factor in x or y-units per pixel. This means, if scale = 10,
101     * every physical pixel on screen are 10 x or 10 y units in the
102     * northing/easting space of the projection.
103     */
104    private double scale = Main.getProjection().getDefaultZoomInPPD();
105    /**
106     * Center n/e coordinate of the desired screen center.
107     */
108    protected EastNorth center = calculateDefaultCenter();
109
110    public NavigatableComponent() {
111        setLayout(null);
112    }
113
114    protected DataSet getCurrentDataSet() {
115        return Main.main.getCurrentDataSet();
116    }
117
118    private EastNorth calculateDefaultCenter() {
119        Bounds b = Main.getProjection().getWorldBoundsLatLon();
120        double lat = (b.getMax().lat() + b.getMin().lat())/2;
121        double lon = (b.getMax().lon() + b.getMin().lon())/2;
122
123        return Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
124    }
125
126    public static String getDistText(double dist) {
127        return getSystemOfMeasurement().getDistText(dist);
128    }
129
130    public String getDist100PixelText()
131    {
132        return getDistText(getDist100Pixel());
133    }
134
135    public double getDist100Pixel()
136    {
137        int w = getWidth()/2;
138        int h = getHeight()/2;
139        LatLon ll1 = getLatLon(w-50,h);
140        LatLon ll2 = getLatLon(w+50,h);
141        return ll1.greatCircleDistance(ll2);
142    }
143
144    /**
145     * @return Returns the center point. A copy is returned, so users cannot
146     *      change the center by accessing the return value. Use zoomTo instead.
147     */
148    public EastNorth getCenter() {
149        return center;
150    }
151
152    /**
153     * @param x X-Pixelposition to get coordinate from
154     * @param y Y-Pixelposition to get coordinate from
155     *
156     * @return Geographic coordinates from a specific pixel coordination
157     *      on the screen.
158     */
159    public EastNorth getEastNorth(int x, int y) {
160        return new EastNorth(
161                center.east() + (x - getWidth()/2.0)*scale,
162                center.north() - (y - getHeight()/2.0)*scale);
163    }
164
165    public ProjectionBounds getProjectionBounds() {
166        return new ProjectionBounds(
167                new EastNorth(
168                        center.east() - getWidth()/2.0*scale,
169                        center.north() - getHeight()/2.0*scale),
170                        new EastNorth(
171                                center.east() + getWidth()/2.0*scale,
172                                center.north() + getHeight()/2.0*scale));
173    }
174
175    /* FIXME: replace with better method - used by MapSlider */
176    public ProjectionBounds getMaxProjectionBounds() {
177        Bounds b = getProjection().getWorldBoundsLatLon();
178        return new ProjectionBounds(getProjection().latlon2eastNorth(b.getMin()),
179                getProjection().latlon2eastNorth(b.getMax()));
180    }
181
182    /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */
183    public Bounds getRealBounds() {
184        return new Bounds(
185                getProjection().eastNorth2latlon(new EastNorth(
186                        center.east() - getWidth()/2.0*scale,
187                        center.north() - getHeight()/2.0*scale)),
188                        getProjection().eastNorth2latlon(new EastNorth(
189                                center.east() + getWidth()/2.0*scale,
190                                center.north() + getHeight()/2.0*scale)));
191    }
192
193    /**
194     * @param x X-Pixelposition to get coordinate from
195     * @param y Y-Pixelposition to get coordinate from
196     *
197     * @return Geographic unprojected coordinates from a specific pixel coordination
198     *      on the screen.
199     */
200    public LatLon getLatLon(int x, int y) {
201        return getProjection().eastNorth2latlon(getEastNorth(x, y));
202    }
203
204    public LatLon getLatLon(double x, double y) {
205        return getLatLon((int)x, (int)y);
206    }
207
208    /**
209     * @param r
210     * @return Minimum bounds that will cover rectangle
211     */
212    public Bounds getLatLonBounds(Rectangle r) {
213        // TODO Maybe this should be (optional) method of Projection implementation
214        EastNorth p1 = getEastNorth(r.x, r.y);
215        EastNorth p2 = getEastNorth(r.x + r.width, r.y + r.height);
216
217        Bounds result = new Bounds(Main.getProjection().eastNorth2latlon(p1));
218
219        double eastMin = Math.min(p1.east(), p2.east());
220        double eastMax = Math.max(p1.east(), p2.east());
221        double northMin = Math.min(p1.north(), p2.north());
222        double northMax = Math.max(p1.north(), p2.north());
223        double deltaEast = (eastMax - eastMin) / 10;
224        double deltaNorth = (northMax - northMin) / 10;
225
226        for (int i=0; i < 10; i++) {
227            result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMin)));
228            result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin + i * deltaEast, northMax)));
229            result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMin, northMin  + i * deltaNorth)));
230            result.extend(Main.getProjection().eastNorth2latlon(new EastNorth(eastMax, northMin  + i * deltaNorth)));
231        }
232
233        return result;
234    }
235
236    public AffineTransform getAffineTransform() {
237        return new AffineTransform(
238                1.0/scale, 0.0, 0.0, -1.0/scale, getWidth()/2.0 - center.east()/scale, getHeight()/2.0 + center.north()/scale);
239    }
240
241    /**
242     * Return the point on the screen where this Coordinate would be.
243     * @param p The point, where this geopoint would be drawn.
244     * @return The point on screen where "point" would be drawn, relative
245     *      to the own top/left.
246     */
247    public Point2D getPoint2D(EastNorth p) {
248        if (null == p)
249            return new Point();
250        double x = (p.east()-center.east())/scale + getWidth()/2;
251        double y = (center.north()-p.north())/scale + getHeight()/2;
252        return new Point2D.Double(x, y);
253    }
254
255    public Point2D getPoint2D(LatLon latlon) {
256        if (latlon == null)
257            return new Point();
258        else if (latlon instanceof CachedLatLon)
259            return getPoint2D(((CachedLatLon)latlon).getEastNorth());
260        else
261            return getPoint2D(getProjection().latlon2eastNorth(latlon));
262    }
263
264    public Point2D getPoint2D(Node n) {
265        return getPoint2D(n.getEastNorth());
266    }
267
268    // looses precision, may overflow (depends on p and current scale)
269    //@Deprecated
270    public Point getPoint(EastNorth p) {
271        Point2D d = getPoint2D(p);
272        return new Point((int) d.getX(), (int) d.getY());
273    }
274
275    // looses precision, may overflow (depends on p and current scale)
276    //@Deprecated
277    public Point getPoint(LatLon latlon) {
278        Point2D d = getPoint2D(latlon);
279        return new Point((int) d.getX(), (int) d.getY());
280    }
281
282    // looses precision, may overflow (depends on p and current scale)
283    //@Deprecated
284    public Point getPoint(Node n) {
285        Point2D d = getPoint2D(n);
286        return new Point((int) d.getX(), (int) d.getY());
287    }
288
289    /**
290     * Zoom to the given coordinate.
291     * @param newCenter The center x-value (easting) to zoom to.
292     * @param scale The scale to use.
293     */
294    public void zoomTo(EastNorth newCenter, double newScale) {
295        Bounds b = getProjection().getWorldBoundsLatLon();
296        LatLon cl = Projections.inverseProject(newCenter);
297        boolean changed = false;
298        double lat = cl.lat();
299        double lon = cl.lon();
300        if(lat < b.getMin().lat()) {changed = true; lat = b.getMin().lat(); }
301        else if(lat > b.getMax().lat()) {changed = true; lat = b.getMax().lat(); }
302        if(lon < b.getMin().lon()) {changed = true; lon = b.getMin().lon(); }
303        else if(lon > b.getMax().lon()) {changed = true; lon = b.getMax().lon(); }
304        if(changed) {
305            newCenter = Projections.project(new LatLon(lat,lon));
306        }
307        int width = getWidth()/2;
308        int height = getHeight()/2;
309        LatLon l1 = new LatLon(b.getMin().lat(), lon);
310        LatLon l2 = new LatLon(b.getMax().lat(), lon);
311        EastNorth e1 = getProjection().latlon2eastNorth(l1);
312        EastNorth e2 = getProjection().latlon2eastNorth(l2);
313        double d = e2.north() - e1.north();
314        if(d < height*newScale)
315        {
316            double newScaleH = d/height;
317            e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMin().lon()));
318            e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMax().lon()));
319            d = e2.east() - e1.east();
320            if(d < width*newScale) {
321                newScale = Math.max(newScaleH, d/width);
322            }
323        }
324        else
325        {
326            d = d/(l1.greatCircleDistance(l2)*height*10);
327            if(newScale < d) {
328                newScale = d;
329            }
330        }
331
332        if (!newCenter.equals(center) || (scale != newScale)) {
333            pushZoomUndo(center, scale);
334            zoomNoUndoTo(newCenter, newScale);
335        }
336    }
337
338    /**
339     * Zoom to the given coordinate without adding to the zoom undo buffer.
340     * @param newCenter The center x-value (easting) to zoom to.
341     * @param scale The scale to use.
342     */
343    private void zoomNoUndoTo(EastNorth newCenter, double newScale) {
344        if (!newCenter.equals(center)) {
345            EastNorth oldCenter = center;
346            center = newCenter;
347            firePropertyChange(PROPNAME_CENTER, oldCenter, newCenter);
348        }
349        if (scale != newScale) {
350            double oldScale = scale;
351            scale = newScale;
352            firePropertyChange(PROPNAME_SCALE, oldScale, newScale);
353        }
354
355        repaint();
356        fireZoomChanged();
357    }
358
359    public void zoomTo(EastNorth newCenter) {
360        zoomTo(newCenter, scale);
361    }
362
363    public void zoomTo(LatLon newCenter) {
364        zoomTo(Projections.project(newCenter));
365    }
366
367    public void smoothScrollTo(LatLon newCenter) {
368        smoothScrollTo(Projections.project(newCenter));
369    }
370
371    /**
372     * Create a thread that moves the viewport to the given center in an
373     * animated fashion.
374     */
375    public void smoothScrollTo(EastNorth newCenter) {
376        // fixme make these configurable.
377        final int fps = 20;     // animation frames per second
378        final int speed = 1500; // milliseconds for full-screen-width pan
379        if (!newCenter.equals(center)) {
380            final EastNorth oldCenter = center;
381            final double distance = newCenter.distance(oldCenter) / scale;
382            final double milliseconds = distance / getWidth() * speed;
383            final double frames = milliseconds * fps / 1000;
384            final EastNorth finalNewCenter = newCenter;
385
386            new Thread(
387                    new Runnable() {
388                        public void run() {
389                            for (int i=0; i<frames; i++)
390                            {
391                                // fixme - not use zoom history here
392                                zoomTo(oldCenter.interpolate(finalNewCenter, (i+1) / frames));
393                                try { Thread.sleep(1000 / fps); } catch (InterruptedException ex) { };
394                            }
395                        }
396                    }
397                    ).start();
398        }
399    }
400
401    public void zoomToFactor(double x, double y, double factor) {
402        double newScale = scale*factor;
403        // New center position so that point under the mouse pointer stays the same place as it was before zooming
404        // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom
405        zoomTo(new EastNorth(
406                center.east() - (x - getWidth()/2.0) * (newScale - scale),
407                center.north() + (y - getHeight()/2.0) * (newScale - scale)),
408                newScale);
409    }
410
411    public void zoomToFactor(EastNorth newCenter, double factor) {
412        zoomTo(newCenter, scale*factor);
413    }
414
415    public void zoomToFactor(double factor) {
416        zoomTo(center, scale*factor);
417    }
418
419    public void zoomTo(ProjectionBounds box) {
420        // -20 to leave some border
421        int w = getWidth()-20;
422        if (w < 20) {
423            w = 20;
424        }
425        int h = getHeight()-20;
426        if (h < 20) {
427            h = 20;
428        }
429
430        double scaleX = (box.maxEast-box.minEast)/w;
431        double scaleY = (box.maxNorth-box.minNorth)/h;
432        double newScale = Math.max(scaleX, scaleY);
433
434        zoomTo(box.getCenter(), newScale);
435    }
436
437    public void zoomTo(Bounds box) {
438        zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()),
439                getProjection().latlon2eastNorth(box.getMax())));
440    }
441
442    private class ZoomData {
443        LatLon center;
444        double scale;
445
446        public ZoomData(EastNorth center, double scale) {
447            this.center = Projections.inverseProject(center);
448            this.scale = scale;
449        }
450
451        public EastNorth getCenterEastNorth() {
452            return getProjection().latlon2eastNorth(center);
453        }
454
455        public double getScale() {
456            return scale;
457        }
458    }
459
460    private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>();
461    private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>();
462    private Date zoomTimestamp = new Date();
463
464    private void pushZoomUndo(EastNorth center, double scale) {
465        Date now = new Date();
466        if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) {
467            zoomUndoBuffer.push(new ZoomData(center, scale));
468            if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) {
469                zoomUndoBuffer.remove(0);
470            }
471            zoomRedoBuffer.clear();
472        }
473        zoomTimestamp = now;
474    }
475
476    public void zoomPrevious() {
477        if (!zoomUndoBuffer.isEmpty()) {
478            ZoomData zoom = zoomUndoBuffer.pop();
479            zoomRedoBuffer.push(new ZoomData(center, scale));
480            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
481        }
482    }
483
484    public void zoomNext() {
485        if (!zoomRedoBuffer.isEmpty()) {
486            ZoomData zoom = zoomRedoBuffer.pop();
487            zoomUndoBuffer.push(new ZoomData(center, scale));
488            zoomNoUndoTo(zoom.getCenterEastNorth(), zoom.getScale());
489        }
490    }
491
492    public boolean hasZoomUndoEntries() {
493        return !zoomUndoBuffer.isEmpty();
494    }
495
496    public boolean hasZoomRedoEntries() {
497        return !zoomRedoBuffer.isEmpty();
498    }
499
500    private BBox getBBox(Point p, int snapDistance) {
501        return new BBox(getLatLon(p.x - snapDistance, p.y - snapDistance),
502                getLatLon(p.x + snapDistance, p.y + snapDistance));
503    }
504
505    /**
506     * The *result* does not depend on the current map selection state,
507     * neither does the result *order*.
508     * It solely depends on the distance to point p.
509     *
510     * @return a sorted map with the keys representing the distance of
511     *      their associated nodes to point p.
512     */
513    private Map<Double, List<Node>> getNearestNodesImpl(Point p,
514            Predicate<OsmPrimitive> predicate) {
515        TreeMap<Double, List<Node>> nearestMap = new TreeMap<Double, List<Node>>();
516        DataSet ds = getCurrentDataSet();
517
518        if (ds != null) {
519            double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
520            snapDistanceSq *= snapDistanceSq;
521
522            for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
523                if (predicate.evaluate(n)
524                        && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq)
525                {
526                    List<Node> nlist;
527                    if (nearestMap.containsKey(dist)) {
528                        nlist = nearestMap.get(dist);
529                    } else {
530                        nlist = new LinkedList<Node>();
531                        nearestMap.put(dist, nlist);
532                    }
533                    nlist.add(n);
534                }
535            }
536        }
537
538        return nearestMap;
539    }
540
541    /**
542     * The *result* does not depend on the current map selection state,
543     * neither does the result *order*.
544     * It solely depends on the distance to point p.
545     *
546     * @return All nodes nearest to point p that are in a belt from
547     *      dist(nearest) to dist(nearest)+4px around p and
548     *      that are not in ignore.
549     *
550     * @param p the point for which to search the nearest segment.
551     * @param ignore a collection of nodes which are not to be returned.
552     * @param predicate the returned objects have to fulfill certain properties.
553     */
554    public final List<Node> getNearestNodes(Point p,
555            Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
556        List<Node> nearestList = Collections.emptyList();
557
558        if (ignore == null) {
559            ignore = Collections.emptySet();
560        }
561
562        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
563        if (!nlists.isEmpty()) {
564            Double minDistSq = null;
565            List<Node> nlist;
566            for (Double distSq : nlists.keySet()) {
567                nlist = nlists.get(distSq);
568
569                // filter nodes to be ignored before determining minDistSq..
570                nlist.removeAll(ignore);
571                if (minDistSq == null) {
572                    if (!nlist.isEmpty()) {
573                        minDistSq = distSq;
574                        nearestList = new ArrayList<Node>();
575                        nearestList.addAll(nlist);
576                    }
577                } else {
578                    if (distSq-minDistSq < (4)*(4)) {
579                        nearestList.addAll(nlist);
580                    }
581                }
582            }
583        }
584
585        return nearestList;
586    }
587
588    /**
589     * The *result* does not depend on the current map selection state,
590     * neither does the result *order*.
591     * It solely depends on the distance to point p.
592     *
593     * @return All nodes nearest to point p that are in a belt from
594     *      dist(nearest) to dist(nearest)+4px around p.
595     * @see #getNearestNodes(Point, Collection, Predicate)
596     *
597     * @param p the point for which to search the nearest segment.
598     * @param predicate the returned objects have to fulfill certain properties.
599     */
600    public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
601        return getNearestNodes(p, null, predicate);
602    }
603
604    /**
605     * The *result* depends on the current map selection state IF use_selected is true.
606     *
607     * If more than one node within node.snap-distance pixels is found,
608     * the nearest node selected is returned IF use_selected is true.
609     *
610     * Else the nearest new/id=0 node within about the same distance
611     * as the true nearest node is returned.
612     *
613     * If no such node is found either, the true nearest
614     * node to p is returned.
615     *
616     * Finally, if a node is not found at all, null is returned.
617     *
618     * @return A node within snap-distance to point p,
619     *      that is chosen by the algorithm described.
620     *
621     * @param p the screen point
622     * @param predicate this parameter imposes a condition on the returned object, e.g.
623     *        give the nearest node that is tagged.
624     */
625    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) {
626        Node n = null;
627
628        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
629        if (!nlists.isEmpty()) {
630            Node ntsel = null, ntnew = null;
631            double minDistSq = nlists.keySet().iterator().next();
632
633            for (Double distSq : nlists.keySet()) {
634                for (Node nd : nlists.get(distSq)) {
635                    // find the nearest selected node
636                    if (ntsel == null && nd.isSelected()) {
637                        ntsel = nd;
638                        // if there are multiple nearest nodes, prefer the one
639                        // that is selected. This is required in order to drag
640                        // the selected node if multiple nodes have the same
641                        // coordinates (e.g. after unglue)
642                        use_selected |= (distSq == minDistSq);
643                    }
644                    // find the nearest newest node that is within about the same
645                    // distance as the true nearest node
646                    if (ntnew == null && nd.isNew() && (distSq-minDistSq < 1)) {
647                        ntnew = nd;
648                    }
649                }
650            }
651
652            // take nearest selected, nearest new or true nearest node to p, in that order
653            n = (ntsel != null && use_selected) ? ntsel
654                    : (ntnew != null) ? ntnew
655                            : nlists.values().iterator().next().get(0);
656        }
657        return n;
658    }
659
660    /**
661     * Convenience method to {@link #getNearestNode(Point, Predicate, boolean)}.
662     *
663     * @return The nearest node to point p.
664     */
665    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
666        return getNearestNode(p, predicate, true);
667    }
668
669    /**
670     * The *result* does not depend on the current map selection state,
671     * neither does the result *order*.
672     * It solely depends on the distance to point p.
673     *
674     * @return a sorted map with the keys representing the perpendicular
675     *      distance of their associated way segments to point p.
676     */
677    private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p,
678            Predicate<OsmPrimitive> predicate) {
679        Map<Double, List<WaySegment>> nearestMap = new TreeMap<Double, List<WaySegment>>();
680        DataSet ds = getCurrentDataSet();
681
682        if (ds != null) {
683            double snapDistanceSq = Main.pref.getInteger("mappaint.segment.snap-distance", 10);
684            snapDistanceSq *= snapDistanceSq;
685
686            for (Way w : ds.searchWays(getBBox(p, Main.pref.getInteger("mappaint.segment.snap-distance", 10)))) {
687                if (!predicate.evaluate(w)) {
688                    continue;
689                }
690                Node lastN = null;
691                int i = -2;
692                for (Node n : w.getNodes()) {
693                    i++;
694                    if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
695                        continue;
696                    }
697                    if (lastN == null) {
698                        lastN = n;
699                        continue;
700                    }
701
702                    Point2D A = getPoint2D(lastN);
703                    Point2D B = getPoint2D(n);
704                    double c = A.distanceSq(B);
705                    double a = p.distanceSq(B);
706                    double b = p.distanceSq(A);
707
708                    /* perpendicular distance squared
709                     * loose some precision to account for possible deviations in the calculation above
710                     * e.g. if identical (A and B) come about reversed in another way, values may differ
711                     * -- zero out least significant 32 dual digits of mantissa..
712                     */
713                    double perDistSq = Double.longBitsToDouble(
714                            Double.doubleToLongBits( a - (a - b + c) * (a - b + c) / 4 / c )
715                            >> 32 << 32); // resolution in numbers with large exponent not needed here..
716
717                    if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
718                        //System.err.println(Double.toHexString(perDistSq));
719
720                        List<WaySegment> wslist;
721                        if (nearestMap.containsKey(perDistSq)) {
722                            wslist = nearestMap.get(perDistSq);
723                        } else {
724                            wslist = new LinkedList<WaySegment>();
725                            nearestMap.put(perDistSq, wslist);
726                        }
727                        wslist.add(new WaySegment(w, i));
728                    }
729
730                    lastN = n;
731                }
732            }
733        }
734
735        return nearestMap;
736    }
737
738    /**
739     * The result *order* depends on the current map selection state.
740     * Segments within 10px of p are searched and sorted by their distance to @param p,
741     * then, within groups of equally distant segments, prefer those that are selected.
742     *
743     * @return all segments within 10px of p that are not in ignore,
744     *          sorted by their perpendicular distance.
745     *
746     * @param p the point for which to search the nearest segments.
747     * @param ignore a collection of segments which are not to be returned.
748     * @param predicate the returned objects have to fulfill certain properties.
749     */
750    public final List<WaySegment> getNearestWaySegments(Point p,
751            Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
752        List<WaySegment> nearestList = new ArrayList<WaySegment>();
753        List<WaySegment> unselected = new LinkedList<WaySegment>();
754
755        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
756            // put selected waysegs within each distance group first
757            // makes the order of nearestList dependent on current selection state
758            for (WaySegment ws : wss) {
759                (ws.way.isSelected() ? nearestList : unselected).add(ws);
760            }
761            nearestList.addAll(unselected);
762            unselected.clear();
763        }
764        if (ignore != null) {
765            nearestList.removeAll(ignore);
766        }
767
768        return nearestList;
769    }
770
771    /**
772     * The result *order* depends on the current map selection state.
773     *
774     * @return all segments within 10px of p, sorted by their perpendicular distance.
775     * @see #getNearestWaySegments(Point, Collection, Predicate)
776     *
777     * @param p the point for which to search the nearest segments.
778     * @param predicate the returned objects have to fulfill certain properties.
779     */
780    public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
781        return getNearestWaySegments(p, null, predicate);
782    }
783
784    /**
785     * The *result* depends on the current map selection state IF use_selected is true.
786     *
787     * @return The nearest way segment to point p,
788     *      and, depending on use_selected, prefers a selected way segment, if found.
789     * @see #getNearestWaySegments(Point, Collection, Predicate)
790     *
791     * @param p the point for which to search the nearest segment.
792     * @param predicate the returned object has to fulfill certain properties.
793     * @param use_selected whether selected way segments should be preferred.
794     */
795    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) {
796        WaySegment wayseg = null, ntsel = null;
797
798        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
799            if (wayseg != null && ntsel != null) {
800                break;
801            }
802            for (WaySegment ws : wslist) {
803                if (wayseg == null) {
804                    wayseg = ws;
805                }
806                if (ntsel == null && ws.way.isSelected()) {
807                    ntsel = ws;
808                }
809            }
810        }
811
812        return (ntsel != null && use_selected) ? ntsel : wayseg;
813    }
814
815    /**
816     * Convenience method to {@link #getNearestWaySegment(Point, Predicate, boolean)}.
817     *
818     * @return The nearest way segment to point p.
819     */
820    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
821        return getNearestWaySegment(p, predicate, true);
822    }
823
824    /**
825     * The *result* does not depend on the current map selection state,
826     * neither does the result *order*.
827     * It solely depends on the perpendicular distance to point p.
828     *
829     * @return all nearest ways to the screen point given that are not in ignore.
830     * @see #getNearestWaySegments(Point, Collection, Predicate)
831     *
832     * @param p the point for which to search the nearest ways.
833     * @param ignore a collection of ways which are not to be returned.
834     * @param predicate the returned object has to fulfill certain properties.
835     */
836    public final List<Way> getNearestWays(Point p,
837            Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
838        List<Way> nearestList = new ArrayList<Way>();
839        Set<Way> wset = new HashSet<Way>();
840
841        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
842            for (WaySegment ws : wss) {
843                if (wset.add(ws.way)) {
844                    nearestList.add(ws.way);
845                }
846            }
847        }
848        if (ignore != null) {
849            nearestList.removeAll(ignore);
850        }
851
852        return nearestList;
853    }
854
855    /**
856     * The *result* does not depend on the current map selection state,
857     * neither does the result *order*.
858     * It solely depends on the perpendicular distance to point p.
859     *
860     * @return all nearest ways to the screen point given.
861     * @see #getNearestWays(Point, Collection, Predicate)
862     *
863     * @param p the point for which to search the nearest ways.
864     * @param predicate the returned object has to fulfill certain properties.
865     */
866    public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
867        return getNearestWays(p, null, predicate);
868    }
869
870    /**
871     * The *result* depends on the current map selection state.
872     *
873     * @return The nearest way to point p,
874     *      prefer a selected way if there are multiple nearest.
875     * @see #getNearestWaySegment(Point, Collection, Predicate)
876     *
877     * @param p the point for which to search the nearest segment.
878     * @param predicate the returned object has to fulfill certain properties.
879     */
880    public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
881        WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
882        return (nearestWaySeg == null) ? null : nearestWaySeg.way;
883    }
884
885    /**
886     * The *result* does not depend on the current map selection state,
887     * neither does the result *order*.
888     * It solely depends on the distance to point p.
889     *
890     * First, nodes will be searched. If there are nodes within BBox found,
891     * return a collection of those nodes only.
892     *
893     * If no nodes are found, search for nearest ways. If there are ways
894     * within BBox found, return a collection of those ways only.
895     *
896     * If nothing is found, return an empty collection.
897     *
898     * @return Primitives nearest to the given screen point that are not in ignore.
899     * @see #getNearestNodes(Point, Collection, Predicate)
900     * @see #getNearestWays(Point, Collection, Predicate)
901     *
902     * @param p The point on screen.
903     * @param ignore a collection of ways which are not to be returned.
904     * @param predicate the returned object has to fulfill certain properties.
905     */
906    public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
907            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
908        List<OsmPrimitive> nearestList = Collections.emptyList();
909        OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
910
911        if (osm != null) {
912            if (osm instanceof Node) {
913                nearestList = new ArrayList<OsmPrimitive>(getNearestNodes(p, predicate));
914            } else if (osm instanceof Way) {
915                nearestList = new ArrayList<OsmPrimitive>(getNearestWays(p, predicate));
916            }
917            if (ignore != null) {
918                nearestList.removeAll(ignore);
919            }
920        }
921
922        return nearestList;
923    }
924
925    /**
926     * The *result* does not depend on the current map selection state,
927     * neither does the result *order*.
928     * It solely depends on the distance to point p.
929     *
930     * @return Primitives nearest to the given screen point.
931     * @see #getNearests(Point, Collection, Predicate)
932     *
933     * @param p The point on screen.
934     * @param predicate the returned object has to fulfill certain properties.
935     */
936    public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
937        return getNearestNodesOrWays(p, null, predicate);
938    }
939
940    /**
941     * This is used as a helper routine to {@link #getNearestNodeOrWay(Point, Predicate, boolean)}
942     * It decides, whether to yield the node to be tested or look for further (way) candidates.
943     *
944     * @return true, if the node fulfills the properties of the function body
945     *
946     * @param osm node to check
947     * @param p point clicked
948     * @param use_selected whether to prefer selected nodes
949     */
950    private boolean isPrecedenceNode(Node osm, Point p, boolean use_selected) {
951        boolean ret = false;
952
953        if (osm != null) {
954            ret |= !(p.distanceSq(getPoint2D(osm)) > (4)*(4));
955            ret |= osm.isTagged();
956            if (use_selected) {
957                ret |= osm.isSelected();
958            }
959        }
960
961        return ret;
962    }
963
964    /**
965     * The *result* depends on the current map selection state IF use_selected is true.
966     *
967     * IF use_selected is true, use {@link #getNearestNode(Point, Predicate)} to find
968     * the nearest, selected node.  If not found, try {@link #getNearestWaySegment(Point, Predicate)}
969     * to find the nearest selected way.
970     *
971     * IF use_selected is false, or if no selected primitive was found, do the following.
972     *
973     * If the nearest node found is within 4px of p, simply take it.
974     * Else, find the nearest way segment. Then, if p is closer to its
975     * middle than to the node, take the way segment, else take the node.
976     *
977     * Finally, if no nearest primitive is found at all, return null.
978     *
979     * @return A primitive within snap-distance to point p,
980     *      that is chosen by the algorithm described.
981     * @see getNearestNode(Point, Predicate)
982     * @see getNearestNodesImpl(Point, Predicate)
983     * @see getNearestWay(Point, Predicate)
984     *
985     * @param p The point on screen.
986     * @param predicate the returned object has to fulfill certain properties.
987     * @param use_selected whether to prefer primitives that are currently selected.
988     */
989    public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean use_selected) {
990        OsmPrimitive osm = getNearestNode(p, predicate, use_selected);
991        WaySegment ws = null;
992
993        if (!isPrecedenceNode((Node)osm, p, use_selected)) {
994            ws = getNearestWaySegment(p, predicate, use_selected);
995
996            if (ws != null) {
997                if ((ws.way.isSelected() && use_selected) || osm == null) {
998                    // either (no _selected_ nearest node found, if desired) or no nearest node was found
999                    osm = ws.way;
1000                } else {
1001                    int maxWaySegLenSq = 3*PROP_SNAP_DISTANCE.get();
1002                    maxWaySegLenSq *= maxWaySegLenSq;
1003
1004                    Point2D wp1 = getPoint2D(ws.way.getNode(ws.lowerIndex));
1005                    Point2D wp2 = getPoint2D(ws.way.getNode(ws.lowerIndex+1));
1006
1007                    // is wayseg shorter than maxWaySegLenSq and
1008                    // is p closer to the middle of wayseg  than  to the nearest node?
1009                    if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
1010                            p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node)osm))) {
1011                        osm = ws.way;
1012                    }
1013                }
1014            }
1015        }
1016
1017        return osm;
1018    }
1019
1020    /**
1021     * @return o as collection of o's type.
1022     */
1023    public static <T> Collection<T> asColl(T o) {
1024        if (o == null)
1025            return Collections.emptySet();
1026        return Collections.singleton(o);
1027    }
1028
1029    public static double perDist(Point2D pt, Point2D a, Point2D b) {
1030        if (pt != null && a != null && b != null) {
1031            double pd = (
1032                    (a.getX()-pt.getX())*(b.getX()-a.getX()) -
1033                    (a.getY()-pt.getY())*(b.getY()-a.getY()) );
1034            return Math.abs(pd) / a.distance(b);
1035        }
1036        return 0d;
1037    }
1038
1039    /**
1040     *
1041     * @param pt point to project onto (ab)
1042     * @param a root of vector
1043     * @param b vector
1044     * @return point of intersection of line given by (ab)
1045     *      with its orthogonal line running through pt
1046     */
1047    public static Point2D project(Point2D pt, Point2D a, Point2D b) {
1048        if (pt != null && a != null && b != null) {
1049            double r = ((
1050                    (pt.getX()-a.getX())*(b.getX()-a.getX()) +
1051                    (pt.getY()-a.getY())*(b.getY()-a.getY()) )
1052                    / a.distanceSq(b));
1053            return project(r, a, b);
1054        }
1055        return null;
1056    }
1057
1058    /**
1059     * if r = 0 returns a, if r=1 returns b,
1060     * if r = 0.5 returns center between a and b, etc..
1061     *
1062     * @param r scale value
1063     * @param a root of vector
1064     * @param b vector
1065     * @return new point at a + r*(ab)
1066     */
1067    public static Point2D project(double r, Point2D a, Point2D b) {
1068        Point2D ret = null;
1069
1070        if (a != null && b != null) {
1071            ret = new Point2D.Double(a.getX() + r*(b.getX()-a.getX()),
1072                    a.getY() + r*(b.getY()-a.getY()));
1073        }
1074        return ret;
1075    }
1076
1077    /**
1078     * The *result* does not depend on the current map selection state,
1079     * neither does the result *order*.
1080     * It solely depends on the distance to point p.
1081     *
1082     * @return a list of all objects that are nearest to point p and
1083     *          not in ignore or an empty list if nothing was found.
1084     *
1085     * @param p The point on screen.
1086     * @param ignore a collection of ways which are not to be returned.
1087     * @param predicate the returned object has to fulfill certain properties.
1088     */
1089    public final List<OsmPrimitive> getAllNearest(Point p,
1090            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
1091        List<OsmPrimitive> nearestList = new ArrayList<OsmPrimitive>();
1092        Set<Way> wset = new HashSet<Way>();
1093
1094        // add nearby ways
1095        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
1096            for (WaySegment ws : wss) {
1097                if (wset.add(ws.way)) {
1098                    nearestList.add(ws.way);
1099                }
1100            }
1101        }
1102       
1103        // add nearby nodes
1104        for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
1105            nearestList.addAll(nlist);
1106        }
1107       
1108        // add parent relations of nearby nodes and ways
1109        Set<OsmPrimitive> parentRelations = new HashSet<OsmPrimitive>();
1110        for (OsmPrimitive o : nearestList) {
1111            for (OsmPrimitive r : o.getReferrers()) {
1112                if (r instanceof Relation && predicate.evaluate(r)) {
1113                    parentRelations.add(r);
1114                }
1115            }
1116        }
1117        nearestList.addAll(parentRelations);
1118       
1119        if (ignore != null) {
1120            nearestList.removeAll(ignore);
1121        }
1122
1123        return nearestList;
1124    }
1125
1126    /**
1127     * The *result* does not depend on the current map selection state,
1128     * neither does the result *order*.
1129     * It solely depends on the distance to point p.
1130     *
1131     * @return a list of all objects that are nearest to point p
1132     *          or an empty list if nothing was found.
1133     * @see #getAllNearest(Point, Collection, Predicate)
1134     *
1135     * @param p The point on screen.
1136     * @param predicate the returned object has to fulfill certain properties.
1137     */
1138    public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
1139        return getAllNearest(p, null, predicate);
1140    }
1141
1142    /**
1143     * @return The projection to be used in calculating stuff.
1144     */
1145    public Projection getProjection() {
1146        return Main.getProjection();
1147    }
1148
1149    public String helpTopic() {
1150        String n = getClass().getName();
1151        return n.substring(n.lastIndexOf('.')+1);
1152    }
1153
1154    /**
1155     * Return a ID which is unique as long as viewport dimensions are the same
1156     */
1157    public int getViewID() {
1158        String x = center.east() + "_" + center.north() + "_" + scale + "_" +
1159                getWidth() + "_" + getHeight() + "_" + getProjection().toString();
1160        java.util.zip.CRC32 id = new java.util.zip.CRC32();
1161        id.update(x.getBytes());
1162        return (int)id.getValue();
1163    }
1164
1165    public static SystemOfMeasurement getSystemOfMeasurement() {
1166        SystemOfMeasurement som = SYSTEMS_OF_MEASUREMENT.get(ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get());
1167        if (som == null)
1168            return METRIC_SOM;
1169        return som;
1170    }
1171
1172    public static class SystemOfMeasurement {
1173        public final double aValue;
1174        public final double bValue;
1175        public final String aName;
1176        public final String bName;
1177
1178        /**
1179         * System of measurement. Currently covers only length units.
1180         *
1181         * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
1182         * x_a == x_m / aValue
1183         */
1184        public SystemOfMeasurement(double aValue, String aName, double bValue, String bName) {
1185            this.aValue = aValue;
1186            this.aName = aName;
1187            this.bValue = bValue;
1188            this.bName = bName;
1189        }
1190
1191        public String getDistText(double dist) {
1192            double a = dist / aValue;
1193            if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && a > bValue / aValue) {
1194                double b = dist / bValue;
1195                return String.format(Locale.US, "%." + (b<10 ? 2 : 1) + "f %s", b, bName);
1196            } else if (a < 0.01)
1197                return "< 0.01 " + aName;
1198            else
1199                return String.format(Locale.US, "%." + (a<10 ? 2 : 1) + "f %s", a, aName);
1200        }
1201    }
1202
1203    public static final SystemOfMeasurement METRIC_SOM = new SystemOfMeasurement(1, "m", 1000, "km");
1204    public static final SystemOfMeasurement CHINESE_SOM = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */);
1205    public static final SystemOfMeasurement IMPERIAL_SOM = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi");
1206
1207    public static final Map<String, SystemOfMeasurement> SYSTEMS_OF_MEASUREMENT;
1208    static {
1209        SYSTEMS_OF_MEASUREMENT = new LinkedHashMap<String, SystemOfMeasurement>();
1210        SYSTEMS_OF_MEASUREMENT.put(marktr("Metric"), METRIC_SOM);
1211        SYSTEMS_OF_MEASUREMENT.put(marktr("Chinese"), CHINESE_SOM);
1212        SYSTEMS_OF_MEASUREMENT.put(marktr("Imperial"), IMPERIAL_SOM);
1213    }
1214
1215    private static class CursorInfo {
1216        public Cursor cursor;
1217        public Object object;
1218        public CursorInfo(Cursor c, Object o) {
1219            cursor = c;
1220            object = o;
1221        }
1222    }
1223
1224    private LinkedList<CursorInfo> Cursors = new LinkedList<CursorInfo>();
1225    /**
1226     * Set new cursor.
1227     */
1228    public void setNewCursor(Cursor cursor, Object reference) {
1229        if(Cursors.size() > 0) {
1230            CursorInfo l = Cursors.getLast();
1231            if(l != null && l.cursor == cursor && l.object == reference)
1232                return;
1233            stripCursors(reference);
1234        }
1235        Cursors.add(new CursorInfo(cursor, reference));
1236        setCursor(cursor);
1237    }
1238    public void setNewCursor(int cursor, Object reference) {
1239        setNewCursor(Cursor.getPredefinedCursor(cursor), reference);
1240    }
1241    /**
1242     * Remove the new cursor and reset to previous
1243     */
1244    public void resetCursor(Object reference) {
1245        if(Cursors.size() == 0) {
1246            setCursor(null);
1247            return;
1248        }
1249        CursorInfo l = Cursors.getLast();
1250        stripCursors(reference);
1251        if(l != null && l.object == reference) {
1252            if(Cursors.size() == 0) {
1253                setCursor(null);
1254            } else {
1255                setCursor(Cursors.getLast().cursor);
1256            }
1257        }
1258    }
1259
1260    private void stripCursors(Object reference) {
1261        LinkedList<CursorInfo> c = new LinkedList<CursorInfo>();
1262        for(CursorInfo i : Cursors) {
1263            if(i.object != reference) {
1264                c.add(i);
1265            }
1266        }
1267        Cursors = c;
1268    }
1269}
Note: See TracBrowser for help on using the repository browser.