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