Map feature popup
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 1 Sep 2023 10:26:24 +0000 (12:26 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 1 Sep 2023 10:26:24 +0000 (12:26 +0200)
org.argeo.app.geo.js/src/org.argeo.app.geo.js/OpenLayersMapPart.js
org.argeo.app.geo.js/src/org.argeo.app.geo.js/index.html
org.argeo.app.geo/src/org/argeo/app/geo/ux/MapPart.java
swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java
swt/org.argeo.app.swt/src/org/argeo/app/swt/ux/SuiteSwtUtils.java
swt/org.argeo.app.ui/src/org/argeo/app/ui/SuiteUiUtils.java

index 6eff99f2ff58d8fe3635474bdc2a0e0628585fff..b2d09115f94309e890f5d31936f0b3f857d9c65a 100644 (file)
@@ -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);
+                       }
+               });
+       }
 }
index f4371ceef66f26f09311cb02a4d3fb24a4e2577f..12fa0a1e4ab8fc0cdcf51f117982dade67a921d1 100644 (file)
@@ -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;
+               }
        </style>
 </head>
 
 <body>
        <div id="map" class="map"></div>
+
+       <!-- Popup -->
+       <div id="popup" class="ol-popup">
+               <div id="popup-content"></div>
+       </div>
 </body>
 
 </html>
\ No newline at end of file
index 13190c98d238f75db9ef86a58b3aea8ac6dce874..11ee0d11875345f1b716acb53df0e9ba9e3be39c 100644 (file)
@@ -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) {
+       };
 }
index 09848f9757f7069008f01f578a586b8f0518aadd..16fa83edb96bd13a129f059d90ba4a351ac6bfb4 100644 (file)
@@ -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<FeatureSelectedEvent> 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<FeatureSingleClickEvent> 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<FeaturePopupEvent, String> toDo) {
+               addCallback("FeaturePopup", (arr) -> {
+                       return toDo.apply(new FeaturePopupEvent((String) arr[0]));
+               });
        }
 
-       protected void addCallback(String suffix, Consumer<Object[]> toDo) {
+       protected void addCallback(String suffix, Function<Object[], 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;
+                                       Object result = toDo.apply(arguments);
+                                       return result;
                                }
 
                        };
index 39cde1b8cfc01c59ce9d842b0a31070d7bded5a7..c5aacd6af6303142e983b66bdc790f7ee58a580a 100644 (file)
@@ -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,
index e649bfc9b1f4118cb5c20f7ed5655609c724b047..62463c792eab8a2a666867e51c536d37fa26e563 100644 (file)
@@ -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)