diff options
-rw-r--r-- | index.html | 1 | ||||
-rw-r--r-- | main.js | 220 | ||||
-rw-r--r-- | style.css | 73 |
3 files changed, 286 insertions, 8 deletions
@@ -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"> @@ -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, + }); + }); +}()); @@ -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"); +} |