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.Dimension;
007import java.util.HashMap;
008import java.util.Map;
009
010import org.openstreetmap.josm.Main;
011import org.openstreetmap.josm.data.Bounds;
012import org.openstreetmap.josm.data.coor.LatLon;
013import org.openstreetmap.josm.data.projection.Ellipsoid;
014import org.openstreetmap.josm.gui.util.GuiHelper;
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        Dimension screenSize = GuiHelper.getScreenSize();
194        int height = screenSize.height;
195        int width = screenSize.width;
196        if (Main.isDisplayingMapView()) {
197            height = Main.map.mapView.getHeight();
198            width = Main.map.mapView.getWidth();
199        }
200        double scale = (1 << zoom) * tileSizeInPixels / (2 * Math.PI * Ellipsoid.WGS84.a);
201        double deltaX = width / 2.0 / scale;
202        double deltaY = height / 2.0 / scale;
203        double x = Math.toRadians(lon) * Ellipsoid.WGS84.a;
204        double y = mercatorY(lat);
205        return new Bounds(
206                invMercatorY(y - deltaY), Math.toDegrees(x - deltaX) / Ellipsoid.WGS84.a,
207                invMercatorY(y + deltaY), Math.toDegrees(x + deltaX) / Ellipsoid.WGS84.a);
208    }
209
210    public static double mercatorY(double lat) {
211        return Math.log(Math.tan(Math.PI/4 + Math.toRadians(lat)/2)) * Ellipsoid.WGS84.a;
212    }
213
214    public static double invMercatorY(double north) {
215        return Math.toDegrees(Math.atan(Math.sinh(north / Ellipsoid.WGS84.a)));
216    }
217
218    public static Pair<Double, Double> getTileOfLatLon(double lat, double lon, double zoom) {
219        double x = Math.floor((lon + 180) / 360 * Math.pow(2.0, zoom));
220        double y = Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI)
221                / 2 * Math.pow(2.0, zoom));
222        return new Pair<>(x, y);
223    }
224
225    public static LatLon getLatLonOfTile(double x, double y, double zoom) {
226        double lon = x / Math.pow(2.0, zoom) * 360.0 - 180;
227        double lat = Math.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, zoom))));
228        return new LatLon(lat, lon);
229    }
230
231    /**
232     * Return OSM Zoom level for a given area
233     *
234     * @param b bounds of the area
235     * @return matching zoom level for area
236     */
237    public static int getZoom(Bounds b) {
238        // convert to mercator (for calculation of zoom only)
239        double latMin = Math.log(Math.tan(Math.PI/4.0+b.getMinLat()/180.0*Math.PI/2.0))*180.0/Math.PI;
240        double latMax = Math.log(Math.tan(Math.PI/4.0+b.getMaxLat()/180.0*Math.PI/2.0))*180.0/Math.PI;
241        double size = Math.max(Math.abs(latMax-latMin), Math.abs(b.getMaxLon()-b.getMinLon()));
242        int zoom = 0;
243        while (zoom <= 20) {
244            if (size >= 180) {
245                break;
246            }
247            size *= 2;
248            zoom++;
249        }
250        return zoom;
251    }
252
253    /**
254     * Return OSM URL for given area.
255     *
256     * @param b bounds of the area
257     * @return link to display that area in OSM map
258     */
259    public static String getURL(Bounds b) {
260        return getURL(b.getCenter(), getZoom(b));
261    }
262
263    /**
264     * Return OSM URL for given position and zoom.
265     *
266     * @param pos center position of area
267     * @param zoom zoom depth of display
268     * @return link to display that area in OSM map
269     */
270    public static String getURL(LatLon pos, int zoom) {
271        return getURL(pos.lat(), pos.lon(), zoom);
272    }
273
274    /**
275     * Return OSM URL for given lat/lon and zoom.
276     *
277     * @param dlat center latitude of area
278     * @param dlon center longitude of area
279     * @param zoom zoom depth of display
280     * @return link to display that area in OSM map
281     *
282     * @since 6453
283     */
284    public static String getURL(double dlat, double dlon, int zoom) {
285        // Truncate lat and lon to something more sensible
286        int decimals = (int) Math.pow(10, zoom / 3d);
287        double lat = Math.round(dlat * decimals);
288        lat /= decimals;
289        double lon = Math.round(dlon * decimals);
290        lon /= decimals;
291        return Main.getOSMWebsite() + "/#map="+zoom+'/'+lat+'/'+lon;
292    }
293}