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