Improve callback support
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 1 Sep 2023 09:14:25 +0000 (11:14 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 1 Sep 2023 09:14:25 +0000 (11:14 +0200)
org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js
org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.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 [new file with mode: 0644]
swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJavaScriptMapPart.java [deleted file]

index 7c157eb19226a1716545aa4e11f09232060292fb..6eff99f2ff58d8fe3635474bdc2a0e0628585fff 100644 (file)
@@ -12,6 +12,7 @@ import { Point } from 'ol/geom.js';
 import VectorLayer from 'ol/layer/Vector.js';
 import GeoJSON from 'ol/format/GeoJSON.js';
 import GPX from 'ol/format/GPX.js';
+import Select from 'ol/interaction/Select.js';
 
 import MapPart from './MapPart.js';
 import { SentinelCloudless } from './OpenLayerTileSources.js';
@@ -20,6 +21,8 @@ import { SentinelCloudless } from './OpenLayerTileSources.js';
 export default class OpenLayersMapPart extends MapPart {
        /** The OpenLayers Map. */
        #map;
+       callbacks = {};
+
        // Constructor
        constructor() {
                super();
@@ -38,6 +41,8 @@ export default class OpenLayersMapPart extends MapPart {
                });
        }
 
+       /* GEOGRAPHICAL METHODS */
+
        setZoom(zoom) {
                this.#map.getView().setZoom(zoom);
        }
@@ -67,4 +72,32 @@ export default class OpenLayersMapPart extends MapPart {
                        source: vectorSource,
                }));
        }
+
+       /* CALLBACKS */
+       enableFeatureSingleClick() {
+               // we cannot use 'this' in the function provided to OpenLayers
+               let mapPart = this;
+               this.#map.on('singleclick', function(e) {
+                       let feature = null;
+                       // we chose only one
+                       e.map.forEachFeatureAtPixel(e.pixel, function(f) {
+                               feature = f;
+                       });
+                       if (feature !== null)
+                               mapPart.callbacks['onFeatureSingleClick'](feature.get('path'));
+               });
+       }
+
+       enableFeatureSelected() {
+               // we cannot use 'this' in the function provided to OpenLayers
+               let mapPart = this;
+               var select = new Select();
+               this.#map.addInteraction(select);
+               select.on('select', function(e) {
+                       if (e.selected.length > 0) {
+                               let feature = e.selected[0];
+                               mapPart.callbacks['onFeatureSelected'](feature.get('path'));
+                       }
+               });
+       }
 }
index 9e61531a035c01244870bef3a5d66739c422b078..13190c98d238f75db9ef86a58b3aea8ac6dce874 100644 (file)
@@ -14,4 +14,12 @@ public interface MapPart {
        void setZoom(int zoom);
 
        void setCenter(double lng, double lat);
+
+       /** Event when a feature has been single-clicked. */
+       record FeatureSingleClickEvent(String path) {
+       };
+
+       /** Event when a feature has been selected. */
+       record FeatureSelectedEvent(String path) {
+       };
 }
