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