diff options
Diffstat (limited to 'main.js')
| -rw-r--r-- | main.js | 8019 |
1 files changed, 5099 insertions, 2920 deletions
@@ -18,8 +18,10 @@ 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'; @@ -34,8 +36,15 @@ 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 VectorLayer from 'ol/layer/Vector.js'; import VectorSource from 'ol/source/Vector.js'; +import Draw from 'ol/interaction/Draw.js'; +import { unByKey } from 'ol/Observable.js'; import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; @@ -44,6 +53,8 @@ import RegularShape from 'ol/style/RegularShape.js'; import Stroke from 'ol/style/Stroke.js'; import Style from 'ol/style/Style.js'; +import Geolocation from 'ol/Geolocation.js'; + import proj4 from 'proj4'; import { get as getProjection } from 'ol/proj.js'; import { register as registerProjection } from 'ol/proj/proj4.js'; @@ -51,12 +62,14 @@ import { register as registerProjection } from 'ol/proj/proj4.js'; import { Modal, Popover } from 'bootstrap'; import './style.css'; +"use strict"; 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'); +const PROJECTION = getProjection('EPSG:3006'); +const LOCALE = 'sv-SE'; /* Lantmäteriet uses a tile-scheme where the origin (upper-left corner) is at * N8500000 E-1200000 (SWEREF99 TM), where each tile is 256×256 pixels, and where @@ -69,7 +82,7 @@ const projection = getProjection('EPSG:3006'); * 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]; +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 @@ -87,82 +100,93 @@ const extent = [110720, 6927136, 1159296, 7975712]; * 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 baseMapSource = new WMTS({ - url: undefined, - 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, -}); - -let baseMapLayer = 'topowebb'; -(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)) { - view.setCenter([x, y]); - } - const z = parseFloat(params.get('z')); - if (!isNaN(z)) { - view.setZoom(z); - } - - if (params.has('basemap')) { - baseMapLayer = params.get('basemap'); - } - baseMapSource.setUrl(`https://minkarta.lantmateriet.se/map/topowebbcache?LAYER=${encodeURIComponent(baseMapLayer)}`); -})(); - - -const map = new Map({ - controls: [], - view: view, - layers: [ - new TileLayer({ - source: baseMapSource +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], }), - ], - target: document.getElementById('map'), -}); + projection: PROJECTION, + wrapX: false, + crossOrigin: 'anonymous', + }); -const popup = document.getElementById('popup'); + const view = new View({ + projection: PROJECTION, + extent: EXTENT, + showFullExtent: true, + 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 = document.getElementById('map-control-container'); +const CONTAINER_MAP = document.getElementById('map-control-container'); +const CONTAINER_STOPEVENT = MAP.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; (function() { - const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; - container0.appendChild(document.getElementById('zoom-control')); - container0.appendChild(container); - container0.appendChild(document.getElementById('modal-info')); - - const backdrop = document.createElement('div'); - container0.appendChild(backdrop); - backdrop.id = 'modal-info-backdrop'; + CONTAINER_STOPEVENT.appendChild(document.getElementById('zoom-control')); + CONTAINER_STOPEVENT.appendChild(CONTAINER_MAP); + CONTAINER_STOPEVENT.appendChild(document.getElementById('info-modal')); + CONTAINER_STOPEVENT.appendChild(document.getElementById('help-modal')); + + const info_backdrop = document.createElement('div'); + CONTAINER_STOPEVENT.appendChild(info_backdrop); + info_backdrop.id = 'info-modal-backdrop'; + + const help_backdrop = document.createElement('div'); + CONTAINER_STOPEVENT.appendChild(help_backdrop); + help_backdrop.id = 'help-modal-backdrop'; + + const age_filter = document.createElement('div'); + age_filter.id = 'age-filter-modal'; + age_filter.classList.add('modal'); + 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 */ @@ -185,7 +209,7 @@ const container = document.getElementById('map-control-container'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } - map.addControl(control); + MAP.addControl(control); })(); /* zoom slider */ @@ -197,29 +221,29 @@ const container = document.getElementById('map-control-container'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } - map.addControl(control); + MAP.addControl(control); })(); /* scale line */ (function() { - const size = map.getSize(); + const size = MAP.getSize(); const control = new ScaleLine({ units: 'metric', minWidth: 150, maxWidth: size[1] < 350 ? size[1] - 50 : 350, - target: container, + target: CONTAINER_MAP, }); control.element.classList.add('modal', 'modal-content'); - map.addControl(control); + MAP.addControl(control); })(); -const menu = document.getElementById('map-menu'); +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); + MENU.appendChild(div); div.classList.add('ol-unselectable', 'ol-control'); const btn = document.createElement('button'); @@ -234,13 +258,13 @@ if (window.location !== window.parent.location) { btn.appendChild(i); i.classList.add('bi', 'bi-box-arrow-up-right'); - btn.onclick = function(event) { - const coordinates = view.getCenter(); + 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', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); + searchParams.set('z', MAP.getView().getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); url.hash = '#' + searchParams.toString(); return window.open(url.href, '_blank'); }; @@ -248,79 +272,53 @@ if (window.location !== window.parent.location) { /* layer selection button and legend */ if (window.location === window.parent.location) { - const btn = (function() { - const div = document.createElement('div'); - menu.appendChild(div); - div.id = 'layer-selection-button'; - div.classList.add('ol-unselectable', 'ol-control'); - - const btn = document.createElement('button'); - div.appendChild(btn); - btn.type = 'button'; - btn.title = 'Lagerval'; - 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-stack'); - - return btn; - })(); - - const btn2 = (function() { - const div = document.createElement('div'); - menu.appendChild(div); - div.id = 'map-legend-button'; - div.classList.add('ol-unselectable', 'ol-control'); - - const btn = document.createElement('button'); - div.appendChild(btn); - btn.type = 'button'; - btn.title = 'Teckenförklaring'; - 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-list-task'); - - return btn; - })(); - - const panel = document.getElementById('layer-selection-panel'); - btn.onclick = function(event) { - if (btn.getAttribute('aria-expanded') === 'true') { - panel.setAttribute('aria-hidden', 'true'); + const buttons = Object.fromEntries([ + {id: 'layer-selection', title: 'Lagerval', bi: 'stack'}, + {id: 'map-legend', title: 'Teckenförklaring', bi: 'list-task'}, + {id: 'measure', title: 'Mät i kartan', bi: 'rulers'}, + {id: 'age-filter', title: 'Filtrera objekt efter ålder', bi: 'clock-history'}, + ].map(function(x) { + const div = document.createElement('div'); + 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.replace('btn-dark', 'btn-light'); - } else { - if (btn2.getAttribute('aria-expanded') === 'true') { - btn2.click(); - } - panel.setAttribute('aria-hidden', 'false'); - btn.setAttribute('aria-expanded', 'true'); - btn.classList.replace('btn-light', 'btn-dark'); - } - }; - - const panel2 = document.getElementById('map-legend-panel'); - btn2.onclick = function(event) { - if (btn2.getAttribute('aria-expanded') === 'true') { - panel2.setAttribute('aria-hidden', 'true'); - btn2.setAttribute('aria-expanded', 'false'); - btn2.classList.replace('btn-dark', 'btn-light'); - } else { - if (btn.getAttribute('aria-expanded') === 'true') { - btn.click(); - } - panel2.setAttribute('aria-hidden', 'false'); - btn2.setAttribute('aria-expanded', 'true'); - btn2.classList.replace('btn-light', 'btn-dark'); + 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 */ @@ -341,20 +339,17 @@ if (window.location === window.parent.location) { labelActive: labelActive, tipLabel: titleInactive, keys: true, - target: menu, - }) + target: MENU, + }); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.add('btn', classInactive); btn.setAttribute('aria-label', btn.title); - map.addControl(control); + MAP.addControl(control); + control.element.id = 'fullscreen-toggle'; /* for the help dialog */ control.addEventListener('enterfullscreen', function() { - featureOverlayLayer.setVisible(false); - const popover = Popover.getInstance(popup); - if (popover !== null) { - /* dispose popover as entering fullscreen messes up its position */ - popover.dispose(); - } + /* dispose popover as entering fullscreen messes up its position */ + disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classInactive, classActive); @@ -366,14 +361,10 @@ if (window.location === window.parent.location) { /* hide export button in fullscreen mode as it exits it */ exp.classList.add('d-none'); } - }) + }); control.addEventListener('leavefullscreen', function() { - featureOverlayLayer.setVisible(false); - const popover = Popover.getInstance(popup); - if (popover !== null) { - /* dispose popover as is might overflow the viewport */ - popover.dispose(); - } + /* dispose popover as is might overflow the viewport */ + disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classActive, classInactive); @@ -384,7 +375,7 @@ if (window.location === window.parent.location) { if (exp !== undefined) { exp.classList.remove('d-none'); } - }) + }); } /* export/download button */ @@ -403,17 +394,17 @@ if (window.location === window.parent.location) { const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-download'); - menu.appendChild(div); + MENU.appendChild(div); - btn.onclick = function(event) { - map.once('rendercomplete', function() { + btn.onclick = function() { + MAP.once('rendercomplete', function() { const canvas0 = document.createElement('canvas'); - const size = map.getSize(); + 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) { + 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); @@ -434,2474 +425,2637 @@ if (window.location === window.parent.location) { }); }); - map.renderSync(); + MAP.renderSync(); }; } -/* info button */ +/* info and help buttons */ (function() { - const div = document.createElement('div'); - menu.appendChild(div); - div.id = 'info-button'; - div.classList.add('ol-unselectable', 'ol-control'); + const add_button = function(x) { + const div = document.createElement('div'); + MENU.appendChild(div); + div.id = x.id + '-button'; + div.classList.add('ol-unselectable', 'ol-control'); - const btn = document.createElement('button'); - div.appendChild(btn); - btn.type = 'button'; - btn.setAttribute('aria-expanded', 'false'); - btn.title = 'Visa information'; - btn.setAttribute('aria-label', btn.title); - btn.classList.add('btn', 'btn-light'); + const btn = document.createElement('button'); + div.appendChild(btn); + btn.type = 'button'; + btn.setAttribute('aria-expanded', 'false'); + btn.title = x.title; + btn.setAttribute('aria-label', btn.title); + btn.classList.add('btn', 'btn-light'); - const i = document.createElement('i'); - btn.appendChild(i); - i.classList.add('bi', 'bi-info-lg'); + const i = document.createElement('i'); + btn.appendChild(i); + i.classList.add('bi', 'bi-' + x.bi); - const panel = document.getElementById('modal-info'); - const modal = new Modal(panel, { - backdrop: false, - }); + const panel = document.getElementById(x.id + '-modal'); + const modal = new Modal(panel, { + backdrop: false, + }); - const backdrop = document.getElementById('modal-info-backdrop'); - backdrop.onclick = function(event) { - modal.hide(); - }; + const backdrop = document.getElementById(x.id + '-modal-backdrop'); + backdrop.onclick = function() { + modal.hide(); + }; - panel.addEventListener('show.bs.modal', function() { - backdrop.classList.add('modal-backdrop', 'show'); - btn.setAttribute('aria-expanded', 'true'); - btn.classList.replace('btn-light', 'btn-dark'); - }); - panel.addEventListener('hidden.bs.modal', function() { - btn.classList.replace('btn-dark', 'btn-light'); - btn.setAttribute('aria-expanded', 'false'); - backdrop.classList.remove('modal-backdrop', 'show'); - }); + panel.addEventListener('show.bs.modal', function() { + backdrop.classList.add('modal-backdrop', 'show'); + btn.setAttribute('aria-expanded', 'true'); + btn.classList.replace('btn-light', 'btn-dark'); + }); + panel.addEventListener('hide.bs.modal', function() { + /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */ + const activeElement = document.activeElement; + if (activeElement instanceof HTMLElement) { + activeElement.blur(); + } + }); - btn.onclick = function(event) { - modal.toggle(); - }; -})(); + panel.addEventListener('hidden.bs.modal', function() { + btn.classList.replace('btn-dark', 'btn-light'); + btn.setAttribute('aria-expanded', 'false'); + backdrop.classList.remove('modal-backdrop', 'show'); + }); -/* we're all set, show the control container now */ -container.setAttribute('aria-hidden', 'false'); + btn.onclick = function() { + modal.show(); + }; -view.on('change', function(event) { - featureOverlayLayer.setVisible(false); - const popover = Popover.getInstance(popup); - if (popover !== null) { - popover.dispose(); - } + /* de-obfuscate email address */ + const CLASSNAME = 'email-address-b64'; + const ATTRNAME = 'data-mailto-b64'; + for (const a of panel.getElementsByClassName(CLASSNAME)) { + if (a.tagName.toLowerCase() === 'a' && a.hasAttribute(ATTRNAME)) { + let href = 'mailto:'; + for (const part of a.getAttribute(ATTRNAME).split(/\s+/)) { + switch (part) { + case '__AT__': + href += '@'; + break; + case '__DOT__': + href += '.'; + break; + default: + href += atob(part); + } + } + a.classList.remove(CLASSNAME); + a.removeAttribute(ATTRNAME); + a.href = href; + } + } - 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(); -}); + return [panel, btn, modal]; + }; + /* info button */ + (function() { + const [panel, btn, modal] = add_button({ + id: 'info', + title: 'Källor och licensinformation', + bi: 'info-lg', + }); -/* TODO: this should really be refactored… */ -const layers = { - mrr_appr_ec: { - popoverTitle: 'Bearbetningskoncession \u2013 beviljad', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Ägare', 'owners'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Giltig från', 'validfrom'], - ['Giltig till', 'validto'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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: { - popoverTitle: 'Bearbetningskoncession \u2013 ansökt', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Sökande', 'owners'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Ansökningsdatum', 'appl_date'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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: { - popoverTitle: 'Undersökningstillstånd, metaller och industrimineral \u2013 beviljad', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Ägare', 'owners'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Giltig från', 'validfrom'], - ['Giltig till', 'validfrom'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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: { - popoverTitle: 'Undersökningstillstånd, metaller och industrimineral \u2013 ansökt', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Sökande', 'owners'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Ansökningsdatum', 'appl_date'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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: { - popoverTitle: 'Undersökningstillstånd, olja, gas och diamant \u2013 beviljad', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Ägare', 'owners'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Giltig från', 'validfrom'], - ['Giltig till', 'validto'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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: { - popoverTitle: 'Undersökningstillstånd, olja, gas och diamant \u2013 ansökt', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Sökande', 'owners'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Ansökningsdatum', 'appl_date'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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: { - popoverTitle: 'Markanvisning till koncession', - popover: [ - ['Namn', 'name'], - ['Tillhörande bearbetnings\u00ADkoncession(er)', 'conc_name'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - 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], - }), + panel.addEventListener('hidden.bs.modal', function() { + infoMetadataAccordions.forEach(function(x, idx) { + /* collapse all accordions */ + const body = x.element.parentNode.parentNode; + const name = 'info-accordion-collapse-' + idx; + if (body.id === name) { + body.classList.remove('show'); + } + if (body.parentNode !== null) { + const headers = body.parentNode.getElementsByClassName('accordion-header'); + for (let i = 0; i < headers.length; i++) { + const buttons = headers[i].getElementsByClassName('accordion-button'); + for (let j = 0; j < buttons.length; j++) { + const btn = buttons[j]; + if (btn.getAttribute('data-bs-target') === '#' + name) { + btn.setAttribute('aria-expanded', 'false'); + btn.classList.add('collapsed'); + } + } + } + } + }); + }); + + const dateFormatter = new Intl.DateTimeFormat(LOCALE); + btn.onclick = function() { + infoMetadataAccordions.forEach((x) => x.element.replaceChildren()); + modal.show(); + Promise.allSettled(Object.entries(mapLayers).map(function([grp,lyr]) { + const baseurl = lyr?.getSource?.()?.get?.('baseurl'); + if (baseurl == null) { + return new Promise(() => { throw new Error(`Unknown source for "${grp}"`); }); + } + return fetch(new URL('metadata.json', baseurl)) + .then(function(resp0) { + if (resp0.status === 200) { + return resp0.json().then((x) => [grp,x]); + } else { + throw new Error(`${resp0.url} [${resp0.status}]`); + } + }); + })) + .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) => dateFormatter.format(new Date(ts))) + .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 td = document.createTextNode(dateFormatter.format(new Date(x.last_modified))); + p.appendChild(td); + const t2 = document.createTextNode('.'); + p.appendChild(t2); + } + }); + }); + }); }); - }), + }; + })(); + + /* help button */ + (function() { + const [panel] = add_button({ + id: 'help', + title: 'Hjälp med att använda kartan', + bi: 'question-circle', + }); + + /* Use the text from the .html file but ensure that buttons are + * listed in the same order as the menu, and spell titles out. This + * avoids duplication and avoids that things would get out of sync */ + const button_map = {}; + const ol = panel.querySelector('#help-describe-functions'); + if (ol != null && ol.tagName.toLowerCase() === 'ol') { + for (const li of ol.children) { + const id = li.getAttribute('data-for-button'); + if (id == null || id === '') { + continue; + } + button_map[id] = li; + } + } + + for (const node of MENU.children) { + if (node.id == null || node.id === '') { + continue + } + const btn = node.getElementsByTagName('button')[0]; + if (btn == null || btn.tagName.toLowerCase() !== 'button') { + continue; + } + const btn2 = btn.cloneNode(true); + const title = btn2.title; + btn2.id = btn2.title = ''; + for (const attr of btn.attributes) { + if (attr.name.toLowerCase().startsWith('aria-') || attr.name === 'id' || attr.name === 'title') { + btn2.removeAttribute(attr.name); + } + } + + const h = document.createElement('h6'); + h.classList.add('help-button-description'); + h.appendChild(btn2); + if (title != null && title != '') { + const t = document.createTextNode(title) + h.appendChild(t); + } + btn2.classList.add('help-button'); + + ol.insertAdjacentElement('beforebegin', h); + + const li = button_map[node.id]; + if (li != null) { + /* move <li>'s children (paragraphs) to the main text */ + while (li.children.length > 0) { + ol.insertAdjacentElement('beforebegin', li.firstElementChild); + } + } + } + ol.remove(); + })(); +})(); + +/* we're all set, show the control container now */ +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, but probably not worth doing before the switch to + * WebGL, which is currently blocking on https://github.com/openlayers/openlayers/issues/15807 + * and https://github.com/openlayers/openlayers/issues/16246 */ +const LAYERS = Object.seal({ + adm: { + lansyta: { + legend: { zoomLevel: 3, type: 'linestring' }, + style: [1.5, 2, 3, 3, 4, 4, 6, 6, 8, 8, 10, 10].map(function(width) { + return new Style({ + zIndex: 0, + fill: null, + stroke: new Stroke({ + width: width, + color: [212, 147, 208, 1], + }), + }); + }), + }, + kommunyta: { + legend: { zoomLevel: 3, type: 'linestring' }, + style: [2, 2, 3, 3, 4, 4, 6, 6, 8, 8, 10, 10].map(function(width) { + return new Style({ + zIndex: 0, + fill: null, + stroke: new Stroke({ + width: width/2, + color: [212, 147, 208, 1], + }), + }); + }), + }, }, - svk_ledningar: { - popoverTitle: 'Kraftledning (befintlig)', - popover: [ - ['Förläggning', 'Placement'], - ['Spänning', 'Voltage', { unit: 'kV' }], - ['Ledlängd', 'geom_length', { fn: 'length' }], - ], - 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, - }), - }); - }), + 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], + }), + }); + }), + }, + 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, + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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, + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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, + }), + }); + }), + }, + 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_stolpar: { - style: [undefined, undefined, undefined, undefined, undefined] - .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { + + 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: 51, - image: new CircleStyle({ + zIndex: 52, + stroke: new Stroke({ + color: 'black', + width: width, + }), + }); + }), + }, + 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', + }), + }), + }); + })), + }, + 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], + }), + }); + }), + }, + 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', }), }), }); - })), - }, - svk_transmissionsnatsprojekt: { - popoverTitle: 'Transmissionsnätsprojekt', - popover: [ - ['Projektnamn', 'Name'], - ['Spänning', 'Voltage', { unit: 'kV' }], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - 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: { - 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, + }) + .concat([.5, 1, 1.5, 2, 2].map(function(width) { + return new Style({ + zIndex: 50, fill: new Fill({ - color: 'black', + color: 'rgba(128, 128, 128, .7)', }), - }), - }); - }) - .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)', - }), - }); - })), + stroke: new Stroke({ + width: width, + color: 'rgb(0, 0, 0)', + }), + }); + })), + }, }, - vbk_area_current: { - popoverTitle: 'Projekteringsområde för vindbruk', - popover: [ - ['Projektnamn', 'Projektnamn'], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Aktuella verk', 'AntalVerk'], - ['Antal ej koordinatsatta verk', 'AntalEjXY'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Planerad byggstart', 'PlaneradByggstart'], - ['Planerat drifttagande', 'PlaneratDrift'], - ['Andringsansokan', 'AndringsansokanPagar'], - ['Under Byggnation', 'UnderByggnation'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Senast uppdaterat', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Projekteringsområde för vindbruk \u2013 ej aktuell', - popover: [ - ['Projektnamn', 'Projektnamn'], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Aktuella verk', 'AntalVerk'], - ['Antal ej koordinatsatta verk', 'AntalEjXY'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Planerad byggstart', 'PlaneradByggstart'], - ['Planerat drifttagande', 'PlaneratDrift'], - ['Andringsansokan', 'AndringsansokanPagar'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Senast uppdaterat', 'SenasteUppdaterat'], - ], - 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_station_completed: { - popoverTitle: 'Vindkraftverk \u2013 uppfört', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Uppförandedatum', 'Uppfort'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Vindkraftverk \u2013 handlagt', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Vindkraftverk \u2013 beviljat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Vindkraftverk \u2013 inte längre aktuell/återkallat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Vindkraftverk \u2013 avslagit/nekat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Vindkraftverk \u2013 nedmonterat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Uppförandedatum', 'Uppfort'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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: { - popoverTitle: 'Vindkraftverk \u2013 överklagat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - 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, - }), - }); - }), + 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], + }), + }); + }), + }, + 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, + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + turbine_completed: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-completed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_processed: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-processed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_approved: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-approved.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_revoked: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-revoked.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_rejected: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-rejected.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_dismounted: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-dismounted.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_appealed: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + 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 - * */ - sks_clearcut_comp: { - popoverTitle: 'Utförd avverkning', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Registeringsår', 'ArendeAr'], - ['Skogstyp', 'Skogstyp'], - ['Areal anmält', 'AnmaldHa', { unit: 'ha' }], - ['Areal naturlig föryngring', 'NatforHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - //['Areal plantering', 'SkogsodlHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - ['Avverkningstyp', 'Avverktyp'], - ['Datum för avverkning', 'Avvdatum'], - ['Ursprung för datum för avverkning', 'KallaDatum'], - //['Ursprung för areal avverkning', 'KallaAreal'], - //['Kommun', 'Kommun'], - //['Län', 'Lan'], - ['Areal för ytan', 'geom_area', { fn: 'area' }], - ], - 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 - * */ - sks_clearcut_appl: { - popoverTitle: 'Avverkningsanmälansområde', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Inkom datum', 'Inkomdatum'], - ['Registeringsår', 'ArendeAr'], - //['Skogstyp', 'Skogstyp'], - ['Areal anmält', 'AnmaldHa', { unit: 'ha' }], - ['Areal naturlig föryngring', 'NatforHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - ['Areal plantering', 'SkogsodlHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - ['Avverkningssäsong', 'AvvSasong'], - //['Avverkningstyp', 'Avverktyp'], - //['Ändamål', 'Andamal'], - //['Kommun', 'Kommun'], - //['Län', 'Lan'], - ['Ärendestatus', 'ArendeStatus'], - ['Avverkad areal', 'AvvHa', { unit: 'ha' }], - ], - 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, - }), - }); - }), + avverk: { + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf + * */ + 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 + * */ + 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, + }), + }); + }), + }, }, - nvr_tilltradesforbud: { - popoverTitle: 'Tillträdesförbud', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Föreskriftsområde', 'FORSKRNAMN'], - ['Namn', 'OBJEKTNAMN'], - ['Beslutsstatus', 'BESLSTAT'], - ['Föreskriftstyp', 'FORESKRTYP'], - ['Föreskriftssubtyp', 'FORESKRIFT'], - ['Från datum', 'FRANDATUM'], - ['Till datum', 'TILLDATUM'], - ['Beskrivning', 'BESKRIVN'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width, z) { - 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], - }), - }); - }), - }, - nvr_nationalpark: { - popoverTitle: 'Nationalpark', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_naturreservat: { - popoverTitle: 'Naturreservat', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_naturreservat_kommunalt: { - popoverTitle: 'Kommunalt naturreservat', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_naturvardsomrade: { - popoverTitle: 'Naturvårdsområde', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_djur_och_vaxtskyddsomrade: { - popoverTitle: 'Djur- och växtskyddsområde', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_kulturreservat: { - popoverTitle: 'Kulturreservat', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_vattenskyddsomrade: { - popoverTitle: 'Vattenskyddsområden', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ikraftträdandedatum föreskrifter', 'IKRAFTDATF'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Tillsynsmyndighet', 'TILLSYNSMH'], - ['Prövningsmyndighet tillstånd', 'PROVNMHTIL'], - ['Prövningsmyndighet dispens', 'PROVNMHDIS'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_landskapsbildsskyddsomrade: { - popoverTitle: 'Landskapsbildsskyddsområde', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_skogligt_biotopskyddsomrade: { - popoverTitle: 'Biotopskydd i skogsmark', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Biotopkategori', 'Biotyp'], - ['Skogstyp', 'Naturtyp'], - ['Registeringsår', 'ArendeAr'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Skogsmarksareal', 'AreaProd', { unit: 'ha' }], - ['Beslutsdatum', 'Datbeslut'], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - 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 < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [134, 90, 71, 1], - }), - }); - }), - }, - nvr_ovrigt_biotopskyddsomrade: { - popoverTitle: 'Biotopskydd utanför skogsmark', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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 < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 95, 0, 1], - }), - }); - }), - }, - nvr_naturminne_yta: { - popoverTitle: 'Naturminne (yta)', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_naturminne_punkt: { - popoverTitle: 'Naturminne (punkt)', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [undefined, undefined, undefined, undefined].concat([3, 4, 6, 8, 12, 16, 20, 24].map(function(width, z) { - return new Style({ - zIndex: 12, - image: new CircleStyle({ - radius: width, + 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], + }), + }); + }), + }, + 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: 'rgba(113, 0, 116, .5)', + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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)', + }), + }), + }); + })) + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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: Math.log2(width)/2, - color: 'rgba(113, 0, 116, 1)', + width: 1.5, + color: [255, 115, 0, 1], }), - }), - }); - })) - }, - nvr_interimistiskt_forbud: { - popoverTitle: 'Interimistiskt förbud', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - 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], - }), - }); - }), - }, - nvr_fageldirektivet: { - popoverTitle: 'Fågeldirektivet (SPA)', - popover: [ - ['Områdeskod', 'SITE_CODE', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Områdestyp', 'OMRADESTYP'], - ['Uppgiftslämnare', 'UPPLAMNARE'], - ['SPA-datum', 'SPA_DATUM'], - ['SCI-förslagsdatum', 'SCI_FORSL'], - ['SCI-datum', 'SCI_DATUM'], - ['SAC-datum', 'SAC_DATUM'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Kvalitet', 'KVALITET'], - ['Kännetecken för området', 'KARAKTAR'], - ['Arter', 'ARTER'], - ['Naturtyper', 'NATURTYPER'], - ['Bevarandeplan', 'BEVPLAN', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - 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], - }), - }); - }), - }, - nvr_habitatdirektivet: { - popoverTitle: 'Art- och habitatdirektivet (SCI)', - popover: [ - ['Områdeskod', 'SITE_CODE', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Områdestyp', 'OMRADESTYP'], - ['Uppgiftslämnare', 'UPPLAMNARE'], - ['SPA-datum', 'SPA_DATUM'], - ['SCI-förslagsdatum', 'SCI_FORSL'], - ['SCI-datum', 'SCI_DATUM'], - ['SAC-datum', 'SAC_DATUM'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Kvalitet', 'KVALITET'], - ['Kännetecken för området', 'KARAKTAR'], - ['Arter', 'ARTER'], - ['Naturtyper', 'NATURTYPER'], - ['Bevarandeplan', 'BEVPLAN', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - 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], - }), - }); - }), - }, - nvr_helcom: { - popoverTitle: 'Marina skyddade områden (Helcom MPA)', - popover: [ - ['Namn', 'NAME'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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)'; - let 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], - }), - }); - }), - }, - nvr_ramsar: { - popoverTitle: 'Ramsar-områden (Våtmarkskonventionen)', - popover: [ - ['Ramsar-ID', 'RAMSAR_ID', { classes: ['feature-objid'] }], - ['Skyddstyp', 'SKYDDSTYP'], - ['Namn', 'NAMN'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ['Ursprungligt beslutsdatum', 'URSPR_BESL'], - ['Senaste beslutsdatum', 'SEN_BESLUT'], - ['Rättsakt', 'LEGAL_ACT'], - ['Länk', 'LINK', { fn: function(v) { - if (v === undefined || v === null || v === '') { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - 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)'; - let 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], - }), - }); - }), - }, - nvr_ospar: { - popoverTitle: 'Marina skyddade områden (Ospar MPA)', - popover: [ - ['Ursprung', 'ORIGIN'], - ['N2000-namn', 'NAMN_N2000'], - ['MPA-ID', 'MPA_ID', { classes: ['feature-objid'] }], - ['MPA-namn', 'MPA_NAMN'], - ['N2000-ID', 'N2000_SITE', { classes: ['feature-objid'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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)'; - let 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], - }), - }); - }), - }, - nvr_varldsarv: { - popoverTitle: 'Världsarv med mycket höga naturvärden (Unesco)', - popover: [ - ['Namn', 'NAMN'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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)'; - let 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], - }), - }); - }), - }, - nvr_biosfarsomraden: { - popoverTitle: 'Biosfärsområde (Unesco)', - popover: [ - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Länk', 'LINK', { fn: function(v) { - if (v === undefined || v === null || v === '') { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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)'; - let 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], - }), - }); - }), - }, - nvr_naturvardsavtal: { - popoverTitle: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', - popover: [ - ['ID', 'ID', { classes: ['feature-objid'] }], - ['Namn', 'OBJNAMN'], - ['Fastighet', 'FASTBET', { classes: ['feature-objid'] }], - ['Giltig från', 'DATSTART'], - ['Giltig till', 'DATSLUT'], - ['Diarienummer', 'DIARIENRNV', { classes: ['feature-attr-dnr'] }], - ['Satus', 'STATUS'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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 < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 0, 197, 1], - }), - }); - }), - }, - nvr_naturvardsavtal_skogsstyrelsen: { - popoverTitle: 'Naturvårdsavtal (Skogsstyrelsen)', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Registeringsår', 'ArendeAr'], - ['Biotopkategori', 'NvaTyp'], - ['Skogstyp', 'Naturtyp'], - ['Avtalsdatum', 'DatAvtal'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Skogsmarksareal', 'AreaProd', { unit: 'ha' }], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Undertyp', 'Undertyp'], - ], - 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 < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 0, 197, 1], - }), - }); - }), + }); + })), + }, }, - ri_naturvard: { - popoverTitle: 'Riksintresse naturvård', - popover: [ - ['Namn', 'NAMN'], - ['Skydd', 'SKYDD'], - ['Ämnesområde', 'AMNESOMRAD'], - ['Beskrivning', 'BESKRIVNIN', { fn: function(v) { - if (v === undefined || v === null || !(v.startsWith('http://') || v.startsWith('https://'))) { - return v; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Lagrum', 'LAGRUM'], - ['Beslutsdatum', 'BESLUTSDAT'], - ['Original-ID', 'ORGINALID', { classes: ['feature-objid'] }], - ['Riks-ID', 'RIKSID', { classes: ['feature-objid'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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: { - popoverTitle: 'Riksintresse friluftsliv', - popover: [ - ['Namn', 'NAMN'], - ['Skydd', 'SKYDD'], - ['Ämnesområde', 'AMNESOMR'], - ['Områdesnummer', 'OMRADESNR', { classes: ['feature-objid'] }], - ['Länk värdebeskrivning', 'LANK_VARDE', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Lagrum', 'LAGRUM'], - ['Beslutsdatum', 'BESLDATUM'], - ['Ärendenummer', 'ARENDENR', { classes: ['feature-attr-dnr'] }], - ['Länk, beslut', 'LANK_BESLU', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Aktivitet', 'AKTIVITET'], - ['Naturtyp', 'NATURTYP'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'AREA_LAND_', { unit: 'ha' }], - ['Areal vatten', 'AREA_VATTE', { unit: 'ha' }], - ], - 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: { - popoverTitle: 'Riksintresse rörligt friluftsliv (MB 4 kap 1§ och 2§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - //['Metodbeskrivning', 'METODBESKR'], - //['Tillk. datum', 'TILLKDATUM'], - //['Rev. datum', 'REVDATUM'], - ['Anmärkning', 'ANM'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - 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: { - popoverTitle: 'Riksintresse obruten kust (MB 4 kap 3§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - //['Metodbeskrivning', 'METODBESKR'], - //['Tillk. datum', 'TILLKDATUM'], - //['Rev. datum', 'REVDATUM'], - ['Anmärkning', 'ANM'], - ['Objekttyp', 'OBJTYP'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - 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: { - popoverTitle: 'Riksintresse obrutet fjäll (MB 4 kap 5§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - ['Metodbeskrivning', 'METODBESKR'], - ['Tillk. datum', 'TILLKDATUM'], - //['Rev. datum', 'REVDATUM'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - 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: { - popoverTitle: 'Riksintresse skyddade vattendrag (MB 4 kap 6§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - ['Metodbeskrivning', 'METODBESKR'], - ['Tillk. datum', 'TILLKDATUM'], - ['Rev. datum', 'REVDATUM'], - ['Anmärkning', 'ANM'], - ['Digitaliseringsskala', 'DIG_SKALA'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - 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], + 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], + }), + }); + })), + }, + 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], + }), + }); + })), + }, + 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], + }), + }); + })), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); }), - 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], + }, + 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], + }), + }); }), - }); - }), + }, }, - ren_betesomraden: { - popoverTitle: 'Samebyarnas betesområde', - popover: [ - ['Sameby', 'NAMN'], - ['Samebys typ', 'SAMEBY_TYP'], - ['Signatur', 'SIGNATUR'], - ['Aktualitet', 'AKTUALITET'], - ], - style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width, z) { - 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: { - popoverTitle: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', - popover: [ - ['Led-ID', 'LED_ID', { classes: ['feature-objid'], fn: (v) => v === 0 ? '' : v }], - ['Sameby #1', 'SAMEBY1'], - ['Sameby #2', 'SAMEBY2'], - ['Sameby #3', 'SAMEBY3'], - ['Beskrivning', 'BESKRIVNIN'], - ['Årstid', 'ARSTID'], - ['Riksintresse', 'RIKSINTR'], - ['Fast led', 'FAST_LED'], - ['Aktualitet', 'AKTUALITET'], - ['Signatur', 'SIGNATUR'], - ['Ledlängd', 'geom_length', { fn: 'length' }], - ], - style: [.75, 1, 1.5, 2, 3, 4, 5, 5, 6, 7, 8, 10].map(function(width, z) { - return new Style({ - zIndex: 7, - stroke: new Stroke({ - width: 2*width, - color: [119, 99, 59, 1], - lineDash: [4 * width], - }), - }); - }), - }, - ren_riks_ren: { - popoverTitle: 'Riksintresse rennäring', - popover: [ - ['Lagrum', 'LAGRUM'], - ['Aktualitet', 'AKTUALITET'], - ['Signatur', 'SIGNATUR'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - 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: { - popoverTitle: '(Kärn)områden av riksintresse rennäring', - popover: [ - ['Områdes-ID', 'OMR_NR', { classes: ['feature-objid'] }], - ['Länk', 'LANK'], - ['Årets runt', 'ARET_RUNT'], - ['Sameby', 'SAMEBY'], - ['Ansvarig', 'ANSVARIG'], - ['Aktualitet', 'AKTUALITET'], - ['Signatur', 'SIGNATUR'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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: 5, + zIndex: 8, fill: new Fill({ - color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], + color: [102, 157, 240, .25], }), stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [41, 109, 197, 1], + lineDash: [width/4, width/3], }), }); }), + }, }, - /* Documentation at - * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf - * */ - misc_dammar: { - popoverTitle: 'Damm', - popover: [ - ['Dammenhetens namn', 'DNamn'], - ['Dammanläggningens namn', 'Namn'], - ['Länsnr', 'LST_OBJID', { classes: ['feature-objid'] }], - ['Status', 'Status', { fn: (v) => v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : '' }], - //['Regleringstyp', 'Regleringstyp'], - ['Byggår', 'ByggAr'], - ['Dammhöjd', 'DammHojd', { unit: 'm' }], - ['Krönlängd', 'KronLangd', { unit: 'm' }], - ['Fiskväg', 'Fiskvag', { 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' : - '' }], - ['Huvudavrinningsområdesnummer', 'HARO', { classes: ['feature-objid'] } ], - ['Vattendistrikt', 'Vattendistrikt', { classes: ['feature-objid'] } ], - ['Verksamhet', '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' : - '' }], - ['Högsta dämningsgräns', 'DG', { unit: 'm' }], - ['Lägsta sänkningsgräns', 'SG', { unit: 'm' }], - ['Magasinsyta', 'MY', { unit: 'km²' }], - ['Reglerbar volym', 'RV', { unit: 'Mm³' }], - ['Kommentar', 'Kommentar'], - ], - 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, + 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({ - color: 'rgb(219, 30, 42)', + /* 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], }), + }); + }), + }, + 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: Math.log2(width) * 2/5, - color: 'rgb(128, 17, 25)', + width: 2*width, + color: [119, 99, 59, 1], + lineDash: [4 * width], }), - }), - }); - }), + }); + }), + }, + 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], + }), + }); + }), + }, + 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], + }), + }); + }), + }, }, - misc_gigafactories: { - popoverTitle: 'Stor industrisatsning', - popover: [ - ['Namn', 'Name'], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [4, 6, 7, 8, 10, 12].map(function(width) { - return new Style({ - zIndex: 60, - image: new CircleStyle({ - radius: width, + misc: { + /* Documentation at + * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf + * */ + 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)', + }), + }), + }); + }), + }, + + 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: 'rgb(152, 78, 163)', + color: 'rgba(152, 78, 163, .4)', }), stroke: new Stroke({ - width: Math.log2(width) * 2/5, + width: width, 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) */ + 2: { style: [169, 0, 230, .2] }, /* #2 Sannolikt påverkad kontinuitetsskog (preciserad) */ + 3: { style: [152, 230, 0, .2] }, /* #3 Sannolikt kontinuitetsskog i fjällen (grövre precisering) */ + 4: { style: [ 76, 115, 0, .2] }, /* #4 Potentiell kontinuitetsskog (2015) */ + }, +}); + +/* process URL parameters (other than 'basemap') */ +const STYLES = Object.seal(Object.fromEntries(Object.entries(LAYERS).map(([k,ls]) => + [k, Object.seal(Object.fromEntries(Object.keys(ls).map((l) => [l, null])))]))); +(function() { + const view = MAP.getView(); + const params = new URLSearchParams(window.location.hash.substring(1)); + const x = parseFloat(params.get('x')), + y = parseFloat(params.get('y')), + z = parseFloat(params.get('z')); + if (!isNaN(x) && !isNaN(y)) { + view.setCenter([x, y]); + view.setZoom(isNaN(z) ? 1 : z); + } else { + /* center of the bbox of the Norrbotten and Västerbotten geometries */ + view.setCenter([694767.48, 7338176.57]); + view.setZoom(1); + const geolocation = new Geolocation({ + projection: view.getProjection(), + tracking: true, + }); + const evt_key = geolocation.on('change:position', function() { + const pos = geolocation.getPosition(); + if (pos == null) { + return; + } + /* ignore further geolocation position changes */ + unByKey(evt_key); + geolocation.setTracking(false); + + const params2 = new URLSearchParams(window.location.hash.substring(1)); + /* ignore geolocation result if coordinates have changed meanwhile */ + if (params2.has('x') || params2.has('y')) { + return; + } + /* ignore geolocation result if not within extent */ + if (EXTENT[0] > pos[0] || pos[0] > EXTENT[2] || EXTENT[1] > pos[1] || pos[1] > EXTENT[3]) { + return; + } + view.setCenter(pos); + params2.set('x', pos[0].toFixed(2).replace(TRAILING_ZEROES, '')); + params2.set('y', pos[1].toFixed(2).replace(TRAILING_ZEROES, '')); + if (!params2.has('z')) { + const accuracy = geolocation.getAccuracy(); + if (accuracy == null || accuracy < 0) { + view.setZoom(Math.max(view.getMinZoom(), 0)); + } else { + /* infer resolution from accuracy, up to zoom level 7 (8px/m) */ + const [width, height] = MAP.getSize(); + const res = 8. * accuracy / Math.min(width, height); + view.setResolution(Math.max(res, view.getResolutionForZoom(7))); + } + params2.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); + } + location.hash = '#' + params2.toString(); + }); + } + if (!params.has('layers') || (!params.get('layers').match(/^\s*$/) && + /* compat redirect/layer subst for old non-hierachical names */ + !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).forEach(([k, v]) => + Object.entries(v) + .filter(([l]) => layersParams.includes(k + '.' + l)) + .forEach(([l,x]) => STYLES[k][l] = x.style ?? null)); + }, + + '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 <input type="date"> */ + ); + 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 tileGrid = createXYZ({ + extent: EXTENT, + tileSize: 1024, + maxResolution: 1024, /* = 1048576/1024 */ + minZoom: 0, + maxZoom: 7, + }); + const isVisible = function(k) { + const styles = STYLES[k]; + return Object.keys(LAYERS[k]).some((l) => styles[l] != 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 = ['adm', '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) { + const baseurl = new URL('/raster/' + k + '/', window.location.toString()).toString(); + const source = new GeoTIFF({ + sources: [{ url: baseurl + encodeURIComponent(k) + '.tiff' }], + normalize: false, + convertToRGB: false, + wrapX: false, + interpolate: false, + /* use the projection found in the source's metadata */ + }); + /* GeoTIFF doesn't allow retrieving the URL later, so we manually store the baseurl instead */ + source.set('baseurl', baseurl, true); + ret[k] = new TileLayerGL({ + /* Naturvårdsverket has a WMS server we could use instead, but by serving it ourselves + * we can filter on he various kskog classes */ + source: source, + visible: false, + style: null, /* filled later */ + }); + MAP.addLayer(ret[k]); + }); } -}; + vectorLayers.forEach(function(k) { + const canFilterByAge0 = canFilterByAge.includes(k); + const styles = STYLES[k]; + const baseurl = new URL('/tiles/' + k + '/', window.location.toString()).toString(); + const source = new VectorTile({ + url: baseurl + '{z}/{x}/{y}.pbf', + format: new MVT(), + projection: PROJECTION, + wrapX: false, + transition: 0, + tileGrid: tileGrid, + }); + source.set('baseurl', baseurl, true); + ret[k] = new VectorTileLayer({ + source: source, + /* XXX switch to 'hybrid' if there are perf issues; but that seems to + * put lines above points regardless of their respective z-index */ + renderMode: 'hybrid', + 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; + } + } else if ((ageFilterSettings._min_ts !== null && ts < ageFilterSettings._min_ts) || + (ageFilterSettings._max_ts !== null && ts > ageFilterSettings._max_ts)) { + return; + } + } + const style = styles[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_ledningar', 'svk_stolpar'], + layer: ['svk.stolpar', 'svk.ledningar'], }, { text: 'Stationer', - layer: 'svk_stationer', + layer: 'svk.stationer', }, { text: 'Transmissionsnätsprojekt ', - layer: 'svk_transmissionsnatsprojekt', + layer: 'svk.transmissionsnatsprojekt', }, ], }, { text: 'Stora industrisatsningar', - layer: 'misc_gigafactories', + layer: 'misc.gigafactories', }, { text: 'Dammar', - layer: 'misc_dammar', + layer: 'misc.dammar', }, { text: 'Mineralrättigheter', @@ -2911,11 +3065,11 @@ const layerHierarchy = [ children: [ { text: 'Beviljad', - layer: 'mrr_appr_ec', + layer: 'mrr.appr_ec', }, { text: 'Ansökt', - layer: 'mrr_appl_ec', + layer: 'mrr.appl_ec', }, ], }, @@ -2924,11 +3078,11 @@ const layerHierarchy = [ children: [ { text: 'Beviljad', - layer: 'mrr_appr_ogd', + layer: 'mrr.appr_ogd', }, { text: 'Ansökt', - layer: 'mrr_appl_ogd', + layer: 'mrr.appl_ogd', }, ], }, @@ -2937,17 +3091,17 @@ const layerHierarchy = [ children: [ { text: 'Beviljad', - layer: 'mrr_appr_met', + layer: 'mrr.appr_met', }, { text: 'Ansökt', - layer: 'mrr_appl_met', + layer: 'mrr.appl_met', }, ], }, { text: 'Markanvisningar till koncession', - layer: 'mrr_appr_dl', + layer: 'mrr.appr_dl', }, ], }, @@ -2955,48 +3109,89 @@ const layerHierarchy = [ text: 'Vindbruk', children: [ { - text: 'Projekteringsområden', + text: 'Landbaserade projekteringsområden', children: [ { text: 'Aktuella', - layer: 'vbk_area_current', + layer: 'vbk.area_current', }, { text: 'Ej aktuella', - layer: 'vbk_area_notcurrent', + layer: 'vbk.area_notcurrent', }, ], }, { - text: 'Vindkraftverk', + text: 'Landbaserade vindkraftverk', children: [ { text: 'Uppförda', - layer: 'vbk_station_completed', + layer: 'vbk.turbine_completed', + }, + { + text: 'Beviljade', + layer: 'vbk.turbine_approved', + }, + { + text: 'Avslagna/nekad', + layer: 'vbk.turbine_rejected', }, { text: 'Handläggs', - layer: 'vbk_station_processed', + layer: 'vbk.turbine_processed', }, { - text: 'Beviljade', - layer: 'vbk_station_approved', + text: 'Nedmonterade', + layer: 'vbk.turbine_dismounted', + }, + { + text: 'Överklagade', + layer: 'vbk.turbine_appealed', }, { text: 'Inte längre aktuella/återkallade', - layer: 'vbk_station_revoked', + layer: 'vbk.turbine_revoked', }, + ], + }, + { + text: 'Havsbaserad vindkraft', + children: [ { - text: 'Avslagna/nekad', - layer: 'vbk_station_rejected', + text: 'Uppförd', + layer: 'vbk.offshore_completed', }, { - text: 'Nedmonterade', - layer: 'vbk_station_dismounted', + text: 'Tillståndsansökan beviljad', + layer: 'vbk.offshore_approved', }, { - text: 'Överklagade', - layer: 'vbk_station_appealed', + 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', }, ], }, @@ -3006,12 +3201,12 @@ const layerHierarchy = [ text: 'Skogsbruk', children: [ { - text: 'Uppförda (sedan 2000)', - layer: 'sks_clearcut_comp', + text: 'Utförda avverkningar', + layer: 'avverk.utford', }, { - text: 'Anmälda', - layer: 'sks_clearcut_appl', + text: 'Avverkningsanmälningar', + layer: 'avverk.anmald', }, ] }, @@ -3019,59 +3214,59 @@ const layerHierarchy = [ text: 'Skyddad natur', children: [ { - text: 'Nationella skyddsformer', + text: 'Nationella skyddsformer från Naturvårdsregistret', children: [ { text: 'Tillträdesförbud', - layer: 'nvr_tilltradesforbud', + layer: 'skydd.tilltradesforbud', }, { text: 'Nationalpark', - layer: 'nvr_nationalpark', + layer: 'skydd.nationalpark', }, { text: 'Naturreservat', - layer: 'nvr_naturreservat', + layer: 'skydd.naturreservat', }, { text: 'Kommunala naturreservat', - layer: 'nvr_naturreservat_kommunalt', + layer: 'skydd.naturreservat_kommunalt', }, { text: 'Naturvårdsområden', - layer: 'nvr_naturvardsomrade', + layer: 'skydd.naturvardsomrade', }, { text: 'Djur- och växtskyddsområden', - layer: 'nvr_djur_och_vaxtskyddsomrade', + layer: 'skydd.djur_och_vaxtskyddsomrade', }, { text: 'Kulturreservat', - layer: 'nvr_kulturreservat', + layer: 'skydd.kulturreservat', }, { text: 'Vattenskyddsområden', - layer: 'nvr_vattenskyddsomrade', + layer: 'skydd.vattenskyddsomrade', }, { text: 'Landskapsbildsskyddsområden', - layer: 'nvr_landskapsbildsskyddsomrade', + layer: 'skydd.landskapsbildsskyddsomrade', }, { text: 'Skogliga biotopskyddsområden', - layer: 'nvr_skogligt_biotopskyddsomrade', + layer: 'skydd.skogligt_biotopskyddsomrade', }, { text: 'Övriga biotopskyddsområden', - layer: 'nvr_ovrigt_biotopskyddsomrade', + layer: 'skydd.ovrigt_biotopskyddsomrade', }, { text: 'Naturminne', - layer: [ 'nvr_naturminne_yta', 'nvr_naturminne_punkt' ], + layer: [ 'skydd.naturminne_yta', 'skydd.naturminne_punkt' ], }, { text: 'Interimistiskt förbud', - layer: 'nvr_interimistiskt_forbud', + layer: 'skydd.interimistiskt_forbud', }, ], }, @@ -3080,11 +3275,11 @@ const layerHierarchy = [ children: [ { text: 'Fågeldirektivet (SPA)', - layer: 'nvr_fageldirektivet', + layer: 'skydd.fageldirektivet', }, { text: 'Art- och habitatdirektivet (SCI)', - layer: 'nvr_habitatdirektivet', + layer: 'skydd.habitatdirektivet', }, ], }, @@ -3093,38 +3288,92 @@ const layerHierarchy = [ children: [ { text: 'Marina skyddade områden (Helcom MPA)', - layer: 'nvr_helcom', + layer: 'skydd.helcom', }, { text: 'Ramsar-områden (Våtmarkskonventionen)', - layer: 'nvr_ramsar', + layer: 'skydd.ramsar', }, { text: 'Marina skyddade områden (Ospar MPA)', - layer: 'nvr_ospar', + layer: 'skydd.ospar', }, { text: 'Världsarv med mycket höga naturvärden (UNESCO)', - layer: 'nvr_varldsarv', + layer: 'skydd.varldsarv', }, { text: 'Biosfärsområden (UNESCO)', - layer: 'nvr_biosfarsomraden', + 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: 'Naturvårdsavtal', + text: 'Skogliga värden', children: [ { - text: 'Naturvårdsverket, Länsstyrelserna', - layer: 'nvr_naturvardsavtal', + 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: 'Skogsstyrelsen', - layer: 'nvr_naturvardsavtal_skogsstyrelsen', + 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', + }, + ], } ] }, @@ -3133,27 +3382,27 @@ const layerHierarchy = [ children: [ { text: 'Naturvård', - layer: 'ri_naturvard', + layer: 'ri.naturvard', }, { text: 'Friluftsliv', - layer: 'ri_friluftsliv', + layer: 'ri.friluftsliv', }, { text: 'Rörligt friluftsliv', - layer: 'ri_rorligt_friluftsliv', + layer: 'ri.rorligt_friluftsliv', }, { text: 'Obruten kust', - layer: 'ri_obruten_kust', + layer: 'ri.obruten_kust', }, { text: 'Obrutet fjäll', - layer: 'ri_obrutet_fjall', + layer: 'ri.obrutet_fjall', }, { text: 'Skyddade vattendrag', - layer: 'ri_skyddade_vattendrag', + layer: 'ri.skyddade_vattendrag', }, ] }, @@ -3164,112 +3413,172 @@ const layerHierarchy = [ children: [ { text: 'Betesområden', - layer: 'ren_betesomraden', + layer: 'ren.betesomrade', }, { text: 'Flyttled', - layer: 'ren_flyttled', + layer: 'ren.flyttled', }, { text: 'Riksintressen', - layer: 'ren_riks_ren', + layer: 'ren.riks_ren', }, { text: '(Kärn)områden av riksintresse', - layer: 'ren_omr_riks', + layer: 'ren.omr_riks', }, ] }, + { + text: 'Administrativa gränser', + type: 'switch', + collapse_children: true, + children: [ + { + text: 'Länsgränser', + layer: 'adm.lansyta', + }, + { + text: 'Kommungränser', + layer: 'adm.kommunyta', + }, + ], + }, ]; -const styles = (function() { - const searchParams = new URLSearchParams(location.hash.substring(1)); - const layersParams = searchParams.has('layers') ? searchParams.get('layers').split(' ') : []; - return Object.keys(layers).reduce(function(result, key) { - if (layersParams.includes(key)) { - result[key] = layers[key].style; +/* 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.appendChild(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).pop(); + const layerName = layer.slice(layerGroup.length + 1); + if (mapLayers[layerGroup] == null || LAYERS[layerGroup]?.[layerName]?.style == null) { + console.log(`Could not find symbol for layer ${layer}, skipping`); + return; + } + const legend = LAYERS[layerGroup][layerName]?.legend; + if (legend === null) { + return; /* layer has opted out from legend */ + } + if (canvas == null || !legend?.reuse_canvas) { + canvas = document.createElement('canvas'); + div.appendChild(canvas); + render = toContext(canvas.getContext('2d'), + { size:[width, parent_height] } + ); + } + + if (mapLayers[layerGroup].getSource() instanceof GeoTIFF) { + /* raster source */ + render.setFillStrokeStyle(new Fill({ + color: LAYERS[layerGroup][layerName].style, + })); + return render.drawGeometry(symbols.polygon); + } + else if (mapLayers[layerGroup].getSource() instanceof VectorTile) { + /* vector source */ + const style = Array.isArray(LAYERS[layerGroup][layerName].style) ? + LAYERS[layerGroup][layerName].style[legend?.zoomLevel ?? 5] : + LAYERS[layerGroup][layerName].style; + const legend_type = legend?.type ?? 'polygon'; + if (legend_type === 'point' && style.getImage(1) instanceof Icon && style.getImage(1).getSrc()) { + /* use a new <img> element since .setStyle() returns the same one and doesn't work in that case */ + const div2 = document.createElement('div'); + 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); } - return result; - }, {}); -})(); -const [vectorLayers, featureOverlayLayer] = (function() { - const xyz = '/{z}/{x}/{y}.pbf'; - const tileGrid = createXYZ({ - extent: extent, - tileSize: 1024, - maxResolution: 1024, /* = 1048576/1024 */ - minZoom: 0, - maxZoom: 7, + elem._legend = li; + if (elem.children != null && 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']); }); - return [ - Object.fromEntries( - ['mrr', 'nvr', 'ren', 'ri', 'sks', 'svk', 'vbk', 'misc'] - .map(function(k) { - let visible = false; - Object.keys(layers).forEach(function(lyr) { - if (lyr.startsWith(k + '_')) { - visible ||= styles[lyr] !== undefined; - } - }); - const vectorLayer = new VectorTileLayer({ - source: new VectorTile({ - url: '/tiles/' + k + xyz, - format: new MVT(), - projection: projection, - wrapX: false, - transition: 0, - tileGrid: tileGrid, - }), - /* XXX switch to 'hybrid' if there are perf issues; but that seems to - * put lines above points regardless of their respective z-index */ - renderMode: 'hybrid', - declutter: false, - visible: visible, - style: function(feature, resolution) { - const style = styles[k + '_' + feature.getProperties().layer]; - if (!Array.isArray(style)) { - return style; - } else { - const maxi = style.length - 1; - const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); - /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ - const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); - // console.log(`resolution=${resolution}, z=${z}, i=${i}`); - return style[i]; - } - }}); - vectorLayer.set('layerGroup', k, true); - map.addLayer(vectorLayer); - return [k, vectorLayer]; - })), - - /* We use a vector tile layer for featureOverlayLayer instead of a simple - * vector layer overlay, since we don't want to clip selected geometries at - * tile boundaries. It sounds overkill to load an entire tile layer to - * display a single feature, but this shouldn't cause much overhead in - * practice (the tiles are most likely cached already). */ - new VectorTileLayer({ - source: new VectorTile({ - urls: [], - format: new MVT(), - projection: projection, - wrapX: false, - transition: 0, - tileGrid: tileGrid, - }), - zIndex: 65535, - declutter: false, - visible: false, - renderMode: 'vector', - map: map, - style: null, - }), - ]; })(); - /* layer selection panel */ +const infoMetadataAccordions = []; (function() { const modal = document.getElementById('layer-selection-panel'); modal.classList.add('modal'); @@ -3294,7 +3603,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { elem._layers = elem.layer === undefined ? [] : Array.isArray(elem.layer) ? elem.layer : [elem.layer]; - if (elem.children !== undefined && elem.children.length > 0) { + if (elem.children != null && elem.children.length > 0) { collectLayers(elem.children); elem.children.forEach(function(child) { child._layers.forEach((l) => elem._layers.push(l)); @@ -3305,55 +3614,82 @@ const [vectorLayers, featureOverlayLayer] = (function() { const setIndeterminateAndChecked = function(list) { return list.forEach(function(elem) { - const layerStyles = elem._layers.map((lyr) => styles[lyr] !== undefined); + const layerStyles = elem._layers.map(function(lyr) { + const layerGroup = lyr.split('.', 1).pop(); + const layerName = lyr.slice(layerGroup.length + 1); + return STYLES[layerGroup]?.[layerName] != null; + }); elem._input.indeterminate = elem._layers.length <= 1 ? false : layerStyles.slice(1).some((v) => v !== layerStyles[0]); - if (!elem._input.indeterminate) { + if (elem._input.indeterminate) { + elem._legend.classList.remove('d-none'); + } else { /* keep checked value if indeterminate */ - elem._input.checked = layerStyles.every((v) => v); + 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) { + if (elem.children != null && elem.children.length > 0) { setIndeterminateAndChecked(elem.children); } }); }; const fixLayerVisibility = function() { - const result = {} - Object.keys(layers).forEach(function(lyr) { - const layerGroup = lyr.split('_', 1)[0]; - if (result[layerGroup] === undefined) { - result[layerGroup] = false; + Object.entries(mapLayers).forEach(function([k,lyr]) { + /* set palette for raster layers */ + if (lyr != null && lyr.getSource() instanceof GeoTIFF) { + const styles = STYLES[k] ?? {}; + const nodata = [0, 0, 0, .0]; + const indices = Object.keys(LAYERS[k]).map((v) => [parseInt(v, 10), v]); + const n = Math.max(...indices.map(([i]) => i)); + const palette = new Array(n+1).fill(nodata); + indices.forEach(([i,l]) => palette[i] = styles[l] ?? nodata); + /* XXX unfortunately calling .setStyle() makes the layer blink; using style variabse would + * be nicer, but `['palette', ['band', 1], [['var', 'val1'], …]]` fails as each palette index + * needs to be a color literal, even when `['var', 'val1']` evaluates to such, cf. + * https://github.com/openlayers/openlayers/blob/v10.6.0/src/ol/expr/expression.js#L976 */ + lyr.setStyle({ color: ['palette', ['band', 1], palette] }); } - result[layerGroup] ||= styles[lyr] !== undefined; }); - Object.entries(result).forEach(function([lyr, visible]) { - const v = vectorLayers[lyr]; - if (v !== undefined) { - //console.log(lyr, visible); - v.setVisible(visible); - } + Object.entries(LAYERS).forEach(function([k,v]) { + const styles = STYLES[k]; + const visible = Object.keys(v).some((l) => styles[l] != null); + // console.log(k, visible); + mapLayers[k]?.setVisible(visible); }); + const btn = document.getElementById('map-legend-button'); + if (Object.values(STYLES).some((v) => Object.values(v).some((v2) => v2 != null))) { + btn.classList.remove('disabled'); + } else { + btn.classList.add('disabled'); + } }; const onClickFunction = function(layerList, event) { - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - const popover = Popover.getInstance(popup); - if (popover !== null) { - popover.dispose(); - } + 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) { - styles[lyr] = layers[lyr].style; - if (!layersParams.includes(lyr)) { - layersParams.push(lyr); + const layerGroup = lyr.split('.', 1).pop(); + if (mapLayers[layerGroup] != null) { + /* keep unexisting layers (eg WebGL layers on a system without WebGL support) unselectable */ + const layerName = lyr.slice(layerGroup.length + 1); + STYLES[layerGroup][layerName] = LAYERS[layerGroup][layerName]?.style ?? null; + if (!layersParams.includes(lyr)) { + layersParams.push(lyr); + } } }); } else { layerList.forEach(function(lyr) { - delete styles[lyr]; + const layerGroup = lyr.split('.', 1).pop(); + const layerName = lyr.slice(layerGroup.length + 1); + STYLES[layerGroup][layerName] = null; }); layersParams = layersParams.filter((lyr) => !layerList.includes(lyr)); } @@ -3361,9 +3697,9 @@ const [vectorLayers, featureOverlayLayer] = (function() { fixLayerVisibility(); layerList - .map((l) => l.split('_', 1)[0]) - .filter((v, i, arr) => arr.indexOf(v) === i) - .forEach((l) => vectorLayers[l].getSource().changed()); + .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(); @@ -3372,7 +3708,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { let layerId = 0; const addAccordionGroup = function(parentNode, children) { const ul = document.createElement('ul'); - parentNode.appendChild(ul); + parentNode?.appendChild?.(ul); ul.classList.add('list-group', 'list-group-flush'); children.forEach(function(child) { @@ -3398,7 +3734,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { const textNode = document.createTextNode(child.text); label.appendChild(textNode); - if (child.children !== undefined && child.children.length > 0) { + if (child.children != null && child.children.length > 0) { addAccordionGroup(li, child.children); } @@ -3453,8 +3789,18 @@ const [vectorLayers, featureOverlayLayer] = (function() { const text0 = document.createTextNode(x.text); label0.appendChild(text0); - if (x.children === undefined || x.children.length === 0) { - item.replaceChild(span0, header); + if (x.children == null || x.children.length === 0 || x.collapse_children) { + span0.removeAttribute('data-bs-toggle'); + span0.removeAttribute('data-bs-target'); + item.replaceChildren(span0); + if (x.type === 'switch') { + span0.classList.add('form-switch'); + input0.setAttribute('role', 'switch'); + } + if (x.children != null && x.children.length > 0) { + /* create inputs for the hash param logic but don't add them to the panel */ + addAccordionGroup(null, x.children); + } } else { const body = document.createElement('div'); collapse.appendChild(body); @@ -3492,44 +3838,1411 @@ const [vectorLayers, featureOverlayLayer] = (function() { label.setAttribute('for', input.id); label.innerHTML = 'Nedtonad bakgrund karta'; - input.checked = baseMapLayer === 'topowebb_nedtonad'; + input.checked = BASEMAP.layer === 'topowebb_nedtonad'; input.onchange = function(event) { - baseMapLayer = event.target.checked ? 'topowebb_nedtonad' : 'topowebb'; - baseMapSource.setUrl(`https://minkarta.lantmateriet.se/map/topowebbcache?LAYER=${encodeURIComponent(baseMapLayer)}`); - - const searchParams = new URLSearchParams(location.hash.substring(1)); - searchParams.set('basemap', baseMapLayer); - location.hash = '#' + searchParams.toString(); + 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).pop(); + return [ groupname, k.slice(groupname.length + 1) ]; + }), + }); + }); + })(); })(); -/* legend panel */ +/* measurement panel */ (function() { - const modal = document.getElementById('map-legend-panel'); - modal.classList.add('modal'); - modal.setAttribute('role', 'dialog'); - modal.setAttribute('aria-hidden', 'true'); + const value = document.createTextNode(''), + unit = document.createTextNode(''); + const reset = function() { + source.clear(true); + const f = { LineString: formatLength, Polygon: formatArea }[getMeasureMode()]; + if (f == null) { + value.nodeValue = unit.nodeValue = ''; + } else { + f(0); + } + }; - const content = document.createElement('div'); - modal.appendChild(content); - content.classList.add('modal-content'); + const source = new VectorSource({ + wrapX: false, + }); - const body = document.createElement('div'); - content.appendChild(body); - body.classList.add('modal-body'); - body.innerHTML = 'legend TODO'; + let draw; + const buttons = Object.fromEntries([ + { + id: 'cancel', + bi: 'trash', + title: 'Avbryt mätningen', + onclick: function() { + reset(); + Object.values(buttons).forEach((btn) => btn.disabled = true); + draw.abortDrawing(); + }, + }, + { + id: 'undo', + bi: 'arrow-counterclockwise', + title: 'Ta bort sista punkten', + onclick: function() { + draw.removeLastPoint(); + const n = { LineString: 2, Polygon: 3 }[getMeasureMode()] ?? Infinity; + draw.getOverlay().getSource().forEachFeature(function(feature) { + const geom = feature.getGeometry(); + if (geom.getType() === 'LineString') { + /* disable OK button if not enough points have been drawn (excluding cursor) */ + buttons.ok.disabled = geom.getCoordinates().length - 1 < n; + return true; /* stop iterating */ + } + }); + } + }, + { + id: 'ok', + bi: 'check-lg', + title: 'Slutför mätningen', + onclick: () => draw.finishDrawing(), + }, + ].map(function(x, idx, arr) { + const btn = document.createElement('button'); + btn.classList.add('btn', 'btn-outline-' + (idx < arr.length-1 ? 'secondary' : 'primary')); + btn.setAttribute('type', 'button'); + btn.title = x.title; + btn.setAttribute('aria-label', btn.title); + + const i = document.createElement('i'); + i.classList.add('bi', 'bi-' + x.bi); + btn.appendChild(i); + + btn.onclick = x.onclick; + return [x.id, btn]; + })); + + const formatLength = (function() { + const formatters = [ + { maximumFractionDigits: 0 }, + { maximumFractionDigits: 1 }, + { maximumFractionDigits: 2 }, + ] + .map((fmt) => new Intl.NumberFormat(LOCALE, { + ...fmt, + minimumFractionDigits: fmt.maximumFractionDigits ?? 0, + })); + return function(v) { + if (v <= 100) { /* ≤ 100 m */ + unit.nodeValue = 'm'; + value.nodeValue = formatters[1].format(v); + } else if (v <= 5_000) { /* ≤ 5 km */ + unit.nodeValue = 'm'; + value.nodeValue = formatters[0].format(v); + } else { + unit.nodeValue = 'km'; + value.nodeValue = formatters[2].format(v/1000); + } + }; + })(); + const formatArea = (function() { + const formatters = [ + { maximumFractionDigits: 1 }, + { maximumFractionDigits: 2 }, + ] + /* XXX would be nice to use Intl.NumberFormat()'s unit support, but m² and km² are not + * supported currently, see https://github.com/tc39/ecma402/issues/767 */ + .map((fmt) => new Intl.NumberFormat(LOCALE, { + ...fmt, + minimumFractionDigits: fmt.maximumFractionDigits ?? 0, + })); + return function(v) { + if (v < 10_000) { /* < 1 ha */ + unit.nodeValue = 'm²'; + value.nodeValue = formatters[0].format(v); + } else if (v < 100_000_000) { /* < 10000 ha (100 km²) */ + unit.nodeValue = 'ha'; + value.nodeValue = formatters[1].format(v/10_000); + } else { + unit.nodeValue = 'km²'; + v /= 1_000_000; + const i = v < 1_000_000 ? 1 : 0; /* ≥10⁶ km² overflows the box with 2 decimals */ + value.nodeValue = formatters[i].format(v); + } + }; + })(); + + const setup = (function() { + const styles = { + Point: new Style({ + image: new CircleStyle({ + radius: 6, + fill: new Fill({ + color: [0, 183, 255, 1], + }), + stroke: new Stroke({ + color: [255, 255, 255, 1], + width: .5, + }), + }), + }), + LineString: [ + new Style({ + stroke: new Stroke({ + color: [255, 255, 255, 1], + width: 4, + }), + }), + new Style({ + stroke: new Stroke({ + color: [0, 183, 255, 1], + width: 3, + lineDash: [10, 10], + }), + }), + ], + Polygon: new Style({ + fill: new Fill({ + color: [255, 255, 255, .5], + }), + }), + }; + const layer = new VectorLayer({ + source: source, + visible: false, + style: [ + new Style({ + fill: styles.Polygon.getFill(), + stroke: styles.LineString[0].getStroke(), + }), + (function() { + const s = styles.LineString[1].clone(); + s?.getStroke?.()?.setLineDash?.(null); + return s; + })(), + ], + map: MAP, + }); + + return function(geom_type) { + if (draw != null) { + draw.abortDrawing(); + MAP.removeInteraction(draw); + } + reset(); /* remove features when toggling between geom types */ + Object.values(buttons).forEach((btn) => btn.disabled = true); + if (geom_type == null) { + layer.setVisible(false); + return; + } + draw = new Draw({ + source: source, + type: geom_type, + style: function(feature) { + return styles[ feature.getGeometry().getType() ]; + }, + }); + MAP.addInteraction(draw); + layer.setVisible(true); + + let listener; + draw.on('drawstart', function(event) { + reset(); + buttons.undo.disabled = buttons.cancel.disabled = false; + const geom = event.feature.getGeometry(); + const [isComplete, measure] = { + LineString: [ + /* 2 points drawn + cursor */ + (g) => g.getCoordinates().length >= 3, + (g) => formatLength(g.getLength()), + ], + Polygon: [ + /* 3 points drawn + cursor + 1st point */ + (g) => g.getCoordinates()[0].length >= 5, + (g) => formatArea(g.getArea()), + ], + }[geom.getType()]; + const btnOK = buttons.ok; + listener = geom.on('change', function(event) { + if (btnOK.disabled && isComplete(geom)) { + btnOK.disabled = false; + } + measure(event.target); + }); + }); + draw.on('drawend', function() { + unByKey(listener); + buttons.ok.disabled = buttons.undo.disabled = true; + }); + draw.on('drawabort', function() { + unByKey(listener); + reset(); + Object.values(buttons).forEach((btn) => btn.disabled = true); + }); + }; + })(); + + const [body, btn_close] = (function() { + const modal = document.getElementById('measure-panel'); + modal.classList.add('modal'); + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-hidden', 'true'); + + const content = document.createElement('div'); + modal.appendChild(content); + content.classList.add('modal-content'); + + const header = document.createElement('div'); + content.appendChild(header); + header.classList.add('modal-header'); + + const title = document.createElement('div'); + title.classList.add('h5'); + title.innerHTML = 'Mät i kartan'; + header.appendChild(title); + + const btn_close = document.createElement('button'); + btn_close.classList.add('btn-close'); + btn_close.type = 'button'; + btn_close.title = 'Stäng'; + btn_close.setAttribute('aria-label', btn_close.title); + header.appendChild(btn_close); + + const body = document.createElement('div'); + content.appendChild(body); + body.classList.add('modal-body'); + return [body, btn_close]; + })(); + + const getMeasureMode = (function() { + const btn_group = document.createElement('div'); + btn_group.classList.add('btn-group'); + btn_group.setAttribute('role', 'group'); + btn_group.setAttribute('aria-label', 'Välj geometrityp'); + body.appendChild(btn_group); + + const radios = [ + { id: 'LineString', text: 'Distans' }, + { id: 'Polygon', text: 'Yta' }, + ].map(function(x, idx) { + const radio = document.createElement('input'); + radio.classList.add('btn-check'); + radio.type = 'radio'; + radio.checked = idx == 0; + radio.name = 'measure-geomtype'; + radio.id = 'measure-geomtype-' + x.id; + radio.setAttribute('aria-expanded', 'false'); + radio.setAttribute('autocomplete', 'off'); + btn_group.appendChild(radio); + + const lbl = document.createElement('label'); + lbl.classList.add('btn', 'btn-lg', 'btn-outline-dark'); + lbl.setAttribute('for', radio.id); + lbl.appendChild(document.createTextNode(x.text)); + btn_group.appendChild(lbl); + + radio.onclick = function() { + setup(x.id); + }; + return [x.id, radio]; + }); + + return () => radios.filter( (x) => x[1].checked )?.[0]?.[0]; + })(); + + (function() { + const div = document.createElement('div'); + div.classList.add('measure-value'); + body.appendChild(div); + const span0 = document.createElement('span'); + span0.appendChild(value); + div.appendChild(span0); + const span1 = document.createElement('span'); + span1.classList.add('measure-unit'); + span1.appendChild(unit); + div.appendChild(span1); + })(); + + (function() { + const btn_group = document.createElement('div'); + btn_group.classList.add('btn-group'); + btn_group.setAttribute('role', 'group'); + body.appendChild(btn_group); + + btn_group.appendChild(buttons.cancel); + btn_group.appendChild(buttons.undo); + btn_group.appendChild(buttons.ok); + })(); + + const button_menu = document.getElementById('measure-button') + .getElementsByTagName('button')[0]; + button_menu.addEventListener('click', function(event) { + if (event.currentTarget.getAttribute('aria-expanded') === 'true') { + disposePopover(); + setup(getMeasureMode()); + } else { + setup(null); + /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */ + const activeElement = document.activeElement; + if (activeElement.isEqualNode(btn_close)) { + activeElement.blur(); + } + } + }); + btn_close.onclick = () => button_menu.click(); })(); /* popup and feature overlays */ -(function() { +const disposePopover = (function() { + /* return an <a> 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['turbine_' + 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.biosfarsomraden = { + title: 'Biosfärsområden (UNESCO)', + fields: [ + { key: 'SKYDDSTYP', desc: 'Skyddstyp' }, + { key: 'NAMN', desc: 'Namn' }, + { key: 'LINK', desc: 'Länk', fn: formatLink }, + fieldMap.geom_area, + ], + }; + + layers.skydd.naturvardsavtal = { + title: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', + fields: [ + { 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 numberFormatters = [ + { }, + { maximumFractionDigits: 1 }, + { maximumFractionDigits: 2 }, + ] + /* XXX would be nice to use Intl.NumberFormat()'s unit support, but m² and km² are not + * supported currently, see https://github.com/tc39/ecma402/issues/767 */ + .map((fmt) => new Intl.NumberFormat(LOCALE, fmt)); + const unitSeparator = '\u00A0'; /* U+00A0 NO-BREAK SPACE */ + const formatValue = function(value, options) { + if (options?.fn == null) { + /* no-op */ + } else if (typeof options.fn === 'function') { + value = options.fn(value); + } else if (options.fn === 'length' && typeof value === 'number' && options?.unit == null) { + if (value <= 5_000) { /* ≤ 5 km */ + value = numberFormatters[1].format(value) + unitSeparator + 'm'; + } else { + value = numberFormatters[2].format(value/1000) + unitSeparator + 'km'; + } + } else if (options.fn === 'area' && typeof value === 'number' && options?.unit == null) { + if (value < 10_000) { /* < 1 ha */ + value = numberFormatters[1].format(value) + unitSeparator + 'm²'; + } else if (value < 100_000_000) { /* < 10000 ha (100 km²) */ + value = numberFormatters[2].format(value/10_000) + unitSeparator + 'ha'; + } else { + value = numberFormatters[2].format(value/1_000_000) + unitSeparator + 'km²'; + } + } + 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 (options?.unit != null) { + return document.createTextNode(numberFormatters[0].format(value) + + unitSeparator + options.unit); + } + return document.createTextNode(value.toString()); + default: + return null; + } + }; + + /* turn the properties into a fine <table> */ + 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: popup, + element: document.getElementById('popup'), }); - map.addOverlay(popupOverlay); + 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, overlayAttributes = [], overlayAttrIdx = 0; + 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'); @@ -3538,23 +5251,24 @@ const [vectorLayers, featureOverlayLayer] = (function() { 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.getInstance(popup).tip; + const popoverTip = popover.tip; if (popoverTip.classList.contains('popover-maximized')) { return; } - pageNode.classList.add('grabbing-area-grabbed'); + 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 maxHeight = document.getElementById('map').getBoundingClientRect().height - 1 - rect.top - - popoverTip.getElementsByClassName('popover-header')[0].getBoundingClientRect().height; - const style = popoverTip.style; style.display = 'none'; /* avoid reflows between the following assignments */ style.position = 'absolute'; @@ -3574,18 +5288,17 @@ const [vectorLayers, featureOverlayLayer] = (function() { }; document.onmouseup = function(event) { + /* done moving around */ if (event.button != 0) { return; } - pageNode.classList.remove('grabbing-area-grabbed'); + headerGrabbingArea.classList.remove('grabbing-area-grabbed'); document.onmousemove = null; document.onmouseup = null; }; }; - const pageNode = document.createElement('h6'); - headerGrabbingArea.appendChild(pageNode); - + /* current number page and total page count */ const pageNum = document.createElement('span'); const pageCount = document.createElement('span'); pageNode.appendChild(document.createTextNode('Träff ')); @@ -3593,48 +5306,48 @@ const [vectorLayers, featureOverlayLayer] = (function() { 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 updateFeatureOverlayLayer = function(layer_group, layer, id) { - const lyr = vectorLayers[layer_group]; - if (lyr === undefined) { + const highlightFeature = function(layer_group, layer, id) { + const source = mapSources[layer_group]; + if (source == null) { return; } - const urls = lyr.getSource().getUrls(); - const source = featureOverlayLayer.getSource(); - if (source.getUrls().length < 1 || source.getUrls()[0] !== urls[0]) { + if (featureOverlayLayer.getSource() !== source) { + /* console.log('Updating source for feature overlay layer'); */ featureOverlayLayer.setVisible(false); - source.setUrls(urls); + featureOverlayLayer.setSource(source); } - featureOverlayLayer.setStyle(function(feature, resolution) { + featureOverlayLayer.setStyle(function(feature) { if (feature.getId() === id && feature.getProperties().layer === layer) { return featureOverlayStyle; - } else { - return undefined; } }); featureOverlayLayer.setVisible(true); featureOverlayLayer.changed(); }; + /* highlight the feature at index overlayAttrIdx within the CGI reply list */ const refreshPopover = function() { const attr = overlayAttributes[overlayAttrIdx]; - updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.ogc_fid); + 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 === null || popover.tip === null) { + if (btn.classList.contains('disabled') || popover?.tip == null) { return; } if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) { - return; + return; /* out of range */ } overlayAttrIdx += offset; @@ -3653,6 +5366,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { setTimeout(function() { btn.blur() }, 100); }; + /* control buttons */ const btnPrev = document.createElement('button'); btnPrev.classList.add('popover-button', 'popover-button-prev'); btnPrev.setAttribute('type', 'button'); @@ -3677,8 +5391,8 @@ const [vectorLayers, featureOverlayLayer] = (function() { const btnExpandTitle = 'Förstora'; const btnExpandTitle2 = 'Förminska'; btnExpand.setAttribute('aria-label', btnExpand.title); - btnExpand.onclick = function(event) { - if (popover === null || popover.tip === null) { + btnExpand.onclick = function() { /* maximize or reduce the popover */ + if (popover?.tip == null) { return; } if (!popover.tip.classList.contains('popover-maximized')) { @@ -3700,119 +5414,30 @@ const [vectorLayers, featureOverlayLayer] = (function() { btnClose.setAttribute('type', 'button'); btnClose.title = 'Stäng'; btnClose.setAttribute('aria-label', btnClose.title); - btnClose.onclick = function(event) { - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - if (popover !== null) { - popover.dispose(); - } - }; + btnClose.onclick = disposePopover; header.appendChild(btnPrev); header.appendChild(btnNext); header.appendChild(btnExpand); header.appendChild(btnClose); - const formatFeaturePropertiesToHTML = function(properties) { - /* turn the properties into a fine table */ - const table = document.createElement('table'); - table.classList.add('table', 'table-sm', 'table-borderless', 'table-hover'); - - const tbody = document.createElement('tbody'); - table.appendChild(tbody); - - const def = layers[properties.layer_group + '_' + properties.layer]; - def.popover.forEach(function([desc, key, opts]) { - let v = properties[key]; - if (opts === undefined) { - opts = {}; - } - if (opts.fn !== undefined) { - if (opts.fn === 'length') { - if (v < 1000) { - opts.unit = 'm'; - } else { - v /= 1000.; - v = Math.round(v*100) / 100.; - opts.unit = 'km'; - } - } else if (opts.fn === 'area') { - if (v < 10000) { - opts.unit = 'm²'; - } else if (v < 10000 * 10000) { - v /= 10000.; - opts.unit = 'ha'; - } else { - v /= 1000000.; - opts.unit = 'km²'; - } - v = Math.round(v*100) / 100.; - } else { - v = opts.fn(v); - } - } - if (v === undefined || v === null) { - v = document.createTextNode(''); - } else if (!(v instanceof HTMLElement)) { - if (typeof(v) === 'number' && opts.unit !== undefined) { - v = v.toLocaleString('sv-SE'); - } else if (typeof(v) === 'boolean') { - v = v ? 'Ja' : 'Nej'; - } - if (opts.unit !== undefined && v !== '') { - v += '\u202F' + opts.unit; - } - v = document.createTextNode(v); - } - - const tr = document.createElement('tr'); - tbody.appendChild(tr); - - const td1 = document.createElement('td'); - tr.appendChild(td1); - const textDesc = document.createTextNode(desc); - td1.appendChild(textDesc); - - const td2 = document.createElement('td'); - tr.appendChild(td2); - td2.appendChild(v); - if (opts.classes !== undefined) { - opts.classes.forEach((c) => td2.classList.add(c)); - } - }); - - const content = document.createElement('div'); - if (def.popoverTitle !== undefined) { - const h = document.createElement('h6'); - content.appendChild(h); - const textNode = document.createTextNode(def.popoverTitle); - h.appendChild(textNode); + const measureButton = document.getElementById('measure-button') + .getElementsByTagName('button')[0]; + MAP.on('singleclick', function(event) { + if (measureButton.getAttribute('aria-expanded') === 'true') { + /* skip popover while measuring */ + return; } - - content.appendChild(table); - return content; - }; - - const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; - map.on('singleclick', function(event) { - /* clear the overlay list */ - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - overlayAttributes = []; - overlayAttrIdx = 0; + disposeFeatureOverlay(); /* dispose any pre-existing popover if not in detached mode */ - popover = Popover.getInstance(popup); - if (popover !== null) { - const popoverTip = popover.tip; - if (popoverTip !== null && !popoverTip.classList.contains('popover-detached')) { - popover.dispose(); - } + popover = Popover.getInstance(popupOverlay.element); + if (popover?.tip != null && !popover.tip.classList.contains('popover-detached')) { + popover.dispose(); } - const size = map.getSize(); - if (size[0] < 576 || size[1] < 576) { - return; + if (window.innerWidth < 200) { + return; /* skip popover if the map is too small */ } /* unclear how many feature we'll find, don't render prev/next buttons for now */ @@ -3821,45 +5446,47 @@ const [vectorLayers, featureOverlayLayer] = (function() { btnNext.classList.add('d-none', 'disabled'); /* never start in maximized mode */ - if (popover !== null && popover.tip !== null) { + 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 = [] - map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { + const fetch_body = []; + event.map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { const layerGroup = layer.get('layerGroup'); const layerName = feature.getProperties().layer; - const def = layers[layerGroup + '_' + layerName]; - if (def !== undefined && def.popover !== undefined) { + 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 */ - if (!fetch_body.length) { - document.body.classList.add('inprogress'); - if (popover !== null && popover.tip !== null) { - popover.tip.classList.add('inprogress'); - } - } - fetch_body.push({ - layer_group: layerGroup, - layer: layerName, - fid: feature.getId(), - }); - if (fetch_body.length >= 100) { - return true; /* enough matches already, stop detection here */ - } + 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: (l) => l !== null && l.get('layerGroup') !== undefined, + layerFilter: (lyr) => lyr.get('layerGroup') != null, }); if (fetch_body.length === 0) { - if (popover !== null) { - /* dispose pre-detached popover */ - popover.dispose(); + /* 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; } @@ -3868,18 +5495,26 @@ const [vectorLayers, featureOverlayLayer] = (function() { method: 'POST', body: JSON.stringify(fetch_body), headers: { - 'Content-Type': 'application/json; charset=UTF-8' - } - }).then((resp) => resp.json()) + '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 + overlayAttributes = data; if (overlayAttributes.length === 0) { - if (popover !== null) { - /* dispose pre-detached popover */ - popover.dispose(); + /* couldn't fetch any attribute for feature(s) at pixel */ + if (popover?.tip != null) { + /* dispose pre-detached popover */ + popover.dispose(); } return; } @@ -3891,14 +5526,14 @@ const [vectorLayers, featureOverlayLayer] = (function() { btnPrev.classList.remove('d-none'); pageNode.classList.remove('d-none'); } - if (popover === null || popover.tip === null) { + 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]; - updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.ogc_fid); - popover = new Popover(popup, { + highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); + popover = new Popover(popupOverlay.element, { template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div>' + '<div class="popover-header"></div><div class="popover-body"></div></div>', title: header, @@ -3906,7 +5541,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { html: true, placement: 'right', fallbackPlacements: ['right', 'left', 'bottom', 'top'], - container: container0, + container: CONTAINER_STOPEVENT, }); popover.show(); } @@ -3916,8 +5551,552 @@ const [vectorLayers, featureOverlayLayer] = (function() { 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 <div>, 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 (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; + })(); +})(); |
