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