/*********************************************************************** * Copyright © 2024-2025 Guilhem Moulin * * 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 * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . **********************************************************************/ import Map from 'ol/Map.js'; import View from 'ol/View.js'; import TileLayer from 'ol/layer/Tile.js'; import TileLayerGL from 'ol/layer/WebGLTile.js'; import WMTS from 'ol/source/WMTS.js'; import GeoTIFF from 'ol/source/GeoTIFF.js'; import WMTSTileGrid from 'ol/tilegrid/WMTS.js'; import FullScreen from 'ol/control/FullScreen.js'; 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'; import { createXYZ } from 'ol/tilegrid.js'; import { toContext } from 'ol/render.js'; import Polygon from 'ol/geom/Polygon.js'; import LineString from 'ol/geom/LineString.js'; import Point from 'ol/geom/Point.js'; import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; import Icon from 'ol/style/Icon.js'; import RegularShape from 'ol/style/RegularShape.js'; import Stroke from 'ol/style/Stroke.js'; import Style from 'ol/style/Style.js'; import proj4 from 'proj4'; import { get as getProjection } from 'ol/proj.js'; import { register as registerProjection } from 'ol/proj/proj4.js'; import { Modal, Popover } from 'bootstrap'; import './style.css'; proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'); registerProjection(proj4); const PROJECTION = getProjection('EPSG:3006'); /* 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 * the resolution at level 0 is 4096m per pixel (each side is 1048.576km long). * * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning_cache_v1.1.0.pdf * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf * * We set the extent to a 4×4 tiles square at level 2 (1024px = 1048.576km per * side) somehow centered on Norrbotten and Västerbotten, and zoom in from there. * This represent a TILEROW (x) offset of 5, and a TILECOL (y) offset of 2. */ const EXTENT = [110720, 6927136, 1159296, 7975712]; /* XXX using the topowebbcache WMTS is fine for testing (as it doesn't require * authentication) but not in production in a public instance as doing so would * violate its current terms of use (as of January 2024 it's not CC0 open data). * See * * https://www.lantmateriet.se/sv/om-lantmateriet/Rattsinformation/upphovsratt-och-publicering-av-lantmateriets-geografiska-information/ * https://www.lantmateriet.se/sv/kartor/vara-karttjanster/min-karta/#anchor-2 * https://help.locusmap.eu/topic/support-for-swedish-lantmateriets-min-karta-wms * * More precise background maps might be available in the future as open data, * though: * * https://www.lantmateriet.se/sv/om-lantmateriet/press/nyheter/lantmateriets-arbete-mot-oppna-data-i-full-gang/ * https://ext-geodatakatalog.lansstyrelsen.se/GeodataKatalogen/srv/swe/catalog.search#/map uses * https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/3c3a9cf47e7cb5ea24542d40d19698/?layer=topowebb&style=default&tilematrixset=3006&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=7&TileCol=237&TileRow=155 */ const [BASEMAP, MAP] = (function() { const param = 'basemap'; const baseMap = Object.seal({ _layer: new URLSearchParams(location.hash.substring(1))?.get?.(param) ?? 'topowebb_nedtonad', get layer() { return this._layer; }, get url() { return 'https://minkarta.lantmateriet.se/map/topowebbcache?' + 'LAYER=' + encodeURIComponent(this.layer); }, set layer(layername) { this._layer = layername; baseMapSource.setUrl(this.url); const searchParams = new URLSearchParams(location.hash.substring(1)); searchParams.set(param, layername); location.hash = '#' + searchParams.toString(); }, }); const baseMapSource = new WMTS({ url: baseMap.url, version: '1.0.0', style: 'default', matrixSet: '3006', format: 'image/png', tileGrid: new WMTSTileGrid({ extent: EXTENT, // https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf tileSize: 256, origin: [-1200000, 8500000], resolutions: [4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8], matrixIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], }), projection: PROJECTION, wrapX: false, crossOrigin: 'anonymous', }); const view = new View({ 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, }); return [ baseMap, new Map({ controls: [], view: view, layers: [ new TileLayer({ source: baseMapSource }), ], target: document.getElementById('map'), }), ]; })(); /* move the control container to the viewport */ const CONTAINER_MAP = document.getElementById('map-control-container'); const CONTAINER_STOPEVENT = MAP.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; (function() { CONTAINER_STOPEVENT.appendChild(document.getElementById('zoom-control')); CONTAINER_STOPEVENT.appendChild(CONTAINER_MAP); CONTAINER_STOPEVENT.appendChild(document.getElementById('info-modal')); const info_backdrop = document.createElement('div'); CONTAINER_STOPEVENT.appendChild(info_backdrop); info_backdrop.id = 'info-modal-backdrop'; const age_filter = document.createElement('div'); age_filter.id = 'age-filter-modal'; age_filter.classList.add('modal'); age_filter.setAttribute('tabindex', '-1'); age_filter.setAttribute('aria-hidden', 'true'); CONTAINER_STOPEVENT.appendChild(age_filter); const age_filter_backdrop = document.createElement('div'); age_filter_backdrop.id = 'age-filter-modal-backdrop'; CONTAINER_STOPEVENT.appendChild(age_filter_backdrop); })(); /* zoom in/out */ (function() { const zoomInLabel = document.createElement('i'); zoomInLabel.classList.add('bi', 'bi-plus'); const zoomOutLabel = document.createElement('i'); zoomOutLabel.classList.add('bi', 'bi-dash'); const control = new Zoom({ zoomInTipLabel: 'Zooma in', zoomInLabel: zoomInLabel, zoomOutTipLabel: 'Zooma ut', zoomOutLabel: zoomOutLabel, target: document.getElementById('zoom-control'), }); control.element.classList.add('btn-group-vertical'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } MAP.addControl(control); })(); /* zoom slider */ (function() { const control = new ZoomSlider({ target: document.getElementById('zoom-control'), }); control.element.classList.add('modal'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } MAP.addControl(control); })(); /* scale line */ (function() { const size = MAP.getSize(); const control = new ScaleLine({ units: 'metric', minWidth: 150, maxWidth: size[1] < 350 ? size[1] - 50 : 350, target: CONTAINER_MAP, }); control.element.classList.add('modal', 'modal-content'); MAP.addControl(control); })(); const MENU = document.getElementById('map-menu'); const TRAILING_ZEROES = /\.?0*$/; /* "open in new tab" button */ if (window.location !== window.parent.location) { const div = document.createElement('div'); MENU.appendChild(div); div.classList.add('ol-unselectable', 'ol-control'); const btn = document.createElement('button'); div.appendChild(btn); btn.type = 'button'; btn.title = 'Öppna karta i ny flik'; btn.setAttribute('aria-label', btn.title); btn.setAttribute('aria-expanded', 'false'); btn.classList.add('btn', 'btn-light'); const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-box-arrow-up-right'); btn.onclick = function() { const coordinates = MAP.getView().getCenter(); const url = new URL(window.location.href); const searchParams = new URLSearchParams(url.hash.substring(1)); searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, '')); searchParams.set('y', coordinates[1].toFixed(2).replace(TRAILING_ZEROES, '')); searchParams.set('z', MAP.getView().getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); url.hash = '#' + searchParams.toString(); return window.open(url.href, '_blank'); }; } /* layer selection button and legend */ 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: 'age-filter', title: 'Filtrera objekt efter ålder', bi: 'clock-history'}, ].map(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.title = x.title; btn.setAttribute('aria-label', btn.title); btn.setAttribute('aria-expanded', 'false'); btn.classList.add('btn', 'btn-light'); const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-' + x.bi); return [x.id, btn] })); Object.entries(buttons).forEach(function([id, btn]) { const panel = document.getElementById(id + '-panel'); if (panel != null) { btn.onclick = function() { if (btn.getAttribute('aria-expanded') === 'true') { panel.setAttribute('aria-hidden', 'true'); btn.setAttribute('aria-expanded', 'false'); btn.classList.replace('btn-dark', 'btn-light'); } else { Object.values(buttons).forEach(function(btn2) { /* close all other panels */ if (!btn.isEqualNode(btn2) && btn2.getAttribute('aria-expanded') === 'true') { btn2.click(); } }); panel.setAttribute('aria-hidden', 'false'); btn.setAttribute('aria-expanded', 'true'); btn.classList.replace('btn-light', 'btn-dark'); } }; } }); } /* fullscreen control */ if (window.location === window.parent.location) { const label = document.createElement('i'); label.classList.add('bi', 'bi-fullscreen'); const labelActive = document.createElement('i'); labelActive.classList.add('bi', 'bi-fullscreen-exit'); const titleInactive = 'Helskärmsläge'; const titleActive = 'Lämna helskärmsläge'; const classInactive = 'btn-light'; const classActive = 'btn-dark'; const control = new FullScreen({ label: label, labelActive: labelActive, tipLabel: titleInactive, keys: true, target: MENU, }); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.add('btn', classInactive); btn.setAttribute('aria-label', btn.title); MAP.addControl(control); control.addEventListener('enterfullscreen', function() { /* dispose popover as entering fullscreen messes up its position */ disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classInactive, classActive); btn.title = titleActive; btn.setAttribute('aria-label', btn.title); const exp = document.getElementById('export-to-image'); if (exp !== undefined) { /* hide export button in fullscreen mode as it exits it */ exp.classList.add('d-none'); } }); control.addEventListener('leavefullscreen', function() { /* dispose popover as is might overflow the viewport */ disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classActive, classInactive); btn.title = titleInactive; btn.setAttribute('aria-label', btn.title); const exp = document.getElementById('export-to-image'); if (exp !== undefined) { exp.classList.remove('d-none'); } }); } /* export/download button */ if (window.location === window.parent.location) { const div = document.createElement('div'); div.classList.add('ol-unselectable', 'ol-control'); div.id = 'export-to-image'; const btn = document.createElement('button'); div.appendChild(btn); btn.classList.add('btn', 'btn-light'); btn.type = 'button'; btn.title = 'Ladda ner som en PNG-fil'; btn.setAttribute('aria-label', btn.title); const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-download'); MENU.appendChild(div); btn.onclick = function() { MAP.once('rendercomplete', function() { const canvas0 = document.createElement('canvas'); const size = MAP.getSize(); canvas0.width = size[0]; canvas0.height = size[1]; const context = canvas0.getContext('2d'); MAP.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer').forEach(function(canvas) { if (canvas.width > 0) { const opacity = canvas.parentNode.style.opacity || canvas.style.opacity; context.globalAlpha = opacity === '' ? 1 : Number(opacity); context.drawImage(canvas, 0, 0); } }); context.globalAlpha = 1; context.setTransform(1, 0, 0, 1, 0, 0); canvas0.toBlob(function(blob) { const a = document.createElement('a'); a.download = 'karta.png'; a.rel = 'noopener'; a.href = URL.createObjectURL(blob); setTimeout(function() { URL.revokeObjectURL(a.href) }, 4E4); // 40s setTimeout(function() { a.click() }, 0); }); }); MAP.renderSync(); }; } /* info button */ (function() { const div = document.createElement('div'); MENU.appendChild(div); div.id = 'info-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 i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-info-lg'); const panel = document.getElementById('info-modal'); const modal = new Modal(panel, { backdrop: false, }); const backdrop = document.getElementById('info-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'); } 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'); } } } } }); }); 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}`); }); } return fetch(url.substr(0, url.length - 16) + '/metadata.json') .then(function(resp0) { if (resp0.status === 200) { return resp0.json().then((x) => [grp,x]); } else { 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); } } }); 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; } 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.copyright != null) { const p = document.createElement('p'); li.appendChild(p); const t = document.createTextNode(x.copyright); p.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) { 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.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); 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 d = new Date(x.last_modified); const td = document.createTextNode(d.toLocaleDateString('sv-SE')); p.appendChild(td); const t2 = document.createTextNode('.'); p.appendChild(t2); } }); }); }); }); }; })(); /* we're all set, show the control container now */ CONTAINER_MAP.setAttribute('aria-hidden', 'false'); /* age filter settings state */ const ageFilterSettings = (function() { const dateToTS = function(d) { if (d != null) { /* number of days since 1970-01-01; take both dates at 00:00:00.0 UTC */ return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())/86_400_000); } }; return Object.seal({ _active: false, get active() { return this._active; }, set active(b) { ageFilterSetActive(b); }, type: 'relative', operator: '<=', quantity: 1, unit: 'y', from: null, to: null, show_unknown: false, _min_ts: null, _max_ts: null, getRelativeDate: function(quantity, unit) { if (quantity == null || isNaN(quantity) || unit == null) { return null; } /* use today noon localtime to avoid issues due to DST when substracting dates */ const d = new Date(); d.setHours(12, 0, 0, 0); switch (unit) { case 'd': d.setDate(d.getDate() - quantity); break; case 'w': d.setDate(d.getDate() - 7 * quantity); break; case 'm': d.setMonth(d.getMonth() - quantity); break; case 'y': d.setFullYear(d.getFullYear() - quantity); break; default: return null; } return d; }, setupMinMax: function() { this._min_ts = this._max_ts = null; switch (this.type) { case 'relative': { const date = this.getRelativeDate(this.quantity, this.unit); const prop = {'<=':'_min_ts', '>=':'_max_ts'}[this.operator]; this[prop] = dateToTS(date); break; } case 'interval': { this._min_ts = dateToTS(this.from); this._max_ts = dateToTS(this.to); break; } } }, }); })(); /* Layer style definitions */ /* TODO: this should really be refactored… */ const LAYERS = { 'mrr.appr_ec': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 22, fill: new Fill({ color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [151, 173, 23, 1], }), }); }), }, 'mrr.appl_ec': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 25, fill: new Fill({ color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [197, 14, 31, 1], lineDash: width >= 1.5 ? [2 * width] : undefined, }), }); }), }, 'mrr.appr_met': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 24, fill: new Fill({ color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [151, 173, 23, 1], }), }); }), }, 'mrr.appl_met': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 26, fill: new Fill({ color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [197, 14, 31, 1], lineDash: width >= 1.5 ? [2 * width] : undefined, }), }); }), }, 'mrr.appr_ogd': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 24, fill: new Fill({ color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [151, 173, 23, 1], }), }); }), }, 'mrr.appl_ogd': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 26, fill: new Fill({ color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [197, 14, 31, 1], lineDash: width >= 1.5 ? [2 * width] : undefined, }), }); }), }, 'mrr.appr_dl': { legend: { zoomLevel: 4 }, style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 20, fill: new Fill({ color: [228, 53, 45, Math.max((.2-1)/6 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [151, 173, 23, 1], }), }); }), }, 'svk.ledningar': { legend: { zoomLevel: 5, type: 'linestring', reuse_canvas: true }, style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { return new Style({ zIndex: 52, stroke: new Stroke({ color: 'black', width: width, }), }); }), }, 'svk.stolpar': { legend: { zoomLevel: 5, type: 'point' }, style: [undefined, undefined, undefined, undefined, undefined] .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { return new Style({ zIndex: 51, image: new CircleStyle({ radius: radius, fill: new Fill({ color: 'black', }), }), }); })), }, 'svk.transmissionsnatsprojekt': { legend: { zoomLevel: 5, type: 'linestring' }, style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { return new Style({ zIndex: 53, stroke: new Stroke({ color: 'black', width: width, lineDash: [4 * width], }), }); }), }, 'svk.stationer': { legend: { zoomLevel: 3, type: 'point' }, style: [3, 4, 5, 6, 7, 8.5, 10].map(function(radius) { return new Style({ zIndex: 50, image: new RegularShape({ radius: radius, points: 4, angle: Math.PI/4, fill: new Fill({ color: 'black', }), }), }); }) .concat([.5, 1, 1.5, 2, 2].map(function(width) { return new Style({ zIndex: 50, fill: new Fill({ color: 'rgba(128, 128, 128, .7)', }), stroke: new Stroke({ width: width, color: 'rgb(0, 0, 0)', }), }); })), }, 'vbk.area_current': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 10, fill: new Fill({ color: [168, 198, 223, Math.max((.2-1)/8 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [56, 96, 130, 1], }), }); }), }, 'vbk.area_notcurrent': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 10, fill: new Fill({ color: [222, 163, 199, Math.max((.2-1)/8 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [148, 55, 112, 1], lineDash: width >= 1.5 ? [2 * width] : undefined, }), }); }), }, 'vbk.offshore_completed': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 17, fill: new Fill({ color: [38, 107, 29, .5], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [38, 107, 29, 1], }), }); }), }, 'vbk.offshore_approved': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 16, fill: new Fill({ color: [56, 160, 44, .5], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [56, 160, 44, 1], }), }); }), }, 'vbk.offshore_amended': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; patternCanvas.width = width/2; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(247, 105, 162, 1)'; patternContext.beginPath(); patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, 1.5*w, 0, 2*Math.PI, true); patternContext.fill(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 17, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: 2*w, color: [247, 105, 162, 1], lineDash: [8 * w], }), }); }), }, 'vbk.offshore_rejected': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 11, fill: new Fill({ color: [227, 26, 28, .5], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [227, 26, 28, 1], }), }); }), }, 'vbk.offshore_appealed': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 15, fill: new Fill({ color: [177, 88, 40, .5], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [177, 88, 40, 1], }), }); }), }, 'vbk.offshore_applied': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 14, fill: new Fill({ color: [255, 127, 0, .5], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [255, 128, 0, 1], }), }); }), }, 'vbk.offshore_consultation': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 13, fill: new Fill({ color: [254, 217, 118, .65], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [254, 183, 82, 1], }), }); }), }, 'vbk.offshore_investigation': { legend: { zoomLevel: 1 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; patternCanvas.width = width*2; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(68, 90, 166, 1)'; patternContext.lineWidth = w; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 12, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: 2*w, color: [68, 90, 166, 1], lineDash: [8 * w], }), }); }), }, 'vbk.offshore_revoked': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 10, fill: new Fill({ color: [105, 61, 154, .5], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [105, 62, 153, 1], }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-completed.svg', declutter: 'none', scale: scale, }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-processed.svg', declutter: 'none', scale: scale, }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-approved.svg', declutter: 'none', scale: scale, }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-revoked.svg', declutter: 'none', scale: scale, }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-rejected.svg', declutter: 'none', scale: scale, }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-dismounted.svg', declutter: 'none', scale: scale, }), }); }), }, 'vbk.station_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({ zIndex: 99, image: new Icon({ src: '/assets/icons/wind-turbine-appealed.svg', declutter: 'none', scale: scale, }), }); }), }, /* Documentation at * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf * */ 'avverk.utford': { legend: { zoomLevel: 7 }, style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { return new Style({ zIndex: 10, fill: new Fill({ color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [204, 0, 0, 1], }), }); }), }, /* Documentation at * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf * */ 'avverk.anmald': { legend: { zoomLevel: 7 }, style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { return new Style({ zIndex: 10, fill: (width === undefined || width === 0) ? new Fill({ color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)*.75] }) : (function() { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); const slope = 45 * Math.PI/180; const spacing = z < 10 ? z*2 : 40; const len = Math.hypot(1, slope); const w = patternCanvas.width = Math.round(1/len + spacing); const h = patternCanvas.height = Math.round(slope/len + spacing * slope); patternContext.fillStyle = 'rgba(255, 102, 102, .1)'; patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); patternContext.strokeStyle = 'rgba(204, 0, 0, 1)'; patternContext.lineWidth = Math.max(1, width/2); patternContext.beginPath(); patternContext.moveTo(0, h); patternContext.lineTo(w, 0); patternContext.moveTo(-w, h); patternContext.lineTo(w, -h); patternContext.moveTo(0, 2*h); patternContext.lineTo(2*w, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Fill({ color: context.createPattern(patternCanvas, 'repeat') }); })(), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [204, 0, 0, 1], lineDash: width >= 1.5 ? [2 * width] : undefined, }), }); }), }, 'skydd.tilltradesforbud': { legend: { zoomLevel: 2 }, style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { return new Style({ zIndex: 23, fill: new Fill({ /* transparent fill so clicking the inside of the polygon triggers a popover */ /* XXX could also use a custom renderer but that doesn't seem to work */ color: [0, 0, 0, 0], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [255, 0, 0, 1], }), }); }), }, 'skydd.nationalpark': { legend: { zoomLevel: 1 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(0, 55, 0, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 22, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [0, 55, 0, 1], }), }); }), }, 'skydd.naturreservat': { legend: { zoomLevel: 1 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 21, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [7, 181, 7, 1], }), }); }), }, 'skydd.naturreservat_kommunalt': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 20, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [7, 181, 7, 1], }), }); }), }, 'skydd.naturvardsomrade': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(176, 255, 176, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 19, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [176, 255, 176, 1], }), }); }), }, 'skydd.djur_och_vaxtskyddsomrade': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(255, 255, 0, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 18, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [255, 255, 0, 1], }), }); }), }, 'skydd.kulturreservat': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(154, 102, 255, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 17, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [154, 102, 255, 1], }), }); }), }, 'skydd.vattenskyddsomrade': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(0, 105, 212, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 16, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [0, 105, 212, 1], }), }); }), }, 'skydd.landskapsbildsskyddsomrade': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(135, 110, 71, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 15, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [134, 110, 71, 1], }), }); }), }, 'skydd.skogligt_biotopskyddsomrade': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(135, 90, 71, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 14, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 4 ? .5 : z <= 5 ? 1 : 2, color: [134, 90, 71, 1], }), }); }), }, 'skydd.ovrigt_biotopskyddsomrade': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(255, 95, 0, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 13, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 4 ? .5 : z <= 5 ? 1 : 2, color: [255, 95, 0, 1], }), }); }), }, 'skydd.naturminne_yta': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(113, 0, 116, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 12, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [134, 0, 116, 1], }), }); }), }, 'skydd.naturminne_punkt': { legend: { zoomLevel: 6, type: 'point' }, style: [undefined, undefined, undefined, undefined].concat([3, 4, 6, 8, 12, 16, 20, 24].map(function(width) { return new Style({ zIndex: 12, image: new CircleStyle({ radius: width, fill: new Fill({ color: 'rgba(113, 0, 116, .5)', }), stroke: new Stroke({ width: Math.log2(width)/2, color: 'rgba(113, 0, 116, 1)', }), }), }); })) }, 'skydd.interimistiskt_forbud': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(168, 0, 0, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 11, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [168, 0, 0, 1], }), }); }), }, 'skydd.fageldirektivet': { legend: { zoomLevel: 1 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width*2; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(230, 0, 0, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); patternContext.beginPath(); patternContext.lineWidth *= 6; patternContext.moveTo(-.5*patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -.5*patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 1.5*patternCanvas.height); patternContext.lineTo(1.5*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 10, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 4 ? .5 : z <= 5 ? 1 : 2, color: [230, 0, 0, 1], }), }); }), }, 'skydd.habitatdirektivet': { legend: { zoomLevel: 1 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width*2; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(0, 77, 168, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); patternContext.beginPath(); patternContext.lineWidth *= 6; patternContext.moveTo(0, -.5*patternCanvas.height); patternContext.lineTo(1.5*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-.5*patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 1.5*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 10, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 4 ? .5 : z <= 5 ? 1 : 2, color: [0, 77, 168, 1], }), }); }), }, 'skydd.helcom': { legend: { zoomLevel: 1 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(130, 130, 130, 1)'; const r = z < 5 ? (z+1)*.75 : z*.5; patternContext.beginPath(); patternContext.arc(.5*patternCanvas.width, .5*patternCanvas.height, r, 0, 2*Math.PI, true); patternContext.fill(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 9, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [130, 130, 130, 1], }), }); }), }, 'skydd.ramsar': { legend: { zoomLevel: 1 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(195, 0, 255, 1)'; const r = z < 5 ? (z+1)*.75 : z*.5; patternContext.beginPath(); patternContext.arc(.25*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true); patternContext.fill(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 9, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [195, 0, 255, 1], }), }); }), }, 'skydd.ospar': { legend: { zoomLevel: 1 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; const r = z < 5 ? (z+1)*.75 : z*.5; patternContext.beginPath(); patternContext.arc(.25*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true); patternContext.fill(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 9, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [168, 0, 0, 1], }), }); }), }, 'skydd.varldsarv': { legend: { zoomLevel: 1 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; const r = z < 5 ? (z+1)*.75 : z*.5; patternContext.beginPath(); patternContext.arc(.75*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true); patternContext.fill(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 9, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [168, 0, 0, 1], }), }); }), }, 'skydd.biosfarsomraden': { legend: { zoomLevel: 1 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(131, 0, 219, 1)'; const r = z < 5 ? (z+1)*.75 : z*.5; patternContext.beginPath(); patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true); patternContext.fill(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 9, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [131, 0, 219, 1], }), }); }), }, 'skydd.naturvardsavtal': { legend: { zoomLevel: 1 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 21, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 4 ? .5 : z <= 5 ? 1 : 2, color: [255, 0, 197, 1], }), }); }), }, 'skydd.naturvardsavtal_skogsstyrelsen': { legend: { zoomLevel: 2 }, style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 20, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 4 ? .5 : z <= 5 ? 1 : 2, color: [255, 0, 197, 1], }), }); }), }, 'skydd.atervatningsavtal': { legend: { zoomLevel: 0 }, style: [0, 1, 2, 3, 4, 5, 6].map(function(width) { return new Style({ zIndex: 5, fill: new Fill({ color: [255, 115, 0, .4], }), stroke: width === 0 ? undefined : new Stroke({ width: .5, color: [255, 115, 0, 1], }), }); }) .concat([7, 8, 9, 10, 11].map(function() { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = 16; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(255, 115, 0, 1)'; patternContext.lineWidth = 1; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 5, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: new Stroke({ width: 1.5, color: [255, 115, 0, 1], }), }); })), }, 'nv.naturvarde_sks': { legend: { zoomLevel: 0 }, style: [0, 1, 2, 3, 4, 5].map(function(width) { return new Style({ zIndex: 6, fill: new Fill({ color: [255, 170, 0, .2], }), stroke: width === 0 ? undefined : new Stroke({ width: .5, color: [255, 170, 0, .8], }), }); }) .concat([6, 7, 8, 9, 10, 11].map(function() { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = 16; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(255, 170, 0, 1)'; patternContext.lineWidth = 1; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 6, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: new Stroke({ width: 1.5, color: [255, 170, 0, 1], }), }); })), }, 'nv.nyckelbiotop': { legend: { zoomLevel: 0 }, style: [0, 1, 2, 3, 4, 5].map(function(width) { return new Style({ zIndex: 6, fill: new Fill({ color: [217, 148, 9, .2], }), stroke: width === 0 ? undefined : new Stroke({ width: .5, color: [217, 148, 9, .8], }), }); }) .concat([6, 7, 8, 9, 10, 11].map(function() { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = 16; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; patternContext.lineWidth = 1; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 6, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: new Stroke({ width: 1.5, color: [217, 148, 9, 1], }), }); })), }, 'nv.nyckelbiotop_storskogsbruk': { legend: { zoomLevel: 0 }, style: [0, 1, 2, 3, 4, 5].map(function(width) { return new Style({ zIndex: 6, fill: new Fill({ color: [217, 148, 9, .2], }), stroke: width === 0 ? undefined : new Stroke({ width: .5, color: [217, 148, 9, .8], }), }); }) .concat([6, 7, 8, 9, 10, 11].map(function() { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = 16; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; patternContext.lineWidth = 1; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 6, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: new Stroke({ width: 1.5, color: [217, 148, 9, 1], }), }); })), }, 'nv.sumpskog': { legend: { zoomLevel: 5 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); const w = Math.max(1, width); patternCanvas.width = z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 6 : 8; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(158, 200, 215, 1)'; patternContext.lineWidth = w; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 5, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: w/2, color: [158, 200, 215, 1], }), }); }), }, 'nv.pagaende_naturreservatsbildning': { legend: { zoomLevel: 1 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.setLineDash([width/4, width/4]); patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(width/4, 0); patternContext.lineTo(width/4, patternCanvas.height); patternContext.stroke(); patternContext.beginPath(); patternContext.lineDashOffset = width/4; patternContext.moveTo(3*width/4, 0); patternContext.lineTo(3*width/4, patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 10, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 3 : 4, color: [7, 181, 7, 1], lineDash: [width/8, width/4], }), }); }), }, 'nv.snus': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { return new Style({ zIndex: 4, fill: new Fill({ color: [168,168,0,.2], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [168,77,0,.75], }), }); }), }, 'ri.naturvard': { legend: { zoomLevel: 0 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(154, 230, 0, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 8, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [154, 230, 0, 1], }), }); }), }, 'ri.friluftsliv': { legend: { zoomLevel: 0 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(0, 127, 232, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 8, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, color: [0, 127, 232, 1], }), }); }), }, 'ri.rorligt_friluftsliv': { legend: { zoomLevel: 0 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(187, 227, 212, .25)'; patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); patternContext.strokeStyle = 'rgba(56, 151, 117, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, patternCanvas.height); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, patternCanvas.height); patternContext.lineTo(patternCanvas.width, -patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, 2*patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 8, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, color: [56, 151, 117, 1], lineDash: [width/4, width/3], }), }); }), }, 'ri.obruten_kust': { legend: { zoomLevel: 0 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(227, 227, 187, .25)'; patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); patternContext.strokeStyle = 'rgba(156, 158, 56, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 8, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, color: [156, 158, 56, 1], lineDash: [width/4, width/3], }), }); }), }, 'ri.obrutet_fjall': { legend: { zoomLevel: 0 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = width; patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'rgba(255, 255, 209, .25)'; patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); patternContext.strokeStyle = 'rgba(219, 183, 60, 1)'; patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(0, -patternCanvas.height); patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); patternContext.stroke(); patternContext.moveTo(-patternCanvas.width, 0); patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 8, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, color: [219, 183, 60, 1], lineDash: [width/4, width/3], }), }); }), }, 'ri.skyddade_vattendrag': { legend: { zoomLevel: 0 }, style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { return new Style({ zIndex: 8, fill: new Fill({ color: [102, 157, 240, .25], }), stroke: width === 0 ? undefined : new Stroke({ width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, color: [41, 109, 197, 1], lineDash: [width/4, width/3], }), }); }), }, 'ren.betesomrade': { legend: { zoomLevel: 0 }, style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { return new Style({ zIndex: 4, fill: new Fill({ /* transparent fill so clicking the inside of the polygon triggers a popover */ /* XXX could also use a custom renderer but that doesn't seem to work */ color: [0, 0, 0, 0], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [179, 153, 102, 1], }), }); }), }, 'ren.flyttled': { legend: { zoomLevel: 2, type: 'linestring' }, style: [.75, 1, 1.5, 2, 3, 4, 5, 5, 6, 7, 8, 10].map(function(width) { return new Style({ zIndex: 7, stroke: new Stroke({ width: 2*width, color: [119, 99, 59, 1], lineDash: [4 * width], }), }); }), }, 'ren.riks_ren': { legend: { zoomLevel: 1 }, style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { const patternCanvas = document.createElement('canvas'); const patternContext = patternCanvas.getContext('2d'); patternCanvas.width = z < 4 ? 4 : z <= 5 ? 8 : Math.pow(2, Math.round(Math.log2(width) + 3)); patternCanvas.height = patternCanvas.width; patternContext.fillStyle = 'transparent'; patternContext.strokeStyle = 'rgba(179, 153, 102, 1)'; patternContext.lineWidth = Math.max(1, width/2); patternContext.beginPath(); patternContext.moveTo(0, 0); patternContext.lineTo(patternCanvas.width, 0); patternContext.stroke(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); return new Style({ zIndex: 6, fill: new Fill({ color: context.createPattern(patternCanvas, 'repeat'), }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [179, 153, 102, 1], }), }); }), }, 'ren.omr_riks': { legend: { zoomLevel: 2 }, style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { return new Style({ zIndex: 5, fill: new Fill({ color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [179, 153, 102, 1], }), }); }), }, /* Documentation at * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf * */ 'misc.dammar': { legend: { zoomLevel: 5, type: 'point' }, style: [2, 3, 4, 4, 4, 6, 8, 8, 8, 10, 16, 32].map(function(width) { return new Style({ zIndex: 59, image: new CircleStyle({ radius: width, fill: new Fill({ color: 'rgb(219, 30, 42)', }), stroke: new Stroke({ width: Math.log2(width) * 2/5, color: 'rgb(128, 17, 25)', }), }), }); }), }, 'misc.gigafactories': { legend: { zoomLevel: 1, type: 'point' }, style: [4, 6, 7, 8, 10, 12].map(function(width) { return new Style({ zIndex: 60, image: new CircleStyle({ radius: width, fill: new Fill({ color: 'rgb(152, 78, 163)', }), stroke: new Stroke({ width: Math.log2(width) * 2/5, color: 'rgb(119, 61, 128)', }), }), }); }) .concat([1.5, 2, 2, 2, 2, 2].map(function(width) { return new Style({ zIndex: 58, fill: new Fill({ color: 'rgba(152, 78, 163, .4)', }), stroke: new Stroke({ width: width, color: 'rgb(119, 61, 128)', }), }); })), }, 'kskog.1' : { style: [ 56, 168, 0, .2] }, /* #1 Sannolikt kontinuitetsskog (preciserad) */ 'kskog.2' : { style: [169, 0, 230, .2] }, /* #2 Sannolikt påverkad kontinuitetsskog (preciserad) */ 'kskog.3' : { style: [152, 230, 0, .2] }, /* #3 Sannolikt kontinuitetsskog i fjällen (grövre precisering) */ 'kskog.4' : { style: [ 76, 115, 0, .2] }, /* #4 Potentiell kontinuitetsskog (2015) */ }; /* process URL parameters (other than 'basemap') */ const STYLES = {}; (function() { const params = new URLSearchParams(window.location.hash.substring(1)); const x = parseFloat(params.get('x')); const y = parseFloat(params.get('y')); if (!isNaN(x) && !isNaN(y)) { MAP.getView().setCenter([x, y]); } const z = parseFloat(params.get('z')); if (!isNaN(z)) { MAP.getView().setZoom(z); } if (!params.has('layers') || (!params.get('layers').match(/^\s*$/) && /* compat redirect/layer subst for old non-hierachical names */ !params.get('layers').split(' ').some((l) => l.includes('.')))) { params.set('layers', [ 'svk.ledningar', 'svk.stolpar', 'svk.stationer', 'svk.transmissionsnatsprojekt', 'misc.gigafactories', 'misc.dammar', 'mrr.appr_ec', 'mrr.appl_ec', 'mrr.appr_ogd', 'mrr.appl_ogd', 'mrr.appr_met', 'mrr.appl_met', 'mrr.appr_dl', 'vbk.area_current', 'vbk.area_notcurrent', ].join(' ')); location.hash = '#' + params.toString(); } /* map each known parameter to a callback processing its value */ Object.entries({ 'layers': function(value) { const layersParams = value.split(' '); Object.entries(LAYERS) .filter(([key]) => layersParams.includes(key)) .forEach(([key, lyr]) => STYLES[key] = lyr.style); }, 'age-filter': function(value) { /* eslint-disable-next-line no-useless-escape */ const m0 = /^([ +\-]?)([0-9]+)([dwmy])$/.exec(value); if (m0 != null) { /* handling relative parameter values add some complexity, but it's worth doing since * users can bookmark the URL with e.g., age-filter=-1y and visit it later with the * timeframe shifted forward, thus not having to reconfigure the age filter */ ageFilterSettings.type = 'relative'; ageFilterSettings.operator = (m0[1] === ' ' || m0[1] === '+' || m0[1] === '') ? '>=' : m0[1] === '-' ? '<=' : null; ageFilterSettings.quantity = parseInt(m0[2], 10); ageFilterSettings.unit = m0[3]; ageFilterSettings.setupMinMax(); ageFilterSettings._active = true; /* don't call the setter as it's not initialized yet */ return; } const m1 = /^([0-9]{8})-([0-9]{8})$/.exec(value); /* YYYYMMDD */ if (m1 != null) { const parse_date = (m) => new Date( parseInt(m.slice(0,4), 10), parseInt(m.slice(4,6), 10)-1, parseInt(m.slice(6,8), 10), 12 /* use 12:00:00.0 like for the */ ); ageFilterSettings.type = 'interval'; ageFilterSettings.from = parse_date(m1[1]); ageFilterSettings.to = parse_date(m1[2]); ageFilterSettings.setupMinMax(); ageFilterSettings._active = true; /* don't call the setter as it's not initialized yet */ return; } //console.log(`Ignoring invalid value for 'age-filter' parameter: ${value}`); }, 'show-unknown-age': function(value) { if (value === '0') { ageFilterSettings.show_unknown = false; } else if (value === '1') { ageFilterSettings.show_unknown = true; } }, }) .forEach(function([param, cb]) { if (params.has(param)) { cb(params.get(param)); } }); })(); MAP.getView().on('change', function(event) { const view = event.target; disposePopover(); const coordinates = view.getCenter(); const searchParams = new URLSearchParams(location.hash.substring(1)); searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, '')); searchParams.set('y', coordinates[1].toFixed(2).replace(TRAILING_ZEROES, '')); searchParams.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); location.hash = '#' + searchParams.toString(); }); /* add layers to the map */ const mapLayers = (function() { const baseurl = '/'; const xyz = '/{z}/{x}/{y}.pbf'; const tileGrid = createXYZ({ extent: EXTENT, tileSize: 1024, maxResolution: 1024, /* = 1048576/1024 */ minZoom: 0, maxZoom: 7, }); const isVisible = function(groupname) { return Object.keys(LAYERS).some((layername) => layername.startsWith(groupname + '.') && STYLES[layername] != null); }; const canWebGL2 = !!document.createElement('canvas').getContext('webgl2'); /* 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 canFilterByAge = ['avverk', 'mrr', 'vbk']; /* layers for which features are dated */ const ret = {}; if (!canWebGL2) { rasterLayers.forEach((k) => ret[k] = null); } else { rasterLayers.forEach(function(k) { 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 */ }), visible: false, style: null, /* filled later */ }); MAP.addLayer(ret[k]); }); } vectorLayers.forEach(function(k) { const canFilterByAge0 = canFilterByAge.includes(k); ret[k] = new VectorTileLayer({ source: new VectorTile({ url: baseurl + '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: isVisible(k), style: function(feature, resolution) { /* WARN: very hot code path! */ const properties = feature.getProperties(); if (ageFilterSettings.active) { /* TODO avoid doing this checks for each feature; instead, set up a * different style function if ageFilterSettings.active */ const ts = properties.ts; if (ts == null) { if (canFilterByAge0 && !ageFilterSettings.show_unknown) { return null; } } else if ((ageFilterSettings._min_ts !== null && ts < ageFilterSettings._min_ts) || (ageFilterSettings._max_ts !== null && ts > ageFilterSettings._max_ts)) { return null; } } const style = STYLES[k + '.' + properties.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]; } } }); ret[k].set('layerGroup', k, true); ret[k].set('canFilterByAge', canFilterByAge0, true); MAP.addLayer(ret[k]); }); return ret; })(); /* layer hierarchy, for the layer selection, legend and info modal */ const layerHierarchy = [ { text: 'Transmissionsnät för el', children: [ { text: 'Kraftledningar (befintliga)', layer: ['svk.stolpar', 'svk.ledningar'], }, { text: 'Stationer', layer: 'svk.stationer', }, { text: 'Transmissionsnätsprojekt ', layer: 'svk.transmissionsnatsprojekt', }, ], }, { text: 'Stora industrisatsningar', layer: 'misc.gigafactories', }, { text: 'Dammar', layer: 'misc.dammar', }, { text: 'Mineralrättigheter', children: [ { text: 'Bearbetningskoncessioner', children: [ { text: 'Beviljad', layer: 'mrr.appr_ec', }, { text: 'Ansökt', layer: 'mrr.appl_ec', }, ], }, { text: 'Undersökningstillstånd, olja, gas och diamant', children: [ { text: 'Beviljad', layer: 'mrr.appr_ogd', }, { text: 'Ansökt', layer: 'mrr.appl_ogd', }, ], }, { text: 'Undersökningstillstånd, metaller och industrimineral', children: [ { text: 'Beviljad', layer: 'mrr.appr_met', }, { text: 'Ansökt', layer: 'mrr.appl_met', }, ], }, { text: 'Markanvisningar till koncession', layer: 'mrr.appr_dl', }, ], }, { text: 'Vindbruk', children: [ { text: 'Landbaserade projekteringsområden', children: [ { text: 'Aktuella', layer: 'vbk.area_current', }, { text: 'Ej aktuella', layer: 'vbk.area_notcurrent', }, ], }, { text: 'Landbaserade vindkraftverk', children: [ { text: 'Uppförda', layer: 'vbk.station_completed', }, { text: 'Beviljade', layer: 'vbk.station_approved', }, { text: 'Avslagna/nekad', layer: 'vbk.station_rejected', }, { text: 'Handläggs', layer: 'vbk.station_processed', }, { text: 'Nedmonterade', layer: 'vbk.station_dismounted', }, { text: 'Överklagade', layer: 'vbk.station_appealed', }, { text: 'Inte längre aktuella/återkallade', layer: 'vbk.station_revoked', }, ], }, { text: 'Havsbaserad vindkraft', children: [ { text: 'Uppförd', layer: 'vbk.offshore_completed', }, { text: 'Tillståndsansökan beviljad', layer: 'vbk.offshore_approved', }, { text: 'Ändringsansökan', layer: 'vbk.offshore_amended', }, { text: 'Tillståndsansökan avslagen', layer: 'vbk.offshore_rejected', }, { text: 'Överklagad', layer: 'vbk.offshore_appealed', }, { text: 'Tillståndsansökan inlämnad', layer: 'vbk.offshore_applied', }, { text: 'Samråd inför tillståndsansökan', layer: 'vbk.offshore_consultation', }, { text: 'Inledande undersökningar', layer: 'vbk.offshore_investigation', }, { text: 'Inte längre aktuell/återkallad', layer: 'vbk.offshore_revoked', }, ], }, ] }, { text: 'Skogsbruk', children: [ { text: 'Utförda avverkningar', layer: 'avverk.utford', }, { text: 'Avverkningsanmälningar', layer: 'avverk.anmald', }, ] }, { text: 'Skyddad natur', children: [ { text: 'Nationella skyddsformer från Naturvårdsregistret', children: [ { text: 'Tillträdesförbud', layer: 'skydd.tilltradesforbud', }, { text: 'Nationalpark', layer: 'skydd.nationalpark', }, { text: 'Naturreservat', layer: 'skydd.naturreservat', }, { text: 'Kommunala naturreservat', layer: 'skydd.naturreservat_kommunalt', }, { text: 'Naturvårdsområden', layer: 'skydd.naturvardsomrade', }, { text: 'Djur- och växtskyddsområden', layer: 'skydd.djur_och_vaxtskyddsomrade', }, { text: 'Kulturreservat', layer: 'skydd.kulturreservat', }, { text: 'Vattenskyddsområden', layer: 'skydd.vattenskyddsomrade', }, { text: 'Landskapsbildsskyddsområden', layer: 'skydd.landskapsbildsskyddsomrade', }, { text: 'Skogliga biotopskyddsområden', layer: 'skydd.skogligt_biotopskyddsomrade', }, { text: 'Övriga biotopskyddsområden', layer: 'skydd.ovrigt_biotopskyddsomrade', }, { text: 'Naturminne', layer: [ 'skydd.naturminne_yta', 'skydd.naturminne_punkt' ], }, { text: 'Interimistiskt förbud', layer: 'skydd.interimistiskt_forbud', }, ], }, { text: 'Natura 2000-områden', children: [ { text: 'Fågeldirektivet (SPA)', layer: 'skydd.fageldirektivet', }, { text: 'Art- och habitatdirektivet (SCI)', layer: 'skydd.habitatdirektivet', }, ], }, { text: 'Områden med internationell status', children: [ { text: 'Marina skyddade områden (Helcom MPA)', layer: 'skydd.helcom', }, { text: 'Ramsar-områden (Våtmarkskonventionen)', layer: 'skydd.ramsar', }, { text: 'Marina skyddade områden (Ospar MPA)', layer: 'skydd.ospar', }, { text: 'Världsarv med mycket höga naturvärden (UNESCO)', layer: 'skydd.varldsarv', }, { text: 'Biosfärsområden (UNESCO)', layer: 'skydd.biosfarsomraden', }, ], }, { text: 'Naturvårdsavtal', children: [ { text: 'Naturvårdsverket, Länsstyrelserna', layer: 'skydd.naturvardsavtal', }, { text: 'Skogsstyrelsen', layer: 'skydd.naturvardsavtal_skogsstyrelsen', } ] }, { text: 'Återvätningsavtal', layer: 'skydd.atervatningsavtal', } ] }, { text: 'Skogliga värden', children: [ { text: 'Objekt med naturvärden (Skogsstyrelsen)', layer: 'nv.naturvarde_sks', }, { text: 'Nyckelbiotoper', layer: 'nv.nyckelbiotop', }, { text: 'Nyckelbiotoper storskogsbruket', layer: 'nv.nyckelbiotop_storskogsbruk', }, { text: 'Sumpskogar', layer: 'nv.sumpskog', }, { text: 'Pågående naturreservatsbildning', layer: 'nv.pagaende_naturreservatsbildning', }, { text: 'Skyddsvärda statliga skogar', layer: 'nv.snus', }, { text: 'Sannolikt och potentiell kontinuitetsskog', children: [ { text: 'Sannolikt kontinuitetsskog (preciserad)', layer: 'kskog.1', }, { text: 'Sannolikt påverkad kontinuitetsskog (preciserad)', layer: 'kskog.2', }, { text: 'Sannolikt kontinuitetsskog i fjällen (grövre precisering)', layer: 'kskog.3', }, { text: 'Potentiell kontinuitetsskog (2015)', layer: 'kskog.4', }, ], } ] }, { text: 'Riksintressen, naturvård och friluftsliv', children: [ { text: 'Naturvård', layer: 'ri.naturvard', }, { text: 'Friluftsliv', layer: 'ri.friluftsliv', }, { text: 'Rörligt friluftsliv', layer: 'ri.rorligt_friluftsliv', }, { text: 'Obruten kust', layer: 'ri.obruten_kust', }, { text: 'Obrutet fjäll', layer: 'ri.obrutet_fjall', }, { text: 'Skyddade vattendrag', layer: 'ri.skyddade_vattendrag', }, ] }, { /* Definitions at * https://ext-dokument.lansstyrelsen.se/Gemensamt/Geodata/Datadistribution/Information,%20Skiktf%C3%B6rteckning%20och%20f%C3%B6rklaringar.pdf */ text: 'Rennäringen', children: [ { text: 'Betesområden', layer: 'ren.betesomrade', }, { text: 'Flyttled', layer: 'ren.flyttled', }, { text: 'Riksintressen', layer: 'ren.riks_ren', }, { text: '(Kärn)områden av riksintresse', layer: 'ren.omr_riks', }, ] }, ]; /* legend panel */ (function() { const modal = document.getElementById('map-legend-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 body = document.createElement('div'); content.appendChild(body); body.classList.add('modal-body'); const createLegend = function(ul, elem, classes) { const li = document.createElement('li'); li.classList.add('list-group-item'); ul.append(li); const t = document.createTextNode(elem.text); if (elem.layer === undefined) { li.appendChild(t); li.classList.add('map-legend-header'); } else { li.classList.add('d-flex', 'flex-row'); const div = document.createElement('div'); div.classList.add('map-legend-symbol'); li.appendChild(div); const parent_height = 24; /* see CSS */ const [width, height] = [32, 20]; const symbols = { polygon: new Polygon([[ [0, (parent_height-height)/2], [width, (parent_height-height)/2], [width, (parent_height+height)/2], [0, (parent_height+height)/2], [0, (parent_height-height)/2], ]]), linestring: new LineString([ [0, parent_height/2], [width, parent_height/2] ]), point: new Point( [width/2, parent_height/2] ), }; let canvas, render; (Array.isArray(elem.layer) ? elem.layer : [elem.layer]) .forEach(function(layer) { /* add symbols for each layer */ const layerGroup = layer.split('.', 1)[0]; if (!layerGroup || !mapLayers[layerGroup] || LAYERS[layer]?.style == null) { console.log(`Could not find symbol for layer ${layer}, skipping`); return; } const legend = LAYERS[layer]?.legend ?? {}; if (canvas == null || !legend.reuse_canvas) { canvas = document.createElement('canvas'); div.appendChild(canvas); render = toContext(canvas.getContext('2d'), { size:[width, parent_height] } ); } if (mapLayers[layerGroup].getSource() instanceof GeoTIFF) { /* raster source */ render.setFillStrokeStyle(new Fill({ color: LAYERS[layer].style, })); return render.drawGeometry(symbols.polygon); } else if (mapLayers[layerGroup].getSource() instanceof VectorTile) { /* vector source */ const style = Array.isArray(LAYERS[layer].style) ? LAYERS[layer].style[legend.zoomLevel ?? 5] : LAYERS[layer].style; const legend_type = legend.type ?? 'polygon'; if (legend_type === 'point' && style.getImage(1) instanceof Icon && style.getImage(1).getSrc()) { /* use a new element since .setStyle() returns the same one and doesn't work in that case */ const div2 = document.createElement('div'); const img = document.createElement('img'); div2.appendChild(img); img.src = style.getImage(1).getSrc(); img.height = height; div.replaceChild(div2, canvas); } else { const style2 = style.clone(); const stroke = style2.getStroke(); if (legend_type === 'polygon' && stroke && stroke.getWidth() > 1) { /* don't stroke too wide to avoid spilling over boxes above/below */ stroke.setWidth(1); } render.setStyle(style2); return render.drawGeometry(symbols[legend_type]); } } else { throw new Error(`Cannot show legend for ${layer}`); } }); const span = document.createElement('div'); span.appendChild(t); li.appendChild(span); } elem._legend = li; if (elem.children !== undefined && elem.children.length > 0) { if (classes.length > 0) { li.classList.add(classes[0]); classes = classes.slice(1); } const ul2 = document.createElement('ul'); ul2.classList.add('list-group', 'list-group-flush'); ul.appendChild(ul2); elem.children.forEach((elem2) => createLegend(ul2, elem2, classes)); } }; const ul = document.createElement('ul'); ul.classList.add('list-group', 'list-group-flush'); body.appendChild(ul); layerHierarchy.forEach(function(x) { createLegend(ul, x, ['h4', 'h5', 'h6']); }); })(); /* layer selection panel */ const infoMetadataAccordions = []; (function() { const modal = document.getElementById('layer-selection-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 body = document.createElement('div'); content.appendChild(body); body.classList.add('modal-body'); const accordion = document.createElement('div'); body.appendChild(accordion); accordion.id = 'layer-selection-accordion'; accordion.classList.add('accordion', 'accordion-flush'); (function collectLayers(list) { list.forEach(function(elem) { elem._layers = elem.layer === undefined ? [] : Array.isArray(elem.layer) ? elem.layer : [elem.layer]; if (elem.children !== undefined && elem.children.length > 0) { collectLayers(elem.children); elem.children.forEach(function(child) { child._layers.forEach((l) => elem._layers.push(l)); }); } }); })(layerHierarchy); const setIndeterminateAndChecked = function(list) { return list.forEach(function(elem) { const layerStyles = elem._layers.map((lyr) => STYLES[lyr] !== undefined); elem._input.indeterminate = elem._layers.length <= 1 ? false : layerStyles.slice(1).some((v) => v !== layerStyles[0]); if (elem._input.indeterminate) { elem._legend.classList.remove('d-none'); } else { /* keep checked value if indeterminate */ const checked = layerStyles.every((v) => v); elem._input.checked = checked; if (checked) { elem._legend.classList.remove('d-none'); } else { elem._legend.classList.add('d-none'); } } if (elem.children !== undefined && elem.children.length > 0) { setIndeterminateAndChecked(elem.children); } }); }; const fixLayerVisibility = function() { const result = {}; const nodata = [ 0, 0, 0, .0]; const kskog_palette = [nodata, nodata, nodata, nodata, nodata]; Object.keys(LAYERS).forEach(function(lyr) { const layerGroup = lyr.split('.', 1)[0]; if (result[layerGroup] === undefined) { result[layerGroup] = false; } result[layerGroup] ||= STYLES[lyr] !== undefined; if (layerGroup === 'kskog') { const i = parseInt(lyr.slice(layerGroup.length + 1)); kskog_palette[i] = STYLES[lyr] ?? nodata; } }); const kskog = mapLayers['kskog']; if (kskog != null) { /* XXX unfortunately calling .setStyle() makes the layer blink */ kskog.setStyle({ color: ['palette', ['band', 1], kskog_palette ] }); } Object.entries(result).forEach(function([lyr, visible]) { //console.log(lyr, visible); mapLayers[lyr]?.setVisible(visible); }); const btn = document.getElementById('map-legend-button'); if (Object.values(STYLES).some((v) => v !== null)) { btn.classList.remove('disabled'); } else { btn.classList.add('disabled'); } }; const onClickFunction = function(layerList, event) { disposePopover(); const searchParams = new URLSearchParams(location.hash.substring(1)); let layersParams = searchParams.get('layers') || ''; layersParams = layersParams.match(/^\s*$/) ? [] : layersParams.split(' '); if (event.target.checked) { layerList.forEach(function(lyr) { if (mapLayers[lyr.split('.', 1)[0]] == null) { return; /* keep unexisting layers (eg WebGL layers on a system without WebGL support) unselectable */ } STYLES[lyr] = LAYERS[lyr].style; if (!layersParams.includes(lyr)) { layersParams.push(lyr); } }); } else { layerList.forEach(function(lyr) { delete STYLES[lyr]; }); layersParams = layersParams.filter((lyr) => !layerList.includes(lyr)); } setIndeterminateAndChecked(layerHierarchy); fixLayerVisibility(); layerList .map((l) => l.split('.', 1)[0]) .filter((v, i, arr) => mapLayers[v] != null && arr.indexOf(v) === i) .forEach((l) => mapLayers[l].getSource().changed()); searchParams.set('layers', layersParams.join(' ')); location.hash = '#' + searchParams.toString(); }; let layerId = 0; const addAccordionGroup = function(parentNode, children) { const ul = document.createElement('ul'); parentNode.appendChild(ul); ul.classList.add('list-group', 'list-group-flush'); children.forEach(function(child) { const li = document.createElement('li'); ul.appendChild(li); li.classList.add('list-group-item'); const div = document.createElement('div'); li.appendChild(div); div.classList.add('d-inline-flex'); const input = child._input = document.createElement('input'); div.appendChild(input); input.classList.add('form-check-input'); input.type = 'checkbox'; input.id = 'layer' + layerId++; const label = document.createElement('label'); div.appendChild(label); label.classList.add('form-check-label'); label.setAttribute('for', input.id); const textNode = document.createTextNode(child.text); label.appendChild(textNode); if (child.children !== undefined && child.children.length > 0) { addAccordionGroup(li, child.children); } input.onclick = function(event) { return onClickFunction(child._layers, event); }; }); }; layerHierarchy.forEach(function(x, idx) { const item = document.createElement('div'); accordion.appendChild(item); item.classList.add('accordion-item'); const header = document.createElement('div'); item.appendChild(header); header.classList.add('accordion-header'); const btn = document.createElement('button'); header.appendChild(btn); const collapse = document.createElement('div'); item.appendChild(collapse); collapse.id = 'accordion-collapse-' + idx; collapse.classList.add('accordion-collapse', 'collapse'); /* never expand more than accordion at a time */ collapse.setAttribute('data-bs-parent', '#' + accordion.id); btn.type = 'button'; btn.setAttribute('data-bs-toggle', 'collapse'); btn.setAttribute('data-bs-target', '#' + collapse.id); btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-controls', collapse.id); btn.classList.add('accordion-button', 'collapsed'); const span0 = document.createElement('span'); btn.appendChild(span0); span0.classList.add('form-check'); span0.setAttribute('data-bs-toggle', 'collapse'); span0.setAttribute('data-bs-target', ''); const input0 = x._input = document.createElement('input'); span0.appendChild(input0); input0.classList.add('form-check-input'); input0.type = 'checkbox'; input0.id = 'layer' + layerId++; const label0 = document.createElement('label'); span0.appendChild(label0); label0.classList.add('form-check-label'); label0.setAttribute('for', input0.id); const text0 = document.createTextNode(x.text); label0.appendChild(text0); if (x.children === undefined || x.children.length === 0) { item.replaceChild(span0, header); } else { const body = document.createElement('div'); collapse.appendChild(body); body.classList.add('accordion-body'); addAccordionGroup(body, x.children); } input0.onclick = function(event) { return onClickFunction(x._layers, event); }; }); setIndeterminateAndChecked(layerHierarchy); fixLayerVisibility(); (function() { const item = document.createElement('div'); accordion.appendChild(item); item.classList.add('accordion-item'); const div = document.createElement('div'); item.appendChild(div); div.classList.add('form-check', 'form-switch'); const input = document.createElement('input'); div.appendChild(input); input.classList.add('form-check-input'); input.type = 'checkbox'; input.setAttribute('role', 'switch'); input.id = 'layer' + layerId++; const label = document.createElement('label'); div.appendChild(label); label.classList.add('form-check-label'); label.setAttribute('for', input.id); label.innerHTML = 'Nedtonad bakgrund karta'; input.checked = BASEMAP.layer === 'topowebb_nedtonad'; input.onchange = function(event) { BASEMAP.layer = event.target.checked ? 'topowebb_nedtonad' : 'topowebb'; }; })(); (function() { const accordion = document.getElementById('info-accordion'); layerHierarchy.forEach(function(x, idx) { const item = document.createElement('div'); accordion.appendChild(item); item.classList.add('accordion-item'); const header = document.createElement('div'); item.appendChild(header); header.classList.add('accordion-header'); const btn = document.createElement('button'); header.appendChild(btn); const collapse = document.createElement('div'); item.appendChild(collapse); collapse.id = 'info-accordion-collapse-' + idx; collapse.classList.add('accordion-collapse', 'collapse'); btn.type = 'button'; btn.setAttribute('data-bs-toggle', 'collapse'); btn.setAttribute('data-bs-target', '#' + collapse.id); btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-controls', collapse.id); btn.classList.add('accordion-button', 'collapsed'); const t = document.createTextNode(x.text); btn.appendChild(t); const body = document.createElement('div'); body.classList.add('accordion-body'); collapse.appendChild(body); const ul = document.createElement('ul'); ul.classList.add('list-group', 'list-group-flush'); body.appendChild(ul); infoMetadataAccordions.push({ element: ul, items: x._layers.map(function(k) { const groupname = k.split('.', 1)[0]; return [ groupname, k.slice(groupname.length + 1) ]; }), }); }); })(); })(); /* popup and feature overlays */ const disposePopover = (function() { /* return an tag with the given URL and optional text */ const reURL = new RegExp('^https?://', 'i'); const formatLink = function(url, text) { if (url == null || typeof url !== 'string' || !reURL.test(url)) { return url; } const a = document.createElement('a'); a.href = url; a.target = '_blank'; if (text != null && text !== '') { const t = document.createTextNode(text + ' '); a.appendChild(t); } const i = document.createElement('i'); i.classList.add('bi', 'bi-box-arrow-up-right'); a.appendChild(i); return a; }; /* test a condition on the field maps */ const condField = function(cond, k) { if (Array.isArray(cond)) { return cond.includes(k); } if (cond instanceof RegExp) { return cond.test(k); } return cond(k); }; /* filter fields by condition */ const filterFields = function(k, fields) { return fields.map(function(v) { if (v.cond == null || condField(v.cond, k)) { return v; } }).filter((f) => f != null); }; /* filter fields using a pre-built map */ const mapFields = function(k, fieldMap, fields) { if (fields === undefined) { return fieldMap.map((v) => k[v]); } return fields.map(function(v) { if (!Array.isArray(v)) { return fieldMap[v]; } else if (condField(v[1], k)) { return fieldMap[v[0]]; } }).filter((f) => f !== undefined); }; /* pre-build the field map so we don't need to duplicate objects accross layers */ const mkFieldMap = function(fieldMap) { return Object.fromEntries(Object.entries(fieldMap).map(function([k, o]) { if (typeof o === 'string') { return [k, Object.seal({key: k, desc: o})]; } else { return [k, Object.seal(Object.assign(o, {key: k}))]; } })); }; const layers = { svk: { ledningar: { title: 'Kraftledning (befintlig)', fields: [ { key: 'Placement', desc: 'Förläggning' }, { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, ], }, transmissionsnatsprojekt: { title: 'Transmissionsnätsprojekt', fields: [ { key: 'Name', desc: 'Projektnamn' }, { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, { key: 'Url', desc: 'Länk', fn: formatLink }, ], }, }, misc: { gigafactories: { title: 'Stor industrisatsning', fields: [ { key: 'Name', desc: 'Namn' }, { key: 'Url', desc: 'Länk', fn: formatLink }, ], }, dammar: { /* Documentation at * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf */ title: 'Damm', fields: [ { key: 'DNamn', desc: 'Dammenhetens namn' }, { key: 'Namn', desc: 'Dammanläggningens namn' }, { key: 'LST_OBJID', desc: 'Länsnr', classes: ['feature-objid'] }, { key: 'Status', desc: 'Status', fn: (v) => v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : '' }, //{ key: 'Regleringstyp', desc: 'Regleringstyp' }, { key: 'ByggAr', desc: 'Byggår' }, { key: 'DammHojd', desc: 'Dammhöjd', unit: 'm' }, { key: 'KronLangd', desc: 'Krönlängd', unit: 'm' }, { key: 'Fiskvag', desc: 'Fiskväg', fn: (v) => v === 1 ? 'Bassängtrappa' : v === 2 ? 'Denilränna' : v === 3 ? 'Slitsränna' : v === 4 ? 'Omlöp' : v === 5 ? 'Inlöp' : v === 6 ? 'Ålledare' : v === 7 ? 'Smoltränna' : v === 8 ? 'Okänd typ' : v === 9 ? 'Ingen' : v === 10 ? 'Annan' : null }, { key: 'HARO', desc: 'Huvudavrinningsområdesnummer', classes: ['feature-objid'] }, { key: 'Vattendistrikt', desc: 'Vattendistrikt', classes: ['feature-objid'] }, { key: 'Verksamhet', desc: 'Verksamhet', fn: (v) => v === 1 ? 'Kraftproduktion' : v === 2 ? 'Industri' : v === 3 ? 'Sjöfart' : v === 4 ? 'Invallning' : v === 5 ? 'Vattenförsörjning' : v === 6 ? 'Spegeldamm' : v === 7 ? 'Historisk' : v === 8 ? 'Övrigt' : null }, { key: 'DG', desc: 'Högsta dämningsgräns', unit: 'm' }, { key: 'SG', desc: 'Lägsta sänkningsgräns', unit: 'm' }, { key: 'MY', desc: 'Magasinsyta', unit: 'km²' }, { key: 'RV', desc: 'Reglerbar volym', unit: 'Mm³' }, { key: 'Kommentar', desc: 'Kommentar' }, ], }, }, }; layers.mrr = {}; (function() { const fields = [ { key: 'name', desc: 'Namn' }, { key: 'mineral', desc: 'Koncessionsmineral', cond: (i) => i < 6 }, { key: 'owners', desc: 'Ägare', cond: [0,2,4] }, { key: 'owners', desc: 'Sökande', cond: [1,3,5] }, { key: 'conc_name', desc: 'Tillhörande bearbetnings\u00ADkoncession(er)', cond: [6] }, { key: 'licenceid', desc: 'Tillståndsid', classes: ['feature-attr-mrr-license-id'], cond: [0,2,4,6] }, { key: 'geom_area', desc: 'Areal', fn: 'area' }, { key: 'validfrom', desc: 'Giltig från', cond: [0,2,4] }, { key: 'validto', desc: 'Giltig till', cond: [0,2,4] }, { key: 'diarynr', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, { key: 'appl_date', desc: 'Ansökningsdatum' }, { key: 'dec_date', desc: 'Beslutsdatum', cond: [0,2,4,6] }, ]; Object.entries({ ec: 'Bearbetningskoncession', met: 'Undersökningstillstånd, metaller och industrimineral', ogd: 'Undersökningstillstånd, olja, gas och diamant', }) .flatMap(([k, title]) => [ /* don't use Object.entries() to guaranty ordering */ ['appr', 'beviljad'], /* even index */ ['appl', 'ansökt'], /* odd index */ ].map(([a,b]) => [a + '_' + k, title + ' \u2013 ' + b])) .concat([['appr_dl', 'Markanvisning till koncession']]) /* index #6 */ .forEach(([k, title], idx) => layers.mrr[k] = { title, fields: filterFields(idx, fields) }); })(); layers.vbk = {}; (function() { const fieldMap = mkFieldMap({ Projektnamn: 'Projektnamn', OmrID: { desc: 'Områdes-ID', classes: ['feature-objid'] }, AntalVerk: 'Aktuella verk', AntalEjXY: 'Antal ej koordinatsatta verk', Projektstatus: 'Projektstatus', Diarienummer: 'Diarienummer', geom_area: { desc: 'Areal', fn: 'area' }, Calprod: { desc: 'Beräknad årsproduktion', unit: 'GWh' }, PlaneradByggstart: 'Planerad byggstart', PlaneratDrift: 'Planerat drifttagande', AndringsansokanPagar: 'Ändringsansökan pågår', UnderByggnation: 'Under byggnation', Organisationsnamn: 'Verksamhetsutövare', Organisationsnummer: { desc: 'Organisationsnummer', classes: ['feature-orgnr'] }, SamradsunderlagInlamnat: 'Samrådsunderlag inlämnat', AnsokanInlamnat: 'Tillståndsansökan inlämnad', AnsokanAterkallad: 'Tillståndsansökan återkallad', AnsokanBeviljad: 'Tillståndsansökan beviljad', AnsokanAvslagen: 'Tillståndsansökan avslagen', AnsokanOverklagad: 'Överklagad', Natura2000_Ansokan: 'Natura2000 ansökan', Natura2000_Beslutdatum: 'Natura2000 beslutsdatum', Uppfort: 'Parken uppförd', PlaneratAntalVerkMin: 'Planerat antal verk (min)', PlaneratAntalVerkMax: 'Planerat antal verk (max)', PlaneradHojdMin: { desc: 'Panerad totalhöjd (min)', unit: 'm' }, PlaneradHojdMax: { desc: 'Panerad totalhöjd (max)', unit: 'm' }, PlaneradProduktionMin: { desc: 'Planerad årsproduktion (min)', unit: 'GWh' }, PlaneradProduktionMax: { desc: 'Planerad årsproduktion (max)', unit: 'GWh' }, BeviljatAntalVerk: 'Beviljat antal verk', UppfortAntalVerk: 'Uppfört antal verk', BeviljadMaxhojd: { desc: 'Beviljad maxhöjd', unit: 'm' }, InstalleradEffekt: { desc: 'Installerad effekt', unit: 'MW' }, ElNamn: 'Elområde', SenasteUppdaterat: 'Senast uppdaterat', }); Object.entries({ current: null, notcurrent: ' \u2013 ej aktuell', }) .forEach(([k, title]) => layers.vbk['area_' + k] = { title: 'Landbaserad projekteringsområde för vindkraft' + (title ?? ''), fields: mapFields(k, fieldMap, [ 'Projektnamn', 'OmrID', 'AntalVerk', 'AntalEjXY', 'geom_area', 'Calprod', 'PlaneradByggstart', 'PlaneratDrift', 'AndringsansokanPagar', ['UnderByggnation', ['current']], 'Organisationsnamn', 'Organisationsnummer', 'ElNamn', 'SenasteUppdaterat', ]), }); [ ['completed', /* 0 */ 'uppförd'], ['approved', /* 1 */ 'tillståndsansökan beviljad'], ['amended', /* 2 */ 'ändringsansökan'], ['rejected', /* 3 */ 'tillståndsansökan avslagen'], ['appealed', /* 4 */ 'överklagad'], ['applied', /* 5 */ 'tillståndsansökan inlämnad'], ['consultation', /* 6 */ 'samråd inför tillståndsansökan'], ['investigation', /* 7 */ 'inledande undersökningar'], ['revoked', /* 8 */ 'inte aktuell eller återkallad'], ] .forEach(([k, title], idx) => layers.vbk['offshore_' + k] = { title: 'Havsbaserad vindkraft \u2013 ' + title, fields: mapFields(idx, fieldMap, [ 'Projektnamn', 'OmrID', 'Organisationsnamn', 'Organisationsnummer', 'Projektstatus', 'Diarienummer', ['AndringsansokanPagar', [1,2,4]], 'geom_area', ['SamradsunderlagInlamnat', (i) => i <= 6 || i === 8], ['AnsokanInlamnat', (i) => i <= 5 || i === 8], ['AnsokanAterkallad', [8]], ['AnsokanBeviljad', [0,1,4,8]], ['AnsokanAvslagen', [3,8]], ['AnsokanOverklagad', [0,1,3,4,8]], ['Natura2000_Ansokan', (i) => i !== 2], ['Natura2000_Beslutdatum', (i) => i !== 2], ['UnderByggnation', [1]], ['PlaneratAntalVerkMin', (i) => i > 0], ['PlaneratAntalVerkMax', (i) => i > 0], ['PlaneradHojdMin', (i) => i > 0], ['PlaneradHojdMax', (i) => i > 0], ['PlaneradProduktionMin', (i) => i > 0], ['PlaneradProduktionMax', (i) => i > 0], ['PlaneradByggstart', (i) => i > 0], ['Uppfort', [0,8]], ['PlaneratDrift', (i) => i > 0], ['BeviljatAntalVerk', [0,1,4,8]], ['UppfortAntalVerk', [0,8]], ['BeviljadMaxhojd', [0,1,4,8]], ['InstalleradEffekt', [0]], ['Calprod', [0]], 'ElNamn', 'SenasteUppdaterat', ]), }); Object.assign(fieldMap, mkFieldMap({ VerkID: { desc: 'Verk-ID', classes: ['feature-objid'] }, Status: 'Status', Handlingstyp: 'Handlingstyp', MB_Tillstand: 'Miljöbalken tillstånd tidsbegränsning', Uppfort: 'Uppförandedatum',/* override previous def */ Totalhojd: { desc: 'Totalhöjd', unit: 'm' }, Navhojd: { desc: 'Navhöjd', unit: 'm' }, Rotordiameter: { desc: 'Rotordiameter', unit: 'm' }, Maxeffekt: { desc: 'Maxeffekt', unit: 'MW' }, Fabrikat: 'Fabrikat', Modell: 'Modell', Placering: 'Placering', })); [ ['completed', /* 0 */ 'uppfört'], ['approved', /* 1 */ 'beviljat'], ['rejected', /* 2 */ 'avslagit/nekat'], ['processed', /* 3 */ 'handlagt'], ['dismounted', /* 4 */ 'nedmonterat'], ['appealed', /* 5 */ 'överklagat'], ['revoked', /* 6 */ 'inte aktuell eller återkallad'], ] .forEach(([k, title], idx) => layers.vbk['station_' + k] = { title: 'Landbaserad vindkraftverk \u2013 ' + title, fields: mapFields(idx, fieldMap, [ 'VerkID', 'OmrID', 'Projektnamn', 'Status', 'Handlingstyp', ['Uppfort', [0,4,6]], 'MB_Tillstand', 'Totalhojd', 'Navhojd', 'Rotordiameter', 'Maxeffekt', 'Calprod', 'Fabrikat', 'Modell', 'Organisationsnamn', 'Organisationsnummer', 'Placering', 'ElNamn', 'SenasteUppdaterat', ]), }); })(); layers.avverk = {}; (function() { const zeroIsNull = (v) => v > 0 ? v : null; const fieldMap = mkFieldMap({ /* Documentation at * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf * and * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf */ Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, ArendeAr: 'Registeringsår', Inkomdatum: 'Inkom datum', Skogstyp: 'Skogstyp', Avvdatum: 'Datum för avverkning', KallaDatum: 'Ursprung för datum för avverkning', AnmaldHa: { desc: 'Areal anmält', unit: 'ha' }, NatforHa: { desc: 'Areal naturlig föryngring', unit: 'ha', fn: zeroIsNull }, SkogsodlHa: { desc: 'Areal plantering', unit: 'ha', fn: zeroIsNull }, AvvSasong: 'Avverkningssäsong', Avverktyp: 'Avverkningstyp', ArendeStatus: 'Ärendestatus', AvvHa: { desc: 'Avverkad areal', unit: 'ha' }, geom_area: { desc: 'Areal för ytan', fn: 'area' }, }); layers.avverk.utford = { title: 'Utförd avverkning', fields: mapFields(fieldMap, [ 'Beteckn', 'ArendeAr', 'Skogstyp', 'AnmaldHa', 'NatforHa', 'Avverktyp', 'Avvdatum', 'KallaDatum', 'geom_area', ]), }; layers.avverk.anmald = { title: 'Avverkningsanmälansområde', fields: mapFields(fieldMap, [ 'Beteckn', 'Inkomdatum', 'ArendeAr', 'AnmaldHa', 'NatforHa', 'SkogsodlHa', 'AvvSasong', 'ArendeStatus', 'AvvHa', ]), }; })(); layers.skydd = {}; (function() { const fieldMap = mkFieldMap({ NVRID: { desc: 'NVR-ID', classes: ['feature-objid'] }, FORSKRNAMN: 'Föreskriftsområde', OBJEKTNAMN: 'Namn', NAMN: 'Namn', BESLSTAT: 'Beslutsstatus', FORESKRTYP: 'Föreskriftstyp', FORESKRIFT: 'Föreskriftssubtyp', FRANDATUM: 'Från datum', TILLDATUM: 'Till datum', BESKRIVN: 'Beskrivning', geom_area: { desc: 'Areal', fn: 'area' }, SKYDDSTYP: 'Skyddstyp', BESLSTATUS: 'Beslutsstatus', URSBESLDAT: 'Beslutsdatum (bildande)', URSGALLDAT: 'Ursprungligt gällandedatum', SENGALLDAT: 'Senaste gällandedatum', FORVALTARE: 'Förvaltare', IUCNKAT: 'IUCN-kategori', DIARIENR: { desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, LAGRUM: 'Lagrum', BESLMYND: 'Beslutsmyndighet', LAND_HA: { desc: 'Areal land', unit: 'ha' }, VATTEN_HA: { desc: 'Areal vatten', unit: 'ha' }, SKOG_HA: { desc: 'Skogsmarksareal', unit: 'ha' }, IKRAFTDATF: 'Ikraftträdandedatum föreskrifter', TILLSYNSMH: 'Tillsynsmyndighet', PROVNMHTIL: 'Prövningsmyndighet tillstånd', PROVNMHDIS: 'Prövningsmyndighet dispens', NAME: 'Namn', RAMSAR_ID: { desc: 'Ramsar-ID', classes: ['feature-objid'] }, LEGAL_ACT: 'Rättsakt', URSPR_BESL: 'Ursprungligt beslutsdatum', SEN_BESLUT: 'Senaste beslutsdatum', LINK: { desc: 'Länk', fn: formatLink }, }); layers.skydd.tilltradesforbud = { title: 'Tillträdesförbud', fields: mapFields(fieldMap, [ 'NVRID', 'FORSKRNAMN', 'OBJEKTNAMN', 'BESLSTAT', 'FORESKRTYP', 'FORESKRIFT', 'FRANDATUM', 'TILLDATUM', 'BESKRIVN', 'geom_area', ]), }; /* Nationella skyddsformer från Naturvårdsregistret */ const isSurface = (k) => !/_punkt$/.test(k); Object.entries({ nationalpark: 'Nationalpark', naturreservat: 'Naturreservat', naturreservat_kommunalt: 'Kommunalt naturreservat', naturvardsomrade: 'Naturvårdsområde', djur_och_vaxtskyddsomrade: 'Djur- och växtskyddsområde', kulturreservat: 'Kulturreservat', vattenskyddsomrade: 'Vattenskyddsområden', landskapsbildsskyddsomrade: 'Landskapsbildsskyddsområde', ovrigt_biotopskyddsomrade: 'Biotopskydd utanför skogsmark', naturminne_yta: 'Naturminne (yta)', naturminne_punkt: 'Naturminne (punkt)', interimistiskt_forbud: 'Interimistiskt förbud', }) .forEach(([k, title]) => layers.skydd[k] = { title: title, fields: mapFields(k, fieldMap, [ 'NVRID', 'NAMN', 'SKYDDSTYP', 'BESLSTATUS', 'URSBESLDAT', ['URSGALLDAT', (k) => k !== 'vattenskyddsomrade'], ['SENGALLDAT', (k) => k !== 'vattenskyddsomrade'], ['FORVALTARE', (k) => k !== 'vattenskyddsomrade'], ['IKRAFTDATF', (k) => k === 'vattenskyddsomrade'], 'IUCNKAT', 'DIARIENR', 'LAGRUM', 'BESLMYND', ['TILLSYNSMH', (k) => k === 'vattenskyddsomrade'], ['PROVNMHTIL', (k) => k === 'vattenskyddsomrade'], ['PROVNMHDIS', (k) => k === 'vattenskyddsomrade'], ['geom_area', isSurface], ['LAND_HA', isSurface], ['VATTEN_HA', isSurface], ['SKOG_HA', isSurface], ]), }); /* Natura 2000-områden */ (function() { const fields = [ { key: 'SITE_CODE', desc: 'Områdeskod', classes: ['feature-objid'] }, { key: 'NAMN', desc: 'Namn' }, { key: 'OMRADESTYP', desc: 'Områdestyp' }, { key: 'UPPLAMNARE', desc: 'Uppgiftslämnare' }, { key: 'SPA_DATUM', desc: 'SPA-datum' }, { key: 'SCI_FORSL', desc: 'SCI-förslagsdatum' }, { key: 'SCI_DATUM', desc: 'SCI-datum' }, { key: 'SAC_DATUM', desc: 'SAC-datum' }, fieldMap.geom_area, { key: 'KVALITET', desc: 'Kvalitet' }, { key: 'KARAKTAR', desc: 'Kännetecken för området' }, { key: 'ARTER', desc: 'Arter' }, { key: 'NATURTYPER', desc: 'Naturtyper' }, { key: 'BEVPLAN', desc: 'Bevarandeplan', fn: formatLink }, ]; Object.entries({ fageldirektivet: 'Fågeldirektivet (SPA)', habitatdirektivet: 'Art- och habitatdirektivet (SCI)', }) .forEach(([k, title]) => layers.skydd[k] = { title, fields }); })(); /* Områden med internationell status */ layers.skydd.helcom = { title: 'Marina skyddade områden (Helcom MPA)', fields: mapFields(fieldMap, [ 'NAME', 'geom_area' ]), }; layers.skydd.ramsar = { title: 'Ramsar-områden (Våtmarkskonventionen)', fields: mapFields(fieldMap, [ 'RAMSAR_ID', 'SKYDDSTYP', 'NAMN', 'geom_area', 'LAND_HA', 'VATTEN_HA', 'SKOG_HA', 'URSPR_BESL', 'SEN_BESLUT', 'LEGAL_ACT', 'LINK', ]), }; layers.skydd.ospar = { title: 'Marina skyddade områden (Ospar MPA)', fields: [ { key: 'ORIGIN', desc: 'Ursprung' }, { key: 'NAMN_N2000', desc: 'N2000-namn' }, { key: 'MPA_ID', desc: 'MPA-ID', classes: ['feature-objid'] }, { key: 'MPA_NAMN', desc: 'MPA-namn' }, { key: 'N2000_SITE', desc: 'N2000-ID', classes: ['feature-objid'] }, fieldMap.geom_area, ], }; layers.skydd.varldsarv = { title: 'Världsarv med mycket höga naturvärden (Unesco)', fields: mapFields(fieldMap, [ 'NAMN', 'geom_area' ]), }; layers.skydd.naturvardsavtal = { title: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', fields: [ { key: 'ID', desc: 'ID', classes: ['feature-objid'] }, { key: 'OBJNAMN', desc: 'Namn' }, { key: 'FASTBET', desc: 'Fastighet', classes: ['feature-objid'] }, { key: 'DATSTART', desc: 'Giltig från' }, { key: 'DATSLUT', desc: 'Giltig till' }, { key: 'DIARIENRNV', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, { key: 'STATUS', desc: 'Satus' }, fieldMap.geom_area, ], }; })(); (function() { const fieldMap = mkFieldMap({ Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, Biotyp: { desc: 'Biotopkategori' }, Naturtyp: { desc: 'Skogstyp' }, ArendeAr: { desc: 'Registeringsår' }, geom_area: { desc: 'Areal', fn: 'area' }, AreaProd: { desc: 'Skogsmarksareal', unit: 'ha' }, Datbeslut: { desc: 'Beslutsdatum' }, Url: { desc: 'Länk', fn: (v) => formatLink(v, 'Skogens Pärlor') }, NvaTyp: 'Biotopkategori', DatAvtal: 'Avtalsdatum', Undertyp: 'Undertyp', AvtalatDatum: 'Avtalat datum', Objnamn: 'Objektnamn', Datinv: 'Datum för fältinventering', }); layers.skydd.skogligt_biotopskyddsomrade = { title: 'Biotopskydd i skogsmark', fields: mapFields(fieldMap, [ 'Beteckn', 'Biotyp', 'Naturtyp', 'ArendeAr', 'geom_area', 'AreaProd', 'Datbeslut', 'Url', ]), }; layers.skydd.naturvardsavtal_skogsstyrelsen = { title: 'Naturvårdsavtal (Skogsstyrelsen)', fields: mapFields(fieldMap, [ 'Beteckn', 'ArendeAr', 'NvaTyp', 'Naturtyp', 'DatAvtal', 'geom_area', 'AreaProd', 'Url', 'Undertyp', ]), }; layers.skydd.atervatningsavtal = { title: 'Återvätningsavtal', fields: mapFields(fieldMap, [ 'Beteckn', 'ArendeAr', 'AvtalatDatum', 'geom_area', 'Url', ]), }; layers.nv = {}; Object.assign(fieldMap, mkFieldMap(Object.fromEntries( [1,2,3].map((i) => [`Biotop${i}`, `Biotoptyp #${i}`]).concat( [1,2,3,4,5,6,7,8].map((i) => [`Beskrivn${i}`, `Nyckelord #${i} som beskriver objektet`]) )))); layers.nv.naturvarde_sks = { title: 'Objekt med naturvärden (Skogsstyrelsen)', fields: mapFields(fieldMap, [ 'Beteckn', 'Objnamn', 'Datinv', 'Biotop1', 'Biotop2', 'Biotop3', 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', 'geom_area', 'Url', ]), }; layers.nv.nyckelbiotop = { title: 'Nyckelbiotop (Skogsstyrelsen)', fields: mapFields(fieldMap, [ 'Beteckn', 'Objnamn', 'Datinv', 'Biotop1', 'Biotop2', 'Biotop3', 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', 'Beskrivn4', 'Beskrivn5', 'Beskrivn6', 'Beskrivn7', 'Beskrivn8', 'geom_area', 'Url', ]), }; layers.nv.nyckelbiotop_storskogsbruk = { title: 'Nyckelbiotop (storskogsbruket)', fields: [ { key: 'Org', desc: 'Uppgifter lämnade av' }, { key: 'InkomDatum', desc: 'Inkom datum' }, fieldMap.geom_area, fieldMap.Url, ], }; layers.nv.sumpskog = { title: 'Sumpskog', fields: [ { key: 'Namn', desc: 'Objektnamn' }, { key: 'Tradtext', desc: 'Skogstyp' }, { key: 'Hydrtext', desc: 'Hydrologisk typ' }, { key: 'Delklass', desc: 'Klass på delobjektet' }, { key: 'Klassu', desc: 'Klass på objektet' }, { key: 'Lovandel', desc: 'Andel löv' }, { key: 'Andelva', desc: 'Andel öppet vatten' }, { key: 'Krontakn', desc: 'Krontäckning' }, { key: 'Huggklas', desc: 'Huggningsklass' }, { key: 'Ingrepp', desc: 'Ingrepp på delobjekt (max 4)' }, { key: 'Ingrpavv', desc: 'Grad av påverkan på delobjekt (max 4)' }, { key: 'Objnyck', desc: 'Nyckelord på objektnivå' }, { key: 'Delnyck', desc: 'Nyckelord på delobjektsnivå' }, { key: 'Flygar', desc: 'Flygbildsår' }, { key: 'Faltdat', desc: 'Datum för fältbesök' }, { key: 'Invtekn', desc: 'Inventeringsteknik' }, { key: 'Invdat', desc: 'Inventeringdatum' }, { key: 'Ansvmynd', desc: 'Ansvarig myndighet' }, fieldMap.geom_area, fieldMap.Url, ], }; })(); layers.nv.pagaende_naturreservatsbildning = { title: 'Pågående naturreservatsbildning', fields: [ { key: 'NAMN', desc: 'Objektnamn' }, /* XXX unclear what "GRANSJUST" means, just a guess */ { key: 'GRANSJUST', desc: 'Senast justerat' }, { key: 'geom_area', desc: 'Areal', fn: 'area' }, ], }; layers.nv.snus = { title: 'Skyddsvärd statlig skog', fields: [ { key: 'NAMN', desc: 'Objektnamn' }, { key: 'AR', desc: 'År' }, { key: 'NATURGEOGR', desc: 'Naturgeografisk region', classes: ['feature-objid'] }, { key: 'OBJEKTKATE', desc: 'Objektskategori', classes: ['feature-objid'] }, { key: 'MARKAGARE', desc: 'Markägare' }, { key: 'VARDEKARNA', desc: 'Areal värdekärna', unit: 'ha' }, { key: 'UTV_MARK', desc: 'Areal utvecklingsmark', unit: 'ha' }, { key: 'TOTAL_AREA', desc: 'Totalareal', unit: 'ha' }, { key: 'LAND', desc: 'Areal land', unit: 'ha' }, { key: 'VATTEN', desc: 'Areal vatten', unit: 'ha' }, { key: 'PROD_SKOG', desc: 'Areal produktiv skogsmark', unit: 'ha' }, { key: 'SKOG_O_FJG', desc: 'Areal produktiv skogsmark ovanför fjällnära gräns', unit: 'ha' }, { key: 'SKOG_N_FJG', desc: 'Areal produktiv skogsmark nedanför fjällnära gräns', unit: 'ha' }, { key: 'SKYDDSZON', desc: 'Areal skyddszon', unit: 'ha' }, { key: 'ARRO_MARK', desc: 'Areal arronderingsmark', unit: 'ha' }, { key: 'KRITERIER', desc: 'Kriterier för urval' }, { key: 'BESKRIVN', desc: 'Beskrivning av området' }, { key: 'LST_BEDOMN', desc: 'Länsstyrelsens bedömning' }, { key: 'KALLOR', desc: 'Källor' }, ], }; layers.ri = {}; (function() { const fieldMap = mkFieldMap({ NAMN: 'Namn', SKYDD: 'Skydd', AMNESOMRAD: 'Ämnesområde', AMNESOMR: 'Ämnesområde', OMRADESNR: { desc: 'Områdesnummer', classes: ['feature-objid'] }, BESKRIVNIN: { desc: 'Beskrivning', fn: formatLink }, LANK_VARDE: { desc: 'Länk värdebeskrivning', fn: formatLink }, LAGRUM: 'Lagrum', BESLUTSDAT: 'Beslutsdatum', BESLDATUM: 'Beslutsdatum', ARENDENR: { desc: 'Ärendenummer', classes: ['feature-attr-dnr'] }, LANK_BESLU: { desc: 'Länk beslut', fn: formatLink }, AKTIVITET: 'Aktivitet', NATURTYP: 'Naturtyp', ORGINALID: { desc: 'Original-ID', classes: ['feature-objid'] }, RIKSID: { desc: 'Riks-ID', classes: ['feature-objid'] }, geom_area: { desc: 'Areal', fn: 'area' }, AREA_LAND_: { desc: 'Areal land', unit: 'ha' }, AREA_VATTE: { desc: 'Areal vatten', unit: 'ha' }, }); layers.ri.naturvard = { title: 'Riksintresse naturvård', fields: mapFields(fieldMap, [ 'NAMN', 'SKYDD', 'AMNESOMRAD', 'BESKRIVNIN', 'LAGRUM', 'BESLUTSDAT', 'ORGINALID', 'RIKSID', 'geom_area', ]), }; layers.ri.friluftsliv = { title: 'Riksintresse friluftsliv', fields: mapFields(fieldMap, [ 'NAMN', 'SKYDD', 'AMNESOMR', 'OMRADESNR', 'LANK_VARDE', 'LAGRUM', 'BESLDATUM', 'ARENDENR', 'LANK_BESLU', 'AKTIVITET', 'NATURTYP', 'geom_area', 'AREA_LAND_', 'AREA_VATTE', ]), }; Object.assign(fieldMap, mkFieldMap({ METODBESKR: 'Metodbeskrivning', TILLKDATUM: 'Tillkomstdatum', REVDATUM: 'Revisionsdatum', ANM: 'Anmärkning', OBJEKTLANK: { desc: 'Objektlänk', fn: formatLink }, REFERENS: 'Referens', OBJTYP: 'Objekttyp', ORIGINALID: fieldMap.ORGINALID, DIG_SKALA: { desc: 'Digitaliseringsskala', fn: (v) => v > 0 ? v : null }, })); [ ['rorligt_friluftsliv', /* 0 */ 'rörligt friluftsliv (MB 4 kap 1§ och 2§)'], ['obruten_kust', /* 1 */ 'obruten kust (MB 4 kap 3§)'], ['obrutet_fjall', /* 2 */ 'obrutet fjäll (MB 4 kap 5§)'], ['skyddade_vattendrag', /* 3 */ 'skyddade vattendrag (MB 4 kap 6§)'], ] .forEach(([k, title], idx) => layers.ri[k] = { title: 'Riksintresse ' + title, fields: mapFields(idx, fieldMap, [ 'NAMN', 'BESKRIVNIN', 'METODBESKR', 'TILLKDATUM', 'REVDATUM', ['OBJTYP', [1]], ['ANM', [0,1,3]], ['DIG_SKALA', [3]], 'OBJEKTLANK', 'geom_area', 'ORIGINALID', 'REFERENS', ]), }); })(); layers.ren = { betesomrade: { title: 'Samebyarnas betesområde', fields: [ { key: 'NAMN', desc: 'Sameby' }, { key: 'SAMEBY_TYP', desc: 'Samebys typ' }, { key: 'SIGNATUR', desc: 'Signatur' }, { key: 'AKTUALITET', desc: 'Aktualitet' }, { key: 'geom_area', desc: 'Areal', fn: 'area' }, ], }, flyttled: { title: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', fields: [ { key: 'LED_ID', desc: 'Led-ID', classes: ['feature-objid'], fn: (v) => v > 0 ? v : null }, { key: 'SAMEBY1', desc: 'Sameby #1' }, { key: 'SAMEBY2', desc: 'Sameby #2' }, { key: 'SAMEBY3', desc: 'Sameby #3' }, { key: 'BESKRIVNIN', desc: 'Beskrivning' }, { key: 'ARSTID', desc: 'Årstid' }, { key: 'RIKSINTR', desc: 'Riksintresse' }, { key: 'FAST_LED', desc: 'Fast led' }, { key: 'AKTUALITET', desc: 'Aktualitet' }, { key: 'SIGNATUR', desc: 'Signatur' }, { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, ], }, riks_ren: { title: 'Riksintresse rennäring', fields: [ { key: 'LAGRUM', desc: 'Lagrum' }, { key: 'AKTUALITET', desc: 'Aktualitet' }, { key: 'SIGNATUR', desc: 'Signatur' }, { key: 'geom_area', desc: 'Areal', fn: 'area' }, ], }, omr_riks: { title: '(Kärn)områden av riksintresse rennäring', fields: [ { key: 'OMR_NR', desc: 'Områdes-ID', classes: ['feature-objid'] }, { key: 'LANK', desc: 'Länk' }, { key: 'ARET_RUNT', desc: 'Årets runt' }, { key: 'SAMEBY', desc: 'Sameby' }, { key: 'ANSVARIG', desc: 'Ansvarig' }, { key: 'AKTUALITET', desc: 'Aktualitet' }, { key: 'SIGNATUR', desc: 'Signatur' }, { key: 'geom_area', desc: 'Areal', fn: 'area' }, ], }, }; /* format value to HTML */ 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 { value /= 1000; value = Math.round(value*100) / 100; unit = '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 { value /= 1000000; unit = 'km²'; } value = Math.round(value*100) / 100; } if (value == null) { return null; } if (value instanceof HTMLElement) { return value; } switch (typeof value) { case 'boolean': return document.createTextNode(value ? 'Ja' : 'Nej'); case 'string': return document.createTextNode(value); case 'number': if (unit != null) { return document.createTextNode(value.toLocaleString('sv-SE') + '\u202F' + unit); } return document.createTextNode(value.toString()); default: return null; } }; /* turn the properties into a fine */ const formatFeaturePropertiesToHTML = function(properties) { const table = document.createElement('table'); table.classList.add('table', 'table-sm', 'table-borderless', 'table-hover'); const tbody = document.createElement('tbody'); table.appendChild(tbody); const def = layers[properties.layer_group][properties.layer]; def.fields.forEach(function(field) { const tr = document.createElement('tr'); tbody.appendChild(tr); const th = document.createElement('th'); th.setAttribute('scope', 'row'); tr.appendChild(th); const textDesc = document.createTextNode(field.desc); th.appendChild(textDesc); const td = document.createElement('td'); tr.appendChild(td); const v = formatValue(properties[field.key], field); if (v != null) { td.appendChild(v); } field.classes?.forEach?.((c) => td.classList.add(c)); }); const content = document.createElement('div'); if (def.title != null) { const h = document.createElement('h6'); content.appendChild(h); const textNode = document.createTextNode(def.title); h.appendChild(textNode); } content.appendChild(table); return content; }; /* initialize popup overlay */ const popupOverlay = new Overlay({ stopEvent: true, element: document.getElementById('popup'), }); MAP.addOverlay(popupOverlay); let featureOverlayLayer = null; let overlayAttributes = [], overlayAttrIdx = 0, mapSources = {}; /* clear the highlighted feature list and make the overlay layer invisible */ const disposeFeatureOverlay = function() { if (featureOverlayLayer?.getVisible?.()) { featureOverlayLayer.setVisible(false); featureOverlayLayer.changed(); } /* clear the overlay list */ overlayAttributes = []; overlayAttrIdx = 0; mapSources = {}; } let popover = null; /* clear overlay layer and dispose popover */ const disposePopover = function() { disposeFeatureOverlay(); if (popover?.tip != null) { popover.dispose(); } }; /* initialize popover */ featureOverlayLayer = new VectorTileLayer({ zIndex: 65535, declutter: false, visible: false, renderMode: 'vector', style: null, map: MAP, }); const header = document.createElement('div'); header.classList.add('d-flex'); const headerGrabbingArea = document.createElement('div'); headerGrabbingArea.classList.add('flex-grow-1', 'grabbing-area', 'pe-2', 'me-2'); header.appendChild(headerGrabbingArea); const pageNode = document.createElement('h6'); headerGrabbingArea.appendChild(pageNode); headerGrabbingArea.onmousedown = function(event) { /* move the popover around */ if (event.button != 0) { return; } const popoverTip = popover.tip; if (popoverTip.classList.contains('popover-maximized')) { return; } headerGrabbingArea.classList.add('grabbing-area-grabbed'); if (!popoverTip.classList.contains('popover-detached')) { /* detach popover tip */ popoverTip.classList.add('popover-detached'); const rect = popoverTip.getBoundingClientRect(); const style = popoverTip.style; style.display = 'none'; /* avoid reflows between the following assignments */ style.position = 'absolute'; style.transform = ''; style.inset = `${rect.top}px auto auto ${rect.left}px`; style.display = ''; } let clientX = event.clientX, clientY = event.clientY; document.onmousemove = function(event) { const offsetX = clientX - event.clientX; const offsetY = clientY - event.clientY; clientX = event.clientX; clientY = event.clientY; popoverTip.style.top = (popoverTip.offsetTop - offsetY).toString() + 'px'; popoverTip.style.left = (popoverTip.offsetLeft - offsetX).toString() + 'px'; }; document.onmouseup = function(event) { /* done moving around */ if (event.button != 0) { return; } headerGrabbingArea.classList.remove('grabbing-area-grabbed'); document.onmousemove = null; document.onmouseup = null; }; }; /* current number page and total page count */ 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); /* highlight a feature */ const featureOverlayStyle = new Style({ stroke: new Stroke({ color: 'rgba(0, 255, 255, .8)', width: 3, }), }); const highlightFeature = function(layer_group, layer, id) { const source = mapSources[layer_group]; if (source == null) { return; } if (featureOverlayLayer.getSource() !== source) { /* console.log('Updating source for feature overlay layer'); */ featureOverlayLayer.setVisible(false); featureOverlayLayer.setSource(source); } featureOverlayLayer.setStyle(function(feature) { if (feature.getId() === id && feature.getProperties().layer === layer) { return featureOverlayStyle; } }); featureOverlayLayer.setVisible(true); featureOverlayLayer.changed(); }; /* highlight the feature at index overlayAttrIdx within the CGI reply list */ const refreshPopover = function() { const attr = overlayAttributes[overlayAttrIdx]; highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); pageNum.innerHTML = (overlayAttrIdx + 1).toString(); const content = formatFeaturePropertiesToHTML(attr); popover.tip.getElementsByClassName('popover-body')[0].replaceChildren(content); }; /* go back/forward in the overlayAttributes list */ const onClickPageChange = function(event, offset) { const btn = event.target; if (btn.classList.contains('disabled') || popover?.tip == null) { return; } if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) { return; /* out of range */ } overlayAttrIdx += offset; if (overlayAttrIdx < 1) { btnPrev.classList.add('disabled'); } else { btnPrev.classList.remove('disabled'); } if (overlayAttrIdx < overlayAttributes.length - 1) { btnNext.classList.remove('disabled'); } else { btnNext.classList.add('disabled'); } refreshPopover(); setTimeout(function() { btn.blur() }, 100); }; /* control buttons */ 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.setAttribute('aria-label', btnPrev.title); 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.setAttribute('aria-label', btnNext.title); btnNext.onclick = function(event) { return onClickPageChange(event, +1); }; const btnExpand = document.createElement('button'); btnExpand.classList.add('popover-button', 'popover-button-expand'); btnExpand.setAttribute('type', 'button'); const btnExpandTitle = 'Förstora'; const btnExpandTitle2 = 'Förminska'; btnExpand.setAttribute('aria-label', btnExpand.title); btnExpand.onclick = function() { /* maximize or reduce the popover */ if (popover?.tip == null) { return; } if (!popover.tip.classList.contains('popover-maximized')) { popover.tip.classList.add('popover-maximized'); btnExpand.classList.replace('popover-button-expand', 'popover-button-reduce'); btnExpand.title = btnExpandTitle2; btnExpand.setAttribute('aria-label', btnExpand.title); } else { popover.tip.classList.remove('popover-maximized'); btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); btnExpand.title = btnExpandTitle; btnExpand.setAttribute('aria-label', btnExpand.title); } setTimeout(function() { btnExpand.blur() }, 100); }; const btnClose = document.createElement('button'); btnClose.classList.add('popover-button', 'popover-button-close'); btnClose.setAttribute('type', 'button'); btnClose.title = 'Stäng'; btnClose.setAttribute('aria-label', btnClose.title); btnClose.onclick = disposePopover; header.appendChild(btnPrev); header.appendChild(btnNext); header.appendChild(btnExpand); header.appendChild(btnClose); MAP.on('singleclick', function(event) { disposeFeatureOverlay(); /* dispose any pre-existing popover if not in detached mode */ popover = Popover.getInstance(popupOverlay.element); if (popover?.tip != null && !popover.tip.classList.contains('popover-detached')) { popover.dispose(); } const size = event.map.getSize(); if (size[0] < 576 || size[1] < 576) { return; /* skip popover if the map is too small */ } /* unclear how many feature we'll find, don't render prev/next buttons for now */ pageNode.classList.add('d-none'); btnPrev.classList.add('d-none', 'disabled'); btnNext.classList.add('d-none', 'disabled'); /* never start in maximized mode */ if (popover?.tip != null) { popover.tip.classList.remove('popover-maximized'); } btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); btnExpand.title = btnExpandTitle; btnExpand.setAttribute('aria-label', btnExpand.title); const fetch_body = []; event.map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { const layerGroup = layer.get('layerGroup'); const layerName = feature.getProperties().layer; mapSources[layerGroup] ??= layer.getSource(); const def = layerName != null ? layers[layerGroup][layerName] : null; if (def?.fields == null) { /* skip layers which didn't opt-in for popover */ return false; } if (fetch_body.length === 0) { /* first feature in the list, mark cursor and detached popover as in-progress */ document.body.classList.add('inprogress'); popover?.tip?.classList?.add?.('inprogress'); } fetch_body.push({ layer_group: layerGroup, layer: layerName, fid: feature.getId() ?? -1, }); if (fetch_body.length >= 100) { return true; /* enough matches already, stop detection here */ } }, { hitTolerance: 5, checkWrapped: false, layerFilter: (lyr) => lyr.get('layerGroup') != null, }); if (fetch_body.length === 0) { /* no feature at pixel (or only within layers which didn't opt-in for popover) */ if (popover?.tip != 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(function(resp) { if (resp.status === 200) { return resp.json(); } else { throw new Error(`${resp.url} [${resp.status}]`); } }) .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; if (overlayAttributes.length === 0) { /* couldn't fetch any attribute for feature(s) at pixel */ if (popover?.tip != null) { /* dispose pre-detached popover */ popover.dispose(); } return; } 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?.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]; highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); popover = new Popover(popupOverlay.element, { template: '', title: header, content: formatFeaturePropertiesToHTML(attr), html: true, placement: 'right', fallbackPlacements: ['right', 'left', 'bottom', 'top'], container: CONTAINER_STOPEVENT, }); popover.show(); } else if (popover.tip.classList.contains('popover-detached')) { /* update existing detached mode popover */ refreshPopover(); popover.tip.classList.remove('inprogress'); } }) .catch(function(e) { console.log(e); }) .finally(function() { /* remove in-progress marking on the cursor */ document.body.classList.remove('inprogress'); }); }); return disposePopover; })(); /* age filter dialog */ const ageFilterSetActive = (function() { const panel = document.getElementById('age-filter-modal'); let updateHelpTextTimeoutID = null; const updateHelpTextState = Object.seal({ value: null }); const dialog_setup = (function() { const dialog = document.createElement('div'); dialog.classList.add('modal-dialog', 'modal-dialog-centered'); panel.appendChild(dialog); const content = document.createElement('div'); content.classList.add('modal-content'); dialog.appendChild(content); const header = document.createElement('div'); header.classList.add('modal-header'); content.appendChild(header); const title = document.createElement('div'); title.classList.add('h4', 'm-0'); header.appendChild(title); title.innerHTML = 'Filtrera objekt efter ålder'; 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); btn_close.setAttribute('data-bs-dismiss', 'modal'); header.appendChild(btn_close); const body = document.createElement('div'); body.classList.add('modal-body'); content.appendChild(body); const p = document.createElement('p'); p.classList.add('small', 'text-muted', 'age-filter-infotext'); body.appendChild(p); p.appendChild(document.createTextNode('Här kan du t.ex. filtrera bort gamla objekt. ' + 'I nulaget gäller filtret endast objekt inom ')); ['mineralrättigheter', 'skogsbruk', 'vindbruk'].forEach(function(l, idx, arr) { if (idx > 0) { p.appendChild(document.createTextNode(idx < arr.length-1 ? ', ' : ' eller ')); } const i = document.createElement('i'); i.innerHTML = l; p.appendChild(i); }); p.appendChild(document.createTextNode(' skikter.')); const form = document.createElement('form'); form.action = '#'; form.id = 'age-filter-form'; body.appendChild(form); const btn_group = document.createElement('div'); btn_group.classList.add('btn-group'); btn_group.setAttribute('role', 'group'); btn_group.setAttribute('aria-label', 'Välj filtreringstyp'); form.appendChild(btn_group); const type_choices = Object.fromEntries([ { id: 'relative', text: 'Åldersgräns', desc: 'som är' }, { id: 'interval', text: 'Datumintervall', desc: 'med datum' }, ].map(function(x) { const radio = document.createElement('input'); radio.classList.add('btn-check'); radio.type = 'radio'; radio.name = 'age-filter-type'; radio.id = 'age-filter-type-' + x.id; radio.required = true; radio.setAttribute('aria-expanded', 'false'); radio.setAttribute('autocomplete', 'off'); btn_group.appendChild(radio); const lbl = document.createElement('label'); lbl.classList.add('btn', 'btn-primary'); lbl.setAttribute('for', radio.id); lbl.appendChild(document.createTextNode(x.text)); btn_group.appendChild(lbl); const div = document.createElement('div'); div.classList.add('d-none', 'age-filter-settings'); div.setAttribute('aria-hidden', 'true'); form.appendChild(div); const p = document.createElement('p'); p.classList.add('age-filter-settings-desc'); div.appendChild(p); const t = document.createTextNode('Visa endast objekt ' + x.desc + ':'); p.appendChild(t); return [x.id, { radio: radio, div: div } ]; })); Object.values(type_choices).forEach(function(type_choice) { type_choice.radio.onclick = function(event) { const radio = event.target; radio.checked = true; radio.setAttribute('aria-expanded', 'true'); if (type_choice === type_choices.relative) { type_choice._update_helptext(); } Object.values(type_choices).forEach(function(x) { const isSame = radio.isEqualNode(x.radio); if (isSame) { x.div.classList.remove('d-none'); x.div.setAttribute('aria-hidden', 'false'); } else { x.div.classList.add('d-none'); x.div.setAttribute('aria-hidden', 'true'); x.radio.checked = false; x.radio.setAttribute('aria-expanded', 'false'); } const inputs = x.div.getElementsByTagName('input'); for (let i = 0; i < inputs.length; i++) { inputs[i].required = isSame; } }); }; }); const create_select = function(parentNode, select_label, options) { const select = document.createElement('select'); select.classList.add('form-select'); select.setAttribute('aria-label', select_label); parentNode.appendChild(select); return [ select, Object.fromEntries(options.map(function(x) { const option = document.createElement('option'); option.appendChild(document.createTextNode(x.text)); option.value = x.id; select.appendChild(option); return [x.id, option]; })) ]; }; const format_date = function(d, s) { /* YYYY-MM-DD or YYYYMMDD */ return d.getFullYear() .toString().padStart(4, '0') + (s ?? '-') + (d.getMonth()+1).toString().padStart(2, '0') + (s ?? '-') + d.getDate() .toString().padStart(2, '0'); }; const parse_date = (m) => new Date( /* YYYY-MM-DD */ parseInt(m.slice(0,4), 10), parseInt(m.slice(5,7), 10)-1, parseInt(m.slice(8,10),10), 12 /* use 12:00:00.0 like for URL parameters */ ); (function(type_choice) { const div = document.createElement('div'); div.classList.add('input-group'); type_choice.div.appendChild(div); type_choice.operator = create_select(div, 'Under eller över åldersgränsen', [ { id: '<=', text: 'Nyare' }, { id: '>=', text: 'Äldre' }, ]); const span = document.createElement('span'); span.classList.add('input-group-text'); div.appendChild(span); span.innerHTML = 'än'; const input_quantity = type_choice.quantity = document.createElement('input'); input_quantity.classList.add('form-control'); input_quantity.type = 'number'; input_quantity.min = 0; input_quantity.max = 65535; input_quantity.placeholder = 'Antal'; input_quantity.setAttribute('aria-label', input_quantity.placeholder); div.appendChild(input_quantity); type_choice.unit = create_select(div, 'Enhet', [ { id: 'd', text: 'dagar' }, { id: 'w', text: 'veckor' }, { id: 'm', text: 'månader' }, { id: 'y', text: 'år' }, ]); const p = document.createElement('p'); p.classList.add('small', 'text-muted', 'invisible'); type_choice.div.appendChild(p); p.appendChild(document.createTextNode('Det vill säga med datum ')); const span_date = document.createElement('span'); p.appendChild(span_date); p.appendChild(document.createTextNode(' eller ')); const span_direction = document.createElement('span'); p.appendChild(span_direction); p.appendChild(document.createTextNode('.')); type_choice._update_helptext = function() { const d = ageFilterSettings.getRelativeDate( parseInt(type_choice.quantity.value, 10), type_choice.unit[0].value ); const operator = type_choice.operator[0].value; if (d == null || operator === '') { p.classList.add('invisible'); return null; } else { /* update help text */ span_date.innerHTML = format_date(d); span_direction.innerHTML = { '>=':'tidigare', '<=':'senare' }[operator]; p.classList.remove('invisible'); return d; } }; type_choice.operator[0].onchange = input_quantity.onchange = type_choice.unit[0].onchange = function() { const d = type_choice._update_helptext(); if (d != null) { /* propagate to interval tab */ const operator = type_choice.operator[0].value; const [d1, d2] = { '>=': [new Date(1900, 0, 1, 12), d], /* between 1900-01-01 and d */ '<=': [d, new Date()], /* between d and today */ }[operator]; type_choices.interval.from.value = format_date(d1); type_choices.interval.to.value = format_date(d2); } }; })(type_choices.relative); (function(type_choice) { const div = document.createElement('div'); div.classList.add('input-group'); type_choice.div.appendChild(div); const span1 = document.createElement('span'); span1.classList.add('input-group-text'); span1.innerHTML = 'Mellan'; span1.id = 'age-filter-interval-from'; div.appendChild(span1); const date1 = type_choice.from = document.createElement('input'); date1.classList.add('form-control'); date1.type = 'date'; date1.setAttribute('aria-label', 'Intervallstart'); date1.setAttribute('aria-describedby', span1.id); div.appendChild(date1); const span2 = document.createElement('span'); span2.classList.add('input-group-text'); span2.innerHTML = 'och'; span2.id = 'age-filter-interval-to'; div.appendChild(span2); const date2 = type_choice.to = document.createElement('input'); date2.classList.add('form-control'); date2.type = 'date'; date2.setAttribute('aria-label', 'Intervallstop'); date2.setAttribute('aria-describedby', span2.id); div.appendChild(date2); /* propagate to relative tab by trying to preserve the operator and unit */ const propagate_to_relative = function() { let d; switch (type_choices.relative.operator[0].value) { case '<=': d = parse_date(date1.value); break; case '>=': d = parse_date(date2.value); break; } const delta = (new Date().getTime() - d.getTime()) / 86_400_000; let v; switch (type_choices.relative.unit[0].value) { case 'd': v = delta; break; case 'w': v = delta/7; break; case 'm': v = delta*12/365.25; break; case 'y': v = delta/365.25; break; } if (v != null) { type_choices.relative.quantity.value = Math.round(v); type_choices.relative._update_helptext(); } }; /* make sure that from_date ≤ to_date */ date1.onchange = function() { if (date1.value !== '' && (date2.value === '' || date1.value > date2.value)) { date2.value = date1.value; } propagate_to_relative(); }; date2.onchange = function() { if (date2.value !== '' && (date1.value === '' || date1.value > date2.value)) { date1.value = date2.value; } propagate_to_relative(); }; })(type_choices.interval); const show_unknown_age = (function() { const div = document.createElement('div'); div.classList.add('form-check', 'form-switch'); form.appendChild(div); const checkbox = document.createElement('input'); checkbox.classList.add('form-check-input'); checkbox.type = 'checkbox'; checkbox.id = 'age-filter-show-unknown'; checkbox.setAttribute('role', 'switch'); checkbox.checked = ageFilterSettings.show_unknown; div.appendChild(checkbox); const lbl = document.createElement('label'); lbl.classList.add('form-check-label'); lbl.setAttribute('for', checkbox.id); lbl.innerHTML = 'Visa även objekt med okänd datum.'; div.appendChild(lbl); return checkbox; })(); const footer = document.createElement('div'); footer.classList.add('modal-footer'); content.appendChild(footer); const btn_cancel = document.createElement('button'); btn_cancel.classList.add('btn', 'btn-secondary'); btn_cancel.type = 'button'; btn_cancel.innerHTML = 'Återställ'; footer.appendChild(btn_cancel); const btn_apply = document.createElement('input'); btn_apply.classList.add('btn', 'btn-primary'); btn_apply.type = 'submit'; btn_apply.value = 'Filter'; btn_apply.setAttribute('form', form.id); footer.appendChild(btn_apply); btn_cancel.onclick = function() { /* deactivate deactivate the filter but preserve its settings */ ageFilterSettings.active = false; const params = new URLSearchParams(window.location.hash.substring(1)); params.delete('age-filter'); params.delete('show-unknown-age'); location.hash = '#' + params.toString(); modal.hide(); }; form.onsubmit = function(event) { event.preventDefault(); const [filter_type, filter_settings] = Object.entries(type_choices).filter( (x) => x[1].radio.checked )[0]; let param; ageFilterSettings._min_ts = ageFilterSettings._max_ts = null; switch (filter_type) { case 'relative': { /* save relative value so it can be autoshifted later */ const operator = ageFilterSettings.operator = filter_settings.operator[0].value; ageFilterSettings.quantity = parseInt(filter_settings.quantity.value, 10); ageFilterSettings.unit = filter_settings.unit[0].value; param = {'<=':'-', '>=':''}[operator]; param += ageFilterSettings.quantity.toString() + ageFilterSettings.unit; break; } case 'interval': { const date1 = ageFilterSettings.from = parse_date(filter_settings.from.value); const date2 = ageFilterSettings.to = parse_date(filter_settings.to.value); param = format_date(date1, '') + '-' + format_date(date2, ''); break; } default: return; } ageFilterSettings.type = filter_type; ageFilterSettings.show_unknown = show_unknown_age.checked; ageFilterSettings.active = true; const params = new URLSearchParams(window.location.hash.substring(1)); params.set('age-filter', param); params.set('show-unknown-age', show_unknown_age.checked ? '1' : '0'); location.hash = '#' + params.toString(); modal.hide(); }; const updateHelpTextTimeout = function cb() { const type_choice = type_choices.relative; const [ms, b] = getDelay(updateHelpTextState); if (type_choice.radio.checked && b) { type_choice._update_helptext(); } updateHelpTextTimeoutID = setTimeout(cb, ms); }; /* Now that all elements have been added to the form, click the radio * button to show the relevant
, mark its fields as required and * fills them. The function is run whenever the modal is shown. */ return function() { const type_choice = type_choices[ageFilterSettings.type]; type_choice.radio.click(); switch (ageFilterSettings.type) { case 'relative': { Object.entries(type_choice.operator[1]).map(function([id, option]) { option.selected = id === ageFilterSettings.operator; }); type_choice.quantity.value = ageFilterSettings.quantity.toString(); Object.entries(type_choice.unit[1]).map(function([id, option]) { option.selected = id === ageFilterSettings.unit; }); type_choice.quantity.dispatchEvent(new Event('change')); /* propagate to absolute */ break; } case 'interval': { type_choice.from.value = format_date(ageFilterSettings.from); type_choice.to.value = format_date(ageFilterSettings.to); type_choice.from.dispatchEvent(new Event('change')); /* propagate to relative */ break; } } if (updateHelpTextTimeoutID == null) { const ms = getDelay(updateHelpTextState)[0]; updateHelpTextTimeoutID = setTimeout(updateHelpTextTimeout, ms); } }; })(); /* Retun the number of milliseconds left for the next full 15min at the * wall clock, along with a boolean indicating whether we just passed * midnight. * Keep using setTimeout(,15min) since unlike setInterval() it allows us to avoid clock * skews by adjusting delays */ const getDelay = function(state) { const i = 900, d = new Date(); /* at *:00,15,30,45:00.50 */ const sec = (d.getHours()*60 + d.getMinutes())*60 + d.getSeconds(); /* add 50ms to ensure formatting the date doesn't end up on the previous period */ const ms = (Math.floor(sec/i)*i + i - sec)*1000 - d.getMilliseconds() + 50; const v = state.value, v2 = d.getDate(); const b = v == null || v !== v2; if (b) { state.value = v2; } return [ms, b]; }; const modal = new Modal(panel, { backdrop: false, }); const backdrop = document.getElementById('age-filter-modal-backdrop'); backdrop.onclick = function() { modal.hide(); }; const btn = document.getElementById('age-filter-button').getElementsByTagName('button')[0]; if (ageFilterSettings.active) { btn.classList.replace('btn-light', 'btn-dark'); } 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() { if (!ageFilterSettings.active) { btn.classList.replace('btn-dark', 'btn-light'); } if (updateHelpTextTimeoutID != null) { clearTimeout(updateHelpTextTimeoutID); updateHelpTextState.value = null; updateHelpTextTimeoutID = null; } btn.setAttribute('aria-expanded', 'false'); backdrop.classList.remove('modal-backdrop', 'show'); }); btn.onclick = function() { disposePopover(); dialog_setup(); modal.show(); }; return (function() { /* setter for ageFilterSettings.active */ let timeoutID = null; const state = { value: null }; const fun = function cb() { const [ms, b] = getDelay(state); if (b && ageFilterSettings.type === 'relative') { ageFilterSettings.setupMinMax(); changed(); } timeoutID = setTimeout(cb, ms); }; const changed = function() { Object.values(mapLayers) .filter((lyr) => lyr?.get('canFilterByAge')) .forEach((lyr) => lyr.changed()); }; const setter = function(active) { if (ageFilterSettings._active !== active) { if (active) { ageFilterSettings.setupMinMax(); } ageFilterSettings._active = active; changed(); } if (active && ageFilterSettings.type === 'relative') { if (timeoutID == null) { timeoutID = setTimeout(fun, getDelay(state)[0]); } } else if (timeoutID != null) { clearTimeout(timeoutID); state.value = null; timeoutID = null; } }; /* initial activation from param URL */ setter(ageFilterSettings._active); return setter; })(); })();