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