001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.utils; 003 004import java.io.UnsupportedEncodingException; 005import java.net.MalformedURLException; 006import java.net.URL; 007import java.net.URLEncoder; 008import java.nio.charset.StandardCharsets; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.EnumSet; 012import java.util.HashMap; 013import java.util.List; 014import java.util.Map; 015import java.util.Map.Entry; 016 017import org.openstreetmap.josm.data.Bounds; 018import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils; 019import org.openstreetmap.josm.tools.I18n; 020import org.openstreetmap.josm.tools.Logging; 021 022public final class StreetsideURL { 023 /** Base URL of the Bing Bubble API. */ 024 private static final String STREETSIDE_BASE_URL = "https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx"; 025 //private static final String BASE_API_V2_URL = "https://a.mapillary.com/v2/"; 026 private static final String CLIENT_ID = "T1Fzd20xZjdtR0s1VDk5OFNIOXpYdzoxNDYyOGRkYzUyYTFiMzgz"; 027 private static final String BING_MAPS_KEY = "AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm"; 028 private static final String TEST_BUBBLE_ID = "80848005"; 029 030 private static final String STREETSIDE_PRIVACY_URL = "https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid="; 031 032 private static final int OSM_BBOX_NORTH = 3; 033 private static final int OSM_BBOX_SOUTH = 1; 034 private static final int OSM_BBOXEAST = 2; 035 private static final int OSM_BBOX_WEST = 0; 036 037 public static final class APIv3 { 038 //private static final String BASE_URL = "https://a.mapillary.com/v3/"; 039 040 private APIv3() { 041 // Private constructor to avoid instantiation 042 } 043 044 /*public static URL getUser(String key) { 045 return StreetsideURL.string2URL(APIv3.BASE_URL, "users/", key, StreetsideURL.queryString(null)); 046 } 047 048 *//** 049 * @return the URL where you can create, get and approve changesets 050 *//* 051 public static URL submitChangeset() { 052 return StreetsideURL.string2URL(APIv3.BASE_URL, "changesets", APIv3.queryString(null)); 053 } 054 055 public static URL searchDetections(Bounds bounds) { 056 return StreetsideURL.string2URL(APIv3.BASE_URL, "detections", APIv3.queryString(bounds)); 057 } 058 059 public static URL searchImages(Bounds bounds) { 060 return StreetsideURL.string2URL(APIv3.BASE_URL, "images", APIv3.queryStreetsideString(bounds)); 061 }*/ 062 063 public static URL searchStreetsideImages(Bounds bounds) { 064 return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_BASE_URL, APIv3.queryStreetsideString(bounds)); 065 } 066 067 /*public static URL searchMapObjects(final Bounds bounds) { 068 return StreetsideURL.string2URL(APIv3.BASE_URL, "objects", APIv3.queryString(bounds)); 069 }*/ 070 071 public static URL searchStreetsideSequences(final Bounds bounds) { 072 return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_BASE_URL, APIv3.queryStreetsideString(bounds)); 073 } 074 075 /** 076 * The APIv3 returns a Link header for each request. It contains a URL for requesting more results. 077 * If you supply the value of the Link header, this method returns the next URL, 078 * if such a URL is defined in the header. 079 * @param value the value of the HTTP-header with key "Link" 080 * @return the {@link URL} for the next result page, or <code>null</code> if no such URL could be found 081 */ 082 public static URL parseNextFromLinkHeaderValue(String value) { 083 if (value != null) { 084 // Iterate over the different entries of the Link header 085 for (final String link : value.split(",", Integer.MAX_VALUE)) { 086 boolean isNext = false; 087 URL url = null; 088 // Iterate over the parts of each entry (typically it's one `rel="‹linkType›"` and one like `<https://URL>`) 089 for (String linkPart : link.split(";", Integer.MAX_VALUE)) { 090 linkPart = linkPart.trim(); 091 isNext |= linkPart.matches("rel\\s*=\\s*\"next\""); 092 if (linkPart.length() >= 1 && linkPart.charAt(0) == '<' && linkPart.endsWith(">")) { 093 try { 094 url = new URL(linkPart.substring(1, linkPart.length() - 1)); 095 } catch (final MalformedURLException e) { 096 Logging.log(Logging.LEVEL_WARN, "Mapillary API v3 returns a malformed URL in the Link header.", e); 097 } 098 } 099 } 100 // If both a URL and the rel=next attribute are present, return the URL. Otherwise null is returned 101 if (url != null && isNext) { 102 return url; 103 } 104 } 105 } 106 return null; 107 } 108 109 public static String queryString(final Bounds bounds) { 110 if (bounds != null) { 111 final Map<String, String> parts = new HashMap<>(); 112 parts.put("bbox", bounds.toBBox().toStringCSV(",")); 113 return StreetsideURL.queryString(parts); 114 } 115 return StreetsideURL.queryString(null); 116 } 117 118 public static String queryStreetsideString(final Bounds bounds) { 119 if (bounds != null) { 120 final Map<String, String> parts = new HashMap<>(); 121 parts.put("bbox", bounds.toBBox().toStringCSV(",")); 122 return StreetsideURL.queryStreetsideBoundsString(parts); 123 } 124 return StreetsideURL.queryStreetsideBoundsString(null); 125 } 126 127 /** 128 * @return the URL where you'll find information about the user account as JSON 129 */ 130 /*public static URL userURL() { 131 return StreetsideURL.string2URL(APIv3.BASE_URL, "me", StreetsideURL.queryString(null)); 132 }*/ 133 } 134 135 public static final class VirtualEarth { 136 private static final String BASE_URL_PREFIX = "https://t.ssl.ak.tiles.virtualearth.net/tiles/hs"; 137 private static final String BASE_URL_SUFFIX = ".jpg?g=6338&n=z"; 138 139 private VirtualEarth() { 140 // Private constructor to avoid instantiation 141 } 142 143 public static URL streetsideTile(String id, boolean thumbnail) { 144 if(thumbnail) { 145 id = id + "01"; 146 } 147 URL url = StreetsideURL.string2URL(VirtualEarth.BASE_URL_PREFIX + id + VirtualEarth.BASE_URL_SUFFIX); 148 Logging.info("Tile task URL {0} invoked.", url.toString()); 149 return url; 150 } 151 } 152 153 public static final class MainWebsite { 154 //private static final String BASE_URL = "https://www.mapillary.com/"; 155 156 private MainWebsite() { 157 // Private constructor to avoid instantiation 158 } 159 160 /** 161 * Gives you the URL for the online viewer of a specific Streetside image. 162 * @param id the id of the image to which you want to link 163 * @return the URL of the online viewer for the image with the given image key 164 * @throws IllegalArgumentException if the image key is <code>null</code> 165 */ 166 public static URL browseImage(String id) { 167 if (id == null) { 168 throw new IllegalArgumentException("The image key must not be null!"); 169 } 170 return StreetsideURL.string2URL(VirtualEarth.BASE_URL_PREFIX + id + VirtualEarth.BASE_URL_SUFFIX); 171 } 172 173 /** 174 * Gives you the URL for the blur editor of the image with the given key. 175 * @param key the key of the image for which you want to open the blur editor 176 * @return the URL of the blur editor 177 * @throws IllegalArgumentException if the image key is <code>null</code> 178 */ 179 /*public static URL blurEditImage(final String key) { 180 if (key == null) { 181 throw new IllegalArgumentException("The image key must not be null!"); 182 } 183 String urlEncodedKey; 184 try { 185 urlEncodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.name()); 186 } catch (final UnsupportedEncodingException e) { 187 Logging.log(Logging.LEVEL_ERROR, "Unsupported encoding when URL encoding", e); 188 urlEncodedKey = key; 189 } 190 return StreetsideURL.string2URL(MainWebsite.BASE_URL, "app/blur?focus=photo&pKey=", urlEncodedKey); 191 }*/ 192 193 /** 194 * Gives you the URL for the blur editor of the image with the given key. 195 * @param id the key of the image for which you want to open the blur editor 196 * @return the URL of the blur editor 197 * @throws IllegalArgumentException if the image key is <code>null</code> 198 */ 199 public static URL streetsidePrivacyLink(final String id) { 200 if (id == null) { 201 throw new IllegalArgumentException("The image id must not be null!"); 202 } 203 String urlEncodedId; 204 try { 205 urlEncodedId = URLEncoder.encode(id, StandardCharsets.UTF_8.name()); 206 } catch (final UnsupportedEncodingException e) { 207 Logging.log(Logging.LEVEL_ERROR, "Unsupported encoding when URL encoding", e); 208 urlEncodedId = id; 209 } 210 return StreetsideURL.string2URL(StreetsideURL.STREETSIDE_PRIVACY_URL, urlEncodedId); 211 } 212 213 /** 214 * Gives you the URL which the user should visit to initiate the OAuth authentication process 215 * @param redirectURI the URI to which the user will be redirected when the authentication is finished. 216 * When this is <code>null</code>, it's omitted from the query string. 217 * @return the URL that the user should visit to start the OAuth authentication 218 */ 219 /*public static URL connect(String redirectURI) { 220 final HashMap<String, String> parts = new HashMap<>(); 221 if (redirectURI != null && redirectURI.length() >= 1) { 222 parts.put("redirect_uri", redirectURI); 223 } 224 parts.put("response_type", "token"); 225 parts.put("scope", "user:read public:upload public:write"); 226 return StreetsideURL.string2URL(MainWebsite.BASE_URL, "connect", StreetsideURL.queryString(parts)); 227 } 228 229 public static URL mapObjectIcon(String key) { 230 return StreetsideURL.string2URL(MainWebsite.BASE_URL, "developer/api-documentation/images/traffic_sign/" + key + ".png"); 231 }*/ 232 } 233 234 private StreetsideURL() { 235 // Private constructor to avoid instantiation 236 } 237 238 public static URL[] string2URLs(String baseUrlPrefix, String cubemapImageId, String baseUrlSuffix) { 239 List<URL> res = new ArrayList<>(); 240 241 switch (StreetsideProperties.SHOW_HIGH_RES_STREETSIDE_IMAGERY.get() ? 16 : 4) { 242 243 case 16: 244 245 EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> { 246 for (int i = 0; i < 4; i++) { 247 for (int j = 0; j < 4; j++) { 248 try { 249 final String urlStr = baseUrlPrefix + cubemapImageId 250 + CubemapUtils.rowCol2StreetsideCellAddressMap 251 .get(String.valueOf(i) + String.valueOf(j)) 252 + baseUrlSuffix; 253 res.add(new URL(urlStr)); 254 } catch (final MalformedURLException e) { 255 Logging.error(I18n.tr("Error creating URL String for cubemap {0}", cubemapImageId)); 256 e.printStackTrace(); 257 } 258 259 } 260 } 261 }); 262 263 case 4: 264 EnumSet.allOf(CubemapUtils.CubemapFaces.class).forEach(face -> { 265 for (int i = 0; i < 4; i++) { 266 267 try { 268 final String urlStr = baseUrlPrefix + cubemapImageId 269 + CubemapUtils.rowCol2StreetsideCellAddressMap.get(String.valueOf(i)) + baseUrlSuffix; 270 res.add(new URL(urlStr)); 271 } catch (final MalformedURLException e) { 272 Logging.error(I18n.tr("Error creating URL String for cubemap {0}", cubemapImageId)); 273 e.printStackTrace(); 274 } 275 276 } 277 }); 278 break; // break is optional 279 default: 280 // Statements 281 } 282 return res.stream().toArray(URL[]::new); 283 } 284 285 /** 286 * @return the URL where you'll find the upload secrets as JSON 287 */ 288 /*public static URL uploadSecretsURL() { 289 return StreetsideURL.string2URL(StreetsideURL.BASE_API_V2_URL, "me/uploads/secrets", StreetsideURL.queryString(null)); 290 }*/ 291 292 /** 293 * Builds a query string from it's parts that are supplied as a {@link Map} 294 * @param parts the parts of the query string 295 * @return the constructed query string (including a leading ?) 296 */ 297 static String queryString(Map<String, String> parts) { 298 final StringBuilder ret = new StringBuilder("?client_id=").append(StreetsideURL.CLIENT_ID); 299 if (parts != null) { 300 for (final Entry<String, String> entry : parts.entrySet()) { 301 try { 302 ret.append('&') 303 .append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name())) 304 .append('=') 305 .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name())); 306 } catch (final UnsupportedEncodingException e) { 307 Logging.error(e); // This should not happen, as the encoding is hard-coded 308 } 309 } 310 } 311 312 Logging.info(I18n.tr("queryString result: {0}", ret.toString())); 313 314 return ret.toString(); 315 } 316 317 static String queryStreetsideBoundsString(Map<String, String> parts) { 318 final StringBuilder ret = new StringBuilder("?n="); 319 if (parts != null) { 320 final List<String> bbox = new ArrayList<>(Arrays.asList(parts.get("bbox").split(","))); 321 try { 322 ret.append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOX_NORTH), StandardCharsets.UTF_8.name())) 323 .append("&s=") 324 .append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOX_SOUTH), StandardCharsets.UTF_8.name())) 325 .append("&e=") 326 .append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOXEAST), StandardCharsets.UTF_8.name())) 327 .append("&w=") 328 .append(URLEncoder.encode(bbox.get(StreetsideURL.OSM_BBOX_WEST), StandardCharsets.UTF_8.name())) 329 .append("&c=1000") 330 .append("&appkey=") 331 .append(StreetsideURL.BING_MAPS_KEY); 332 } catch (final UnsupportedEncodingException e) { 333 Logging.error(e); // This should not happen, as the encoding is hard-coded 334 } 335 } 336 Logging.info(I18n.tr("queryStreetsideBoundsString result: {0}", ret.toString())); 337 338 return ret.toString(); 339 } 340 341 static String queryByIdString(Map<String, String> parts) { 342 final StringBuilder ret = new StringBuilder("?id="); 343 try { 344 ret.append(URLEncoder.encode(StreetsideURL.TEST_BUBBLE_ID, StandardCharsets.UTF_8.name())); 345 ret.append('&').append(URLEncoder.encode("appkey=", StandardCharsets.UTF_8.name())).append('=') 346 .append(URLEncoder.encode(StreetsideURL.BING_MAPS_KEY, StandardCharsets.UTF_8.name())); 347 } catch (final UnsupportedEncodingException e) { 348 Logging.error(e); // This should not happen, as the encoding is hard-coded 349 } 350 Logging.info(I18n.tr("queryById result: {0}", ret.toString())); 351 return ret.toString(); 352 } 353 354 /** 355 * Converts a {@link String} into a {@link URL} without throwing a {@link MalformedURLException}. 356 * Instead such an exception will lead to an {@link Logging#error(Throwable)}. 357 * So you should be very confident that your URL is well-formed when calling this method. 358 * @param strings the Strings describing the URL 359 * @return the URL that is constructed from the given string 360 */ 361 static URL string2URL(String... strings) { 362 final StringBuilder builder = new StringBuilder(); 363 for (int i = 0; strings != null && i < strings.length; i++) { 364 builder.append(strings[i]); 365 } 366 try { 367 return new URL(builder.toString()); 368 } catch (final MalformedURLException e) { 369 Logging.log(Logging.LEVEL_ERROR, String.format( 370 "The class '%s' produces malformed URLs like '%s'!", 371 StreetsideURL.class.getName(), 372 builder 373 ), e); 374 return null; 375 } 376 } 377}