source: josm/trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java@ 16412

Last change on this file since 16412 was 16412, checked in by simon04, 4 years ago

fix #19193 - WMSImagery: fix parsing of version 1.3.0 capabilities

For the constant CAPABILITIES_ROOT_130, the namespaceURI and localPart were interchanged.

  • Property svn:eol-style set to native
File size: 31.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.imagery;
3
4import static java.nio.charset.StandardCharsets.UTF_8;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.File;
8import java.io.IOException;
9import java.io.InputStream;
10import java.net.MalformedURLException;
11import java.net.URL;
12import java.nio.file.InvalidPathException;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.List;
18import java.util.Map;
19import java.util.Set;
20import java.util.concurrent.ConcurrentHashMap;
21import java.util.function.UnaryOperator;
22import java.util.regex.Pattern;
23import java.util.stream.Collectors;
24
25import javax.imageio.ImageIO;
26import javax.xml.namespace.QName;
27import javax.xml.stream.XMLStreamException;
28import javax.xml.stream.XMLStreamReader;
29
30import org.openstreetmap.josm.data.Bounds;
31import org.openstreetmap.josm.data.coor.EastNorth;
32import org.openstreetmap.josm.data.imagery.DefaultLayer;
33import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper;
34import org.openstreetmap.josm.data.imagery.ImageryInfo;
35import org.openstreetmap.josm.data.imagery.LayerDetails;
36import org.openstreetmap.josm.data.projection.Projection;
37import org.openstreetmap.josm.data.projection.Projections;
38import org.openstreetmap.josm.io.CachedFile;
39import org.openstreetmap.josm.tools.Logging;
40import org.openstreetmap.josm.tools.Utils;
41
42/**
43 * This class represents the capabilities of a WMS imagery server.
44 */
45public class WMSImagery {
46
47 private static final String SERVICE_WMS = "SERVICE=WMS";
48 private static final String REQUEST_GET_CAPABILITIES = "REQUEST=GetCapabilities";
49 private static final String CAPABILITIES_QUERY_STRING = SERVICE_WMS + "&" + REQUEST_GET_CAPABILITIES;
50
51 /**
52 * WMS namespace address
53 */
54 public static final String WMS_NS_URL = "http://www.opengis.net/wms";
55
56 // CHECKSTYLE.OFF: SingleSpaceSeparator
57 // WMS 1.0 - 1.3.0
58 private static final QName CAPABILITIES_ROOT_130 = new QName(WMS_NS_URL, "WMS_Capabilities");
59 private static final QName QN_ABSTRACT = new QName(WMS_NS_URL, "Abstract");
60 private static final QName QN_CAPABILITY = new QName(WMS_NS_URL, "Capability");
61 private static final QName QN_CRS = new QName(WMS_NS_URL, "CRS");
62 private static final QName QN_DCPTYPE = new QName(WMS_NS_URL, "DCPType");
63 private static final QName QN_FORMAT = new QName(WMS_NS_URL, "Format");
64 private static final QName QN_GET = new QName(WMS_NS_URL, "Get");
65 private static final QName QN_GETMAP = new QName(WMS_NS_URL, "GetMap");
66 private static final QName QN_HTTP = new QName(WMS_NS_URL, "HTTP");
67 private static final QName QN_LAYER = new QName(WMS_NS_URL, "Layer");
68 private static final QName QN_NAME = new QName(WMS_NS_URL, "Name");
69 private static final QName QN_REQUEST = new QName(WMS_NS_URL, "Request");
70 private static final QName QN_SERVICE = new QName(WMS_NS_URL, "Service");
71 private static final QName QN_STYLE = new QName(WMS_NS_URL, "Style");
72 private static final QName QN_TITLE = new QName(WMS_NS_URL, "Title");
73 private static final QName QN_BOUNDINGBOX = new QName(WMS_NS_URL, "BoundingBox");
74 private static final QName QN_EX_GEOGRAPHIC_BBOX = new QName(WMS_NS_URL, "EX_GeographicBoundingBox");
75 private static final QName QN_WESTBOUNDLONGITUDE = new QName(WMS_NS_URL, "westBoundLongitude");
76 private static final QName QN_EASTBOUNDLONGITUDE = new QName(WMS_NS_URL, "eastBoundLongitude");
77 private static final QName QN_SOUTHBOUNDLATITUDE = new QName(WMS_NS_URL, "southBoundLatitude");
78 private static final QName QN_NORTHBOUNDLATITUDE = new QName(WMS_NS_URL, "northBoundLatitude");
79 private static final QName QN_ONLINE_RESOURCE = new QName(WMS_NS_URL, "OnlineResource");
80
81 // WMS 1.1 - 1.1.1
82 private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities");
83 private static final QName QN_SRS = new QName("SRS");
84 private static final QName QN_LATLONBOUNDINGBOX = new QName("LatLonBoundingBox");
85
86 // CHECKSTYLE.ON: SingleSpaceSeparator
87
88 /**
89 * An exception that is thrown if there was an error while getting the capabilities of the WMS server.
90 */
91 public static class WMSGetCapabilitiesException extends Exception {
92 private final String incomingData;
93
94 /**
95 * Constructs a new {@code WMSGetCapabilitiesException}
96 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method)
97 * @param incomingData the answer from WMS server
98 */
99 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 + String.join(",", selectedLayers)
355 + "&STYLES="
356 + (selectedStyles != null ? String.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 this.version = Utils.firstNotEmptyString("1.1.1",
388 reader.getAttributeValue(null, "version"));
389 }
390 if (tagEquals(CAPABILITIES_ROOT_130, reader.getName())) {
391 this.version = Utils.firstNotEmptyString("1.3.0",
392 reader.getAttributeValue(WMS_NS_URL, "version"),
393 reader.getAttributeValue(null, "version"));
394 }
395 if (tagEquals(QN_SERVICE, reader.getName())) {
396 parseService(reader);
397 }
398
399 if (tagEquals(QN_CAPABILITY, reader.getName())) {
400 parseCapability(reader);
401 }
402 }
403 }
404 } catch (XMLStreamException e) {
405 String content = new String(cf.getByteContent(), UTF_8);
406 cf.clear(); // if there is a problem with parsing of the file, remove it from the cache
407 throw new WMSGetCapabilitiesException(e, content);
408 }
409 }
410 }
411
412 private void parseService(XMLStreamReader reader) throws XMLStreamException {
413 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) {
414 this.title = reader.getElementText();
415 // CHECKSTYLE.OFF: EmptyBlock
416 for (int event = reader.getEventType();
417 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName()));
418 event = reader.next()) {
419 // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done
420 }
421 // CHECKSTYLE.ON: EmptyBlock
422 }
423 }
424
425 private void parseCapability(XMLStreamReader reader) throws XMLStreamException {
426 for (int event = reader.getEventType();
427 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName()));
428 event = reader.next()) {
429
430 if (event == XMLStreamReader.START_ELEMENT) {
431 if (tagEquals(QN_REQUEST, reader.getName())) {
432 parseRequest(reader);
433 }
434 if (tagEquals(QN_LAYER, reader.getName())) {
435 parseLayer(reader, null);
436 }
437 }
438 }
439 }
440
441 private void parseRequest(XMLStreamReader reader) throws XMLStreamException {
442 String mode = "";
443 String getMapUrl = "";
444 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) {
445 for (int event = reader.getEventType();
446 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName()));
447 event = reader.next()) {
448
449 if (event == XMLStreamReader.START_ELEMENT) {
450 if (tagEquals(QN_FORMAT, reader.getName())) {
451 String value = reader.getElementText();
452 if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) {
453 this.formats.add(value);
454 }
455 }
456 if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader,
457 this::tagEquals, QN_HTTP, QN_GET)) {
458 mode = reader.getName().getLocalPart();
459 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) {
460 getMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
461 }
462 // TODO should we handle also POST?
463 if ("GET".equalsIgnoreCase(mode) && getMapUrl != null && !"".equals(getMapUrl)) {
464 try {
465 String query = (new URL(getMapUrl)).getQuery();
466 if (query == null) {
467 this.getMapUrl = getMapUrl + "?";
468 } else {
469 this.getMapUrl = getMapUrl;
470 }
471 } catch (MalformedURLException e) {
472 throw new XMLStreamException(e);
473 }
474 }
475 }
476 }
477 }
478 }
479 }
480
481 private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException {
482 LayerDetails ret = new LayerDetails(parentLayer);
483 for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer
484 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName()));
485 event = reader.next()) {
486
487 if (event == XMLStreamReader.START_ELEMENT) {
488 if (tagEquals(QN_NAME, reader.getName())) {
489 ret.setName(reader.getElementText());
490 } else if (tagEquals(QN_ABSTRACT, reader.getName())) {
491 ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader));
492 } else if (tagEquals(QN_TITLE, reader.getName())) {
493 ret.setTitle(reader.getElementText());
494 } else if (tagEquals(QN_CRS, reader.getName())) {
495 ret.addCrs(reader.getElementText());
496 } else if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) {
497 ret.addCrs(reader.getElementText());
498 } else if (tagEquals(QN_STYLE, reader.getName())) {
499 parseAndAddStyle(reader, ret);
500 } else if (tagEquals(QN_LAYER, reader.getName())) {
501 parseLayer(reader, ret);
502 } else if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()) && ret.getBounds() == null) {
503 ret.setBounds(parseExGeographic(reader));
504 } else if (tagEquals(QN_BOUNDINGBOX, reader.getName())) {
505 Projection conv;
506 if (belowWMS130()) {
507 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS"));
508 } else {
509 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS"));
510 }
511 if (ret.getBounds() == null && conv != null) {
512 ret.setBounds(parseBoundingBox(reader, conv));
513 }
514 } else if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130() && ret.getBounds() == null) {
515 ret.setBounds(parseBoundingBox(reader, null));
516 } else {
517 // unknown tag, move to its end as it may have child elements
518 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
519 }
520 }
521 }
522 this.layers.add(ret);
523 }
524
525 /**
526 * @return if this service operates at protocol level below 1.3.0
527 */
528 public boolean belowWMS130() {
529 return "1.1.1".equals(version) || "1.1".equals(version) || "1.0".equals(version);
530 }
531
532 private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException {
533 String name = null;
534 String title = null;
535 for (int event = reader.getEventType();
536 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName()));
537 event = reader.next()) {
538 if (event == XMLStreamReader.START_ELEMENT) {
539 if (tagEquals(QN_NAME, reader.getName())) {
540 name = reader.getElementText();
541 }
542 if (tagEquals(QN_TITLE, reader.getName())) {
543 title = reader.getElementText();
544 }
545 }
546 }
547 if (name == null) {
548 name = "";
549 }
550 ld.addStyle(name, title);
551 }
552
553 private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException {
554 String minx = null, maxx = null, maxy = null, miny = null;
555
556 for (int event = reader.getEventType();
557 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()));
558 event = reader.next()) {
559 if (event == XMLStreamReader.START_ELEMENT) {
560 if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) {
561 minx = reader.getElementText();
562 }
563
564 if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) {
565 maxx = reader.getElementText();
566 }
567
568 if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) {
569 miny = reader.getElementText();
570 }
571
572 if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) {
573 maxy = reader.getElementText();
574 }
575 }
576 }
577 return parseBBox(null, miny, minx, maxy, maxx);
578 }
579
580 private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) {
581 UnaryOperator<String> attrGetter = tag -> belowWMS130() ?
582 reader.getAttributeValue(null, tag)
583 : reader.getAttributeValue(WMS_NS_URL, tag);
584
585 return parseBBox(
586 conv,
587 attrGetter.apply("miny"),
588 attrGetter.apply("minx"),
589 attrGetter.apply("maxy"),
590 attrGetter.apply("maxx")
591 );
592 }
593
594 private static Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) {
595 if (miny == null || minx == null || maxy == null || maxx == null) {
596 return null;
597 }
598 if (conv != null) {
599 return new Bounds(
600 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))),
601 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy)))
602 );
603 }
604 return new Bounds(
605 getDecimalDegree(miny),
606 getDecimalDegree(minx),
607 getDecimalDegree(maxy),
608 getDecimalDegree(maxx)
609 );
610 }
611
612 private static double getDecimalDegree(String value) {
613 // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
614 return Double.parseDouble(value.replace(',', '.'));
615 }
616
617 private static String normalizeUrl(String serviceUrlStr) throws MalformedURLException {
618 URL getCapabilitiesUrl = null;
619 String ret = null;
620
621 if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
622 // If the url doesn't already have GetCapabilities, add it in
623 getCapabilitiesUrl = new URL(serviceUrlStr);
624 if (getCapabilitiesUrl.getQuery() == null) {
625 ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING;
626 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
627 ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING;
628 } else {
629 ret = serviceUrlStr + CAPABILITIES_QUERY_STRING;
630 }
631 } else {
632 // Otherwise assume it's a good URL and let the subsequent error
633 // handling systems deal with problems
634 ret = serviceUrlStr;
635 }
636 return ret;
637 }
638
639 private static boolean isImageFormatSupportedWarn(String format) {
640 boolean isFormatSupported = isImageFormatSupported(format);
641 if (!isFormatSupported) {
642 Logging.info("Skipping unsupported image format {0}", format);
643 }
644 return isFormatSupported;
645 }
646
647 static boolean isImageFormatSupported(final String format) {
648 return ImageIO.getImageReadersByMIMEType(format).hasNext()
649 // handles image/tiff image/tiff8 image/geotiff image/geotiff8
650 || isImageFormatSupported(format, "tiff", "geotiff")
651 || isImageFormatSupported(format, "png")
652 || isImageFormatSupported(format, "svg")
653 || isImageFormatSupported(format, "bmp");
654 }
655
656 static boolean isImageFormatSupported(String format, String... mimeFormats) {
657 for (String mime : mimeFormats) {
658 if (format.startsWith("image/" + mime)) {
659 return ImageIO.getImageReadersBySuffix(mimeFormats[0]).hasNext();
660 }
661 }
662 return false;
663 }
664
665 static boolean imageFormatHasTransparency(final String format) {
666 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif")
667 || format.startsWith("image/svg") || format.startsWith("image/tiff"));
668 }
669
670 /**
671 * Creates ImageryInfo object from this GetCapabilities document
672 *
673 * @param name name of imagery layer
674 * @param selectedLayers layers which are to be used by this imagery layer
675 * @param selectedStyles styles that should be used for selectedLayers
676 * @param format format of the response - one of {@link #getFormats()}
677 * @param transparent if layer should be transparent
678 * @return ImageryInfo object
679 * @since 15228
680 */
681 public ImageryInfo toImageryInfo(
682 String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) {
683 ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, format, transparent));
684 if (selectedLayers != null && !selectedLayers.isEmpty()) {
685 i.setServerProjections(getServerProjections(selectedLayers));
686 }
687 return i;
688 }
689
690 /**
691 * Returns projections that server supports for provided list of layers. This will be intersection of projections
692 * defined for each layer
693 *
694 * @param selectedLayers list of layers
695 * @return projection code
696 */
697 public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) {
698 if (selectedLayers.isEmpty()) {
699 return Collections.emptyList();
700 }
701 Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs());
702
703 // set intersect with all layers
704 for (LayerDetails ld: selectedLayers) {
705 proj.retainAll(ld.getCrs());
706 }
707 return proj;
708 }
709
710 /**
711 * @param defaultLayers default layers that should select layer object
712 * @return collection of LayerDetails specified by DefaultLayers
713 */
714 public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) {
715 Collection<String> layerNames = defaultLayers.stream().map(DefaultLayer::getLayerName).collect(Collectors.toList());
716 return layers.stream()
717 .flatMap(LayerDetails::flattened)
718 .filter(x -> layerNames.contains(x.getName()))
719 .collect(Collectors.toList());
720 }
721
722 /**
723 * @return title of this service
724 */
725 public String getTitle() {
726 return title;
727 }
728}
Note: See TracBrowser for help on using the repository browser.