source: josm/trunk/src/org/openstreetmap/josm/tools/OsmUrlToBounds.java

Last change on this file was 18129, checked in by Don-vip, 3 years ago

tools update: checkstyle 8.44, spotbugs 4.2.3, proguard 7.1.1, error-prone 2.8.1

  • Property svn:eol-style set to native
File size: 10.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Dimension;
7import java.util.HashMap;
8import java.util.Map;
9import java.util.Objects;
10import java.util.function.Supplier;
11
12import org.openstreetmap.josm.data.Bounds;
13import org.openstreetmap.josm.data.coor.EastNorth;
14import org.openstreetmap.josm.data.coor.LatLon;
15import org.openstreetmap.josm.data.projection.Ellipsoid;
16import org.openstreetmap.josm.data.projection.Projection;
17import org.openstreetmap.josm.data.projection.Projections;
18import org.openstreetmap.josm.spi.preferences.Config;
19
20/**
21 * Parses various URL used in OpenStreetMap projects into {@link Bounds}.
22 */
23public final class OsmUrlToBounds {
24 private static final String SHORTLINK_PREFIX = "http://osm.org/go/";
25
26 private static volatile Supplier<Dimension> mapSize = () -> new Dimension(800, 600);
27
28 private OsmUrlToBounds() {
29 // Hide default constructor for utils classes
30 }
31
32 /**
33 * Parses an URL into {@link Bounds}
34 * @param url the URL to be parsed
35 * @return the parsed {@link Bounds}, or {@code null}
36 */
37 public static Bounds parse(String url) {
38 if (url.startsWith("geo:")) {
39 return GeoUrlToBounds.parse(url);
40 }
41 try {
42 // a percent sign indicates an encoded URL (RFC 1738).
43 if (url.contains("%")) {
44 url = Utils.decodeUrl(url);
45 }
46 } catch (IllegalArgumentException ex) {
47 Logging.error(ex);
48 }
49 Bounds b = parseShortLink(url);
50 if (b != null)
51 return b;
52 if (url.contains("#map") || url.contains("/#")) {
53 // probably it's a URL following the new scheme?
54 return parseHashURLs(url);
55 }
56 final int i = url.indexOf('?');
57 if (i == -1) {
58 return null;
59 }
60 String[] args = url.substring(i+1).split("&", -1);
61 Map<String, String> map = new HashMap<>();
62 for (String arg : args) {
63 int eq = arg.indexOf('=');
64 if (eq != -1) {
65 map.put(arg.substring(0, eq), arg.substring(eq + 1));
66 }
67 }
68
69 try {
70 if (map.containsKey("bbox")) {
71 String[] bbox = map.get("bbox").split(",", -1);
72 b = new Bounds(
73 Double.parseDouble(bbox[1]), Double.parseDouble(bbox[0]),
74 Double.parseDouble(bbox[3]), Double.parseDouble(bbox[2]));
75 } else if (map.containsKey("minlat")) {
76 double minlat = Double.parseDouble(map.get("minlat"));
77 double minlon = Double.parseDouble(map.get("minlon"));
78 double maxlat = Double.parseDouble(map.get("maxlat"));
79 double maxlon = Double.parseDouble(map.get("maxlon"));
80 b = new Bounds(minlat, minlon, maxlat, maxlon);
81 } else {
82 String z = map.get("zoom");
83 b = positionToBounds(parseDouble(map, "lat"), parseDouble(map, "lon"),
84 z == null ? 18 : Integer.parseInt(z));
85 }
86 } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ex) {
87 Logging.log(Logging.LEVEL_ERROR, url, ex);
88 }
89 return b;
90 }
91
92 /**
93 * Openstreetmap.org changed it's URL scheme in August 2013, which breaks the URL parsing.
94 * The following function, called by the old parse function if necessary, provides parsing new URLs
95 * the new URLs follow the scheme https://www.openstreetmap.org/#map=18/51.71873/8.76164&amp;layers=CN
96 * @param url string for parsing
97 * @return Bounds if hashurl, {@code null} otherwise
98 */
99 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("/", -1);
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="https://github.com/openstreetmap/openstreetmap-website/blob/master/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 (int j = 0; j < shortLink.length(); j++) {
177 char ch = shortLink.charAt(j);
178 if (array.containsKey(ch)) {
179 int val = array.get(ch);
180 for (int i = 0; i < 3; ++i) {
181 x <<= 1;
182 if ((val & 32) != 0) {
183 x |= 1;
184 }
185 val <<= 1;
186
187 y <<= 1;
188 if ((val & 32) != 0) {
189 y |= 1;
190 }
191 val <<= 1;
192 }
193 zoom += 3;
194 } else {
195 zoomOffset--;
196 }
197 }
198
199 x <<= 32 - zoom;
200 y <<= 32 - zoom;
201
202 // 2**32 == 4294967296
203 return positionToBounds(y * 180.0 / 4294967296.0 - 90.0,
204 x * 360.0 / 4294967296.0 - 180.0,
205 // TODO: -2 was not in ruby code
206 zoom - 8 - (zoomOffset % 3) - 2);
207 }
208
209 /**
210 * Sets the map size supplier.
211 * @param mapSizeSupplier returns the map size in pixels
212 * @since 12796
213 */
214 public static void setMapSizeSupplier(Supplier<Dimension> mapSizeSupplier) {
215 mapSize = Objects.requireNonNull(mapSizeSupplier, "mapSizeSupplier");
216 }
217
218 private static final int TILE_SIZE_IN_PIXELS = 256;
219
220 /**
221 * Compute the bounds for a given lat/lon position and the zoom level
222 * @param lat The latitude
223 * @param lon The longitude
224 * @param zoom The current zoom level
225 * @return The bounds the OSM server would display
226 */
227 public static Bounds positionToBounds(final double lat, final double lon, final int zoom) {
228 final Dimension screenSize = mapSize.get();
229 double scale = (1L << zoom) * TILE_SIZE_IN_PIXELS / (2.0 * Math.PI * Ellipsoid.WGS84.a);
230 double deltaX = screenSize.getWidth() / 2.0 / scale;
231 double deltaY = screenSize.getHeight() / 2.0 / scale;
232 final Projection mercator = Projections.getProjectionByCode("EPSG:3857");
233 final EastNorth projected = mercator.latlon2eastNorth(new LatLon(lat, lon));
234 return new Bounds(
235 mercator.eastNorth2latlon(projected.add(-deltaX, -deltaY)),
236 mercator.eastNorth2latlon(projected.add(deltaX, deltaY)));
237 }
238
239 /**
240 * Return OSM Zoom level for a given area
241 *
242 * @param b bounds of the area
243 * @return matching zoom level for area
244 */
245 public static int getZoom(Bounds b) {
246 final Projection mercator = Projections.getProjectionByCode("EPSG:3857");
247 final EastNorth min = mercator.latlon2eastNorth(b.getMin());
248 final EastNorth max = mercator.latlon2eastNorth(b.getMax());
249 final double deltaX = max.getX() - min.getX();
250 final double scale = mapSize.get().getWidth() / deltaX;
251 final double x = scale * (2 * Math.PI * Ellipsoid.WGS84.a) / TILE_SIZE_IN_PIXELS;
252 return (int) Math.round(Math.log(x) / Math.log(2));
253 }
254
255 /**
256 * Return OSM URL for given area.
257 *
258 * @param b bounds of the area
259 * @return link to display that area in OSM map
260 */
261 public static String getURL(Bounds b) {
262 return getURL(b.getCenter(), getZoom(b));
263 }
264
265 /**
266 * Return OSM URL for given position and zoom.
267 *
268 * @param pos center position of area
269 * @param zoom zoom depth of display
270 * @return link to display that area in OSM map
271 */
272 public static String getURL(LatLon pos, int zoom) {
273 return getURL(pos.lat(), pos.lon(), zoom);
274 }
275
276 /**
277 * Return OSM URL for given lat/lon and zoom.
278 *
279 * @param dlat center latitude of area
280 * @param dlon center longitude of area
281 * @param zoom zoom depth of display
282 * @return link to display that area in OSM map
283 *
284 * @since 6453
285 */
286 public static String getURL(double dlat, double dlon, int zoom) {
287 // Truncate lat and lon to something more sensible
288 int decimals = (int) Math.pow(10, zoom / 3d);
289 double lat = Math.round(dlat * decimals);
290 lat /= decimals;
291 double lon = Math.round(dlon * decimals);
292 lon /= decimals;
293 return Config.getUrls().getOSMWebsite() + "/#map="+zoom+'/'+lat+'/'+lon;
294 }
295}
Note: See TracBrowser for help on using the repository browser.