    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}