index 517a2dc371863033ea977bd0fd8ea9898dbee7b7..d32ea45aa12dfc8f1d1c468b2000042d22dd2f9d 100644 (file)
@@ -10,7 +10,7 @@ public class MapUiProvider implements SwtUiProvider {
 
        @Override
        public Control createUiPart(Composite parent, Content context) {
-               SwtJavaScriptMapPart map = new SwtJavaScriptMapPart(parent, 0);
+               SwtJSMapPart map = new SwtJSMapPart(parent, 0);
                map.setCenter(13.404954, 52.520008); // Berlin
 //             map.setCenter(-74.00597, 40.71427); // NYC
 //             map.addPoint(-74.00597, 40.71427, null);
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
new file mode 100644 (file)
index 0000000..09848f9
--- /dev/null
@@ -0,0 +1,152 @@
+package org.argeo.app.geo.swt;
+
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
+
+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;
+import org.eclipse.swt.browser.ProgressEvent;
+import org.eclipse.swt.browser.ProgressListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * An SWT implementation of {@link MapPart} based on JavaScript execute in a
+ * {@link Browser} control.
+ */
+public class SwtJSMapPart extends Composite implements MapPart {
+       static final long serialVersionUID = 2713128477504858552L;
+
+       private final static CmsLog log = CmsLog.getLog(SwtJSMapPart.class);
+
+       private final static String GLOBAL_THIS_ = "globalThis.";
+
+       private final Browser browser;
+
+       private final CompletableFuture<Boolean> pageLoaded = new CompletableFuture<>();
+
+       private String jsImplementation = JsImplementation.OPENLAYERS_MAP_PART.getJsClass();
+       private String mapVar = "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);
+               browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               browser.setUrl("/pkg/org.argeo.app.geo.js/index.html");
+               browser.addProgressListener(new ProgressListener() {
+                       static final long serialVersionUID = 1L;
+
+                       @Override
+                       public void completed(ProgressEvent event) {
+                               try {
+                                       // create map
+                                       browser.execute(getJsMapVar() + " = new " + jsImplementation + "();");
+                                       pageLoaded.complete(true);
+                               } catch (Exception e) {
+                                       log.error("Cannot create map in browser", e);
+                                       pageLoaded.complete(false);
+                               }
+                       }
+
+                       @Override
+                       public void changed(ProgressEvent event) {
+                       }
+               });
+       }
+
+       /*
+        * MapPart.js METHODS
+        */
+
+       @Override
+       public void addPoint(double lng, double lat, String style) {
+               callMapMethod("addPoint(%f, %f, %s)", lng, lat, style == null ? "'default'" : style);
+       }
+
+       @Override
+       public void addUrlLayer(String url, GeoFormat format) {
+               callMapMethod("addUrlLayer('%s', '%s')", url, format.name());
+       }
+
+       @Override
+       public void setZoom(int zoom) {
+               callMapMethod("setZoom(%d)", zoom);
+       }
+
+       @Override
+       public void setCenter(double lng, double lat) {
+               callMapMethod("setCenter(%f, %f)", lng, lat);
+       }
+
+       protected CompletionStage<Object> callMapMethod(String methodCall, Object... args) {
+               return callMethod(getJsMapVar(), methodCall, args);
+       }
+
+       protected CompletionStage<Object> callMethod(String jsObject, String methodCall, Object... args) {
+               return evaluate(jsObject + '.' + methodCall, args);
+       }
+
+       private String getJsMapVar() {
+               return GLOBAL_THIS_ + mapVar;
+       }
+
+       /**
+        * Execute this JavaScript on the client side after making sure that the page
+        * has been loaded and the map object has been created.
+        * 
+        * @param js   the JavaScript code, possibly formatted according to
+        *             {@link String#format}, with {@link Locale#ROOT} as locale (for
+        *             stability of decimal separator, as expected by JavaScript.
+        * @param args the optional arguments of
+        *             {@link String#format(String, Object...)}
+        */
+       protected CompletionStage<Object> evaluate(String js, Object... args) {
+               CompletableFuture<Object> res = pageLoaded.thenApply((ready) -> {
+                       if (!ready)
+                               throw new IllegalStateException("Map " + mapVar + " is not initialised.");
+                       Object result = browser.evaluate(String.format(Locale.ROOT, js, args));
+                       return result;
+               });
+               return res.minimalCompletionStage();
+       }
+
+       /*
+        * CALLBACKS
+        */
+       public void onFeatureSelected(Consumer<FeatureSelectedEvent> toDo) {
+               addCallback("FeatureSelected", (arr) -> toDo.accept(new FeatureSelectedEvent((String) arr[0])));
+       }
+
+       public void onFeatureSingleClick(Consumer<FeatureSingleClickEvent> toDo) {
+               addCallback("FeatureSingleClick", (arr) -> toDo.accept(new FeatureSingleClickEvent((String) arr[0])));
+       }
+
+       protected void addCallback(String suffix, Consumer<Object[]> toDo) {
+               pageLoaded.thenAccept((ready) -> {
+                       // browser functions must be directly on window (RAP specific)
+                       new BrowserFunction(browser, mapVar + "__on" + suffix) {
+
+                               @Override
+                               public Object function(Object[] arguments) {
+                                       toDo.accept(arguments);
+                                       return null;
+                               }
+
+                       };
+                       browser.execute(getJsMapVar() + ".callbacks['on" + suffix + "']=window." + mapVar + "__on" + suffix + ";");
+                       callMethod(mapVar, "enable" + suffix + "()");
+               });
+       }
+}
diff --git a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJavaScriptMapPart.java b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJavaScriptMapPart.java
deleted file mode 100644 (file)
index 7d5d71d..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-package org.argeo.app.geo.swt;
-
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-
-import org.argeo.api.cms.CmsConstants;
-import org.argeo.api.cms.CmsLog;
-import org.argeo.api.cms.ux.CmsView;
-import org.argeo.app.geo.ux.JsImplementation;
-import org.argeo.app.geo.ux.MapPart;
-import org.argeo.app.ux.SuiteUxEvent;
-import org.argeo.cms.swt.CmsSwtUtils;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.browser.Browser;
-import org.eclipse.swt.browser.BrowserFunction;
-import org.eclipse.swt.browser.ProgressEvent;
-import org.eclipse.swt.browser.ProgressListener;
-import org.eclipse.swt.layout.GridData;
-import org.eclipse.swt.widgets.Composite;
-
-/**
- * An SWT implementation of {@link MapPart} based on JavaScript execute in a
- * {@link Browser} control.
- */
-public class SwtJavaScriptMapPart extends Composite implements MapPart {
-       static final long serialVersionUID = 2713128477504858552L;
-
-       private final static CmsLog log = CmsLog.getLog(SwtJavaScriptMapPart.class);
-
-       private Browser browser;
-
-       private CompletableFuture<Boolean> pageLoaded = new CompletableFuture<>();
-
-       private String jsImplementation = JsImplementation.OPENLAYERS_MAP_PART.getJsClass();
-       private String mapVar = "globalThis.argeoMap";
-
-       private final CmsView cmsView;
-
-       public SwtJavaScriptMapPart(Composite parent, int style) {
-               super(parent, style);
-               parent.setLayout(CmsSwtUtils.noSpaceGridLayout());
-               setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
-               setLayout(CmsSwtUtils.noSpaceGridLayout());
-
-               cmsView = CmsSwtUtils.getCmsView(parent);
-
-               browser = new Browser(this, SWT.BORDER);
-               browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
-
-               // functions exposed to JavaScript
-               new onFeatureSelect();
-
-               browser.setUrl("/pkg/org.argeo.app.geo.js/index.html");
-               browser.addProgressListener(new ProgressListener() {
-                       static final long serialVersionUID = 1L;
-
-                       @Override
-                       public void completed(ProgressEvent event) {
-                               try {
-                                       // create map
-                                       browser.execute(mapVar + " = new " + jsImplementation + "();");
-                                       pageLoaded.complete(true);
-                               } catch (Exception e) {
-                                       log.error("Cannot create map in browser", e);
-                                       pageLoaded.complete(false);
-                               }
-                       }
-
-                       @Override
-                       public void changed(ProgressEvent event) {
-                       }
-               });
-       }
-
-       @Override
-       public void addPoint(double lng, double lat, String style) {
-               callMethod(mapVar, "addPoint(%f, %f, %s)", lng, lat, style == null ? "'default'" : style);
-       }
-
-       @Override
-       public void addUrlLayer(String url, GeoFormat format) {
-               callMethod(mapVar, "addUrlLayer('%s', '%s')", url, format.name());
-       }
-
-       @Override
-       public void setZoom(int zoom) {
-               callMethod(mapVar, "setZoom(%d)", zoom);
-       }
-
-       @Override
-       public void setCenter(double lng, double lat) {
-               callMethod(mapVar, "setCenter(%f, %f)", lng, lat);
-       }
-
-       protected CompletionStage<Object> callMethod(String jsObject, String methodCall, Object... args) {
-               return evaluate(jsObject + '.' + methodCall, args);
-       }
-
-       /**
-        * Execute this JavaScript on the client side after making sure that the page
-        * has been loaded and the map object has been created.
-        * 
-        * @param js   the JavaScript code, possibly formatted according to
-        *             {@link String#format}, with {@link Locale#ROOT} as locale (for
-        *             stability of decimal separator, as expected by JavaScript.
-        * @param args the optional arguments of
-        *             {@link String#format(String, Object...)}
-        */
-       protected CompletionStage<Object> evaluate(String js, Object... args) {
-               CompletableFuture<Object> res = pageLoaded.thenApply((ready) -> {
-                       if (!ready)
-                               throw new IllegalStateException("Map " + mapVar + " is not initialised.");
-                       Object result = browser.evaluate(String.format(Locale.ROOT, js, args));
-                       return result;
-               });
-               return res.minimalCompletionStage();
-       }
-
-       /** JavaScript function called when a feature is selected on the map. */
-       private class onFeatureSelect extends BrowserFunction {
-
-               onFeatureSelect() {
-                       super(browser, onFeatureSelect.class.getSimpleName());
-               }
-
-               @Override
-               public Object function(Object[] arguments) {
-                       if (arguments.length == 0)
-                               return null;
-                       String path = arguments[0].toString();
-                       Map<String, Object> properties = new HashMap<>();
-                       properties.put(SuiteUxEvent.CONTENT_PATH, '/' + CmsConstants.SYS_WORKSPACE + path);
-                       cmsView.sendEvent(SuiteUxEvent.refreshPart.topic(), properties);
-                       return null;
-               }
-
-       }
-}