From b384a9cbe93b83b3aa94fe46cf2ff0a929f0332c Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Wed, 4 Oct 2023 12:43:52 +0200 Subject: [PATCH] bounding box support --- js/src/geo/OpenLayersMapPart.js | 56 +++++++++++++++++-- js/src/geo/export-package.js | 2 + .../src/org/argeo/app/ux/js/JsClient.java | 2 + .../src/org/argeo/app/ux/js/JsReference.java | 17 ++++++ .../org/argeo/app/geo/acr/GeoEntityUtils.java | 22 ++++++-- .../argeo/app/geo/http/WfsHttpHandler.java | 55 ++++++++++++++++-- .../argeo/app/geo/ux/OpenLayersMapPart.java | 4 ++ .../src/org/argeo/app/ol/VectorSource.java | 13 ++++- .../src/org/argeo/app/ol/View.java | 4 ++ 9 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 org.argeo.app.core/src/org/argeo/app/ux/js/JsReference.java diff --git a/js/src/geo/OpenLayersMapPart.js b/js/src/geo/OpenLayersMapPart.js index a759566..71b9a17 100644 --- a/js/src/geo/OpenLayersMapPart.js +++ b/js/src/geo/OpenLayersMapPart.js @@ -6,7 +6,7 @@ import Map from 'ol/Map.js'; import View from 'ol/View.js'; import OSM from 'ol/source/OSM.js'; import TileLayer from 'ol/layer/Tile.js'; -import { fromLonLat, getPointResolution } from 'ol/proj.js'; +import { fromLonLat, getPointResolution, transformExtent } from 'ol/proj.js'; import VectorSource from 'ol/source/Vector.js'; import Feature from 'ol/Feature.js'; import { Point } from 'ol/geom.js'; @@ -21,6 +21,8 @@ import * as SLDReader from '@nieuwlandgeo/sldreader'; import MapPart from './MapPart.js'; +import { bbox } from 'ol/loadingstrategy'; + /** OpenLayers implementation of MapPart. */ export default class OpenLayersMapPart extends MapPart { /** The OpenLayers Map. */ @@ -46,12 +48,14 @@ export default class OpenLayersMapPart extends MapPart { // transition: 0, // }), ], - // view: new View({ - // center: [0, 0], - // zoom: 2, - // }), + // view: new View({ + // projection: 'EPSG:4326', + // center: [0, 0], + // zoom: 2, + // }), target: this.getMapName(), }); + //this.#map.getView().set('projection', 'EPSG:4326', true); } /* GEOGRAPHICAL METHODS */ @@ -282,4 +286,44 @@ export default class OpenLayersMapPart extends MapPart { }); vectorLayer.setStyle(olStyleFunction); } -} \ No newline at end of file + + // + // BBOX + // + applyBboxStrategy(layerName) { + const layer = this.getLayerByName(layerName); + const vectorSource = layer.getSource(); + const baseUrl = vectorSource.getUrl(); + if (typeof baseUrl === 'function') + throw new Error('A strategy was already applied to layer ' + layerName); + + const loadFunction = function(extent, resolution, projection, success, failure) { + + const proj = projection.getCode(); + var bbox = transformExtent(extent, proj, 'EPSG:4326'); + + const url = baseUrl + '&' + + 'bbox=' + bbox.join(',') ; +// 'bbox=' + extent.join(',') + ',' + proj; + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + const onError = function() { + vectorSource.removeLoadedExtent(extent); + failure(); + } + xhr.onerror = onError; + xhr.onload = function() { + if (xhr.status == 200) { + const features = vectorSource.getFormat().readFeatures(xhr.responseText); + vectorSource.addFeatures(features); + success(features); + } else { + onError(); + } + } + xhr.send(); + } + + vectorSource.setLoader(loadFunction); + } +} diff --git a/js/src/geo/export-package.js b/js/src/geo/export-package.js index 8aaf937..ca10dfc 100644 --- a/js/src/geo/export-package.js +++ b/js/src/geo/export-package.js @@ -8,6 +8,7 @@ import TileLayer from 'ol/layer/Tile.js'; import VectorSource from 'ol/source/Vector.js'; import VectorLayer from 'ol/layer/Vector.js'; import GeoJSON from 'ol/format/GeoJSON.js'; +import {bbox} from 'ol/loadingstrategy'; // PSEUDO PACKAGE if (typeof globalThis.argeo === 'undefined') @@ -34,6 +35,7 @@ globalThis.argeo.tp.ol.OSM = OSM; globalThis.argeo.tp.ol.VectorSource = VectorSource; globalThis.argeo.tp.ol.VectorLayer = VectorLayer; globalThis.argeo.tp.ol.GeoJSON = GeoJSON; +globalThis.argeo.tp.ol.bbox = bbox; "use strict"; diff --git a/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java b/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java index f889fd9..060f120 100644 --- a/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java +++ b/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java @@ -94,6 +94,8 @@ public interface JsClient { return jsObject.newJs(); else return jsObject.getJsReference(); + } else if (o instanceof JsReference jsReference) { + return jsReference.get(); } else return '\'' + o.toString() + '\''; } diff --git a/org.argeo.app.core/src/org/argeo/app/ux/js/JsReference.java b/org.argeo.app.core/src/org/argeo/app/ux/js/JsReference.java new file mode 100644 index 0000000..0cd8d81 --- /dev/null +++ b/org.argeo.app.core/src/org/argeo/app/ux/js/JsReference.java @@ -0,0 +1,17 @@ +package org.argeo.app.ux.js; + +import java.util.function.Supplier; + +/** Plain JS reference, to be directly serialised. */ +public class JsReference implements Supplier { + private final String reference; + + public JsReference(String reference) { + this.reference = reference; + } + + public String get() { + return reference; + } + +} diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/acr/GeoEntityUtils.java b/org.argeo.app.geo/src/org/argeo/app/geo/acr/GeoEntityUtils.java index 6019ee9..f10d89e 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/acr/GeoEntityUtils.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/acr/GeoEntityUtils.java @@ -55,7 +55,7 @@ public class GeoEntityUtils { } catch (IOException e) { throw new UncheckedIOException("Cannot add geometry " + name + " to " + c, e); } - updateBoundingBox(c); + // updateBoundingBox(c); } public static T getGeometry(Content c, QNamed name, Class clss) { @@ -81,9 +81,10 @@ public class GeoEntityUtils { if (c.hasContentClass(EntityType.geopoint)) { Double lat = c.get(WGS84PosName.lat, Double.class).orElseThrow(); Double lon = c.get(WGS84PosName.lon, Double.class).orElseThrow(); - Double alt = c.get(WGS84PosName.alt, Double.class).orElse(null); - return JTS.GEOMETRY_FACTORY_WGS84 - .createPoint(alt != null ? new Coordinate(lat, lon, alt) : new Coordinate(lat, lon)); + return JTS.GEOMETRY_FACTORY_WGS84.createPoint(new Coordinate(lat, lon)); +// Double alt = c.get(WGS84PosName.alt, Double.class).orElse(null); +// return JTS.GEOMETRY_FACTORY_WGS84 +// .createPoint(alt != null ? new Coordinate(lat, lon, alt) : new Coordinate(lat, lon)); } return null; } @@ -118,6 +119,19 @@ public class GeoEntityUtils { entity.put(EntityName.maxLon, bbox.getMaxY()); } + public static void updateBoundingBox(Content entity, QName prop) { + Geometry geom = getGeometry(entity, prop, Geometry.class); + if (geom == null) + return; + entity.addContentClasses(EntityType.geobounded.qName()); + + Envelope bbox = geom.getEnvelopeInternal(); + entity.put(EntityName.minLat, bbox.getMinX()); + entity.put(EntityName.minLon, bbox.getMinY()); + entity.put(EntityName.maxLat, bbox.getMaxX()); + entity.put(EntityName.maxLon, bbox.getMaxY()); + } + /** singleton */ private GeoEntityUtils() { } diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java index 6eb1279..580866d 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java @@ -35,7 +35,6 @@ import org.argeo.cms.acr.json.AcrJsonUtils; import org.argeo.cms.http.HttpHeader; import org.argeo.cms.http.server.HttpServerUtils; import org.argeo.cms.util.LangUtils; -import org.argeo.cms.util.StreamUtils; import org.geotools.data.DataUtilities; import org.geotools.data.geojson.GeoJSONWriter; import org.geotools.feature.DefaultFeatureCollection; @@ -43,10 +42,12 @@ import org.geotools.feature.NameImpl; import org.geotools.feature.SchemaException; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.geometry.jts.JTSFactoryFinder; +import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.wfs.GML; import org.geotools.wfs.GML.Version; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; @@ -56,6 +57,10 @@ import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.Name; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -72,6 +77,7 @@ public class WfsHttpHandler implements HttpHandler { final static String OUTPUT_FORMAT = "outputFormat"; final static String TYPE_NAMES = "typeNames"; final static String CQL_FILTER = "cql_filter"; + final static String BBOX = "bbox"; private final Map featureAdapters = new HashMap<>(); @@ -81,6 +87,7 @@ public class WfsHttpHandler implements HttpHandler { ContentSession session = HttpServerUtils.getContentSession(contentRepository, exchange); // Content content = session.get(path); + // PARAMETERS Map> parameters = HttpServerUtils.parseParameters(exchange); String cql = getKvpParameter(parameters, CQL_FILTER); String typeNamesStr = getKvpParameter(parameters, TYPE_NAMES); @@ -88,6 +95,40 @@ public class WfsHttpHandler implements HttpHandler { if (outputFormat == null) { outputFormat = "application/json"; } + String bboxStr = getKvpParameter(parameters, BBOX); + log.debug(bboxStr); + final Envelope bbox; + if (bboxStr != null) { + String srs; + String[] arr = bboxStr.split(","); + // TODO check SRS and convert to WGS84 + double minLat = Double.parseDouble(arr[0]); + double minLon = Double.parseDouble(arr[1]); + double maxLat = Double.parseDouble(arr[2]); + double maxLon = Double.parseDouble(arr[3]); + if (arr.length == 5) { + srs = arr[4]; + } else { + srs = null; + } + + if (srs != null) { + try { + CoordinateReferenceSystem sourceCRS = CRS.decode(srs); + CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:4326"); + MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS, true); + bbox = org.geotools.geometry.jts.JTS.transform( + new Envelope(new Coordinate(minLat, minLon), new Coordinate(maxLat, maxLon)), transform); + } catch (FactoryException | TransformException e) { + throw new IllegalArgumentException("Cannot convert bounding box", e); + // bbox = null; + } + } else { + bbox = new Envelope(new Coordinate(minLat, minLon), new Coordinate(maxLat, maxLon)); + } + } else { + bbox = null; + } switch (outputFormat) { case "application/json" -> { @@ -114,13 +155,13 @@ public class WfsHttpHandler implements HttpHandler { if (typeNames.size() > 1) throw new UnsupportedOperationException("Only one type name is currently supported"); + // QUERY Stream res = session.search((search) -> { if (cql != null) { CqlUtils.filter(search.from(path), cql); } else { search.from(path); } -// search.getWhere().any((f) -> { for (QName typeName : typeNames) { FeatureAdapter featureAdapter = featureAdapters.get(typeName); if (featureAdapter == null) @@ -128,7 +169,13 @@ public class WfsHttpHandler implements HttpHandler { // f.isContentClass(typeName); featureAdapter.addConstraintsForFeature((AndFilter) search.getWhere(), typeName); } -// }); + + if (bbox != null) { + search.getWhere().gte(EntityName.minLat, bbox.getMinX()); + search.getWhere().gte(EntityName.minLon, bbox.getMinY()); + search.getWhere().lte(EntityName.maxLat, bbox.getMaxX()); + search.getWhere().lte(EntityName.maxLon, bbox.getMaxY()); + } }); exchange.sendResponseHeaders(200, 0); @@ -230,7 +277,7 @@ public class WfsHttpHandler implements HttpHandler { if (featureId != null) generator.write("id", featureId); -// GeoJson.writeBBox(generator, defaultGeometry); + GeoJson.writeBBox(generator, defaultGeometry); generator.writeStartObject(GeoJson.GEOMETRY); GeoJson.writeGeometry(generator, defaultGeometry); generator.writeEnd();// geometry object diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java b/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java index 8fad7e2..c66ed3a 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java @@ -28,6 +28,10 @@ public class OpenLayersMapPart extends AbstractGeoJsObject { executeMethod(getMethodName(), layerName, styledLayerName); } + public void applyBboxStrategy(String layerName) { + executeMethod(getMethodName(), layerName); + } + public Layer getLayer(String name) { // TODO deal with not found String reference = "getLayerByName('" + name + "')"; diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java b/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java index 3b60d0b..122ef36 100644 --- a/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java +++ b/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java @@ -1,5 +1,7 @@ package org.argeo.app.ol; +import org.argeo.app.ux.js.JsReference; + public class VectorSource extends Source { public VectorSource(Object... args) { @@ -7,8 +9,17 @@ public class VectorSource extends Source { } public VectorSource(String url, FeatureFormat format) { - setUrl(url); + this(url, format, false); + } + + public VectorSource(String url, FeatureFormat format, boolean bboxStrategy) { setFormat(format); + if (bboxStrategy) { + setUrl(url); + getNewOptions().put("strategy", new JsReference(getJsPackage() + ".bbox")); + } else { + setUrl(url); + } } public void setUrl(String url) { diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/View.java b/org.argeo.app.geo/src/org/argeo/app/ol/View.java index 8ed2de3..f607208 100644 --- a/org.argeo.app.geo/src/org/argeo/app/ol/View.java +++ b/org.argeo.app.geo/src/org/argeo/app/ol/View.java @@ -18,4 +18,8 @@ public class View extends AbstractOlObject { else executeMethod(getMethodName(), zoom); } + +// public void setProjection(String projection) { +// doSetValue(getMethodName(), "projection", projection); +// } } -- 2.30.2