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

Last change on this file since 12822 was 12803, checked in by Don-vip, 7 years ago

see #15229 - see #15182 - remove GUI references from OsmConnection

  • Property svn:eol-style set to native
File size: 10.6 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.Main;
13import org.openstreetmap.josm.data.Bounds;
14import org.openstreetmap.josm.data.coor.EastNorth;
15import org.openstreetmap.josm.data.coor.LatLon;
16import org.openstreetmap.josm.data.projection.Ellipsoid;
17import org.openstreetmap.josm.data.projection.Projection;
18import org.openstreetmap.josm.data.projection.Projections;
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 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("&");
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(",");
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("/");
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}
Note: See TracBrowser for help on using the repository browser.