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; 009import java.util.Objects; 010import java.util.function.Supplier; 011 012import org.openstreetmap.josm.Main; 013import org.openstreetmap.josm.data.Bounds; 014import org.openstreetmap.josm.data.coor.EastNorth; 015import org.openstreetmap.josm.data.coor.LatLon; 016import org.openstreetmap.josm.data.projection.Ellipsoid; 017import org.openstreetmap.josm.data.projection.Projection; 018import org.openstreetmap.josm.data.projection.Projections; 019 020/** 021 * Parses various URL used in OpenStreetMap projects into {@link Bounds}. 022 */ 023public final class OsmUrlToBounds { 024 private static final String SHORTLINK_PREFIX = "http://osm.org/go/"; 025 026 private static volatile Supplier<Dimension> mapSize = () -> new Dimension(800, 600); 027 028 private OsmUrlToBounds() { 029 // Hide default constructor for utils classes 030 } 031 032 /** 033 * Parses an URL into {@link Bounds} 034 * @param url the URL to be parsed 035 * @return the parsed {@link Bounds}, or {@code null} 036 */ 037 public static Bounds parse(String url) { 038 if (url.startsWith("geo:")) { 039 return GeoUrlToBounds.parse(url); 040 } 041 try { 042 // a percent sign indicates an encoded URL (RFC 1738). 043 if (url.contains("%")) { 044 url = Utils.decodeUrl(url); 045 } 046 } catch (IllegalArgumentException ex) { 047 Logging.error(ex); 048 } 049 Bounds b = parseShortLink(url); 050 if (b != null) 051 return b; 052 if (url.contains("#map") || url.contains("/#")) { 053 // probably it's a URL following the new scheme? 054 return parseHashURLs(url); 055 } 056 final int i = url.indexOf('?'); 057 if (i == -1) { 058 return null; 059 } 060 String[] args = url.substring(i+1).split("&"); 061 Map<String, String> map = new HashMap<>(); 062 for (String arg : args) { 063 int eq = arg.indexOf('='); 064 if (eq != -1) { 065 map.put(arg.substring(0, eq), arg.substring(eq + 1)); 066 } 067 } 068 069 try { 070 if (map.containsKey("bbox")) { 071 String[] bbox = map.get("bbox").split(","); 072 b = new Bounds( 073 Double.parseDouble(bbox[1]), Double.parseDouble(bbox[0]), 074 Double.parseDouble(bbox[3]), Double.parseDouble(bbox[2])); 075 } else if (map.containsKey("minlat")) { 076 double minlat = Double.parseDouble(map.get("minlat")); 077 double minlon = Double.parseDouble(map.get("minlon")); 078 double maxlat = Double.parseDouble(map.get("maxlat")); 079 double maxlon = Double.parseDouble(map.get("maxlon")); 080 b = new Bounds(minlat, minlon, maxlat, maxlon); 081 } else { 082 String z = map.get("zoom"); 083 b = positionToBounds(parseDouble(map, "lat"), parseDouble(map, "lon"), 084 z == null ? 18 : Integer.parseInt(z)); 085 } 086 } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ex) { 087 Logging.log(Logging.LEVEL_ERROR, url, ex); 088 } 089 return b; 090 } 091 092 /** 093 * Openstreetmap.org changed it's URL scheme in August 2013, which breaks the URL parsing. 094 * The following function, called by the old parse function if necessary, provides parsing new URLs 095 * the new URLs follow the scheme https://www.openstreetmap.org/#map=18/51.71873/8.76164&layers=CN 096 * @param url string for parsing 097 * @return Bounds if hashurl, {@code null} otherwise 098 */ 099 private static Bounds parseHashURLs(String url) { 100 int startIndex = url.indexOf('#'); 101 if (startIndex == -1) return null; 102 int endIndex = url.indexOf('&', startIndex); 103 if (endIndex == -1) endIndex = url.length(); 104 String coordPart = url.substring(startIndex+(url.contains("#map=") ? "#map=".length() : "#".length()), endIndex); 105 String[] parts = coordPart.split("/"); 106 if (parts.length < 3) { 107 Logging.warn(tr("URL does not contain {0}/{1}/{2}", tr("zoom"), tr("latitude"), tr("longitude"))); 108 return null; 109 } 110 int zoom; 111 try { 112 zoom = Integer.parseInt(parts[0]); 113 } catch (NumberFormatException e) { 114 Logging.warn(tr("URL does not contain valid {0}", tr("zoom")), e); 115 return null; 116 } 117 double lat, lon; 118 try { 119 lat = Double.parseDouble(parts[1]); 120 } catch (NumberFormatException e) { 121 Logging.warn(tr("URL does not contain valid {0}", tr("latitude")), e); 122 return null; 123 } 124 try { 125 lon = Double.parseDouble(parts[2]); 126 } catch (NumberFormatException e) { 127 Logging.warn(tr("URL does not contain valid {0}", tr("longitude")), e); 128 return null; 129 } 130 return positionToBounds(lat, lon, zoom); 131 } 132 133 private static double parseDouble(Map<String, String> map, String key) { 134 if (map.containsKey(key)) 135 return Double.parseDouble(map.get(key)); 136 if (map.containsKey('m'+key)) 137 return Double.parseDouble(map.get('m'+key)); 138 throw new IllegalArgumentException(map.toString() + " does not contain " + key); 139 } 140 141 private static final char[] SHORTLINK_CHARS = { 142 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 143 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 144 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 145 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 146 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 147 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 148 'w', 'x', 'y', 'z', '0', '1', '2', '3', 149 '4', '5', '6', '7', '8', '9', '_', '@' 150 }; 151 152 /** 153 * Parse OSM short link 154 * 155 * @param url string for parsing 156 * @return Bounds if shortlink, null otherwise 157 * @see <a href="http://trac.openstreetmap.org/browser/sites/rails_port/lib/short_link.rb">short_link.rb</a> 158 */ 159 private static Bounds parseShortLink(final String url) { 160 if (!url.startsWith(SHORTLINK_PREFIX)) 161 return null; 162 final String shortLink = url.substring(SHORTLINK_PREFIX.length()); 163 164 final Map<Character, Integer> array = new HashMap<>(); 165 166 for (int i = 0; i < SHORTLINK_CHARS.length; ++i) { 167 array.put(SHORTLINK_CHARS[i], i); 168 } 169 170 // long is necessary (need 32 bit positive value is needed) 171 long x = 0; 172 long y = 0; 173 int zoom = 0; 174 int zoomOffset = 0; 175 176 for (final char ch : shortLink.toCharArray()) { 177 if (array.containsKey(ch)) { 178 int val = array.get(ch); 179 for (int i = 0; i < 3; ++i) { 180 x <<= 1; 181 if ((val & 32) != 0) { 182 x |= 1; 183 } 184 val <<= 1; 185 186 y <<= 1; 187 if ((val & 32) != 0) { 188 y |= 1; 189 } 190 val <<= 1; 191 } 192 zoom += 3; 193 } else { 194 zoomOffset--; 195 } 196 } 197 198 x <<= 32 - zoom; 199 y <<= 32 - zoom; 200 201 // 2**32 == 4294967296 202 return positionToBounds(y * 180.0 / 4294967296.0 - 90.0, 203 x * 360.0 / 4294967296.0 - 180.0, 204 // TODO: -2 was not in ruby code 205 zoom - 8 - (zoomOffset % 3) - 2); 206 } 207 208 /** 209 * Sets the map size supplier. 210 * @param mapSizeSupplier returns the map size in pixels 211 * @since 12796 212 */ 213 public static void setMapSizeSupplier(Supplier<Dimension> mapSizeSupplier) { 214 mapSize = Objects.requireNonNull(mapSizeSupplier, "mapSizeSupplier"); 215 } 216 217 private static final int TILE_SIZE_IN_PIXELS = 256; 218 219 /** 220 * Compute the bounds for a given lat/lon position and the zoom level 221 * @param lat The latitude 222 * @param lon The longitude 223 * @param zoom The current zoom level 224 * @return The bounds the OSM server would display 225 */ 226 public static Bounds positionToBounds(final double lat, final double lon, final int zoom) { 227 final Dimension screenSize = mapSize.get(); 228 double scale = (1 << zoom) * TILE_SIZE_IN_PIXELS / (2 * Math.PI * Ellipsoid.WGS84.a); 229 double deltaX = screenSize.getWidth() / 2.0 / scale; 230 double deltaY = screenSize.getHeight() / 2.0 / scale; 231 final Projection mercator = Projections.getProjectionByCode("EPSG:3857"); 232 final EastNorth projected = mercator.latlon2eastNorth(new LatLon(lat, lon)); 233 return new Bounds( 234 mercator.eastNorth2latlon(projected.add(-deltaX, -deltaY)), 235 mercator.eastNorth2latlon(projected.add(deltaX, deltaY))); 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 final Projection mercator = Projections.getProjectionByCode("EPSG:3857"); 246 final EastNorth min = mercator.latlon2eastNorth(b.getMin()); 247 final EastNorth max = mercator.latlon2eastNorth(b.getMax()); 248 final double deltaX = max.getX() - min.getX(); 249 final double scale = mapSize.get().getWidth() / deltaX; 250 final double x = scale * (2 * Math.PI * Ellipsoid.WGS84.a) / TILE_SIZE_IN_PIXELS; 251 return (int) Math.round(Math.log(x) / Math.log(2)); 252 } 253 254 /** 255 * Return OSM URL for given area. 256 * 257 * @param b bounds of the area 258 * @return link to display that area in OSM map 259 */ 260 public static String getURL(Bounds b) { 261 return getURL(b.getCenter(), getZoom(b)); 262 } 263 264 /** 265 * Return OSM URL for given position and zoom. 266 * 267 * @param pos center position of area 268 * @param zoom zoom depth of display 269 * @return link to display that area in OSM map 270 */ 271 public static String getURL(LatLon pos, int zoom) { 272 return getURL(pos.lat(), pos.lon(), zoom); 273 } 274 275 /** 276 * Return OSM URL for given lat/lon and zoom. 277 * 278 * @param dlat center latitude of area 279 * @param dlon center longitude of area 280 * @param zoom zoom depth of display 281 * @return link to display that area in OSM map 282 * 283 * @since 6453 284 */ 285 public static String getURL(double dlat, double dlon, int zoom) { 286 // Truncate lat and lon to something more sensible 287 int decimals = (int) Math.pow(10, zoom / 3d); 288 double lat = Math.round(dlat * decimals); 289 lat /= decimals; 290 double lon = Math.round(dlon * decimals); 291 lon /= decimals; 292 return Main.getOSMWebsite() + "/#map="+zoom+'/'+lat+'/'+lon; 293 } 294}