aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2024-01-20 21:15:18 +0100
committerGuilhem Moulin <guilhem@fripost.org>2024-01-22 01:18:51 +0100
commit1b6b4952f53c90e7e071fa5a71cea48d74e8d2e4 (patch)
treeb506c7becadf76c20c35a4442f334df2d091ad51
parent46fc2671c139cdce834b29e700deba60e626820f (diff)
Show feature properties on popover via singleclick on map.
We use bootstrap's Popover for that: https://getbootstrap.com/docs/5.3/components/popovers/#options
-rw-r--r--index.html1
-rw-r--r--main.js220
-rw-r--r--style.css73
3 files changed, 286 insertions, 8 deletions
diff --git a/index.html b/index.html
index 9eaa190..33b7f7d 100644
--- a/index.html
+++ b/index.html
@@ -12,6 +12,7 @@
<div id="layer-selection-panel"></div>
<div id="map-legend-panel"></div>
<div id="map-menu"></div>
+ <div id="popup"></div>
</div>
<div class="modal" id="modal-info" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
diff --git a/main.js b/main.js
index 77d2bdf..bd8dbff 100644
--- a/main.js
+++ b/main.js
@@ -27,6 +27,8 @@ import ScaleLine from 'ol/control/ScaleLine.js';
import Zoom from 'ol/control/Zoom.js';
import ZoomSlider from 'ol/control/ZoomSlider.js';
+import Overlay from 'ol/Overlay.js';
+
import MVT from 'ol/format/MVT.js';
import VectorTileLayer from 'ol/layer/VectorTile.js';
import VectorTile from 'ol/source/VectorTile.js';
@@ -42,7 +44,7 @@ import proj4 from 'proj4';
import { get as getProjection } from 'ol/proj.js';
import { register as registerProjection } from 'ol/proj/proj4.js';
-import { Modal } from 'bootstrap';
+import { Modal, Popover } from 'bootstrap';
import './style.css';
@@ -140,6 +142,7 @@ const map = new Map({
],
target: document.getElementById('map'),
});
+const popup = document.getElementById('popup');
/* move the control container to the viewport */
const container = document.getElementById('map-control-container');
@@ -344,6 +347,12 @@ if (window.location !== window.parent.location) {
}
})
control.addEventListener('leavefullscreen', function() {
+ const popover = Popover.getInstance(popup);
+ if (popover !== null) {
+ /* dispose popover as is might overflow the viewport */
+ popover.dispose();
+ }
+
const btn = control.element.getElementsByTagName('button')[0];
btn.classList.replace(classActive, classInactive);
btn.title = titleInactive;
@@ -452,14 +461,12 @@ if (window.location !== window.parent.location) {
/* we're all set, show the control container now */
container.setAttribute('aria-hidden', 'false');
-map.on('singleclick', function(event) {
- const size = map.getSize();
- if (size[0] < 576 || size[1] < 576) {
- return;
- }
-});
-
view.on('change', function(event) {
+ const popover = Popover.getInstance(popup);
+ if (popover !== null) {
+ popover.dispose();
+ }
+
const coordinates = view.getCenter();
const searchParams = new URLSearchParams(location.hash.substring(1));
searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, ''));
@@ -471,6 +478,11 @@ view.on('change', function(event) {
const layers = {
svk_lines: {
+ popoverTitle: 'Kraftledning (befintlig)',
+ popover: [
+ ['Förläggn', 'FÖRLÄGGN'],
+ ['Spänning', 'SPÄNNING', { fn: (v) => v + '\u202FkV' }],
+ ],
style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) {
return new Style({
zIndex: 2,
@@ -782,3 +794,195 @@ map.addLayer(new VectorTileLayer({
body.classList.add('modal-body');
body.innerHTML = 'legend TODO';
})();
+
+/* popup overlay */
+(function() {
+ const popupOverlay = new Overlay({
+ stopEvent: true,
+ element: popup,
+ });
+
+ map.addOverlay(popupOverlay);
+ const features = [];
+ let popover, featureNum = 0;
+
+ const header = document.createElement('div');
+ header.classList.add('d-flex');
+
+ const pageNode = document.createElement('div');
+ pageNode.classList.add('flex-grow-1', 'pe-4');
+ header.appendChild(pageNode);
+
+ const pageNum = document.createElement('span');
+ const pageCount = document.createElement('span');
+ pageNode.appendChild(document.createTextNode('Träff '));
+ pageNode.appendChild(pageNum);
+ pageNode.appendChild(document.createTextNode(' av '));
+ pageNode.appendChild(pageCount);
+
+ const onClickPageChange = function(event, offset) {
+ const btn = event.target;
+ if (btn.classList.contains('disabled') || popover === null || popover.tip === null) {
+ return;
+ }
+ if (featureNum + offset < 0 || featureNum + offset > features.length - 1) {
+ return;
+ }
+
+ featureNum += offset;
+ if (featureNum < 1) {
+ btnPrev.classList.add('disabled');
+ } else {
+ btnPrev.classList.remove('disabled');
+ }
+ if (featureNum < features.length - 1) {
+ btnNext.classList.remove('disabled');
+ } else {
+ btnNext.classList.add('disabled');
+ }
+
+ pageNum.innerHTML = (featureNum + 1).toString();
+ popover.tip.getElementsByClassName('popover-body')[0].replaceChildren(features[featureNum]);
+ setTimeout(function() { btn.blur() }, 100);
+ };
+
+ const btnPrev = document.createElement('button');
+ btnPrev.classList.add('popover-button', 'popover-button-prev');
+ btnPrev.setAttribute('type', 'button');
+ btnPrev.title = 'Föregående träff';
+ btnPrev.onclick = function(event) {
+ return onClickPageChange(event, -1);
+ };
+
+ const btnNext = document.createElement('button');
+ btnNext.classList.add('popover-button', 'popover-button-next');
+ btnNext.setAttribute('type', 'button');
+ btnNext.title = 'Nästa träff';
+ btnNext.onclick = function(event) {
+ return onClickPageChange(event, +1);
+ };
+
+ const btnClose = document.createElement('button');
+ btnClose.classList.add('popover-button', 'popover-button-close');
+ btnClose.setAttribute('type', 'button');
+ btnClose.title = 'Stäng';
+ btnClose.onclick = function(event) {
+ if (popover !== null) {
+ popover.dispose();
+ }
+ };
+
+ header.appendChild(btnPrev);
+ header.appendChild(btnNext);
+ header.appendChild(btnClose);
+
+ const container0 = map.getViewport().querySelector('.ol-overlay-container.ol-selectable');
+ map.on('singleclick', function(event) {
+ /* clear the features list */
+ features.length = 0;
+ featureNum = 0;
+
+ /* dispose any pre-existing popover */
+ popover = Popover.getInstance(popup);
+ if (popover !== null) {
+ popover.dispose();
+ }
+
+ const size = map.getSize();
+ if (size[0] < 576 || size[1] < 576) {
+ return;
+ }
+
+ /* unclear how many feature we'll find, render prev/next buttons invisible for now */
+ pageNode.classList.add('invisible');
+ btnPrev.classList.add('invisible', 'disabled');
+ btnNext.classList.add('invisible', 'disabled');
+
+ map.forEachFeatureAtPixel(event.pixel, function(feature, layer) {
+ const properties = feature.getProperties();
+ const def = layers[properties.layer];
+ if (def === undefined || def.popover === undefined) {
+ /* skip layers which didn't opt-in for popover */
+ return;
+ }
+
+ /* turn the properties into a fine table */
+ const table = document.createElement('table');
+ table.classList.add('table', 'table-sm', 'table-borderless', 'table-hover');
+
+ const tbody = document.createElement('tbody');
+ table.appendChild(tbody);
+
+ def.popover.forEach(function([desc, key, opts]) {
+ let v = properties[key];
+ if (opts === undefined) {
+ opts = {};
+ }
+ if (opts.fn !== undefined) {
+ v = opts.fn(v);
+ }
+ if (v === undefined) {
+ v = document.createTextNode('');
+ } else if (!(v instanceof HTMLElement)) {
+ v = document.createTextNode(v);
+ }
+
+ const tr = document.createElement('tr');
+ tbody.appendChild(tr);
+
+ const td1 = document.createElement('td');
+ tr.appendChild(td1);
+ const textDesc = document.createTextNode(desc);
+ td1.appendChild(textDesc);
+
+ const td2 = document.createElement('td');
+ tr.appendChild(td2);
+ td2.appendChild(v);
+ if (opts.classes !== undefined) {
+ opts.classes.forEach((c) => td2.classList.add(c));
+ }
+ });
+
+ const content = document.createElement('div');
+ if (def.popoverTitle !== undefined) {
+ const h = document.createElement('h6');
+ content.appendChild(h);
+ const textNode = document.createTextNode(def.popoverTitle);
+ h.appendChild(textNode);
+ }
+
+ // console.log(properties);
+ content.appendChild(table);
+ features.push(content);
+
+ pageCount.innerHTML = features.length.toString();
+ if (features.length == 2) {
+ /* there are ≥2 features, make prev/pre buttons visible */
+ btnNext.classList.remove('invisible', 'disabled');
+ btnPrev.classList.remove('invisible');
+ pageNode.classList.remove('invisible');
+ }
+
+ if (popover === null || popover.tip === null) {
+ /* create a new popover if we're not already showing one */
+ pageNum.innerHTML = (featureNum + 1).toString();
+ popupOverlay.setPosition(event.coordinate);
+
+ popover = new Popover(popup, {
+ template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div>' +
+ '<div class="popover-header"></div><div class="popover-body"></div></div>',
+ title: header,
+ content: content,
+ html: true,
+ placement: 'right',
+ fallbackPlacements: ['right', 'left', 'bottom', 'top'],
+ container: container0,
+ });
+ popover.show();
+ }
+ }, {
+ hitTolerance: 5,
+ checkWrapped: false,
+ });
+ });
+}());
diff --git a/style.css b/style.css
index 96e8bca..aff1fc8 100644
--- a/style.css
+++ b/style.css
@@ -303,3 +303,76 @@ html, body {
#layer-selection-panel .accordion-item .form-check > .form-check-label {
display: inline;
}
+
+.popover {
+ --bs-popover-header-padding-x: .5rem;
+ --bs-popover-header-padding-y: .5rem;
+ --bs-popover-body-padding-x: .25rem;
+ --bs-popover-body-padding-y: .25rem;
+ --bs-popover-header-bg: var(--bs-popover-bg);
+ --bs-popover-zindex: 1000;
+ --bs-popover-max-width: 450px;
+ width: var(--bs-popover-max-width);
+}
+.popover-body {
+ max-height: 1024px;
+ overflow: auto auto;
+}
+.popover-body h6, .popover-body h5 {
+ margin-bottom: var(--bs-popover-body-padding-y);
+ margin-left: calc(var(--bs-popover-header-padding-x) - var(--bs-popover-body-padding-x));
+ margin-right: calc(var(--bs-popover-header-padding-x) - var(--bs-popover-body-padding-x));
+}
+.popover-body .table {
+ --bs-table-bg: none;
+ --bs-table-hover-bg: rgba(0, 0, 0, 0.04);
+ margin: 0;
+}
+.popover-body table > tbody > tr > td:first-child {
+ font-style: italic;
+ white-space: nowrap;
+ padding-right: .75em;
+}
+.popover-body table > tbody > tr > td {
+ padding: 0.1rem var(--bs-popover-body-padding-x);
+}
+
+/* inspired from bootstrap's .btn-close */
+.popover-header .popover-button {
+ box-sizing: content-box;
+ width: 1em;
+ height: 1em;
+ padding: 0.25em 0.25em;
+ color: #000;
+ background: transparent var(--popover-button-icon) center/1em auto no-repeat;
+ border: 0;
+ border-radius: 0.375rem;
+ opacity: .5;
+}
+.popover-header .popover-button:hover {
+ text-decoration: none;
+ opacity: .75;
+}
+.popover-header .popover-button:focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+ opacity: 1;
+}
+.popover-header .popover-button:disabled, .popover-header .popover-button.disabled {
+ pointer-events: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ opacity: .25;
+}
+
+/* SVG from bootstrap icons v1.11.0 (released under MIT License) */
+.popover-header .popover-button-prev {
+ --popover-button-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath transform='scale(1.3799) translate(-2.2025 -2.2025)' d='M10 12.796V3.204L4.519 8zm-.659.753-5.48-4.796a1 1 0 0 1 0-1.506l5.48-4.796A1 1 0 0 1 11 3.204v9.592a1 1 0 0 1-1.659.753'/%3e%3c/svg%3e");
+}
+.popover-header .popover-button-next {
+ --popover-button-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath transform='scale(1.3799) translate(-2.2025 -2.2025)' d='M6 12.796V3.204L11.481 8zm.659.753 5.48-4.796a1 1 0 0 0 0-1.506L6.66 2.451C6.011 1.885 5 2.345 5 3.204v9.592a1 1 0 0 0 1.659.753'/%3e%3c/svg%3e");
+}
+.popover-header .popover-button-close {
+ --popover-button-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath transform='scale(1.3332) translate(-2 -2)' d='M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z'/%3e%3c/svg%3e");
+}