Vector layers and styling
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 26 Sep 2023 09:09:10 +0000 (11:09 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 26 Sep 2023 09:09:10 +0000 (11:09 +0200)
14 files changed:
js/src/geo/LayerStyles.js [new file with mode: 0644]
js/src/geo/OpenLayersMapPart.js
org.argeo.app.core/src/org/argeo/app/ux/js/JsClient.java
org.argeo.app.geo/src/org/argeo/app/geo/GeoUtils.java
org.argeo.app.geo/src/org/argeo/app/geo/ux/AbstractGeoJsObject.java
org.argeo.app.geo/src/org/argeo/app/geo/ux/OpenLayersMapPart.java
org.argeo.app.geo/src/org/argeo/app/ol/AbstractOlObject.java
org.argeo.app.geo/src/org/argeo/app/ol/FeatureFormat.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/ol/GeoJSON.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/ol/Layer.java
org.argeo.app.geo/src/org/argeo/app/ol/VectorLayer.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/ol/VectorSource.java [new file with mode: 0644]
swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java
swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java

diff --git a/js/src/geo/LayerStyles.js b/js/src/geo/LayerStyles.js
new file mode 100644 (file)
index 0000000..8c50c67
--- /dev/null
@@ -0,0 +1,6 @@
+import * as SLDReader from '@nieuwlandgeo/sldreader';
+
+export default class LayerStyles {
+       #sld;
+
+}
\ No newline at end of file
index 177ce33862919d750769ae9029e187d1467df6db..a75956680ee1e165ebf0151015d973725df8ef62 100644 (file)
@@ -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);
index b7fc724d4d0a3b46a748f836506d9db5da1967c5..f889fd96bdaf6c0ca170551b896ade0e1ff5ade3 100644 (file)
@@ -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<Boolean> 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("\"", "\\\"");
+       }
+
 }
index 3cec484c2f14d358f86b2cb66bbcab8bb8ee51c4..8f971291aeb49c877e145bee691ff92ea87dac51 100644 (file)
@@ -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() {
        }
index 9b4e360c34a7c0ef7b127ba47eedd50b5045ba54..63b93515dbd98235c83aae15ad06059e328321b9 100644 (file)
@@ -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;
        }
-
 }
index 829b343a38bccd88dffe60f99899e9e22263e4dd..8fad7e26b8ee50a2adc22330a6e6a61aa5e24d01 100644 (file)
@@ -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);
+               }
+       }
 }
index 2806afe09b392d33d03eefe66aa946e2e847030b..db64d965a41a6030e6869100998edeb67d46dd28 100644 (file)
@@ -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<String, Object>() });
        }
 
-       public AbstractOlObject(Map<String, Object> options) {
-               super(options);
-       }
+//     public AbstractOlObject(Map<String, Object> 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<String, Object>) 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 (file)
index 0000000..2d0f8bc
--- /dev/null
@@ -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 (file)
index 0000000..3cab7bc
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.app.ol;
+
+public class GeoJSON extends FeatureFormat {
+
+       public GeoJSON(Object... args) {
+               super(args);
+       }
+
+}
index 8b8ea921ee5afddc80fcaecef3bf8f6de1bd99b1..6cc73f3fa0d891223a8f87aaf9949cc06cb17175 100644 (file)
@@ -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 (file)
index 0000000..5a4b6b4
--- /dev/null
@@ -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 (file)
index 0000000..3b60d0b
--- /dev/null
@@ -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);
+       }
+}
index 55c7d6cfb269467d45bc826ec1e61db2fed243c8..283cbce735f449c2048be3aa65db35765978a5d3 100644 (file)
@@ -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
index 6282b1e30f1fbac8959cb2730e3c82bbc54cbc78..ec359c6865de427b7532d7978afef4147f9dd7a5 100644 (file)
@@ -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<Supplier<Boolean>> preReadyToDos = new ArrayList<>();
+       private List<PreReadyToDo> 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<Boolean> 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<Boolean> 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<Boolean> getReadyStage() {
+       public CompletionStage<Boolean> 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<Boolean> 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
         */