From 1b6b4952f53c90e7e071fa5a71cea48d74e8d2e4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 20 Jan 2024 21:15:18 +0100 Subject: 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 --- main.js | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 212 insertions(+), 8 deletions(-) (limited to 'main.js') 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: '', + title: header, + content: content, + html: true, + placement: 'right', + fallbackPlacements: ['right', 'left', 'bottom', 'top'], + container: container0, + }); + popover.show(); + } + }, { + hitTolerance: 5, + checkWrapped: false, + }); + }); +}()); -- cgit v1.2.3