Customisable map styling
authorMathieu Baudier <mbaudier@argeo.org>
Sat, 2 Sep 2023 10:20:17 +0000 (12:20 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Sat, 2 Sep 2023 10:20:17 +0000 (12:20 +0200)
org.argeo.app.geo.js/src/org.argeo.app.geo.js/MapPart.js
org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js
org.argeo.app.geo.js/src/org.argeo.app.geo.js/index.html
org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.java
org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GeoJsonHttpHandler.java
swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java
swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java

index ef19f58143143de9cf0ea5d25a50ccf0a7173df5..6635b0809e427e56db749e01cf794fa6e2ae02e3 100644 (file)
@@ -5,6 +5,18 @@
 /** Abstract base class for displaying a map. */
 export default class MapPart {
 
+       /** The name of the map, will also be the name of the variable */
+       #mapName;
+
+       constructor(mapName) {
+               this.#mapName = mapName;
+               this.createMapDiv(this.#mapName);
+       }
+
+       //
+       // ABSTRACT METHODS
+       //
+
        /** Zoom the map to the given value. */
        setZoom(zoom) {
                throw new Error("Abstract method");
@@ -23,4 +35,38 @@ export default class MapPart {
        addUrlLayer(url, format) {
                throw new Error("Abstract method");
        }
+
+       //
+       // EXTENSIONS
+       //
+       loadMapModule(url) {
+               var script = document.createElement("script");
+               script.src = url;
+               document.head.appendChild(script);
+               //              import(url)
+               //                      .then(module => { })
+               //                      .catch((error) => 'An error occurred while loading the component');
+       }
+
+       //
+       // AcCESSORS
+       //
+       getMapName() {
+               return this.#mapName;
+       }
+
+       //
+       // HTML
+       //
+       createMapDiv(id) {
+               var mapDiv = document.createElement('div');
+               mapDiv.id = id;
+               mapDiv.className = this.getMapDivCssClass();
+               mapDiv.style.cssText = 'width: 100%; height: 100vh;';
+               document.body.appendChild(mapDiv);
+       }
+
+       getMapDivCssClass() {
+               throw new Error("Abstract method");
+       }
 }
index b2d09115f94309e890f5d31936f0b3f857d9c65a..8206d4fd76ea0d1190d9b1d62616be2fe996925b 100644 (file)
@@ -3,6 +3,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';
@@ -14,6 +15,7 @@ import GeoJSON from 'ol/format/GeoJSON.js';
 import GPX from 'ol/format/GPX.js';
 import Select from 'ol/interaction/Select.js';
 import Overlay from 'ol/Overlay.js';
+import { Style, Icon } from 'ol/style.js';
 
 import MapPart from './MapPart.js';
 import { SentinelCloudless } from './OpenLayerTileSources.js';
@@ -22,23 +24,25 @@ import { SentinelCloudless } from './OpenLayerTileSources.js';
 export default class OpenLayersMapPart extends MapPart {
        /** The OpenLayers Map. */
        #map;
+
+       /** Externally added callback functions. */
        callbacks = {};
 
-       // Constructor
-       constructor() {
-               super();
+       /** Constructor taking the mapName as an argument. */
+       constructor(mapName) {
+               super(mapName);
                this.#map = new Map({
                        layers: [
-                               new TileLayer({
-                                       source: new SentinelCloudless(),
-                               }),
+                               //                              new TileLayer({
+                               //                                      source: new SentinelCloudless(),
+                               //                              }),
                                new TileLayer({
                                        source: new OSM(),
                                        opacity: 0.4,
                                        transition: 0,
                                }),
                        ],
-                       target: 'map',
+                       target: this.getMapName(),
                });
        }
 
@@ -58,10 +62,13 @@ export default class OpenLayersMapPart extends MapPart {
                                geometry: new Point(fromLonLat([lng, lat]))
                        })]
                });
-               this.#map.addLayer(new VectorLayer({ source: vectorSource }));
+               this.#map.addLayer(new VectorLayer({
+                       source: vectorSource,
+                       style: style,
+               }));
        }
 
-       addUrlLayer(url, format) {
+       addUrlLayer(url, format, style) {
                let vectorSource;
                if (format === 'GEOJSON') {
                        vectorSource = new VectorSource({ url: url, format: new GeoJSON() })
@@ -71,6 +78,7 @@ export default class OpenLayersMapPart extends MapPart {
                }
                this.#map.addLayer(new VectorLayer({
                        source: vectorSource,
+                       style: style,
                }));
        }
 
@@ -143,6 +151,8 @@ export default class OpenLayersMapPart extends MapPart {
                        }
                        const coordinate = e.coordinate;
                        const path = selected.get('path');
