1
2
3
4
5
6 package org.tailormap.api.persistence.helper;
7
8 import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
9 import static org.tailormap.api.persistence.json.GeoServiceProtocol.PROXIEDLEGEND;
10 import static org.tailormap.api.persistence.json.GeoServiceProtocol.QUANTIZEDMESH;
11 import static org.tailormap.api.persistence.json.GeoServiceProtocol.TILES3D;
12 import static org.tailormap.api.persistence.json.GeoServiceProtocol.XYZ;
13 import static org.tailormap.api.util.TMStringUtils.nullIfEmpty;
14
15 import jakarta.persistence.EntityManager;
16 import java.lang.invoke.MethodHandles;
17 import java.net.URI;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Optional;
23 import java.util.Set;
24 import org.apache.commons.lang3.ObjectUtils;
25 import org.apache.commons.lang3.tuple.Triple;
26 import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
27 import org.geotools.referencing.util.CRSUtilities;
28 import org.geotools.referencing.wkt.Formattable;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.springframework.stereotype.Service;
32 import org.springframework.transaction.annotation.Transactional;
33 import org.tailormap.api.controller.GeoServiceProxyController;
34 import org.tailormap.api.persistence.Application;
35 import org.tailormap.api.persistence.Configuration;
36 import org.tailormap.api.persistence.GeoService;
37 import org.tailormap.api.persistence.SearchIndex;
38 import org.tailormap.api.persistence.TMFeatureType;
39 import org.tailormap.api.persistence.json.AppContent;
40 import org.tailormap.api.persistence.json.AppLayerSettings;
41 import org.tailormap.api.persistence.json.AppTreeLayerNode;
42 import org.tailormap.api.persistence.json.AppTreeLevelNode;
43 import org.tailormap.api.persistence.json.AppTreeNode;
44 import org.tailormap.api.persistence.json.Bounds;
45 import org.tailormap.api.persistence.json.GeoServiceDefaultLayerSettings;
46 import org.tailormap.api.persistence.json.GeoServiceLayer;
47 import org.tailormap.api.persistence.json.GeoServiceLayerSettings;
48 import org.tailormap.api.persistence.json.ServicePublishingSettings;
49 import org.tailormap.api.persistence.json.TileLayerHiDpiMode;
50 import org.tailormap.api.repository.ApplicationRepository;
51 import org.tailormap.api.repository.ConfigurationRepository;
52 import org.tailormap.api.repository.FeatureSourceRepository;
53 import org.tailormap.api.repository.GeoServiceRepository;
54 import org.tailormap.api.repository.SearchIndexRepository;
55 import org.tailormap.api.security.AuthorizationService;
56 import org.tailormap.api.viewer.model.AppLayer;
57 import org.tailormap.api.viewer.model.LayerSearchIndex;
58 import org.tailormap.api.viewer.model.LayerTreeNode;
59 import org.tailormap.api.viewer.model.MapResponse;
60 import org.tailormap.api.viewer.model.TMCoordinateReferenceSystem;
61
62 @Service
63 public class ApplicationHelper {
64 private static final Logger logger =
65 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
66 private static final String DEFAULT_WEB_MERCATOR_CRS = "EPSG:3857";
67
68 private final GeoServiceHelper geoServiceHelper;
69 private final GeoServiceRepository geoServiceRepository;
70 private final ConfigurationRepository configurationRepository;
71 private final ApplicationRepository applicationRepository;
72 private final FeatureSourceRepository featureSourceRepository;
73 private final EntityManager entityManager;
74 private final AuthorizationService authorizationService;
75 private final SearchIndexRepository searchIndexRepository;
76
77 public ApplicationHelper(
78 GeoServiceHelper geoServiceHelper,
79 GeoServiceRepository geoServiceRepository,
80 ConfigurationRepository configurationRepository,
81 ApplicationRepository applicationRepository,
82 FeatureSourceRepository featureSourceRepository,
83 EntityManager entityManager,
84 AuthorizationService authorizationService,
85 SearchIndexRepository searchIndexRepository) {
86 this.geoServiceHelper = geoServiceHelper;
87 this.geoServiceRepository = geoServiceRepository;
88 this.configurationRepository = configurationRepository;
89 this.applicationRepository = applicationRepository;
90 this.featureSourceRepository = featureSourceRepository;
91 this.entityManager = entityManager;
92 this.authorizationService = authorizationService;
93 this.searchIndexRepository = searchIndexRepository;
94 }
95
96 public Application getServiceApplication(String baseAppName, String projection, GeoService service) {
97 if (baseAppName == null) {
98 baseAppName = Optional.ofNullable(service.getSettings().getPublishing())
99 .map(ServicePublishingSettings::getBaseApp)
100 .orElseGet(() -> configurationRepository.get(Configuration.DEFAULT_BASE_APP));
101 }
102
103 Application baseApp = null;
104 if (baseAppName != null) {
105 baseApp = applicationRepository.findByName(baseAppName);
106 if (baseApp != null) {
107
108
109 entityManager.detach(baseApp);
110 }
111 }
112
113 Application app = baseApp != null ? baseApp : new Application().setContentRoot(new AppContent());
114
115 if (projection != null) {
116
117 throw new UnsupportedOperationException("Projection filtering not yet supported");
118 } else {
119 if (baseApp != null) {
120 projection = baseApp.getCrs();
121 } else {
122 projection = DEFAULT_WEB_MERCATOR_CRS;
123 }
124 }
125
126 app.setName(service.getId()).setTitle(service.getTitle()).setCrs(projection);
127
128 return app;
129 }
130
131 @Transactional
132 public MapResponse toMapResponse(Application app) {
133 MapResponse mapResponse = new MapResponse();
134 setCrsAndBounds(app, mapResponse);
135 setLayers(app, mapResponse);
136 return mapResponse;
137 }
138
139 public void setCrsAndBounds(Application a, MapResponse mapResponse) {
140 CoordinateReferenceSystem gtCrs = a.getGeoToolsCoordinateReferenceSystem();
141 if (gtCrs == null) {
142 throw new IllegalArgumentException("Invalid CRS: " + a.getCrs());
143 }
144
145 TMCoordinateReferenceSystem crs = new TMCoordinateReferenceSystem()
146 .code(a.getCrs())
147 .definition(((Formattable) gtCrs).toWKT(0))
148 .bounds(GeoToolsHelper.fromCRS(gtCrs))
149 .unit(Optional.ofNullable(CRSUtilities.getUnit(gtCrs.getCoordinateSystem()))
150 .map(Objects::toString)
151 .orElse(null));
152
153 Bounds maxExtent = a.getMaxExtent() != null ? a.getMaxExtent() : crs.getBounds();
154 Bounds initialExtent = a.getInitialExtent() != null ? a.getInitialExtent() : maxExtent;
155
156 mapResponse.crs(crs).maxExtent(maxExtent).initialExtent(initialExtent);
157 }
158
159 public void setLayers(Application app, MapResponse mr) {
160 new MapResponseLayerBuilder(app, mr).buildLayers();
161 }
162
163 private String getProxyUrl(GeoService geoService, Application application, AppTreeLayerNode appTreeLayerNode) {
164 String baseProxyUrl = linkTo(
165 GeoServiceProxyController.class,
166 Map.of(
167 "viewerKind", "app",
168 "viewerName", application.getName(),
169 "appLayerId", appTreeLayerNode.getId()))
170 .toString();
171
172 String protocolPath = "/" + geoService.getProtocol().getValue();
173
174 if (geoService.getProtocol() == TILES3D) {
175 return baseProxyUrl + protocolPath + "/" + GeoServiceProxyController.TILES3D_DESCRIPTION_PATH;
176 }
177 return baseProxyUrl + protocolPath;
178 }
179
180 private String getLegendProxyUrl(Application application, AppTreeLayerNode appTreeLayerNode) {
181 return linkTo(
182 GeoServiceProxyController.class,
183 Map.of(
184 "viewerKind",
185 "app",
186 "viewerName",
187 application.getName(),
188 "appLayerId",
189 appTreeLayerNode.getId(),
190 "protocol",
191 PROXIEDLEGEND.getValue()))
192 .toString();
193 }
194
195 private class MapResponseLayerBuilder {
196 private final Application app;
197 private final MapResponse mr;
198
199 private final Map<GeoServiceLayer, String> serviceLayerServiceIds = new HashMap<>();
200
201 public MapResponseLayerBuilder(Application app, MapResponse mr) {
202 this.app = app;
203 this.mr = mr;
204 }
205
206 public void buildLayers() {
207 if (app.getContentRoot() != null) {
208 buildBackgroundLayers();
209 buildOverlayLayers();
210 buildTerrainLayers();
211 }
212 }
213
214 private void buildBackgroundLayers() {
215 if (app.getContentRoot().getBaseLayerNodes() != null) {
216 for (AppTreeNode node : app.getContentRoot().getBaseLayerNodes()) {
217 addAppTreeNodeItem(node, mr.getBaseLayerTreeNodes());
218 }
219 }
220 }
221
222 private void buildOverlayLayers() {
223 if (app.getContentRoot().getLayerNodes() != null) {
224 for (AppTreeNode node : app.getContentRoot().getLayerNodes()) {
225 addAppTreeNodeItem(node, mr.getLayerTreeNodes());
226 }
227 }
228 }
229
230 private void buildTerrainLayers() {
231 if (app.getContentRoot().getTerrainLayerNodes() != null) {
232 for (AppTreeNode node : app.getContentRoot().getTerrainLayerNodes()) {
233 addAppTreeNodeItem(node, mr.getTerrainLayerTreeNodes());
234 }
235 }
236 }
237
238 private void addAppTreeNodeItem(AppTreeNode node, List<LayerTreeNode> layerTreeNodeList) {
239 LayerTreeNode layerTreeNode = new LayerTreeNode();
240 if ("AppTreeLayerNode".equals(node.getObjectType())) {
241 AppTreeLayerNode appTreeLayerNode = (AppTreeLayerNode) node;
242 layerTreeNode.setId(appTreeLayerNode.getId());
243 layerTreeNode.setAppLayerId(appTreeLayerNode.getId());
244 if (!addAppLayerItem(appTreeLayerNode)) {
245 return;
246 }
247
248 layerTreeNode.setName(appTreeLayerNode.getLayerName());
249 layerTreeNode.setDescription(appTreeLayerNode.getDescription());
250 } else if ("AppTreeLevelNode".equals(node.getObjectType())) {
251 AppTreeLevelNode appTreeLevelNode = (AppTreeLevelNode) node;
252 layerTreeNode.setId(appTreeLevelNode.getId());
253 layerTreeNode.setChildrenIds(appTreeLevelNode.getChildrenIds());
254 layerTreeNode.setRoot(Boolean.TRUE.equals(appTreeLevelNode.getRoot()));
255
256 layerTreeNode.setName(appTreeLevelNode.getTitle());
257 layerTreeNode.setDescription(appTreeLevelNode.getDescription());
258 }
259 layerTreeNodeList.add(layerTreeNode);
260 }
261
262 private boolean addAppLayerItem(AppTreeLayerNode layerRef) {
263 Triple<GeoService, GeoServiceLayer, GeoServiceLayerSettings> serviceWithLayer = findServiceLayer(layerRef);
264 GeoService service = serviceWithLayer.getLeft();
265 GeoServiceLayer serviceLayer = serviceWithLayer.getMiddle();
266
267 if (service == null || serviceLayer == null) {
268 return false;
269 }
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285 GeoServiceDefaultLayerSettings defaultLayerSettings = Optional.ofNullable(
286 service.getSettings().getDefaultLayerSettings())
287 .orElseGet(GeoServiceDefaultLayerSettings::new);
288 GeoServiceLayerSettings serviceLayerSettings =
289 Optional.ofNullable(serviceWithLayer.getRight()).orElseGet(GeoServiceLayerSettings::new);
290
291 AppLayerSettings appLayerSettings = app.getAppLayerSettings(layerRef);
292
293 String title = Objects.requireNonNullElse(
294 nullIfEmpty(appLayerSettings.getTitle()),
295
296
297 service.getTitleWithSettingsOverrides(layerRef.getLayerName()));
298
299
300
301 String description = ObjectUtils.firstNonNull(
302 nullIfEmpty(appLayerSettings.getDescription()),
303 nullIfEmpty(serviceLayerSettings.getDescription()),
304 nullIfEmpty(defaultLayerSettings.getDescription()));
305
306 String attribution = ObjectUtils.firstNonNull(
307 nullIfEmpty(appLayerSettings.getAttribution()),
308 nullIfEmpty(serviceLayerSettings.getAttribution()),
309 nullIfEmpty(defaultLayerSettings.getAttribution()));
310
311
312
313
314 boolean tilingDisabled = ObjectUtils.firstNonNull(
315 serviceLayerSettings.getTilingDisabled(), defaultLayerSettings.getTilingDisabled(), true);
316 Integer tilingGutter = ObjectUtils.firstNonNull(
317 serviceLayerSettings.getTilingGutter(), defaultLayerSettings.getTilingGutter(), 0);
318 boolean hiDpiDisabled = ObjectUtils.firstNonNull(
319 serviceLayerSettings.getHiDpiDisabled(), defaultLayerSettings.getHiDpiDisabled(), true);
320 TileLayerHiDpiMode hiDpiMode = ObjectUtils.firstNonNull(
321 serviceLayerSettings.getHiDpiMode(), defaultLayerSettings.getHiDpiMode(), null);
322
323 String hiDpiSubstituteLayer = serviceLayerSettings.getHiDpiSubstituteLayer();
324
325 TMFeatureType tmft = service.findFeatureTypeForLayer(serviceLayer, featureSourceRepository);
326
327 boolean proxied = service.getSettings().getUseProxy();
328
329 String legendImageUrl = serviceLayerSettings.getLegendImageId();
330 if (legendImageUrl == null && serviceLayer.getStyles() != null) {
331
332 URI serviceLegendUrl = GeoServiceHelper.getLayerLegendUrlFromStyles(service, serviceLayer);
333 legendImageUrl = serviceLegendUrl != null ? serviceLegendUrl.toString() : null;
334 if (null != legendImageUrl && proxied) {
335
336 legendImageUrl = getLegendProxyUrl(app, layerRef);
337 }
338 }
339
340 SearchIndex searchIndex = null;
341 if (appLayerSettings.getSearchIndexId() != null) {
342 searchIndex = searchIndexRepository
343 .findById(appLayerSettings.getSearchIndexId())
344 .orElse(null);
345 }
346
347 boolean webMercatorAvailable = this.isWebMercatorAvailable(service, serviceLayer);
348
349 mr.addAppLayersItem(new AppLayer()
350 .id(layerRef.getId())
351 .serviceId(serviceLayerServiceIds.get(serviceLayer))
352 .layerName(layerRef.getLayerName())
353 .hasAttributes(tmft != null)
354 .editable(TMFeatureTypeHelper.isEditable(app, layerRef, tmft))
355 .url(proxied ? getProxyUrl(service, app, layerRef) : null)
356
357
358 .maxScale(serviceLayer.getMaxScale())
359 .minScale(serviceLayer.getMinScale())
360 .title(title)
361 .tilingDisabled(tilingDisabled)
362 .tilingGutter(tilingGutter)
363 .hiDpiDisabled(hiDpiDisabled)
364 .hiDpiMode(hiDpiMode)
365 .hiDpiSubstituteLayer(hiDpiSubstituteLayer)
366 .minZoom(serviceLayerSettings.getMinZoom())
367 .maxZoom(serviceLayerSettings.getMaxZoom())
368 .tileSize(serviceLayerSettings.getTileSize())
369 .tileGridExtent(serviceLayerSettings.getTileGridExtent())
370 .opacity(appLayerSettings.getOpacity())
371 .autoRefreshInSeconds(appLayerSettings.getAutoRefreshInSeconds())
372 .searchIndex(
373 searchIndex != null
374 ? new LayerSearchIndex()
375 .id(searchIndex.getId())
376 .name(searchIndex.getName())
377 : null)
378 .legendImageUrl(legendImageUrl)
379 .visible(layerRef.getVisible())
380 .attribution(attribution)
381 .description(description)
382 .webMercatorAvailable(webMercatorAvailable)
383 .hiddenFunctionality(appLayerSettings.getHiddenFunctionality()));
384
385 return true;
386 }
387
388 private Triple<GeoService, GeoServiceLayer, GeoServiceLayerSettings> findServiceLayer(
389 AppTreeLayerNode layerRef) {
390 GeoService service =
391 geoServiceRepository.findById(layerRef.getServiceId()).orElse(null);
392 if (service == null) {
393 logger.warn(
394 "App {} references layer \"{}\" of missing service {}",
395 app.getId(),
396 layerRef.getLayerName(),
397 layerRef.getServiceId());
398 return Triple.of(null, null, null);
399 }
400
401 if (!authorizationService.userMayView(service)) {
402 return Triple.of(null, null, null);
403 }
404
405 if (authorizationService.mustDenyAccessForSecuredProxy(app, service)) {
406 return Triple.of(null, null, null);
407 }
408
409 GeoServiceLayer serviceLayer = service.findLayer(layerRef.getLayerName());
410
411 if (serviceLayer == null) {
412 logger.warn(
413 "App {} references layer \"{}\" not found in capabilities of service {}",
414 app.getId(),
415 layerRef.getLayerName(),
416 service.getId());
417 return Triple.of(null, null, null);
418 }
419
420 if (!authorizationService.userMayView(service, serviceLayer)) {
421 return Triple.of(null, null, null);
422 }
423
424 serviceLayerServiceIds.put(serviceLayer, service.getId());
425
426 if (mr.getServices().stream()
427 .filter(s -> s.getId().equals(service.getId()))
428 .findAny()
429 .isEmpty()) {
430 mr.addServicesItem(service.toJsonPojo(geoServiceHelper));
431 }
432
433 GeoServiceLayerSettings layerSettings = service.getLayerSettings(layerRef.getLayerName());
434 return Triple.of(service, serviceLayer, layerSettings);
435 }
436
437 private boolean isWebMercatorAvailable(GeoService service, GeoServiceLayer serviceLayer) {
438 if (service.getProtocol() == XYZ) {
439 return DEFAULT_WEB_MERCATOR_CRS.equals(service.getSettings().getXyzCrs());
440 }
441 if (service.getProtocol() == TILES3D || service.getProtocol() == QUANTIZEDMESH) {
442 return false;
443 }
444 while (serviceLayer != null) {
445 Set<String> layerCrs = serviceLayer.getCrs();
446 if (layerCrs.contains(DEFAULT_WEB_MERCATOR_CRS)) {
447 return true;
448 }
449 if (serviceLayer.getRoot()) {
450 break;
451 }
452 serviceLayer = service.getParentLayer(serviceLayer.getId());
453 }
454 return false;
455 }
456 }
457 }