aboutsummaryrefslogtreecommitdiffstats
path: root/main.js
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2025-06-12 14:18:28 +0200
committerGuilhem Moulin <guilhem@fripost.org>2025-06-12 19:31:47 +0200
commit05a018f27aba3a20fd581cb88daa8afbbd3407de (patch)
tree8d27d74c988a18cbac27caff6bc95e9a5d1b3240 /main.js
parent1f09018cf8c5e2ddc27a5afa89efeaf19c0eac4a (diff)
Factor out Popover handling into separate module.
Diffstat (limited to 'main.js')
-rw-r--r--main.js407
1 files changed, 2 insertions, 405 deletions
diff --git a/main.js b/main.js
index acc5cd6..aa58199 100644
--- a/main.js
+++ b/main.js
@@ -29,8 +29,6 @@ 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';
@@ -43,8 +41,6 @@ import Point from 'ol/geom/Point.js';
import Fill from 'ol/style/Fill.js';
import Icon from 'ol/style/Icon.js';
-import Stroke from 'ol/style/Stroke.js';
-import Style from 'ol/style/Style.js';
import proj4 from 'proj4';
import { get as getProjection } from 'ol/proj.js';
@@ -53,6 +49,7 @@ import { register as registerProjection } from 'ol/proj/proj4.js';
import { Modal, Popover } from 'bootstrap';
import { layers } from './src/layers.js';
+import { popover } from './src/popover.js';
import './src/style.css';
proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs');
@@ -2234,404 +2231,4 @@ const infoMetadataAccordions = [];
};
})();
-/* popup and feature overlays */
-(function() {
- const popupOverlay = new Overlay({
- stopEvent: true,
- element: popup,
- });
- map.addOverlay(popupOverlay);
-
- let popover, overlayAttributes = [], overlayAttrIdx = 0;
-
- const header = document.createElement('div');
- header.classList.add('d-flex');
-
- const headerGrabbingArea = document.createElement('div');
- headerGrabbingArea.classList.add('flex-grow-1', 'grabbing-area', 'pe-2', 'me-2');
- header.appendChild(headerGrabbingArea);
-
- headerGrabbingArea.onmousedown = function(event) {
- if (event.button != 0) {
- return;
- }
- const popoverTip = Popover.getInstance(popup).tip;
- if (popoverTip.classList.contains('popover-maximized')) {
- return;
- }
- pageNode.classList.add('grabbing-area-grabbed');
-
- if (!popoverTip.classList.contains('popover-detached')) {
- /* detach popover tip */
- popoverTip.classList.add('popover-detached');
- const rect = popoverTip.getBoundingClientRect();
- const style = popoverTip.style;
- style.display = 'none'; /* avoid reflows between the following assignments */
- style.position = 'absolute';
- style.transform = '';
- style.inset = `${rect.top}px auto auto ${rect.left}px`;
- style.display = '';
- }
-
- let clientX = event.clientX, clientY = event.clientY;
- document.onmousemove = function(event) {
- const offsetX = clientX - event.clientX;
- const offsetY = clientY - event.clientY;
- clientX = event.clientX;
- clientY = event.clientY;
- popoverTip.style.top = (popoverTip.offsetTop - offsetY).toString() + 'px';
- popoverTip.style.left = (popoverTip.offsetLeft - offsetX).toString() + 'px';
- };
-
- document.onmouseup = function(event) {
- if (event.button != 0) {
- return;
- }
- pageNode.classList.remove('grabbing-area-grabbed');
- document.onmousemove = null;
- document.onmouseup = null;
- };
- };
-
- const pageNode = document.createElement('h6');
- headerGrabbingArea.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 featureOverlayStyle = new Style({
- stroke: new Stroke({
- color: 'rgba(0, 255, 255, .8)',
- width: 3,
- }),
- });
- const updateFeatureOverlayLayer = function(layer_group, layer, id) {
- const lyr = mapLayers[layer_group];
- if (lyr == null) {
- return;
- }
- const urls = lyr.getSource().getUrls();
- const source = featureOverlayLayer.getSource();
- if (source.getUrls().length < 1 || source.getUrls()[0] !== urls[0]) {
- featureOverlayLayer.setVisible(false);
- source.setUrls(urls);
- }
- featureOverlayLayer.setStyle(function(feature) {
- if (feature.getId() === id && feature.getProperties().layer === layer) {
- return featureOverlayStyle;
- } else {
- return undefined;
- }
- });
- featureOverlayLayer.setVisible(true);
- featureOverlayLayer.changed();
- };
- const refreshPopover = function() {
- const attr = overlayAttributes[overlayAttrIdx];
- updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.ogc_fid);
-
- pageNum.innerHTML = (overlayAttrIdx + 1).toString();
- const content = formatFeaturePropertiesToHTML(attr);
- popover.tip.getElementsByClassName('popover-body')[0].replaceChildren(content);
- };
- const onClickPageChange = function(event, offset) {
- const btn = event.target;
- if (btn.classList.contains('disabled') || popover?.tip == null) {
- return;
- }
- if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) {
- return;
- }
-
- overlayAttrIdx += offset;
- if (overlayAttrIdx < 1) {
- btnPrev.classList.add('disabled');
- } else {
- btnPrev.classList.remove('disabled');
- }
- if (overlayAttrIdx < overlayAttributes.length - 1) {
- btnNext.classList.remove('disabled');
- } else {
- btnNext.classList.add('disabled');
- }
-
- refreshPopover();
- 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.setAttribute('aria-label', btnPrev.title);
- 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.setAttribute('aria-label', btnNext.title);
- btnNext.onclick = function(event) {
- return onClickPageChange(event, +1);
- };
-
- const btnExpand = document.createElement('button');
- btnExpand.classList.add('popover-button', 'popover-button-expand');
- btnExpand.setAttribute('type', 'button');
- const btnExpandTitle = 'Förstora';
- const btnExpandTitle2 = 'Förminska';
- btnExpand.setAttribute('aria-label', btnExpand.title);
- btnExpand.onclick = function() {
- if (popover?.tip == null) {
- return;
- }
- if (!popover.tip.classList.contains('popover-maximized')) {
- popover.tip.classList.add('popover-maximized');
- btnExpand.classList.replace('popover-button-expand', 'popover-button-reduce');
- btnExpand.title = btnExpandTitle2;
- btnExpand.setAttribute('aria-label', btnExpand.title);
- } else {
- popover.tip.classList.remove('popover-maximized');
- btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand');
- btnExpand.title = btnExpandTitle;
- btnExpand.setAttribute('aria-label', btnExpand.title);
- }
- setTimeout(function() { btnExpand.blur() }, 100);
- };
-
- const btnClose = document.createElement('button');
- btnClose.classList.add('popover-button', 'popover-button-close');
- btnClose.setAttribute('type', 'button');
- btnClose.title = 'Stäng';
- btnClose.setAttribute('aria-label', btnClose.title);
- btnClose.onclick = function() {
- featureOverlayLayer.setVisible(false);
- featureOverlayLayer.changed();
- popover?.dispose();
- };
-
- header.appendChild(btnPrev);
- header.appendChild(btnNext);
- header.appendChild(btnExpand);
- header.appendChild(btnClose);
-
- const formatFeaturePropertiesToHTML = function(properties) {
- /* 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);
-
- const def = layers[properties.layer_group + '.' + properties.layer];
- def.popover.forEach(function([desc, key, opts]) {
- let v = properties[key];
- if (opts === undefined) {
- opts = {};
- }
- if (opts.fn !== undefined) {
- if (opts.fn === 'length') {
- if (v < 1000) {
- opts.unit = 'm';
- } else {
- v /= 1000;
- v = Math.round(v*100) / 100;
- opts.unit = 'km';
- }
- } else if (opts.fn === 'area') {
- if (v < 10000) {
- opts.unit = 'm²';
- } else if (v < 10000 * 10000) {
- v /= 10000;
- opts.unit = 'ha';
- } else {
- v /= 1000000;
- opts.unit = 'km²';
- }
- v = Math.round(v*100) / 100;
- } else {
- v = opts.fn(v);
- }
- }
- if (v == null) {
- v = document.createTextNode('');
- } else if (!(v instanceof HTMLElement)) {
- if (typeof(v) === 'number' && opts.unit !== undefined) {
- v = v.toLocaleString('sv-SE');
- } else if (typeof(v) === 'boolean') {
- v = v ? 'Ja' : 'Nej';
- }
- if (opts.unit !== undefined && v !== '') {
- v += '\u202F' + opts.unit;
- }
- 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);
- }
-
- content.appendChild(table);
- return content;
- };
-
- const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0];
- map.on('singleclick', function(event) {
- /* clear the overlay list */
- featureOverlayLayer.setVisible(false);
- featureOverlayLayer.changed();
- overlayAttributes = [];
- overlayAttrIdx = 0;
-
- /* dispose any pre-existing popover if not in detached mode */
- popover = Popover.getInstance(popup);
- if (popover !== null) {
- const popoverTip = popover.tip;
- if (popoverTip !== null && !popoverTip.classList.contains('popover-detached')) {
- popover.dispose();
- }
- }
-
- const size = map.getSize();
- if (size[0] < 576 || size[1] < 576) {
- return;
- }
-
- /* unclear how many feature we'll find, don't render prev/next buttons for now */
- pageNode.classList.add('d-none');
- btnPrev.classList.add('d-none', 'disabled');
- btnNext.classList.add('d-none', 'disabled');
-
- /* never start in maximized mode */
- if (popover?.tip != null) {
- popover.tip.classList.remove('popover-maximized');
- }
- btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand');
- btnExpand.title = btnExpandTitle;
- btnExpand.setAttribute('aria-label', btnExpand.title);
-
- const fetch_body = []
- map.forEachFeatureAtPixel(event.pixel, function(feature, layer) {
- const layerGroup = layer.get('layerGroup');
- const layerName = feature.getProperties().layer;
- const def = layers[layerGroup + '.' + layerName];
- if (def?.popover != null) {
- /* skip layers which didn't opt-in for popover */
- if (!fetch_body.length) {
- document.body.classList.add('inprogress');
- if (popover?.tip != null) {
- popover.tip.classList.add('inprogress');
- }
- }
- fetch_body.push({
- layer_group: layerGroup,
- layer: layerName,
- fid: feature.getId() ?? -1,
- });
- if (fetch_body.length >= 100) {
- return true; /* enough matches already, stop detection here */
- }
- }
- }, {
- hitTolerance: 5,
- checkWrapped: false,
- layerFilter: (l) => l.get('layerGroup') != null,
- });
-
- if (fetch_body.length === 0) {
- /* dispose pre-detached popover */
- popover?.dispose();
- return;
- }
-
- fetch('/q', {
- method: 'POST',
- body: JSON.stringify(fetch_body),
- headers: {
- 'Content-Type': 'application/json; charset=UTF-8'
- }
- })
- .then(function(resp) {
- if (resp.status === 200) {
- return resp.json();
- } else {
- throw new Error(`${resp.url} [${resp.status}]`);
- }
- })
- .then(function(data) {
- /* the data is received from the CGI in the order it was sent */
- /* TODO optimizations on the CGI would break the above assumption, so the
- * decoded JSON response would need to be reordered to match fetch_body */
- overlayAttributes = data
- if (overlayAttributes.length === 0) {
- /* dispose pre-detached popover */
- popover?.dispose();
- return;
- }
-
- pageCount.innerHTML = overlayAttributes.length.toString();
- if (overlayAttributes.length >= 2) {
- /* render prev/pre buttons */
- btnNext.classList.remove('d-none', 'disabled');
- btnPrev.classList.remove('d-none');
- pageNode.classList.remove('d-none');
- }
- if (popover?.tip == null) {
- /* create a new popover (we're not already showing one in detached mode) */
- pageNum.innerHTML = (overlayAttrIdx + 1).toString();
- popupOverlay.setPosition(event.coordinate);
-
- const attr = overlayAttributes[0];
- updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.ogc_fid);
- 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: formatFeaturePropertiesToHTML(attr),
- html: true,
- placement: 'right',
- fallbackPlacements: ['right', 'left', 'bottom', 'top'],
- container: container0,
- });
- popover.show();
- }
- else if (popover.tip.classList.contains('popover-detached')) {
- /* update existing detached mode popover */
- refreshPopover();
- popover.tip.classList.remove('inprogress');
- }
- })
- .catch(function(e) {
- console.log(e);
- })
- .finally(function() {
- document.body.classList.remove('inprogress');
- });
- });
-}());
+popover(map, mapLayers, featureOverlayLayer);