+                       if (path === null)
+                               return true;
                        const res = mapPart.callbacks['onFeaturePopup'](path);
                        if (res != null) {
                                content.innerHTML = res;
@@ -152,4 +162,24 @@ export default class OpenLayersMapPart extends MapPart {
                        }
                });
        }
+
+       //
+       // HTML
+       //
+       getMapDivCssClass() {
+               return 'map';
+       }
+
+       //
+       // STATIC FOR EXTENSION
+       //
+       static newStyle(args){
+               return new Style(args);
+       }
+       
+       static newIcon(args){
+               return new Icon(args);
+       }
+       
+       
 }
index 12fa0a1e4ab8fc0cdcf51f117982dade67a921d1..bafe7fed417f385241b4061f7c216f9b2c59fa3e 100644 (file)
@@ -4,11 +4,6 @@
 <head>
        <meta charset="UTF-8">
        <style>
-               .map {
-                       width: 100%;
-                       height: 100vh;
-               }
-
                .ol-popup {
                        position: absolute;
                        background-color: white;
@@ -64,8 +59,6 @@
 </head>
 
 <body>
-       <div id="map" class="map"></div>
-
        <!-- Popup -->
        <div id="popup" class="ol-popup">
                <div id="popup-content"></div>
index 11ee0d11875345f1b716acb53df0e9ba9e3be39c..84f4dd541cf6b0acfed6623e6c5680eb48965ac4 100644 (file)
@@ -9,7 +9,7 @@ public interface MapPart {
 
        void addPoint(double lng, double lat, String style);
 
-       void addUrlLayer(String url, GeoFormat format);
+       void addUrlLayer(String url, GeoFormat format, String style);
 
        void setZoom(int zoom);
 
@@ -22,6 +22,7 @@ public interface MapPart {
        /** Event when a feature has been selected. */
        record FeatureSelectedEvent(String path) {
        };
+
        /** Event when a feature popup is requested. */
        record FeaturePopupEvent(String path) {
        };
index 8840a4ef4dae07c7f165d23dc0dcfc7735fc1f73..ca812581d86ba916c53145d5f1ec351017c9b087 100644 (file)
@@ -3,19 +3,24 @@ package org.argeo.app.internal.geo.http;
 import static org.argeo.app.geo.CqlUtils.CQL_FILTER;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.UncheckedIOException;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
+import javax.xml.namespace.QName;
+
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentSession;
 import org.argeo.api.acr.NamespaceUtils;
 import org.argeo.api.acr.ldap.LdapAttr;
 import org.argeo.api.acr.spi.ProvidedRepository;
+import org.argeo.app.api.EntityName;
 import org.argeo.app.api.EntityType;
 import org.argeo.app.api.WGS84PosName;
 import org.argeo.app.geo.CqlUtils;
+import org.argeo.app.geo.GpxUtils;
 import org.argeo.cms.http.HttpHeader;
 import org.argeo.cms.http.server.HttpServerUtils;
 import org.geotools.data.DataUtilities;
@@ -25,8 +30,8 @@ import org.geotools.feature.simple.SimpleFeatureBuilder;
 import org.geotools.filter.text.cql2.CQL;
 import org.geotools.geometry.jts.JTSFactoryFinder;
 import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryFactory;
-import org.locationtech.jts.geom.Point;
 import org.opengis.feature.simple.SimpleFeature;
 import org.opengis.feature.simple.SimpleFeatureType;
 import org.opengis.filter.Filter;
@@ -43,7 +48,7 @@ public class GeoJsonHttpHandler implements HttpHandler {
                ContentSession session = HttpServerUtils.getContentSession(contentRepository, exchange);
                // Content content = session.get(path);
 
-               exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/json; charset=utf-8");
+               exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/json");
 
                Map<String, List<String>> parameters = HttpServerUtils.parseParameters(exchange);
                String cql = parameters.containsKey(CQL_FILTER) ? parameters.get(CQL_FILTER).get(0) : null;
@@ -54,6 +59,9 @@ public class GeoJsonHttpHandler implements HttpHandler {
                                search.getWhere().isContentClass(EntityType.local);
                        });
 
+                       exchange.sendResponseHeaders(200, 0);
+
+                       // BODY PROCESSING
                        GeoJSONWriter geoJSONWriter = new GeoJSONWriter(exchange.getResponseBody());
                        geoJSONWriter.setPrettyPrinting(true);
 
@@ -64,9 +72,13 @@ public class GeoJsonHttpHandler implements HttpHandler {
 //                                       "features": [
 //                                     """);
 
+                       boolean gpx = false;
                        SimpleFeatureType TYPE;
                        try {
-                               TYPE = DataUtilities.createType("Content", "the_geom:Point:srid=4326,path:String,name:String");
+                               if (gpx)
+                                       TYPE = DataUtilities.createType("Content", "the_geom:Polygon:srid=4326,path:String,type:String");
+                               else
+                                       TYPE = DataUtilities.createType("Content", "the_geom:Point:srid=4326,path:String,type:String");
                        } catch (SchemaException e) {
                                throw new RuntimeException(e);
                        }
@@ -74,19 +86,41 @@ public class GeoJsonHttpHandler implements HttpHandler {
                        GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
 
                        res.forEach((c) -> {
-                               if (!c.hasContentClass(EntityType.geopoint))
-                                       return;
-
-                               double latitude = c.get(WGS84PosName.lat, Double.class).get();
-                               double longitude = c.get(WGS84PosName.lng, Double.class).get();
+                               Geometry the_geom;
+                               if (gpx) {// experimental
+                                       Content area = c.getContent("gpx/area.gpx").orElse(null);
+                                       if (area == null)
+                                               return;
+                                       try (InputStream in = area.open(InputStream.class)) {
+                                               SimpleFeature feature = GpxUtils.parseGpxToPolygon(in);
+                                               the_geom = (Geometry) feature.getDefaultGeometry();
+                                       } catch (IOException e) {
+                                               throw new UncheckedIOException("Cannot parse " + c, e);
+                                       }
+                               } else {
+                                       if (!c.hasContentClass(EntityType.geopoint))
+                                               return;
+
+                                       double latitude = c.get(WGS84PosName.lat, Double.class).get();
+                                       double longitude = c.get(WGS84PosName.lng, Double.class).get();
+
+                                       Coordinate coordinate = new Coordinate(longitude, latitude);
+                                       the_geom = geometryFactory.createPoint(coordinate);
 
-                               Coordinate coordinate = new Coordinate(longitude, latitude);
-                               Point point = geometryFactory.createPoint(coordinate);
+                               }
 
-                               featureBuilder.add(point);
+                               featureBuilder.add(the_geom);
                                String pth = c.getPath();
                                featureBuilder.add(pth);
-                               featureBuilder.add(NamespaceUtils.toPrefixedName(c.getName()));
+                               if (c.hasContentClass(EntityType.local)) {
+                                       String type = c.attr(EntityName.type);
+                                       featureBuilder.add(type);
+                               } else {
+                                       List<QName> contentClasses = c.getContentClasses();
+                                       if (!contentClasses.isEmpty()) {
+                                               featureBuilder.add(NamespaceUtils.toPrefixedName(contentClasses.get(0)));
+                                       }
+                               }
 
                                String uuid = c.attr(LdapAttr.entryUUID);
 
index d32ea45aa12dfc8f1d1c468b2000042d22dd2f9d..fa70146f3d48e557ae5720e1abb8a596e66b2495 100644 (file)
@@ -10,14 +10,14 @@ public class MapUiProvider implements SwtUiProvider {
 
        @Override
        public Control createUiPart(Composite parent, Content context) {
-               SwtJSMapPart map = new SwtJSMapPart(parent, 0);
+               SwtJSMapPart map = new SwtJSMapPart("defaultOverviewMap", parent, 0);
                map.setCenter(13.404954, 52.520008); // Berlin
 //             map.setCenter(-74.00597, 40.71427); // NYC
 //             map.addPoint(-74.00597, 40.71427, null);
                map.setZoom(6);
                // map.addUrlLayer("https://openlayers.org/en/v4.6.5/examples/data/geojson/countries.geojson",
                // Format.GEOJSON);
-               return map;
+               return map.getControl();
        }
 
 }
index 16fa83edb96bd13a129f059d90ba4a351ac6bfb4..913636b3acd67d3e559654a0d5ff34935655ab8f 100644 (file)
@@ -9,7 +9,6 @@ import java.util.function.Function;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.app.geo.ux.JsImplementation;
 import org.argeo.app.geo.ux.MapPart;
-import org.argeo.cms.swt.CmsSwtUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.browser.Browser;
 import org.eclipse.swt.browser.BrowserFunction;
@@ -17,12 +16,13 @@ import org.eclipse.swt.browser.ProgressEvent;
 import org.eclipse.swt.browser.ProgressListener;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
 
 /**
  * An SWT implementation of {@link MapPart} based on JavaScript execute in a
  * {@link Browser} control.
  */
-public class SwtJSMapPart extends Composite implements MapPart {
+public class SwtJSMapPart implements MapPart {
        static final long serialVersionUID = 2713128477504858552L;
 
        private final static CmsLog log = CmsLog.getLog(SwtJSMapPart.class);
@@ -34,15 +34,11 @@ public class SwtJSMapPart extends Composite implements MapPart {
        private final CompletableFuture<Boolean> pageLoaded = new CompletableFuture<>();
 
        private String jsImplementation = JsImplementation.OPENLAYERS_MAP_PART.getJsClass();
-       private String mapVar = "argeoMap";
+       private final String mapName;// = "argeoMap";
 
-       public SwtJSMapPart(Composite parent, int style) {
-               super(parent, style);
-               parent.setLayout(CmsSwtUtils.noSpaceGridLayout());
-               setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
-               setLayout(CmsSwtUtils.noSpaceGridLayout());
-
-               browser = new Browser(this, SWT.BORDER);
+       public SwtJSMapPart(String mapName, Composite parent, int style) {
+               this.mapName = mapName;
+               browser = new Browser(parent, 0);
                browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
 
                browser.setUrl("/pkg/org.argeo.app.geo.js/index.html");
@@ -53,7 +49,8 @@ public class SwtJSMapPart extends Composite implements MapPart {
                        public void completed(ProgressEvent event) {
                                try {
                                        // create map
-                                       browser.execute(getJsMapVar() + " = new " + jsImplementation + "();");
+                                       browser.execute(getJsMapVar() + " = new " + jsImplementation + "('" + mapName + "');");
+                                       loadExtensions();
                                        pageLoaded.complete(true);
                                } catch (Exception e) {
                                        log.error("Cannot create map in browser", e);
@@ -67,6 +64,10 @@ public class SwtJSMapPart extends Composite implements MapPart {
                });
        }
 
+       public Control getControl() {
+               return browser;
+       }
+
        /*
         * MapPart.js METHODS
         */
@@ -77,8 +78,8 @@ public class SwtJSMapPart extends Composite implements MapPart {
        }
 
        @Override
-       public void addUrlLayer(String url, GeoFormat format) {
-               callMapMethod("addUrlLayer('%s', '%s')", url, format.name());
+       public void addUrlLayer(String url, GeoFormat format, String style) {
+               callMapMethod("addUrlLayer('%s', '%s', %s)", url, format.name(), style);
        }
 
        @Override
@@ -100,7 +101,7 @@ public class SwtJSMapPart extends Composite implements MapPart {
        }
 
        private String getJsMapVar() {
-               return GLOBAL_THIS_ + mapVar;
+               return GLOBAL_THIS_ + mapName;
        }
 
        /**
@@ -116,13 +117,28 @@ public class SwtJSMapPart extends Composite implements MapPart {
        protected CompletionStage<Object> evaluate(String js, Object... args) {
                CompletableFuture<Object> res = pageLoaded.thenApply((ready) -> {
                        if (!ready)
-                               throw new IllegalStateException("Map " + mapVar + " is not initialised.");
+                               throw new IllegalStateException("Map " + mapName + " is not initialised.");
                        Object result = browser.evaluate(String.format(Locale.ROOT, js, args));
                        return result;
                });
                return res.minimalCompletionStage();
        }
 
+       protected void loadExtension(String url) {
+//             String js = """
+//                             var script = document.createElement("script");
+//                             script.src = '%s';
+//                             document.head.appendChild(script);
+//                             """;
+//             browser.evaluate(String.format(Locale.ROOT, js, url));
+               browser.evaluate(String.format(Locale.ROOT, "import('%s')", url));
+       }
+
+       /** To be overridden with calls to {@link #loadExtension(String)}. */
+       protected void loadExtensions() {
+
+       }
+
        /*
         * CALLBACKS
         */
@@ -149,7 +165,7 @@ public class SwtJSMapPart extends Composite implements MapPart {
        protected void addCallback(String suffix, Function<Object[], Object> toDo) {
                pageLoaded.thenAccept((ready) -> {
                        // browser functions must be directly on window (RAP specific)
-                       new BrowserFunction(browser, mapVar + "__on" + suffix) {
+                       new BrowserFunction(browser, mapName + "__on" + suffix) {
 
                                @Override
                                public Object function(Object[] arguments) {
@@ -158,8 +174,8 @@ public class SwtJSMapPart extends Composite implements MapPart {
                                }
 
                        };
-                       browser.execute(getJsMapVar() + ".callbacks['on" + suffix + "']=window." + mapVar + "__on" + suffix + ";");
-                       callMethod(mapVar, "enable" + suffix + "()");
+                       browser.execute(getJsMapVar() + ".callbacks['on" + suffix + "']=window." + mapName + "__on" + suffix + ";");
+                       callMethod(mapName, "enable" + suffix + "()");
                });
        }
 }