From: Mathieu Baudier Date: Fri, 1 Sep 2023 09:14:25 +0000 (+0200) Subject: Improve callback support X-Git-Tag: v2.3.16~45 X-Git-Url: https://git.argeo.org/?p=gpl%2Fargeo-suite.git;a=commitdiff_plain;h=69adaa7b31078cb30043b073dada14dee1a9e75d Improve callback support --- 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 7c157eb..6eff99f 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 @@ -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')); + } + }); + } } diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.java b/org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.java index 9e61531..13190c9 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.java @@ -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) { + }; } diff --git a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java index 517a2dc..d32ea45 100644 --- a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java +++ b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java @@ -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 index 0000000..09848f9 --- /dev/null +++ b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java @@ -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 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 callMapMethod(String methodCall, Object... args) { + return callMethod(getJsMapVar(), methodCall, args); + } + + protected CompletionStage 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 evaluate(String js, Object... args) { + CompletableFuture 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 toDo) { + addCallback("FeatureSelected", (arr) -> toDo.accept(new FeatureSelectedEvent((String) arr[0]))); + } + + public void onFeatureSingleClick(Consumer toDo) { + addCallback("FeatureSingleClick", (arr) -> toDo.accept(new FeatureSingleClickEvent((String) arr[0]))); + } + + protected void addCallback(String suffix, Consumer 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 index 7d5d71d..0000000 --- a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJavaScriptMapPart.java +++ /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 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 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 evaluate(String js, Object... args) { - CompletableFuture 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 properties = new HashMap<>(); - properties.put(SuiteUxEvent.CONTENT_PATH, '/' + CmsConstants.SYS_WORKSPACE + path); - cmsView.sendEvent(SuiteUxEvent.refreshPart.topic(), properties); - return null; - } - - } -}