diff options
author | Guilhem Moulin <guilhem@fripost.org> | 2025-06-12 14:18:28 +0200 |
---|---|---|
committer | Guilhem Moulin <guilhem@fripost.org> | 2025-06-12 19:31:47 +0200 |
commit | 05a018f27aba3a20fd581cb88daa8afbbd3407de (patch) | |
tree | 8d27d74c988a18cbac27caff6bc95e9a5d1b3240 /main.js | |
parent | 1f09018cf8c5e2ddc27a5afa89efeaf19c0eac4a (diff) |
Factor out Popover handling into separate module.
Diffstat (limited to 'main.js')
-rw-r--r-- | main.js | 407 |
1 files changed, 2 insertions, 405 deletions
@@ -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); |