1
2
3
4
5
6 package org.tailormap.api.persistence.helper;
7
8 import static org.tailormap.api.persistence.json.GeoServiceProtocol.QUANTIZEDMESH;
9 import static org.tailormap.api.persistence.json.GeoServiceProtocol.TILES3D;
10 import static org.tailormap.api.persistence.json.GeoServiceProtocol.XYZ;
11
12 import java.io.IOException;
13 import java.lang.invoke.MethodHandles;
14 import java.net.URI;
15 import java.net.URISyntaxException;
16 import java.nio.charset.StandardCharsets;
17 import java.time.Instant;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Objects;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.function.Predicate;
24 import java.util.stream.Collectors;
25 import org.apache.commons.lang3.StringUtils;
26 import org.geotools.api.data.ServiceInfo;
27 import org.geotools.data.ows.AbstractOpenWebService;
28 import org.geotools.data.ows.Capabilities;
29 import org.geotools.data.ows.OperationType;
30 import org.geotools.http.HTTPClientFinder;
31 import org.geotools.ows.wms.Layer;
32 import org.geotools.ows.wms.WMSCapabilities;
33 import org.geotools.ows.wms.WebMapServer;
34 import org.geotools.ows.wmts.WebMapTileServer;
35 import org.geotools.ows.wmts.model.WMTSLayer;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.springframework.beans.factory.annotation.Autowired;
39 import org.springframework.http.MediaType;
40 import org.springframework.stereotype.Service;
41 import org.springframework.web.util.UriComponentsBuilder;
42 import org.tailormap.api.configuration.TailormapConfig;
43 import org.tailormap.api.geotools.ResponseTeeingHTTPClient;
44 import org.tailormap.api.geotools.WMSServiceExceptionUtil;
45 import org.tailormap.api.persistence.GeoService;
46 import org.tailormap.api.persistence.json.GeoServiceLayer;
47 import org.tailormap.api.persistence.json.ServiceAuthentication;
48 import org.tailormap.api.persistence.json.TMServiceCapabilitiesRequest;
49 import org.tailormap.api.persistence.json.TMServiceCapabilitiesRequestGetFeatureInfo;
50 import org.tailormap.api.persistence.json.TMServiceCapabilitiesRequestGetMap;
51 import org.tailormap.api.persistence.json.TMServiceCaps;
52 import org.tailormap.api.persistence.json.TMServiceCapsCapabilities;
53 import org.tailormap.api.persistence.json.TMServiceInfo;
54 import org.tailormap.api.persistence.json.WMSStyle;
55
56 @Service
57 public class GeoServiceHelper {
58
59 private static final Logger logger =
60 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
61 private final TailormapConfig tailormapConfig;
62
63 @Autowired
64 public GeoServiceHelper(TailormapConfig tailormapConfig) {
65 this.tailormapConfig = tailormapConfig;
66 }
67
68 public static org.tailormap.api.viewer.model.Service.ServerTypeEnum guessServerTypeFromUrl(String url) {
69
70 if (StringUtils.isBlank(url)) {
71 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GENERIC;
72 }
73 if (url.contains("/arcgis/")) {
74 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GENERIC;
75 }
76 if (url.contains("/geoserver/")) {
77 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GEOSERVER;
78 }
79 if (url.contains("/mapserv")) {
80 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.MAPSERVER;
81 }
82 return org.tailormap.api.viewer.model.Service.ServerTypeEnum.GENERIC;
83 }
84
85 public static String getWmsRequest(String uri) {
86 return getWmsRequest(uri == null ? null : URI.create(uri));
87 }
88
89
90
91
92
93
94
95 public static String getWmsRequest(URI uri) {
96 if (uri == null || uri.getQuery() == null) {
97 return null;
98 }
99 return UriComponentsBuilder.fromUri(uri).build().getQueryParams().entrySet().stream()
100 .filter(entry -> "request".equalsIgnoreCase(entry.getKey()))
101 .map(entry -> entry.getValue().getFirst())
102 .findFirst()
103 .orElse(null);
104 }
105
106 public void loadServiceCapabilities(GeoService geoService) throws Exception {
107
108 if (geoService.getProtocol() == XYZ) {
109 setXyzCapabilities(geoService);
110 return;
111 }
112
113 if (geoService.getProtocol() == TILES3D) {
114 set3DTilesCapabilities(geoService);
115 return;
116 }
117
118 if (geoService.getProtocol() == QUANTIZEDMESH) {
119 setQuantizedMeshCapabilities(geoService);
120 return;
121 }
122
123 ResponseTeeingHTTPClient client = new ResponseTeeingHTTPClient(
124 HTTPClientFinder.createClient(), null, Set.of("Access-Control-Allow-Origin"));
125
126 ServiceAuthentication auth = geoService.getAuthentication();
127 if (auth != null && auth.getMethod() == ServiceAuthentication.MethodEnum.PASSWORD) {
128 client.setUser(auth.getUsername());
129 client.setPassword(auth.getPassword());
130 }
131
132 client.setReadTimeout(this.tailormapConfig.getTimeout());
133 client.setConnectTimeout(this.tailormapConfig.getTimeout());
134 client.setTryGzip(true);
135
136 logger.info(
137 "Get capabilities for {} {} from URL {}",
138 geoService.getProtocol(),
139 geoService.getId() == null ? "(new)" : "id " + geoService.getId(),
140 geoService.getUrl());
141
142
143
144 switch (geoService.getProtocol()) {
145 case WMS -> loadWMSCapabilities(geoService, client);
146 case WMTS -> loadWMTSCapabilities(geoService, client);
147 default ->
148 throw new UnsupportedOperationException(
149 "Unsupported geo service protocol: " + geoService.getProtocol());
150 }
151
152 if (geoService.getTitle() == null) {
153 geoService.setTitle(Optional.ofNullable(geoService.getServiceCapabilities())
154 .map(TMServiceCaps::getServiceInfo)
155 .map(TMServiceInfo::getTitle)
156 .orElse(null));
157 }
158
159 if (logger.isDebugEnabled()) {
160 logger.debug("Loaded service layers: {}", geoService.getLayers());
161 } else {
162 logger.info(
163 "Loaded service layers: {}",
164 geoService.getLayers().stream()
165 .filter(Predicate.not(GeoServiceLayer::getVirtual))
166 .map(GeoServiceLayer::getName)
167 .collect(Collectors.toList()));
168 }
169 }
170
171 private static void setXyzCapabilities(GeoService geoService) {
172 geoService.setLayers(List.of(new GeoServiceLayer()
173 .id("0")
174 .root(true)
175 .name("xyz")
176 .title(geoService.getTitle())
177 .crs(Set.of(geoService.getSettings().getXyzCrs()))
178 .virtual(false)
179 .queryable(false)));
180 }
181
182 private static void set3DTilesCapabilities(GeoService geoService) {
183 geoService.setLayers(List.of(new GeoServiceLayer()
184 .id("0")
185 .root(true)
186 .name("tiles3d")
187 .title(geoService.getTitle())
188 .virtual(false)
189 .queryable(false)));
190 }
191
192 private static void setQuantizedMeshCapabilities(GeoService geoService) {
193 geoService.setLayers(List.of(new GeoServiceLayer()
194 .id("0")
195 .root(true)
196 .name("quantizedmesh")
197 .title(geoService.getTitle())
198 .virtual(false)
199 .queryable(false)));
200 }
201
202 private void setServiceInfo(
203 GeoService geoService,
204 ResponseTeeingHTTPClient client,
205 AbstractOpenWebService<? extends Capabilities, Layer> ows) {
206 geoService.setCapabilities(client.getLatestResponseCopy());
207 geoService.setCapabilitiesContentType(MediaType.APPLICATION_XML_VALUE);
208 geoService.setCapabilitiesFetched(Instant.now());
209
210 ServiceInfo info = ows.getInfo();
211
212 TMServiceCaps caps = new TMServiceCaps();
213 geoService.setServiceCapabilities(caps);
214
215 caps.setCorsAllowOrigin(client.getLatestResponse().getResponseHeader("Access-Control-Allow-Origin"));
216
217 if (info != null) {
218 if (StringUtils.isBlank(geoService.getTitle())) {
219 geoService.setTitle(info.getTitle());
220 }
221
222 caps.serviceInfo(new TMServiceInfo()
223 .keywords(info.getKeywords())
224 .description(info.getDescription())
225 .title(info.getTitle())
226 .publisher(info.getPublisher())
227 .schema(info.getSchema())
228 .source(info.getSource()));
229
230 geoService.setAdvertisedUrl(info.getSource().toString());
231 } else if (ows.getCapabilities() != null && ows.getCapabilities().getService() != null) {
232 org.geotools.data.ows.Service service = ows.getCapabilities().getService();
233
234 if (StringUtils.isBlank(geoService.getTitle())) {
235 geoService.setTitle(service.getTitle());
236 }
237 caps.setServiceInfo(new TMServiceInfo().keywords(Set.copyOf(List.of(service.getKeywordList()))));
238 }
239 }
240
241 private GeoServiceLayer toGeoServiceLayer(Layer l, List<? extends Layer> layers) {
242 return new GeoServiceLayer()
243 .id(String.valueOf(layers.indexOf(l)))
244 .name(l.getName())
245 .root(l.getParent() == null)
246 .title(l.getTitle())
247 .maxScale(Double.isNaN(l.getScaleDenominatorMax()) ? null : l.getScaleDenominatorMax())
248 .minScale(Double.isNaN(l.getScaleDenominatorMin()) ? null : l.getScaleDenominatorMin())
249 .virtual(l.getName() == null)
250 .crs(l.getSrs())
251 .latLonBoundingBox(GeoToolsHelper.boundsFromCRSEnvelope(l.getLatLonBoundingBox()))
252 .styles(l.getStyles().stream()
253 .map(gtStyle -> {
254 WMSStyle style = new WMSStyle()
255 .name(gtStyle.getName())
256 .title(Optional.ofNullable(gtStyle.getTitle())
257 .map(Objects::toString)
258 .orElse(null))
259 .abstractText(Optional.ofNullable(gtStyle.getAbstract())
260 .map(Objects::toString)
261 .orElse(null));
262 try {
263 List<?> legendUrls = gtStyle.getLegendURLs();
264
265 if (legendUrls != null && !legendUrls.isEmpty() && legendUrls.getFirst() != null) {
266 style.legendUrl(new URI((String) legendUrls.getFirst()));
267 }
268 } catch (URISyntaxException ignored) {
269
270
271 }
272 return style;
273 })
274 .collect(Collectors.toList()))
275 .queryable(l.isQueryable())
276 .abstractText(l.get_abstract())
277 .children(l.getLayerChildren().stream()
278 .map(layers::indexOf)
279 .map(String::valueOf)
280 .collect(Collectors.toList()));
281 }
282
283 private void addLayerRecursive(
284 GeoService geoService, List<? extends Layer> layers, Layer layer, Set<String> parentCrs) {
285 GeoServiceLayer geoServiceLayer = toGeoServiceLayer(layer, layers);
286
287
288
289 geoServiceLayer.getCrs().removeAll(parentCrs);
290 geoService.getLayers().add(geoServiceLayer);
291 for (Layer l : layer.getLayerChildren()) {
292 addLayerRecursive(geoService, layers, l, layer.getSrs());
293 }
294 }
295
296 void loadWMSCapabilities(GeoService geoService, ResponseTeeingHTTPClient client) throws Exception {
297 WebMapServer wms;
298 try {
299 wms = new WebMapServer(new URI(geoService.getUrl()).toURL(), client);
300 } catch (ClassCastException | IllegalStateException e) {
301
302
303
304
305
306
307
308 String contentType = client.getLatestResponse().getContentType();
309 if (contentType != null && contentType.contains("text/xml")) {
310 String wmsException =
311 WMSServiceExceptionUtil.tryGetServiceExceptionMessage(client.getLatestResponseCopy());
312 throw new Exception("Error loading WMS capabilities: "
313 + (wmsException != null
314 ? wmsException
315 : new String(client.getLatestResponseCopy(), StandardCharsets.UTF_8)));
316 } else {
317 throw e;
318 }
319 } catch (IOException e) {
320
321
322
323 if (e.getMessage().contains("Server returned HTTP response code: 401 for URL:")) {
324 throw new Exception(
325 "Error loading WMS, got 401 unauthorized response (credentials may be required or invalid)");
326 } else {
327 throw e;
328 }
329 }
330
331 OperationType getMap = wms.getCapabilities().getRequest().getGetMap();
332 OperationType getFeatureInfo = wms.getCapabilities().getRequest().getGetFeatureInfo();
333
334 if (getMap == null) {
335 throw new Exception("Service does not support GetMap");
336 }
337
338 setServiceInfo(geoService, client, wms);
339
340 WMSCapabilities wmsCapabilities = wms.getCapabilities();
341
342
343
344 geoService
345 .getServiceCapabilities()
346 .capabilities(new TMServiceCapsCapabilities()
347 .version(wmsCapabilities.getVersion())
348 .updateSequence(wmsCapabilities.getUpdateSequence())
349 .abstractText(wmsCapabilities.getService().get_abstract())
350 .request(new TMServiceCapabilitiesRequest()
351 .getMap(new TMServiceCapabilitiesRequestGetMap()
352 .formats(Set.copyOf(getMap.getFormats())))
353 .getFeatureInfo(
354 getFeatureInfo == null
355 ? null
356 : new TMServiceCapabilitiesRequestGetFeatureInfo()
357 .formats(Set.copyOf(getFeatureInfo.getFormats())))
358 .describeLayer(
359 wms.getCapabilities().getRequest().getDescribeLayer() != null)));
360
361 if (logger.isDebugEnabled()) {
362 logger.debug("Loaded capabilities, service capabilities: {}", geoService.getServiceCapabilities());
363 } else {
364 logger.info(
365 "Loaded capabilities from \"{}\", title: \"{}\"",
366 geoService.getUrl(),
367 geoService.getServiceCapabilities() != null
368 && geoService.getServiceCapabilities().getServiceInfo() != null
369 ? geoService
370 .getServiceCapabilities()
371 .getServiceInfo()
372 .getTitle()
373 : "(none)");
374 }
375 geoService.setLayers(new ArrayList<>());
376 addLayerRecursive(
377 geoService,
378 wms.getCapabilities().getLayerList(),
379 wms.getCapabilities().getLayer(),
380 Set.of());
381 }
382
383 void loadWMTSCapabilities(GeoService geoService, ResponseTeeingHTTPClient client) throws Exception {
384 WebMapTileServer wmts = new WebMapTileServer(new URI(geoService.getUrl()).toURL(), client);
385 setServiceInfo(geoService, client, wmts);
386
387
388
389 List<WMTSLayer> layers = wmts.getCapabilities().getLayerList();
390 geoService.setLayers(
391 layers.stream().map(l -> toGeoServiceLayer(l, layers)).collect(Collectors.toList()));
392 }
393
394
395
396
397
398
399
400
401
402
403
404 public static URI getLayerLegendUrlFromStyles(GeoService service, GeoServiceLayer serviceLayer) {
405 if (serviceLayer.getRoot()) {
406
407
408 return serviceLayer.getStyles().stream()
409 .findFirst()
410 .map(WMSStyle::getLegendUrl)
411 .orElse(null);
412 }
413
414 final List<WMSStyle> allOurLayersStyles = serviceLayer.getStyles();
415 if (allOurLayersStyles.size() == 1) {
416 return allOurLayersStyles.getFirst().getLegendUrl();
417 }
418
419 service.getLayers().stream()
420 .filter(layer -> !layer.equals(serviceLayer))
421 .forEach(layer -> allOurLayersStyles.removeAll(layer.getStyles()));
422
423 return allOurLayersStyles.stream()
424 .findFirst()
425 .map(WMSStyle::getLegendUrl)
426 .orElse(null);
427 }
428 }