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

Last change on this file was 18918, checked in by taylor.smock, 6 months ago

Fix #23290: Validate the regions a tag is expected to be in (patch by Sarabjeet108, modified)

Modifications are as follows:

  • Allow the use of the new region attributes for keys inside a preset
  • Basic tests

regions comes from Vespucci's extensions: https://vespucci.io/tutorials/presets/#extensions

  • Property svn:eol-style set to native
File size: 34.6 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.Arrays;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.HashSet;
18import java.util.List;
19import java.util.Map;
20import java.util.Set;
21import java.util.concurrent.ConcurrentHashMap;
22import java.util.function.UnaryOperator;
23import java.util.regex.Pattern;
24import java.util.stream.Collectors;
25
26import javax.imageio.ImageIO;
27import javax.xml.namespace.QName;
28import javax.xml.stream.XMLStreamException;
29import javax.xml.stream.XMLStreamReader;
30
31import org.openstreetmap.josm.data.Bounds;
32import org.openstreetmap.josm.data.coor.EastNorth;
33import org.openstreetmap.josm.data.imagery.DefaultLayer;
34import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper;
35import org.openstreetmap.josm.data.imagery.ImageryInfo;
36import org.openstreetmap.josm.data.imagery.LayerDetails;
37import org.openstreetmap.josm.data.projection.Projection;
38import org.openstreetmap.josm.data.projection.Projections;
39import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
40import org.openstreetmap.josm.gui.progress.ProgressMonitor;
41import org.openstreetmap.josm.io.CachedFile;
42import org.openstreetmap.josm.tools.Logging;
43import org.openstreetmap.josm.tools.Utils;
44
45/**
46 * This class represents the capabilities of a WMS imagery server.
47 */
48public class WMSImagery {
49
50 private static final String SERVICE_WMS = "SERVICE=WMS";
51 private static final String REQUEST_GET_CAPABILITIES = "REQUEST=GetCapabilities";
52 private static final String CAPABILITIES_QUERY_STRING = SERVICE_WMS + "&" + REQUEST_GET_CAPABILITIES;
53
54 /**
55 * WMS namespace address
56 */
57 public static final String WMS_NS_URL = "http://www.opengis.net/wms";
58
59 // CHECKSTYLE.OFF: SingleSpaceSeparator
60 // WMS 1.0 - 1.3.0
61 private static final QName CAPABILITIES_ROOT_130 = new QName(WMS_NS_URL, "WMS_Capabilities");
62 private static final QName QN_ABSTRACT = new QName(WMS_NS_URL, "Abstract");
63 private static final QName QN_CAPABILITY = new QName(WMS_NS_URL, "Capability");
64 private static final QName QN_CRS = new QName(WMS_NS_URL, "CRS");
65 private static final QName QN_DCPTYPE = new QName(WMS_NS_URL, "DCPType");
66 private static final QName QN_FORMAT = new QName(WMS_NS_URL, "Format");
67 private static final QName QN_GET = new QName(WMS_NS_URL, "Get");
68 private static final QName QN_GETMAP = new QName(WMS_NS_URL, "GetMap");
69 private static final QName QN_HTTP = new QName(WMS_NS_URL, "HTTP");
70 private static final QName QN_LAYER = new QName(WMS_NS_URL, "Layer");
71 private static final QName QN_NAME = new QName(WMS_NS_URL, "Name");
72 private static final QName QN_REQUEST = new QName(WMS_NS_URL, "Request");
73 private static final QName QN_SERVICE = new QName(WMS_NS_URL, "Service");
74 private static final QName QN_STYLE = new QName(WMS_NS_URL, "Style");
75 private static final QName QN_TITLE = new QName(WMS_NS_URL, "Title");
76 private static final QName QN_BOUNDINGBOX = new QName(WMS_NS_URL, "BoundingBox");
77 private static final QName QN_EX_GEOGRAPHIC_BBOX = new QName(WMS_NS_URL, "EX_GeographicBoundingBox");
78 private static final QName QN_WESTBOUNDLONGITUDE = new QName(WMS_NS_URL, "westBoundLongitude");
79 private static final QName QN_EASTBOUNDLONGITUDE = new QName(WMS_NS_URL, "eastBoundLongitude");
80 private static final QName QN_SOUTHBOUNDLATITUDE = new QName(WMS_NS_URL, "southBoundLatitude");
81 private static final QName QN_NORTHBOUNDLATITUDE = new QName(WMS_NS_URL, "northBoundLatitude");
82 private static final QName QN_ONLINE_RESOURCE = new QName(WMS_NS_URL, "OnlineResource");
83
84 // WMS 1.1 - 1.1.1
85 private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities");
86 private static final QName QN_SRS = new QName("SRS");
87 private static final QName QN_LATLONBOUNDINGBOX = new QName("LatLonBoundingBox");
88
89 // CHECKSTYLE.ON: SingleSpaceSeparator
90
91 /**
92 * An exception that is thrown if there was an error while getting the capabilities of the WMS server.
93 */
94 public static class WMSGetCapabilitiesException extends Exception {
95 private final String incomingData;
96
97 /**
98 * Constructs a new {@code WMSGetCapabilitiesException}
99 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method)
100 * @param incomingData the answer from WMS server
101 */
102 public WMSGetCapabilitiesException(Throwable cause, String incomingData) {
103 super(cause);
104 this.incomingData = incomingData;
105 }
106
107 /**
108 * Constructs a new {@code WMSGetCapabilitiesException}
109 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method
110 * @param incomingData the answer from the server
111 * @since 10520
112 */
113 public WMSGetCapabilitiesException(String message, String incomingData) {
114 super(message);
115 this.incomingData = incomingData;
116 }
117
118 /**
119 * The data that caused this exception.
120 * @return The server response to the capabilities request.
121 */
122 public String getIncomingData() {
123 return incomingData;
124 }
125 }
126
127 private final Map<String, String> headers = new ConcurrentHashMap<>();
128 private String version = "1.1.1"; // default version
129 private String getMapUrl;
130 private URL capabilitiesUrl;
131 private final List<String> formats = new ArrayList<>();
132 private List<LayerDetails> layers = new ArrayList<>();
133
134 private String title;
135
136 /**
137 * Make getCapabilities request towards given URL
138 * @param url service url
139 * @throws IOException when connection error when fetching get capabilities document
140 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document
141 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file
142 */
143 public WMSImagery(String url) throws IOException, WMSGetCapabilitiesException {
144 this(url, null);
145 }
146
147 /**
148 * Make getCapabilities request towards given URL using headers
149 * @param url service url
150 * @param headers HTTP headers to be sent with request
151 * @throws IOException when connection error when fetching get capabilities document
152 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document
153 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file
154 */
155 public WMSImagery(String url, Map<String, String> headers) throws IOException, WMSGetCapabilitiesException {
156 this(url, headers, NullProgressMonitor.INSTANCE);
157 }
158
159 /**
160 * Make getCapabilities request towards given URL using headers
161 * @param url service url
162 * @param headers HTTP headers to be sent with request
163 * @param monitor Feedback for which URL we are currently trying, the integer is the <i>total number of urls</i> we are going to try
164 * @throws IOException when connection error when fetching get capabilities document
165 * @throws WMSGetCapabilitiesException when there are errors when parsing get capabilities document
166 * @throws InvalidPathException if a Path object cannot be constructed for the capabilities cached file
167 * @since 18780
168 */
169 public WMSImagery(String url, Map<String, String> headers, ProgressMonitor monitor)
170 throws IOException, WMSGetCapabilitiesException {
171 if (headers != null) {
172 this.headers.putAll(headers);
173 }
174
175 IOException savedExc = null;
176 String workingAddress = null;
177 final String[] baseAdditions = {
178 normalizeUrl(url),
179 url,
180 url + CAPABILITIES_QUERY_STRING,
181 };
182 final String[] versionAdditions = {"", "&VERSION=1.3.0", "&VERSION=1.1.1"};
183 final int totalNumberOfUrlsToTry = baseAdditions.length * versionAdditions.length;
184 monitor.setTicksCount(totalNumberOfUrlsToTry);
185 url_search:
186 for (String z : baseAdditions) {
187 for (String ver : versionAdditions) {
188 if (monitor.isCanceled()) {
189 break url_search;
190 }
191 try {
192 monitor.setCustomText(z + ver);
193 monitor.worked(1);
194 attemptGetCapabilities(z + ver);
195 workingAddress = z;
196 calculateChildren();
197 // clear saved exception - we've got something working
198 savedExc = null;
199 break url_search;
200 } catch (IOException e) {
201 savedExc = e;
202 Logging.warn(e);
203 }
204 }
205 }
206
207 if (workingAddress != null) {
208 try {
209 capabilitiesUrl = new URL(workingAddress);
210 } catch (MalformedURLException e) {
211 if (savedExc == null) {
212 savedExc = e;
213 }
214 try {
215 capabilitiesUrl = new File(workingAddress).toURI().toURL();
216 } catch (MalformedURLException e1) { // NOPMD
217 // do nothing, raise original exception
218 Logging.trace(e1);
219 }
220 }
221 }
222 if (savedExc != null) {
223 throw savedExc;
224 }
225 }
226
227 private void calculateChildren() {
228 Map<LayerDetails, List<LayerDetails>> layerChildren = layers.stream()
229 .filter(x -> x.getParent() != null) // exclude top-level elements
230 .collect(Collectors.groupingBy(LayerDetails::getParent));
231 for (LayerDetails ld: layers) {
232 if (layerChildren.containsKey(ld)) {
233 ld.setChildren(layerChildren.get(ld));
234 }
235 }
236 // leave only top-most elements in the list
237 layers = layers.stream().filter(x -> x.getParent() == null).collect(Collectors.toCollection(ArrayList::new));
238 }
239
240 /**
241 * Returns the list of top-level layers.
242 * @return the list of top-level layers
243 */
244 public List<LayerDetails> getLayers() {
245 return Collections.unmodifiableList(layers);
246 }
247
248 /**
249 * Returns the list of supported formats.
250 * @return the list of supported formats
251 */
252 public Collection<String> getFormats() {
253 return Collections.unmodifiableList(formats);
254 }
255
256 /**
257 * Gets the preferred format for this imagery layer.
258 * @return The preferred format as mime type.
259 */
260 public String getPreferredFormat() {
261 if (formats.contains("image/png")) {
262 return "image/png";
263 } else if (formats.contains("image/jpeg")) {
264 return "image/jpeg";
265 } else if (formats.isEmpty()) {
266 return null;
267 } else {
268 return formats.get(0);
269 }
270 }
271
272 /**
273 * Returns root URL of services in this GetCapabilities.
274 * @return root URL of services in this GetCapabilities
275 */
276 public String buildRootUrl() {
277 if (getMapUrl == null && capabilitiesUrl == null) {
278 return null;
279 }
280 if (getMapUrl != null) {
281 return getMapUrl;
282 }
283
284 URL serviceUrl = capabilitiesUrl;
285 StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
286 a.append("://").append(serviceUrl.getHost());
287 if (serviceUrl.getPort() != -1) {
288 a.append(':').append(serviceUrl.getPort());
289 }
290 a.append(serviceUrl.getPath()).append('?');
291 if (serviceUrl.getQuery() != null) {
292 a.append(serviceUrl.getQuery());
293 if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) {
294 a.append('&');
295 }
296 }
297 return a.toString();
298 }
299
300 /**
301 * Returns root URL of services without the GetCapabilities call.
302 * @return root URL of services without the GetCapabilities call
303 * @since 15209
304 */
305 public String buildRootUrlWithoutCapabilities() {
306 return buildRootUrl()
307 .replace(CAPABILITIES_QUERY_STRING, "")
308 .replace(SERVICE_WMS, "")
309 .replace(REQUEST_GET_CAPABILITIES, "")
310 .replace("?&", "?");
311 }
312
313 /**
314 * Returns URL for accessing GetMap service. String will contain following parameters:
315 * <ul>
316 * <li>{proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})</li>
317 * <li>{width} - that needs to be replaced with width of the tile</li>
318 * <li>{height} - that needs to be replaces with height of the tile</li>
319 * <li>{bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)</li>
320 * </ul>
321 * Format of the response will be calculated using {@link #getPreferredFormat()}
322 *
323 * @param selectedLayers list of DefaultLayer selection of layers to be shown
324 * @param transparent whether returned images should contain transparent pixels (if supported by format)
325 * @return URL template for GetMap service containing
326 */
327 public String buildGetMapUrl(List<DefaultLayer> selectedLayers, boolean transparent) {
328 return buildGetMapUrl(
329 getLayers(selectedLayers),
330 selectedLayers.stream().map(DefaultLayer::getStyle).collect(Collectors.toList()),
331 transparent);
332 }
333
334 /**
335 * Returns URL for accessing GetMap service. String will contain following parameters:
336 * <ul>
337 * <li>{proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})</li>
338 * <li>{width} - that needs to be replaced with width of the tile</li>
339 * <li>{height} - that needs to be replaces with height of the tile</li>
340 * <li>{bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)</li>
341 * </ul>
342 * Format of the response will be calculated using {@link #getPreferredFormat()}
343 *
344 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()}
345 * @param selectedStyles selected styles for all selectedLayers
346 * @param transparent whether returned images should contain transparent pixels (if supported by format)
347 * @return URL template for GetMap service
348 * @see #buildGetMapUrl(List, boolean)
349 */
350 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
351 return buildGetMapUrl(selectedLayers, selectedStyles, getPreferredFormat(), transparent);
352 }
353
354 /**
355 * Returns URL for accessing GetMap service. String will contain following parameters:
356 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})
357 * * {width} - that needs to be replaced with width of the tile
358 * * {height} - that needs to be replaces with height of the tile
359 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)
360 *
361 * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()}
362 * @param selectedStyles selected styles for all selectedLayers
363 * @param format format of the response - one of {@link #getFormats()}
364 * @param transparent whether returned images should contain transparent pixels (if supported by format)
365 * @return URL template for GetMap service
366 * @see #buildGetMapUrl(List, boolean)
367 * @since 15228
368 */
369 public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) {
370 return buildGetMapUrl(
371 selectedLayers.stream().map(LayerDetails::getName).collect(Collectors.toList()),
372 selectedStyles,
373 format,
374 transparent);
375 }
376
377 /**
378 * Returns URL for accessing GetMap service. String will contain following parameters:
379 * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})
380 * * {width} - that needs to be replaced with width of the tile
381 * * {height} - that needs to be replaces with height of the tile
382 * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)
383 *
384 * @param selectedLayers selected layers as list of strings
385 * @param selectedStyles selected styles of layers as list of strings
386 * @param format format of the response - one of {@link #getFormats()}
387 * @param transparent whether returned images should contain transparent pixels (if supported by format)
388 * @return URL template for GetMap service
389 * @see #buildGetMapUrl(List, boolean)
390 */
391 public String buildGetMapUrl(List<String> selectedLayers,
392 Collection<String> selectedStyles,
393 String format,
394 boolean transparent) {
395
396 Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(),
397 tr("Styles size {0} does not match layers size {1}"),
398 selectedStyles == null ? 0 : selectedStyles.size(),
399 selectedLayers.size());
400
401 return buildRootUrlWithoutCapabilities()
402 + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "")
403 + "&VERSION=" + this.version + "&" + SERVICE_WMS + "&REQUEST=GetMap&LAYERS="
404 + String.join(",", selectedLayers)
405 + "&STYLES="
406 + (selectedStyles != null ? String.join(",", selectedStyles) : "")
407 + "&"
408 + (belowWMS130() ? "SRS" : "CRS")
409 + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
410 }
411
412 private boolean tagEquals(QName a, QName b) {
413 boolean ret = a.equals(b);
414 if (ret) {
415 return true;
416 }
417
418 if (belowWMS130()) {
419 return a.getLocalPart().equals(b.getLocalPart());
420 }
421
422 return false;
423 }
424
425 private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException {
426 Logging.debug("Trying WMS GetCapabilities with url {0}", url);
427 try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
428 setMaxAge(7 * CachedFile.DAYS).
429 setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
430 getInputStream()) {
431
432 try {
433 XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in);
434 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
435 if (event == XMLStreamReader.START_ELEMENT) {
436 if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) {
437 this.version = Utils.firstNotEmptyString("1.1.1",
438 reader.getAttributeValue(null, "version"));
439 }
440 if (tagEquals(CAPABILITIES_ROOT_130, reader.getName())) {
441 this.version = Utils.firstNotEmptyString("1.3.0",
442 reader.getAttributeValue(WMS_NS_URL, "version"),
443 reader.getAttributeValue(null, "version"));
444 }
445 if (tagEquals(QN_SERVICE, reader.getName())) {
446 parseService(reader);
447 }
448
449 if (tagEquals(QN_CAPABILITY, reader.getName())) {
450 parseCapability(reader);
451 }
452 }
453 }
454 } catch (XMLStreamException e) {
455 String content = new String(cf.getByteContent(), UTF_8);
456 cf.clear(); // if there is a problem with parsing of the file, remove it from the cache
457 throw new WMSGetCapabilitiesException(e, content);
458 }
459 }
460 }
461
462 private void parseService(XMLStreamReader reader) throws XMLStreamException {
463 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) {
464 this.title = reader.getElementText();
465 // CHECKSTYLE.OFF: EmptyBlock
466 for (int event = reader.getEventType();
467 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName()));
468 event = reader.next()) {
469 // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done
470 }
471 // CHECKSTYLE.ON: EmptyBlock
472 }
473 }
474
475 private void parseCapability(XMLStreamReader reader) throws XMLStreamException {
476 for (int event = reader.getEventType();
477 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName()));
478 event = reader.next()) {
479
480 if (event == XMLStreamReader.START_ELEMENT) {
481 if (tagEquals(QN_REQUEST, reader.getName())) {
482 parseRequest(reader);
483 }
484 if (tagEquals(QN_LAYER, reader.getName())) {
485 parseLayer(reader, null);
486 }
487 }
488 }
489 }
490
491 private void parseRequest(XMLStreamReader reader) throws XMLStreamException {
492 String mode;
493 String newGetMapUrl = "";
494 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) {
495 for (int event = reader.getEventType();
496 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName()));
497 event = reader.next()) {
498
499 if (event == XMLStreamReader.START_ELEMENT) {
500 if (tagEquals(QN_FORMAT, reader.getName())) {
501 String value = reader.getElementText();
502 if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) {
503 this.formats.add(value);
504 }
505 }
506 if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader,
507 this::tagEquals, QN_HTTP, QN_GET)) {
508 mode = reader.getName().getLocalPart();
509 if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) {
510 newGetMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
511 }
512 // TODO should we handle also POST?
513 if ("GET".equalsIgnoreCase(mode) && newGetMapUrl != null && !newGetMapUrl.isEmpty()) {
514 try {
515 String query = new URL(newGetMapUrl).getQuery();
516 if (query == null) {
517 this.getMapUrl = newGetMapUrl + "?";
518 } else {
519 this.getMapUrl = newGetMapUrl;
520 }
521 } catch (MalformedURLException e) {
522 throw new XMLStreamException(e);
523 }
524 }
525 }
526 }
527 }
528 }
529 }
530
531 private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException {
532 LayerDetails ret = new LayerDetails(parentLayer);
533 for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer
534 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName()));
535 event = reader.next()) {
536
537 if (event == XMLStreamReader.START_ELEMENT) {
538 if (tagEquals(QN_NAME, reader.getName())) {
539 ret.setName(reader.getElementText());
540 } else if (tagEquals(QN_ABSTRACT, reader.getName())) {
541 ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader));
542 } else if (tagEquals(QN_TITLE, reader.getName())) {
543 ret.setTitle(reader.getElementText());
544 } else if (tagEquals(QN_CRS, reader.getName())) {
545 ret.addCrs(reader.getElementText());
546 } else if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) {
547 ret.addCrs(reader.getElementText());
548 } else if (tagEquals(QN_STYLE, reader.getName())) {
549 parseAndAddStyle(reader, ret);
550 } else if (tagEquals(QN_LAYER, reader.getName())) {
551 parseLayer(reader, ret);
552 } else if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()) && ret.getBounds() == null) {
553 ret.setBounds(parseExGeographic(reader));
554 } else if (tagEquals(QN_BOUNDINGBOX, reader.getName())) {
555 Projection conv;
556 if (belowWMS130()) {
557 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS"));
558 } else {
559 conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS"));
560 }
561 if (ret.getBounds() == null && conv != null) {
562 ret.setBounds(parseBoundingBox(reader, conv));
563 }
564 } else if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130() && ret.getBounds() == null) {
565 ret.setBounds(parseBoundingBox(reader, null));
566 } else {
567 // unknown tag, move to its end as it may have child elements
568 GetCapabilitiesParseHelper.moveReaderToEndCurrentTag(reader);
569 }
570 }
571 }
572 this.layers.add(ret);
573 }
574
575 /**
576 * Determines if this service operates at protocol level below WMS 1.3.0
577 * @return if this service operates at protocol level below 1.3.0
578 */
579 public boolean belowWMS130() {
580 return "1.1.1".equals(version) || "1.1".equals(version) || "1.0".equals(version);
581 }
582
583 private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException {
584 String name = null;
585 String styleTitle = null;
586 for (int event = reader.getEventType();
587 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName()));
588 event = reader.next()) {
589 if (event == XMLStreamReader.START_ELEMENT) {
590 if (tagEquals(QN_NAME, reader.getName())) {
591 name = reader.getElementText();
592 }
593 if (tagEquals(QN_TITLE, reader.getName())) {
594 styleTitle = reader.getElementText();
595 }
596 }
597 }
598 if (name == null) {
599 name = "";
600 }
601 ld.addStyle(name, styleTitle);
602 }
603
604 private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException {
605 String minx = null, maxx = null, maxy = null, miny = null;
606
607 for (int event = reader.getEventType();
608 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()));
609 event = reader.next()) {
610 if (event == XMLStreamReader.START_ELEMENT) {
611 if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) {
612 minx = reader.getElementText();
613 }
614
615 if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) {
616 maxx = reader.getElementText();
617 }
618
619 if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) {
620 miny = reader.getElementText();
621 }
622
623 if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) {
624 maxy = reader.getElementText();
625 }
626 }
627 }
628 return parseBBox(null, miny, minx, maxy, maxx);
629 }
630
631 private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) {
632 UnaryOperator<String> attrGetter = tag -> belowWMS130() ?
633 reader.getAttributeValue(null, tag)
634 : reader.getAttributeValue(WMS_NS_URL, tag);
635
636 return parseBBox(
637 conv,
638 attrGetter.apply("miny"),
639 attrGetter.apply("minx"),
640 attrGetter.apply("maxy"),
641 attrGetter.apply("maxx")
642 );
643 }
644
645 private static Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) {
646 if (miny == null || minx == null || maxy == null || maxx == null || Arrays.asList(miny, minx, maxy, maxx).contains("nan")) {
647 return null;
648 }
649 if (conv != null) {
650 return new Bounds(
651 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))),
652 conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy)))
653 );
654 }
655 return new Bounds(
656 getDecimalDegree(miny),
657 getDecimalDegree(minx),
658 getDecimalDegree(maxy),
659 getDecimalDegree(maxx)
660 );
661 }
662
663 private static double getDecimalDegree(String value) {
664 // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
665 return Double.parseDouble(value.replace(',', '.'));
666 }
667
668 private static String normalizeUrl(String serviceUrlStr) throws MalformedURLException {
669 URL getCapabilitiesUrl;
670 String ret;
671
672 if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
673 // If the url doesn't already have GetCapabilities, add it in
674 getCapabilitiesUrl = new URL(serviceUrlStr);
675 if (getCapabilitiesUrl.getQuery() == null) {
676 ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING;
677 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
678 ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING;
679 } else {
680 ret = serviceUrlStr + CAPABILITIES_QUERY_STRING;
681 }
682 } else {
683 // Otherwise assume it's a good URL and let the subsequent error
684 // handling systems deal with problems
685 ret = serviceUrlStr;
686 }
687 return ret;
688 }
689
690 private static boolean isImageFormatSupportedWarn(String format) {
691 boolean isFormatSupported = isImageFormatSupported(format);
692 if (!isFormatSupported) {
693 Logging.info("Skipping unsupported image format {0}", format);
694 }
695 return isFormatSupported;
696 }
697
698 static boolean isImageFormatSupported(final String format) {
699 return ImageIO.getImageReadersByMIMEType(format).hasNext()
700 // handles image/tiff image/tiff8 image/geotiff image/geotiff8
701 || isImageFormatSupported(format, "tiff", "geotiff")
702 || isImageFormatSupported(format, "png")
703 || isImageFormatSupported(format, "svg")
704 || isImageFormatSupported(format, "bmp");
705 }
706
707 static boolean isImageFormatSupported(String format, String... mimeFormats) {
708 for (String mime : mimeFormats) {
709 if (format.startsWith("image/" + mime)) {
710 return ImageIO.getImageReadersBySuffix(mimeFormats[0]).hasNext();
711 }
712 }
713 return false;
714 }
715
716 static boolean imageFormatHasTransparency(final String format) {
717 return format != null && (format.startsWith("image/png") || format.startsWith("image/gif")
718 || format.startsWith("image/svg") || format.startsWith("image/tiff"));
719 }
720
721 /**
722 * Creates ImageryInfo object from this GetCapabilities document
723 *
724 * @param name name of imagery layer
725 * @param selectedLayers layers which are to be used by this imagery layer
726 * @param selectedStyles styles that should be used for selectedLayers
727 * @param format format of the response - one of {@link #getFormats()}
728 * @param transparent if layer should be transparent
729 * @return ImageryInfo object
730 * @since 15228
731 */
732 public ImageryInfo toImageryInfo(
733 String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, String format, boolean transparent) {
734 ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, format, transparent));
735 if (!selectedLayers.isEmpty()) {
736 i.setServerProjections(getServerProjections(selectedLayers));
737 }
738 return i;
739 }
740
741 /**
742 * Returns projections that server supports for provided list of layers. This will be intersection of projections
743 * defined for each layer
744 *
745 * @param selectedLayers list of layers
746 * @return projection code
747 */
748 public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) {
749 if (selectedLayers.isEmpty()) {
750 return Collections.emptyList();
751 }
752 Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs());
753
754 // set intersect with all layers
755 for (LayerDetails ld: selectedLayers) {
756 proj.retainAll(ld.getCrs());
757 }
758 return proj;
759 }
760
761 /**
762 * Returns collection of LayerDetails specified by defaultLayers.
763 * @param defaultLayers default layers that should select layer object
764 * @return collection of LayerDetails specified by defaultLayers
765 */
766 public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) {
767 Collection<String> layerNames = defaultLayers.stream().map(DefaultLayer::getLayerName).collect(Collectors.toList());
768 return layers.stream()
769 .flatMap(LayerDetails::flattened)
770 .filter(x -> layerNames.contains(x.getName()))
771 .collect(Collectors.toList());
772 }
773
774 /**
775 * Returns title of this service.
776 * @return title of this service
777 */
778 public String getTitle() {
779 return title;
780 }
781}
Note: See TracBrowser for help on using the repository browser.