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&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}