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