SLD styling support, based on Geographical CSS
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 4 Sep 2023 12:12:49 +0000 (14:12 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 4 Sep 2023 12:12:49 +0000 (14:12 +0200)
org.argeo.app.geo.js/package-lock.json
org.argeo.app.geo.js/package.json
org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js
org.argeo.app.geo/src/org/argeo/app/geo/GeoUtils.java
org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java
swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java

index 9c61428890c611bf282b47aea4546dc652cb661e..071bfeaf603406f57336302e56ea2759c20257f8 100644 (file)
@@ -8,6 +8,7 @@
                        "version": "2.3.0.next",
                        "license": "GPL",
                        "dependencies": {
+                               "@nieuwlandgeo/sldreader": "^0.3.1",
                                "ol": "7.5.x"
                        },
                        "devDependencies": {
                        "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",
                        "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",
index 76cd660c556057f2a636c9d9bef5cd5ccfc2da73..9869bbeae949006d71c384294fc9df3611b8b5da 100644 (file)
@@ -23,6 +23,7 @@
                "webpack-merge": "^5.9.0"
        },
        "dependencies": {
+               "@nieuwlandgeo/sldreader": "^0.3.1",
                "ol": "7.5.x"
        }
 }
index 8206d4fd76ea0d1190d9b1d62616be2fe996925b..13597fa8cfe8c61c06c4545e704e069a89d847ec 100644 (file)
@@ -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);
+       }
+
+
 }
index 1f8846d579e97518e8e37c124a1d9d8eb66f9b0b..3cec484c2f14d358f86b2cb66bbcab8bb8ee51c4 100644 (file)
@@ -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<GraphicalSymbol> 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<GraphicalSymbol> 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<org.opengis.style.Symbolizer> 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() {
        }
index 6408b40a320d39c4d702284d36300715f9f22c20..070bcb8dcd0aae1671c2dfaca2215e6e986f795a 100644 (file)
@@ -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);
 
index 2e909004a92e83ce02c9ae1ec2847a059db4ee9f..ece95085a9b0b002bcbad9f0b832a5a2baab445e 100644 (file)
@@ -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