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;
014
015public final class OsmUrlToBounds {
016    private static final String SHORTLINK_PREFIX = "http://osm.org/go/";
017
018    private OsmUrlToBounds() {
019        // Hide default constructor for utils classes
020    }
021
022    public static Bounds parse(String url) {
023        try {
024            // a percent sign indicates an encoded URL (RFC 1738).
025            if (url.contains("%")) {
026                url = Utils.decodeUrl(url);
027            }
028        } catch (IllegalArgumentException x) {
029            Main.error(x);
030        }
031        Bounds b = parseShortLink(url);
032        if (b != null)
033            return b;
034        int i = url.indexOf("#map");
035        if (i >= 0) {
036            // probably it's a URL following the new scheme?
037            return parseHashURLs(url);
038        }
039        i = url.indexOf('?');
040        if (i == -1) {
041            return null;
042        }
043        String[] args = url.substring(i+1).split("&");
044        Map<String, String> map = new HashMap<>();
045        for (String arg : args) {
046            int eq = arg.indexOf('=');
047            if (eq != -1) {
048                map.put(arg.substring(0, eq), arg.substring(eq + 1));
049            }
050        }
051
052        try {
053            if (map.containsKey("bbox")) {
054                String[] bbox = map.get("bbox").split(",");
055                b = new Bounds(
056                        Double.parseDouble(bbox[1]), Double.parseDouble(bbox[0]),
057                        Double.parseDouble(bbox[3]), Double.parseDouble(bbox[2]));
058            } else if (map.containsKey("minlat")) {
059                double minlat = Double.parseDouble(map.get("minlat"));
060                double minlon = Double.parseDouble(map.get("minlon"));
061                double maxlat = Double.parseDouble(map.get("maxlat"));
062                double maxlon = Double.parseDouble(map.get("maxlon"));
063                b = new Bounds(minlat, minlon, maxlat, maxlon);
064            } else {
065                String z = map.get("zoom");
066                b = positionToBounds(parseDouble(map, "lat"),
067                        parseDouble(map, "lon"),
068                        z == null ? 18 : Integer.parseInt(z));
069            }
070        } catch (NumberFormatException | NullPointerException | ArrayIndexOutOfBoundsException x) {
071            Main.error(x);
072        }
073        return b;
074    }
075
076    /**
077     * Openstreetmap.org changed it's URL scheme in August 2013, which breaks the URL parsing.
078     * The following function, called by the old parse function if necessary, provides parsing new URLs
079     * the new URLs follow the scheme https://www.openstreetmap.org/#map=18/51.71873/8.76164&amp;layers=CN
080     * @param url string for parsing
081     * @return Bounds if hashurl, {@code null} otherwise
082     */
083    private static Bounds parseHashURLs(String url) {
084        int startIndex = url.indexOf("#map=");
085        if (startIndex == -1) return null;
086        int endIndex = url.indexOf('&', startIndex);
087        if (endIndex == -1) endIndex = url.length();
088        String coordPart = url.substring(startIndex+5, endIndex);
089        String[] parts = coordPart.split("/");
090        if (parts.length < 3) {
091            Main.warn(tr("URL does not contain {0}/{1}/{2}", tr("zoom"), tr("latitude"), tr("longitude")));
092            return null;
093        }
094        int zoom;
095        double lat, lon;
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        try {
103            lat = Double.parseDouble(parts[1]);
104        } catch (NumberFormatException e) {
105            Main.warn(tr("URL does not contain valid {0}", tr("latitude")), e);
106            return null;
107        }
108        try {
109            lon = Double.parseDouble(parts[2]);
110        } catch (NumberFormatException e) {
111            Main.warn(tr("URL does not contain valid {0}", tr("longitude")), e);
112            return null;
113        }
114        return positionToBounds(lat, lon, zoom);
115    }
116
117    private static double parseDouble(Map<String, String> map, String key) {
118        if (map.containsKey(key))
119            return Double.parseDouble(map.get(key));
120        return Double.parseDouble(map.get('m'+key));
121    }
122
123    private static final char[] SHORTLINK_CHARS = {
124        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
125        'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
126        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
127        'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
128        'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
129        'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
130        'w', 'x', 'y', 'z', '0', '1', '2', '3',
131        '4', '5', '6', '7', '8', '9', '_', '@'
132    };
133
134    /**
135     * Parse OSM short link
136     *
137     * @param url string for parsing
138     * @return Bounds if shortlink, null otherwise
139     * @see <a href="http://trac.openstreetmap.org/browser/sites/rails_port/lib/short_link.rb">short_link.rb</a>
140     */
141    private static Bounds parseShortLink(final String url) {
142        if (!url.startsWith(SHORTLINK_PREFIX))
143            return null;
144        final String shortLink = url.substring(SHORTLINK_PREFIX.length());
145
146        final Map<Character, Integer> array = new HashMap<>();
147
148        for (int i = 0; i < SHORTLINK_CHARS.length; ++i) {
149            array.put(SHORTLINK_CHARS[i], i);
150        }
151
152        // long is necessary (need 32 bit positive value is needed)
153        long x = 0;
154        long y = 0;
155        int zoom = 0;
156        int zoomOffset = 0;
157
158        for (final char ch : shortLink.toCharArray()) {
159            if (array.containsKey(ch)) {
160                int val = array.get(ch);
161                for (int i = 0; i < 3; ++i) {
162                    x <<= 1;
163                    if ((val & 32) != 0) {
164                        x |= 1;
165                    }
166                    val <<= 1;
167
168                    y <<= 1;
169                    if ((val & 32) != 0) {
170                        y |= 1;
171                    }
172                    val <<= 1;
173                }
174                zoom += 3;
175            } else {
176                zoomOffset--;
177            }
178        }
179
180        x <<= 32 - zoom;
181        y <<= 32 - zoom;
182
183        // 2**32 == 4294967296
184        return positionToBounds(y * 180.0 / 4294967296.0 - 90.0,
185                x * 360.0 / 4294967296.0 - 180.0,
186                // TODO: -2 was not in ruby code
187                zoom - 8 - (zoomOffset % 3) - 2);
188    }
189
190    /** radius of the earth */
191    public static final double R = 6378137.0;
192
193    public static Bounds positionToBounds(final double lat, final double lon, final int zoom) {
194        int tileSizeInPixels = 256;
195        int height;
196        int width;
197        try {
198            height = Toolkit.getDefaultToolkit().getScreenSize().height;
199            width = Toolkit.getDefaultToolkit().getScreenSize().width;
200            if (Main.isDisplayingMapView()) {
201                height = Main.map.mapView.getHeight();
202                width = Main.map.mapView.getWidth();
203            }
204        } catch (HeadlessException he) {
205            // in headless mode, when running tests
206            height = 480;
207            width = 640;
208        }
209        double scale = (1 << zoom) * tileSizeInPixels / (2 * Math.PI * R);
210        double deltaX = width / 2.0 / scale;
211        double deltaY = height / 2.0 / scale;
212        double x = Math.toRadians(lon) * R;
213        double y = mercatorY(lat);
214        return new Bounds(invMercatorY(y - deltaY), Math.toDegrees(x - deltaX) / R, invMercatorY(y + deltaY), Math.toDegrees(x + deltaX) / R);
215    }
216
217    public static double mercatorY(double lat) {
218        return Math.log(Math.tan(Math.PI/4 + Math.toRadians(lat)/2)) * R;
219    }
220
221    public static double invMercatorY(double north) {
222        return Math.toDegrees(Math.atan(Math.sinh(north / R)));
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}