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 CAPABILITIES_ROOT_130 = new QName(WMS_NS_URL, "WMS_Capabilities"); 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 * Returns root URL of services in this GetCapabilities. 248 * @return root URL of services in this GetCapabilities 249 */ 250 public String buildRootUrl() { 251 if (getMapUrl == null && capabilitiesUrl == null) { 252 return null; 253 } 254 if (getMapUrl != null) { 255 return getMapUrl; 256 } 257 258 URL serviceUrl = capabilitiesUrl; 259 StringBuilder a = new StringBuilder(serviceUrl.getProtocol()); 260 a.append("://").append(serviceUrl.getHost()); 261 if (serviceUrl.getPort() != -1) { 262 a.append(':').append(serviceUrl.getPort()); 263 } 264 a.append(serviceUrl.getPath()).append('?'); 265 if (serviceUrl.getQuery() != null) { 266 a.append(serviceUrl.getQuery()); 267 if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) { 268 a.append('&'); 269 } 270 } 271 return a.toString(); 272 } 273 274 /** 275 * Returns root URL of services without the GetCapabilities call. 276 * @return root URL of services without the GetCapabilities call 277 * @since 15209 278 */ 279 public String buildRootUrlWithoutCapabilities() { 280 return buildRootUrl() 281 .replace(CAPABILITIES_QUERY_STRING, "") 282 .replace(SERVICE_WMS, "") 283 .replace(REQUEST_GET_CAPABILITIES, "") 284 .replace("?&", "?"); 285 } 286 287 /** 288 * Returns URL for accessing GetMap service. String will contain following parameters: 289 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)}) 290 * * {width} - that needs to be replaced with width of the tile 291 * * {height} - that needs to be replaces with height of the tile 292 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates) 293 * 294 * Format of the response will be calculated using {@link #getPreferredFormat()} 295 * 296 * @param selectedLayers list of DefaultLayer selection of layers to be shown 297 * @param transparent whether returned images should contain transparent pixels (if supported by format) 298 * @return URL template for GetMap service containing 299 */ 300 public String buildGetMapUrl(List<DefaultLayer> selectedLayers, boolean transparent) { 301 return buildGetMapUrl( 302 getLayers(selectedLayers), 303 selectedLayers.stream().map(DefaultLayer::getStyle).collect(Collectors.toList()), 304 transparent); 305 } 306 307 /** 308 * Returns URL for accessing GetMap service. String will contain following parameters: 309 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)}) 310 * * {width} - that needs to be replaced with width of the tile 311 * * {height} - that needs to be replaces with height of the tile 312 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates) 313 * 314 * Format of the response will be calculated using {@link #getPreferredFormat()} 315 * 316 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()} 317 * @param selectedStyles selected styles for all selectedLayers 318 * @param transparent whether returned images should contain transparent pixels (if supported by format) 319 * @return URL template for GetMap service 320 * @see #buildGetMapUrl(List, boolean) 321 */ 322 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) { 323 return buildGetMapUrl(selectedLayers, selectedStyles, getPreferredFormat(), transparent); 324 } 325 326 /** 327 * Returns URL for accessing GetMap service. String will contain following parameters: 328 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)}) 329 * * {width} - that needs to be replaced with width of the tile 330 * * {height} - that needs to be replaces with height of the tile 331 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates) 332 * 333 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()} 334 * @param selectedStyles selected styles for all selectedLayers 335 * @param format format of the response - one of {@link #getFormats()} 336 * @param transparent whether returned images should contain transparent pixels (if supported by format) 337 * @return URL template for GetMap service 338 * @see #buildGetMapUrl(List, boolean) 339 * @since 15228 340 */ 341 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) { 342 return buildGetMapUrl( 343 selectedLayers.stream().map(LayerDetails::getName).collect(Collectors.toList()), 344 selectedStyles, 345 format, 346 transparent); 347 } 348 349 /** 350 * Returns URL for accessing GetMap service. String will contain following parameters: 351 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)}) 352 * * {width} - that needs to be replaced with width of the tile 353 * * {height} - that needs to be replaces with height of the tile 354 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates) 355 * 356 * @param selectedLayers selected layers as list of strings 357 * @param selectedStyles selected styles of layers as list of strings 358 * @param format format of the response - one of {@link #getFormats()} 359 * @param transparent whether returned images should contain transparent pixels (if supported by format) 360 * @return URL template for GetMap service 361 * @see #buildGetMapUrl(List, boolean) 362 */ 363 public String buildGetMapUrl(List<String> selectedLayers, 364 Collection<String> selectedStyles, 365 String format, 366 boolean transparent) { 367 368 Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(), 369 tr("Styles size {0} does not match layers size {1}"), 370 selectedStyles == null ? 0 : selectedStyles.size(), 371 selectedLayers.size()); 372 373 return buildRootUrlWithoutCapabilities() 374 + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "") 375 + "&VERSION=" + this.version + "&" + SERVICE_WMS + "&REQUEST=GetMap&LAYERS=" 376 + String.join(",", selectedLayers) 377 + "&STYLES=" 378 + (selectedStyles != null ? String.join(",", selectedStyles) : "") 379 + "&" 380 + (belowWMS130() ? "SRS" : "CRS") 381 + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}"; 382 } 383 384 private boolean tagEquals(QName a, QName b) { 385 boolean ret = a.equals(b); 386 if (ret) { 387 return ret; 388 } 389 390 if (belowWMS130()) { 391 return a.getLocalPart().equals(b.getLocalPart()); 392 } 393 394 return false; 395 } 396 397 private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException { 398 Logging.debug("Trying WMS GetCapabilities with url {0}", url); 399 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers). 400 setMaxAge(7 * CachedFile.DAYS). 401 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 402 getInputStream()) { 403 404 try { 405 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in); 406 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 407 if (event == XMLStreamReader.START_ELEMENT) { 408 if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) { 409 this.version = Utils.firstNotEmptyString("1.1.1", 410 reader.getAttributeValue(null, "version")); 411 } 412 if (tagEquals(CAPABILITIES_ROOT_130, reader.getName())) { 413 this.version = Utils.firstNotEmptyString("1.3.0", 414 reader.getAttributeValue(WMS_NS_URL, "version"), 415 reader.getAttributeValue(null, "version")); 416 } 417 if (tagEquals(QN_SERVICE, reader.getName())) { 418 parseService(reader); 419 } 420 421 if (tagEquals(QN_CAPABILITY, reader.getName())) { 422 parseCapability(reader); 423 } 424 } 425 } 426 } catch (XMLStreamException e) { 427 String content = new String(cf.getByteContent(), UTF_8); 428 cf.clear(); // if there is a problem with parsing of the file, remove it from the cache 429 throw new WMSGetCapabilitiesException(e, content); 430 } 431 } 432 } 433 434 private void parseService(XMLStreamReader reader) throws XMLStreamException { 435 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) { 436 this.title = reader.getElementText(); 437 // CHECKSTYLE.OFF: EmptyBlock 438 for (int event = reader.getEventType(); 439 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName())); 440 event = reader.next()) { 441 // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done 442 } 443 // CHECKSTYLE.ON: EmptyBlock 444 } 445 } 446 447 private void parseCapability(XMLStreamReader reader) throws XMLStreamException { 448 for (int event = reader.getEventType(); 449 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName())); 450 event = reader.next()) { 451 452 if (event == XMLStreamReader.START_ELEMENT) { 453 if (tagEquals(QN_REQUEST, reader.getName())) { 454 parseRequest(reader); 455 } 456 if (tagEquals(QN_LAYER, reader.getName())) { 457 parseLayer(reader, null); 458 } 459 } 460 } 461 } 462 463 private void parseRequest(XMLStreamReader reader) throws XMLStreamException { 464 String mode = ""; 465 String getMapUrl = ""; 466 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) { 467 for (int event = reader.getEventType(); 468 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName())); 469 event = reader.next()) { 470 471 if (event == XMLStreamReader.START_ELEMENT) { 472 if (tagEquals(QN_FORMAT, reader.getName())) { 473 String value = reader.getElementText(); 474 if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) { 475 this.formats.add(value); 476 } 477 } 478 if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader, 479 this::tagEquals, QN_HTTP, QN_GET)) { 480 mode = reader.getName().getLocalPart(); 481 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) { 482 getMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"); 483 } 484 // TODO should we handle also POST? 485 if ("GET".equalsIgnoreCase(mode) && getMapUrl != null && !"".equals(getMapUrl)) { 486 try { 487 String query = new URL(getMapUrl).getQuery(); 488 if (query == null) { 489 this.getMapUrl = getMapUrl + "?"; 490 } else { 491 this.getMapUrl = getMapUrl; 492 } 493 } catch (MalformedURLException e) { 494 throw new XMLStreamException(e); 495 } 496 } 497 } 498 } 499 } 500 } 501 } 502 503 private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException { 504 LayerDetails ret = new LayerDetails(parentLayer); 505 for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer 506 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName())); 507 event = reader.next()) { 508 509 if (event == XMLStreamReader.START_ELEMENT) { 510 if (tagEquals(QN_NAME, reader.getName())) { 511 ret.setName(reader.getElementText()); 512 } else if (tagEquals(QN_ABSTRACT, reader.getName())) { 513 ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader)); 514 } else if (tagEquals(QN_TITLE, reader.getName())) { 515 ret.setTitle(reader.getElementText()); 516 } else if (tagEquals(QN_CRS, reader.getName())) { 517 ret.addCrs(reader.getElementText()); 518 } else if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) { 519 ret.addCrs(reader.getElementText()); 520 } else if (tagEquals(QN_STYLE, reader.getName())) { 521 parseAndAddStyle(reader, ret); 522 } else if (tagEquals(QN_LAYER, reader.getName())) { 523 parseLayer(reader, ret); 524 } else if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()) && ret.getBounds() == null) { 525 ret.setBounds(parseExGeographic(reader)); 526 } else if (tagEquals(QN_BOUNDINGBOX, reader.getName())) { 527 Projection conv; 528 if (belowWMS130()) { 529 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS")); 530 } else { 531 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS")); 532 } 533 if (ret.getBounds() == null && conv != null) { 534 ret.setBounds(parseBoundingBox(reader, conv)); 535 } 536 } else if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130() && ret.getBounds() == null) { 537 ret.setBounds(parseBoundingBox(reader, null)); 538 } else { 539 // unknown tag, move to its end as it may have child elements 540 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader); 541 } 542 } 543 } 544 this.layers.add(ret); 545 } 546 547 /** 548 * Determines if this service operates at protocol level below WMS 1.3.0 549 * @return if this service operates at protocol level below 1.3.0 550 */ 551 public boolean belowWMS130() { 552 return "1.1.1".equals(version) || "1.1".equals(version) || "1.0".equals(version); 553 } 554 555 private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException { 556 String name = null; 557 String title = null; 558 for (int event = reader.getEventType(); 559 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName())); 560 event = reader.next()) { 561 if (event == XMLStreamReader.START_ELEMENT) { 562 if (tagEquals(QN_NAME, reader.getName())) { 563 name = reader.getElementText(); 564 } 565 if (tagEquals(QN_TITLE, reader.getName())) { 566 title = reader.getElementText(); 567 } 568 } 569 } 570 if (name == null) { 571 name = ""; 572 } 573 ld.addStyle(name, title); 574 } 575 576 private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException { 577 String minx = null, maxx = null, maxy = null, miny = null; 578 579 for (int event = reader.getEventType(); 580 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName())); 581 event = reader.next()) { 582 if (event == XMLStreamReader.START_ELEMENT) { 583 if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) { 584 minx = reader.getElementText(); 585 } 586 587 if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) { 588 maxx = reader.getElementText(); 589 } 590 591 if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) { 592 miny = reader.getElementText(); 593 } 594 595 if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) { 596 maxy = reader.getElementText(); 597 } 598 } 599 } 600 return parseBBox(null, miny, minx, maxy, maxx); 601 } 602 603 private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) { 604 UnaryOperator<String> attrGetter = tag -> belowWMS130() ? 605 reader.getAttributeValue(null, tag) 606 : reader.getAttributeValue(WMS_NS_URL, tag); 607 608 return parseBBox( 609 conv, 610 attrGetter.apply("miny"), 611 attrGetter.apply("minx"), 612 attrGetter.apply("maxy"), 613 attrGetter.apply("maxx") 614 ); 615 } 616 617 private static Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) { 618 if (miny == null || minx == null || maxy == null || maxx == null) { 619 return null; 620 } 621 if (conv != null) { 622 return new Bounds( 623 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))), 624 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy))) 625 ); 626 } 627 return new Bounds( 628 getDecimalDegree(miny), 629 getDecimalDegree(minx), 630 getDecimalDegree(maxy), 631 getDecimalDegree(maxx) 632 ); 633 } 634 635 private static double getDecimalDegree(String value) { 636 // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server) 637 return Double.parseDouble(value.replace(',', '.')); 638 } 639 640 private static String normalizeUrl(String serviceUrlStr) throws MalformedURLException { 641 URL getCapabilitiesUrl = null; 642 String ret = null; 643 644 if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) { 645 // If the url doesn't already have GetCapabilities, add it in 646 getCapabilitiesUrl = new URL(serviceUrlStr); 647 if (getCapabilitiesUrl.getQuery() == null) { 648 ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING; 649 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) { 650 ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING; 651 } else { 652 ret = serviceUrlStr + CAPABILITIES_QUERY_STRING; 653 } 654 } else { 655 // Otherwise assume it's a good URL and let the subsequent error 656 // handling systems deal with problems 657 ret = serviceUrlStr; 658 } 659 return ret; 660 } 661 662 private static boolean isImageFormatSupportedWarn(String format) { 663 boolean isFormatSupported = isImageFormatSupported(format); 664 if (!isFormatSupported) { 665 Logging.info("Skipping unsupported image format {0}", format); 666 } 667 return isFormatSupported; 668 } 669 670 static boolean isImageFormatSupported(final String format) { 671 return ImageIO.getImageReadersByMIMEType(format).hasNext() 672 // handles image/tiff image/tiff8 image/geotiff image/geotiff8 673 || isImageFormatSupported(format, "tiff", "geotiff") 674 || isImageFormatSupported(format, "png") 675 || isImageFormatSupported(format, "svg") 676 || isImageFormatSupported(format, "bmp"); 677 } 678 679 static boolean isImageFormatSupported(String format, String... mimeFormats) { 680 for (String mime : mimeFormats) { 681 if (format.startsWith("image/" + mime)) { 682 return ImageIO.getImageReadersBySuffix(mimeFormats[0]).hasNext(); 683 } 684 } 685 return false; 686 } 687 688 static boolean imageFormatHasTransparency(final String format) { 689 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif") 690 || format.startsWith("image/svg") || format.startsWith("image/tiff")); 691 } 692 693 /** 694 * Creates ImageryInfo object from this GetCapabilities document 695 * 696 * @param name name of imagery layer 697 * @param selectedLayers layers which are to be used by this imagery layer 698 * @param selectedStyles styles that should be used for selectedLayers 699 * @param format format of the response - one of {@link #getFormats()} 700 * @param transparent if layer should be transparent 701 * @return ImageryInfo object 702 * @since 15228 703 */ 704 public ImageryInfo toImageryInfo( 705 String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) { 706 ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, format, transparent)); 707 if (!selectedLayers.isEmpty()) { 708 i.setServerProjections(getServerProjections(selectedLayers)); 709 } 710 return i; 711 } 712 713 /** 714 * Returns projections that server supports for provided list of layers. This will be intersection of projections 715 * defined for each layer 716 * 717 * @param selectedLayers list of layers 718 * @return projection code 719 */ 720 public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) { 721 if (selectedLayers.isEmpty()) { 722 return Collections.emptyList(); 723 } 724 Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs()); 725 726 // set intersect with all layers 727 for (LayerDetails ld: selectedLayers) { 728 proj.retainAll(ld.getCrs()); 729 } 730 return proj; 731 } 732 733 /** 734 * Returns collection of LayerDetails specified by defaultLayers. 735 * @param defaultLayers default layers that should select layer object 736 * @return collection of LayerDetails specified by defaultLayers 737 */ 738 public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) { 739 Collection<String> layerNames = defaultLayers.stream().map(DefaultLayer::getLayerName).collect(Collectors.toList()); 740 return layers.stream() 741 .flatMap(LayerDetails::flattened) 742 .filter(x -> layerNames.contains(x.getName())) 743 .collect(Collectors.toList()); 744 } 745 746 /** 747 * Returns title of this service. 748 * @return title of this service 749 */ 750 public String getTitle() { 751 return title; 752 } 753}