Ignore:
Timestamp:
2018-05-12T14:18:57+02:00 (18 months ago)
Author:
wiktorn
Message:

Imagery definition refactor

Extend imagery definitions by:

  • allowing setting default layers for WMS_ENDPOINT and WMTS
  • allowing setting minimum expires time for tile for this imagery
  • allowing setting custom headers that will be sent for all requests

(get map, get capabilities) for this imagery

Additional changes in code:

  • use TileJobOptions to pass miscellaneous options to loaders
  • refactor WMSImagery to use SAX parser

See: #15981, #7953, #16224, #15940, #16249

Location:
trunk/src/org/openstreetmap/josm/io/imagery
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java

    r13536 r13733  
    77import java.util.ArrayList;
    88import java.util.Arrays;
    9 import java.util.HashMap;
    109import java.util.List;
    1110import java.util.Map;
    1211import java.util.Objects;
    1312import java.util.Stack;
     13import java.util.concurrent.ConcurrentHashMap;
    1414
    1515import javax.xml.parsers.ParserConfigurationException;
    1616
     17import org.openstreetmap.josm.data.imagery.DefaultLayer;
    1718import org.openstreetmap.josm.data.imagery.ImageryInfo;
    1819import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
     
    5758        NO_TILESUM,
    5859        METADATA,
    59         UNKNOWN,            // element is not recognized in the current context
     60        DEFAULT_LAYERS,
     61        CUSTOM_HTTP_HEADERS,
     62        NOOP,
     63        UNKNOWN,             // element is not recognized in the current context
    6064    }
    6165
     
    132136        private MultiMap<String, String> noTileChecksums;
    133137        private Map<String, String> metadataHeaders;
     138        private List<DefaultLayer> defaultLayers;
     139        private Map<String, String> customHttpHeaders;
    134140
    135141        @Override
     
    145151            noTileHeaders = null;
    146152            noTileChecksums = null;
     153            customHttpHeaders = null;
    147154        }
    148155
     
    164171                    noTileHeaders = new MultiMap<>();
    165172                    noTileChecksums = new MultiMap<>();
    166                     metadataHeaders = new HashMap<>();
     173                    metadataHeaders = new ConcurrentHashMap<>();
     174                    defaultLayers = new ArrayList<>();
     175                    customHttpHeaders = new ConcurrentHashMap<>();
    167176                    String best = atts.getValue("eli-best");
    168177                    if (TRUE.equals(best)) {
     
    215224                        TILE_SIZE,
    216225                        "valid-georeference",
    217                         "mod-tile-features"
     226                        "mod-tile-features",
     227                        "transparent",
     228                        "minimum-tile-expire"
    218229                ).contains(qName)) {
    219230                    newState = State.ENTRY_ATTRIBUTE;
     
    247258                    metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
    248259                    newState = State.METADATA;
     260                } else if ("defaultLayers".equals(qName)) {
     261                    newState = State.DEFAULT_LAYERS;
     262                } else if ("custom-http-header".equals(qName)) {
     263                   customHttpHeaders.put(atts.getValue("header-name"), atts.getValue("header-value"));
     264                   newState = State.CUSTOM_HTTP_HEADERS;
    249265                }
    250266                break;
     
    269285                if ("code".equals(qName)) {
    270286                    newState = State.CODE;
     287                }
     288                break;
     289            case DEFAULT_LAYERS:
     290                if ("layer".equals(qName)) {
     291                    newState = State.NOOP;
     292                    defaultLayers.add(new DefaultLayer(entry.getImageryType(), atts.getValue("name"),atts.getValue("style"), atts.getValue("tileMatrixSet")));
    271293                }
    272294                break;
     
    306328                    entry.setMetadataHeaders(metadataHeaders);
    307329                    metadataHeaders = null;
     330                    entry.setDefaultLayers(defaultLayers);
     331                    defaultLayers = null;
     332                    entry.setCustomHttpHeaders(customHttpHeaders);
     333                    customHttpHeaders = null;
    308334
    309335                    if (!skipEntry) {
     
    323349                    switch(qName) {
    324350                    case "type":
     351                        ImageryType.values();
    325352                        boolean found = false;
    326353                        for (ImageryType type : ImageryType.values()) {
     
    487514                case "mod-tile-features":
    488515                    entry.setModTileFeatures(Boolean.parseBoolean(accumulator.toString()));
     516                    break;
     517                case "transparent":
     518                    entry.setTransparent(Boolean.parseBoolean(accumulator.toString()));
     519                    break;
     520                case "minimum-tile-expire":
     521                    entry.setMinimumTileExpire(Integer.valueOf(accumulator.toString()));
    489522                    break;
    490523                default: // Do nothing
  • trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java

    r13699 r13733  
    22package org.openstreetmap.josm.io.imagery;
    33
    4 import java.awt.HeadlessException;
     4import static java.nio.charset.StandardCharsets.UTF_8;
     5import static org.openstreetmap.josm.tools.I18n.tr;
     6
     7import java.io.File;
    58import java.io.IOException;
    69import java.io.InputStream;
    7 import java.io.StringReader;
    8 import java.io.StringWriter;
    910import java.net.MalformedURLException;
    1011import java.net.URL;
     
    1314import java.util.Collections;
    1415import java.util.HashSet;
    15 import java.util.Iterator;
    1616import java.util.List;
    17 import java.util.Locale;
    18 import java.util.NoSuchElementException;
     17import java.util.Map;
    1918import java.util.Set;
    20 import java.util.regex.Matcher;
     19import java.util.concurrent.ConcurrentHashMap;
     20import java.util.function.Function;
    2121import java.util.regex.Pattern;
    2222import java.util.stream.Collectors;
    23 import java.util.stream.Stream;
    24 import java.util.stream.StreamSupport;
    2523
    2624import javax.imageio.ImageIO;
    27 import javax.xml.parsers.DocumentBuilder;
    28 import javax.xml.parsers.ParserConfigurationException;
    29 import javax.xml.transform.TransformerException;
    30 import javax.xml.transform.TransformerFactory;
    31 import javax.xml.transform.TransformerFactoryConfigurationError;
    32 import javax.xml.transform.dom.DOMSource;
    33 import javax.xml.transform.stream.StreamResult;
     25import javax.xml.namespace.QName;
     26import javax.xml.stream.XMLStreamException;
     27import javax.xml.stream.XMLStreamReader;
    3428
    3529import org.openstreetmap.josm.data.Bounds;
     30import org.openstreetmap.josm.data.coor.EastNorth;
     31import org.openstreetmap.josm.data.imagery.DefaultLayer;
    3632import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper;
    3733import org.openstreetmap.josm.data.imagery.ImageryInfo;
     34import org.openstreetmap.josm.data.imagery.LayerDetails;
     35import org.openstreetmap.josm.data.projection.Projection;
    3836import org.openstreetmap.josm.data.projection.Projections;
    39 import org.openstreetmap.josm.tools.HttpClient;
    40 import org.openstreetmap.josm.tools.HttpClient.Response;
     37import org.openstreetmap.josm.io.CachedFile;
    4138import org.openstreetmap.josm.tools.Logging;
    4239import org.openstreetmap.josm.tools.Utils;
    43 import org.w3c.dom.Document;
    44 import org.w3c.dom.Element;
    45 import org.w3c.dom.Node;
    46 import org.w3c.dom.NodeList;
    47 import org.xml.sax.InputSource;
    48 import org.xml.sax.SAXException;
    4940
    5041/**
     
    5344public class WMSImagery {
    5445
    55     private static final class ChildIterator implements Iterator<Element> {
    56         private Element child;
    57 
    58         ChildIterator(Element parent) {
    59             child = advanceToElement(parent.getFirstChild());
    60         }
    61 
    62         private static Element advanceToElement(Node firstChild) {
    63             Node node = firstChild;
    64             while (node != null && !(node instanceof Element)) {
    65                 node = node.getNextSibling();
    66             }
    67             return (Element) node;
    68         }
    69 
    70         @Override
    71         public boolean hasNext() {
    72             return child != null;
    73         }
    74 
    75         @Override
    76         public Element next() {
    77             if (!hasNext()) {
    78                 throw new NoSuchElementException("No next sibling.");
    79             }
    80             Element next = child;
    81             child = advanceToElement(child.getNextSibling());
    82             return next;
    83         }
    84     }
     46
     47    private static final String CAPABILITIES_QUERY_STRING = "SERVICE=WMS&REQUEST=GetCapabilities";
     48
     49    /**
     50     * WMS namespace address
     51     */
     52    public static final String WMS_NS_URL = "http://www.opengis.net/wms";
     53
     54    // CHECKSTYLE.OFF: SingleSpaceSeparator
     55    // WMS 1.0 - 1.3.0
     56    private static final QName CAPABILITITES_ROOT_130 = new QName("WMS_Capabilities", WMS_NS_URL);
     57    private static final QName QN_ABSTRACT            = new QName(WMS_NS_URL, "Abstract");
     58    private static final QName QN_CAPABILITY          = new QName(WMS_NS_URL, "Capability");
     59    private static final QName QN_CRS                 = new QName(WMS_NS_URL, "CRS");
     60    private static final QName QN_DCPTYPE             = new QName(WMS_NS_URL, "DCPType");
     61    private static final QName QN_FORMAT              = new QName(WMS_NS_URL, "Format");
     62    private static final QName QN_GET                 = new QName(WMS_NS_URL, "Get");
     63    private static final QName QN_GETMAP              = new QName(WMS_NS_URL, "GetMap");
     64    private static final QName QN_HTTP                = new QName(WMS_NS_URL, "HTTP");
     65    private static final QName QN_LAYER               = new QName(WMS_NS_URL, "Layer");
     66    private static final QName QN_NAME                = new QName(WMS_NS_URL, "Name");
     67    private static final QName QN_REQUEST             = new QName(WMS_NS_URL, "Request");
     68    private static final QName QN_SERVICE             = new QName(WMS_NS_URL, "Service");
     69    private static final QName QN_STYLE               = new QName(WMS_NS_URL, "Style");
     70    private static final QName QN_TITLE               = new QName(WMS_NS_URL, "Title");
     71    private static final QName QN_BOUNDINGBOX         = new QName(WMS_NS_URL, "BoundingBox");
     72    private static final QName QN_EX_GEOGRAPHIC_BBOX  = new QName(WMS_NS_URL, "EX_GeographicBoundingBox");
     73    private static final QName QN_WESTBOUNDLONGITUDE  = new QName(WMS_NS_URL, "westBoundLongitude");
     74    private static final QName QN_EASTBOUNDLONGITUDE  = new QName(WMS_NS_URL, "eastBoundLongitude");
     75    private static final QName QN_SOUTHBOUNDLATITUDE  = new QName(WMS_NS_URL, "southBoundLatitude");
     76    private static final QName QN_NORTHBOUNDLATITUDE  = new QName(WMS_NS_URL, "northBoundLatitude");
     77    private static final QName QN_ONLINE_RESOURCE     = new QName(WMS_NS_URL, "OnlineResource");
     78
     79    // WMS 1.1 - 1.1.1
     80    private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities");
     81    private static final QName QN_SRS                = new QName("SRS");
     82    private static final QName QN_LATLONBOUNDINGBOX  = new QName("LatLonBoundingBox");
     83
     84    // CHECKSTYLE.ON: SingleSpaceSeparator
    8585
    8686    /**
     
    120120    }
    121121
    122     private List<LayerDetails> layers;
    123     private URL serviceUrl;
    124     private List<String> formats;
    125     private String version = "1.1.1";
    126 
    127     /**
    128      * Returns the list of layers.
    129      * @return the list of layers
     122    private Map<String, String> headers = new ConcurrentHashMap<>();
     123    private String version = "1.1.1"; // default version
     124    private String getMapUrl;
     125    private URL capabilitiesUrl;
     126    private List<String> formats = new ArrayList<>();
     127    private List<LayerDetails> layers = new ArrayList<>();
     128
     129    private String title;
     130
     131    /**
     132     * Make getCapabilities request towards given URL
     133     * @param url service url
     134     * @throws IOException
     135     * @throws WMSGetCapabilitiesException
     136     */
     137    public WMSImagery(String url) throws IOException, WMSGetCapabilitiesException {
     138        this(url, null);
     139    }
     140
     141    /**
     142     * Make getCapabilities request towards given URL using headers
     143     * @param url service url
     144     * @param headers HTTP headers to be sent with request
     145     * @throws IOException
     146     * @throws WMSGetCapabilitiesException
     147     */
     148    public WMSImagery(String url, Map<String, String> headers) throws IOException, WMSGetCapabilitiesException {
     149        if (headers != null) {
     150            this.headers.putAll(headers);
     151        }
     152
     153        IOException savedExc = null;
     154        String workingAddress = null;
     155        url_search:
     156        for (String z: new String[]{
     157                normalizeUrl(url),
     158                url,
     159                url + CAPABILITIES_QUERY_STRING,
     160        }) {
     161            for (String ver: new String[]{"", "&VERSION=1.3.0", "&VERSION=1.1.1"}) {
     162                try {
     163                    attemptGetCapabilities(z + ver);
     164                    workingAddress = z;
     165                    calculateChildren();
     166                    // clear saved exception - we've got something working
     167                    savedExc = null;
     168                    break url_search;
     169                } catch (IOException e) {
     170                    savedExc = e;
     171                    Logging.warn(e);
     172                }
     173            }
     174        }
     175
     176        if (workingAddress != null) {
     177            try {
     178                capabilitiesUrl = new URL(workingAddress);
     179            } catch (MalformedURLException e) {
     180                if (savedExc != null) {
     181                    savedExc = e;
     182                }
     183                try {
     184                    capabilitiesUrl = new File(workingAddress).toURI().toURL();
     185                } catch (MalformedURLException e1) {
     186                    // do nothing, raise original exception
     187                }
     188            }
     189        }
     190
     191        if (savedExc != null) {
     192            throw savedExc;
     193        }
     194    }
     195
     196    private void calculateChildren() {
     197        Map<LayerDetails, List<LayerDetails>> layerChildren = layers.stream()
     198                .filter(x -> x.getParent() != null) // exclude top-level elements
     199                .collect(Collectors.groupingBy(LayerDetails::getParent));
     200        for (LayerDetails ld: layers) {
     201            if (layerChildren.containsKey(ld)) {
     202                ld.setChildren(layerChildren.get(ld));
     203            }
     204        }
     205        // leave only top-most elements in the list
     206        layers = layers.stream().filter(x -> x.getParent() == null).collect(Collectors.toCollection(ArrayList::new));
     207    }
     208
     209    /**
     210     * Returns the list of top-level layers.
     211     * @return the list of top-level layers
    130212     */
    131213    public List<LayerDetails> getLayers() {
     
    134216
    135217    /**
    136      * Returns the service URL.
    137      * @return the service URL
    138      */
    139     public URL getServiceUrl() {
    140         return serviceUrl;
    141     }
    142 
    143     /**
    144      * Returns the WMS version used.
    145      * @return the WMS version used (1.1.1 or 1.3.0)
    146      * @since 13358
    147      */
    148     public String getVersion() {
    149         return version;
    150     }
    151 
    152     /**
    153218     * Returns the list of supported formats.
    154219     * @return the list of supported formats
    155220     */
    156     public List<String> getFormats() {
     221    public Collection<String> getFormats() {
    157222        return Collections.unmodifiableList(formats);
    158223    }
    159224
    160225    /**
    161      * Gets the preffered format for this imagery layer.
    162      * @return The preffered format as mime type.
    163      */
    164     public String getPreferredFormats() {
    165         if (formats.contains("image/jpeg")) {
     226     * Gets the preferred format for this imagery layer.
     227     * @return The preferred format as mime type.
     228     */
     229    public String getPreferredFormat() {
     230        if (formats.contains("image/png")) {
     231            return "image/png";
     232        } else if (formats.contains("image/jpeg")) {
    166233            return "image/jpeg";
    167         } else if (formats.contains("image/png")) {
    168             return "image/png";
    169234        } else if (formats.isEmpty()) {
    170235            return null;
     
    174239    }
    175240
    176     String buildRootUrl() {
    177         if (serviceUrl == null) {
     241    /**
     242     * @return root URL of services in this GetCapabilities
     243     */
     244    public String buildRootUrl() {
     245        if (getMapUrl == null && capabilitiesUrl == null) {
    178246            return null;
    179247        }
     248        if (getMapUrl != null) {
     249            return getMapUrl;
     250        }
     251
     252        URL serviceUrl = capabilitiesUrl;
    180253        StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
    181254        a.append("://").append(serviceUrl.getHost());
     
    194267
    195268    /**
    196      * Returns the URL for the "GetMap" WMS request in JPEG format.
    197      * @param selectedLayers the list of selected layers, matching the "LAYERS" WMS request argument
    198      * @return the URL for the "GetMap" WMS request
    199      */
    200     public String buildGetMapUrl(Collection<LayerDetails> selectedLayers) {
    201         return buildGetMapUrl(selectedLayers, "image/jpeg");
    202     }
    203 
    204     /**
    205      * Returns the URL for the "GetMap" WMS request.
    206      * @param selectedLayers the list of selected layers, matching the "LAYERS" WMS request argument
    207      * @param format the requested image format, matching the "FORMAT" WMS request argument
    208      * @return the URL for the "GetMap" WMS request
    209      */
    210     public String buildGetMapUrl(Collection<LayerDetails> selectedLayers, String format) {
    211         return buildRootUrl() + "FORMAT=" + format + (imageFormatHasTransparency(format) ? "&TRANSPARENT=TRUE" : "")
    212                 + "&VERSION=" + version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS="
    213                 + selectedLayers.stream().map(x -> x.ident).collect(Collectors.joining(","))
    214                 + "&STYLES=&" + ("1.3.0".equals(version) ? "CRS" : "SRS") + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
    215     }
    216 
    217     /**
    218      * Attempts WMS "GetCapabilities" request and initializes internal variables if successful.
    219      * @param serviceUrlStr WMS service URL
    220      * @throws IOException if any I/O errors occurs
    221      * @throws WMSGetCapabilitiesException if the WMS server replies a ServiceException
    222      */
    223     public void attemptGetCapabilities(String serviceUrlStr) throws IOException, WMSGetCapabilitiesException {
     269     * Returns URL for accessing GetMap service. String will contain following parameters:
     270     * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})
     271     * * {width} - that needs to be replaced with width of the tile
     272     * * {height} - that needs to be replaces with height of the tile
     273     * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)
     274     *
     275     * Format of the response will be calculated using {@link #getPreferredFormat()}
     276     *
     277     * @param selectedLayers list of DefaultLayer selection of layers to be shown
     278     * @param transparent whether returned images should contain transparent pixels (if supported by format)
     279     * @return URL template for GetMap service containing
     280     */
     281    public String buildGetMapUrl(List<DefaultLayer> selectedLayers, boolean transparent) {
     282        return buildGetMapUrl(
     283                getLayers(selectedLayers),
     284                selectedLayers.stream().map(x -> x.getStyle()).collect(Collectors.toList()),
     285                transparent);
     286    }
     287
     288    /**
     289     * @see #buildGetMapUrl(List, boolean)
     290     *
     291     * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()}
     292     * @param selectedStyles selected styles for all selectedLayers
     293     * @param transparent whether returned images should contain transparent pixels (if supported by format)
     294     * @return URL template for GetMap service
     295     */
     296    public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
     297        return buildGetMapUrl(
     298                selectedLayers.stream().map(x -> x.getName()).collect(Collectors.toList()),
     299                selectedStyles,
     300                getPreferredFormat(),
     301                transparent);
     302    }
     303
     304    /**
     305     * @see #buildGetMapUrl(List, boolean)
     306     *
     307     * @param selectedLayers selected layers as list of strings
     308     * @param selectedStyles selected styles of layers as list of strings
     309     * @param format format of the response - one of {@link #getFormats()}
     310     * @param transparent whether returned images should contain transparent pixels (if supported by format)
     311     * @return URL template for GetMap service
     312     */
     313    public String buildGetMapUrl(List<String> selectedLayers,
     314            Collection<String> selectedStyles,
     315            String format,
     316            boolean transparent) {
     317
     318        Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(),
     319                tr("Styles size {0} doesn't match layers size {1}"),
     320                selectedStyles == null ? 0 : selectedStyles.size(),
     321                        selectedLayers.size());
     322
     323        return buildRootUrl() + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "")
     324                + "&VERSION=" + this.version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS="
     325                + selectedLayers.stream().collect(Collectors.joining(","))
     326                + "&STYLES="
     327                + (selectedStyles != null ? Utils.join(",", selectedStyles) : "")
     328                + "&"
     329                + (belowWMS130() ? "SRS" : "CRS")
     330                + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
     331    }
     332
     333    private boolean tagEquals(QName a, QName b) {
     334        boolean ret = a.equals(b);
     335        if (ret) {
     336            return ret;
     337        }
     338
     339        if (belowWMS130()) {
     340            return a.getLocalPart().equals(b.getLocalPart());
     341        }
     342
     343        return false;
     344    }
     345
     346    private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException {
     347        Logging.debug("Trying WMS getcapabilities with url {0}", url);
     348        try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
     349                setMaxAge(7 * CachedFile.DAYS).
     350                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
     351                getInputStream()) {
     352
     353            try {
     354                XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in);
     355                for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
     356                    if (event == XMLStreamReader.START_ELEMENT) {
     357                        if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) {
     358                            // version 1.1.1
     359                            this.version = reader.getAttributeValue(null, "version");
     360                            if (this.version == null) {
     361                                this.version = "1.1.1";
     362                            }
     363                        }
     364                        if (tagEquals(CAPABILITITES_ROOT_130, reader.getName())) {
     365                            this.version = reader.getAttributeValue(WMS_NS_URL, "version");
     366                        }
     367                        if (tagEquals(QN_SERVICE, reader.getName())) {
     368                            parseService(reader);
     369                        }
     370
     371                        if (tagEquals(QN_CAPABILITY, reader.getName())) {
     372                            parseCapability(reader);
     373                        }
     374                    }
     375                }
     376            } catch (XMLStreamException e) {
     377                String content = new String(cf.getByteContent(), UTF_8);
     378                cf.clear(); // if there is a problem with parsing of the file, remove it from the cache
     379                throw new WMSGetCapabilitiesException(e, content);
     380            }
     381        }
     382    }
     383
     384    private void parseService(XMLStreamReader reader) throws XMLStreamException {
     385        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) {
     386            this.title = reader.getElementText();
     387            for (int event = reader.getEventType();
     388                    reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName()));
     389                    event = reader.next()) {
     390                // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done
     391            }
     392        }
     393    }
     394
     395    private void parseCapability(XMLStreamReader reader) throws XMLStreamException {
     396        for (int event = reader.getEventType();
     397                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName()));
     398                event = reader.next()) {
     399
     400            if (event == XMLStreamReader.START_ELEMENT) {
     401                if (tagEquals(QN_REQUEST, reader.getName())) {
     402                    parseRequest(reader);
     403                }
     404                if (tagEquals(QN_LAYER, reader.getName())) {
     405                    parseLayer(reader, null);
     406                }
     407            }
     408        }
     409    }
     410
     411    private void parseRequest(XMLStreamReader reader) throws XMLStreamException {
     412        String mode = "";
     413        String getMapUrl = "";
     414        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) {
     415            for (int event = reader.getEventType();
     416                    reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName()));
     417                    event = reader.next()) {
     418
     419                if (event == XMLStreamReader.START_ELEMENT) {
     420                    if (tagEquals(QN_FORMAT, reader.getName())) {
     421                        String value = reader.getElementText();
     422                        if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) {
     423                            this.formats.add(value);
     424                        }
     425                    }
     426                    if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader,
     427                            this::tagEquals, QN_HTTP, QN_GET)) {
     428                        mode = reader.getName().getLocalPart();
     429                        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) {
     430                            getMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
     431                        }
     432                        // TODO should we handle also POST?
     433                        if ("GET".equalsIgnoreCase(mode) && getMapUrl != null && !"".equals(getMapUrl)) {
     434                            this.getMapUrl = getMapUrl;
     435                        }
     436                    }
     437                }
     438            }
     439        }
     440    }
     441
     442    private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException {
     443        LayerDetails ret = new LayerDetails(parentLayer);
     444        for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer
     445                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName()));
     446                event = reader.next()) {
     447
     448            if (event == XMLStreamReader.START_ELEMENT) {
     449                if (tagEquals(QN_NAME, reader.getName())) {
     450                    ret.setName(reader.getElementText());
     451                }
     452                if (tagEquals(QN_ABSTRACT, reader.getName())) {
     453                    ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader));
     454                }
     455                if (tagEquals(QN_TITLE, reader.getName())) {
     456                    ret.setTitle(reader.getElementText());
     457                }
     458                if (tagEquals(QN_CRS, reader.getName())) {
     459                    ret.addCrs(reader.getElementText());
     460                }
     461                if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) {
     462                    ret.addCrs(reader.getElementText());
     463                }
     464                if (tagEquals(QN_STYLE, reader.getName())) {
     465                    parseAndAddStyle(reader, ret);
     466                }
     467                if (tagEquals(QN_LAYER, reader.getName())) {
     468
     469                    parseLayer(reader, ret);
     470                }
     471                if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName())) {
     472                    if (ret.getBounds() == null) {
     473                        Bounds bbox = parseExGeographic(reader);
     474                        ret.setBounds(bbox);
     475                    }
     476
     477                }
     478                if (tagEquals(QN_BOUNDINGBOX, reader.getName())) {
     479                    Projection conv;
     480                    if (belowWMS130()) {
     481                        conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS"));
     482                    } else {
     483                        conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS"));
     484                    }
     485                    if (ret.getBounds() == null && conv != null) {
     486                        Bounds bbox = parseBoundingBox(reader, conv);
     487                        ret.setBounds(bbox);
     488                    }
     489                }
     490                if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130()) {
     491                    if (ret.getBounds() == null) {
     492                        Bounds bbox = parseBoundingBox(reader, null);
     493                        ret.setBounds(bbox);
     494                    }
     495                }
     496            }
     497        }
     498        this.layers.add(ret);
     499    }
     500
     501    /**
     502     * @return if this service operates at protocol level below 1.3.0
     503     */
     504    public boolean belowWMS130() {
     505        return this.version.equals("1.1.1") || this.version.equals("1.1") || this.version.equals("1.0");
     506    }
     507
     508    private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException {
     509        String name = null;
     510        String title = null;
     511        for (int event = reader.getEventType();
     512                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName()));
     513                event = reader.next()) {
     514            if (event == XMLStreamReader.START_ELEMENT) {
     515                if (tagEquals(QN_NAME, reader.getName())) {
     516                    name = reader.getElementText();
     517                }
     518                if (tagEquals(QN_TITLE, reader.getName())) {
     519                    title = reader.getElementText();
     520                }
     521            }
     522        }
     523        if (name == null) {
     524            name = "";
     525        }
     526        ld.addStyle(name, title);
     527    }
     528
     529    private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException {
     530        String minx = null, maxx = null, maxy = null, miny = null;
     531
     532        for (int event = reader.getEventType();
     533                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()));
     534                event = reader.next()) {
     535            if (event == XMLStreamReader.START_ELEMENT) {
     536                if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) {
     537                    minx = reader.getElementText();
     538                }
     539
     540                if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) {
     541                    maxx = reader.getElementText();
     542                }
     543
     544                if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) {
     545                    miny = reader.getElementText();
     546                }
     547
     548                if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) {
     549                    maxy = reader.getElementText();
     550                }
     551            }
     552        }
     553        return parseBBox(null, miny, minx, maxy, maxx);
     554    }
     555
     556    private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) {
     557        Function<String, String> attrGetter = tag -> belowWMS130() ?
     558                reader.getAttributeValue(null, tag)
     559                : reader.getAttributeValue(WMS_NS_URL, tag);
     560
     561                return parseBBox(
     562                        conv,
     563                        attrGetter.apply("miny"),
     564                        attrGetter.apply("minx"),
     565                        attrGetter.apply("maxy"),
     566                        attrGetter.apply("maxx")
     567                        );
     568    }
     569
     570    private Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) {
     571        if (miny == null || minx == null || maxy == null || maxx == null) {
     572            return null;
     573        }
     574        if (conv != null) {
     575            new Bounds(
     576                    conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))),
     577                    conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy)))
     578                    );
     579        }
     580        return new Bounds(
     581                getDecimalDegree(miny),
     582                getDecimalDegree(minx),
     583                getDecimalDegree(maxy),
     584                getDecimalDegree(maxx)
     585                );
     586    }
     587
     588    private static double getDecimalDegree(String value) {
     589        // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
     590        return Double.parseDouble(value.replace(',', '.'));
     591    }
     592
     593
     594    private String normalizeUrl(String serviceUrlStr) throws MalformedURLException {
    224595        URL getCapabilitiesUrl = null;
    225         try {
    226             if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
    227                 // If the url doesn't already have GetCapabilities, add it in
    228                 getCapabilitiesUrl = new URL(serviceUrlStr);
    229                 final String getCapabilitiesQuery = "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities";
    230                 if (getCapabilitiesUrl.getQuery() == null) {
    231                     getCapabilitiesUrl = new URL(serviceUrlStr + '?' + getCapabilitiesQuery);
    232                 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
    233                     getCapabilitiesUrl = new URL(serviceUrlStr + '&' + getCapabilitiesQuery);
    234                 } else {
    235                     getCapabilitiesUrl = new URL(serviceUrlStr + getCapabilitiesQuery);
    236                 }
     596        String ret = null;
     597
     598        if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
     599            // If the url doesn't already have GetCapabilities, add it in
     600            getCapabilitiesUrl = new URL(serviceUrlStr);
     601            ret = serviceUrlStr;
     602            if (getCapabilitiesUrl.getQuery() == null) {
     603                ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING;
     604            } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
     605                ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING;
    237606            } else {
    238                 // Otherwise assume it's a good URL and let the subsequent error
    239                 // handling systems deal with problems
    240                 getCapabilitiesUrl = new URL(serviceUrlStr);
    241             }
    242             // Make sure we don't keep GetCapabilities request in service URL
    243             serviceUrl = new URL(serviceUrlStr.replace("REQUEST=GetCapabilities", "").replace("&&", "&"));
    244         } catch (HeadlessException e) {
    245             Logging.warn(e);
    246             return;
    247         }
    248 
    249         doAttemptGetCapabilities(serviceUrlStr, getCapabilitiesUrl);
    250     }
    251 
    252     /**
    253      * Attempts WMS GetCapabilities with version 1.1.1 first, then 1.3.0 in case of specific errors.
    254      * @param serviceUrlStr WMS service URL
    255      * @param getCapabilitiesUrl GetCapabilities URL
    256      * @throws IOException if any I/O error occurs
    257      * @throws WMSGetCapabilitiesException if any HTTP or parsing error occurs
    258      */
    259     private void doAttemptGetCapabilities(String serviceUrlStr, URL getCapabilitiesUrl)
    260             throws IOException, WMSGetCapabilitiesException {
    261         final String url = getCapabilitiesUrl.toExternalForm();
    262         final Response response = HttpClient.create(getCapabilitiesUrl).connect();
    263 
    264         // Is the HTTP connection successul ?
    265         if (response.getResponseCode() >= 400) {
    266             // HTTP error for servers handling only WMS 1.3.0 ?
    267             String errorMessage = response.getResponseMessage();
    268             String errorContent = response.fetchContent();
    269             Matcher tomcat = HttpClient.getTomcatErrorMatcher(errorContent);
    270             boolean messageAbout130 = errorMessage != null && errorMessage.contains("1.3.0");
    271             boolean contentAbout130 = errorContent != null && tomcat != null && tomcat.matches() && tomcat.group(1).contains("1.3.0");
    272             if (url.contains("VERSION=1.1.1") && (messageAbout130 || contentAbout130)) {
    273                 doAttemptGetCapabilities130(serviceUrlStr, url);
    274                 return;
    275             }
    276             throw new WMSGetCapabilitiesException(errorMessage, errorContent);
    277         }
    278 
    279         try {
    280             // Parse XML capabilities sent by the server
    281             parseCapabilities(serviceUrlStr, response.getContent());
    282         } catch (WMSGetCapabilitiesException e) {
    283             // ServiceException for servers handling only WMS 1.3.0 ?
    284             if (e.getCause() == null && url.contains("VERSION=1.1.1")) {
    285                 doAttemptGetCapabilities130(serviceUrlStr, url);
    286             } else {
    287                 throw e;
    288             }
    289         }
    290     }
    291 
    292     /**
    293      * Attempts WMS GetCapabilities with version 1.3.0.
    294      * @param serviceUrlStr WMS service URL
    295      * @param url GetCapabilities URL
    296      * @throws IOException if any I/O error occurs
    297      * @throws WMSGetCapabilitiesException if any HTTP or parsing error occurs
    298      * @throws MalformedURLException in case of invalid URL
    299      */
    300     private void doAttemptGetCapabilities130(String serviceUrlStr, final String url)
    301             throws IOException, WMSGetCapabilitiesException {
    302         doAttemptGetCapabilities(serviceUrlStr, new URL(url.replace("VERSION=1.1.1", "VERSION=1.3.0")));
    303         if (serviceUrl.toExternalForm().contains("VERSION=1.1.1")) {
    304             serviceUrl = new URL(serviceUrl.toExternalForm().replace("VERSION=1.1.1", "VERSION=1.3.0"));
    305         }
    306         version = "1.3.0";
    307     }
    308 
    309     void parseCapabilities(String serviceUrlStr, InputStream contentStream) throws IOException, WMSGetCapabilitiesException {
    310         String incomingData = null;
    311         try {
    312             DocumentBuilder builder = Utils.newSafeDOMBuilder();
    313             builder.setEntityResolver((publicId, systemId) -> {
    314                 Logging.info("Ignoring DTD " + publicId + ", " + systemId);
    315                 return new InputSource(new StringReader(""));
    316             });
    317             Document document = builder.parse(contentStream);
    318             Element root = document.getDocumentElement();
    319 
    320             try {
    321                 StringWriter writer = new StringWriter();
    322                 TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(writer));
    323                 incomingData = writer.getBuffer().toString();
    324                 Logging.debug("Server response to Capabilities request:");
    325                 Logging.debug(incomingData);
    326             } catch (TransformerFactoryConfigurationError | TransformerException e) {
    327                 Logging.warn(e);
    328             }
    329 
    330             // Check if the request resulted in ServiceException
    331             if ("ServiceException".equals(root.getTagName())) {
    332                 throw new WMSGetCapabilitiesException(root.getTextContent(), incomingData);
    333             }
    334 
    335             // Some WMS service URLs specify a different base URL for their GetMap service
    336             Element child = getChild(root, "Capability");
    337             child = getChild(child, "Request");
    338             child = getChild(child, "GetMap");
    339 
    340             formats = getChildrenStream(child, "Format")
    341                     .map(Node::getTextContent)
    342                     .filter(WMSImagery::isImageFormatSupportedWarn)
    343                     .collect(Collectors.toList());
    344 
    345             child = getChild(child, "DCPType");
    346             child = getChild(child, "HTTP");
    347             child = getChild(child, "Get");
    348             child = getChild(child, "OnlineResource");
    349             if (child != null) {
    350                 String baseURL = child.getAttributeNS(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
    351                 if (!baseURL.equals(serviceUrlStr)) {
    352                     URL newURL = new URL(baseURL);
    353                     if (newURL.getAuthority() != null) {
    354                         Logging.info("GetCapabilities specifies a different service URL: " + baseURL);
    355                         serviceUrl = newURL;
    356                     }
    357                 }
    358             }
    359 
    360             Element capabilityElem = getChild(root, "Capability");
    361             List<Element> children = getChildren(capabilityElem, "Layer");
    362             layers = parseLayers(children, new HashSet<String>());
    363         } catch (MalformedURLException | ParserConfigurationException | SAXException e) {
    364             throw new WMSGetCapabilitiesException(e, incomingData);
    365         }
     607                ret = serviceUrlStr + CAPABILITIES_QUERY_STRING;
     608            }
     609        } else {
     610            // Otherwise assume it's a good URL and let the subsequent error
     611            // handling systems deal with problems
     612            ret = serviceUrlStr;
     613        }
     614        return ret;
    366615    }
    367616
     
    392641    }
    393642
     643
    394644    static boolean imageFormatHasTransparency(final String format) {
    395645        return format != null && (format.startsWith("image/png") || format.startsWith("image/gif")
     
    398648
    399649    /**
    400      * Returns a new {@code ImageryInfo} describing the given service name and selected WMS layers.
    401      * @param name service name
    402      * @param selectedLayers selected WMS layers
    403      * @return a new {@code ImageryInfo} describing the given service name and selected WMS layers
    404      */
    405     public ImageryInfo toImageryInfo(String name, Collection<LayerDetails> selectedLayers) {
    406         ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers));
    407         if (selectedLayers != null) {
    408             Set<String> proj = new HashSet<>();
    409             for (WMSImagery.LayerDetails l : selectedLayers) {
    410                 proj.addAll(l.getProjections());
    411             }
    412             i.setServerProjections(proj);
     650     * Creates ImageryInfo object from this GetCapabilities document
     651     *
     652     * @param name name of imagery layer
     653     * @param selectedLayers layers which are to be used by this imagery layer
     654     * @param selectedStyles styles that should be used for selectedLayers
     655     * @param transparent if layer should be transparent
     656     * @return ImageryInfo object
     657     */
     658    public ImageryInfo toImageryInfo(String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
     659        ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, transparent));
     660        if (selectedLayers != null && !selectedLayers.isEmpty()) {
     661            i.setServerProjections(getServerProjections(selectedLayers));
    413662        }
    414663        return i;
    415664    }
    416665
    417     private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) {
    418         List<LayerDetails> details = new ArrayList<>(children.size());
    419         for (Element element : children) {
    420             details.add(parseLayer(element, parentCrs));
    421         }
    422         return details;
    423     }
    424 
    425     private LayerDetails parseLayer(Element element, Set<String> parentCrs) {
    426         String name = getChildContent(element, "Title", null, null);
    427         String ident = getChildContent(element, "Name", null, null);
    428         String abstr = getChildContent(element, "Abstract", null, null);
    429 
    430         // The set of supported CRS/SRS for this layer
    431         Set<String> crsList = new HashSet<>();
    432         // ...including this layer's already-parsed parent projections
    433         crsList.addAll(parentCrs);
    434 
    435         // Parse the CRS/SRS pulled out of this layer's XML element
    436         // I think CRS and SRS are the same at this point
    437         getChildrenStream(element)
    438             .filter(child -> "CRS".equals(child.getNodeName()) || "SRS".equals(child.getNodeName()))
    439             .map(WMSImagery::getContent)
    440             .filter(crs -> !crs.isEmpty())
    441             .map(crs -> crs.trim().toUpperCase(Locale.ENGLISH))
    442             .forEach(crsList::add);
    443 
    444         // Check to see if any of the specified projections are supported by JOSM
    445         boolean josmSupportsThisLayer = false;
    446         for (String crs : crsList) {
    447             josmSupportsThisLayer |= isProjSupported(crs);
    448         }
    449 
    450         Bounds bounds = null;
    451         Element bboxElem = getChild(element, "EX_GeographicBoundingBox");
    452         if (bboxElem != null) {
    453             // Attempt to use EX_GeographicBoundingBox for bounding box
    454             double left = Double.parseDouble(getChildContent(bboxElem, "westBoundLongitude", null, null));
    455             double top = Double.parseDouble(getChildContent(bboxElem, "northBoundLatitude", null, null));
    456             double right = Double.parseDouble(getChildContent(bboxElem, "eastBoundLongitude", null, null));
    457             double bot = Double.parseDouble(getChildContent(bboxElem, "southBoundLatitude", null, null));
    458             bounds = new Bounds(bot, left, top, right);
    459         } else {
    460             // If that's not available, try LatLonBoundingBox
    461             bboxElem = getChild(element, "LatLonBoundingBox");
    462             if (bboxElem != null) {
    463                 double left = getDecimalDegree(bboxElem, "minx");
    464                 double top = getDecimalDegree(bboxElem, "maxy");
    465                 double right = getDecimalDegree(bboxElem, "maxx");
    466                 double bot = getDecimalDegree(bboxElem, "miny");
    467                 bounds = new Bounds(bot, left, top, right);
    468             }
    469         }
    470 
    471         List<Element> layerChildren = getChildren(element, "Layer");
    472         List<LayerDetails> childLayers = parseLayers(layerChildren, crsList);
    473 
    474         return new LayerDetails(name, ident, abstr, crsList, josmSupportsThisLayer, bounds, childLayers);
    475     }
    476 
    477     private static double getDecimalDegree(Element elem, String attr) {
    478         // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
    479         return Double.parseDouble(elem.getAttribute(attr).replace(',', '.'));
    480     }
    481 
    482     private static boolean isProjSupported(String crs) {
    483         return Projections.getProjectionByCode(crs) != null;
    484     }
    485 
    486     private static String getChildContent(Element parent, String name, String missing, String empty) {
    487         Element child = getChild(parent, name);
    488         if (child == null)
    489             return missing;
    490         else {
    491             String content = getContent(child);
    492             return (!content.isEmpty()) ? content : empty;
    493         }
    494     }
    495 
    496     private static String getContent(Element element) {
    497         NodeList nl = element.getChildNodes();
    498         StringBuilder content = new StringBuilder();
    499         for (int i = 0; i < nl.getLength(); i++) {
    500             Node node = nl.item(i);
    501             switch (node.getNodeType()) {
    502                 case Node.ELEMENT_NODE:
    503                     content.append(getContent((Element) node));
    504                     break;
    505                 case Node.CDATA_SECTION_NODE:
    506                 case Node.TEXT_NODE:
    507                     content.append(node.getNodeValue());
    508                     break;
    509                 default: // Do nothing
    510             }
    511         }
    512         return content.toString().trim();
    513     }
    514 
    515     private static Stream<Element> getChildrenStream(Element parent) {
    516         if (parent == null) {
    517             // ignore missing elements
    518             return Stream.empty();
    519         } else {
    520             Iterable<Element> it = () -> new ChildIterator(parent);
    521             return StreamSupport.stream(it.spliterator(), false);
    522         }
    523     }
    524 
    525     private static Stream<Element> getChildrenStream(Element parent, String name) {
    526         return getChildrenStream(parent).filter(child -> name.equals(child.getNodeName()));
    527     }
    528 
    529     private static List<Element> getChildren(Element parent, String name) {
    530         return getChildrenStream(parent, name).collect(Collectors.toList());
    531     }
    532 
    533     private static Element getChild(Element parent, String name) {
    534         return getChildrenStream(parent, name).findFirst().orElse(null);
    535     }
    536 
    537     /**
    538      * The details of a layer of this WMS server.
    539      */
    540     public static class LayerDetails {
    541 
    542         /**
    543          * The layer name (WMS {@code Title})
    544          */
    545         public final String name;
    546         /**
    547          * The layer ident (WMS {@code Name})
    548          */
    549         public final String ident;
    550         /**
    551          * The layer abstract (WMS {@code Abstract})
    552          * @since 13199
    553          */
    554         public final String abstr;
    555         /**
    556          * The child layers of this layer
    557          */
    558         public final List<LayerDetails> children;
    559         /**
    560          * The bounds this layer can be used for
    561          */
    562         public final Bounds bounds;
    563         /**
    564          * the CRS/SRS pulled out of this layer's XML element
    565          */
    566         public final Set<String> crsList;
    567         /**
    568          * {@code true} if any of the specified projections are supported by JOSM
    569          */
    570         public final boolean supported;
    571 
    572         /**
    573          * Constructs a new {@code LayerDetails}.
    574          * @param name The layer name (WMS {@code Title})
    575          * @param ident The layer ident (WMS {@code Name})
    576          * @param abstr The layer abstract (WMS {@code Abstract})
    577          * @param crsList The CRS/SRS pulled out of this layer's XML element
    578          * @param supportedLayer {@code true} if any of the specified projections are supported by JOSM
    579          * @param bounds The bounds this layer can be used for
    580          * @param childLayers The child layers of this layer
    581          * @since 13199
    582          */
    583         public LayerDetails(String name, String ident, String abstr, Set<String> crsList, boolean supportedLayer, Bounds bounds,
    584                 List<LayerDetails> childLayers) {
    585             this.name = name;
    586             this.ident = ident;
    587             this.abstr = abstr;
    588             this.supported = supportedLayer;
    589             this.children = childLayers;
    590             this.bounds = bounds;
    591             this.crsList = crsList;
    592         }
    593 
    594         /**
    595          * Determines if any of the specified projections are supported by JOSM.
    596          * @return {@code true} if any of the specified projections are supported by JOSM
    597          */
    598         public boolean isSupported() {
    599             return this.supported;
    600         }
    601 
    602         /**
    603          * Returns the CRS/SRS pulled out of this layer's XML element.
    604          * @return the CRS/SRS pulled out of this layer's XML element
    605          */
    606         public Set<String> getProjections() {
    607             return crsList;
    608         }
    609 
    610         @Override
    611         public String toString() {
    612             String baseName = (name == null || name.isEmpty()) ? ident : name;
    613             return abstr == null || abstr.equalsIgnoreCase(baseName) ? baseName : baseName + " (" + abstr + ')';
    614         }
     666    /**
     667     * Returns projections that server supports for provided list of layers. This will be intersection of projections
     668     * defined for each layer
     669     *
     670     * @param selectedLayers list of layers
     671     * @return projection code
     672     */
     673    public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) {
     674        if (selectedLayers.isEmpty()) {
     675            return Collections.emptyList();
     676        }
     677        Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs());
     678
     679        // set intersect with all layers
     680        for (LayerDetails ld: selectedLayers) {
     681            proj.retainAll(ld.getCrs());
     682        }
     683        return proj;
     684    }
     685
     686
     687    /**
     688     * @param defaultLayers
     689     * @return collection of LayerDetails specified by DefaultLayers
     690     */
     691    public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) {
     692        Collection<String> layerNames = defaultLayers.stream().map(x -> x.getLayerName()).collect(Collectors.toList());
     693        return layers.stream()
     694                .flatMap(LayerDetails::flattened)
     695                .filter(x -> layerNames.contains(x.getName()))
     696                .collect(Collectors.toList());
     697    }
     698
     699    /**
     700     * @return title of this service
     701     */
     702    public String getTitle() {
     703        return title;
    615704    }
    616705}
Note: See TracChangeset for help on using the changeset viewer.