001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.HeadlessException;
007import java.awt.Toolkit;
008import java.io.UnsupportedEncodingException;
009import java.net.URLDecoder;
010import java.util.HashMap;
011import java.util.Map;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.data.Bounds;
015import org.openstreetmap.josm.data.coor.LatLon;
016
017public final class OsmUrlToBounds {
018    private static final String SHORTLINK_PREFIX = "http://osm.org/go/";
019
020    private OsmUrlToBounds() {
021        // Hide default constructor for utils classes
022    }
023
024    public static Bounds parse(String url) throws IllegalArgumentException {
025        try {
026            // a percent sign indicates an encoded URL (RFC 1738).
027            if (url.contains("%")) {
028                url = URLDecoder.decode(url, "UTF-8");
029            }
030        } catch (UnsupportedEncodingException x) {
031            Main.error(x);
032        } catch (IllegalArgumentException x) {
033            Main.error(x);
034        }
035        Bounds b = parseShortLink(url);
036        if (b != null)
037            return b;
038        int i = url.indexOf("#map");
039        if (i >= 0) {
040            // probably it's a URL following the new scheme?
041            return parseHashURLs(url);
042        }
043        i = url.indexOf('?');
044        if (i == -1) {
045            return null;
046        }
047        String[] args = url.substring(i+1).split("&");
048        HashMap<String, String> map = new HashMap<String, String>();
049        for (String arg : args) {
050            int eq = arg.indexOf('=');
051            if (eq != -1) {
052                map.put(arg.substring(0, eq), arg.substring(eq + 1));
053            }
054        }
055
056        try {
057            if (map.containsKey("bbox")) {
058                String[] bbox = map.get("bbox").split(",");
059                b = new Bounds(
060                        Double.parseDouble(bbox[1]), Double.parseDouble(bbox[0]),
061                        Double.parseDouble(bbox[3]), Double.parseDouble(bbox[2]));
062            } else if (map.containsKey("minlat")) {
063                double minlat = Double.parseDouble(map.get("minlat"));
064                double minlon = Double.parseDouble(map.get("minlon"));
065                double maxlat = Double.parseDouble(map.get("maxlat"));
066                double maxlon = Double.parseDouble(map.get("maxlon"));
067                b = new Bounds(minlat, minlon, maxlat, maxlon);
068            } else {
069                String z = map.get("zoom");
070                b = positionToBounds(parseDouble(map, "lat"),
071                        parseDouble(map, "lon"),
072                        z == null ? 18 : Integer.parseInt(z));
073            }
074        } catch (NumberFormatException x) {
075            x.printStackTrace();
076        } catch (NullPointerException x) {
077            x.printStackTrace();
078        } catch (ArrayIndexOutOfBoundsException x) {
079            x.printStackTrace();
080        }
081        return b;
082    }
083
084    /**
085     * Openstreetmap.org changed it's URL scheme in August 2013, which breaks the URL parsing.
086     * The following function, called by the old parse function if necessary, provides parsing new URLs
087     * the new URLs follow the scheme http://www.openstreetmap.org/#map=18/51.71873/8.76164&layers=CN
088     * @param url string for parsing
089     * @return Bounds if hashurl, {@code null} otherwise
090     * @throws IllegalArgumentException if URL is invalid
091     */
092    private static Bounds parseHashURLs(String url) throws IllegalArgumentException {
093        int startIndex = url.indexOf("#map=");
094        if (startIndex == -1) return null;
095        int endIndex = url.indexOf('&', startIndex);
096        if (endIndex == -1) endIndex = url.length();
097        String coordPart = url.substring(startIndex+5, endIndex);
098        String[] parts = coordPart.split("/");
099        if (parts.length < 3) {
100            throw new IllegalArgumentException(tr("URL does not contain {0}/{1}/{2}", tr("zoom"), tr("latitude"), tr("longitude")));
101        }
102        int zoom;
103        double lat, lon;
104        try {
105            zoom = Integer.parseInt(parts[0]);
106        } catch (NumberFormatException e) {
107            throw new IllegalArgumentException(tr("URL does not contain valid {0}", tr("zoom")), e);
108        }
109        try {
110            lat = Double.parseDouble(parts[1]);
111        } catch (NumberFormatException e) {
112            throw new IllegalArgumentException(tr("URL does not contain valid {0}", tr("latitude")), e);
113        }
114        try {
115            lon = Double.parseDouble(parts[2]);
116        } catch (NumberFormatException e) {
117            throw new IllegalArgumentException(tr("URL does not contain valid {0}", tr("longitude")), e);
118        }
119        return positionToBounds(lat, lon, zoom);
120    }
121
122    private static double parseDouble(Map<String, String> map, String key) {
123        if (map.containsKey(key))
124            return Double.parseDouble(map.get(key));
125        return Double.parseDouble(map.get("m"+key));
126    }
127
128    private static final char[] SHORTLINK_CHARS = {
129        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
130        'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
131        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
132        'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
133        'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
134        'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
135        'w', 'x', 'y', 'z', '0', '1', '2', '3',
136        '4', '5', '6', '7', '8', '9', '_', '@'
137    };
138
139    /**
140     * Parse OSM short link
141     *
142     * @param url string for parsing
143     * @return Bounds if shortlink, null otherwise
144     * @see <a href="http://trac.openstreetmap.org/browser/sites/rails_port/lib/short_link.rb">short_link.rb</a>
145     */
146    private static Bounds parseShortLink(final String url) {
147        if (!url.startsWith(SHORTLINK_PREFIX))
148            return null;
149        final String shortLink = url.substring(SHORTLINK_PREFIX.length());
150
151        final Map<Character, Integer> array = new HashMap<Character, Integer>();
152
153        for (int i=0; i<SHORTLINK_CHARS.length; ++i) {
154            array.put(SHORTLINK_CHARS[i], i);
155        }
156
157        // long is necessary (need 32 bit positive value is needed)
158        long x = 0;
159        long y = 0;
160        int zoom = 0;
161        int zoomOffset = 0;
162
163        for (final char ch : shortLink.toCharArray()) {
164            if (array.containsKey(ch)) {
165                int val = array.get(ch);
166                for (int i=0; i<3; ++i) {
167                    x <<= 1;
168                    if ((val & 32) != 0) {
169                        x |= 1;
170                    }
171                    val <<= 1;
172
173                    y <<= 1;
174                    if ((val & 32) != 0) {
175                        y |= 1;
176                    }
177                    val <<= 1;
178                }
179                zoom += 3;
180            } else {
181                zoomOffset--;
182            }
183        }
184
185        x <<= 32 - zoom;
186        y <<= 32 - zoom;
187
188        // 2**32 == 4294967296
189        return positionToBounds(y * 180.0 / 4294967296.0 - 90.0,
190                x * 360.0 / 4294967296.0 - 180.0,
191                // TODO: -2 was not in ruby code
192                zoom - 8 - (zoomOffset % 3) - 2);
193    }
194
195    /** radius of the earth */
196    public static final double R = 6378137.0;
197
198    public static Bounds positionToBounds(final double lat, final double lon, final int zoom) {
199        int tileSizeInPixels = 256;
200        int height;
201        int width;
202        try {
203            height = Toolkit.getDefaultToolkit().getScreenSize().height;
204            width = Toolkit.getDefaultToolkit().getScreenSize().width;
205            if (Main.isDisplayingMapView()) {
206                height = Main.map.mapView.getHeight();
207                width = Main.map.mapView.getWidth();
208            }
209        } catch (HeadlessException he) {
210            // in headless mode, when running tests
211            height = 480;
212            width = 640;
213        }
214        double scale = (1 << zoom) * tileSizeInPixels / (2 * Math.PI * R);
215        double deltaX = width / 2.0 / scale;
216        double deltaY = height / 2.0 / scale;
217        double x = Math.toRadians(lon) * R;
218        double y = mercatorY(lat);
219        return new Bounds(invMercatorY(y - deltaY), Math.toDegrees(x - deltaX) / R, invMercatorY(y + deltaY), Math.toDegrees(x + deltaX) / R);
220    }
221
222    public static double mercatorY(double lat) {
223        return Math.log(Math.tan(Math.PI/4 + Math.toRadians(lat)/2)) * R;
224    }
225
226    public static double invMercatorY(double north) {
227        return Math.toDegrees(Math.atan(Math.sinh(north / R)));
228    }
229
230    public static Pair<Double, Double> getTileOfLatLon(double lat, double lon, double zoom) {
231        double x = Math.floor((lon + 180) / 360 * Math.pow(2.0, zoom));
232        double y = Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI)
233                / 2 * Math.pow(2.0, zoom));
234        return new Pair<Double, Double>(x, y);
235    }
236
237    public static LatLon getLatLonOfTile(double x, double y, double zoom) {
238        double lon = x / Math.pow(2.0, zoom) * 360.0 - 180;
239        double lat = Math.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, zoom))));
240        return new LatLon(lat, lon);
241    }
242
243    /**
244     * Return OSM Zoom level for a given area
245     *
246     * @param b bounds of the area
247     * @return matching zoom level for area
248     */
249    static public int getZoom(Bounds b) {
250        // convert to mercator (for calculation of zoom only)
251        double latMin = Math.log(Math.tan(Math.PI/4.0+b.getMinLat()/180.0*Math.PI/2.0))*180.0/Math.PI;
252        double latMax = Math.log(Math.tan(Math.PI/4.0+b.getMaxLat()/180.0*Math.PI/2.0))*180.0/Math.PI;
253        double size = Math.max(Math.abs(latMax-latMin), Math.abs(b.getMaxLon()-b.getMinLon()));
254        int zoom = 0;
255        while (zoom <= 20) {
256            if (size >= 180) {
257                break;
258            }
259            size *= 2;
260            zoom++;
261        }
262        return zoom;
263    }
264
265    /**
266     * Return OSM URL for given area.
267     *
268     * @param b bounds of the area
269     * @return link to display that area in OSM map
270     */
271    public static String getURL(Bounds b) {
272        return getURL(b.getCenter(), getZoom(b));
273    }
274
275    /**
276     * Return OSM URL for given position and zoom.
277     *
278     * @param pos center position of area
279     * @param zoom zoom depth of display
280     * @return link to display that area in OSM map
281     */
282    public static String getURL(LatLon pos, int zoom) {
283        return getURL(pos.lat(), pos.lon(), zoom);
284    }
285
286    /**
287     * Return OSM URL for given lat/lon and zoom.
288     *
289     * @param dlat center latitude of area
290     * @param dlon center longitude of area
291     * @param zoom zoom depth of display
292     * @return link to display that area in OSM map
293     * 
294     * @since 6453
295     */
296    public static String getURL(double dlat, double dlon, int zoom) {
297        // Truncate lat and lon to something more sensible
298        int decimals = (int) Math.pow(10, (zoom / 3));
299        double lat = (Math.round(dlat * decimals));
300        lat /= decimals;
301        double lon = (Math.round(dlon * decimals));
302        lon /= decimals;
303        return Main.OSM_WEBSITE + "/#map="+zoom+"/"+lat+"/"+lon;
304    }
305}