001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.imagery; 003 004import static java.nio.charset.StandardCharsets.UTF_8; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.net.MalformedURLException; 011import java.net.URL; 012import java.nio.file.InvalidPathException; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.HashSet; 017import java.util.List; 018import java.util.Map; 019import java.util.Set; 020import java.util.concurrent.ConcurrentHashMap; 021import java.util.function.UnaryOperator; 022import java.util.regex.Pattern; 023import java.util.stream.Collectors; 024 025import javax.imageio.ImageIO; 026import javax.xml.namespace.QName; 027import javax.xml.stream.XMLStreamException; 028import javax.xml.stream.XMLStreamReader; 029 030import org.openstreetmap.josm.data.Bounds; 031import org.openstreetmap.josm.data.coor.EastNorth; 032import org.openstreetmap.josm.data.imagery.DefaultLayer; 033import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper; 034import org.openstreetmap.josm.data.imagery.ImageryInfo; 035import org.openstreetmap.josm.data.imagery.LayerDetails; 036import org.openstreetmap.josm.data.projection.Projection; 037import org.openstreetmap.josm.data.projection.Projections; 038import org.openstreetmap.josm.io.CachedFile; 039import org.openstreetmap.josm.tools.Logging; 040import org.openstreetmap.josm.tools.Utils; 041 042/** 043 * This class represents the capabilities of a WMS imagery server. 044 */ 045public class WMSImagery { 046 047 private static final String SERVICE_WMS = "SERVICE=WMS"; 048 private static final String REQUEST_GET_CAPABILITIES = "REQUEST=GetCapabilities"; 049 private static final String CAPABILITIES_QUERY_STRING = SERVICE_WMS + "&" + REQUEST_GET_CAPABILITIES; 050 051 /** 052 * WMS namespace address 053 */ 054 public static final String WMS_NS_URL = "http://www.opengis.net/wms"; 055 056 // CHECKSTYLE.OFF: SingleSpaceSeparator 057 // WMS 1.0 - 1.3.0 058 private static final QName CAPABILITITES_ROOT_130 = new QName("WMS_Capabilities", WMS_NS_URL); 059 private static final QName QN_ABSTRACT = new QName(WMS_NS_URL, "Abstract"); 060 private static final QName QN_CAPABILITY = new QName(WMS_NS_URL, "Capability"); 061 private static final QName QN_CRS = new QName(WMS_NS_URL, "CRS"); 062 private static final QName QN_DCPTYPE = new QName(WMS_NS_URL, "DCPType"); 063 private static final QName QN_FORMAT = new QName(WMS_NS_URL, "Format"); 064 private static final QName QN_GET = new QName(WMS_NS_URL, "Get"); 065 private static final QName QN_GETMAP = new QName(WMS_NS_URL, "GetMap"); 066 private static final QName QN_HTTP = new QName(WMS_NS_URL, "HTTP"); 067 private static final QName QN_LAYER = new QName(WMS_NS_URL, "Layer"); 068 private static final QName QN_NAME = new QName(WMS_NS_URL, "Name"); 069 private static final QName QN_REQUEST = new QName(WMS_NS_URL, "Request"); 070 private static final QName QN_SERVICE = new QName(WMS_NS_URL, "Service"); 071 private static final QName QN_STYLE = new QName(WMS_NS_URL, "Style"); 072 private static final QName QN_TITLE = new QName(WMS_NS_URL, "Title"); 073 private static final QName QN_BOUNDINGBOX = new QName(WMS_NS_URL, "BoundingBox"); 074 private static final QName QN_EX_GEOGRAPHIC_BBOX = new QName(WMS_NS_URL, "EX_GeographicBoundingBox"); 075 private static final QName QN_WESTBOUNDLONGITUDE = new QName(WMS_NS_URL, "westBoundLongitude"); 076 private static final QName QN_EASTBOUNDLONGITUDE = new QName(WMS_NS_URL, "eastBoundLongitude"); 077 private static final QName QN_SOUTHBOUNDLATITUDE = new QName(WMS_NS_URL, "southBoundLatitude"); 078 private static final QName QN_NORTHBOUNDLATITUDE = new QName(WMS_NS_URL, "northBoundLatitude"); 079 private static final QName QN_ONLINE_RESOURCE = new QName(WMS_NS_URL, "OnlineResource"); 080 081 // WMS 1.1 - 1.1.1 082 private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities"); 083 private static final QName QN_SRS = new QName("SRS"); 084 private static final QName QN_LATLONBOUNDINGBOX = new QName("LatLonBoundingBox"); 085 086 // CHECKSTYLE.ON: SingleSpaceSeparator 087 088 /** 089 * An exception that is thrown if there was an error while getting the capabilities of the WMS server. 090 */ 091 public static class WMSGetCapabilitiesException extends Exception { 092 private final String incomingData; 093 094 /** 095 * Constructs a new {@code WMSGetCapabilitiesException} 096 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method) 097 * @param incomingData the answer from WMS server 098 */ 099 public WMSGetCapabilitiesException(Throwable cause, String incomingData) { 100 super(cause); 101 this.incomingData = incomingData; 102 } 103 104 /** 105 * Constructs a new {@code WMSGetCapabilitiesException} 106 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method 107 * @param incomingData the answer from the server 108 * @since 10520 109 */ 110 public WMSGetCapabilitiesException(String message, String incomingData) { 111 super(message); 112 this.incomingData = incomingData; 113 } 114 115 /** 116 * The data that caused this exception. 117 * @return The server response to the capabilities request. 118 */ 119 public String getIncomingData() { 120 return incomingData; 121 } 122 } 123 124 private final Map<String, String> headers = new ConcurrentHashMap<>(); 125 private String version = "1.1.1"; // default version 126 private String getMapUrl; 127 private URL capabilitiesUrl; 128 private final List<String> formats = new ArrayList<>(); 129 private List<LayerDetails> layers = new ArrayList<>(); 130 131 private String title; 132 133 /** 134 * Make getCapabilities request towards given URL 135 * @param url service url 136 * @throws IOException when connection error when fetching get capabilities document 137 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document 138 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file 139 */ 140 public WMSImagery(String url) throws IOException, WMSGetCapabilitiesException { 141 this(url, null); 142 } 143 144 /** 145 * Make getCapabilities request towards given URL using headers 146 * @param url service url 147 * @param headers HTTP headers to be sent with request 148 * @throws IOException when connection error when fetching get capabilities document 149 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document 150 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file 151 */ 152 public WMSImagery(String url, Map<String, String> headers) throws IOException, WMSGetCapabilitiesException { 153 if (headers != null) { 154 this.headers.putAll(headers); 155 } 156 157 IOException savedExc = null; 158 String workingAddress = null; 159 url_search: 160 for (String z: new String[]{ 161 normalizeUrl(url), 162 url, 163 url + CAPABILITIES_QUERY_STRING, 164 }) { 165 for (String ver: new String[]{"", "&VERSION=1.3.0", "&VERSION=1.1.1"}) { 166 try { 167 attemptGetCapabilities(z + ver); 168 workingAddress = z; 169 calculateChildren(); 170 // clear saved exception - we've got something working 171 savedExc = null; 172 break url_search; 173 } catch (IOException e) { 174 savedExc = e; 175 Logging.warn(e); 176 } 177 } 178 } 179 180 if (workingAddress != null) { 181 try { 182 capabilitiesUrl = new URL(workingAddress); 183 } catch (MalformedURLException e) { 184 if (savedExc == null) { 185 savedExc = e; 186 } 187 try { 188 capabilitiesUrl = new File(workingAddress).toURI().toURL(); 189 } catch (MalformedURLException e1) { // NOPMD 190 // do nothing, raise original exception 191 Logging.trace(e1); 192 } 193 } 194 } 195 196 if (savedExc != null) { 197 throw savedExc; 198 } 199 } 200 201 private void calculateChildren() { 202 Map<LayerDetails, List<LayerDetails>> layerChildren = layers.stream() 203 .filter(x -> x.getParent() != null) // exclude top-level elements 204 .collect(Collectors.groupingBy(LayerDetails::getParent)); 205 for (LayerDetails ld: layers) { 206 if (layerChildren.containsKey(ld)) { 207 ld.setChildren(layerChildren.get(ld)); 208 } 209 } 210 // leave only top-most elements in the list 211 layers = layers.stream().filter(x -> x.getParent() == null).collect(Collectors.toCollection(ArrayList::new)); 212 } 213 214 /** 215 * Returns the list of top-level layers. 216 * @return the list of top-level layers 217 */ 218 public List<LayerDetails> getLayers() { 219 return Collections.unmodifiableList(layers); 220 } 221 222 /** 223 * Returns the list of supported formats. 224 * @return the list of supported formats 225 */ 226 public Collection<String> getFormats() { 227 return Collections.unmodifiableList(formats); 228 } 229 230 /** 231 * Gets the preferred format for this imagery layer. 232 * @return The preferred format as mime type. 233 */ 234 public String getPreferredFormat() { 235 if (formats.contains("image/png")) { 236 return "image/png"; 237 } else if (formats.contains("image/jpeg")) { 238 return "image/jpeg"; 239 } else if (formats.isEmpty()) { 240 return null; 241 } else { 242 return formats.get(0); 243 } 244 } 245 246 /** 247 * @return root URL of services in this GetCapabilities 248 */ 249 public String buildRootUrl() { 250 if (getMapUrl == null && capabilitiesUrl == null) { 251 return null; 252 } 253 if (getMapUrl != null) { 254 return getMapUrl; 255 } 256 257 URL serviceUrl = capabilitiesUrl; 258 StringBuilder a = new StringBuilder(serviceUrl.getProtocol()); 259 a.append("://").append(serviceUrl.getHost()); 260 if (serviceUrl.getPort() != -1) { 261 a.append(':').append(serviceUrl.getPort()); 262 } 263 a.append(serviceUrl.getPath()).append('?'); 264 if (serviceUrl.getQuery() != null) { 265 a.append(serviceUrl.getQuery()); 266 if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) { 267 a.append('&'); 268 } 269 } 270 return a.toString(); 271 } 272 273 /** 274 * @return root URL of services without the GetCapabilities call 275 * @since 15209 276 */ 277 public String buildRootUrlWithoutCapabilities() { 278 return buildRootUrl() 279 .replace(CAPABILITIES_QUERY_STRING, "") 280 .replace(SERVICE_WMS, "") 281 .replace(REQUEST_GET_CAPABILITIES, "") 282 .replace("?&", "?"); 283 } 284 285 /** 286 * Returns URL for accessing GetMap service. String will contain following parameters: 287 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)}) 288 * * {width} - that needs to be replaced with width of the tile 289 * * {height} - that needs to be replaces with height of the tile 290 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates) 291 * 292 * Format of the response will be calculated using {@link #getPreferredFormat()} 293 * 294 * @param selectedLayers list of DefaultLayer selection of layers to be shown 295 * @param transparent whether returned images should contain transparent pixels (if supported by format) 296 * @return URL template for GetMap service containing 297 */ 298 public String buildGetMapUrl(List<DefaultLayer> selectedLayers, boolean transparent) { 299 return buildGetMapUrl( 300 getLayers(selectedLayers), 301 selectedLayers.stream().map(DefaultLayer::getStyle).collect(Collectors.toList()), 302 transparent); 303 } 304 305 /** 306 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()} 307 * @param selectedStyles selected styles for all selectedLayers 308 * @param transparent whether returned images should contain transparent pixels (if supported by format) 309 * @return URL template for GetMap service 310 * @see #buildGetMapUrl(List, boolean) 311 */ 312 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) { 313 return buildGetMapUrl(selectedLayers, selectedStyles, getPreferredFormat(), transparent); 314 } 315 316 /** 317 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()} 318 * @param selectedStyles selected styles for all selectedLayers 319 * @param format format of the response - one of {@link #getFormats()} 320 * @param transparent whether returned images should contain transparent pixels (if supported by format) 321 * @return URL template for GetMap service 322 * @see #buildGetMapUrl(List, boolean) 323 * @since 15228 324 */ 325 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) { 326 return buildGetMapUrl( 327 selectedLayers.stream().map(LayerDetails::getName).collect(Collectors.toList()), 328 selectedStyles, 329 format, 330 transparent); 331 } 332 333 /** 334 * @param selectedLayers selected layers as list of strings 335 * @param selectedStyles selected styles of layers as list of strings 336 * @param format format of the response - one of {@link #getFormats()} 337 * @param transparent whether returned images should contain transparent pixels (if supported by format) 338 * @return URL template for GetMap service 339 * @see #buildGetMapUrl(List, boolean) 340 */ 341 public String buildGetMapUrl(List<String> selectedLayers, 342 Collection<String> selectedStyles, 343 String format, 344 boolean transparent) { 345 346 Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(), 347 tr("Styles size {0} does not match layers size {1}"), 348 selectedStyles == null ? 0 : selectedStyles.size(), 349 selectedLayers.size()); 350 351 return buildRootUrlWithoutCapabilities() 352 + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "") 353 + "&VERSION=" + this.version + "&" + SERVICE_WMS + "&REQUEST=GetMap&LAYERS=" 354 + selectedLayers.stream().collect(Collectors.joining(",")) 355 + "&STYLES=" 356 + (selectedStyles != null ? Utils.join(",", selectedStyles) : "") 357 + "&" 358 + (belowWMS130() ? "SRS" : "CRS") 359 + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}"; 360 } 361 362 private boolean tagEquals(QName a, QName b) { 363 boolean ret = a.equals(b); 364 if (ret) { 365 return ret; 366 } 367 368 if (belowWMS130()) { 369 return a.getLocalPart().equals(b.getLocalPart()); 370 } 371 372 return false; 373 } 374 375 private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException { 376 Logging.debug("Trying WMS getcapabilities with url {0}", url); 377 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers). 378 setMaxAge(7 * CachedFile.DAYS). 379 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 380 getInputStream()) { 381 382 try { 383 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in); 384 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 385 if (event == XMLStreamReader.START_ELEMENT) { 386 if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) { 387 // version 1.1.1 388 this.version = reader.getAttributeValue(null, "version"); 389 if (this.version == null) { 390 this.version = "1.1.1"; 391 } 392 } 393 if (tagEquals(CAPABILITITES_ROOT_130, reader.getName())) { 394 this.version = reader.getAttributeValue(WMS_NS_URL, "version"); 395 } 396 if (tagEquals(QN_SERVICE, reader.getName())) { 397 parseService(reader); 398 } 399 400 if (tagEquals(QN_CAPABILITY, reader.getName())) { 401 parseCapability(reader); 402 } 403 } 404 } 405 } catch (XMLStreamException e) { 406 String content = new String(cf.getByteContent(), UTF_8); 407 cf.clear(); // if there is a problem with parsing of the file, remove it from the cache 408 throw new WMSGetCapabilitiesException(e, content); 409 } 410 } 411 } 412 413 private void parseService(XMLStreamReader reader) throws XMLStreamException { 414 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) { 415 this.title = reader.getElementText(); 416 // CHECKSTYLE.OFF: EmptyBlock 417 for (int event = reader.getEventType(); 418 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName())); 419 event = reader.next()) { 420 // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done 421 } 422 // CHECKSTYLE.ON: EmptyBlock 423 } 424 } 425 426 private void parseCapability(XMLStreamReader reader) throws XMLStreamException { 427 for (int event = reader.getEventType(); 428 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName())); 429 event = reader.next()) { 430 431 if (event == XMLStreamReader.START_ELEMENT) { 432 if (tagEquals(QN_REQUEST, reader.getName())) { 433 parseRequest(reader); 434 } 435 if (tagEquals(QN_LAYER, reader.getName())) { 436 parseLayer(reader, null); 437 } 438 } 439 } 440 } 441 442 private void parseRequest(XMLStreamReader reader) throws XMLStreamException { 443 String mode = ""; 444 String getMapUrl = ""; 445 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) { 446 for (int event = reader.getEventType(); 447 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName())); 448 event = reader.next()) { 449 450 if (event == XMLStreamReader.START_ELEMENT) { 451 if (tagEquals(QN_FORMAT, reader.getName())) { 452 String value = reader.getElementText(); 453 if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) { 454 this.formats.add(value); 455 } 456 } 457 if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader, 458 this::tagEquals, QN_HTTP, QN_GET)) { 459 mode = reader.getName().getLocalPart(); 460 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) { 461 getMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"); 462 } 463 // TODO should we handle also POST? 464 if ("GET".equalsIgnoreCase(mode) && getMapUrl != null && !"".equals(getMapUrl)) { 465 try { 466 String query = (new URL(getMapUrl)).getQuery(); 467 if (query == null) { 468 this.getMapUrl = getMapUrl + "?"; 469 } else { 470 this.getMapUrl = getMapUrl; 471 } 472 } catch (MalformedURLException e) { 473 throw new XMLStreamException(e); 474 } 475 } 476 } 477 } 478 } 479 } 480 } 481 482 private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException { 483 LayerDetails ret = new LayerDetails(parentLayer); 484 for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer 485 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName())); 486 event = reader.next()) { 487 488 if (event == XMLStreamReader.START_ELEMENT) { 489 if (tagEquals(QN_NAME, reader.getName())) { 490 ret.setName(reader.getElementText()); 491 } else if (tagEquals(QN_ABSTRACT, reader.getName())) { 492 ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader)); 493 } else if (tagEquals(QN_TITLE, reader.getName())) { 494 ret.setTitle(reader.getElementText()); 495 } else if (tagEquals(QN_CRS, reader.getName())) { 496 ret.addCrs(reader.getElementText()); 497 } else if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) { 498 ret.addCrs(reader.getElementText()); 499 } else if (tagEquals(QN_STYLE, reader.getName())) { 500 parseAndAddStyle(reader, ret); 501 } else if (tagEquals(QN_LAYER, reader.getName())) { 502 parseLayer(reader, ret); 503 } else if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()) && ret.getBounds() == null) { 504 ret.setBounds(parseExGeographic(reader)); 505 } else if (tagEquals(QN_BOUNDINGBOX, reader.getName())) { 506 Projection conv; 507 if (belowWMS130()) { 508 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS")); 509 } else { 510 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS")); 511 } 512 if (ret.getBounds() == null && conv != null) { 513 ret.setBounds(parseBoundingBox(reader, conv)); 514 } 515 } else if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130() && ret.getBounds() == null) { 516 ret.setBounds(parseBoundingBox(reader, null)); 517 } else { 518 // unknown tag, move to its end as it may have child elements 519 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader); 520 } 521 } 522 } 523 this.layers.add(ret); 524 } 525 526 /** 527 * @return if this service operates at protocol level below 1.3.0 528 */ 529 public boolean belowWMS130() { 530 return "1.1.1".equals(version) || "1.1".equals(version) || "1.0".equals(version); 531 } 532 533 private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException { 534 String name = null; 535 String title = null; 536 for (int event = reader.getEventType(); 537 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName())); 538 event = reader.next()) { 539 if (event == XMLStreamReader.START_ELEMENT) { 540 if (tagEquals(QN_NAME, reader.getName())) { 541 name = reader.getElementText(); 542 } 543 if (tagEquals(QN_TITLE, reader.getName())) { 544 title = reader.getElementText(); 545 } 546 } 547 } 548 if (name == null) { 549 name = ""; 550 } 551 ld.addStyle(name, title); 552 } 553 554 private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException { 555 String minx = null, maxx = null, maxy = null, miny = null; 556 557 for (int event = reader.getEventType(); 558 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName())); 559 event = reader.next()) { 560 if (event == XMLStreamReader.START_ELEMENT) { 561 if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) { 562 minx = reader.getElementText(); 563 } 564 565 if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) { 566 maxx = reader.getElementText(); 567 } 568 569 if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) { 570 miny = reader.getElementText(); 571 } 572 573 if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) { 574 maxy = reader.getElementText(); 575 } 576 } 577 } 578 return parseBBox(null, miny, minx, maxy, maxx); 579 } 580 581 private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) { 582 UnaryOperator<String> attrGetter = tag -> belowWMS130() ? 583 reader.getAttributeValue(null, tag) 584 : reader.getAttributeValue(WMS_NS_URL, tag); 585 586 return parseBBox( 587 conv, 588 attrGetter.apply("miny"), 589 attrGetter.apply("minx"), 590 attrGetter.apply("maxy"), 591 attrGetter.apply("maxx") 592 ); 593 } 594 595 private static Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) { 596 if (miny == null || minx == null || maxy == null || maxx == null) { 597 return null; 598 } 599 if (conv != null) { 600 return new Bounds( 601 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))), 602 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy))) 603 ); 604 } 605 return new Bounds( 606 getDecimalDegree(miny), 607 getDecimalDegree(minx), 608 getDecimalDegree(maxy), 609 getDecimalDegree(maxx) 610 ); 611 } 612 613 private static double getDecimalDegree(String value) { 614 // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server) 615 return Double.parseDouble(value.replace(',', '.')); 616 } 617 618 private static String normalizeUrl(String serviceUrlStr) throws MalformedURLException { 619 URL getCapabilitiesUrl = null; 620 String ret = null; 621 622 if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) { 623 // If the url doesn't already have GetCapabilities, add it in 624 getCapabilitiesUrl = new URL(serviceUrlStr); 625 if (getCapabilitiesUrl.getQuery() == null) { 626 ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING; 627 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) { 628 ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING; 629 } else { 630 ret = serviceUrlStr + CAPABILITIES_QUERY_STRING; 631 } 632 } else { 633 // Otherwise assume it's a good URL and let the subsequent error 634 // handling systems deal with problems 635 ret = serviceUrlStr; 636 } 637 return ret; 638 } 639 640 private static boolean isImageFormatSupportedWarn(String format) { 641 boolean isFormatSupported = isImageFormatSupported(format); 642 if (!isFormatSupported) { 643 Logging.info("Skipping unsupported image format {0}", format); 644 } 645 return isFormatSupported; 646 } 647 648 static boolean isImageFormatSupported(final String format) { 649 return ImageIO.getImageReadersByMIMEType(format).hasNext() 650 // handles image/tiff image/tiff8 image/geotiff image/geotiff8 651 || isImageFormatSupported(format, "tiff", "geotiff") 652 || isImageFormatSupported(format, "png") 653 || isImageFormatSupported(format, "svg") 654 || isImageFormatSupported(format, "bmp"); 655 } 656 657 static boolean isImageFormatSupported(String format, String... mimeFormats) { 658 for (String mime : mimeFormats) { 659 if (format.startsWith("image/" + mime)) { 660 return ImageIO.getImageReadersBySuffix(mimeFormats[0]).hasNext(); 661 } 662 } 663 return false; 664 } 665 666 static boolean imageFormatHasTransparency(final String format) { 667 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif") 668 || format.startsWith("image/svg") || format.startsWith("image/tiff")); 669 } 670 671 /** 672 * Creates ImageryInfo object from this GetCapabilities document 673 * 674 * @param name name of imagery layer 675 * @param selectedLayers layers which are to be used by this imagery layer 676 * @param selectedStyles styles that should be used for selectedLayers 677 * @param format format of the response - one of {@link #getFormats()} 678 * @param transparent if layer should be transparent 679 * @return ImageryInfo object 680 * @since 15228 681 */ 682 public ImageryInfo toImageryInfo( 683 String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) { 684 ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, format, transparent)); 685 if (selectedLayers != null && !selectedLayers.isEmpty()) { 686 i.setServerProjections(getServerProjections(selectedLayers)); 687 } 688 return i; 689 } 690 691 /** 692 * Returns projections that server supports for provided list of layers. This will be intersection of projections 693 * defined for each layer 694 * 695 * @param selectedLayers list of layers 696 * @return projection code 697 */ 698 public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) { 699 if (selectedLayers.isEmpty()) { 700 return Collections.emptyList(); 701 } 702 Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs()); 703 704 // set intersect with all layers 705 for (LayerDetails ld: selectedLayers) { 706 proj.retainAll(ld.getCrs()); 707 } 708 return proj; 709 } 710 711 /** 712 * @param defaultLayers default layers that should select layer object 713 * @return collection of LayerDetails specified by DefaultLayers 714 */ 715 public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) { 716 Collection<String> layerNames = defaultLayers.stream().map(DefaultLayer::getLayerName).collect(Collectors.toList()); 717 return layers.stream() 718 .flatMap(LayerDetails::flattened) 719 .filter(x -> layerNames.contains(x.getName())) 720 .collect(Collectors.toList()); 721 } 722 723 /** 724 * @return title of this service 725 */ 726 public String getTitle() { 727 return title; 728 } 729}