diff options
Diffstat (limited to 'main.js')
-rw-r--r-- | main.js | 297 |
1 files changed, 168 insertions, 129 deletions
@@ -1,5 +1,5 @@ /*********************************************************************** - * Copyright © 2024 Guilhem Moulin <info@guilhem.se> + * Copyright © 2024-2025 Guilhem Moulin <info@guilhem.se> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -36,7 +36,6 @@ import { createXYZ } from 'ol/tilegrid.js'; import VectorLayer from 'ol/layer/Vector.js'; import VectorSource from 'ol/source/Vector.js'; -import GeoJSON from 'ol/format/GeoJSON.js'; import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; @@ -152,7 +151,6 @@ const map = new Map({ }); const popup = document.getElementById('popup'); -const featureOverlaySource = new VectorSource(); /* move the control container to the viewport */ const container = document.getElementById('map-control-container'); @@ -351,7 +349,7 @@ if (window.location === window.parent.location) { map.addControl(control); control.addEventListener('enterfullscreen', function() { - featureOverlaySource.clear(true); + featureOverlayLayer.setVisible(false); const popover = Popover.getInstance(popup); if (popover !== null) { /* dispose popover as entering fullscreen messes up its position */ @@ -370,7 +368,7 @@ if (window.location === window.parent.location) { } }) control.addEventListener('leavefullscreen', function() { - featureOverlaySource.clear(true); + featureOverlayLayer.setVisible(false); const popover = Popover.getInstance(popup); if (popover !== null) { /* dispose popover as is might overflow the viewport */ @@ -489,7 +487,7 @@ if (window.location === window.parent.location) { container.setAttribute('aria-hidden', 'false'); view.on('change', function(event) { - featureOverlaySource.clear(true); + featureOverlayLayer.setVisible(false); const popover = Popover.getInstance(popup); if (popover !== null) { popover.dispose(); @@ -3195,7 +3193,7 @@ const styles = (function() { }, {}); })(); -const vectorLayers = (function() { +const [vectorLayers, featureOverlayLayer] = (function() { const xyz = '/{z}/{x}/{y}.pbf'; const tileGrid = createXYZ({ extent: extent, @@ -3204,46 +3202,70 @@ const vectorLayers = (function() { minZoom: 0, maxZoom: 7, }); - return Object.fromEntries( - ['mrr', 'nvr', 'ren', 'ri', 'sks', 'svk', 'vbk', 'misc'] - .map(function(k) { - let visible = false; - Object.keys(layers).forEach(function(lyr) { - if (lyr.startsWith(k + '_')) { - visible ||= styles[lyr] !== undefined; - } - }); - const vectorLayer = new VectorTileLayer({ - source: new VectorTile({ - url: '/tiles/' + k + xyz, - format: new MVT(), - projection: projection, - wrapX: false, - transition: 0, - tileGrid: tileGrid, - }), - /* XXX switch to 'hybrid' if there are perf issues; but that seems to - * put lines above points regardless of their respective z-index */ - renderMode: 'hybrid', - declutter: false, - visible: visible, - style: function(feature, resolution) { - const style = styles[k + '_' + feature.getProperties().layer]; - if (!Array.isArray(style)) { - return style; - } else { - const maxi = style.length - 1; - const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); - /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ - const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); - // console.log(`resolution=${resolution}, z=${z}, i=${i}`); - return style[i]; + return [ + Object.fromEntries( + ['mrr', 'nvr', 'ren', 'ri', 'sks', 'svk', 'vbk', 'misc'] + .map(function(k) { + let visible = false; + Object.keys(layers).forEach(function(lyr) { + if (lyr.startsWith(k + '_')) { + visible ||= styles[lyr] !== undefined; } - }}); - vectorLayer.set('layerGroup', k, true); - map.addLayer(vectorLayer); - return [k, vectorLayer]; - })); + }); + const vectorLayer = new VectorTileLayer({ + source: new VectorTile({ + url: '/tiles/' + k + xyz, + format: new MVT(), + projection: projection, + wrapX: false, + transition: 0, + tileGrid: tileGrid, + }), + /* XXX switch to 'hybrid' if there are perf issues; but that seems to + * put lines above points regardless of their respective z-index */ + renderMode: 'hybrid', + declutter: false, + visible: visible, + style: function(feature, resolution) { + const style = styles[k + '_' + feature.getProperties().layer]; + if (!Array.isArray(style)) { + return style; + } else { + const maxi = style.length - 1; + const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); + /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ + const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); + // console.log(`resolution=${resolution}, z=${z}, i=${i}`); + return style[i]; + } + }}); + vectorLayer.set('layerGroup', k, true); + map.addLayer(vectorLayer); + return [k, vectorLayer]; + })), + + /* We use a vector tile layer for featureOverlayLayer instead of a simple + * vector layer overlay, since we don't want to clip selected geometries at + * tile boundaries. It sounds overkill to load an entire tile layer to + * display a single feature, but this shouldn't cause much overhead in + * practice (the tiles are most likely cached already). */ + new VectorTileLayer({ + source: new VectorTile({ + urls: [], + format: new MVT(), + projection: projection, + wrapX: false, + transition: 0, + tileGrid: tileGrid, + }), + zIndex: 65535, + declutter: false, + visible: false, + renderMode: 'vector', + map: map, + style: null, + }), + ]; })(); @@ -3501,19 +3523,7 @@ const vectorLayers = (function() { }); map.addOverlay(popupOverlay); - map.addLayer(new VectorLayer({ - source: featureOverlaySource, - zIndex: 65535, - style: new Style({ - stroke: new Stroke({ - color: 'rgba(0, 255, 255, .8)', - width: 3, - }), - }), - })); - - const features = []; - let popover, featureNum = 0; + let popover, overlayAttributes = [], overlayAttrIdx = 0; const header = document.createElement('div'); header.classList.add('d-flex'); @@ -3577,12 +3587,38 @@ const vectorLayers = (function() { 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 = vectorLayers[layer_group]; + if (lyr === undefined) { + 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, resolution) { + if (feature.getId() === id && feature.getProperties().layer === layer) { + return featureOverlayStyle; + } else { + return undefined; + } + }); + featureOverlayLayer.setVisible(true); + featureOverlayLayer.changed(); + }; const refreshPopover = function() { - const x = features[featureNum]; - featureOverlaySource.clear(true); - featureOverlaySource.addFeature(x.feature); + const x = overlayAttributes[overlayAttrIdx]; + updateFeatureOverlayLayer(x.layer_group, x.layer, x.id); - pageNum.innerHTML = (featureNum + 1).toString(); + pageNum.innerHTML = (overlayAttrIdx + 1).toString(); popover.tip.getElementsByClassName('popover-body')[0]. replaceChildren(x.formattedContent); }; @@ -3591,17 +3627,17 @@ const vectorLayers = (function() { if (btn.classList.contains('disabled') || popover === null || popover.tip === null) { return; } - if (featureNum + offset < 0 || featureNum + offset > features.length - 1) { + if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) { return; } - featureNum += offset; - if (featureNum < 1) { + overlayAttrIdx += offset; + if (overlayAttrIdx < 1) { btnPrev.classList.add('disabled'); } else { btnPrev.classList.remove('disabled'); } - if (featureNum < features.length - 1) { + if (overlayAttrIdx < overlayAttributes.length - 1) { btnNext.classList.remove('disabled'); } else { btnNext.classList.add('disabled'); @@ -3659,7 +3695,8 @@ const vectorLayers = (function() { btnClose.title = 'Stäng'; btnClose.setAttribute('aria-label', btnClose.title); btnClose.onclick = function(event) { - featureOverlaySource.clear(true); + featureOverlayLayer.setVisible(false); + featureOverlayLayer.changed(); if (popover !== null) { popover.dispose(); } @@ -3672,10 +3709,11 @@ const vectorLayers = (function() { const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; map.on('singleclick', function(event) { - /* clear the features list */ - featureOverlaySource.clear(true); - features.length = 0; - featureNum = 0; + /* 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); @@ -3729,21 +3767,26 @@ const vectorLayers = (function() { layerFilter: (l) => l !== null && l.get('layerGroup') !== undefined, }); - if (fetch_body.length > 0) { - // TODO use https://github.com/uhop/stream-json/wiki/StreamArray - // to process each item immediately - fetch('/q', { - method: 'POST', - body: JSON.stringify(fetch_body), - headers: { - 'Content-Type': 'application/json; charset=UTF-8' - } - }).then((resp) => resp.json()) - .then((data) => data.forEach(function(data) { - document.body.classList.remove('progress'); - const properties = data.properties || {}; - const feature0 = new GeoJSON().readFeature(data); + if (fetch_body.length === 0) { + if (popover !== null) { + /* 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((resp) => resp.json()) + .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.map(function(properties) { /* turn the properties into a fine table */ const table = document.createElement('table'); table.classList.add('table', 'table-sm', 'table-borderless', 'table-hover'); @@ -3820,51 +3863,47 @@ const vectorLayers = (function() { } content.appendChild(table); - features.push({feature: feature0, formattedContent: content}); - - pageCount.innerHTML = features.length.toString(); - if (features.length == 2) { - /* there are ≥2 features, render prev/pre buttons */ - btnNext.classList.remove('d-none', 'disabled'); - btnPrev.classList.remove('d-none'); - pageNode.classList.remove('d-none'); - } - - if (features.length > 1) { - /* we're already showing the first feature */ - return; - } + return { + layer_group: properties.layer_group, + layer: properties.layer, + id: properties.ogc_fid, + formattedContent: content + }; + }); - 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(); - featureOverlaySource.addFeature(feature0); - } - else if (popover.tip.classList.contains('popover-detached')) { - refreshPopover(); - } - })) - .catch(function() { + 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 === null || 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]; + 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: attr.formattedContent, + html: true, + placement: 'right', + fallbackPlacements: ['right', 'left', 'bottom', 'top'], + container: container0, + }); + popover.show(); + updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.id); + } + else if (popover.tip.classList.contains('popover-detached')) { + /* update existing detached mode popover */ + refreshPopover(); + } + }) + .finally(function() { document.body.classList.remove('progress'); }); - } - - if (features.length === 0 && popover !== null && popover.tip !== null) { - /* dispose pre-detached popover */ - popover.dispose(); - } }); }()); |