From 8a490e540ac623b3545b1bd3da65ecbf2e4b6436 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Tue, 26 Sep 2023 11:09:10 +0200 Subject: [PATCH] Vector layers and styling --- js/src/geo/LayerStyles.js | 6 ++ js/src/geo/OpenLayersMapPart.js | 68 ++++++++++++++++--- .../src/org/argeo/app/ux/js/JsClient.java | 19 +++++- .../src/org/argeo/app/geo/GeoUtils.java | 36 +++++++--- .../argeo/app/geo/ux/AbstractGeoJsObject.java | 6 +- .../argeo/app/geo/ux/OpenLayersMapPart.java | 25 ++++++- .../org/argeo/app/ol/AbstractOlObject.java | 53 +++++++++++++-- .../src/org/argeo/app/ol/FeatureFormat.java | 9 +++ .../src/org/argeo/app/ol/GeoJSON.java | 9 +++ .../src/org/argeo/app/ol/Layer.java | 23 +++++-- .../src/org/argeo/app/ol/VectorLayer.java | 16 +++++ .../src/org/argeo/app/ol/VectorSource.java | 21 ++++++ .../org/argeo/app/geo/swt/MapUiProvider.java | 11 ++- .../argeo/app/swt/js/SwtBrowserJsPart.java | 41 +++++++---- 14 files changed, 294 insertions(+), 49 deletions(-) create mode 100644 js/src/geo/LayerStyles.js create mode 100644 org.argeo.app.geo/src/org/argeo/app/ol/FeatureFormat.java create mode 100644 org.argeo.app.geo/src/org/argeo/app/ol/GeoJSON.java create mode 100644 org.argeo.app.geo/src/org/argeo/app/ol/VectorLayer.java create mode 100644 org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java diff --git a/js/src/geo/LayerStyles.js b/js/src/geo/LayerStyles.js new file mode 100644 index 0000000..8c50c67 --- /dev/null +++ b/js/src/geo/LayerStyles.js @@ -0,0 +1,6 @@ +import * as SLDReader from '@nieuwlandgeo/sldreader'; + +export default class LayerStyles { + #sld; + +} \ No newline at end of file diff --git a/js/src/geo/OpenLayersMapPart.js b/js/src/geo/OpenLayersMapPart.js index 177ce33..a759566 100644 --- a/js/src/geo/OpenLayersMapPart.js +++ b/js/src/geo/OpenLayersMapPart.js @@ -26,6 +26,9 @@ export default class OpenLayersMapPart extends MapPart { /** The OpenLayers Map. */ #map; + /** Styled layer descriptor */ + #sld; + /** Externally added callback functions. */ callbacks = {}; @@ -37,16 +40,16 @@ export default class OpenLayersMapPart extends MapPart { // new TileLayer({ // source: new SentinelCloudless(), // }), -// new TileLayer({ -// source: new OSM(), -// opacity: 0.4, -// transition: 0, -// }), + // new TileLayer({ + // source: new OSM(), + // opacity: 0.4, + // transition: 0, + // }), ], -// view: new View({ -// center: [0, 0], -// zoom: 2, -// }), + // view: new View({ + // center: [0, 0], + // zoom: 2, + // }), target: this.getMapName(), }); } @@ -108,6 +111,19 @@ export default class OpenLayersMapPart extends MapPart { return this.#map; } + getLayerByName(name) { + let layers = this.#map.getLayers(); + for (let i = 0; i < layers.getLength(); i++) { + let layer = layers.item(i); + let n = layer.get('name'); + if (n !== undefined) { + if (name === n) + return layer; + } + } + return undefined; + } + /* CALLBACKS */ enableFeatureSingleClick() { // we cannot use 'this' in the function provided to OpenLayers @@ -210,6 +226,40 @@ export default class OpenLayersMapPart extends MapPart { // // SLD STYLING // + + setSld(xml) { + this.#sld = SLDReader.Reader(xml); + } + + /** Get a FeatureTypeStyle (https://nieuwlandgeo.github.io/SLDReader/api.html#FeatureTypeStyle). */ + getFeatureTypeStyle(styledLayerName, styleName) { + const sldLayer = SLDReader.getLayer(this.#sld, styledLayerName); + const style = styleName === undefined ? SLDReader.getStyle(sldLayer) : SLDReader.getStyle(sldLayer, styleName); + // OpenLayers can only use one definition + const featureTypeStyle = style.featuretypestyles[0]; + return featureTypeStyle; + } + + applyStyle(layerName, styledLayerName, styleName) { + const layer = this.getLayerByName(layerName); + const featureTypeStyle = this.getFeatureTypeStyle(styledLayerName, styleName); + 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: () => { + layer.changed(); + }, + }); + layer.setStyle(olStyleFunction); + } + #applySLD(vectorLayer, text) { const sldObject = SLDReader.Reader(text); const sldLayer = SLDReader.getLayer(sldObject); diff --git a/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java b/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java index b7fc724..f889fd9 100644 --- a/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java +++ b/org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.Locale; import java.util.Map; import java.util.StringJoiner; +import java.util.concurrent.CompletionStage; import java.util.function.Function; /** @@ -45,6 +46,12 @@ public interface JsClient { /** Get a global variable name. */ public String getJsVarName(String name); + /** + * Completion stage when the client is ready (typically the page has loaded in + * the browser). + */ + CompletionStage getReadyStage(); + /* * DEFAULTS */ @@ -57,13 +64,17 @@ public interface JsClient { execute(jsObject + '.' + methodCall, args); } + default boolean isInstanceOf(String reference, String jsClass) { + return (Boolean) evaluate(getJsVarName(reference) + " instanceof " + jsClass); + } + /* * UTILITIES */ static String toJsValue(Object o) { if (o instanceof CharSequence) - return '\"' + o.toString() + '\"'; + return '\'' + o.toString() + '\''; else if (o instanceof Number) return o.toString(); else if (o instanceof Boolean) @@ -84,7 +95,7 @@ public interface JsClient { else return jsObject.getJsReference(); } else - return '\"' + o.toString() + '\"'; + return '\'' + o.toString() + '\''; } static String toJsArgs(Object... arr) { @@ -128,4 +139,8 @@ public interface JsClient { return sj.toString(); } + static String escapeQuotes(String str) { + return str.replace("'", "\\'").replace("\"", "\\\""); + } + } 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 3cec484..8f97129 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 @@ -32,6 +32,7 @@ import org.geotools.styling.FeatureTypeConstraint; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.Fill; import org.geotools.styling.Graphic; +import org.geotools.styling.NamedLayer; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.Stroke; @@ -176,7 +177,11 @@ public class GeoUtils { } } - public static org.opengis.style.Style createStyleFromCss(String css) { + /* + * STYLING + */ + + public static Style createStyleFromCss(String css) { Stylesheet ss = CssParser.parse(css); CssTranslator translator = new CssTranslator(); org.opengis.style.Style style = translator.translate(ss); @@ -190,31 +195,32 @@ public class GeoUtils { // e.printStackTrace(); // } - return style; + return (Style) style; } public static String createSldFromCss(String name, String title, String css) { - - StyleFactory sf = CommonFactoryFinder.getStyleFactory(); - - StyledLayerDescriptor sld = sf.createStyledLayerDescriptor(); + StyledLayerDescriptor sld = GeoTools.STYLE_FACTORY.createStyledLayerDescriptor(); sld.setName(name); sld.setTitle(title); - UserLayer layer = sf.createUserLayer(); - layer.setName("default"); + NamedLayer layer = GeoTools.STYLE_FACTORY.createNamedLayer(); + layer.setName(name); - org.opengis.style.Style style = createStyleFromCss(css); - layer.userStyles().add((Style) style); + Style style = createStyleFromCss(css); + layer.styles().add(style); sld.layers().add(layer); + return sldToXml(sld); + } + + public static String sldToXml(StyledLayerDescriptor sld) { try { SLDTransformer styleTransform = new SLDTransformer(); String xml = styleTransform.transform(sld); // System.out.println(xml); return xml; } catch (TransformerException e) { - throw new IllegalStateException(e); + throw new IllegalStateException("Cannot write SLD as XML", e); } } @@ -335,6 +341,14 @@ public class GeoUtils { } + public static long getScaleFromResolution(long resolution) { + // see https://gis.stackexchange.com/questions/242424/how-to-get-map-units-to-find-current-scale-in-openlayers + final double INCHES_PER_UNIT = 39.37;// m + // final double INCHES_PER_UNIT = 4374754;// dd + final long DOTS_PER_INCH = 72; + return Math.round(INCHES_PER_UNIT * DOTS_PER_INCH * resolution); + } + /** Singleton. */ private GeoUtils() { } diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/ux/AbstractGeoJsObject.java b/org.argeo.app.geo/src/org/argeo/app/geo/ux/AbstractGeoJsObject.java index 9b4e360..63b9351 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/ux/AbstractGeoJsObject.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/ux/AbstractGeoJsObject.java @@ -3,13 +3,15 @@ package org.argeo.app.geo.ux; import org.argeo.app.ux.js.AbstractJsObject; public class AbstractGeoJsObject extends AbstractJsObject { + public final static String ARGEO_APP_GEO_JS_URL = "/pkg/org.argeo.app.js/geo.html"; + public final static String JS_PACKAGE = "argeo.app.geo"; + public AbstractGeoJsObject(Object... args) { super(args); } @Override public String getJsPackage() { - return "argeo.app.geo"; + return JS_PACKAGE; } - } diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java b/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java index 829b343..8fad7e2 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java @@ -1,7 +1,10 @@ package org.argeo.app.geo.ux; +import org.argeo.app.ol.AbstractOlObject; +import org.argeo.app.ol.Layer; import org.argeo.app.ol.OlMap; -import org.argeo.app.ux.js.AbstractJsObject; +import org.argeo.app.ol.TileLayer; +import org.argeo.app.ol.VectorLayer; import org.argeo.app.ux.js.JsClient; public class OpenLayersMapPart extends AbstractGeoJsObject { @@ -16,4 +19,24 @@ public class OpenLayersMapPart extends AbstractGeoJsObject { public OlMap getMap() { return new OlMap(getJsClient(), getReference() + ".getMap()"); } + + public void setSld(String xml) { + executeMethod(getMethodName(), JsClient.escapeQuotes(xml)); + } + + public void applyStyle(String layerName, String styledLayerName) { + executeMethod(getMethodName(), layerName, styledLayerName); + } + + public Layer getLayer(String name) { + // TODO deal with not found + String reference = "getLayerByName('" + name + "')"; + if (getJsClient().isInstanceOf(reference, AbstractOlObject.getJsClassName(VectorLayer.class))) { + return new VectorLayer(getJsClient(), reference); + } else if (getJsClient().isInstanceOf(reference, AbstractOlObject.getJsClassName(TileLayer.class))) { + return new TileLayer(getJsClient(), reference); + } else { + return new Layer(getJsClient(), reference); + } + } } diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/AbstractOlObject.java b/org.argeo.app.geo/src/org/argeo/app/ol/AbstractOlObject.java index 2806afe..db64d96 100644 --- a/org.argeo.app.geo/src/org/argeo/app/ol/AbstractOlObject.java +++ b/org.argeo.app.geo/src/org/argeo/app/ol/AbstractOlObject.java @@ -2,21 +2,23 @@ package org.argeo.app.ol; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.argeo.app.ux.js.AbstractJsObject; public abstract class AbstractOlObject extends AbstractJsObject { + public final static String JS_PACKAGE = "argeo.tp.ol"; public AbstractOlObject(Object... args) { super(args.length > 0 ? args : new Object[] { new HashMap() }); } - public AbstractOlObject(Map options) { - super(options); - } +// public AbstractOlObject(Map options) { +// super(new Object[] { options }); +// } public String getJsPackage() { - return "argeo.tp.ol"; + return JS_PACKAGE; } @SuppressWarnings("unchecked") @@ -28,4 +30,47 @@ public abstract class AbstractOlObject extends AbstractJsObject { throw new IllegalStateException("Object " + getJsClassName() + " has no available options"); return (Map) args[0]; } + + protected void doSetValue(String methodName, String newOption, Object value) { + if (isNew()) { + Objects.requireNonNull(newOption, "Value cannot be set as an option for " + getJsClassName() + ", use " + + methodName + "() after the object has been created"); + getNewOptions().put(newOption, value); + } else { + Objects.requireNonNull(methodName, "Value cannot be set via a method for " + getJsClassName() + ", use " + + newOption + " before the object is created"); + executeMethod(methodName, value); + } + } + + public void set(String key, Object value) { + set(key, value, false); + } + + public void set(String key, Object value, boolean silent) { + if (isNew()) { + getNewOptions().put(key, value); + } else { + executeMethod(getMethodName(), new Object[] { key, value, silent }); + } + } + + public Object get(String key) { + if (isNew()) { + return getNewOptions().get(key); + } else { + // TDO deal with reference if we are trying to get an object + return callMethod(getMethodName(), key); + } + + } + + public static String getJsClassName(Class clss) { + if (AbstractOlObject.class.isAssignableFrom(clss)) { + // NB: would failed for renamed classes + return JS_PACKAGE + "." + clss.getSimpleName(); + } + throw new IllegalArgumentException(clss + " is not an OpenLayers object"); + } + } diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/FeatureFormat.java b/org.argeo.app.geo/src/org/argeo/app/ol/FeatureFormat.java new file mode 100644 index 0000000..2d0f8bc --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/ol/FeatureFormat.java @@ -0,0 +1,9 @@ +package org.argeo.app.ol; + +public abstract class FeatureFormat extends AbstractOlObject { + + public FeatureFormat(Object... args) { + super(args); + } + +} diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/GeoJSON.java b/org.argeo.app.geo/src/org/argeo/app/ol/GeoJSON.java new file mode 100644 index 0000000..3cab7bc --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/ol/GeoJSON.java @@ -0,0 +1,9 @@ +package org.argeo.app.ol; + +public class GeoJSON extends FeatureFormat { + + public GeoJSON(Object... args) { + super(args); + } + +} diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/Layer.java b/org.argeo.app.geo/src/org/argeo/app/ol/Layer.java index 8b8ea92..6cc73f3 100644 --- a/org.argeo.app.geo/src/org/argeo/app/ol/Layer.java +++ b/org.argeo.app.geo/src/org/argeo/app/ol/Layer.java @@ -2,7 +2,11 @@ package org.argeo.app.ol; import java.util.Objects; -public abstract class Layer extends AbstractOlObject { +public class Layer extends AbstractOlObject { + public final static String NAME_KEY = "name"; + + // cached + private String name; public Layer(Object... args) { super(args); @@ -11,10 +15,11 @@ public abstract class Layer extends AbstractOlObject { public void setOpacity(double opacity) { if (opacity < 0 || opacity > 1) throw new IllegalArgumentException("Opacity must be between 0 and 1"); - if (isNew()) - getNewOptions().put("opacity", opacity); - else - executeMethod(getMethodName(), opacity); +// if (isNew()) +// getNewOptions().put("opacity", opacity); +// else +// executeMethod(getMethodName(), opacity); + doSetValue(getMethodName(), "opacity", opacity); } public void setSource(Source source) { @@ -39,4 +44,12 @@ public abstract class Layer extends AbstractOlObject { executeMethod(getMethodName(), maxResolution); } + public void setName(String name) { + set(NAME_KEY, name); + this.name = name; + } + + public String getName() { + return name; + } } diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/VectorLayer.java b/org.argeo.app.geo/src/org/argeo/app/ol/VectorLayer.java new file mode 100644 index 0000000..5a4b6b4 --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/ol/VectorLayer.java @@ -0,0 +1,16 @@ +package org.argeo.app.ol; + +public class VectorLayer extends Layer { + public VectorLayer(Object... args) { + super(args); + } + + public VectorLayer(String name, Source source) { + this(source); + setName(name); + } + + public VectorLayer(Source source) { + setSource(source); + } +} diff --git a/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java b/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java new file mode 100644 index 0000000..3b60d0b --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java @@ -0,0 +1,21 @@ +package org.argeo.app.ol; + +public class VectorSource extends Source { + + public VectorSource(Object... args) { + super(args); + } + + public VectorSource(String url, FeatureFormat format) { + setUrl(url); + setFormat(format); + } + + public void setUrl(String url) { + doSetValue(getMethodName(), "url", url); + } + + public void setFormat(FeatureFormat format) { + doSetValue(null, "format", format); + } +} 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 55c7d6c..283cbce 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 @@ -1,11 +1,15 @@ package org.argeo.app.geo.swt; import org.argeo.api.acr.Content; +import org.argeo.app.geo.ux.AbstractGeoJsObject; import org.argeo.app.geo.ux.OpenLayersMapPart; import org.argeo.app.geo.ux.SentinelCloudless; +import org.argeo.app.ol.GeoJSON; import org.argeo.app.ol.Layer; import org.argeo.app.ol.OSM; import org.argeo.app.ol.TileLayer; +import org.argeo.app.ol.VectorLayer; +import org.argeo.app.ol.VectorSource; import org.argeo.app.swt.js.SwtBrowserJsPart; import org.argeo.app.ux.js.JsClient; import org.argeo.cms.swt.acr.SwtUiProvider; @@ -17,7 +21,7 @@ public class MapUiProvider implements SwtUiProvider { @Override public Control createUiPart(Composite parent, Content context) { - JsClient jsClient = new SwtBrowserJsPart(parent, 0, "/pkg/org.argeo.app.js/geo.html"); + JsClient jsClient = new SwtBrowserJsPart(parent, 0, AbstractGeoJsObject.ARGEO_APP_GEO_JS_URL); OpenLayersMapPart mapPart = new OpenLayersMapPart(jsClient, "defaultOverviewMap"); mapPart.getMap().getView().setCenter(new int[] { 0, 0 }); mapPart.getMap().getView().setZoom(6); @@ -25,11 +29,16 @@ public class MapUiProvider implements SwtUiProvider { Layer satelliteLayer = new TileLayer(new SentinelCloudless()); satelliteLayer.setMaxResolution(200); mapPart.getMap().addLayer(satelliteLayer); + TileLayer baseLayer = new TileLayer(); baseLayer.setSource(new OSM()); baseLayer.setOpacity(0.5); mapPart.getMap().addLayer(baseLayer); + Layer dataLayer = new VectorLayer(new VectorSource( + "https://openlayers.org/en/v4.6.5/examples/data/geojson/countries.geojson", new GeoJSON())); + mapPart.getMap().addLayer(dataLayer); + // SwtJsMapPart map = new SwtJsMapPart("defaultOverviewMap", parent, 0); // map.setCenter(13.404954, 52.520008); // Berlin //// map.setCenter(-74.00597, 40.71427); // NYC diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java index 6282b1e..ec359c6 100644 --- a/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java @@ -6,7 +6,6 @@ import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Function; -import java.util.function.Supplier; import org.argeo.api.cms.CmsLog; import org.argeo.app.ux.js.JsClient; @@ -37,7 +36,7 @@ public class SwtBrowserJsPart implements JsClient { * Tasks that were requested before the context was ready. Typically * configuration methods on the part while the user interfaces is being build. */ - private List> preReadyToDos = new ArrayList<>(); + private List preReadyToDos = new ArrayList<>(); public SwtBrowserJsPart(Composite parent, int style, String url) { this.browser = new Browser(parent, 0); @@ -55,10 +54,8 @@ public class SwtBrowserJsPart implements JsClient { init(); loadExtensions(); // execute todos in order - for (Supplier toDo : preReadyToDos) { - boolean success = toDo.get(); - if (!success) - throw new IllegalStateException("Post-initalisation JavaScript execution failed"); + for (PreReadyToDo toDo : preReadyToDos) { + toDo.run(); } preReadyToDos.clear(); readyStage.complete(true); @@ -87,7 +84,10 @@ public class SwtBrowserJsPart implements JsClient { protected void init() { } - /** To be overridden with calls to {@link #loadExtension(String)}. */ + /** + * To be overridden with calls to {@link #loadExtension( Supplier toDo + * = () -> { boolean success = browser.execute(); return success; }; String)}. + */ protected void loadExtensions() { } @@ -102,7 +102,7 @@ public class SwtBrowserJsPart implements JsClient { browser.evaluate(String.format(Locale.ROOT, "import('%s')", url)); } - protected CompletionStage getReadyStage() { + public CompletionStage getReadyStage() { return readyStage.minimalCompletionStage(); } @@ -114,7 +114,7 @@ public class SwtBrowserJsPart implements JsClient { public Object evaluate(String js, Object... args) { assert browser.getDisplay().equals(Display.findDisplay(Thread.currentThread())) : "Not the proper UI thread."; if (!readyStage.isDone()) - throw new IllegalStateException("Methods returning a result can only be called after UI initilaisation."); + throw new IllegalStateException("Methods returning a result can only be called after UI initialisation."); // wait for the context to be ready // boolean ready = readyStage.join(); // if (!ready) @@ -125,15 +125,13 @@ public class SwtBrowserJsPart implements JsClient { @Override public void execute(String js, Object... args) { + String jsToExecute = String.format(Locale.ROOT, js, args); if (readyStage.isDone()) { - boolean success = browser.execute(String.format(Locale.ROOT, js, args)); + boolean success = browser.execute(jsToExecute); if (!success) throw new RuntimeException("JavaScript execution failed."); } else { - Supplier toDo = () -> { - boolean success = browser.execute(String.format(Locale.ROOT, js, args)); - return success; - }; + PreReadyToDo toDo = new PreReadyToDo(jsToExecute); preReadyToDos.add(toDo); } } @@ -167,6 +165,21 @@ public class SwtBrowserJsPart implements JsClient { return GLOBAL_THIS_ + name; } + class PreReadyToDo implements Runnable { + private String js; + + public PreReadyToDo(String js) { + this.js = js; + } + + @Override + public void run() { + boolean success = browser.execute(js); + if (!success) + throw new IllegalStateException("Pre-ready JavaScript failed: " + js); + } + } + /* * ACCESSORS */ -- 2.30.2