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}