Changeset 13733 in josm for trunk/src


Ignore:
Timestamp:
2018-05-12T14:18:57+02:00 (17 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
Files:
5 added
1 deleted
20 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java

    r13388 r13733  
    1313import java.util.ArrayList;
    1414import java.util.Collection;
    15 import java.util.HashSet;
    1615import java.util.List;
    17 import java.util.Set;
     16import java.util.stream.Collectors;
    1817
    1918import javax.swing.JComboBox;
     
    2726import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    2827import org.openstreetmap.josm.data.imagery.WMTSTileSource;
     28import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
    2929import org.openstreetmap.josm.gui.ExtendedDialog;
    3030import org.openstreetmap.josm.gui.layer.AlignImageryPanel;
     
    3434import org.openstreetmap.josm.gui.util.GuiHelper;
    3535import org.openstreetmap.josm.io.imagery.WMSImagery;
    36 import org.openstreetmap.josm.io.imagery.WMSImagery.LayerDetails;
    3736import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
    3837import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    9695            case WMS_ENDPOINT:
    9796                // convert to WMS type
    98                 return getWMSLayerInfo(info);
     97                if (info.getDefaultLayers() == null || info.getDefaultLayers().isEmpty()) {
     98                    return getWMSLayerInfo(info);
     99                } else {
     100                    return info;
     101                }
    99102            case WMTS:
    100103                // specify which layer to use
    101                 DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
    102                 if (layerId != null) {
    103                     ImageryInfo copy = new ImageryInfo(info);
    104                     Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
    105                     defaultLayers.add(layerId);
    106                     copy.setDefaultLayers(defaultLayers);
    107                     return copy;
    108                 }
    109                 // layer not selected - refuse to add
    110                 return null;
     104                if (info.getDefaultLayers() == null || info.getDefaultLayers().isEmpty()) {
     105                    DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
     106                    if (layerId != null) {
     107                        ImageryInfo copy = new ImageryInfo(info);
     108                        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
     109                        defaultLayers.add(layerId);
     110                        copy.setDefaultLayers(defaultLayers);
     111                        return copy;
     112                    }
     113                    return null;
     114                } else {
     115                    return info;
     116                }
    111117            default:
    112118                return info;
     
    130136            }
    131137            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
     138        } catch (WMTSGetCapabilitiesException e) {
     139            if (!GraphicsEnvironment.isHeadless()) {
     140                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMTS layer list."),
     141                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     142            }
     143            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMTS layer list.", e);
    132144        }
    133145        return null;
     
    166178     */
    167179    protected static ImageryInfo getWMSLayerInfo(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
    168         CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
    169 
    170         final WMSImagery wms = new WMSImagery();
    171         wms.attemptGetCapabilities(info.getUrl());
    172 
    173         final WMSLayerTree tree = new WMSLayerTree();
    174         tree.updateTree(wms);
    175         List<String> wmsFormats = wms.getFormats();
    176         final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[0]));
    177         formats.setSelectedItem(wms.getPreferredFormats());
    178         formats.setToolTipText(tr("Select image format for WMS layer"));
    179 
    180         if (!GraphicsEnvironment.isHeadless() && 1 != new SelectWmsLayersDialog(tree, formats).showDialog().getValue()) {
    181             return null;
    182         }
    183 
    184         final String url = wms.buildGetMapUrl(
    185                 tree.getSelectedLayers(), (String) formats.getSelectedItem());
    186         Set<String> supportedCrs = new HashSet<>();
    187         boolean first = true;
    188         StringBuilder layersString = new StringBuilder();
    189         for (LayerDetails layer: tree.getSelectedLayers()) {
    190             if (first) {
    191                 supportedCrs.addAll(layer.getProjections());
    192                 first = false;
    193             }
    194             layersString.append(layer.name);
    195             layersString.append(", ");
    196             supportedCrs.retainAll(layer.getProjections());
    197         }
    198 
    199         // copy all information from WMS
    200         ImageryInfo ret = new ImageryInfo(info);
    201         // and update according to user choice
    202         ret.setUrl(url);
    203         ret.setImageryType(ImageryType.WMS);
    204         if (layersString.length() > 2) {
    205             ret.setName(ret.getName() + ' ' + layersString.substring(0, layersString.length() - 2));
    206         }
    207         ret.setServerProjections(supportedCrs);
    208         return ret;
     180        try {
     181            CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
     182            final WMSImagery wms = new WMSImagery(info.getUrl());
     183
     184            final WMSLayerTree tree = new WMSLayerTree();
     185            tree.updateTree(wms);
     186
     187            Collection<String> wmsFormats = wms.getFormats();
     188            final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[wmsFormats.size()]));
     189            formats.setSelectedItem(wms.getPreferredFormat());
     190            formats.setToolTipText(tr("Select image format for WMS layer"));
     191
     192            if (!GraphicsEnvironment.isHeadless()) {
     193                if (1 != new ExtendedDialog(Main.parent, tr("Select WMS layers"), new String[]{tr("Add layers"), tr("Cancel")}) { {
     194                    final JScrollPane scrollPane = new JScrollPane(tree.getLayerTree());
     195                    scrollPane.setPreferredSize(new Dimension(400, 400));
     196                    final JPanel panel = new JPanel(new GridBagLayout());
     197                    panel.add(scrollPane, GBC.eol().fill());
     198                    panel.add(formats, GBC.eol().fill(GBC.HORIZONTAL));
     199                    setContent(panel);
     200                } }.showDialog().getValue()) {
     201                    return null;
     202                }
     203            }
     204
     205            final String url = wms.buildGetMapUrl(
     206                    tree.getSelectedLayers().stream().map(x -> x.getName()).collect(Collectors.toList()),
     207                    (List<String>) null,
     208                    (String) formats.getSelectedItem(),
     209                    true // TODO: ask the user if (s)he wants transparent layer
     210                    );
     211
     212            String selectedLayers = tree.getSelectedLayers().stream()
     213                    .map(x -> x.getName())
     214                    .collect(Collectors.joining(", "));
     215            ImageryInfo ret = new ImageryInfo(info.getName() + selectedLayers,
     216                    url,
     217                    "wms",
     218                    info.getEulaAcceptanceRequired(),
     219                    info.getCookies());
     220
     221            ret.setServerProjections(wms.getServerProjections(tree.getSelectedLayers()));
     222
     223            return ret;
     224        } catch (MalformedURLException ex) {
     225            if (!GraphicsEnvironment.isHeadless()) {
     226                JOptionPane.showMessageDialog(Main.parent, tr("Invalid service URL."),
     227                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     228            }
     229            Logging.log(Logging.LEVEL_ERROR, ex);
     230        } catch (IOException ex) {
     231            if (!GraphicsEnvironment.isHeadless()) {
     232                JOptionPane.showMessageDialog(Main.parent, tr("Could not retrieve WMS layer list."),
     233                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     234            }
     235            Logging.log(Logging.LEVEL_ERROR, ex);
     236        } catch (WMSGetCapabilitiesException ex) {
     237            if (!GraphicsEnvironment.isHeadless()) {
     238                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMS layer list."),
     239                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     240            }
     241            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
     242        }
     243        return null;
    209244    }
    210245
  • trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java

    r13358 r13733  
    2121import org.apache.commons.jcs.engine.behavior.ICacheElement;
    2222import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
     23import org.openstreetmap.josm.data.imagery.TileJobOptions;
    2324import org.openstreetmap.josm.data.preferences.IntegerProperty;
    2425import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    9394    private Runnable finishTask;
    9495    private boolean force;
     96    private long minimumExpiryTime;
    9597
    9698    /**
    9799     * @param cache cache instance that we will work on
    98      * @param headers HTTP headers to be sent together with request
    99      * @param readTimeout when connecting to remote resource
    100      * @param connectTimeout when connecting to remote resource
     100     * @param options options of the request
    101101     * @param downloadJobExecutor that will be executing the jobs
    102102     */
    103103    public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache,
    104             int connectTimeout, int readTimeout,
    105             Map<String, String> headers,
     104            TileJobOptions options,
    106105            ThreadPoolExecutor downloadJobExecutor) {
    107106        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
    108107        this.cache = cache;
    109108        this.now = System.currentTimeMillis();
    110         this.connectTimeout = connectTimeout;
    111         this.readTimeout = readTimeout;
    112         this.headers = headers;
     109        this.connectTimeout = options.getConnectionTimeout();
     110        this.readTimeout = options.getReadTimeout();
     111        this.headers = options.getHeaders();
    113112        this.downloadJobExecutor = downloadJobExecutor;
     113        this.minimumExpiryTime = TimeUnit.SECONDS.toMillis(options.getMinimumExpiryTime());
    114114    }
    115115
    116116    /**
    117117     * @param cache cache instance that we will work on
    118      * @param headers HTTP headers to be sent together with request
    119      * @param readTimeout when connecting to remote resource
    120      * @param connectTimeout when connecting to remote resource
     118     * @param options of the request
    121119     */
    122120    public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache,
    123             int connectTimeout, int readTimeout,
    124             Map<String, String> headers) {
    125         this(cache, connectTimeout, readTimeout,
    126                 headers, DEFAULT_DOWNLOAD_JOB_DISPATCHER);
     121            TileJobOptions options) {
     122        this(cache, options, DEFAULT_DOWNLOAD_JOB_DISPATCHER);
    127123    }
    128124
     
    278274            // put a limit to the expire time (some servers send a value
    279275            // that is too large)
    280             expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT);
     276            expires = Math.min(expires, attributes.getCreateTime() + Math.max(EXPIRE_TIME_SERVER_LIMIT, minimumExpiryTime));
    281277            if (now > expires) {
    282278                Logging.debug("JCS - Object {0} has expired -> valid to {1}, now is: {2}",
     
    285281            }
    286282        } else if (attributes.getLastModification() > 0 &&
    287                 now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {
     283                now - attributes.getLastModification() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) {
    288284            // check by file modification date
    289285            Logging.debug("JCS - Object has expired, maximum file age reached {0}", getUrlNoException());
    290286            return false;
    291         } else if (now - attributes.getCreateTime() > DEFAULT_EXPIRE_TIME) {
     287        } else if (now - attributes.getCreateTime() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) {
    292288            Logging.debug("JCS - Object has expired, maximum time since object creation reached {0}", getUrlNoException());
    293289            return false;
     
    330326                // and the server answers with a HTTP 304 = "Not Modified"
    331327                Logging.debug("JCS - If-Modified-Since/ETag test: local version is up to date: {0}", getUrl());
     328                // update cache attributes
     329                attributes = parseHeaders(urlConn);
     330                cache.put(getCacheKey(), cacheData, attributes);
    332331                return true;
    333332            } else if (isObjectLoadable() // we have an object in cache, but we haven't received 304 response code
     
    453452                Logging.trace(e);
    454453            }
    455         }
    456 
    457         ret.setExpirationTime(lng);
     454            if (lng.equals(0L)) {
     455                lng = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;
     456            }
     457        }
     458
     459        ret.setExpirationTime(Math.max(minimumExpiryTime + System.currentTimeMillis(), lng));
    458460        ret.setLastModification(now);
    459461        ret.setEtag(urlConn.getHeaderField("ETag"));
     
    480482        final HttpClient.Response urlConn = getRequest("HEAD", false).connect();
    481483        long lastModified = urlConn.getLastModified();
    482         return (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getHeaderField("ETag"))) ||
     484        boolean ret = (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getHeaderField("ETag"))) ||
    483485                (lastModified != 0 && lastModified <= attributes.getLastModification());
     486        if (ret) {
     487            // update attributes
     488            attributes = parseHeaders(urlConn);
     489            cache.put(getCacheKey(), cacheData, attributes);
     490        }
     491        return ret;
    484492    }
    485493
  • trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java

    r12669 r13733  
    33
    44import java.awt.Point;
     5import java.text.DecimalFormat;
     6import java.text.DecimalFormatSymbols;
     7import java.text.NumberFormat;
     8import java.util.Locale;
    59
    610import org.openstreetmap.gui.jmapviewer.Projected;
     
    2428public abstract class AbstractWMSTileSource extends TMSTileSource {
    2529
     30    static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
     31
    2632    private EastNorth anchorPosition;
    2733    private int[] tileXMin;
     
    209215        return this.tileProjection.toCode();
    210216    }
     217
     218    protected String getBbox(int zoom, int tilex, int tiley, boolean switchLatLon) {
     219        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
     220        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
     221
     222        double w = nw.getX();
     223        double n = nw.getY();
     224
     225        double s = se.getY();
     226        double e = se.getX();
     227
     228        return (
     229                switchLatLon ?
     230                        String.format("%s,%s,%s,%s",
     231                                LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e))
     232                        :
     233                        String.format("%s,%s,%s,%s",
     234                                LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n))
     235
     236                );
     237    }
    211238}
  • trunk/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java

    r13647 r13733  
    4444                    TileLoaderListener.class,
    4545                    ICacheAccess.class,
    46                     int.class,
    47                     int.class,
    48                     Map.class);
     46                    TileJobOptions.class
     47                    );
    4948        } catch (NoSuchMethodException | SecurityException e) {
    5049            Logging.log(Logging.LEVEL_WARN, "Unable to initialize cache tile loader factory", e);
     
    6463
    6564    @Override
    66     public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
     65    public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders, long minimumExpiryTime) {
    6766        Map<String, String> headers = new ConcurrentHashMap<>();
    6867        headers.put("User-Agent", Version.getInstance().getFullAgentString());
     
    7271
    7372        return getLoader(listener, cache,
    74                 (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15)),
    75                 (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30)),
    76                 headers);
     73                new TileJobOptions(
     74                        (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15)),
     75                        (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30)),
     76                        headers,
     77                        minimumExpiryTime
     78                        )
     79                );
    7780    }
    7881
    7982    protected TileLoader getLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
    80             int connectTimeout, int readTimeout, Map<String, String> headers) {
     83            TileJobOptions options) {
    8184        try {
    8285            return tileLoaderConstructor.newInstance(
    8386                    listener,
    8487                    cache,
    85                     connectTimeout,
    86                     readTimeout,
    87                     headers);
     88                    options
     89                    );
    8890        } catch (IllegalArgumentException e) {
    8991            Logging.warn(e);
  • trunk/src/org/openstreetmap/josm/data/imagery/DefaultLayer.java

    r11257 r13733  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import javax.json.Json;
     7import javax.json.JsonObject;
     8import javax.json.JsonObjectBuilder;
     9
     10import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    311
    412/**
     
    1220 */
    1321public class DefaultLayer {
    14 
    15     protected String layerName;
     22    private final String layerName;
     23    private final String tileMatrixSet;
     24    private final String style;
    1625
    1726    /**
    1827     * Constructor
    19      * @param layerName that is the DefaultLayer
     28     * @param imageryType for which this layer is defined
     29     * @param layerName as returned by getIdentifier for WMTS and getName for WMS
     30     * @param style of the layer
     31     * @param tileMatrixSet only for WMTS - tileMatrixSet to use
    2032     */
    21     public DefaultLayer(String layerName) {
    22         this.layerName = layerName;
     33    public DefaultLayer(ImageryType imageryType, String layerName, String style, String tileMatrixSet) {
     34        this.layerName = layerName == null ? "" : layerName;
     35        this.style = style == null ? "" : style;
     36        if (!imageryType.equals(ImageryType.WMTS) && !(tileMatrixSet == null || "".equals(tileMatrixSet))) {
     37            throw new IllegalArgumentException(tr("{0} imagery has tileMatrixSet defined to: {1}", imageryType, tileMatrixSet));
     38        }
     39        this.tileMatrixSet = tileMatrixSet == null ? "" : tileMatrixSet;
    2340    }
    2441
     
    3047    }
    3148
     49    /**
     50     * @return default tileMatrixSet. Only usable for WMTS
     51     */
     52    public String getTileMatrixSet() {
     53        return tileMatrixSet;
     54    }
     55
     56    /**
     57     * @return style for this WMS / WMTS layer to use
     58     */
     59    public String getStyle() {
     60        return style;
     61    }
     62
     63    /**
     64     * @return JSON representation of the default layer object
     65     */
     66    public JsonObject toJson() {
     67        JsonObjectBuilder ret = Json.createObjectBuilder();
     68        ret.add("layerName", layerName);
     69        ret.add("style", style);
     70        ret.add("tileMatrixSet", tileMatrixSet);
     71        return ret.build();
     72    }
     73
     74    /**
     75     * Factory method creating DefaultLayer from JSON objects
     76     * @param o serialized DefaultLayer object
     77     * @param type of ImageryType serialized
     78     * @return DefaultLayer instance based on JSON object
     79     */
     80    public static DefaultLayer fromJson(JsonObject o, ImageryType type) {
     81        return new DefaultLayer(type, o.getString("layerName"), o.getString("style"), o.getString("tileMatrixSet"));
     82    }
    3283}
  • trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

    r13537 r13733  
    55
    66import java.awt.Image;
     7import java.io.StringReader;
    78import java.util.ArrayList;
    89import java.util.Arrays;
     
    1516import java.util.Set;
    1617import java.util.TreeSet;
     18import java.util.concurrent.ConcurrentHashMap;
     19import java.util.concurrent.TimeUnit;
    1720import java.util.regex.Matcher;
    1821import java.util.regex.Pattern;
    1922import java.util.stream.Collectors;
    2023
     24import javax.json.Json;
     25import javax.json.JsonObject;
     26import javax.json.stream.JsonCollectors;
    2127import javax.swing.ImageIcon;
    2228
     
    218224    private boolean isGeoreferenceValid;
    219225    /** which layers should be activated by default on layer addition. **/
    220     private Collection<DefaultLayer> defaultLayers = Collections.emptyList();
    221     // when adding a field, also adapt the ImageryInfo(ImageryInfo)
    222     // and ImageryInfo(ImageryPreferenceEntry) constructor, equals method, and ImageryPreferenceEntry
     226    private List<DefaultLayer> defaultLayers = new ArrayList<>();
     227    /** HTTP headers **/
     228    private Map<String, String> customHttpHeaders = new ConcurrentHashMap<>();
     229    /** Should this map be transparent **/
     230    private boolean transparent = true;
     231    private int minimumTileExpire = (int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get());
     232    /** when adding a field, also adapt the:
     233     * {@link #ImageryPreferenceEntry ImageryPreferenceEntry object}
     234     * {@link #ImageryPreferenceEntry#ImageryPreferenceEntry(ImageryInfo) ImageryPreferenceEntry constructor}
     235     * {@link #ImageryInfo(ImageryPreferenceEntry) ImageryInfo constructor}
     236     * {@link #ImageryInfo(ImageryInfo) ImageryInfo constructor}
     237     * {@link #equalsPref(ImageryPreferenceEntry) equalsPref method}
     238     **/
    223239
    224240    /**
     
    258274        @StructEntry boolean modTileFeatures;
    259275        @StructEntry boolean overlay;
    260         // TODO: disabled until change of layers is implemented
    261         // @StructEntry String default_layers;
     276        @StructEntry String default_layers;
     277        @StructEntry Map<String, String> customHttpHeaders;
     278        @StructEntry boolean transparent;
     279        @StructEntry int minimumTileExpire;
    262280
    263281        /**
     
    308326                }
    309327            }
    310             projections = i.serverProjections.stream().collect(Collectors.joining(","));
     328            if (!i.serverProjections.isEmpty()) {
     329                projections = i.serverProjections.stream().collect(Collectors.joining(","));
     330            }
    311331            if (i.noTileHeaders != null && !i.noTileHeaders.isEmpty()) {
    312332                noTileHeaders = new MultiMap<>(i.noTileHeaders);
     
    325345            valid_georeference = i.isGeoreferenceValid();
    326346            modTileFeatures = i.isModTileFeatures();
    327             // TODO disabled until change of layers is implemented
    328             // default_layers = i.defaultLayers.stream().collect(Collectors.joining(","));
     347            if (!i.defaultLayers.isEmpty()) {
     348                default_layers = i.defaultLayers.stream().map(x -> x.toJson()).collect(JsonCollectors.toJsonArray()).toString();
     349            }
     350            customHttpHeaders = i.customHttpHeaders;
     351            transparent = i.isTransparent();
     352            minimumTileExpire = i.minimumTileExpire;
    329353        }
    330354
     
    468492        isGeoreferenceValid = e.valid_georeference;
    469493        modTileFeatures = e.modTileFeatures;
    470         // TODO disabled until change of layers is implemented
    471         // defaultLayers = Arrays.asList(e.default_layers.split(","));
     494        if (e.default_layers != null) {
     495            defaultLayers = Json.createReader(new StringReader(e.default_layers)).
     496                    readArray().
     497                    stream().
     498                    map(x -> DefaultLayer.fromJson((JsonObject) x, imageryType)).
     499                    collect(Collectors.toList());
     500        }
     501        customHttpHeaders = e.customHttpHeaders;
     502        transparent = e.transparent;
     503        minimumTileExpire = e.minimumTileExpire;
    472504    }
    473505
     
    514546        this.isGeoreferenceValid = i.isGeoreferenceValid;
    515547        this.defaultLayers = i.defaultLayers;
     548        this.customHttpHeaders = i.customHttpHeaders;
     549        this.transparent = i.transparent;
     550        this.minimumTileExpire = i.minimumTileExpire;
    516551    }
    517552
     
    565600                Objects.equals(this.noTileChecksums, other.noTileChecksums) &&
    566601                Objects.equals(this.metadataHeaders, other.metadataHeaders) &&
    567                 Objects.equals(this.defaultLayers, other.defaultLayers);
     602                Objects.equals(this.defaultLayers, other.defaultLayers) &&
     603                Objects.equals(this.customHttpHeaders, other.customHttpHeaders) &&
     604                Objects.equals(this.transparent, other.transparent) &&
     605                Objects.equals(this.minimumTileExpire, other.minimumTileExpire);
    568606        // CHECKSTYLE.ON: BooleanExpressionComplexity
    569607    }
     
    13721410     * @return Collection of the layer names
    13731411     */
    1374     public Collection<DefaultLayer> getDefaultLayers() {
     1412    public List<DefaultLayer> getDefaultLayers() {
    13751413        return defaultLayers;
    13761414    }
     
    13801418     * @param layers set the list of default layers
    13811419     */
    1382     public void setDefaultLayers(Collection<DefaultLayer> layers) {
    1383         if (ImageryType.WMTS.equals(this.imageryType)) {
    1384             CheckParameterUtil.ensureThat(layers == null ||
    1385                     layers.isEmpty() ||
    1386                     layers.iterator().next() instanceof WMTSDefaultLayer, "Incorrect default layer");
    1387         }
     1420    public void setDefaultLayers(List<DefaultLayer> layers) {
    13881421        this.defaultLayers = layers;
    13891422    }
     1423
     1424    /**
     1425     * Returns custom HTTP headers that should be sent with request towards imagery provider
     1426     * @return headers
     1427     */
     1428    public Map<String, String> getCustomHttpHeaders() {
     1429        return customHttpHeaders;
     1430    }
     1431
     1432    /**
     1433     * Sets custom HTTP headers that should be sent with request towards imagery provider
     1434     * @param customHttpHeaders
     1435     */
     1436    public void setCustomHttpHeaders(Map<String, String> customHttpHeaders) {
     1437        this.customHttpHeaders = customHttpHeaders;
     1438    }
     1439
     1440    /**
     1441     * @return should this imagery be transparent
     1442     */
     1443    public boolean isTransparent() {
     1444        return transparent;
     1445    }
     1446
     1447    /**
     1448     *
     1449     * @param transparent set to true if imagery should be transparent
     1450     */
     1451    public void setTransparent(boolean transparent) {
     1452        this.transparent = transparent;
     1453    }
     1454
     1455    /**
     1456     * @return minimum tile expiration in seconds
     1457     */
     1458    public int getMinimumTileExpire() {
     1459        return minimumTileExpire;
     1460    }
     1461
     1462    /**
     1463     * Sets minimum tile expiration in seconds
     1464     * @param minimumTileExpire
     1465     */
     1466    public void setMinimumTileExpire(int minimumTileExpire) {
     1467        this.minimumTileExpire = minimumTileExpire;
     1468
     1469    }
    13901470}
  • trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java

    r13112 r13733  
    22package org.openstreetmap.josm.data.imagery;
    33
    4 import java.util.Map;
    54import java.util.concurrent.ThreadPoolExecutor;
    65import java.util.concurrent.TimeUnit;
     
    2726
    2827    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
    29     protected final int connectTimeout;
    30     protected final int readTimeout;
    31     protected final Map<String, String> headers;
    3228    protected final TileLoaderListener listener;
    3329
     
    5046
    5147    private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
     48    protected final TileJobOptions options;
    5249
    5350    /**
    5451     * Constructor
    5552     * @param listener          called when tile loading has finished
    56      * @param cache              of the cache
    57      * @param connectTimeout    to remote resource
    58      * @param readTimeout       to remote resource
    59      * @param headers           HTTP headers to be sent along with request
     53     * @param cache             of the cache
     54     * @param options           tile job options
    6055     */
    6156    public TMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
    62             int connectTimeout, int readTimeout, Map<String, String> headers) {
     57           TileJobOptions options) {
    6358        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
    6459        this.cache = cache;
    65         this.connectTimeout = connectTimeout;
    66         this.readTimeout = readTimeout;
    67         this.headers = headers;
     60        this.options = options;
    6861        this.listener = listener;
    6962    }
     
    9891    @Override
    9992    public TileJob createTileLoaderJob(Tile tile) {
    100         return new TMSCachedTileLoaderJob(listener, tile, cache,
    101                 connectTimeout, readTimeout, headers, getDownloadExecutor());
     93        return new TMSCachedTileLoaderJob(
     94                listener,
     95                tile,
     96                cache,
     97                options,
     98                getDownloadExecutor());
    10299    }
    103100
  • trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java

    r13730 r13733  
    4444 */
    4545public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
    46     private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
    47     private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
     46    /** General maximum expires for tiles. Might be overridden by imagery settings */
     47    public static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
     48    /** General minimum expires for tiles. Might be overridden by imagery settings */
     49    public static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
    4850    static final Pattern SERVICE_EXCEPTION_PATTERN = Pattern.compile("(?s).+<ServiceException[^>]*>(.+)</ServiceException>.+");
    4951    protected final Tile tile;
    5052    private volatile URL url;
     53    private final TileJobOptions options;
    5154
    5255    // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
     
    5962     * @param tile to be fetched from cache
    6063     * @param cache object
    61      * @param connectTimeout when connecting to remote resource
    62      * @param readTimeout when connecting to remote resource
    63      * @param headers HTTP headers to be sent together with request
     64     * @param options for job (such as http headers, timeouts etc.)
    6465     * @param downloadExecutor that will be executing the jobs
    6566     */
    6667    public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
    6768            ICacheAccess<String, BufferedImageCacheEntry> cache,
    68             int connectTimeout, int readTimeout, Map<String, String> headers,
     69            TileJobOptions options,
    6970            ThreadPoolExecutor downloadExecutor) {
    70         super(cache, connectTimeout, readTimeout, headers, downloadExecutor);
     71        super(cache, options, downloadExecutor);
    7172        this.tile = tile;
     73        this.options = options;
    7274        if (listener != null) {
    7375            String deduplicationKey = getCacheKey();
     
    245247        // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
    246248        // at least for some short period of time, but not too long
    247         if (ret.getExpirationTime() < now + MINIMUM_EXPIRES.get()) {
     249        if (ret.getExpirationTime() < now + Math.max(MINIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
    248250            ret.setExpirationTime(now + MINIMUM_EXPIRES.get());
    249251        }
    250         if (ret.getExpirationTime() > now + MAXIMUM_EXPIRES.get()) {
     252        if (ret.getExpirationTime() > now + Math.max(MAXIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
    251253            ret.setExpirationTime(now + MAXIMUM_EXPIRES.get());
    252254        }
  • trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java

    r12537 r13733  
    5959        super(info, tileProjection);
    6060        this.serverProjections = new TreeSet<>(info.getServerProjections());
     61        this.headers.putAll(info.getCustomHttpHeaders());
    6162        handleTemplate();
    6263        initProjection();
     
    109110            switchLatLon = Main.getProjection().switchXY();
    110111        }
    111         String bbox;
    112         if (switchLatLon) {
    113             bbox = String.format("%s,%s,%s,%s",
    114                     LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e));
    115         } else {
    116             bbox = String.format("%s,%s,%s,%s",
    117                     LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n));
    118         }
     112        String bbox = getBbox(zoom, tilex, tiley, switchLatLon);
    119113
    120114        // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
  • trunk/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java

    r10651 r13733  
    1919     * @param listener that will be notified, when tile has finished loading
    2020     * @param headers that will be sent with requests to TileSource. <code>null</code> indicates none
     21     * @param minimumExpiryTime minimum expiry time
    2122     * @return TileLoader that uses both of above
    2223     */
    23     TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
     24    TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers, long minimumExpiryTime);
    2425}
  • trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java

    r11453 r13733  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.imagery;
    3 
    4 import java.util.Map;
    53
    64import org.apache.commons.jcs.access.behavior.ICacheAccess;
     
    3533     */
    3634    public WMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
    37             int connectTimeout, int readTimeout, Map<String, String> headers) {
     35            TileJobOptions options) {
    3836
    39         super(listener, cache, connectTimeout, readTimeout, headers);
     37        super(listener, cache, options);
    4038        setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS-downloader-%d", THREAD_LIMIT.get()));
    4139    }
     
    4341    @Override
    4442    public TileJob createTileLoaderJob(Tile tile) {
    45         return new WMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers, getDownloadExecutor());
     43        return new WMSCachedTileLoaderJob(listener, tile, cache, options, getDownloadExecutor());
    4644    }
    4745}
  • trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java

    r11858 r13733  
    22package org.openstreetmap.josm.data.imagery;
    33
    4 import java.util.Map;
    54import java.util.concurrent.ThreadPoolExecutor;
    65
     
    2322     * @param tile to load
    2423     * @param cache to use (get/put)
    25      * @param connectTimeout to tile source
    26      * @param readTimeout to tile source
    27      * @param headers to be sent with request
     24     * @param options options for tile job
    2825     * @param downloadExecutor that will execute the download task (if needed)
    2926     */
    30     public WMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
    31             ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout,
    32             Map<String, String> headers, ThreadPoolExecutor downloadExecutor) {
    33         super(listener, tile, cache, connectTimeout, readTimeout, headers, downloadExecutor);
     27    public WMSCachedTileLoaderJob(TileLoaderListener listener,
     28            Tile tile,
     29            ICacheAccess<String, BufferedImageCacheEntry> cache,
     30            TileJobOptions options,
     31            ThreadPoolExecutor downloadExecutor) {
     32        super(listener, tile, cache, options, downloadExecutor);
    3433    }
    3534
  • trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java

    r13542 r13733  
    5151import org.openstreetmap.josm.data.coor.EastNorth;
    5252import org.openstreetmap.josm.data.coor.LatLon;
     53import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
     54import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    5355import org.openstreetmap.josm.data.projection.Projection;
    5456import org.openstreetmap.josm.data.projection.Projections;
     
    126128    }
    127129
    128     private static class TileMatrixSet {
     130    /**
     131     *
     132     * class representing WMTS TileMatrixSet
     133     * This connects projection and TileMatrix (how the map is divided in tiles)
     134     *
     135     */
     136    public static class TileMatrixSet {
    129137
    130138        private final List<TileMatrix> tileMatrix;
     
    154162            return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']';
    155163        }
     164
     165        /**
     166         *
     167         * @return identifier of this TileMatrixSet
     168         */
     169        public String getIdentifier() {
     170            return identifier;
     171        }
    156172    }
    157173
     
    162178    }
    163179
    164     private static class Layer {
     180    /**
     181     * Class representing WMTS Layer information
     182     *
     183     */
     184    public static class Layer {
    165185        private String format;
    166186        private String identifier;
     
    202222                    + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']';
    203223        }
     224
     225        /**
     226         *
     227         * @return identifier of this layer
     228         */
     229        public String getIdentifier() {
     230            return identifier;
     231        }
     232
     233        /**
     234         *
     235         * @return style of this layer
     236         */
     237        public String getStyle() {
     238            return style;
     239        }
     240
     241        /**
     242         *
     243         * @return
     244         */
     245        public TileMatrixSet getTileMatrixSet() {
     246            return tileMatrixSet;
     247        }
     248    }
     249
     250    /**
     251     * Exception thrown when praser doesn't find expected information in GetCapabilities document
     252     *
     253     */
     254    public static class WMTSGetCapabilitiesException extends Exception {
     255
     256        /**
     257         * @param cause description of cause
     258         */
     259        public WMTSGetCapabilitiesException(String cause) {
     260            super(cause);
     261        }
    204262    }
    205263
     
    211269            super(Main.parent, tr("Select WMTS layer"), tr("Add layers"), tr("Cancel"));
    212270            this.layers = groupLayersByNameAndTileMatrixSet(layers);
    213             //getLayersTable(layers, Main.getProjection())
    214             this.list = new JTable(
    215                     new AbstractTableModel() {
    216                         @Override
    217                         public Object getValueAt(int rowIndex, int columnIndex) {
    218                             switch (columnIndex) {
    219                             case 0:
    220                                 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
    221                                         .stream()
    222                                         .map(Layer::getUserTitle)
    223                                         .collect(Collectors.joining(", ")); //this should be only one
    224                             case 1:
    225                                 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
    226                                         .stream()
    227                                         .map(x -> x.tileMatrixSet.crs)
    228                                         .collect(Collectors.joining(", "));
    229                             case 2:
    230                                 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
    231                                         .stream()
    232                                         .map(x -> x.tileMatrixSet.identifier)
    233                                         .collect(Collectors.joining(", ")); //this should be only one
    234                             default:
    235                                 throw new IllegalArgumentException();
    236                             }
    237                         }
    238 
    239                         @Override
    240                         public int getRowCount() {
    241                             return SelectLayerDialog.this.layers.size();
    242                         }
    243 
    244                         @Override
    245                         public int getColumnCount() {
    246                             return 3;
    247                         }
    248 
    249                         @Override
    250                         public String getColumnName(int column) {
    251                             switch (column) {
    252                             case 0: return tr("Layer name");
    253                             case 1: return tr("Projection");
    254                             case 2: return tr("Matrix set identifier");
    255                             default:
    256                                 throw new IllegalArgumentException();
    257                             }
    258                         }
    259                     });
    260             this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    261             this.list.setAutoCreateRowSorter(true);
    262             this.list.setRowSelectionAllowed(true);
    263             this.list.setColumnSelectionAllowed(false);
     271            this.list = getLayerSelectionPanel(this.layers);
    264272            JPanel panel = new JPanel(new GridBagLayout());
    265273            panel.add(new JScrollPane(this.list), GBC.eol().fill());
     
    273281            }
    274282            Layer selectedLayer = layers.get(list.convertRowIndexToModel(index)).getValue().get(0);
    275             return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier);
    276         }
    277 
    278         private static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
    279             Map<String, List<Layer>> layerByName = layers.stream().collect(
    280                     Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
    281             return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
    282         }
     283            return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
     284        }
     285
    283286    }
    284287
     
    292295    private ScaleList nativeScaleList;
    293296
    294     private final WMTSDefaultLayer defaultLayer;
     297    private final DefaultLayer defaultLayer;
    295298
    296299    private Projection tileProjection;
     
    300303     * @param info imagery info
    301304     * @throws IOException if any I/O error occurs
     305     * @throws WMTSGetCapabilitiesException
    302306     * @throws IllegalArgumentException if any other error happens for the given imagery info
    303307     */
    304     public WMTSTileSource(ImageryInfo info) throws IOException {
     308    public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException {
    305309        super(info);
    306310        CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
    307 
     311        this.headers.putAll(info.getCustomHttpHeaders());
    308312        this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
    309         this.layers = getCapabilities();
     313        WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
     314        this.layers =  capabilities.getLayers();
     315        this.baseUrl = capabilities.getBaseUrl();
     316        this.transferMode = capabilities.getTransferMode();
    310317        if (info.getDefaultLayers().isEmpty()) {
    311318            Logging.warn(tr("No default layer selected, choosing first layer."));
    312319            if (!layers.isEmpty()) {
    313320                Layer first = layers.iterator().next();
    314                 this.defaultLayer = new WMTSDefaultLayer(first.identifier, first.tileMatrixSet.identifier);
     321                this.defaultLayer = new DefaultLayer(info.getImageryType(), first.identifier, first.style, first.tileMatrixSet.identifier);
    315322            } else {
    316323                this.defaultLayer = null;
    317324            }
    318325        } else {
    319             DefaultLayer defLayer = info.getDefaultLayers().iterator().next();
    320             if (defLayer instanceof WMTSDefaultLayer) {
    321                 this.defaultLayer = (WMTSDefaultLayer) defLayer;
    322             } else {
    323                 this.defaultLayer = null;
    324             }
     326            this.defaultLayer = info.getDefaultLayers().iterator().next();
    325327        }
    326328        if (this.layers.isEmpty())
     
    343345                // only one tile matrix set with matching projection - no point in asking
    344346                Layer selectedLayer = ls.get(0);
    345                 return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier);
     347                return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
    346348            }
    347349        }
     
    366368    }
    367369
    368     /**
     370
     371    /**
     372     * @param url of the getCapabilities document
     373     * @param headers HTTP headers to set when calling getCapabilities url
    369374     * @return capabilities
    370375     * @throws IOException in case of any I/O error
     376     * @throws WMTSGetCapabilitiesException
    371377     * @throws IllegalArgumentException in case of any other error
    372378     */
    373     private Collection<Layer> getCapabilities() throws IOException {
    374         try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers).
     379    public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException {
     380        try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
    375381                setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)).
    376382                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
     
    379385            if (data.length == 0) {
    380386                cf.clear();
    381                 throw new IllegalArgumentException("Could not read data from: " + baseUrl);
     387                throw new IllegalArgumentException("Could not read data from: " + url);
    382388            }
    383389
    384390            try {
    385391                XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
    386                 Collection<Layer> ret = new ArrayList<>();
     392                WMTSCapabilities ret = null;
     393                Collection<Layer> layers = null;
    387394                for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
    388395                    if (event == XMLStreamReader.START_ELEMENT) {
    389396                        if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) {
    390                             parseOperationMetadata(reader);
     397                            ret = parseOperationMetadata(reader);
    391398                        }
    392399
    393400                        if (QN_CONTENTS.equals(reader.getName())) {
    394                             ret = parseContents(reader);
     401                            layers = parseContents(reader);
    395402                        }
    396403                    }
    397404                }
     405                if (ret == null) {
     406                    /*
     407                     *  see #12168 - create dummy operation metadata - not all WMTS services provide this information
     408                     *
     409                     *  WMTS Standard:
     410                     *  > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section.
     411                     *
     412                     *  And OperationMetada is not mandatory element. So REST mode is justifiable
     413                     */
     414                    ret = new WMTSCapabilities(url, TransferMode.REST);
     415                }
     416                if (layers == null) {
     417                    throw new WMTSGetCapabilitiesException(tr("WMTS Capabilties document did not contain layers in url:  {0}", url));
     418                }
     419                ret.addLayers(layers);
    398420                return ret;
    399421            } catch (XMLStreamException e) {
     
    456478        supportedMimeTypes.add("image/jpgpng");         // used by ESRI
    457479        supportedMimeTypes.add("image/png8");           // used by geoserver
     480        if (supportedMimeTypes.contains("image/jpeg")) {
     481            supportedMimeTypes.add("image/jpg"); // sometimes mispelled by Arcgis
     482        }
    458483        Collection<String> unsupportedFormats = new ArrayList<>();
    459484
     
    639664    /**
    640665     * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
    641      * Sets this.baseUrl and this.transferMode
     666     * return WMTSCapabilities with baseUrl and transferMode
    642667     *
    643668     * @param reader StAX reader instance
     669     * @return WMTSCapabilities with baseUrl and transferMode set
    644670     * @throws XMLStreamException See {@link XMLStreamReader}
    645671     */
    646     private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
     672    private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
    647673        for (int event = reader.getEventType();
    648674                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
     
    657683                            GetCapabilitiesParseHelper.QN_OWS_GET
    658684                    )) {
    659                 this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
    660                 this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader);
    661             }
    662         }
     685                return new WMTSCapabilities(
     686                        reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"),
     687                        GetCapabilitiesParseHelper.getTransferMode(reader)
     688                        );
     689            }
     690        }
     691        return null;
    663692    }
    664693
     
    671700            return;
    672701        List<Layer> matchingLayers = layers.stream().filter(
    673                 l -> l.identifier.equals(defaultLayer.layerName) && l.tileMatrixSet.crs.equals(proj.toCode()))
     702                l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode()))
    674703                .collect(Collectors.toList());
    675704        if (matchingLayers.size() > 1) {
     
    686715                this.tileProjection = null;
    687716                for (Layer layer : layers) {
    688                     if (!layer.identifier.equals(defaultLayer.layerName)) {
     717                    if (!layer.identifier.equals(defaultLayer.getLayerName())) {
    689718                        continue;
    690719                    }
     
    922951    }
    923952
     953    public static JTable getLayerSelectionPanel(List<Entry<String, List<Layer>>> layers) {
     954        JTable list = new JTable(
     955                new AbstractTableModel() {
     956                    @Override
     957                    public Object getValueAt(int rowIndex, int columnIndex) {
     958                        switch (columnIndex) {
     959                        case 0:
     960                            return layers.get(rowIndex).getValue()
     961                                    .stream()
     962                                    .map(Layer::getUserTitle)
     963                                    .collect(Collectors.joining(", ")); //this should be only one
     964                        case 1:
     965                            return layers.get(rowIndex).getValue()
     966                                    .stream()
     967                                    .map(x -> x.tileMatrixSet.crs)
     968                                    .collect(Collectors.joining(", "));
     969                        case 2:
     970                            return layers.get(rowIndex).getValue()
     971                                    .stream()
     972                                    .map(x -> x.tileMatrixSet.identifier)
     973                                    .collect(Collectors.joining(", ")); //this should be only one
     974                        default:
     975                            throw new IllegalArgumentException();
     976                        }
     977                    }
     978
     979                    @Override
     980                    public int getRowCount() {
     981                        return layers.size();
     982                    }
     983
     984                    @Override
     985                    public int getColumnCount() {
     986                        return 3;
     987                    }
     988
     989                    @Override
     990                    public String getColumnName(int column) {
     991                        switch (column) {
     992                        case 0: return tr("Layer name");
     993                        case 1: return tr("Projection");
     994                        case 2: return tr("Matrix set identifier");
     995                        default:
     996                            throw new IllegalArgumentException();
     997                        }
     998                    }
     999                });
     1000        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
     1001        list.setAutoCreateRowSorter(true);
     1002        list.setRowSelectionAllowed(true);
     1003        list.setColumnSelectionAllowed(false);
     1004        return list;
     1005    }
     1006
     1007    public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
     1008        Map<String, List<Layer>> layerByName = layers.stream().collect(
     1009                Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
     1010        return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
     1011    }
     1012
     1013
    9241014    /**
    9251015     * @return set of projection codes that this TileSource supports
  • trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java

    r13434 r13733  
    2121import java.util.Set;
    2222import java.util.concurrent.CopyOnWriteArrayList;
     23import java.util.concurrent.TimeUnit;
    2324
    2425import javax.swing.ButtonModel;
     
    158159        TileLoaderFactory cachedLoaderFactory = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class);
    159160        if (cachedLoaderFactory != null) {
    160             cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers);
     161            cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers, TimeUnit.HOURS.toSeconds(1));
    161162        } else {
    162163            cachedLoader = null;
  • trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    r13674 r13733  
    203203    // prepared to be moved to the painter
    204204    protected TileCoordinateConverter coordinateConverter;
     205    private final long minimumTileExpire;
    205206
    206207    /**
     
    214215        getFilterSettings().addFilterChangeListener(this);
    215216        getDisplaySettings().addSettingsChangeListener(this);
     217        this.minimumTileExpire = info.getMinimumTileExpire();
    216218    }
    217219
     
    274276        Map<String, String> headers = getHeaders(tileSource);
    275277
    276         tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
     278        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers, minimumTileExpire);
    277279
    278280        try {
     
    17591761        public PrecacheTask(ProgressMonitor progressMonitor) {
    17601762            this.progressMonitor = progressMonitor;
    1761             this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
     1763            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource), minimumTileExpire);
    17621764            if (this.tileLoader instanceof TMSCachedTileLoader) {
    17631765                ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
  • trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

    r13244 r13733  
    196196        switch(info.getImageryType()) {
    197197        case WMS:
     198        case WMS_ENDPOINT:
    198199            return new WMSLayer(info);
    199200        case WMTS:
  • trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java

    r13674 r13733  
    2525import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
    2626import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
     27import org.openstreetmap.josm.data.imagery.WMSEndpointTileSource;
    2728import org.openstreetmap.josm.data.preferences.BooleanProperty;
    2829import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    5758    private static final String CACHE_REGION_NAME = "WMS";
    5859
    59     private final List<String> serverProjections;
     60    private List<String> serverProjections;
    6061
    6162    /**
     
    6566    public WMSLayer(ImageryInfo info) {
    6667        super(info);
    67         CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS, "ImageryType is WMS");
     68        CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.WMS_ENDPOINT, "ImageryType is WMS");
    6869        CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
    69         TemplatedWMSTileSource.checkUrl(info.getUrl());
     70        if (info.getImageryType() == ImageryType.WMS) {
     71            TemplatedWMSTileSource.checkUrl(info.getUrl());
     72
     73        }
    7074        this.serverProjections = new ArrayList<>(info.getServerProjections());
    7175    }
     
    8993    @Override
    9094    protected AbstractWMSTileSource getTileSource() {
    91         AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(
    92                 info, chooseProjection(Main.getProjection()));
     95        AbstractWMSTileSource tileSource;
     96        if (info.getImageryType() == ImageryType.WMS) {
     97            tileSource = new TemplatedWMSTileSource(info, chooseProjection(Main.getProjection()));
     98        } else {
     99            /*
     100             *  Chicken-and-egg problem. We want to create tile source, but supported projections we can get only
     101             *  from this tile source. So create tilesource first with dummy Main.getProjection(), and then update
     102             *  once we update server projections.
     103             *
     104             *  Thus:
     105             *  * it is not required to provide projections for wms_endpoint imagery types
     106             *  * we always use current definitions returned by server
     107             */
     108            WMSEndpointTileSource endpointTileSource = new WMSEndpointTileSource(info, Main.getProjection());
     109            this.serverProjections = endpointTileSource.getServerProjections();
     110            endpointTileSource.setTileProjection(chooseProjection(Main.getProjection()));
     111            tileSource = endpointTileSource;
     112        }
    93113        info.setAttribution(tileSource);
    94114        return tileSource;
  • trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java

    r12630 r13733  
    1313import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
    1414import org.openstreetmap.josm.data.imagery.WMTSTileSource;
     15import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
    1516import org.openstreetmap.josm.data.projection.Projection;
    1617import org.openstreetmap.josm.gui.MainApplication;
     
    6465            }
    6566            return null;
    66         } catch (IOException e) {
     67        } catch (IOException | WMTSGetCapabilitiesException e) {
    6768            Logging.warn(e);
    6869            throw new IllegalArgumentException(e);
  • 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.