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