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; 014import org.openstreetmap.josm.data.projection.Ellipsoid; 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&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 int height; 194 int width; 195 try { 196 height = Toolkit.getDefaultToolkit().getScreenSize().height; 197 width = Toolkit.getDefaultToolkit().getScreenSize().width; 198 if (Main.isDisplayingMapView()) { 199 height = Main.map.mapView.getHeight(); 200 width = Main.map.mapView.getWidth(); 201 } 202 } catch (HeadlessException he) { 203 // in headless mode, when running tests 204 height = 480; 205 width = 640; 206 } 207 double scale = (1 << zoom) * tileSizeInPixels / (2 * Math.PI * Ellipsoid.WGS84.a); 208 double deltaX = width / 2.0 / scale; 209 double deltaY = height / 2.0 / scale; 210 double x = Math.toRadians(lon) * Ellipsoid.WGS84.a; 211 double y = mercatorY(lat); 212 return new Bounds( 213 invMercatorY(y - deltaY), Math.toDegrees(x - deltaX) / Ellipsoid.WGS84.a, 214 invMercatorY(y + deltaY), Math.toDegrees(x + deltaX) / Ellipsoid.WGS84.a); 215 } 216 217 public static double mercatorY(double lat) { 218 return Math.log(Math.tan(Math.PI/4 + Math.toRadians(lat)/2)) * Ellipsoid.WGS84.a; 219 } 220 221 public static double invMercatorY(double north) { 222 return Math.toDegrees(Math.atan(Math.sinh(north / Ellipsoid.WGS84.a))); 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}