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; 014 015public final class OsmUrlToBounds { 016 private static final String SHORTLINK_PREFIX = "http://osm.org/go/"; 017 018 private OsmUrlToBounds() { 019 // Hide default constructor for utils classes 020 } 021 022 public static Bounds parse(String url) { 023 try { 024 // a percent sign indicates an encoded URL (RFC 1738). 025 if (url.contains("%")) { 026 url = Utils.decodeUrl(url); 027 } 028 } catch (IllegalArgumentException x) { 029 Main.error(x); 030 } 031 Bounds b = parseShortLink(url); 032 if (b != null) 033 return b; 034 int i = url.indexOf("#map"); 035 if (i >= 0) { 036 // probably it's a URL following the new scheme? 037 return parseHashURLs(url); 038 } 039 i = url.indexOf('?'); 040 if (i == -1) { 041 return null; 042 } 043 String[] args = url.substring(i+1).split("&"); 044 Map<String, String> map = new HashMap<>(); 045 for (String arg : args) { 046 int eq = arg.indexOf('='); 047 if (eq != -1) { 048 map.put(arg.substring(0, eq), arg.substring(eq + 1)); 049 } 050 } 051 052 try { 053 if (map.containsKey("bbox")) { 054 String[] bbox = map.get("bbox").split(","); 055 b = new Bounds( 056 Double.parseDouble(bbox[1]), Double.parseDouble(bbox[0]), 057 Double.parseDouble(bbox[3]), Double.parseDouble(bbox[2])); 058 } else if (map.containsKey("minlat")) { 059 double minlat = Double.parseDouble(map.get("minlat")); 060 double minlon = Double.parseDouble(map.get("minlon")); 061 double maxlat = Double.parseDouble(map.get("maxlat")); 062 double maxlon = Double.parseDouble(map.get("maxlon")); 063 b = new Bounds(minlat, minlon, maxlat, maxlon); 064 } else { 065 String z = map.get("zoom"); 066 b = positionToBounds(parseDouble(map, "lat"), 067 parseDouble(map, "lon"), 068 z == null ? 18 : Integer.parseInt(z)); 069 } 070 } catch (NumberFormatException | NullPointerException | ArrayIndexOutOfBoundsException x) { 071 Main.error(x); 072 } 073 return b; 074 } 075 076 /** 077 * Openstreetmap.org changed it's URL scheme in August 2013, which breaks the URL parsing. 078 * The following function, called by the old parse function if necessary, provides parsing new URLs 079 * the new URLs follow the scheme https://www.openstreetmap.org/#map=18/51.71873/8.76164&layers=CN 080 * @param url string for parsing 081 * @return Bounds if hashurl, {@code null} otherwise 082 */ 083 private static Bounds parseHashURLs(String url) { 084 int startIndex = url.indexOf("#map="); 085 if (startIndex == -1) return null; 086 int endIndex = url.indexOf('&', startIndex); 087 if (endIndex == -1) endIndex = url.length(); 088 String coordPart = url.substring(startIndex+5, endIndex); 089 String[] parts = coordPart.split("/"); 090 if (parts.length < 3) { 091 Main.warn(tr("URL does not contain {0}/{1}/{2}", tr("zoom"), tr("latitude"), tr("longitude"))); 092 return null; 093 } 094 int zoom; 095 double lat, lon; 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 try { 103 lat = Double.parseDouble(parts[1]); 104 } catch (NumberFormatException e) { 105 Main.warn(tr("URL does not contain valid {0}", tr("latitude")), e); 106 return null; 107 } 108 try { 109 lon = Double.parseDouble(parts[2]); 110 } catch (NumberFormatException e) { 111 Main.warn(tr("URL does not contain valid {0}", tr("longitude")), e); 112 return null; 113 } 114 return positionToBounds(lat, lon, zoom); 115 } 116 117 private static double parseDouble(Map<String, String> map, String key) { 118 if (map.containsKey(key)) 119 return Double.parseDouble(map.get(key)); 120 return Double.parseDouble(map.get('m'+key)); 121 } 122 123 private static final char[] SHORTLINK_CHARS = { 124 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 125 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 126 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 127 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 128 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 129 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 130 'w', 'x', 'y', 'z', '0', '1', '2', '3', 131 '4', '5', '6', '7', '8', '9', '_', '@' 132 }; 133 134 /** 135 * Parse OSM short link 136 * 137 * @param url string for parsing 138 * @return Bounds if shortlink, null otherwise 139 * @see <a href="http://trac.openstreetmap.org/browser/sites/rails_port/lib/short_link.rb">short_link.rb</a> 140 */ 141 private static Bounds parseShortLink(final String url) { 142 if (!url.startsWith(SHORTLINK_PREFIX)) 143 return null; 144 final String shortLink = url.substring(SHORTLINK_PREFIX.length()); 145 146 final Map<Character, Integer> array = new HashMap<>(); 147 148 for (int i = 0; i < SHORTLINK_CHARS.length; ++i) { 149 array.put(SHORTLINK_CHARS[i], i); 150 } 151 152 // long is necessary (need 32 bit positive value is needed) 153 long x = 0; 154 long y = 0; 155 int zoom = 0; 156 int zoomOffset = 0; 157 158 for (final char ch : shortLink.toCharArray()) { 159 if (array.containsKey(ch)) { 160 int val = array.get(ch); 161 for (int i = 0; i < 3; ++i) { 162 x <<= 1; 163 if ((val & 32) != 0) { 164 x |= 1; 165 } 166 val <<= 1; 167 168 y <<= 1; 169 if ((val & 32) != 0) { 170 y |= 1; 171 } 172 val <<= 1; 173 } 174 zoom += 3; 175 } else { 176 zoomOffset--; 177 } 178 } 179 180 x <<= 32 - zoom; 181 y <<= 32 - zoom; 182 183 // 2**32 == 4294967296 184 return positionToBounds(y * 180.0 / 4294967296.0 - 90.0, 185 x * 360.0 / 4294967296.0 - 180.0, 186 // TODO: -2 was not in ruby code 187 zoom - 8 - (zoomOffset % 3) - 2); 188 } 189 190 /** radius of the earth */ 191 public static final double R = 6378137.0; 192 193 public static Bounds positionToBounds(final double lat, final double lon, final int zoom) { 194 int tileSizeInPixels = 256; 195 int height; 196 int width; 197 try { 198 height = Toolkit.getDefaultToolkit().getScreenSize().height; 199 width = Toolkit.getDefaultToolkit().getScreenSize().width; 200 if (Main.isDisplayingMapView()) { 201 height = Main.map.mapView.getHeight(); 202 width = Main.map.mapView.getWidth(); 203 } 204 } catch (HeadlessException he) { 205 // in headless mode, when running tests 206 height = 480; 207 width = 640; 208 } 209 double scale = (1 << zoom) * tileSizeInPixels / (2 * Math.PI * R); 210 double deltaX = width / 2.0 / scale; 211 double deltaY = height / 2.0 / scale; 212 double x = Math.toRadians(lon) * R; 213 double y = mercatorY(lat); 214 return new Bounds(invMercatorY(y - deltaY), Math.toDegrees(x - deltaX) / R, invMercatorY(y + deltaY), Math.toDegrees(x + deltaX) / R); 215 } 216 217 public static double mercatorY(double lat) { 218 return Math.log(Math.tan(Math.PI/4 + Math.toRadians(lat)/2)) * R; 219 } 220 221 public static double invMercatorY(double north) { 222 return Math.toDegrees(Math.atan(Math.sinh(north / R))); 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}