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.util.ArrayList; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.HashSet; 016import java.util.List; 017import java.util.Map; 018import java.util.Set; 019import java.util.concurrent.ConcurrentHashMap; 020import java.util.function.UnaryOperator; 021import java.util.regex.Pattern; 022import java.util.stream.Collectors; 023 024import javax.imageio.ImageIO; 025import javax.xml.namespace.QName; 026import javax.xml.stream.XMLStreamException; 027import javax.xml.stream.XMLStreamReader; 028 029import org.openstreetmap.josm.data.Bounds; 030import org.openstreetmap.josm.data.coor.EastNorth; 031import org.openstreetmap.josm.data.imagery.DefaultLayer; 032import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper; 033import org.openstreetmap.josm.data.imagery.ImageryInfo; 034import org.openstreetmap.josm.data.imagery.LayerDetails; 035import org.openstreetmap.josm.data.projection.Projection; 036import org.openstreetmap.josm.data.projection.Projections; 037import org.openstreetmap.josm.io.CachedFile; 038import org.openstreetmap.josm.tools.Logging; 039import org.openstreetmap.josm.tools.Utils; 040 041/** 042 * This class represents the capabilities of a WMS imagery server. 043 */ 044public class WMSImagery { 045 046 private static final String CAPABILITIES_QUERY_STRING = "SERVICE=WMS&REQUEST=GetCapabilities"; 047 048 /** 049 * WMS namespace address 050 */ 051 public static final String WMS_NS_URL = "http://www.opengis.net/wms"; 052 053 // CHECKSTYLE.OFF: SingleSpaceSeparator 054 // WMS 1.0 - 1.3.0 055 private static final QName CAPABILITITES_ROOT_130 = new QName("WMS_Capabilities", WMS_NS_URL); 056 private static final QName QN_ABSTRACT = new QName(WMS_NS_URL, "Abstract"); 057 private static final QName QN_CAPABILITY = new QName(WMS_NS_URL, "Capability"); 058 private static final QName QN_CRS = new QName(WMS_NS_URL, "CRS"); 059 private static final QName QN_DCPTYPE = new QName(WMS_NS_URL, "DCPType"); 060 private static final QName QN_FORMAT = new QName(WMS_NS_URL, "Format"); 061 private static final QName QN_GET = new QName(WMS_NS_URL, "Get"); 062 private static final QName QN_GETMAP = new QName(WMS_NS_URL, "GetMap"); 063 private static final QName QN_HTTP = new QName(WMS_NS_URL, "HTTP"); 064 private static final QName QN_LAYER = new QName(WMS_NS_URL, "Layer"); 065 private static final QName QN_NAME = new QName(WMS_NS_URL, "Name"); 066 private static final QName QN_REQUEST = new QName(WMS_NS_URL, "Request"); 067 private static final QName QN_SERVICE = new QName(WMS_NS_URL, "Service"); 068 private static final QName QN_STYLE = new QName(WMS_NS_URL, "Style"); 069 private static final QName QN_TITLE = new QName(WMS_NS_URL, "Title"); 070 private static final QName QN_BOUNDINGBOX = new QName(WMS_NS_URL, "BoundingBox"); 071 private static final QName QN_EX_GEOGRAPHIC_BBOX = new QName(WMS_NS_URL, "EX_GeographicBoundingBox"); 072 private static final QName QN_WESTBOUNDLONGITUDE = new QName(WMS_NS_URL, "westBoundLongitude"); 073 private static final QName QN_EASTBOUNDLONGITUDE = new QName(WMS_NS_URL, "eastBoundLongitude"); 074 private static final QName QN_SOUTHBOUNDLATITUDE = new QName(WMS_NS_URL, "southBoundLatitude"); 075 private static final QName QN_NORTHBOUNDLATITUDE = new QName(WMS_NS_URL, "northBoundLatitude"); 076 private static final QName QN_ONLINE_RESOURCE = new QName(WMS_NS_URL, "OnlineResource"); 077 078 // WMS 1.1 - 1.1.1 079 private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities"); 080 private static final QName QN_SRS = new QName("SRS"); 081 private static final QName QN_LATLONBOUNDINGBOX = new QName("LatLonBoundingBox"); 082 083 // CHECKSTYLE.ON: SingleSpaceSeparator 084 085 /** 086 * An exception that is thrown if there was an error while getting the capabilities of the WMS server. 087 */ 088 public static class WMSGetCapabilitiesException extends Exception { 089 private final String incomingData; 090 091 /** 092 * Constructs a new {@code WMSGetCapabilitiesException} 093 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method) 094 * @param incomingData the answer from WMS server 095 */ 096 public WMSGetCapabilitiesException(Throwable cause, String incomingData) { 097 super(cause); 098 this.incomingData = incomingData; 099 } 100 101 /** 102 * Constructs a new {@code WMSGetCapabilitiesException} 103 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method 104 * @param incomingData the answer from the server 105 * @since 10520 106 */ 107 public WMSGetCapabilitiesException(String message, String incomingData) { 108 super(message); 109 this.incomingData = incomingData; 110 } 111 112 /** 113 * The data that caused this exception. 114 * @return The server response to the capabilities request. 115 */ 116 public String getIncomingData() { 117 return incomingData; 118 } 119 } 120 121 private final Map<String, String> headers = new ConcurrentHashMap<>(); 122 private String version = "1.1.1"; // default version 123 private String getMapUrl; 124 private URL capabilitiesUrl; 125 private final List<String> formats = new ArrayList<>(); 126 private List<LayerDetails> layers = new ArrayList<>(); 127 128 private String title; 129 130 /** 131 * Make getCapabilities request towards given URL 132 * @param url service url 133 * @throws IOException when connection error when fetching get capabilities document 134 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document 135 */ 136 public WMSImagery(String url) throws IOException, WMSGetCapabilitiesException { 137 this(url, null); 138 } 139 140 /** 141 * Make getCapabilities request towards given URL using headers 142 * @param url service url 143 * @param headers HTTP headers to be sent with request 144 * @throws IOException when connection error when fetching get capabilities document 145 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document 146 */ 147 public WMSImagery(String url, Map<String, String> headers) throws IOException, WMSGetCapabilitiesException { 148 if (headers != null) { 149 this.headers.putAll(headers); 150 } 151 152 IOException savedExc = null; 153 String workingAddress = null; 154 url_search: 155 for (String z: new String[]{ 156 normalizeUrl(url), 157 url, 158 url + CAPABILITIES_QUERY_STRING, 159 }) { 160 for (String ver: new String[]{"", "&VERSION=1.3.0", "&VERSION=1.1.1"}) { 161 try { 162 attemptGetCapabilities(z + ver); 163 workingAddress = z; 164 calculateChildren(); 165 // clear saved exception - we've got something working 166 savedExc = null; 167 break url_search; 168 } catch (IOException e) { 169 savedExc = e; 170 Logging.warn(e); 171 } 172 } 173 } 174 175 if (workingAddress != null) { 176 try { 177 capabilitiesUrl = new URL(workingAddress); 178 } catch (MalformedURLException e) { 179 if (savedExc == null) { 180 savedExc = e; 181 } 182 try { 183 capabilitiesUrl = new File(workingAddress).toURI().toURL(); 184 } catch (MalformedURLException e1) { // NOPMD 185 // do nothing, raise original exception 186 Logging.trace(e1); 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 212 */ 213 public List<LayerDetails> getLayers() { 214 return Collections.unmodifiableList(layers); 215 } 216 217 /** 218 * Returns the list of supported formats. 219 * @return the list of supported formats 220 */ 221 public Collection<String> getFormats() { 222 return Collections.unmodifiableList(formats); 223 } 224 225 /** 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")) { 233 return "image/jpeg"; 234 } else if (formats.isEmpty()) { 235 return null; 236 } else { 237 return formats.get(0); 238 } 239 } 240 241 /** 242 * @return root URL of services in this GetCapabilities 243 */ 244 public String buildRootUrl() { 245 if (getMapUrl == null && capabilitiesUrl == null) { 246 return null; 247 } 248 if (getMapUrl != null) { 249 return getMapUrl; 250 } 251 252 URL serviceUrl = capabilitiesUrl; 253 StringBuilder a = new StringBuilder(serviceUrl.getProtocol()); 254 a.append("://").append(serviceUrl.getHost()); 255 if (serviceUrl.getPort() != -1) { 256 a.append(':').append(serviceUrl.getPort()); 257 } 258 a.append(serviceUrl.getPath()).append('?'); 259 if (serviceUrl.getQuery() != null) { 260 a.append(serviceUrl.getQuery()); 261 if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) { 262 a.append('&'); 263 } 264 } 265 return a.toString(); 266 } 267 268 /** 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(DefaultLayer::getStyle).collect(Collectors.toList()), 285 transparent); 286 } 287 288 /** 289 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()} 290 * @param selectedStyles selected styles for all selectedLayers 291 * @param transparent whether returned images should contain transparent pixels (if supported by format) 292 * @return URL template for GetMap service 293 * @see #buildGetMapUrl(List, boolean) 294 */ 295 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) { 296 return buildGetMapUrl( 297 selectedLayers.stream().map(LayerDetails::getName).collect(Collectors.toList()), 298 selectedStyles, 299 getPreferredFormat(), 300 transparent); 301 } 302 303 /** 304 * @param selectedLayers selected layers as list of strings 305 * @param selectedStyles selected styles of layers as list of strings 306 * @param format format of the response - one of {@link #getFormats()} 307 * @param transparent whether returned images should contain transparent pixels (if supported by format) 308 * @return URL template for GetMap service 309 * @see #buildGetMapUrl(List, boolean) 310 */ 311 public String buildGetMapUrl(List<String> selectedLayers, 312 Collection<String> selectedStyles, 313 String format, 314 boolean transparent) { 315 316 Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(), 317 tr("Styles size {0} does not match layers size {1}"), 318 selectedStyles == null ? 0 : selectedStyles.size(), 319 selectedLayers.size()); 320 321 return buildRootUrl() + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "") 322 + "&VERSION=" + this.version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS=" 323 + selectedLayers.stream().collect(Collectors.joining(",")) 324 + "&STYLES=" 325 + (selectedStyles != null ? Utils.join(",", selectedStyles) : "") 326 + "&" 327 + (belowWMS130() ? "SRS" : "CRS") 328 + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}"; 329 } 330 331 private boolean tagEquals(QName a, QName b) { 332 boolean ret = a.equals(b); 333 if (ret) { 334 return ret; 335 } 336 337 if (belowWMS130()) { 338 return a.getLocalPart().equals(b.getLocalPart()); 339 } 340 341 return false; 342 } 343 344 private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException { 345 Logging.debug("Trying WMS getcapabilities with url {0}", url); 346 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers). 347 setMaxAge(7 * CachedFile.DAYS). 348 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince). 349 getInputStream()) { 350 351 try { 352 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in); 353 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) { 354 if (event == XMLStreamReader.START_ELEMENT) { 355 if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) { 356 // version 1.1.1 357 this.version = reader.getAttributeValue(null, "version"); 358 if (this.version == null) { 359 this.version = "1.1.1"; 360 } 361 } 362 if (tagEquals(CAPABILITITES_ROOT_130, reader.getName())) { 363 this.version = reader.getAttributeValue(WMS_NS_URL, "version"); 364 } 365 if (tagEquals(QN_SERVICE, reader.getName())) { 366 parseService(reader); 367 } 368 369 if (tagEquals(QN_CAPABILITY, reader.getName())) { 370 parseCapability(reader); 371 } 372 } 373 } 374 } catch (XMLStreamException e) { 375 String content = new String(cf.getByteContent(), UTF_8); 376 cf.clear(); // if there is a problem with parsing of the file, remove it from the cache 377 throw new WMSGetCapabilitiesException(e, content); 378 } 379 } 380 } 381 382 private void parseService(XMLStreamReader reader) throws XMLStreamException { 383 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) { 384 this.title = reader.getElementText(); 385 // CHECKSTYLE.OFF: EmptyBlock 386 for (int event = reader.getEventType(); 387 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName())); 388 event = reader.next()) { 389 // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done 390 } 391 // CHECKSTYLE.ON: EmptyBlock 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 try { 435 String query = (new URL(getMapUrl)).getQuery(); 436 if (query == null) { 437 this.getMapUrl = getMapUrl + "?"; 438 } else { 439 this.getMapUrl = getMapUrl; 440 } 441 } catch (MalformedURLException e) { 442 throw new XMLStreamException(e); 443 } 444 } 445 } 446 } 447 } 448 } 449 } 450 451 private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException { 452 LayerDetails ret = new LayerDetails(parentLayer); 453 for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer 454 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName())); 455 event = reader.next()) { 456 457 if (event == XMLStreamReader.START_ELEMENT) { 458 if (tagEquals(QN_NAME, reader.getName())) { 459 ret.setName(reader.getElementText()); 460 } 461 if (tagEquals(QN_ABSTRACT, reader.getName())) { 462 ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader)); 463 } 464 if (tagEquals(QN_TITLE, reader.getName())) { 465 ret.setTitle(reader.getElementText()); 466 } 467 if (tagEquals(QN_CRS, reader.getName())) { 468 ret.addCrs(reader.getElementText()); 469 } 470 if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) { 471 ret.addCrs(reader.getElementText()); 472 } 473 if (tagEquals(QN_STYLE, reader.getName())) { 474 parseAndAddStyle(reader, ret); 475 } 476 if (tagEquals(QN_LAYER, reader.getName())) { 477 parseLayer(reader, ret); 478 } 479 if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()) && ret.getBounds() == null) { 480 ret.setBounds(parseExGeographic(reader)); 481 } 482 if (tagEquals(QN_BOUNDINGBOX, reader.getName())) { 483 Projection conv; 484 if (belowWMS130()) { 485 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS")); 486 } else { 487 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS")); 488 } 489 if (ret.getBounds() == null && conv != null) { 490 ret.setBounds(parseBoundingBox(reader, conv)); 491 } 492 } 493 if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130() && ret.getBounds() == null) { 494 ret.setBounds(parseBoundingBox(reader, null)); 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 "1.1.1".equals(version) || "1.1".equals(version) || "1.0".equals(version); 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 UnaryOperator<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 static 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 return 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 private static String normalizeUrl(String serviceUrlStr) throws MalformedURLException { 594 URL getCapabilitiesUrl = null; 595 String ret = null; 596 597 if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) { 598 // If the url doesn't already have GetCapabilities, add it in 599 getCapabilitiesUrl = new URL(serviceUrlStr); 600 if (getCapabilitiesUrl.getQuery() == null) { 601 ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING; 602 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) { 603 ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING; 604 } else { 605 ret = serviceUrlStr + CAPABILITIES_QUERY_STRING; 606 } 607 } else { 608 // Otherwise assume it's a good URL and let the subsequent error 609 // handling systems deal with problems 610 ret = serviceUrlStr; 611 } 612 return ret; 613 } 614 615 private static boolean isImageFormatSupportedWarn(String format) { 616 boolean isFormatSupported = isImageFormatSupported(format); 617 if (!isFormatSupported) { 618 Logging.info("Skipping unsupported image format {0}", format); 619 } 620 return isFormatSupported; 621 } 622 623 static boolean isImageFormatSupported(final String format) { 624 return ImageIO.getImageReadersByMIMEType(format).hasNext() 625 // handles image/tiff image/tiff8 image/geotiff image/geotiff8 626 || isImageFormatSupported(format, "tiff", "geotiff") 627 || isImageFormatSupported(format, "png") 628 || isImageFormatSupported(format, "svg") 629 || isImageFormatSupported(format, "bmp"); 630 } 631 632 static boolean isImageFormatSupported(String format, String... mimeFormats) { 633 for (String mime : mimeFormats) { 634 if (format.startsWith("image/" + mime)) { 635 return ImageIO.getImageReadersBySuffix(mimeFormats[0]).hasNext(); 636 } 637 } 638 return false; 639 } 640 641 static boolean imageFormatHasTransparency(final String format) { 642 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif") 643 || format.startsWith("image/svg") || format.startsWith("image/tiff")); 644 } 645 646 /** 647 * Creates ImageryInfo object from this GetCapabilities document 648 * 649 * @param name name of imagery layer 650 * @param selectedLayers layers which are to be used by this imagery layer 651 * @param selectedStyles styles that should be used for selectedLayers 652 * @param transparent if layer should be transparent 653 * @return ImageryInfo object 654 */ 655 public ImageryInfo toImageryInfo(String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) { 656 ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, transparent)); 657 if (selectedLayers != null && !selectedLayers.isEmpty()) { 658 i.setServerProjections(getServerProjections(selectedLayers)); 659 } 660 return i; 661 } 662 663 /** 664 * Returns projections that server supports for provided list of layers. This will be intersection of projections 665 * defined for each layer 666 * 667 * @param selectedLayers list of layers 668 * @return projection code 669 */ 670 public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) { 671 if (selectedLayers.isEmpty()) { 672 return Collections.emptyList(); 673 } 674 Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs()); 675 676 // set intersect with all layers 677 for (LayerDetails ld: selectedLayers) { 678 proj.retainAll(ld.getCrs()); 679 } 680 return proj; 681 } 682 683 /** 684 * @param defaultLayers default layers that should select layer object 685 * @return collection of LayerDetails specified by DefaultLayers 686 */ 687 public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) { 688 Collection<String> layerNames = defaultLayers.stream().map(DefaultLayer::getLayerName).collect(Collectors.toList()); 689 return layers.stream() 690 .flatMap(LayerDetails::flattened) 691 .filter(x -> layerNames.contains(x.getName())) 692 .collect(Collectors.toList()); 693 } 694 695 /** 696 * @return title of this service 697 */ 698 public String getTitle() { 699 return title; 700 } 701}