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