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

Last change on this file since 14411 was 14411, checked in by wiktorn, 5 years ago

Skip unkown tags in WMS Getcapabilities

In Layer definition, if there is any other tag than ones that are interpreted,
move to the end of the tag, as it may be complex tag and affect further parsing.

Closes: #16940

  • Property svn:eol-style set to native
File size: 29.9 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 CAPABILITIES_QUERY_STRING = "SERVICE=WMS&REQUEST=GetCapabilities";
48
49 /**
50 * WMS namespace address
51 */
52 public static final String WMS_NS_URL = "http://www.opengis.net/wms";
53
54 // CHECKSTYLE.OFF: SingleSpaceSeparator
55 // WMS 1.0 - 1.3.0
56 private static final QName CAPABILITITES_ROOT_130 = new QName("WMS_Capabilities", WMS_NS_URL);
57 private static final QName QN_ABSTRACT = new QName(WMS_NS_URL, "Abstract");
58 private static final QName QN_CAPABILITY = new QName(WMS_NS_URL, "Capability");
59 private static final QName QN_CRS = new QName(WMS_NS_URL, "CRS");
60 private static final QName QN_DCPTYPE = new QName(WMS_NS_URL, "DCPType");
61 private static final QName QN_FORMAT = new QName(WMS_NS_URL, "Format");
62 private static final QName QN_GET = new QName(WMS_NS_URL, "Get");
63 private static final QName QN_GETMAP = new QName(WMS_NS_URL, "GetMap");
64 private static final QName QN_HTTP = new QName(WMS_NS_URL, "HTTP");
65 private static final QName QN_LAYER = new QName(WMS_NS_URL, "Layer");
66 private static final QName QN_NAME = new QName(WMS_NS_URL, "Name");
67 private static final QName QN_REQUEST = new QName(WMS_NS_URL, "Request");
68 private static final QName QN_SERVICE = new QName(WMS_NS_URL, "Service");
69 private static final QName QN_STYLE = new QName(WMS_NS_URL, "Style");
70 private static final QName QN_TITLE = new QName(WMS_NS_URL, "Title");
71 private static final QName QN_BOUNDINGBOX = new QName(WMS_NS_URL, "BoundingBox");
72 private static final QName QN_EX_GEOGRAPHIC_BBOX = new QName(WMS_NS_URL, "EX_GeographicBoundingBox");
73 private static final QName QN_WESTBOUNDLONGITUDE = new QName(WMS_NS_URL, "westBoundLongitude");
74 private static final QName QN_EASTBOUNDLONGITUDE = new QName(WMS_NS_URL, "eastBoundLongitude");
75 private static final QName QN_SOUTHBOUNDLATITUDE = new QName(WMS_NS_URL, "southBoundLatitude");
76 private static final QName QN_NORTHBOUNDLATITUDE = new QName(WMS_NS_URL, "northBoundLatitude");
77 private static final QName QN_ONLINE_RESOURCE = new QName(WMS_NS_URL, "OnlineResource");
78
79 // WMS 1.1 - 1.1.1
80 private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities");
81 private static final QName QN_SRS = new QName("SRS");
82 private static final QName QN_LATLONBOUNDINGBOX = new QName("LatLonBoundingBox");
83
84 // CHECKSTYLE.ON: SingleSpaceSeparator
85
86 /**
87 * An exception that is thrown if there was an error while getting the capabilities of the WMS server.
88 */
89 public static class WMSGetCapabilitiesException extends Exception {
90 private final String incomingData;
91
92 /**
93 * Constructs a new {@code WMSGetCapabilitiesException}
94 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method)
95 * @param incomingData the answer from WMS server
96 */
97 public WMSGetCapabilitiesException(Throwable cause, String incomingData) {
98 super(cause);
99 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}
Note: See TracBrowser for help on using the repository browser.