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

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

see #19334 - https://errorprone.info/bugpattern/UnnecessaryParentheses

  • Property svn:eol-style set to native
File size: 33.0 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 * 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 != null && !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}
Note: See TracBrowser for help on using the repository browser.