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}