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