From 21433ef7b049abb44bbd3c815b6724a15912accf Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 1 Sep 2023 12:26:24 +0200 Subject: [PATCH] Map feature popup --- .../org.argeo.app.geo.js/OpenLayersMapPart.js | 54 +++++++++++++++++- .../src/org.argeo.app.geo.js/index.html | 57 +++++++++++++++++++ .../src/org/argeo/app/geo/ux/MapPart.java | 3 + .../org/argeo/app/geo/swt/SwtJSMapPart.java | 23 ++++++-- .../org/argeo/app/swt/ux/SuiteSwtUtils.java | 3 +- .../src/org/argeo/app/ui/SuiteUiUtils.java | 3 +- 6 files changed, 135 insertions(+), 8 deletions(-) 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 6eff99f..b2d0911 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 @@ -13,6 +13,7 @@ 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 Overlay from 'ol/Overlay.js'; import MapPart from './MapPart.js'; import { SentinelCloudless } from './OpenLayerTileSources.js'; @@ -79,9 +80,10 @@ export default class OpenLayersMapPart extends MapPart { let mapPart = this; this.#map.on('singleclick', function(e) { let feature = null; - // we chose only one + // we chose the first one e.map.forEachFeatureAtPixel(e.pixel, function(f) { feature = f; + return true; }); if (feature !== null) mapPart.callbacks['onFeatureSingleClick'](feature.get('path')); @@ -100,4 +102,54 @@ export default class OpenLayersMapPart extends MapPart { } }); } + + enableFeaturePopup() { + // we cannot use 'this' in the function provided to OpenLayers + let mapPart = this; + /** + * Elements that make up the popup. + */ + const container = document.getElementById('popup'); + const content = document.getElementById('popup-content'); + const closer = document.getElementById('popup-closer'); + + /** + * Create an overlay to anchor the popup to the map. + */ + const overlay = new Overlay({ + element: container, + autoPan: false, + autoPanAnimation: { + duration: 250, + }, + }); + this.#map.addOverlay(overlay); + + let selected = null; + this.#map.on('pointermove', function(e) { + if (selected !== null) { + selected.setStyle(undefined); + selected = null; + } + + e.map.forEachFeatureAtPixel(e.pixel, function(f) { + selected = f; + return true; + }); + + if (selected == null) { + overlay.setPosition(undefined); + return; + } + const coordinate = e.coordinate; + const path = selected.get('path'); + const res = mapPart.callbacks['onFeaturePopup'](path); + if (res != null) { + content.innerHTML = res; + overlay.setPosition(coordinate); + } else { + overlay.setPosition(undefined); + } + }); + } } diff --git a/org.argeo.app.geo.js/src/org.argeo.app.geo.js/index.html b/org.argeo.app.geo.js/src/org.argeo.app.geo.js/index.html index f4371ce..12fa0a1 100644 --- a/org.argeo.app.geo.js/src/org.argeo.app.geo.js/index.html +++ b/org.argeo.app.geo.js/src/org.argeo.app.geo.js/index.html @@ -8,11 +8,68 @@ width: 100%; height: 100vh; } + + .ol-popup { + position: absolute; + background-color: white; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + padding: 5px; + border-radius: 10px; + border: 1px solid #cccccc; + bottom: 12px; + left: -50px; + min-width: 130px; + } + + .ol-popup:after, + .ol-popup:before { + top: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + .ol-popup:after { + border-top-color: white; + border-width: 10px; + left: 48px; + margin-left: -10px; + } + + .ol-popup:before { + border-top-color: #cccccc; + border-width: 11px; + left: 48px; + margin-left: -11px; + } + + .ol-popup-closer { + text-decoration: none; + position: absolute; + top: 2px; + right: 8px; + } + + .ol-popup-closer:after { + content: "✖"; + } + + #popup-content { + font: 16px sans-serif; + }
+ + + \ No newline at end of file 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 13190c9..11ee0d1 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 @@ -22,4 +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) { + }; } 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 index 09848f9..16fa83e 100644 --- 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 @@ -4,6 +4,7 @@ import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Function; import org.argeo.api.cms.CmsLog; import org.argeo.app.geo.ux.JsImplementation; @@ -126,22 +127,34 @@ public class SwtJSMapPart extends Composite implements MapPart { * CALLBACKS */ public void onFeatureSelected(Consumer toDo) { - addCallback("FeatureSelected", (arr) -> toDo.accept(new FeatureSelectedEvent((String) arr[0]))); + addCallback("FeatureSelected", (arr) -> { + toDo.accept(new FeatureSelectedEvent((String) arr[0])); + return null; + }); } public void onFeatureSingleClick(Consumer toDo) { - addCallback("FeatureSingleClick", (arr) -> toDo.accept(new FeatureSingleClickEvent((String) arr[0]))); + addCallback("FeatureSingleClick", (arr) -> { + toDo.accept(new FeatureSingleClickEvent((String) arr[0])); + return null; + }); + } + + public void onFeaturePopup(Function toDo) { + addCallback("FeaturePopup", (arr) -> { + return toDo.apply(new FeaturePopupEvent((String) arr[0])); + }); } - protected void addCallback(String suffix, Consumer toDo) { + protected void addCallback(String suffix, Function 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; + Object result = toDo.apply(arguments); + return result; } }; diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/ux/SuiteSwtUtils.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/ux/SuiteSwtUtils.java index 39cde1b..c5aacd6 100644 --- a/swt/org.argeo.app.swt/src/org/argeo/app/swt/ux/SuiteSwtUtils.java +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/ux/SuiteSwtUtils.java @@ -11,6 +11,7 @@ import org.argeo.api.cms.ux.CmsEditable; import org.argeo.api.cms.ux.CmsStyle; import org.argeo.app.ux.SuiteStyle; import org.argeo.cms.Localized; +import org.argeo.cms.acr.ContentUtils; import org.argeo.cms.swt.CmsSwtUtils; import org.argeo.cms.swt.acr.Img; import org.argeo.cms.swt.dialogs.CmsFeedback; @@ -182,7 +183,7 @@ public class SuiteSwtUtils { * CONTENT */ public static String toLink(Content content) { - return content != null ? "#" + CmsSwtUtils.cleanPathForUrl(content.getPath()) : null; + return content != null ? "#" + ContentUtils.cleanPathForUrl(content.getPath()) : null; } public static Text addFormLine(Composite parent, Localized label, Content content, QNamed property, diff --git a/swt/org.argeo.app.ui/src/org/argeo/app/ui/SuiteUiUtils.java b/swt/org.argeo.app.ui/src/org/argeo/app/ui/SuiteUiUtils.java index e649bfc..62463c7 100644 --- a/swt/org.argeo.app.ui/src/org/argeo/app/ui/SuiteUiUtils.java +++ b/swt/org.argeo.app.ui/src/org/argeo/app/ui/SuiteUiUtils.java @@ -19,6 +19,7 @@ import org.argeo.app.api.EntityNames; import org.argeo.app.api.EntityType; import org.argeo.app.swt.ux.SuiteSwtUtils; import org.argeo.app.ux.SuiteUxEvent; +import org.argeo.cms.acr.ContentUtils; import org.argeo.cms.jcr.acr.JcrContent; import org.argeo.cms.swt.CmsSwtUtils; import org.argeo.cms.swt.dialogs.LightweightDialog; @@ -220,7 +221,7 @@ public class SuiteUiUtils { } public static String toLink(Node node) { - return node != null ? "#" + CmsSwtUtils.cleanPathForUrl(JcrContent.nodeToContent(node).getPath()) : null; + return node != null ? "#" + ContentUtils.cleanPathForUrl(JcrContent.nodeToContent(node).getPath()) : null; } public static Control addLink(Composite parent, String label, Node node, CmsStyle style) -- 2.30.2