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