1
2
3
4
5
6 package org.tailormap.api.controller;
7
8 import static org.springframework.web.bind.annotation.RequestMethod.GET;
9 import static org.springframework.web.bind.annotation.RequestMethod.POST;
10
11 import io.micrometer.core.annotation.Timed;
12 import java.io.IOException;
13 import java.io.Serializable;
14 import java.lang.invoke.MethodHandles;
15 import java.util.regex.Pattern;
16 import org.geotools.api.data.Query;
17 import org.geotools.api.data.SimpleFeatureSource;
18 import org.geotools.api.filter.Filter;
19 import org.geotools.api.referencing.FactoryException;
20 import org.geotools.api.referencing.operation.MathTransform;
21 import org.geotools.data.oracle.OracleDialect;
22 import org.geotools.data.postgis.PostGISDialect;
23 import org.geotools.data.sqlserver.SQLServerDialect;
24 import org.geotools.filter.text.cql2.CQLException;
25 import org.geotools.filter.text.ecql.ECQL;
26 import org.geotools.geometry.jts.ReferencedEnvelope;
27 import org.geotools.jdbc.JDBCDataStore;
28 import org.locationtech.jts.geom.Envelope;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.springframework.http.HttpStatus;
32 import org.springframework.http.MediaType;
33 import org.springframework.http.ResponseEntity;
34 import org.springframework.transaction.annotation.Transactional;
35 import org.springframework.validation.annotation.Validated;
36 import org.springframework.web.bind.annotation.ModelAttribute;
37 import org.springframework.web.bind.annotation.RequestMapping;
38 import org.springframework.web.bind.annotation.RequestParam;
39 import org.springframework.web.server.ResponseStatusException;
40 import org.tailormap.api.annotation.AppRestController;
41 import org.tailormap.api.geotools.TransformationUtil;
42 import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
43 import org.tailormap.api.geotools.processing.GeometryProcessor;
44 import org.tailormap.api.persistence.Application;
45 import org.tailormap.api.persistence.GeoService;
46 import org.tailormap.api.persistence.TMFeatureType;
47 import org.tailormap.api.persistence.helper.GeoToolsHelper;
48 import org.tailormap.api.persistence.json.AppTreeLayerNode;
49 import org.tailormap.api.persistence.json.GeoServiceLayer;
50 import org.tailormap.api.repository.FeatureSourceRepository;
51
52 @AppRestController
53 @Validated
54 @RequestMapping(
55 path = "${tailormap-api.base-path}/{viewerKind}/{viewerName}/layer/{appLayerId}/bounds",
56 produces = MediaType.APPLICATION_JSON_VALUE)
57 public class LayerBoundsController {
58 private static final Logger logger =
59 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
60
61 private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
62
63 private final FeatureSourceRepository featureSourceRepository;
64
65 public LayerBoundsController(
66 FeatureSourceFactoryHelper featureSourceFactoryHelper, FeatureSourceRepository featureSourceRepository) {
67 this.featureSourceFactoryHelper = featureSourceFactoryHelper;
68 this.featureSourceRepository = featureSourceRepository;
69 }
70
71 @Transactional
72 @RequestMapping(method = {GET, POST})
73 @Timed(value = "calculate_layer_bounds", description = "time spent calculating (filtered) layer bounds")
74 public ResponseEntity<Serializable> calculateLayerBounds(
75 @ModelAttribute AppTreeLayerNode appTreeLayerNode,
76 @ModelAttribute GeoService service,
77 @ModelAttribute GeoServiceLayer layer,
78 @ModelAttribute Application application,
79 @RequestParam(required = false, name = "filter") String filterCQL) {
80
81 if (layer == null) {
82 throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Can't find layer " + appTreeLayerNode);
83 }
84
85 TMFeatureType tmft = service.findFeatureTypeForLayer(layer, featureSourceRepository);
86 if (tmft == null) {
87 throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Layer does not have feature type");
88 }
89
90 SimpleFeatureSource featureSource = null;
91 try {
92 featureSource = featureSourceFactoryHelper.openGeoToolsFeatureSource(tmft);
93 Query query = new Query(tmft.getName());
94 query.setHandle("calculateLayerBounds");
95
96 if (filterCQL != null && !filterCQL.isEmpty()) {
97 try {
98 Filter parsedFilter = ECQL.toFilter(filterCQL);
99
100
101
102 MathTransform transform =
103 TransformationUtil.getTransformationToDataSource(application, featureSource);
104 String defaultGeom = tmft.getDefaultGeometryAttribute();
105 if (transform != null
106 && defaultGeom != null
107
108
109
110 && Pattern.compile("\\(\\s*" + Pattern.quote(defaultGeom) + "\\s*,")
111 .matcher(filterCQL)
112 .find()) {
113
114
115
116 logger.warn(
117 "Application CRS is different from feature source CRS and filter '{}' contains spatial predicates on the default geometry, but filter geometries are not transformed. This will lead to incorrect results or errors.",
118 filterCQL);
119 }
120
121 query.setFilter(parsedFilter);
122 } catch (CQLException e) {
123 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
124 }
125 }
126
127 if ((filterCQL == null || filterCQL.isBlank())
128 && featureSource.getDataStore() instanceof JDBCDataStore jdbcDataStore) {
129
130 logger.debug(
131 "Turning off estimated extents for layer {} because no filter is applied and the datastore is a JDBCDataStore",
132 appTreeLayerNode.getLayerName());
133
134 switch (jdbcDataStore.getSQLDialect()) {
135 case SQLServerDialect sqlServerDialect -> sqlServerDialect.setEstimatedExtentsEnabled(false);
136 case PostGISDialect postGISDialect -> postGISDialect.setEstimatedExtentsEnabled(false);
137 case OracleDialect oracleDialect -> oracleDialect.setEstimatedExtentsEnabled(false);
138 default -> {
139
140 }
141 }
142 }
143
144
145
146 ReferencedEnvelope referencedEnvelope = featureSource.getBounds(query);
147 if (referencedEnvelope == null) {
148 referencedEnvelope = featureSource.getFeatures(query).getBounds();
149 }
150
151 if (referencedEnvelope == null || referencedEnvelope.isNull()) {
152 throw new ResponseStatusException(
153 HttpStatus.BAD_REQUEST,
154 "No features found for layer "
155 + appTreeLayerNode.getLayerName()
156 + " and filter '"
157 + filterCQL
158 + "'");
159 }
160
161
162 Envelope envelope = GeometryProcessor.transformEnvelope(
163 referencedEnvelope, TransformationUtil.getTransformationToApplication(application, featureSource));
164
165 return ResponseEntity.ok(GeoToolsHelper.fromEnvelope(envelope));
166 } catch (FactoryException | IOException e) {
167 logger.error("Error calculating bounds for layer {} and filter '{}'", appTreeLayerNode, filterCQL, e);
168 throw new ResponseStatusException(
169 HttpStatus.INTERNAL_SERVER_ERROR,
170 "Error calculating layer bounds for: " + appTreeLayerNode.getLayerName() + ". " + e.getMessage(),
171 e);
172 } finally {
173 if (featureSource != null) {
174 featureSource.getDataStore().dispose();
175 }
176 }
177 }
178 }