aboutsummaryrefslogtreecommitdiffstats
path: root/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'main.js')
-rw-r--r--main.js1127
1 files changed, 850 insertions, 277 deletions
diff --git a/main.js b/main.js
index b1d466f..9840746 100644
--- a/main.js
+++ b/main.js
@@ -41,6 +41,11 @@ import Polygon from 'ol/geom/Polygon.js';
import LineString from 'ol/geom/LineString.js';
import Point from 'ol/geom/Point.js';
+import VectorLayer from 'ol/layer/Vector.js';
+import VectorSource from 'ol/source/Vector.js';
+import Draw from 'ol/interaction/Draw.js';
+import { unByKey } from 'ol/Observable.js';
+
import CircleStyle from 'ol/style/Circle.js';
import Fill from 'ol/style/Fill.js';
import Icon from 'ol/style/Icon.js';
@@ -48,6 +53,8 @@ import RegularShape from 'ol/style/RegularShape.js';
import Stroke from 'ol/style/Stroke.js';
import Style from 'ol/style/Style.js';
+import Geolocation from 'ol/Geolocation.js';
+
import proj4 from 'proj4';
import { get as getProjection } from 'ol/proj.js';
import { register as registerProjection } from 'ol/proj/proj4.js';
@@ -62,6 +69,7 @@ proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0
registerProjection(proj4);
const PROJECTION = getProjection('EPSG:3006');
+const LOCALE = 'sv-SE';
/* Lantmäteriet uses a tile-scheme where the origin (upper-left corner) is at
* N8500000 E-1200000 (SWEREF99 TM), where each tile is 256×256 pixels, and where
@@ -134,9 +142,6 @@ const [BASEMAP, MAP] = (function() {
projection: PROJECTION,
extent: EXTENT,
showFullExtent: true,
- /* center of the bbox of the Norrbotten and Västerbotten geometries */
- center: [694767.48, 7338176.57],
- zoom: 1,
enableRotation: false,
resolutions: [1024, 512, 256, 128, 64, 32, 16, 8],
constrainResolution: false,
@@ -163,11 +168,16 @@ const CONTAINER_STOPEVENT = MAP.getViewport().getElementsByClassName('ol-overlay
CONTAINER_STOPEVENT.appendChild(document.getElementById('zoom-control'));
CONTAINER_STOPEVENT.appendChild(CONTAINER_MAP);
CONTAINER_STOPEVENT.appendChild(document.getElementById('info-modal'));
+ CONTAINER_STOPEVENT.appendChild(document.getElementById('help-modal'));
const info_backdrop = document.createElement('div');
CONTAINER_STOPEVENT.appendChild(info_backdrop);
info_backdrop.id = 'info-modal-backdrop';
+ const help_backdrop = document.createElement('div');
+ CONTAINER_STOPEVENT.appendChild(help_backdrop);
+ help_backdrop.id = 'help-modal-backdrop';
+
const age_filter = document.createElement('div');
age_filter.id = 'age-filter-modal';
age_filter.classList.add('modal');
@@ -265,6 +275,7 @@ if (window.location === window.parent.location) {
const buttons = Object.fromEntries([
{id: 'layer-selection', title: 'Lagerval', bi: 'stack'},
{id: 'map-legend', title: 'Teckenförklaring', bi: 'list-task'},
+ {id: 'measure', title: 'Mät i kartan', bi: 'rulers'},
{id: 'age-filter', title: 'Filtrera objekt efter ålder', bi: 'clock-history'},
].map(function(x) {
const div = document.createElement('div');
@@ -334,6 +345,7 @@ if (window.location === window.parent.location) {
btn.classList.add('btn', classInactive);
btn.setAttribute('aria-label', btn.title);
MAP.addControl(control);
+ control.element.id = 'fullscreen-toggle'; /* for the help dialog */
control.addEventListener('enterfullscreen', function() {
/* dispose popover as entering fullscreen messes up its position */
@@ -417,84 +429,128 @@ if (window.location === window.parent.location) {
};
}
-/* info button */
+/* info and help buttons */
(function() {
- const div = document.createElement('div');
- MENU.appendChild(div);
- div.id = 'info-button';
- div.classList.add('ol-unselectable', 'ol-control');
+ const add_button = function(x) {
+ const div = document.createElement('div');
+ MENU.appendChild(div);
+ div.id = x.id + '-button';
+ div.classList.add('ol-unselectable', 'ol-control');
- const btn = document.createElement('button');
- div.appendChild(btn);
- btn.type = 'button';
- btn.setAttribute('aria-expanded', 'false');
- btn.title = 'Visa information';
- btn.setAttribute('aria-label', btn.title);
- btn.classList.add('btn', 'btn-light');
+ const btn = document.createElement('button');
+ div.appendChild(btn);
+ btn.type = 'button';
+ btn.setAttribute('aria-expanded', 'false');
+ btn.title = x.title;
+ btn.setAttribute('aria-label', btn.title);
+ btn.classList.add('btn', 'btn-light');
- const i = document.createElement('i');
- btn.appendChild(i);
- i.classList.add('bi', 'bi-info-lg');
+ const i = document.createElement('i');
+ btn.appendChild(i);
+ i.classList.add('bi', 'bi-' + x.bi);
- const panel = document.getElementById('info-modal');
- const modal = new Modal(panel, {
- backdrop: false,
- });
+ const panel = document.getElementById(x.id + '-modal');
+ const modal = new Modal(panel, {
+ backdrop: false,
+ });
- const backdrop = document.getElementById('info-modal-backdrop');
- backdrop.onclick = function() {
- modal.hide();
- };
+ const backdrop = document.getElementById(x.id + '-modal-backdrop');
+ backdrop.onclick = function() {
+ modal.hide();
+ };
- panel.addEventListener('show.bs.modal', function() {
- backdrop.classList.add('modal-backdrop', 'show');
- btn.setAttribute('aria-expanded', 'true');
- btn.classList.replace('btn-light', 'btn-dark');
- });
- panel.addEventListener('hide.bs.modal', function() {
- /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */
- const activeElement = document.activeElement;
- if (activeElement instanceof HTMLElement) {
- activeElement.blur();
- }
- });
- panel.addEventListener('hidden.bs.modal', function() {
- btn.classList.replace('btn-dark', 'btn-light');
- btn.setAttribute('aria-expanded', 'false');
- backdrop.classList.remove('modal-backdrop', 'show');
- infoMetadataAccordions.forEach(function(x, idx) {
- /* collapse all accordions */
- const body = x.element.parentNode.parentNode;
- const name = 'info-accordion-collapse-' + idx;
- if (body.id === name) {
- body.classList.remove('show');
+ panel.addEventListener('show.bs.modal', function() {
+ backdrop.classList.add('modal-backdrop', 'show');
+ btn.setAttribute('aria-expanded', 'true');
+ btn.classList.replace('btn-light', 'btn-dark');
+ });
+ panel.addEventListener('hide.bs.modal', function() {
+ /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */
+ const activeElement = document.activeElement;
+ if (activeElement instanceof HTMLElement) {
+ activeElement.blur();
}
- if (body.parentNode !== null) {
- const headers = body.parentNode.getElementsByClassName('accordion-header');
- for (let i = 0; i < headers.length; i++) {
- const buttons = headers[i].getElementsByClassName('accordion-button');
- for (let j = 0; j < buttons.length; j++) {
- const btn = buttons[j];
- if (btn.getAttribute('data-bs-target') === '#' + name) {
- btn.setAttribute('aria-expanded', 'false');
- btn.classList.add('collapsed');
- }
+ });
+
+ panel.addEventListener('hidden.bs.modal', function() {
+ btn.classList.replace('btn-dark', 'btn-light');
+ btn.setAttribute('aria-expanded', 'false');
+ backdrop.classList.remove('modal-backdrop', 'show');
+ });
+
+ btn.onclick = function() {
+ modal.show();
+ };
+
+ /* de-obfuscate email address */
+ const CLASSNAME = 'email-address-b64';
+ const ATTRNAME = 'data-mailto-b64';
+ for (const a of panel.getElementsByClassName(CLASSNAME)) {
+ if (a.tagName.toLowerCase() === 'a' && a.hasAttribute(ATTRNAME)) {
+ let href = 'mailto:';
+ for (const part of a.getAttribute(ATTRNAME).split(/\s+/)) {
+ switch (part) {
+ case '__AT__':
+ href += '@';
+ break;
+ case '__DOT__':
+ href += '.';
+ break;
+ default:
+ href += atob(part);
}
}
+ a.classList.remove(CLASSNAME);
+ a.removeAttribute(ATTRNAME);
+ a.href = href;
}
+ }
+
+ return [panel, btn, modal];
+ };
+
+ /* info button */
+ (function() {
+ const [panel, btn, modal] = add_button({
+ id: 'info',
+ title: 'Källor och licensinformation',
+ bi: 'info-lg',
});
- });
- btn.onclick = function() {
- infoMetadataAccordions.forEach((x) => x.element.replaceChildren());
- modal.show();
- Promise.allSettled(Object.entries(mapLayers).map(function([grp,lyr]) {
- if (lyr?.getSource() instanceof VectorTile) {
- const url = lyr.getSource().getUrls()[0];
- if (url == null || url.length <= 16 || url.substr(url.length - 16) !== '/{z}/{x}/{y}.pbf') {
- return new Promise(() => { throw new Error(`Invalid URL ${url}`); });
+ panel.addEventListener('hidden.bs.modal', function() {
+ infoMetadataAccordions.forEach(function(x, idx) {
+ /* collapse all accordions */
+ const body = x.element.parentNode.parentNode;
+ const name = 'info-accordion-collapse-' + idx;
+ if (body.id === name) {
+ body.classList.remove('show');
+ }
+ if (body.parentNode !== null) {
+ const headers = body.parentNode.getElementsByClassName('accordion-header');
+ for (let i = 0; i < headers.length; i++) {
+ const buttons = headers[i].getElementsByClassName('accordion-button');
+ for (let j = 0; j < buttons.length; j++) {
+ const btn = buttons[j];
+ if (btn.getAttribute('data-bs-target') === '#' + name) {
+ btn.setAttribute('aria-expanded', 'false');
+ btn.classList.add('collapsed');
+ }
+ }
+ }
+ }
+ });
+ });
+
+ const dateFormatter = new Intl.DateTimeFormat(LOCALE);
+ btn.onclick = function() {
+ infoMetadataAccordions.forEach((x) => x.element.replaceChildren());
+ modal.show();
+ Promise.allSettled(Object.entries(mapLayers).map(function([grp,lyr]) {
+ const baseurl = lyr?.getSource?.()?.get?.('baseurl');
+ if (baseurl == null) {
+ return new Promise(() => { throw new Error(`Unknown source for "${grp}"`); });
}
- return fetch(url.substr(0, url.length - 16) + '/metadata.json')
+ return fetch(new URL('metadata.json', baseurl))
.then(function(resp0) {
if (resp0.status === 200) {
return resp0.json().then((x) => [grp,x]);
@@ -502,153 +558,213 @@ if (window.location === window.parent.location) {
throw new Error(`${resp0.url} [${resp0.status}]`);
}
});
- }
- return new Promise(() => { throw new Error(`Unknown source for "${grp}"`); });
- }))
- .then(function(rs) {
- const metadata = Object.fromEntries(rs.filter(function(r) {
- if (r.status === 'fulfilled') {
- return true;
- } else if (r.status === 'rejected') {
- console.log(r.reason);
- }
- return false;
- }).map((r) => r.value));
-
- infoMetadataAccordions.forEach(function(x) {
- const ul = x.element;
- const groupnames = new Set();
- const last_updated = [];
- x.items.forEach(function([groupname]) {
- const layer_group = metadata[groupname];
- if (layer_group == null) {
- return;
- }
- if (!groupnames.has(groupname)) {
- groupnames.add(groupname);
- if (layer_group.last_updated != null) {
- last_updated.push(layer_group.last_updated);
- }
+ }))
+ .then(function(rs) {
+ const metadata = Object.fromEntries(rs.filter(function(r) {
+ if (r.status === 'fulfilled') {
+ return true;
+ } else if (r.status === 'rejected') {
+ console.log(r.reason);
}
- });
- if (last_updated.length > 0) {
- /* show creation time of the MVT layers */
- const li = document.createElement('li');
- li.classList.add('list-group-item', 'text-muted');
- ul.appendChild(li);
- const i = document.createElement('i');
- i.classList.add('bi', 'bi-map');
- li.appendChild(i);
- const t = document.createTextNode(
- ' Lokalt skikt (vectiler) genererades ' +
- last_updated
- .sort()
- .map((ts) => new Date(ts).toLocaleDateString('sv-SE'))
- .join('; ') + '.'
- );
- li.appendChild(t);
- }
-
- const source_files = new Set();
- x.items.forEach(function([groupname, layername]) {
- /* for each source file associated with the accordion header, show copyright, license and timing information */
- const layer_group = metadata[groupname];
- if (layer_group?.layers == null || layer_group?.source_files == null) {
- return;
- }
- const def = layer_group.layers[layername];
- if (def?.source_files == null) {
- return;
- }
- def.source_files.forEach(function(source_file) {
- if (source_files.has(source_file)) {
+ return false;
+ }).map((r) => r.value));
+
+ infoMetadataAccordions.forEach(function(x) {
+ const ul = x.element;
+ const groupnames = new Set();
+ const last_updated = [];
+ x.items.forEach(function([groupname]) {
+ const layer_group = metadata[groupname];
+ if (layer_group == null) {
return;
}
- const x = layer_group.source_files[source_file];
- source_files.add(source_file);
-
+ if (!groupnames.has(groupname)) {
+ groupnames.add(groupname);
+ if (layer_group.last_updated != null) {
+ last_updated.push(layer_group.last_updated);
+ }
+ }
+ });
+ if (last_updated.length > 0) {
+ /* show creation time of the MVT layers */
const li = document.createElement('li');
- li.classList.add('list-group-item');
+ li.classList.add('list-group-item', 'text-muted');
ul.appendChild(li);
- const h = document.createElement('h6');
- li.appendChild(h);
- if (x.description != null) {
- const t = document.createTextNode(x.description);
- h.appendChild(t);
- }
+ const i = document.createElement('i');
+ i.classList.add('bi', 'bi-map');
+ li.appendChild(i);
+ const t = document.createTextNode(
+ ' Lokalt skikt (vectiler) genererades ' +
+ last_updated
+ .sort()
+ .map((ts) => dateFormatter.format(new Date(ts)))
+ .join('; ') + '.'
+ );
+ li.appendChild(t);
+ }
- if (x.copyright != null) {
- const p = document.createElement('p');
- li.appendChild(p);
- const t = document.createTextNode(x.copyright);
- p.appendChild(t);
+ const source_files = new Set();
+ x.items.forEach(function([groupname, layername]) {
+ /* for each source file associated with the accordion header, show copyright, license and timing information */
+ const layer_group = metadata[groupname];
+ if (layer_group?.layers == null || layer_group?.source_files == null) {
+ return;
+ }
+ const def = layer_group.layers[layername];
+ if (def?.source_files == null) {
+ return;
}
+ def.source_files.forEach(function(source_file) {
+ if (source_files.has(source_file)) {
+ return;
+ }
+ const x = layer_group.source_files[source_file];
+ source_files.add(source_file);
+
+ const li = document.createElement('li');
+ li.classList.add('list-group-item');
+ ul.appendChild(li);
+ const h = document.createElement('h6');
+ li.appendChild(h);
+ if (x.description != null) {
+ const t = document.createTextNode(x.description);
+ h.appendChild(t);
+ }
- if (x.license != null) {
- const p = document.createElement('p');
- li.appendChild(p);
- p.appendChild(document.createTextNode('Licensvillkor: '));
- const t = document.createTextNode(x.license.name);
- if (x.license.url == null) {
+ if (x.copyright != null) {
+ const p = document.createElement('p');
+ li.appendChild(p);
+ const t = document.createTextNode(x.copyright);
p.appendChild(t);
- } else {
+ }
+
+ if (x.license != null) {
+ const p = document.createElement('p');
+ li.appendChild(p);
+ p.appendChild(document.createTextNode('Licensvillkor: '));
+ const t = document.createTextNode(x.license.name);
+ if (x.license.url == null) {
+ p.appendChild(t);
+ } else {
+ const a = document.createElement('a');
+ a.href = x.license.url;
+ a.target = '_blank';
+ a.appendChild(t);
+ p.appendChild(a);
+ }
+ }
+
+ if (x.product_url != null) {
+ const p = document.createElement('p');
+ li.appendChild(p);
+ const t = document.createTextNode('Produktlänk ');
+ const i = document.createElement('i');
+ i.classList.add('bi', 'bi-box-arrow-up-right');
const a = document.createElement('a');
- a.href = x.license.url;
+ a.href = x.product_url;
a.target = '_blank';
a.appendChild(t);
+ a.appendChild(i);
p.appendChild(a);
}
- }
-
- if (x.product_url != null) {
- const p = document.createElement('p');
- li.appendChild(p);
- const t = document.createTextNode('Produktlänk ');
- const i = document.createElement('i');
- i.classList.add('bi', 'bi-box-arrow-up-right');
- const a = document.createElement('a');
- a.href = x.product_url;
- a.target = '_blank';
- a.appendChild(t);
- a.appendChild(i);
- p.appendChild(a);
- }
- if (x.last_modified != null) {
- const p = document.createElement('p');
- p.classList.add('small', 'text-muted');
- li.appendChild(p);
- const i = document.createElement('i');
- i.classList.add('bi', 'bi-file-earmark-code');
- p.appendChild(i);
- p.appendChild(document.createTextNode(' '));
- const t0 = document.createTextNode('Källfil');
- if (x.url == null) {
- p.appendChild(t0);
- } else {
- const a = document.createElement('a');
- p.appendChild(a);
+ if (x.last_modified != null) {
+ const p = document.createElement('p');
+ p.classList.add('small', 'text-muted');
+ li.appendChild(p);
const i = document.createElement('i');
- i.classList.add('bi', 'bi-box-arrow-up-right');
- a.appendChild(t0);
- a.appendChild(document.createTextNode(' '));
- a.appendChild(i);
- a.href = x.url;
- a.target = '_blank';
+ i.classList.add('bi', 'bi-file-earmark-code');
+ p.appendChild(i);
+ p.appendChild(document.createTextNode(' '));
+ const t0 = document.createTextNode('Källfil');
+ if (x.url == null) {
+ p.appendChild(t0);
+ } else {
+ const a = document.createElement('a');
+ p.appendChild(a);
+ const i = document.createElement('i');
+ i.classList.add('bi', 'bi-box-arrow-up-right');
+ a.appendChild(t0);
+ a.appendChild(document.createTextNode(' '));
+ a.appendChild(i);
+ a.href = x.url;
+ a.target = '_blank';
+ }
+ const t1 = document.createTextNode(' ändrades senast ');
+ p.appendChild(t1);
+ const td = document.createTextNode(dateFormatter.format(new Date(x.last_modified)));
+ p.appendChild(td);
+ const t2 = document.createTextNode('.');
+ p.appendChild(t2);
}
- const t1 = document.createTextNode(' ändrades senast ');
- p.appendChild(t1);
- const d = new Date(x.last_modified);
- const td = document.createTextNode(d.toLocaleDateString('sv-SE'));
- p.appendChild(td);
- const t2 = document.createTextNode('.');
- p.appendChild(t2);
- }
+ });
});
});
});
- });
- };
+ };
+ })();
+
+ /* help button */
+ (function() {
+ const [panel] = add_button({
+ id: 'help',
+ title: 'Hjälp med att använda kartan',
+ bi: 'question-circle',
+ });
+
+ /* Use the text from the .html file but ensure that buttons are
+ * listed in the same order as the menu, and spell titles out. This
+ * avoids duplication and avoids that things would get out of sync */
+ const button_map = {};
+ const ol = panel.querySelector('#help-describe-functions');
+ if (ol != null && ol.tagName.toLowerCase() === 'ol') {
+ for (const li of ol.children) {
+ const id = li.getAttribute('data-for-button');
+ if (id == null || id === '') {
+ continue;
+ }
+ button_map[id] = li;
+ }
+ }
+
+ for (const node of MENU.children) {
+ if (node.id == null || node.id === '') {
+ continue
+ }
+ const btn = node.getElementsByTagName('button')[0];
+ if (btn == null || btn.tagName.toLowerCase() !== 'button') {
+ continue;
+ }
+ const btn2 = btn.cloneNode(true);
+ const title = btn2.title;
+ btn2.id = btn2.title = '';
+ for (const attr of btn.attributes) {
+ if (attr.name.toLowerCase().startsWith('aria-') || attr.name === 'id' || attr.name === 'title') {
+ btn2.removeAttribute(attr.name);
+ }
+ }
+
+ const h = document.createElement('h6');
+ h.classList.add('help-button-description');
+ h.appendChild(btn2);
+ if (title != null && title != '') {
+ const t = document.createTextNode(title)
+ h.appendChild(t);
+ }
+ btn2.classList.add('help-button');
+
+ ol.insertAdjacentElement('beforebegin', h);
+
+ const li = button_map[node.id];
+ if (li != null) {
+ /* move <li>'s children (paragraphs) to the main text */
+ while (li.children.length > 0) {
+ ol.insertAdjacentElement('beforebegin', li.firstElementChild);
+ }
+ }
+ }
+ ol.remove();
+ })();
})();
/* we're all set, show the control container now */
@@ -728,6 +844,35 @@ const ageFilterSettings = (function() {
* WebGL, which is currently blocking on https://github.com/openlayers/openlayers/issues/15807
* and https://github.com/openlayers/openlayers/issues/16246 */
const LAYERS = Object.seal({
+ adm: {
+ lansyta: {
+ legend: { zoomLevel: 3, type: 'linestring' },
+ style: [1.5, 2, 3, 3, 4, 4, 6, 6, 8, 8, 10, 10].map(function(width) {
+ return new Style({
+ zIndex: 0,
+ fill: null,
+ stroke: new Stroke({
+ width: width,
+ color: [212, 147, 208, 1],
+ }),
+ });
+ }),
+ },
+ kommunyta: {
+ legend: { zoomLevel: 3, type: 'linestring' },
+ style: [2, 2, 3, 3, 4, 4, 6, 6, 8, 8, 10, 10].map(function(width) {
+ return new Style({
+ zIndex: 0,
+ fill: null,
+ stroke: new Stroke({
+ width: width/2,
+ color: [212, 147, 208, 1],
+ }),
+ });
+ }),
+ },
+ },
+
mrr: {
appr_ec: {
legend: { zoomLevel: 4 },
@@ -1112,7 +1257,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_completed: {
+ turbine_completed: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -1125,7 +1270,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_processed: {
+ turbine_processed: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -1138,7 +1283,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_approved: {
+ turbine_approved: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -1151,7 +1296,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_revoked: {
+ turbine_revoked: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -1164,7 +1309,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_rejected: {
+ turbine_rejected: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -1177,7 +1322,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_dismounted: {
+ turbine_dismounted: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -1190,7 +1335,7 @@ const LAYERS = Object.seal({
});
}),
},
- station_appealed: {
+ turbine_appealed: {
legend: { zoomLevel: 7, type: 'point' },
style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) {
return scale === undefined ? undefined : new Style({
@@ -2635,15 +2780,57 @@ const LAYERS = Object.seal({
const STYLES = Object.seal(Object.fromEntries(Object.entries(LAYERS).map(([k,ls]) =>
[k, Object.seal(Object.fromEntries(Object.keys(ls).map((l) => [l, null])))])));
(function() {
+ const view = MAP.getView();
const params = new URLSearchParams(window.location.hash.substring(1));
- const x = parseFloat(params.get('x'));
- const y = parseFloat(params.get('y'));
+ const x = parseFloat(params.get('x')),
+ y = parseFloat(params.get('y')),
+ z = parseFloat(params.get('z'));
if (!isNaN(x) && !isNaN(y)) {
- MAP.getView().setCenter([x, y]);
- }
- const z = parseFloat(params.get('z'));
- if (!isNaN(z)) {
- MAP.getView().setZoom(z);
+ view.setCenter([x, y]);
+ view.setZoom(isNaN(z) ? 1 : z);
+ } else {
+ /* center of the bbox of the Norrbotten and Västerbotten geometries */
+ view.setCenter([694767.48, 7338176.57]);
+ view.setZoom(1);
+ const geolocation = new Geolocation({
+ projection: view.getProjection(),
+ tracking: true,
+ });
+ const evt_key = geolocation.on('change:position', function() {
+ const pos = geolocation.getPosition();
+ if (pos == null) {
+ return;
+ }
+ /* ignore further geolocation position changes */
+ unByKey(evt_key);
+ geolocation.setTracking(false);
+
+ const params2 = new URLSearchParams(window.location.hash.substring(1));
+ /* ignore geolocation result if coordinates have changed meanwhile */
+ if (params2.has('x') || params2.has('y')) {
+ return;
+ }
+ /* ignore geolocation result if not within extent */
+ if (EXTENT[0] > pos[0] || pos[0] > EXTENT[2] || EXTENT[1] > pos[1] || pos[1] > EXTENT[3]) {
+ return;
+ }
+ view.setCenter(pos);
+ params2.set('x', pos[0].toFixed(2).replace(TRAILING_ZEROES, ''));
+ params2.set('y', pos[1].toFixed(2).replace(TRAILING_ZEROES, ''));
+ if (!params2.has('z')) {
+ const accuracy = geolocation.getAccuracy();
+ if (accuracy == null || accuracy < 0) {
+ view.setZoom(Math.max(view.getMinZoom(), 0));
+ } else {
+ /* infer resolution from accuracy, up to zoom level 7 (8px/m) */
+ const [width, height] = MAP.getSize();
+ const res = 8. * accuracy / Math.min(width, height);
+ view.setResolution(Math.max(res, view.getResolutionForZoom(7)));
+ }
+ params2.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, ''));
+ }
+ location.hash = '#' + params2.toString();
+ });
}
if (!params.has('layers') || (!params.get('layers').match(/^\s*$/) &&
/* compat redirect/layer subst for old non-hierachical names */
@@ -2741,8 +2928,6 @@ MAP.getView().on('change', function(event) {
/* add layers to the map */
const mapLayers = (function() {
- const baseurl = '/';
- const xyz = '/{z}/{x}/{y}.pbf';
const tileGrid = createXYZ({
extent: EXTENT,
tileSize: 1024,
@@ -2759,7 +2944,7 @@ const mapLayers = (function() {
/* Note: layers are added in the order below, so leave SvK and
* misc at the end so they show up on top of suface features */
const rasterLayers = ['kskog'];
- const vectorLayers = ['nv', 'mrr', 'skydd', 'ren', 'ri', 'avverk', 'vbk', 'svk', 'misc'];
+ const vectorLayers = ['adm', 'nv', 'mrr', 'skydd', 'ren', 'ri', 'avverk', 'vbk', 'svk', 'misc'];
const canFilterByAge = ['avverk', 'mrr', 'vbk']; /* layers for which features are dated */
const ret = {};
@@ -2767,19 +2952,21 @@ const mapLayers = (function() {
rasterLayers.forEach((k) => ret[k] = null);
} else {
rasterLayers.forEach(function(k) {
+ const baseurl = new URL('/raster/' + k + '/', window.location.toString()).toString();
+ const source = new GeoTIFF({
+ sources: [{ url: baseurl + encodeURIComponent(k) + '.tiff' }],
+ normalize: false,
+ convertToRGB: false,
+ wrapX: false,
+ interpolate: false,
+ /* use the projection found in the source's metadata */
+ });
+ /* GeoTIFF doesn't allow retrieving the URL later, so we manually store the baseurl instead */
+ source.set('baseurl', baseurl, true);
ret[k] = new TileLayerGL({
/* Naturvårdsverket has a WMS server we could use instead, but by serving it ourselves
* we can filter on he various kskog classes */
- source: new GeoTIFF({
- sources: [{
- url: baseurl + 'raster/' + k + '.tiff',
- }],
- normalize: false,
- convertToRGB: false,
- wrapX: false,
- interpolate: false,
- /* use the projection found in the source's metadata */
- }),
+ source: source,
visible: false,
style: null, /* filled later */
});
@@ -2790,15 +2977,18 @@ const mapLayers = (function() {
vectorLayers.forEach(function(k) {
const canFilterByAge0 = canFilterByAge.includes(k);
const styles = STYLES[k];
+ const baseurl = new URL('/tiles/' + k + '/', window.location.toString()).toString();
+ const source = new VectorTile({
+ url: baseurl + '{z}/{x}/{y}.pbf',
+ format: new MVT(),
+ projection: PROJECTION,
+ wrapX: false,
+ transition: 0,
+ tileGrid: tileGrid,
+ });
+ source.set('baseurl', baseurl, true);
ret[k] = new VectorTileLayer({
- source: new VectorTile({
- url: baseurl + 'tiles/' + k + xyz,
- format: new MVT(),
- projection: PROJECTION,
- wrapX: false,
- transition: 0,
- tileGrid: tileGrid,
- }),
+ source: source,
/* 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',
@@ -2936,31 +3126,31 @@ const layerHierarchy = [
children: [
{
text: 'Uppförda',
- layer: 'vbk.station_completed',
+ layer: 'vbk.turbine_completed',
},
{
text: 'Beviljade',
- layer: 'vbk.station_approved',
+ layer: 'vbk.turbine_approved',
},
{
text: 'Avslagna/nekad',
- layer: 'vbk.station_rejected',
+ layer: 'vbk.turbine_rejected',
},
{
text: 'Handläggs',
- layer: 'vbk.station_processed',
+ layer: 'vbk.turbine_processed',
},
{
text: 'Nedmonterade',
- layer: 'vbk.station_dismounted',
+ layer: 'vbk.turbine_dismounted',
},
{
text: 'Överklagade',
- layer: 'vbk.station_appealed',
+ layer: 'vbk.turbine_appealed',
},
{
text: 'Inte längre aktuella/återkallade',
- layer: 'vbk.station_revoked',
+ layer: 'vbk.turbine_revoked',
},
],
},
@@ -3239,6 +3429,21 @@ const layerHierarchy = [
},
]
},
+ {
+ text: 'Administrativa gränser',
+ type: 'switch',
+ collapse_children: true,
+ children: [
+ {
+ text: 'Länsgränser',
+ layer: 'adm.lansyta',
+ },
+ {
+ text: 'Kommungränser',
+ layer: 'adm.kommunyta',
+ },
+ ],
+ },
];
/* legend panel */
@@ -3259,7 +3464,7 @@ const layerHierarchy = [
const createLegend = function(ul, elem, classes) {
const li = document.createElement('li');
li.classList.add('list-group-item');
- ul.append(li);
+ ul.appendChild(li);
const t = document.createTextNode(elem.text);
if (elem.layer === undefined) {
@@ -3298,8 +3503,11 @@ const layerHierarchy = [
console.log(`Could not find symbol for layer ${layer}, skipping`);
return;
}
- const legend = LAYERS[layerGroup][layerName]?.legend ?? {};
- if (canvas == null || !legend.reuse_canvas) {
+ const legend = LAYERS[layerGroup][layerName]?.legend;
+ if (legend === null) {
+ return; /* layer has opted out from legend */
+ }
+ if (canvas == null || !legend?.reuse_canvas) {
canvas = document.createElement('canvas');
div.appendChild(canvas);
render = toContext(canvas.getContext('2d'),
@@ -3317,9 +3525,9 @@ const layerHierarchy = [
else if (mapLayers[layerGroup].getSource() instanceof VectorTile) {
/* vector source */
const style = Array.isArray(LAYERS[layerGroup][layerName].style) ?
- LAYERS[layerGroup][layerName].style[legend.zoomLevel ?? 5] :
+ LAYERS[layerGroup][layerName].style[legend?.zoomLevel ?? 5] :
LAYERS[layerGroup][layerName].style;
- const legend_type = legend.type ?? 'polygon';
+ const legend_type = legend?.type ?? 'polygon';
if (legend_type === 'point' && style.getImage(1) instanceof Icon && style.getImage(1).getSrc()) {
/* use a new <img> element since .setStyle() returns the same one and doesn't work in that case */
const div2 = document.createElement('div');
@@ -3349,7 +3557,7 @@ const layerHierarchy = [
}
elem._legend = li;
- if (elem.children !== undefined && elem.children.length > 0) {
+ if (elem.children != null && elem.children.length > 0) {
if (classes.length > 0) {
li.classList.add(classes[0]);
classes = classes.slice(1);
@@ -3395,7 +3603,7 @@ const infoMetadataAccordions = [];
elem._layers = elem.layer === undefined ? []
: Array.isArray(elem.layer) ? elem.layer
: [elem.layer];
- if (elem.children !== undefined && elem.children.length > 0) {
+ if (elem.children != null && elem.children.length > 0) {
collectLayers(elem.children);
elem.children.forEach(function(child) {
child._layers.forEach((l) => elem._layers.push(l));
@@ -3425,7 +3633,7 @@ const infoMetadataAccordions = [];
elem._legend.classList.add('d-none');
}
}
- if (elem.children !== undefined && elem.children.length > 0) {
+ if (elem.children != null && elem.children.length > 0) {
setIndeterminateAndChecked(elem.children);
}
});
@@ -3500,7 +3708,7 @@ const infoMetadataAccordions = [];
let layerId = 0;
const addAccordionGroup = function(parentNode, children) {
const ul = document.createElement('ul');
- parentNode.appendChild(ul);
+ parentNode?.appendChild?.(ul);
ul.classList.add('list-group', 'list-group-flush');
children.forEach(function(child) {
@@ -3526,7 +3734,7 @@ const infoMetadataAccordions = [];
const textNode = document.createTextNode(child.text);
label.appendChild(textNode);
- if (child.children !== undefined && child.children.length > 0) {
+ if (child.children != null && child.children.length > 0) {
addAccordionGroup(li, child.children);
}
@@ -3581,8 +3789,18 @@ const infoMetadataAccordions = [];
const text0 = document.createTextNode(x.text);
label0.appendChild(text0);
- if (x.children === undefined || x.children.length === 0) {
- item.replaceChild(span0, header);
+ if (x.children == null || x.children.length === 0 || x.collapse_children) {
+ span0.removeAttribute('data-bs-toggle');
+ span0.removeAttribute('data-bs-target');
+ item.replaceChildren(span0);
+ if (x.type === 'switch') {
+ span0.classList.add('form-switch');
+ input0.setAttribute('role', 'switch');
+ }
+ if (x.children != null && x.children.length > 0) {
+ /* create inputs for the hash param logic but don't add them to the panel */
+ addAccordionGroup(null, x.children);
+ }
} else {
const body = document.createElement('div');
collapse.appendChild(body);
@@ -3674,6 +3892,344 @@ const infoMetadataAccordions = [];
})();
})();
+/* measurement panel */
+(function() {
+ const value = document.createTextNode(''),
+ unit = document.createTextNode('');
+ const reset = function() {
+ source.clear(true);
+ const f = { LineString: formatLength, Polygon: formatArea }[getMeasureMode()];
+ if (f == null) {
+ value.nodeValue = unit.nodeValue = '';
+ } else {
+ f(0);
+ }
+ };
+
+ const source = new VectorSource({
+ wrapX: false,
+ });
+
+ let draw;
+ const buttons = Object.fromEntries([
+ {
+ id: 'cancel',
+ bi: 'trash',
+ title: 'Avbryt mätningen',
+ onclick: function() {
+ reset();
+ Object.values(buttons).forEach((btn) => btn.disabled = true);
+ draw.abortDrawing();
+ },
+ },
+ {
+ id: 'undo',
+ bi: 'arrow-counterclockwise',
+ title: 'Ta bort sista punkten',
+ onclick: function() {
+ draw.removeLastPoint();
+ const n = { LineString: 2, Polygon: 3 }[getMeasureMode()] ?? Infinity;
+ draw.getOverlay().getSource().forEachFeature(function(feature) {
+ const geom = feature.getGeometry();
+ if (geom.getType() === 'LineString') {
+ /* disable OK button if not enough points have been drawn (excluding cursor) */
+ buttons.ok.disabled = geom.getCoordinates().length - 1 < n;
+ return true; /* stop iterating */
+ }
+ });
+ }
+ },
+ {
+ id: 'ok',
+ bi: 'check-lg',
+ title: 'Slutför mätningen',
+ onclick: () => draw.finishDrawing(),
+ },
+ ].map(function(x, idx, arr) {
+ const btn = document.createElement('button');
+ btn.classList.add('btn', 'btn-outline-' + (idx < arr.length-1 ? 'secondary' : 'primary'));
+ btn.setAttribute('type', 'button');
+ btn.title = x.title;
+ btn.setAttribute('aria-label', btn.title);
+
+ const i = document.createElement('i');
+ i.classList.add('bi', 'bi-' + x.bi);
+ btn.appendChild(i);
+
+ btn.onclick = x.onclick;
+ return [x.id, btn];
+ }));
+
+ const formatLength = (function() {
+ const formatters = [
+ { maximumFractionDigits: 0 },
+ { maximumFractionDigits: 1 },
+ { maximumFractionDigits: 2 },
+ ]
+ .map((fmt) => new Intl.NumberFormat(LOCALE, {
+ ...fmt,
+ minimumFractionDigits: fmt.maximumFractionDigits ?? 0,
+ }));
+ return function(v) {
+ if (v <= 100) { /* ≤ 100 m */
+ unit.nodeValue = 'm';
+ value.nodeValue = formatters[1].format(v);
+ } else if (v <= 5_000) { /* ≤ 5 km */
+ unit.nodeValue = 'm';
+ value.nodeValue = formatters[0].format(v);
+ } else {
+ unit.nodeValue = 'km';
+ value.nodeValue = formatters[2].format(v/1000);
+ }
+ };
+ })();
+ const formatArea = (function() {
+ const formatters = [
+ { maximumFractionDigits: 1 },
+ { maximumFractionDigits: 2 },
+ ]
+ /* XXX would be nice to use Intl.NumberFormat()'s unit support, but m² and km² are not
+ * supported currently, see https://github.com/tc39/ecma402/issues/767 */
+ .map((fmt) => new Intl.NumberFormat(LOCALE, {
+ ...fmt,
+ minimumFractionDigits: fmt.maximumFractionDigits ?? 0,
+ }));
+ return function(v) {
+ if (v < 10_000) { /* < 1 ha */
+ unit.nodeValue = 'm²';
+ value.nodeValue = formatters[0].format(v);
+ } else if (v < 100_000_000) { /* < 10000 ha (100 km²) */
+ unit.nodeValue = 'ha';
+ value.nodeValue = formatters[1].format(v/10_000);
+ } else {
+ unit.nodeValue = 'km²';
+ v /= 1_000_000;
+ const i = v < 1_000_000 ? 1 : 0; /* ≥10⁶ km² overflows the box with 2 decimals */
+ value.nodeValue = formatters[i].format(v);
+ }
+ };
+ })();
+
+ const setup = (function() {
+ const styles = {
+ Point: new Style({
+ image: new CircleStyle({
+ radius: 6,
+ fill: new Fill({
+ color: [0, 183, 255, 1],
+ }),
+ stroke: new Stroke({
+ color: [255, 255, 255, 1],
+ width: .5,
+ }),
+ }),
+ }),
+ LineString: [
+ new Style({
+ stroke: new Stroke({
+ color: [255, 255, 255, 1],
+ width: 4,
+ }),
+ }),
+ new Style({
+ stroke: new Stroke({
+ color: [0, 183, 255, 1],
+ width: 3,
+ lineDash: [10, 10],
+ }),
+ }),
+ ],
+ Polygon: new Style({
+ fill: new Fill({
+ color: [255, 255, 255, .5],
+ }),
+ }),
+ };
+ const layer = new VectorLayer({
+ source: source,
+ visible: false,
+ style: [
+ new Style({
+ fill: styles.Polygon.getFill(),
+ stroke: styles.LineString[0].getStroke(),
+ }),
+ (function() {
+ const s = styles.LineString[1].clone();
+ s?.getStroke?.()?.setLineDash?.(null);
+ return s;
+ })(),
+ ],
+ map: MAP,
+ });
+
+ return function(geom_type) {
+ if (draw != null) {
+ draw.abortDrawing();
+ MAP.removeInteraction(draw);
+ }
+ reset(); /* remove features when toggling between geom types */
+ Object.values(buttons).forEach((btn) => btn.disabled = true);
+ if (geom_type == null) {
+ layer.setVisible(false);
+ return;
+ }
+ draw = new Draw({
+ source: source,
+ type: geom_type,
+ style: function(feature) {
+ return styles[ feature.getGeometry().getType() ];
+ },
+ });
+ MAP.addInteraction(draw);
+ layer.setVisible(true);
+
+ let listener;
+ draw.on('drawstart', function(event) {
+ reset();
+ buttons.undo.disabled = buttons.cancel.disabled = false;
+ const geom = event.feature.getGeometry();
+ const [isComplete, measure] = {
+ LineString: [
+ /* 2 points drawn + cursor */
+ (g) => g.getCoordinates().length >= 3,
+ (g) => formatLength(g.getLength()),
+ ],
+ Polygon: [
+ /* 3 points drawn + cursor + 1st point */
+ (g) => g.getCoordinates()[0].length >= 5,
+ (g) => formatArea(g.getArea()),
+ ],
+ }[geom.getType()];
+ const btnOK = buttons.ok;
+ listener = geom.on('change', function(event) {
+ if (btnOK.disabled && isComplete(geom)) {
+ btnOK.disabled = false;
+ }
+ measure(event.target);
+ });
+ });
+ draw.on('drawend', function() {
+ unByKey(listener);
+ buttons.ok.disabled = buttons.undo.disabled = true;
+ });
+ draw.on('drawabort', function() {
+ unByKey(listener);
+ reset();
+ Object.values(buttons).forEach((btn) => btn.disabled = true);
+ });
+ };
+ })();
+
+ const [body, btn_close] = (function() {
+ const modal = document.getElementById('measure-panel');
+ modal.classList.add('modal');
+ modal.setAttribute('role', 'dialog');
+ modal.setAttribute('aria-hidden', 'true');
+
+ const content = document.createElement('div');
+ modal.appendChild(content);
+ content.classList.add('modal-content');
+
+ const header = document.createElement('div');
+ content.appendChild(header);
+ header.classList.add('modal-header');
+
+ const title = document.createElement('div');
+ title.classList.add('h5');
+ title.innerHTML = 'Mät i kartan';
+ header.appendChild(title);
+
+ const btn_close = document.createElement('button');
+ btn_close.classList.add('btn-close');
+ btn_close.type = 'button';
+ btn_close.title = 'Stäng';
+ btn_close.setAttribute('aria-label', btn_close.title);
+ header.appendChild(btn_close);
+
+ const body = document.createElement('div');
+ content.appendChild(body);
+ body.classList.add('modal-body');
+ return [body, btn_close];
+ })();
+
+ const getMeasureMode = (function() {
+ const btn_group = document.createElement('div');
+ btn_group.classList.add('btn-group');
+ btn_group.setAttribute('role', 'group');
+ btn_group.setAttribute('aria-label', 'Välj geometrityp');
+ body.appendChild(btn_group);
+
+ const radios = [
+ { id: 'LineString', text: 'Distans' },
+ { id: 'Polygon', text: 'Yta' },
+ ].map(function(x, idx) {
+ const radio = document.createElement('input');
+ radio.classList.add('btn-check');
+ radio.type = 'radio';
+ radio.checked = idx == 0;
+ radio.name = 'measure-geomtype';
+ radio.id = 'measure-geomtype-' + x.id;
+ radio.setAttribute('aria-expanded', 'false');
+ radio.setAttribute('autocomplete', 'off');
+ btn_group.appendChild(radio);
+
+ const lbl = document.createElement('label');
+ lbl.classList.add('btn', 'btn-lg', 'btn-outline-dark');
+ lbl.setAttribute('for', radio.id);
+ lbl.appendChild(document.createTextNode(x.text));
+ btn_group.appendChild(lbl);
+
+ radio.onclick = function() {
+ setup(x.id);
+ };
+ return [x.id, radio];
+ });
+
+ return () => radios.filter( (x) => x[1].checked )?.[0]?.[0];
+ })();
+
+ (function() {
+ const div = document.createElement('div');
+ div.classList.add('measure-value');
+ body.appendChild(div);
+ const span0 = document.createElement('span');
+ span0.appendChild(value);
+ div.appendChild(span0);
+ const span1 = document.createElement('span');
+ span1.classList.add('measure-unit');
+ span1.appendChild(unit);
+ div.appendChild(span1);
+ })();
+
+ (function() {
+ const btn_group = document.createElement('div');
+ btn_group.classList.add('btn-group');
+ btn_group.setAttribute('role', 'group');
+ body.appendChild(btn_group);
+
+ btn_group.appendChild(buttons.cancel);
+ btn_group.appendChild(buttons.undo);
+ btn_group.appendChild(buttons.ok);
+ })();
+
+ const button_menu = document.getElementById('measure-button')
+ .getElementsByTagName('button')[0];
+ button_menu.addEventListener('click', function(event) {
+ if (event.currentTarget.getAttribute('aria-expanded') === 'true') {
+ disposePopover();
+ setup(getMeasureMode());
+ } else {
+ setup(null);
+ /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */
+ const activeElement = document.activeElement;
+ if (activeElement.isEqualNode(btn_close)) {
+ activeElement.blur();
+ }
+ }
+ });
+ btn_close.onclick = () => button_menu.click();
+})();
+
/* popup and feature overlays */
const disposePopover = (function() {
/* return an <a> tag with the given URL and optional text */
@@ -3980,7 +4536,7 @@ const disposePopover = (function() {
['appealed', /* 5 */ 'överklagat'],
['revoked', /* 6 */ 'inte aktuell eller återkallad'],
]
- .forEach(([k, title], idx) => layers.vbk['station_' + k] = {
+ .forEach(([k, title], idx) => layers.vbk['turbine_' + k] = {
title: 'Landbaserad vindkraftverk \u2013 ' + title,
fields: mapFields(idx, fieldMap, [
'VerkID',
@@ -4227,6 +4783,16 @@ const disposePopover = (function() {
fields: mapFields(fieldMap, [ 'NAMN', 'geom_area' ]),
};
+ layers.skydd.biosfarsomraden = {
+ title: 'Biosfärsområden (UNESCO)',
+ fields: [
+ { key: 'SKYDDSTYP', desc: 'Skyddstyp' },
+ { key: 'NAMN', desc: 'Namn' },
+ { key: 'LINK', desc: 'Länk', fn: formatLink },
+ fieldMap.geom_area,
+ ],
+ };
+
layers.skydd.naturvardsavtal = {
title: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)',
fields: [
@@ -4545,31 +5111,34 @@ const disposePopover = (function() {
};
/* format value to HTML */
+ const numberFormatters = [
+ { },
+ { maximumFractionDigits: 1 },
+ { maximumFractionDigits: 2 },
+ ]
+ /* XXX would be nice to use Intl.NumberFormat()'s unit support, but m² and km² are not
+ * supported currently, see https://github.com/tc39/ecma402/issues/767 */
+ .map((fmt) => new Intl.NumberFormat(LOCALE, fmt));
+ const unitSeparator = '\u00A0'; /* U+00A0 NO-BREAK SPACE */
const formatValue = function(value, options) {
- let unit = options?.unit;
if (options?.fn == null) {
/* no-op */
} else if (typeof options.fn === 'function') {
value = options.fn(value);
- } else if (options.fn === 'length' && typeof value === 'number' && unit == null) {
- if (value < 1000) {
- unit = 'm';
+ } else if (options.fn === 'length' && typeof value === 'number' && options?.unit == null) {
+ if (value <= 5_000) { /* ≤ 5 km */
+ value = numberFormatters[1].format(value) + unitSeparator + 'm';
} else {
- value /= 1000;
- value = Math.round(value*100) / 100;
- unit = 'km';
+ value = numberFormatters[2].format(value/1000) + unitSeparator + 'km';
}
- } else if (options.fn === 'area' && typeof value === 'number' && unit == null) {
- if (value < 10000) {
- unit = 'm²';
- } else if (value < 10000 * 10000) {
- value /= 10000;
- unit = 'ha';
+ } else if (options.fn === 'area' && typeof value === 'number' && options?.unit == null) {
+ if (value < 10_000) { /* < 1 ha */
+ value = numberFormatters[1].format(value) + unitSeparator + 'm²';
+ } else if (value < 100_000_000) { /* < 10000 ha (100 km²) */
+ value = numberFormatters[2].format(value/10_000) + unitSeparator + 'ha';
} else {
- value /= 1000000;
- unit = 'km²';
+ value = numberFormatters[2].format(value/1_000_000) + unitSeparator + 'km²';
}
- value = Math.round(value*100) / 100;
}
if (value == null) {
return null;
@@ -4583,8 +5152,9 @@ const disposePopover = (function() {
case 'string':
return document.createTextNode(value);
case 'number':
- if (unit != null) {
- return document.createTextNode(value.toLocaleString('sv-SE') + '\u202F' + unit);
+ if (options?.unit != null) {
+ return document.createTextNode(numberFormatters[0].format(value)
+ + unitSeparator + options.unit);
}
return document.createTextNode(value.toString());
default:
@@ -4851,7 +5421,13 @@ const disposePopover = (function() {
header.appendChild(btnExpand);
header.appendChild(btnClose);
+ const measureButton = document.getElementById('measure-button')
+ .getElementsByTagName('button')[0];
MAP.on('singleclick', function(event) {
+ if (measureButton.getAttribute('aria-expanded') === 'true') {
+ /* skip popover while measuring */
+ return;
+ }
disposeFeatureOverlay();
/* dispose any pre-existing popover if not in detached mode */
@@ -4860,8 +5436,7 @@ const disposePopover = (function() {
popover.dispose();
}
- const size = event.map.getSize();
- if (size[0] < 576 || size[1] < 576) {
+ if (window.innerWidth < 200) {
return; /* skip popover if the map is too small */
}
@@ -4883,7 +5458,7 @@ const disposePopover = (function() {
const layerGroup = layer.get('layerGroup');
const layerName = feature.getProperties().layer;
mapSources[layerGroup] ??= layer.getSource();
- const def = layerName != null ? layers[layerGroup][layerName] : null;
+ const def = layerName != null ? layers[layerGroup]?.[layerName] : null;
if (def?.fields == null) {
/* skip layers which didn't opt-in for popover */
return false;
@@ -5505,13 +6080,11 @@ const ageFilterSetActive = (function() {
.forEach((lyr) => lyr.changed());
};
const setter = function(active) {
- if (ageFilterSettings._active !== active) {
- if (active) {
- ageFilterSettings.setupMinMax();
- }
- ageFilterSettings._active = active;
- changed();
+ if (active) {
+ ageFilterSettings.setupMinMax();
}
+ ageFilterSettings._active = active;
+ changed();
if (active && ageFilterSettings.type === 'relative') {
if (timeoutID == null) {
timeoutID = setTimeout(fun, getDelay(state)[0]);