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}