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.LatLon; 013import org.openstreetmap.josm.data.projection.Ellipsoid; 014import org.openstreetmap.josm.gui.util.GuiHelper; 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 Dimension screenSize = GuiHelper.getScreenSize(); 194 int height = screenSize.height; 195 int width = screenSize.width; 196 if (Main.isDisplayingMapView()) { 197 height = Main.map.mapView.getHeight(); 198 width = Main.map.mapView.getWidth(); 199 } 200 double scale = (1 << zoom) * tileSizeInPixels / (2 * Math.PI * Ellipsoid.WGS84.a); 201 double deltaX = width / 2.0 / scale; 202 double deltaY = height / 2.0 / scale; 203 double x = Math.toRadians(lon) * Ellipsoid.WGS84.a; 204 double y = mercatorY(lat); 205 return new Bounds( 206 invMercatorY(y - deltaY), Math.toDegrees(x - deltaX) / Ellipsoid.WGS84.a, 207 invMercatorY(y + deltaY), Math.toDegrees(x + deltaX) / Ellipsoid.WGS84.a); 208 } 209 210 public static double mercatorY(double lat) { 211 return Math.log(Math.tan(Math.PI/4 + Math.toRadians(lat)/2)) * Ellipsoid.WGS84.a; 212 } 213 214 public static double invMercatorY(double north) { 215 return Math.toDegrees(Math.atan(Math.sinh(north / Ellipsoid.WGS84.a))); 216 } 217 218 public static Pair<Double, Double> getTileOfLatLon(double lat, double lon, double zoom) { 219 double x = Math.floor((lon + 180) / 360 * Math.pow(2.0, zoom)); 220 double y = Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) 221 / 2 * Math.pow(2.0, zoom)); 222 return new Pair<>(x, y); 223 } 224 225 public static LatLon getLatLonOfTile(double x, double y, double zoom) { 226 double lon = x / Math.pow(2.0, zoom) * 360.0 - 180; 227 double lat = Math.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, zoom)))); 228 return new LatLon(lat, lon); 229 } 230 231 /** 232 * Return OSM Zoom level for a given area 233 * 234 * @param b bounds of the area 235 * @return matching zoom level for area 236 */ 237 public static int getZoom(Bounds b) { 238 // convert to mercator (for calculation of zoom only) 239 double latMin = Math.log(Math.tan(Math.PI/4.0+b.getMinLat()/180.0*Math.PI/2.0))*180.0/Math.PI; 240 double latMax = Math.log(Math.tan(Math.PI/4.0+b.getMaxLat()/180.0*Math.PI/2.0))*180.0/Math.PI; 241 double size = Math.max(Math.abs(latMax-latMin), Math.abs(b.getMaxLon()-b.getMinLon())); 242 int zoom = 0; 243 while (zoom <= 20) { 244 if (size >= 180) { 245 break; 246 } 247 size *= 2; 248 zoom++; 249 } 250 return zoom; 251 } 252 253 /** 254 * Return OSM URL for given area. 255 * 256 * @param b bounds of the area 257 * @return link to display that area in OSM map 258 */ 259 public static String getURL(Bounds b) { 260 return getURL(b.getCenter(), getZoom(b)); 261 } 262 263 /** 264 * Return OSM URL for given position and zoom. 265 * 266 * @param pos center position of area 267 * @param zoom zoom depth of display 268 * @return link to display that area in OSM map 269 */ 270 public static String getURL(LatLon pos, int zoom) { 271 return getURL(pos.lat(), pos.lon(), zoom); 272 } 273 274 /** 275 * Return OSM URL for given lat/lon and zoom. 276 * 277 * @param dlat center latitude of area 278 * @param dlon center longitude of area 279 * @param zoom zoom depth of display 280 * @return link to display that area in OSM map 281 * 282 * @since 6453 283 */ 284 public static String getURL(double dlat, double dlon, int zoom) { 285 // Truncate lat and lon to something more sensible 286 int decimals = (int) Math.pow(10, zoom / 3d); 287 double lat = Math.round(dlat * decimals); 288 lat /= decimals; 289 double lon = Math.round(dlon * decimals); 290 lon /= decimals; 291 return Main.getOSMWebsite() + "/#map="+zoom+'/'+lat+'/'+lon; 292 } 293}