aboutsummaryrefslogtreecommitdiffstats
path: root/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'main.js')
-rw-r--r--main.js297
1 files changed, 168 insertions, 129 deletions
diff --git a/main.js b/main.js
index 250cfa6..3e6ff84 100644
--- a/main.js
+++ b/main.js
@@ -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();
- }
});
}());