- Timestamp:
- 2018-05-12T14:18:57+02:00 (6 years ago)
- 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 13 13 import java.util.ArrayList; 14 14 import java.util.Collection; 15 import java.util.HashSet;16 15 import java.util.List; 17 import java.util. Set;16 import java.util.stream.Collectors; 18 17 19 18 import javax.swing.JComboBox; … … 27 26 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 28 27 import org.openstreetmap.josm.data.imagery.WMTSTileSource; 28 import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException; 29 29 import org.openstreetmap.josm.gui.ExtendedDialog; 30 30 import org.openstreetmap.josm.gui.layer.AlignImageryPanel; … … 34 34 import org.openstreetmap.josm.gui.util.GuiHelper; 35 35 import org.openstreetmap.josm.io.imagery.WMSImagery; 36 import org.openstreetmap.josm.io.imagery.WMSImagery.LayerDetails;37 36 import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException; 38 37 import org.openstreetmap.josm.tools.CheckParameterUtil; … … 96 95 case WMS_ENDPOINT: 97 96 // 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 } 99 102 case WMTS: 100 103 // 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 } 111 117 default: 112 118 return info; … … 130 136 } 131 137 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); 132 144 } 133 145 return null; … … 166 178 */ 167 179 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; 209 244 } 210 245 -
trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
r13358 r13733 21 21 import org.apache.commons.jcs.engine.behavior.ICacheElement; 22 22 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult; 23 import org.openstreetmap.josm.data.imagery.TileJobOptions; 23 24 import org.openstreetmap.josm.data.preferences.IntegerProperty; 24 25 import org.openstreetmap.josm.tools.CheckParameterUtil; … … 93 94 private Runnable finishTask; 94 95 private boolean force; 96 private long minimumExpiryTime; 95 97 96 98 /** 97 99 * @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 101 101 * @param downloadJobExecutor that will be executing the jobs 102 102 */ 103 103 public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache, 104 int connectTimeout, int readTimeout, 105 Map<String, String> headers, 104 TileJobOptions options, 106 105 ThreadPoolExecutor downloadJobExecutor) { 107 106 CheckParameterUtil.ensureParameterNotNull(cache, "cache"); 108 107 this.cache = cache; 109 108 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(); 113 112 this.downloadJobExecutor = downloadJobExecutor; 113 this.minimumExpiryTime = TimeUnit.SECONDS.toMillis(options.getMinimumExpiryTime()); 114 114 } 115 115 116 116 /** 117 117 * @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 121 119 */ 122 120 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); 127 123 } 128 124 … … 278 274 // put a limit to the expire time (some servers send a value 279 275 // 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)); 281 277 if (now > expires) { 282 278 Logging.debug("JCS - Object {0} has expired -> valid to {1}, now is: {2}", … … 285 281 } 286 282 } else if (attributes.getLastModification() > 0 && 287 now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {283 now - attributes.getLastModification() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) { 288 284 // check by file modification date 289 285 Logging.debug("JCS - Object has expired, maximum file age reached {0}", getUrlNoException()); 290 286 return false; 291 } else if (now - attributes.getCreateTime() > DEFAULT_EXPIRE_TIME) {287 } else if (now - attributes.getCreateTime() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) { 292 288 Logging.debug("JCS - Object has expired, maximum time since object creation reached {0}", getUrlNoException()); 293 289 return false; … … 330 326 // and the server answers with a HTTP 304 = "Not Modified" 331 327 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); 332 331 return true; 333 332 } else if (isObjectLoadable() // we have an object in cache, but we haven't received 304 response code … … 453 452 Logging.trace(e); 454 453 } 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)); 458 460 ret.setLastModification(now); 459 461 ret.setEtag(urlConn.getHeaderField("ETag")); … … 480 482 final HttpClient.Response urlConn = getRequest("HEAD", false).connect(); 481 483 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"))) || 483 485 (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; 484 492 } 485 493 -
trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java
r12669 r13733 3 3 4 4 import java.awt.Point; 5 import java.text.DecimalFormat; 6 import java.text.DecimalFormatSymbols; 7 import java.text.NumberFormat; 8 import java.util.Locale; 5 9 6 10 import org.openstreetmap.gui.jmapviewer.Projected; … … 24 28 public abstract class AbstractWMSTileSource extends TMSTileSource { 25 29 30 static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US)); 31 26 32 private EastNorth anchorPosition; 27 33 private int[] tileXMin; … … 209 215 return this.tileProjection.toCode(); 210 216 } 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 } 211 238 } -
trunk/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java
r13647 r13733 44 44 TileLoaderListener.class, 45 45 ICacheAccess.class, 46 int.class, 47 int.class, 48 Map.class); 46 TileJobOptions.class 47 ); 49 48 } catch (NoSuchMethodException | SecurityException e) { 50 49 Logging.log(Logging.LEVEL_WARN, "Unable to initialize cache tile loader factory", e); … … 64 63 65 64 @Override 66 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders ) {65 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders, long minimumExpiryTime) { 67 66 Map<String, String> headers = new ConcurrentHashMap<>(); 68 67 headers.put("User-Agent", Version.getInstance().getFullAgentString()); … … 72 71 73 72 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 ); 77 80 } 78 81 79 82 protected TileLoader getLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache, 80 int connectTimeout, int readTimeout, Map<String, String> headers) {83 TileJobOptions options) { 81 84 try { 82 85 return tileLoaderConstructor.newInstance( 83 86 listener, 84 87 cache, 85 connectTimeout, 86 readTimeout, 87 headers); 88 options 89 ); 88 90 } catch (IllegalArgumentException e) { 89 91 Logging.warn(e); -
trunk/src/org/openstreetmap/josm/data/imagery/DefaultLayer.java
r11257 r13733 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import javax.json.Json; 7 import javax.json.JsonObject; 8 import javax.json.JsonObjectBuilder; 9 10 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 3 11 4 12 /** … … 12 20 */ 13 21 public class DefaultLayer { 14 15 protected String layerName; 22 private final String layerName; 23 private final String tileMatrixSet; 24 private final String style; 16 25 17 26 /** 18 27 * 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 20 32 */ 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; 23 40 } 24 41 … … 30 47 } 31 48 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 } 32 83 } -
trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
r13537 r13733 5 5 6 6 import java.awt.Image; 7 import java.io.StringReader; 7 8 import java.util.ArrayList; 8 9 import java.util.Arrays; … … 15 16 import java.util.Set; 16 17 import java.util.TreeSet; 18 import java.util.concurrent.ConcurrentHashMap; 19 import java.util.concurrent.TimeUnit; 17 20 import java.util.regex.Matcher; 18 21 import java.util.regex.Pattern; 19 22 import java.util.stream.Collectors; 20 23 24 import javax.json.Json; 25 import javax.json.JsonObject; 26 import javax.json.stream.JsonCollectors; 21 27 import javax.swing.ImageIcon; 22 28 … … 218 224 private boolean isGeoreferenceValid; 219 225 /** 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 **/ 223 239 224 240 /** … … 258 274 @StructEntry boolean modTileFeatures; 259 275 @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; 262 280 263 281 /** … … 308 326 } 309 327 } 310 projections = i.serverProjections.stream().collect(Collectors.joining(",")); 328 if (!i.serverProjections.isEmpty()) { 329 projections = i.serverProjections.stream().collect(Collectors.joining(",")); 330 } 311 331 if (i.noTileHeaders != null && !i.noTileHeaders.isEmpty()) { 312 332 noTileHeaders = new MultiMap<>(i.noTileHeaders); … … 325 345 valid_georeference = i.isGeoreferenceValid(); 326 346 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; 329 353 } 330 354 … … 468 492 isGeoreferenceValid = e.valid_georeference; 469 493 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; 472 504 } 473 505 … … 514 546 this.isGeoreferenceValid = i.isGeoreferenceValid; 515 547 this.defaultLayers = i.defaultLayers; 548 this.customHttpHeaders = i.customHttpHeaders; 549 this.transparent = i.transparent; 550 this.minimumTileExpire = i.minimumTileExpire; 516 551 } 517 552 … … 565 600 Objects.equals(this.noTileChecksums, other.noTileChecksums) && 566 601 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); 568 606 // CHECKSTYLE.ON: BooleanExpressionComplexity 569 607 } … … 1372 1410 * @return Collection of the layer names 1373 1411 */ 1374 public Collection<DefaultLayer> getDefaultLayers() {1412 public List<DefaultLayer> getDefaultLayers() { 1375 1413 return defaultLayers; 1376 1414 } … … 1380 1418 * @param layers set the list of default layers 1381 1419 */ 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) { 1388 1421 this.defaultLayers = layers; 1389 1422 } 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 } 1390 1470 } -
trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
r13112 r13733 2 2 package org.openstreetmap.josm.data.imagery; 3 3 4 import java.util.Map;5 4 import java.util.concurrent.ThreadPoolExecutor; 6 5 import java.util.concurrent.TimeUnit; … … 27 26 28 27 protected final ICacheAccess<String, BufferedImageCacheEntry> cache; 29 protected final int connectTimeout;30 protected final int readTimeout;31 protected final Map<String, String> headers;32 28 protected final TileLoaderListener listener; 33 29 … … 50 46 51 47 private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER; 48 protected final TileJobOptions options; 52 49 53 50 /** 54 51 * Constructor 55 52 * @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 60 55 */ 61 56 public TMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache, 62 int connectTimeout, int readTimeout, Map<String, String> headers) {57 TileJobOptions options) { 63 58 CheckParameterUtil.ensureParameterNotNull(cache, "cache"); 64 59 this.cache = cache; 65 this.connectTimeout = connectTimeout; 66 this.readTimeout = readTimeout; 67 this.headers = headers; 60 this.options = options; 68 61 this.listener = listener; 69 62 } … … 98 91 @Override 99 92 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()); 102 99 } 103 100 -
trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
r13730 r13733 44 44 */ 45 45 public 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)); 48 50 static final Pattern SERVICE_EXCEPTION_PATTERN = Pattern.compile("(?s).+<ServiceException[^>]*>(.+)</ServiceException>.+"); 49 51 protected final Tile tile; 50 52 private volatile URL url; 53 private final TileJobOptions options; 51 54 52 55 // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created … … 59 62 * @param tile to be fetched from cache 60 63 * @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.) 64 65 * @param downloadExecutor that will be executing the jobs 65 66 */ 66 67 public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile, 67 68 ICacheAccess<String, BufferedImageCacheEntry> cache, 68 int connectTimeout, int readTimeout, Map<String, String> headers,69 TileJobOptions options, 69 70 ThreadPoolExecutor downloadExecutor) { 70 super(cache, connectTimeout, readTimeout, headers, downloadExecutor);71 super(cache, options, downloadExecutor); 71 72 this.tile = tile; 73 this.options = options; 72 74 if (listener != null) { 73 75 String deduplicationKey = getCacheKey(); … … 245 247 // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles 246 248 // at least for some short period of time, but not too long 247 if (ret.getExpirationTime() < now + M INIMUM_EXPIRES.get()) {249 if (ret.getExpirationTime() < now + Math.max(MINIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) { 248 250 ret.setExpirationTime(now + MINIMUM_EXPIRES.get()); 249 251 } 250 if (ret.getExpirationTime() > now + M AXIMUM_EXPIRES.get()) {252 if (ret.getExpirationTime() > now + Math.max(MAXIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) { 251 253 ret.setExpirationTime(now + MAXIMUM_EXPIRES.get()); 252 254 } -
trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
r12537 r13733 59 59 super(info, tileProjection); 60 60 this.serverProjections = new TreeSet<>(info.getServerProjections()); 61 this.headers.putAll(info.getCustomHttpHeaders()); 61 62 handleTemplate(); 62 63 initProjection(); … … 109 110 switchLatLon = Main.getProjection().switchXY(); 110 111 } 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); 119 113 120 114 // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll -
trunk/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java
r10651 r13733 19 19 * @param listener that will be notified, when tile has finished loading 20 20 * @param headers that will be sent with requests to TileSource. <code>null</code> indicates none 21 * @param minimumExpiryTime minimum expiry time 21 22 * @return TileLoader that uses both of above 22 23 */ 23 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers );24 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers, long minimumExpiryTime); 24 25 } -
trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java
r11453 r13733 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.util.Map;5 3 6 4 import org.apache.commons.jcs.access.behavior.ICacheAccess; … … 35 33 */ 36 34 public WMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache, 37 int connectTimeout, int readTimeout, Map<String, String> headers) {35 TileJobOptions options) { 38 36 39 super(listener, cache, connectTimeout, readTimeout, headers);37 super(listener, cache, options); 40 38 setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS-downloader-%d", THREAD_LIMIT.get())); 41 39 } … … 43 41 @Override 44 42 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()); 46 44 } 47 45 } -
trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java
r11858 r13733 2 2 package org.openstreetmap.josm.data.imagery; 3 3 4 import java.util.Map;5 4 import java.util.concurrent.ThreadPoolExecutor; 6 5 … … 23 22 * @param tile to load 24 23 * @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 28 25 * @param downloadExecutor that will execute the download task (if needed) 29 26 */ 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); 34 33 } 35 34 -
trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
r13542 r13733 51 51 import org.openstreetmap.josm.data.coor.EastNorth; 52 52 import org.openstreetmap.josm.data.coor.LatLon; 53 import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode; 54 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 53 55 import org.openstreetmap.josm.data.projection.Projection; 54 56 import org.openstreetmap.josm.data.projection.Projections; … … 126 128 } 127 129 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 { 129 137 130 138 private final List<TileMatrix> tileMatrix; … … 154 162 return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']'; 155 163 } 164 165 /** 166 * 167 * @return identifier of this TileMatrixSet 168 */ 169 public String getIdentifier() { 170 return identifier; 171 } 156 172 } 157 173 … … 162 178 } 163 179 164 private static class Layer { 180 /** 181 * Class representing WMTS Layer information 182 * 183 */ 184 public static class Layer { 165 185 private String format; 166 186 private String identifier; … … 202 222 + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']'; 203 223 } 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 } 204 262 } 205 263 … … 211 269 super(Main.parent, tr("Select WMTS layer"), tr("Add layers"), tr("Cancel")); 212 270 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); 264 272 JPanel panel = new JPanel(new GridBagLayout()); 265 273 panel.add(new JScrollPane(this.list), GBC.eol().fill()); … … 273 281 } 274 282 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 283 286 } 284 287 … … 292 295 private ScaleList nativeScaleList; 293 296 294 private final WMTSDefaultLayer defaultLayer;297 private final DefaultLayer defaultLayer; 295 298 296 299 private Projection tileProjection; … … 300 303 * @param info imagery info 301 304 * @throws IOException if any I/O error occurs 305 * @throws WMTSGetCapabilitiesException 302 306 * @throws IllegalArgumentException if any other error happens for the given imagery info 303 307 */ 304 public WMTSTileSource(ImageryInfo info) throws IOException {308 public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException { 305 309 super(info); 306 310 CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported"); 307 311 this.headers.putAll(info.getCustomHttpHeaders()); 308 312 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(); 310 317 if (info.getDefaultLayers().isEmpty()) { 311 318 Logging.warn(tr("No default layer selected, choosing first layer.")); 312 319 if (!layers.isEmpty()) { 313 320 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); 315 322 } else { 316 323 this.defaultLayer = null; 317 324 } 318 325 } 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(); 325 327 } 326 328 if (this.layers.isEmpty()) … … 343 345 // only one tile matrix set with matching projection - no point in asking 344 346 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); 346 348 } 347 349 } … … 366 368 } 367 369 368 /** 370 371 /** 372 * @param url of the getCapabilities document 373 * @param headers HTTP headers to set when calling getCapabilities url 369 374 * @return capabilities 370 375 * @throws IOException in case of any I/O error 376 * @throws WMTSGetCapabilitiesException 371 377 * @throws IllegalArgumentException in case of any other error 372 378 */ 373 p rivate 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). 375 381 setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)). 376 382 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). … … 379 385 if (data.length == 0) { 380 386 cf.clear(); 381 throw new IllegalArgumentException("Could not read data from: " + baseUrl);387 throw new IllegalArgumentException("Could not read data from: " + url); 382 388 } 383 389 384 390 try { 385 391 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data)); 386 Collection<Layer> ret = new ArrayList<>(); 392 WMTSCapabilities ret = null; 393 Collection<Layer> layers = null; 387 394 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 388 395 if (event == XMLStreamReader.START_ELEMENT) { 389 396 if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) { 390 parseOperationMetadata(reader);397 ret = parseOperationMetadata(reader); 391 398 } 392 399 393 400 if (QN_CONTENTS.equals(reader.getName())) { 394 ret= parseContents(reader);401 layers = parseContents(reader); 395 402 } 396 403 } 397 404 } 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); 398 420 return ret; 399 421 } catch (XMLStreamException e) { … … 456 478 supportedMimeTypes.add("image/jpgpng"); // used by ESRI 457 479 supportedMimeTypes.add("image/png8"); // used by geoserver 480 if (supportedMimeTypes.contains("image/jpeg")) { 481 supportedMimeTypes.add("image/jpg"); // sometimes mispelled by Arcgis 482 } 458 483 Collection<String> unsupportedFormats = new ArrayList<>(); 459 484 … … 639 664 /** 640 665 * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag. 641 * Sets this.baseUrl and this.transferMode666 * return WMTSCapabilities with baseUrl and transferMode 642 667 * 643 668 * @param reader StAX reader instance 669 * @return WMTSCapabilities with baseUrl and transferMode set 644 670 * @throws XMLStreamException See {@link XMLStreamReader} 645 671 */ 646 private voidparseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {672 private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException { 647 673 for (int event = reader.getEventType(); 648 674 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && … … 657 683 GetCapabilitiesParseHelper.QN_OWS_GET 658 684 )) { 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; 663 692 } 664 693 … … 671 700 return; 672 701 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())) 674 703 .collect(Collectors.toList()); 675 704 if (matchingLayers.size() > 1) { … … 686 715 this.tileProjection = null; 687 716 for (Layer layer : layers) { 688 if (!layer.identifier.equals(defaultLayer. layerName)) {717 if (!layer.identifier.equals(defaultLayer.getLayerName())) { 689 718 continue; 690 719 } … … 922 951 } 923 952 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 924 1014 /** 925 1015 * @return set of projection codes that this TileSource supports -
trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
r13434 r13733 21 21 import java.util.Set; 22 22 import java.util.concurrent.CopyOnWriteArrayList; 23 import java.util.concurrent.TimeUnit; 23 24 24 25 import javax.swing.ButtonModel; … … 158 159 TileLoaderFactory cachedLoaderFactory = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class); 159 160 if (cachedLoaderFactory != null) { 160 cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers );161 cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers, TimeUnit.HOURS.toSeconds(1)); 161 162 } else { 162 163 cachedLoader = null; -
trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
r13674 r13733 203 203 // prepared to be moved to the painter 204 204 protected TileCoordinateConverter coordinateConverter; 205 private final long minimumTileExpire; 205 206 206 207 /** … … 214 215 getFilterSettings().addFilterChangeListener(this); 215 216 getDisplaySettings().addSettingsChangeListener(this); 217 this.minimumTileExpire = info.getMinimumTileExpire(); 216 218 } 217 219 … … 274 276 Map<String, String> headers = getHeaders(tileSource); 275 277 276 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers );278 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers, minimumTileExpire); 277 279 278 280 try { … … 1759 1761 public PrecacheTask(ProgressMonitor progressMonitor) { 1760 1762 this.progressMonitor = progressMonitor; 1761 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource) );1763 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource), minimumTileExpire); 1762 1764 if (this.tileLoader instanceof TMSCachedTileLoader) { 1763 1765 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor( -
trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
r13244 r13733 196 196 switch(info.getImageryType()) { 197 197 case WMS: 198 case WMS_ENDPOINT: 198 199 return new WMSLayer(info); 199 200 case WMTS: -
trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java
r13674 r13733 25 25 import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource; 26 26 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader; 27 import org.openstreetmap.josm.data.imagery.WMSEndpointTileSource; 27 28 import org.openstreetmap.josm.data.preferences.BooleanProperty; 28 29 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 57 58 private static final String CACHE_REGION_NAME = "WMS"; 58 59 59 private finalList<String> serverProjections;60 private List<String> serverProjections; 60 61 61 62 /** … … 65 66 public WMSLayer(ImageryInfo info) { 66 67 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"); 68 69 CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url"); 69 TemplatedWMSTileSource.checkUrl(info.getUrl()); 70 if (info.getImageryType() == ImageryType.WMS) { 71 TemplatedWMSTileSource.checkUrl(info.getUrl()); 72 73 } 70 74 this.serverProjections = new ArrayList<>(info.getServerProjections()); 71 75 } … … 89 93 @Override 90 94 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 } 93 113 info.setAttribution(tileSource); 94 114 return tileSource; -
trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
r12630 r13733 13 13 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader; 14 14 import org.openstreetmap.josm.data.imagery.WMTSTileSource; 15 import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException; 15 16 import org.openstreetmap.josm.data.projection.Projection; 16 17 import org.openstreetmap.josm.gui.MainApplication; … … 64 65 } 65 66 return null; 66 } catch (IOException e) {67 } catch (IOException | WMTSGetCapabilitiesException e) { 67 68 Logging.warn(e); 68 69 throw new IllegalArgumentException(e); -
trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java
r13536 r13733 7 7 import java.util.ArrayList; 8 8 import java.util.Arrays; 9 import java.util.HashMap;10 9 import java.util.List; 11 10 import java.util.Map; 12 11 import java.util.Objects; 13 12 import java.util.Stack; 13 import java.util.concurrent.ConcurrentHashMap; 14 14 15 15 import javax.xml.parsers.ParserConfigurationException; 16 16 17 import org.openstreetmap.josm.data.imagery.DefaultLayer; 17 18 import org.openstreetmap.josm.data.imagery.ImageryInfo; 18 19 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds; … … 57 58 NO_TILESUM, 58 59 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 60 64 } 61 65 … … 132 136 private MultiMap<String, String> noTileChecksums; 133 137 private Map<String, String> metadataHeaders; 138 private List<DefaultLayer> defaultLayers; 139 private Map<String, String> customHttpHeaders; 134 140 135 141 @Override … … 145 151 noTileHeaders = null; 146 152 noTileChecksums = null; 153 customHttpHeaders = null; 147 154 } 148 155 … … 164 171 noTileHeaders = new MultiMap<>(); 165 172 noTileChecksums = new MultiMap<>(); 166 metadataHeaders = new HashMap<>(); 173 metadataHeaders = new ConcurrentHashMap<>(); 174 defaultLayers = new ArrayList<>(); 175 customHttpHeaders = new ConcurrentHashMap<>(); 167 176 String best = atts.getValue("eli-best"); 168 177 if (TRUE.equals(best)) { … … 215 224 TILE_SIZE, 216 225 "valid-georeference", 217 "mod-tile-features" 226 "mod-tile-features", 227 "transparent", 228 "minimum-tile-expire" 218 229 ).contains(qName)) { 219 230 newState = State.ENTRY_ATTRIBUTE; … … 247 258 metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key")); 248 259 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; 249 265 } 250 266 break; … … 269 285 if ("code".equals(qName)) { 270 286 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"))); 271 293 } 272 294 break; … … 306 328 entry.setMetadataHeaders(metadataHeaders); 307 329 metadataHeaders = null; 330 entry.setDefaultLayers(defaultLayers); 331 defaultLayers = null; 332 entry.setCustomHttpHeaders(customHttpHeaders); 333 customHttpHeaders = null; 308 334 309 335 if (!skipEntry) { … … 323 349 switch(qName) { 324 350 case "type": 351 ImageryType.values(); 325 352 boolean found = false; 326 353 for (ImageryType type : ImageryType.values()) { … … 487 514 case "mod-tile-features": 488 515 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())); 489 522 break; 490 523 default: // Do nothing -
trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java
r13699 r13733 2 2 package org.openstreetmap.josm.io.imagery; 3 3 4 import java.awt.HeadlessException; 4 import static java.nio.charset.StandardCharsets.UTF_8; 5 import static org.openstreetmap.josm.tools.I18n.tr; 6 7 import java.io.File; 5 8 import java.io.IOException; 6 9 import java.io.InputStream; 7 import java.io.StringReader;8 import java.io.StringWriter;9 10 import java.net.MalformedURLException; 10 11 import java.net.URL; … … 13 14 import java.util.Collections; 14 15 import java.util.HashSet; 15 import java.util.Iterator;16 16 import java.util.List; 17 import java.util.Locale; 18 import java.util.NoSuchElementException; 17 import java.util.Map; 19 18 import java.util.Set; 20 import java.util.regex.Matcher; 19 import java.util.concurrent.ConcurrentHashMap; 20 import java.util.function.Function; 21 21 import java.util.regex.Pattern; 22 22 import java.util.stream.Collectors; 23 import java.util.stream.Stream;24 import java.util.stream.StreamSupport;25 23 26 24 import 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; 25 import javax.xml.namespace.QName; 26 import javax.xml.stream.XMLStreamException; 27 import javax.xml.stream.XMLStreamReader; 34 28 35 29 import org.openstreetmap.josm.data.Bounds; 30 import org.openstreetmap.josm.data.coor.EastNorth; 31 import org.openstreetmap.josm.data.imagery.DefaultLayer; 36 32 import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper; 37 33 import org.openstreetmap.josm.data.imagery.ImageryInfo; 34 import org.openstreetmap.josm.data.imagery.LayerDetails; 35 import org.openstreetmap.josm.data.projection.Projection; 38 36 import org.openstreetmap.josm.data.projection.Projections; 39 import org.openstreetmap.josm.tools.HttpClient; 40 import org.openstreetmap.josm.tools.HttpClient.Response; 37 import org.openstreetmap.josm.io.CachedFile; 41 38 import org.openstreetmap.josm.tools.Logging; 42 39 import 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;49 40 50 41 /** … … 53 44 public class WMSImagery { 54 45 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 85 85 86 86 /** … … 120 120 } 121 121 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 130 212 */ 131 213 public List<LayerDetails> getLayers() { … … 134 216 135 217 /** 136 * Returns the service URL.137 * @return the service URL138 */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 13358147 */148 public String getVersion() {149 return version;150 }151 152 /**153 218 * Returns the list of supported formats. 154 219 * @return the list of supported formats 155 220 */ 156 public List<String> getFormats() {221 public Collection<String> getFormats() { 157 222 return Collections.unmodifiableList(formats); 158 223 } 159 224 160 225 /** 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")) { 166 233 return "image/jpeg"; 167 } else if (formats.contains("image/png")) {168 return "image/png";169 234 } else if (formats.isEmpty()) { 170 235 return null; … … 174 239 } 175 240 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) { 178 246 return null; 179 247 } 248 if (getMapUrl != null) { 249 return getMapUrl; 250 } 251 252 URL serviceUrl = capabilitiesUrl; 180 253 StringBuilder a = new StringBuilder(serviceUrl.getProtocol()); 181 254 a.append("://").append(serviceUrl.getHost()); … … 194 267 195 268 /** 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 { 224 595 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; 237 606 } 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; 366 615 } 367 616 … … 392 641 } 393 642 643 394 644 static boolean imageFormatHasTransparency(final String format) { 395 645 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif") … … 398 648 399 649 /** 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)); 413 662 } 414 663 return i; 415 664 } 416 665 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; 615 704 } 616 705 }
Note:
See TracChangeset
for help on using the changeset viewer.