/*********************************************************************** * Copyright © 2024 Guilhem Moulin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . **********************************************************************/ import Map from 'ol/Map.js'; import View from 'ol/View.js'; import TileLayer from 'ol/layer/Tile.js'; import WMTS from 'ol/source/WMTS.js'; import WMTSTileGrid from 'ol/tilegrid/WMTS.js'; import FullScreen from 'ol/control/FullScreen.js'; import ScaleLine from 'ol/control/ScaleLine.js'; import Zoom from 'ol/control/Zoom.js'; import ZoomSlider from 'ol/control/ZoomSlider.js'; import Overlay from 'ol/Overlay.js'; import MVT from 'ol/format/MVT.js'; import VectorTileLayer from 'ol/layer/VectorTile.js'; import VectorTile from 'ol/source/VectorTile.js'; import { createXYZ } from 'ol/tilegrid.js'; import VectorLayer from 'ol/layer/Vector.js'; import VectorSource from 'ol/source/Vector.js'; import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; import Icon from 'ol/style/Icon.js'; import RegularShape from 'ol/style/RegularShape.js'; import Stroke from 'ol/style/Stroke.js'; import Style from 'ol/style/Style.js'; import proj4 from 'proj4'; import { get as getProjection } from 'ol/proj.js'; import { register as registerProjection } from 'ol/proj/proj4.js'; import { Modal, Popover } from 'bootstrap'; import './style.css'; proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'); registerProjection(proj4); const projection = getProjection('EPSG:3006'); /* Lantmäteriet uses a tile-scheme where the origin (top-left corner) is * at N8500000 E-1200000 (SWEREF99 TM), where each tile is 256x256 * pixels, and where the resolution at level 0 is 4096m per pixel * (each side is 1048.576km long). * * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning_cache_v1.1.0.pdf * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf * * We set the extent to a 4x4 tiles square at level 2 (1024px = * 1048.576km per side) somehow centered on Norrbotten and Västerbotten, * and zoom in from there. This represent a TILEROW (x) offset of 5, * and a TILECOL (y) offset of 2. */ const extent = [110720, 6927136, 1159296, 7975712]; /* XXX using the topowebbcache WMTS is fine for testing (as it * doesn't require authentication) but not in production in a public * instance as doing so would violate its current terms of use (as * of January 2024 it's not CC0 open data). See * * https://www.lantmateriet.se/sv/om-lantmateriet/Rattsinformation/upphovsratt-och-publicering-av-lantmateriets-geografiska-information/ * https://www.lantmateriet.se/sv/kartor/vara-karttjanster/min-karta/#anchor-2 * https://help.locusmap.eu/topic/support-for-swedish-lantmateriets-min-karta-wms * * More precise background maps might be available in the future * as open data, though: * * https://www.lantmateriet.se/sv/om-lantmateriet/press/nyheter/lantmateriets-arbete-mot-oppna-data-i-full-gang/ */ 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 }), ], target: document.getElementById('map'), }); const popup = document.getElementById('popup'); const featureOverlaySource = new VectorSource(); /* move the control container to the viewport */ const container = document.getElementById('map-control-container'); (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'; })(); /* zoom in/out */ (function() { const zoomInLabel = document.createElement('i'); zoomInLabel.classList.add('bi', 'bi-plus'); const zoomOutLabel = document.createElement('i'); zoomOutLabel.classList.add('bi', 'bi-dash'); const control = new Zoom({ zoomInTipLabel: 'Zooma in', zoomInLabel: zoomInLabel, zoomOutTipLabel: 'Zooma ut', zoomOutLabel: zoomOutLabel, target: document.getElementById('zoom-control'), }); control.element.classList.add('btn-group-vertical'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } map.addControl(control); })(); /* zoom slider */ (function() { const control = new ZoomSlider({ target: document.getElementById('zoom-control'), }); control.element.classList.add('modal'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } map.addControl(control); })(); /* scale line */ (function() { const size = map.getSize(); const control = new ScaleLine({ units: 'metric', minWidth: 150, maxWidth: size[1] < 350 ? size[1] - 50 : 350, target: container, }); control.element.classList.add('modal', 'modal-content'); map.addControl(control); })(); const menu = document.getElementById('map-menu'); const TRAILING_ZEROES = /\.?0*$/; /* "open in new tab" button */ if (window.location !== window.parent.location) { const div = document.createElement('div'); menu.appendChild(div); div.classList.add('ol-unselectable', 'ol-control'); const btn = document.createElement('button'); div.appendChild(btn); btn.type = 'button'; btn.title = 'Öppna karta i ny flik'; btn.setAttribute('aria-label', btn.title); btn.setAttribute('aria-expanded', 'false'); btn.classList.add('btn', 'btn-light'); const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-box-arrow-up-right'); btn.onclick = function(event) { const coordinates = view.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, '')); url.hash = '#' + searchParams.toString(); return window.open(url.href, '_blank'); }; } /* 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'); 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'); } }; } /* fullscreen control */ if (window.location === window.parent.location) { const label = document.createElement('i'); label.classList.add('bi', 'bi-fullscreen'); const labelActive = document.createElement('i'); labelActive.classList.add('bi', 'bi-fullscreen-exit'); const titleInactive = 'Helskärmsläge'; const titleActive = 'Lämna helskärmsläge'; const classInactive = 'btn-light'; const classActive = 'btn-dark'; const control = new FullScreen({ label: label, labelActive: labelActive, tipLabel: titleInactive, keys: true, target: menu, }) const btn = control.element.getElementsByTagName('button')[0]; btn.classList.add('btn', classInactive); btn.setAttribute('aria-label', btn.title); map.addControl(control); control.addEventListener('enterfullscreen', function() { featureOverlaySource.clear(true); const popover = Popover.getInstance(popup); if (popover !== null) { /* dispose popover as entering fullscreen messes up its position */ popover.dispose(); } const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classInactive, classActive); btn.title = titleActive; btn.setAttribute('aria-label', btn.title); const exp = document.getElementById('export-to-image'); if (exp !== undefined) { /* hide export button in fullscreen mode as it exits it */ exp.classList.add('d-none'); } }) control.addEventListener('leavefullscreen', function() { featureOverlaySource.clear(true); const popover = Popover.getInstance(popup); if (popover !== null) { /* dispose popover as is might overflow the viewport */ popover.dispose(); } const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classActive, classInactive); btn.title = titleInactive; btn.setAttribute('aria-label', btn.title); const exp = document.getElementById('export-to-image'); if (exp !== undefined) { exp.classList.remove('d-none'); } }) } /* export/download button */ if (window.location === window.parent.location) { const div = document.createElement('div'); div.classList.add('ol-unselectable', 'ol-control'); div.id = 'export-to-image'; const btn = document.createElement('button'); div.appendChild(btn); btn.classList.add('btn', 'btn-light'); btn.type = 'button'; btn.title = 'Ladda ner som en PNG-fil'; btn.setAttribute('aria-label', btn.title); const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-download'); menu.appendChild(div); btn.onclick = function(event) { map.once('rendercomplete', function() { const canvas0 = document.createElement('canvas'); const size = map.getSize(); canvas0.width = size[0]; canvas0.height = size[1]; const context = canvas0.getContext('2d'); map.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer').forEach(function(canvas) { if (canvas.width > 0) { const opacity = canvas.parentNode.style.opacity || canvas.style.opacity; context.globalAlpha = opacity === '' ? 1 : Number(opacity); context.drawImage(canvas, 0, 0); } }); context.globalAlpha = 1; context.setTransform(1, 0, 0, 1, 0, 0); canvas0.toBlob(function(blob) { const a = document.createElement('a'); a.download = 'karta.png'; a.rel = 'noopener'; a.href = URL.createObjectURL(blob); setTimeout(function() { URL.revokeObjectURL(a.href) }, 4E4); // 40s setTimeout(function() { a.click() }, 0); }); }); map.renderSync(); }; } /* info button */ (function() { const div = document.createElement('div'); menu.appendChild(div); div.id = 'info-button'; div.classList.add('ol-unselectable', 'ol-control'); const btn = document.createElement('button'); div.appendChild(btn); btn.type = 'button'; btn.setAttribute('aria-expanded', 'false'); btn.title = 'Visa information'; btn.setAttribute('aria-label', btn.title); btn.classList.add('btn', 'btn-light'); const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-info-lg'); const panel = document.getElementById('modal-info'); const modal = new Modal(panel, { backdrop: false, }); const backdrop = document.getElementById('modal-info-backdrop'); backdrop.onclick = function(event) { 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'); }); btn.onclick = function(event) { modal.toggle(); }; })(); /* we're all set, show the control container now */ container.setAttribute('aria-hidden', 'false'); view.on('change', function(event) { featureOverlaySource.clear(true); const popover = Popover.getInstance(popup); if (popover !== null) { popover.dispose(); } 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(); }); const layers = { mrr_appr_ec: { popoverTitle: 'Bearbetningskoncession \u2013 beviljad', popover: [ ['Namn', 'Name'], ['Koncessionsmineral', 'Mineral'], ['Ägare', 'Owner'], ['Area', 'Area'], ['Giltig från', 'Valid from'], ['Giltig till', 'Valid to'], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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', 'Applicant'], ['Area', 'Area'], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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', 'Owner'], ['Licence id', 'Licence id', { classes: ['feature-attr-mrr-license-id'] }], ['Area', 'Area'], ['Giltig från', 'Valid from'], ['Giltig till', 'Valid to'], ['Diary nr', 'Diary nr', { classes: ['feature-attr-dnr'] }], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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', 'Applicant'], ['Area', 'Area'], ['Ansökningsdatum', 'Application date'], ['Diary nr', 'Diary nr', { classes: ['feature-attr-dnr'] }], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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', 'Owner'], ['Licence id', 'Licence id', { classes: ['feature-attr-mrr-license-id'] }], ['Area', 'Area'], ['Giltig från', 'Valid from'], ['Giltig till', 'Valid to'], ['Diary nr', 'Diary nr', { classes: ['feature-attr-dnr'] }], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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', 'Applicant'], ['Area', 'Area'], ['Ansökningsdatum', 'Application date'], ['Diary nr', 'Diary nr', { classes: ['feature-attr-dnr'] }], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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'], ['Area', 'Area'], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], 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], }), }); }), }, mrr_appr_pc: { popoverTitle: 'Gällande torvkoncession', popover: [ ['Namn', 'Name'], ['Ägare', 'Owner'], ['Area', 'Area'], ['Giltig från', 'Valid from'], ['Giltig till', 'Valid to'], ['Kommun', 'Municipality'], ['Län', 'County'], ['Senast uppdaterad', 'Last updated'], ], style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { return new Style({ zIndex: 21, fill: new Fill({ color: [65, 40, 27, Math.max((.2-1)/8 * z + 1, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [151, 173, 23, 1], }), }); }), }, svk_lines: { popoverTitle: 'Kraftledning (befintlig)', popover: [ ['Förläggn', 'FÖRLÄGGN'], ['Spänning', 'SPÄNNING', { unit: 'kV' }], ], style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { return new Style({ zIndex: 52, stroke: new Stroke({ color: 'black', width: width, }), }); }), }, svk_pylons: { style: [undefined, undefined, undefined, undefined, undefined] .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { return new Style({ zIndex: 51, image: new CircleStyle({ radius: radius, fill: new Fill({ color: 'black', }), }), }); })), }, svk_planned: { 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_stations: { style: [3, 4, 5, 6, 7, 8.5, 10].map(function(radius) { return new Style({ zIndex: 50, image: new RegularShape({ radius: radius, points: 4, angle: Math.PI/4, fill: new Fill({ color: 'black', }), }), }); }) .concat([.5, 1, 1.5, 2, 2].map(function(width) { return new Style({ zIndex: 50, fill: new Fill({ color: 'rgba(128, 128, 128, .7)', }), stroke: new Stroke({ width: width, color: 'rgb(0, 0, 0)', }), }); })), }, vbk_area_current: { popoverTitle: 'Projekteringsområde för vindbruk', popover: [ ['Projektnamn', 'PROJNAMN'], ['Områdes-ID', 'OMRID', { classes: ['feature-objid'] }], ['Aktuella verk', 'ANTALVERK'], ['Antal ej koordinatsatta verk', 'AntalejXY', { fn: (v) => v || 0 }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Planerad byggstart', 'PBYGGSTART'], ['Planerat drifttagande', 'PDRIFT'], ['Andringsansokan', 'Andringsansokan'], ['Under Byggnation', 'UnderByggnation'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNR', { classes: ['feature-orgnr'] }], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Senast uppdaterat', 'ArendeStatusUppdaterat'], ], 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', 'PROJNAMN'], ['Områdes-ID', 'OMRID', { classes: ['feature-objid'] }], ['Aktuella verk', 'ANTALVERK'], ['Antal ej koordinatsatta verk', 'AntalejXY', { fn: (v) => v || 0 }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Planerad byggstart', 'PBYGGSTART'], ['Planerat drifttagande', 'PDRIFT'], ['Andringsansokan', 'Andringsansokan'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNR', { classes: ['feature-orgnr'] }], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Senast uppdaterat', 'ArendeStatusUppdaterat'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Uppförandedatum', 'UPPFORT'], ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand_TIDSBEGRANS_DAT'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand_TIDSBEGRANS_DAT'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand_TIDSBEGRANS_DAT'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand_TIDSBEGRANS_DAT'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Uppförandedatum', 'UPPFORT'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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', 'PROJNAMN'], ['Status', 'STATUS'], ['Handlingstyp', 'HANDLINGSTYP'], ['Totalhöjd', 'TOTALHOJD', { unit: 'm' }], ['Navhöjd', 'NAVHOJD', { unit: 'm' }], ['Rotordiameter', 'ROTDIAMETER', { unit: 'm' }], ['Maxeffekt', 'MAXEFFEKT', { unit: 'MW' }], ['Beräknad årsproduktion', 'CALPROD', { unit: 'GWh' }], ['Fabrikat', 'FABRIKAT'], ['Modell', 'MODELL'], ['Organisationsnamn', 'ORGNAMN'], ['Organisationsnummer', 'ORGNMR', { classes: ['feature-orgnr'] }], ['Placering', 'PLACERING'], ['Kommun', 'KOMNAMN'], ['Län', 'LANSNAMN'], ['Elområde', 'EL_NAMN'], ['Datum för senaste uppdatering av verk', 'SenasteUppdatering'], ], 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'] }], ['Objekt-ID', 'OBJECTID', { 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', 'Arealha', { 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: 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'] }], ['Objekt-ID', 'OBJECTID', { 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'], ['Kommun', 'Kommun'], ['Län', 'Lan'], ['Ärendestatus', 'ArendeStat'], ['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, }), }); }), }, st_renbete: { popoverTitle: 'Samebyarnas betesområde', popover: [ ['Sameby', 'NAMN'], ['Samebys typ', 'SAMEBY_TYP'], ['Objekt-ID', 'OBJECTID', { classes: ['feature-objid'] }], ['Sameby-ID', 'SAMEBY_ID', { classes: ['feature-objid'] }], ['By-ID', 'BY_ID', { classes: ['feature-objid'] }], ['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], }), }); }), }, st_flyttled: { popoverTitle: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', popover: [ ['Sameby #1', 'SAMEBY1'], ['Sameby #2', 'SAMEBY2'], ['Sameby #3', 'SAMEBY3'], ['Beskrivning', 'BESKRIVNIN'], ['Årstid', 'ARSTID'], ['Sameby #1 Nr', 'BYNR1', { classes: ['feature-objid'], fn: (v) => v === 0 ? '' : v }], ['Sameby #2 Nr', 'BYNR2', { classes: ['feature-objid'], fn: (v) => v === 0 ? '' : v }], ['Sameby #3 Nr', 'BYNR3', { classes: ['feature-objid'], fn: (v) => v === 0 ? '' : v }], ['Led-ID', 'LED_ID', { classes: ['feature-objid'], fn: (v) => v === 0 ? '' : v }], ['Objekt-ID', 'OBJECTID', { classes: ['feature-objid'] }], ['Riksintresse', 'RIKSINTR'], ['Fast led', 'FAST_LED'], ['Aktualitet', 'AKTUALITET'], ['Signatur', 'SIGNATUR'], ['Globalt ID', 'GlobalID', { classes: ['feature-objid'] }], ], 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], }), }); }), }, st_riks_ren: { popoverTitle: 'Riksintresse rennäring', popover: [ ['Objekt-ID', 'OBJECTID', { classes: ['feature-objid'] }], ['Lagrum', 'LAGRUM'], ['Aktualitet', 'AKTUALITET'], ['Signatur', 'SIGNATUR'], ['Globalt ID', 'GlobalID', { classes: ['feature-objid'] }], ], 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], }), }); }), }, st_riks_ren_core: { popoverTitle: '(Kärn)områden av riksintresse rennäring', popover: [ ['Objekt-ID', 'OBJECTID', { classes: ['feature-objid'] }], ['Områdes-ID', 'OMR_NR', { classes: ['feature-objid'] }], ['Länk', 'LANK'], ['Årets runt', 'ARET_RUNT'], ['Sameby', 'SAMEBY'], ['Ansvarig', 'ANSVARIG'], ['Aktualitet', 'AKTUALITET'], ['Signatur', 'SIGNATUR'], ['Globalt ID', 'GlobalID', { classes: ['feature-objid'] }], ['Area', 'AREA_HA', { unit: 'ha' }], ['Länskod', 'LANSKOD'], ], style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { return new Style({ zIndex: 5, fill: new Fill({ color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], }), stroke: width === 0 ? undefined : new Stroke({ width: width, color: [179, 153, 102, 1], }), }); }), }, /* Documentation at * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf * */ smhi_dam: { popoverTitle: 'Damm', popover: [ ['Dammenhetens namn', 'dnamn'], ['Dammanläggningens namn', 'namn'], ['Länsnr', 'lst_objid', { classes: ['feature-objid'] }], ['Status', 'status', { fn: (v) => v === 0 ? '' : v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : v }], ['Regleringstyp', 'regl_typ', { fn: (v) => v === 0 ? '' : v }], ['Byggår', 'byggar', { fn: (v) => v === 0 ? '' : v }], ['Dammhöjd', 'dammhojd', { unit: 'm', fn: (v) => v === 0 ? '' : v }], ['Krönlängd', 'kron', { unit: 'm', fn: (v) => v === 0 ? '' : v }], ['Fiskväg', 'fiskvag', { fn: (v) => v === 0 ? '' : 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' : v }], ['Huvudavrinningsområdesnummer', 'haro', { classes: ['feature-objid'] } ], ['Vattendistrikt', 'RBD', { classes: ['feature-objid'] } ], ['Verksamhet', 'verksmht', { fn: (v) => v === 0 ? '' : 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' : v }], ['Högsta dämningsgräns', 'dg', { unit: 'm', fn: (v) => v === 0 ? '' : v }], ['Lägsta sänkningsgräns', 'sg', { unit: 'm', fn: (v) => v === 0 ? '' : v }], ['Magasinsyta', 'my', { unit: 'km²', fn: (v) => v === 0 ? '' : v }], ['Reglerbar volym', 'my', { unit: 'Mm³', fn: (v) => v === 0 ? '' : v }], ['Kommentar', 'kommentar'], ['Damm-ID', 'dammid', { classes: ['feature-objid'] }], ], 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: { 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, fill: new Fill({ color: 'rgb(152, 78, 163)', }), stroke: new Stroke({ width: Math.log2(width) * 2/5, color: 'rgb(119, 61, 128)', }), }), }); }) .concat([1.5, 2, 2, 2, 2, 2].map(function(width) { return new Style({ zIndex: 58, fill: new Fill({ color: 'rgba(152, 78, 163, .4)', }), stroke: new Stroke({ width: width, color: 'rgb(119, 61, 128)', }), }); })), } }; const layerHierarchy = [ { text: 'Transmissionsnät för el', children: [ { text: 'Kraftledningar (befintliga)', layer: ['svk_lines', 'svk_pylons'], }, { text: 'Stationer', layer: 'svk_stations', }, { text: 'Transmissionsnätsprojekt ', layer: 'svk_planned', }, ], }, { text: 'Stora industrisatsningar', layer: 'gigafactories', }, { text: 'Dammar', layer: 'smhi_dam', }, { text: 'Mineralrättigheter', children: [ { text: 'Bearbetningskoncessioner', children: [ { text: 'Beviljad', layer: 'mrr_appr_ec', }, { text: 'Ansökt', layer: 'mrr_appl_ec', }, ], }, { text: 'Undersökningstillstånd, olja, gas och diamant', children: [ { text: 'Beviljad', layer: 'mrr_appr_ogd', }, { text: 'Ansökt', layer: 'mrr_appl_ogd', }, ], }, { text: 'Undersökningstillstånd, metaller och industrimineral', children: [ { text: 'Beviljad', layer: 'mrr_appr_met', }, { text: 'Ansökt', layer: 'mrr_appl_met', }, ], }, { text: 'Markanvisningar till koncession', layer: 'mrr_appr_dl', }, { text: 'Gällande torvkoncessioner', layer: 'mrr_appr_pc', }, ], }, { text: 'Vindbruk', children: [ { text: 'Projekteringsområden', children: [ { text: 'Aktuella', layer: 'vbk_area_current', }, { text: 'Ej aktuella', layer: 'vbk_area_notcurrent', }, ], }, { text: 'Vindkraftverk', children: [ { text: 'Uppförda', layer: 'vbk_station_completed', }, { text: 'Handläggs', layer: 'vbk_station_processed', }, { text: 'Beviljade', layer: 'vbk_station_approved', }, { text: 'Inte längre aktuella/återkallade', layer: 'vbk_station_revoked', }, { text: 'Avslagna/nekad', layer: 'vbk_station_rejected', }, { text: 'Nedmonterade', layer: 'vbk_station_dismounted', }, { text: 'Överklagade', layer: 'vbk_station_appealed', }, ], }, ] }, { text: 'Skogsbruk', children: [ { text: 'Uppförda (sedan 2000)', layer: 'sks_clearcut_comp', }, { text: 'Anmälda', layer: 'sks_clearcut_appl', }, ] }, { /* Definitions at * https://ext-dokument.lansstyrelsen.se/Gemensamt/Geodata/Datadistribution/Information,%20Skiktf%C3%B6rteckning%20och%20f%C3%B6rklaringar.pdf */ text: 'Rennäringen', children: [ { text: 'Betesområden', layer: 'st_renbete', }, { text: 'Flyttled', layer: 'st_flyttled', }, { text: 'Riksintressen', layer: 'st_riks_ren', }, { text: '(Kärn)områden av riksintresse', layer: 'st_riks_ren_core', }, ] } ]; const vectorSource = new VectorTile({ url: '/tiles/a/{z}/{x}/{y}.pbf', format: new MVT(), projection: projection, wrapX: false, transition: 0, tileGrid: createXYZ({ extent: extent, tileSize: 1024, maxResolution: 1024, /* = 1048576/1024 */ minZoom: 0, maxZoom: 9, }), }); const vectorSource2 = new VectorTile({ url: '/tiles/b/{z}/{x}/{y}.pbf', format: new MVT(), projection: projection, wrapX: false, transition: 0, tileGrid: createXYZ({ extent: extent, tileSize: 1024, maxResolution: 1024, /* = 1048576/1024 */ minZoom: 0, maxZoom: 9, }), }); 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; } return result; }, {}); })(); const vectorLayer = new VectorTileLayer({ source: vectorSource, /* 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: false, style: function(feature, resolution) { const style = styles[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]; } }, }); map.addLayer(vectorLayer); const vectorLayer2 = new VectorTileLayer({ source: vectorSource2, renderMode: 'hybrid', declutter: false, visible: false, style: function(feature, resolution) { const style = styles[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]; } }, }); map.addLayer(vectorLayer2); /* layer selection panel */ (function() { const modal = document.getElementById('layer-selection-panel'); modal.classList.add('modal'); modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-hidden', 'true'); const content = document.createElement('div'); modal.appendChild(content); content.classList.add('modal-content'); const body = document.createElement('div'); content.appendChild(body); body.classList.add('modal-body'); const accordion = document.createElement('div'); body.appendChild(accordion); accordion.id = 'layer-selection-accordion'; accordion.classList.add('accordion', 'accordion-flush'); (function collectLayers(list) { list.forEach(function(elem) { elem._layers = elem.layer === undefined ? [] : Array.isArray(elem.layer) ? elem.layer : [elem.layer]; if (elem.children !== undefined && elem.children.length > 0) { collectLayers(elem.children); elem.children.forEach(function(child) { child._layers.forEach((l) => elem._layers.push(l)); }); } }); })(layerHierarchy); const setIndeterminateAndChecked = function(list) { return list.forEach(function(elem) { const layerStyles = elem._layers.map((lyr) => styles[lyr] !== undefined); elem._input.indeterminate = elem._layers.length <= 1 ? false : layerStyles.slice(1).some((v) => v !== layerStyles[0]); if (!elem._input.indeterminate) { /* keep checked value if indeterminate */ elem._input.checked = layerStyles.every((v) => v); } if (elem.children !== undefined && elem.children.length > 0) { setIndeterminateAndChecked(elem.children); } }); }; /* TODO refactor */ const layerList1 = Object.keys(layers).filter((l) => !l.startsWith('sks_') && !l.startsWith('st_')); const layerList2 = Object.keys(layers).filter((l) => l.startsWith('sks_') || l.startsWith('st_')); const fixLayerVisibility = function() { vectorLayer .setVisible(layerList1.some((lyr) => styles[lyr] !== undefined)); vectorLayer2.setVisible(layerList2.some((lyr) => styles[lyr] !== undefined)); }; const onClickFunction = function(layerList, event) { 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); } }); } else { layerList.forEach(function(lyr) { delete styles[lyr]; }); layersParams = layersParams.filter((lyr) => !layerList.includes(lyr)); } setIndeterminateAndChecked(layerHierarchy); fixLayerVisibility(); vectorSource.changed(); vectorSource2.changed(); searchParams.set('layers', layersParams.join(' ')); location.hash = '#' + searchParams.toString(); }; let layerId = 0; const addAccordionGroup = function(parentNode, children) { const ul = document.createElement('ul'); parentNode.appendChild(ul); ul.classList.add('list-group', 'list-group-flush'); children.forEach(function(child) { const li = document.createElement('li'); ul.appendChild(li); li.classList.add('list-group-item'); const div = document.createElement('div'); li.appendChild(div); div.classList.add('d-inline-flex'); const input = child._input = document.createElement('input'); div.appendChild(input); input.classList.add('form-check-input'); input.type = 'checkbox'; input.id = 'layer' + layerId++; const label = document.createElement('label'); div.appendChild(label); label.classList.add('form-check-label'); label.setAttribute('for', input.id); const textNode = document.createTextNode(child.text); label.appendChild(textNode); if (child.children !== undefined && child.children.length > 0) { addAccordionGroup(li, child.children); } input.onclick = function(event) { return onClickFunction(child._layers, event); }; }); }; layerHierarchy.forEach(function(x, idx) { const item = document.createElement('div'); accordion.appendChild(item); item.classList.add('accordion-item'); const header = document.createElement('div'); item.appendChild(header); header.classList.add('accordion-header'); const btn = document.createElement('button'); header.appendChild(btn); const collapse = document.createElement('div'); item.appendChild(collapse); collapse.id = 'accordion-collapse-' + idx; collapse.classList.add('accordion-collapse', 'collapse'); /* never expand more than accordion at a time */ collapse.setAttribute('data-bs-parent', '#' + accordion.id); btn.type = 'button'; btn.setAttribute('data-bs-toggle', 'collapse'); btn.setAttribute('data-bs-target', '#' + collapse.id); btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-controls', collapse.id); btn.classList.add('accordion-button', 'collapsed'); const span0 = document.createElement('span'); btn.appendChild(span0); span0.classList.add('form-check'); span0.setAttribute('data-bs-toggle', 'collapse'); span0.setAttribute('data-bs-target', ''); const input0 = x._input = document.createElement('input'); span0.appendChild(input0); input0.classList.add('form-check-input'); input0.type = 'checkbox'; input0.id = 'layer' + layerId++; const label0 = document.createElement('label'); span0.appendChild(label0); label0.classList.add('form-check-label'); label0.setAttribute('for', input0.id); const text0 = document.createTextNode(x.text); label0.appendChild(text0); if (x.children === undefined || x.children.length === 0) { item.replaceChild(span0, header); } else { const body = document.createElement('div'); collapse.appendChild(body); body.classList.add('accordion-body'); addAccordionGroup(body, x.children); } input0.onclick = function(event) { return onClickFunction(x._layers, event); }; }); setIndeterminateAndChecked(layerHierarchy); fixLayerVisibility(); (function() { const item = document.createElement('div'); accordion.appendChild(item); item.classList.add('accordion-item'); const div = document.createElement('div'); item.appendChild(div); div.classList.add('form-check', 'form-switch'); const input = document.createElement('input'); div.appendChild(input); input.classList.add('form-check-input'); input.type = 'checkbox'; input.setAttribute('role', 'switch'); input.id = 'layer' + layerId++; const label = document.createElement('label'); div.appendChild(label); label.classList.add('form-check-label'); label.setAttribute('for', input.id); label.innerHTML = 'Nedtonad bakgrund karta'; input.checked = baseMapLayer === '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(); }; })(); })(); /* 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'); body.innerHTML = 'legend TODO'; })(); /* popup and feature overlays */ (function() { const popupOverlay = new Overlay({ stopEvent: true, element: popup, }); map.addOverlay(popupOverlay); map.addLayer(new VectorLayer({ source: featureOverlaySource, style: new Style({ stroke: new Stroke({ color: 'rgba(0, 255, 255, .8)', width: 3, }), }), })); const features = []; let popover, featureNum = 0; const header = document.createElement('div'); header.classList.add('d-flex'); const headerGrabbingArea = document.createElement('div'); headerGrabbingArea.classList.add('flex-grow-1', 'grabbing-area', 'pe-2', 'me-2'); header.appendChild(headerGrabbingArea); headerGrabbingArea.onmousedown = function(event) { if (event.button != 0) { return; } const popoverTip = Popover.getInstance(popup).tip; pageNode.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'; style.transform = ''; style.inset = `${rect.top}px auto auto ${rect.left}px`; style.display = ''; } let clientX = event.clientX, clientY = event.clientY; document.onmousemove = function(event) { const offsetX = clientX - event.clientX; const offsetY = clientY - event.clientY; clientX = event.clientX; clientY = event.clientY; popoverTip.style.top = (popoverTip.offsetTop - offsetY).toString() + 'px'; popoverTip.style.left = (popoverTip.offsetLeft - offsetX).toString() + 'px'; }; document.onmouseup = function(event) { if (event.button != 0) { return; } pageNode.classList.remove('grabbing-area-grabbed'); document.onmousemove = null; document.onmouseup = null; }; }; const pageNode = document.createElement('h6'); headerGrabbingArea.appendChild(pageNode); const pageNum = document.createElement('span'); const pageCount = document.createElement('span'); pageNode.appendChild(document.createTextNode('Träff ')); pageNode.appendChild(pageNum); pageNode.appendChild(document.createTextNode(' av ')); pageNode.appendChild(pageCount); const onClickPageChange = function(event, offset) { const btn = event.target; if (btn.classList.contains('disabled') || popover === null || popover.tip === null) { return; } if (featureNum + offset < 0 || featureNum + offset > features.length - 1) { return; } featureNum += offset; if (featureNum < 1) { btnPrev.classList.add('disabled'); } else { btnPrev.classList.remove('disabled'); } if (featureNum < features.length - 1) { btnNext.classList.remove('disabled'); } else { btnNext.classList.add('disabled'); } const x = features[featureNum]; featureOverlaySource.clear(true); featureOverlaySource.addFeature(x.feature); pageNum.innerHTML = (featureNum + 1).toString(); popover.tip.getElementsByClassName('popover-body')[0]. replaceChildren(x.formattedContent); setTimeout(function() { btn.blur() }, 100); }; const btnPrev = document.createElement('button'); btnPrev.classList.add('popover-button', 'popover-button-prev'); btnPrev.setAttribute('type', 'button'); btnPrev.title = 'Föregående träff'; btnPrev.setAttribute('aria-label', btnPrev.title); btnPrev.onclick = function(event) { return onClickPageChange(event, -1); }; const btnNext = document.createElement('button'); btnNext.classList.add('popover-button', 'popover-button-next'); btnNext.setAttribute('type', 'button'); btnNext.title = 'Nästa träff'; btnNext.setAttribute('aria-label', btnNext.title); btnNext.onclick = function(event) { return onClickPageChange(event, +1); }; const btnClose = document.createElement('button'); btnClose.classList.add('popover-button', 'popover-button-close'); btnClose.setAttribute('type', 'button'); btnClose.title = 'Stäng'; btnClose.setAttribute('aria-label', btnClose.title); btnClose.onclick = function(event) { featureOverlaySource.clear(true); if (popover !== null) { popover.dispose(); } }; header.appendChild(btnPrev); header.appendChild(btnNext); header.appendChild(btnClose); const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; map.on('singleclick', function(event) { /* clear the features list */ featureOverlaySource.clear(true); features.length = 0; featureNum = 0; /* dispose any pre-existing popover */ popover = Popover.getInstance(popup); if (popover !== null) { popover.dispose(); } const size = map.getSize(); if (size[0] < 576 || size[1] < 576) { return; } /* unclear how many feature we'll find, don't render prev/next buttons for now */ pageNode.classList.add('d-none'); btnPrev.classList.add('d-none', 'disabled'); btnNext.classList.add('d-none', 'disabled'); map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { const properties = feature.getProperties(); const def = layers[properties.layer]; if (def === undefined || def.popover === undefined) { /* skip layers which didn't opt-in for popover */ return; } /* 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); def.popover.forEach(function([desc, key, opts]) { let v = properties[key]; if (opts === undefined) { opts = {}; } if (opts.fn !== undefined) { v = opts.fn(v); } if (opts.unit !== undefined && v !== undefined && v !== '') { v += '\u202F' + opts.unit; } if (v === undefined) { v = document.createTextNode(''); } else if (!(v instanceof HTMLElement)) { 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); } // console.log(properties); content.appendChild(table); features.push({feature: feature, formattedContent: content}); pageCount.innerHTML = features.length.toString(); if (features.length == 2) { /* there are ≥2 features, render prev/pre buttons */ btnNext.classList.remove('d-none', 'disabled'); btnPrev.classList.remove('d-none'); pageNode.classList.remove('d-none'); } if (popover === null || popover.tip === null) { /* create a new popover if we're not already showing one */ pageNum.innerHTML = (featureNum + 1).toString(); popupOverlay.setPosition(event.coordinate); popover = new Popover(popup, { template: '', title: header, content: content, html: true, placement: 'right', fallbackPlacements: ['right', 'left', 'bottom', 'top'], container: container0, }); popover.show(); featureOverlaySource.addFeature(feature); } }, { hitTolerance: 5, checkWrapped: false, layerFilter: (l) => l === vectorLayer || l === vectorLayer2, }); }); }());