From: Mathieu Baudier Date: Mon, 4 Sep 2023 12:12:49 +0000 (+0200) Subject: SLD styling support, based on Geographical CSS X-Git-Tag: v2.3.16~39 X-Git-Url: https://git.argeo.org/?p=gpl%2Fargeo-suite.git;a=commitdiff_plain;h=737346afd15e56f9339a7c41ed4e26d65bbcbe69 SLD styling support, based on Geographical CSS --- diff --git a/org.argeo.app.geo.js/package-lock.json b/org.argeo.app.geo.js/package-lock.json index 9c61428..071bfea 100644 --- a/org.argeo.app.geo.js/package-lock.json +++ b/org.argeo.app.geo.js/package-lock.json @@ -8,6 +8,7 @@ "version": "2.3.0.next", "license": "GPL", "dependencies": { + "@nieuwlandgeo/sldreader": "^0.3.1", "ol": "7.5.x" }, "devDependencies": { @@ -232,6 +233,14 @@ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, + "node_modules/@nieuwlandgeo/sldreader": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@nieuwlandgeo/sldreader/-/sldreader-0.3.1.tgz", + "integrity": "sha512-gP1dw7ftVT34L6nv8dDtERNIJYENwe2I37Vwdm3NQH+KKHDk7vwrTANxvgKgbNybMXHF29jvI97Z/bkZYBqdxQ==", + "peerDependencies": { + "ol": ">= 5.3.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7080,6 +7089,12 @@ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, + "@nieuwlandgeo/sldreader": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@nieuwlandgeo/sldreader/-/sldreader-0.3.1.tgz", + "integrity": "sha512-gP1dw7ftVT34L6nv8dDtERNIJYENwe2I37Vwdm3NQH+KKHDk7vwrTANxvgKgbNybMXHF29jvI97Z/bkZYBqdxQ==", + "requires": {} + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/org.argeo.app.geo.js/package.json b/org.argeo.app.geo.js/package.json index 76cd660..9869bbe 100644 --- a/org.argeo.app.geo.js/package.json +++ b/org.argeo.app.geo.js/package.json @@ -23,6 +23,7 @@ "webpack-merge": "^5.9.0" }, "dependencies": { + "@nieuwlandgeo/sldreader": "^0.3.1", "ol": "7.5.x" } } diff --git a/org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js b/org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js index 8206d4f..13597fa 100644 --- a/org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js +++ b/org.argeo.app.geo.js/src/org.argeo.app.geo.js/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 } from 'ol/proj.js'; +import { fromLonLat, getPointResolution } from 'ol/proj.js'; import VectorSource from 'ol/source/Vector.js'; import Feature from 'ol/Feature.js'; import { Point } from 'ol/geom.js'; @@ -17,6 +17,8 @@ import Select from 'ol/interaction/Select.js'; import Overlay from 'ol/Overlay.js'; import { Style, Icon } from 'ol/style.js'; +import * as SLDReader from '@nieuwlandgeo/sldreader'; + import MapPart from './MapPart.js'; import { SentinelCloudless } from './OpenLayerTileSources.js'; @@ -68,20 +70,32 @@ export default class OpenLayersMapPart extends MapPart { })); } - addUrlLayer(url, format, style) { - let vectorSource; + addUrlLayer(url, format, style, sld) { + let featureFormat; if (format === 'GEOJSON') { - vectorSource = new VectorSource({ url: url, format: new GeoJSON() }) + featureFormat = new GeoJSON(); } else if (format === 'GPX') { - vectorSource = new VectorSource({ url: url, format: new GPX() }) + featureFormat = new GPX(); + } else { + throw new Error("Unsupported format " + format); } - this.#map.addLayer(new VectorLayer({ + const vectorSource = new VectorSource({ + url: url, + format: featureFormat, + }); + const vectorLayer = new VectorLayer({ source: vectorSource, - style: style, - })); + }); + if (sld) { + this.#applySLD(vectorLayer, style); + } else { + vectorLayer.setStyle(style); + } + this.#map.addLayer(vectorLayer); } + /* CALLBACKS */ enableFeatureSingleClick() { // we cannot use 'this' in the function provided to OpenLayers @@ -173,13 +187,39 @@ export default class OpenLayersMapPart extends MapPart { // // STATIC FOR EXTENSION // - static newStyle(args){ + static newStyle(args) { return new Style(args); } - - static newIcon(args){ + + static newIcon(args) { return new Icon(args); } - - + + // + // SLD STYLING + // + #applySLD(vectorLayer, text) { + const sldObject = SLDReader.Reader(text); + const sldLayer = SLDReader.getLayer(sldObject); + const style = SLDReader.getStyle(sldLayer); + const featureTypeStyle = style.featuretypestyles[0]; + + const viewProjection = this.#map.getView().getProjection(); + const olStyleFunction = SLDReader.createOlStyleFunction(featureTypeStyle, { + // Use the convertResolution option to calculate a more accurate resolution. + convertResolution: viewResolution => { + const viewCenter = this.#map.getView().getCenter(); + return getPointResolution(viewProjection, viewResolution, viewCenter); + }, + // If you use point icons with an ExternalGraphic, you have to use imageLoadCallback + // to update the vector layer when an image finishes loading. + // If you do not do this, the image will only be visible after next layer pan/zoom. + imageLoadedCallback: () => { + vectorLayer.changed(); + }, + }); + vectorLayer.setStyle(olStyleFunction); + } + + } diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/GeoUtils.java b/org.argeo.app.geo/src/org/argeo/app/geo/GeoUtils.java index 1f8846d..3cec484 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/GeoUtils.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/GeoUtils.java @@ -11,6 +11,7 @@ import java.util.Map; import javax.measure.Quantity; import javax.measure.quantity.Area; +import javax.xml.transform.TransformerException; import org.geotools.data.DefaultTransaction; import org.geotools.data.Transaction; @@ -21,19 +22,41 @@ import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; +import org.geotools.factory.CommonFactoryFinder; import org.geotools.geometry.jts.JTS; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; +import org.geotools.styling.AnchorPoint; +import org.geotools.styling.Displacement; +import org.geotools.styling.FeatureTypeConstraint; +import org.geotools.styling.FeatureTypeStyle; +import org.geotools.styling.Fill; +import org.geotools.styling.Graphic; +import org.geotools.styling.PointSymbolizer; +import org.geotools.styling.Rule; +import org.geotools.styling.Stroke; +import org.geotools.styling.Style; +import org.geotools.styling.StyleFactory; +import org.geotools.styling.StyledLayerDescriptor; +import org.geotools.styling.UserLayer; +import org.geotools.styling.css.CssParser; +import org.geotools.styling.css.CssTranslator; +import org.geotools.styling.css.Stylesheet; +import org.geotools.xml.styling.SLDTransformer; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; +import org.opengis.filter.Filter; +import org.opengis.filter.FilterFactory2; +import org.opengis.filter.expression.Expression; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; +import org.opengis.style.GraphicalSymbol; import si.uom.SI; import tech.units.indriya.quantity.Quantities; @@ -153,6 +176,165 @@ public class GeoUtils { } } + public static org.opengis.style.Style createStyleFromCss(String css) { + Stylesheet ss = CssParser.parse(css); + CssTranslator translator = new CssTranslator(); + org.opengis.style.Style style = translator.translate(ss); + +// try { +// SLDTransformer styleTransform = new SLDTransformer(); +// String xml = styleTransform.transform(style); +// System.out.println(xml); +// } catch (TransformerException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } + + return style; + } + + public static String createSldFromCss(String name, String title, String css) { + + StyleFactory sf = CommonFactoryFinder.getStyleFactory(); + + StyledLayerDescriptor sld = sf.createStyledLayerDescriptor(); + sld.setName(name); + sld.setTitle(title); + + UserLayer layer = sf.createUserLayer(); + layer.setName("default"); + + org.opengis.style.Style style = createStyleFromCss(css); + layer.userStyles().add((Style) style); + + sld.layers().add(layer); + try { + SLDTransformer styleTransform = new SLDTransformer(); + String xml = styleTransform.transform(sld); +// System.out.println(xml); + return xml; + } catch (TransformerException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String... args) { + String css = """ + * { + mark: symbol(circle); + mark-size: 6px; + } + + :mark { + fill: red; + } + + """; + createSldFromCss("test", "Test", css); + } + + public static String createTestSLD() { + + StyleFactory sf = CommonFactoryFinder.getStyleFactory(); + FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(); + + StyledLayerDescriptor sld = sf.createStyledLayerDescriptor(); + sld.setName("sld"); + sld.setTitle("Example"); + sld.setAbstract("Example Style Layer Descriptor"); + + UserLayer layer = sf.createUserLayer(); + layer.setName("layer"); + + // + // define constraint limited what features the sld applies to + FeatureTypeConstraint constraint = sf.createFeatureTypeConstraint("Feature", Filter.INCLUDE); + + layer.layerFeatureConstraints().add(constraint); + + // + // create a "user defined" style + Style style = sf.createStyle(); + style.setName("style"); + style.getDescription().setTitle("User Style"); + style.getDescription().setAbstract("Definition of Style"); + + // + // define feature type styles used to actually define how features are rendered + FeatureTypeStyle featureTypeStyle = sf.createFeatureTypeStyle(); + + // RULE 1 + // first rule to draw cities + Rule rule1 = sf.createRule(); + rule1.setName("rule1"); + rule1.getDescription().setTitle("City"); + rule1.getDescription().setAbstract("Rule for drawing cities"); +// rule1.setFilter(ff.less(ff.property("POPULATION"), ff.literal(50000))); + + // + // create the graphical mark used to represent a city + Stroke stroke = sf.stroke(ff.literal("#000000"), null, null, null, null, null, null); + Fill fill = sf.fill(null, ff.literal(java.awt.Color.BLUE), ff.literal(1.0)); + +// // OnLineResource implemented by gt-metadata - so no factory! +// OnLineResourceImpl svg = new OnLineResourceImpl(new URI("file:city.svg")); +// svg.freeze(); // freeze to prevent modification at runtime +// +// OnLineResourceImpl png = new OnLineResourceImpl(new URI("file:city.png")); +// png.freeze(); // freeze to prevent modification at runtime + + // + // List of symbols is considered in order with the rendering engine choosing + // the first one it can handle. Allowing for svg, png, mark order + List symbols = new ArrayList<>(); +// symbols.add(sf.externalGraphic(svg, "svg", null)); // svg preferred +// symbols.add(sf.externalGraphic(png, "png", null)); // png preferred + symbols.add(sf.mark(ff.literal("circle"), fill, stroke)); // simple circle backup plan + + Expression opacity = null; // use default + Expression size = ff.literal(10); + Expression rotation = null; // use default + AnchorPoint anchor = null; // use default + Displacement displacement = null; // use default + + // define a point symbolizer of a small circle + Graphic city = sf.graphic(symbols, opacity, size, rotation, anchor, displacement); + PointSymbolizer pointSymbolizer = sf.pointSymbolizer("point", ff.property("the_geom"), null, null, city); + + rule1.symbolizers().add(pointSymbolizer); + + featureTypeStyle.rules().add(rule1); + + // + // RULE 2 Default + +// List dotSymbols = new ArrayList<>(); +// dotSymbols.add(sf.mark(ff.literal("circle"), null, null)); +// Graphic dotGraphic = sf.graphic(dotSymbols, null, ff.literal(3), null, null, null); +// PointSymbolizer dotSymbolizer = sf.pointSymbolizer("dot", ff.property("the_geom"), null, null, dotGraphic); +// List symbolizers = new ArrayList<>(); +// symbolizers.add(dotSymbolizer); +// Filter other = null; // null will mark this rule as "other" accepting all remaining features +// Rule rule2 = sf.rule("default", null, null, Double.MIN_VALUE, Double.MAX_VALUE, symbolizers, other); +// featureTypeStyle.rules().add(rule2); + + style.featureTypeStyles().add(featureTypeStyle); + + layer.userStyles().add(style); + + sld.layers().add(layer); + + try { + SLDTransformer styleTransform = new SLDTransformer(); + String xml = styleTransform.transform(sld); + System.out.println(xml); + return xml; + } catch (TransformerException e) { + throw new IllegalStateException(e); + } + + } + /** Singleton. */ private GeoUtils() { } 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 6408b40..070bcb8 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 @@ -96,9 +96,9 @@ public class WfsHttpHandler implements HttpHandler { SimpleFeatureType TYPE; try { if (gpx) - TYPE = DataUtilities.createType("Content", "the_geom:Polygon:srid=4326,path:String,type:String"); + TYPE = DataUtilities.createType("Content", "the_geom:Polygon:srid=4326,path:String,type:String,name:String"); else - TYPE = DataUtilities.createType("Content", "the_geom:Point:srid=4326,path:String,type:String"); + TYPE = DataUtilities.createType("Content", "the_geom:Point:srid=4326,path:String,type:String,name:String"); } catch (SchemaException e) { throw new RuntimeException(e); } @@ -141,6 +141,7 @@ public class WfsHttpHandler implements HttpHandler { featureBuilder.add(NamespaceUtils.toPrefixedName(contentClasses.get(0))); } } + featureBuilder.add(NamespaceUtils.toPrefixedName(c.getName())); String uuid = c.attr(LdapAttr.entryUUID); diff --git a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java index 2e90900..ece9508 100644 --- a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java +++ b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java @@ -3,7 +3,7 @@ package org.argeo.app.geo.swt; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; import java.util.function.Function; - +import org.argeo.app.geo.GeoUtils; import org.argeo.app.geo.ux.JsImplementation; import org.argeo.app.geo.ux.MapPart; import org.argeo.app.swt.js.SwtBrowserJsPart; @@ -40,7 +40,12 @@ public class SwtJSMapPart extends SwtBrowserJsPart implements MapPart { @Override public void addUrlLayer(String url, GeoFormat format, String style) { - callMapMethod("addUrlLayer('%s', '%s', %s)", url, format.name(), style); + callMapMethod("addUrlLayer('%s', '%s', %s, false)", url, format.name(), style); + } + + public void addCssUrlLayer(String url, GeoFormat format, String css) { + String style = GeoUtils.createSldFromCss("layer", "Layer", css); + callMapMethod("addUrlLayer('%s', '%s', '%s', true)", url, format.name(), style); } @Override