diff options
author | Guilhem Moulin <guilhem@fripost.org> | 2025-06-14 22:12:57 +0200 |
---|---|---|
committer | Guilhem Moulin <guilhem@fripost.org> | 2025-06-15 18:34:05 +0200 |
commit | fd662f3d3fac0f8b11e8e883409b1828cbfca3bd (patch) | |
tree | 3f5b8b1760f8483f5ba1e4714debeee79e52d09b | |
parent | 0fc7bdd8bf374c36fa0ba27702d1fafed09277ac (diff) |
Undo splitting out to multiple files.
The reason is that we want the different modules to produce side-effects
(to avoid creating functions and keeping references to it) and we
therefore need to control the order in which they are inlined during
`vite build`. Unfortunately this doesn't seem to be possible right now,
cf. https://github.com/storybookjs/storybook/issues/30768 .
This reverts commits 670bba058d83620abdb3e8db5fd4ea89dba08142,
05a018f27aba3a20fd581cb88daa8afbbd3407de and
0fc7bdd8bf374c36fa0ba27702d1fafed09277ac.
-rw-r--r-- | main.js | 3989 | ||||
-rw-r--r-- | src/layers.js | 1908 | ||||
-rw-r--r-- | src/map.js | 103 | ||||
-rw-r--r-- | src/popover.js | 1348 | ||||
-rw-r--r-- | style.css (renamed from src/style.css) | 0 |
5 files changed, 3645 insertions, 3703 deletions
@@ -15,14 +15,22 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. **********************************************************************/ +import Map from 'ol/Map.js'; +import View from 'ol/View.js'; +import TileLayer from 'ol/layer/Tile.js'; import TileLayerGL from 'ol/layer/WebGLTile.js'; + +import WMTS from 'ol/source/WMTS.js'; import GeoTIFF from 'ol/source/GeoTIFF.js'; +import WMTSTileGrid from 'ol/tilegrid/WMTS.js'; import FullScreen from 'ol/control/FullScreen.js'; import ScaleLine from 'ol/control/ScaleLine.js'; import Zoom from 'ol/control/Zoom.js'; import ZoomSlider from 'ol/control/ZoomSlider.js'; +import Overlay from 'ol/Overlay.js'; + import MVT from 'ol/format/MVT.js'; import VectorTileLayer from 'ol/layer/VectorTile.js'; import VectorTile from 'ol/source/VectorTile.js'; @@ -33,173 +41,130 @@ import Polygon from 'ol/geom/Polygon.js'; import LineString from 'ol/geom/LineString.js'; import Point from 'ol/geom/Point.js'; +import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; import Icon from 'ol/style/Icon.js'; +import RegularShape from 'ol/style/RegularShape.js'; +import Stroke from 'ol/style/Stroke.js'; +import Style from 'ol/style/Style.js'; -import { Modal } from 'bootstrap'; - -import { map, baseMapSource, extent, projection } from './src/map.js'; -import { layers } from './src/layers.js'; -import { disposePopover } from './src/popover.js'; -import './src/style.css'; - -const age_filter_settings = { - active: false, - type: 'relative', - operator: '<=', - quantity: 1, - unit: 'y', - show_unknown: false, - get_relative_date: 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; - }, - _min_ts: null, - _max_ts: null, - _date_to_ts: function(d) { - if (d == null) { - return 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); - }, - setup_minmax: function() { - this._min_ts = this._max_ts = null; - switch (this.type) { - case 'relative': { - const date = this.get_relative_date(this.quantity, this.unit); - const prop = {'<=':'_min_ts', '>=':'_max_ts'}[this.operator]; - this[prop] = this._date_to_ts(date); - break; - } - case 'interval': { - this._min_ts = this._date_to_ts(this.from); - this._max_ts = this._date_to_ts(this.to); - break; - } - } - }, -}; +import proj4 from 'proj4'; +import { get as getProjection } from 'ol/proj.js'; +import { register as registerProjection } from 'ol/proj/proj4.js'; -let baseMapLayer = 'topowebb_nedtonad'; -(function() { - const params = new URLSearchParams(window.location.hash.substring(1)); - const x = parseFloat(params.get('x')); - const y = parseFloat(params.get('y')); - if (!isNaN(x) && !isNaN(y)) { - map.getView().setCenter([x, y]); - } - const z = parseFloat(params.get('z')); - if (!isNaN(z)) { - map.getView().setZoom(z); - } - if (!params.has('layers') || (!params.get('layers').match(/^\s*$/) && - /* compat redirect/layer subst for old non-hierachical names */ - !params.get('layers').split(' ').some((l) => l.includes('.')))) { - params.set('layers', [ - 'svk.ledningar', - 'svk.stolpar', - 'svk.stationer', - 'svk.transmissionsnatsprojekt', - 'misc.gigafactories', - 'misc.dammar', - 'mrr.appr_ec', - 'mrr.appl_ec', - 'mrr.appr_ogd', - 'mrr.appl_ogd', - 'mrr.appr_met', - 'mrr.appl_met', - 'mrr.appr_dl', - 'vbk.area_current', - 'vbk.area_notcurrent', - ].join(' ')); - location.hash = '#' + params.toString(); - } +import { Modal, Popover } from 'bootstrap'; - if (params.has('basemap')) { - baseMapLayer = params.get('basemap'); - } - baseMapSource.setUrl(`https://minkarta.lantmateriet.se/map/topowebbcache?LAYER=${encodeURIComponent(baseMapLayer)}`); +import './style.css'; - if (params.has('age-filter')) { - (function(param) { - if (param === '') { - return; - } - /* eslint-disable-next-line no-useless-escape */ - const m0 = /^([ +\-]?)([0-9]+)([dwmy])$/.exec(param); - if (m0 != null) { - age_filter_settings.type = 'relative'; - age_filter_settings.operator = (m0[1] === ' ' || m0[1] === '+' || m0[1] === '') ? '>=' - : m0[1] === '-' ? '<=' - : null; - age_filter_settings.quantity = parseInt(m0[2], 10); - age_filter_settings.unit = m0[3]; - age_filter_settings.setup_minmax(); - age_filter_settings.active = true; - return; - } - const m1 = /^([0-9]{8})-([0-9]{8})$/.exec(param); /* 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"> */ - ); - age_filter_settings.type = 'interval'; - age_filter_settings.from = parse_date(m1[1]); - age_filter_settings.to = parse_date(m1[2]); - age_filter_settings.setup_minmax(); - age_filter_settings.active = true; - return; - } - console.log(`Ignoring invalid value for 'age-filter' parameter: ${param}`); - })(params.get('age-filter')); - } - if (params.has('show-unknown-age')) { - const param = params.get('show-unknown-age'); - if (param === '0') { - age_filter_settings.show_unknown = false; - } else if (param === '1') { - age_filter_settings.show_unknown = true; - } - } -})(); -map.setTarget(document.getElementById('map')); +proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'); +registerProjection(proj4); + +const PROJECTION = getProjection('EPSG:3006'); + +/* Lantmäteriet uses a tile-scheme where the origin (upper-left corner) is at + * N8500000 E-1200000 (SWEREF99 TM), where each tile is 256×256 pixels, and where + * the resolution at level 0 is 4096m per pixel (each side is 1048.576km long). + * + * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning_cache_v1.1.0.pdf + * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf + * + * We set the extent to a 4×4 tiles square at level 2 (1024px = 1048.576km per + * side) somehow centered on Norrbotten and Västerbotten, and zoom in from there. + * This represent a TILEROW (x) offset of 5, and a TILECOL (y) offset of 2. + */ +const EXTENT = [110720, 6927136, 1159296, 7975712]; + +/* XXX using the topowebbcache WMTS is fine for testing (as it doesn't require + * authentication) but not in production in a public instance as doing so would + * violate its current terms of use (as of January 2024 it's not CC0 open data). + * See + * + * https://www.lantmateriet.se/sv/om-lantmateriet/Rattsinformation/upphovsratt-och-publicering-av-lantmateriets-geografiska-information/ + * https://www.lantmateriet.se/sv/kartor/vara-karttjanster/min-karta/#anchor-2 + * https://help.locusmap.eu/topic/support-for-swedish-lantmateriets-min-karta-wms + * + * More precise background maps might be available in the future as open data, + * though: + * + * https://www.lantmateriet.se/sv/om-lantmateriet/press/nyheter/lantmateriets-arbete-mot-oppna-data-i-full-gang/ + * https://ext-geodatakatalog.lansstyrelsen.se/GeodataKatalogen/srv/swe/catalog.search#/map uses + * https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/3c3a9cf47e7cb5ea24542d40d19698/?layer=topowebb&style=default&tilematrixset=3006&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=7&TileCol=237&TileRow=155 + */ +const [BASEMAP, MAP] = (function() { + const param = 'basemap'; + const baseMap = Object.seal({ + _layer: new URLSearchParams(location.hash.substring(1))?.get?.(param) ?? 'topowebb_nedtonad', + get layer() { + return this._layer; + }, + get url() { + return 'https://minkarta.lantmateriet.se/map/topowebbcache?' + + 'LAYER=' + encodeURIComponent(this.layer); + }, + set layer(layername) { + this._layer = layername; + baseMapSource.setUrl(this.url); + const searchParams = new URLSearchParams(location.hash.substring(1)); + searchParams.set(param, layername); + location.hash = '#' + searchParams.toString(); + }, + }); + const baseMapSource = new WMTS({ + url: baseMap.url, + version: '1.0.0', + style: 'default', + matrixSet: '3006', + format: 'image/png', + tileGrid: new WMTSTileGrid({ + extent: EXTENT, + // https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf + tileSize: 256, + origin: [-1200000, 8500000], + resolutions: [4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8], + matrixIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }), + projection: PROJECTION, + wrapX: false, + crossOrigin: 'anonymous', + }); + + const view = new View({ + projection: PROJECTION, + extent: EXTENT, + showFullExtent: true, + /* center of the bbox of the Norrbotten and Västerbotten geometries */ + center: [694767.48, 7338176.57], + zoom: 1, + enableRotation: false, + resolutions: [1024, 512, 256, 128, 64, 32, 16, 8], + constrainResolution: false, + }); + return [ + baseMap, + new Map({ + controls: [], + view: view, + layers: [ + new TileLayer({ + source: baseMapSource + }), + ], + target: document.getElementById('map'), + }), + ]; +})(); /* move the control container to the viewport */ -const container = 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('info-modal')); + CONTAINER_STOPEVENT.appendChild(document.getElementById('zoom-control')); + CONTAINER_STOPEVENT.appendChild(CONTAINER_MAP); + CONTAINER_STOPEVENT.appendChild(document.getElementById('info-modal')); const info_backdrop = document.createElement('div'); - container0.appendChild(info_backdrop); + CONTAINER_STOPEVENT.appendChild(info_backdrop); info_backdrop.id = 'info-modal-backdrop'; const age_filter = document.createElement('div'); @@ -207,10 +172,10 @@ const container = document.getElementById('map-control-container'); age_filter.classList.add('modal'); age_filter.setAttribute('tabindex', '-1'); age_filter.setAttribute('aria-hidden', 'true'); - container0.appendChild(age_filter); + CONTAINER_STOPEVENT.appendChild(age_filter); const age_filter_backdrop = document.createElement('div'); age_filter_backdrop.id = 'age-filter-modal-backdrop'; - container0.appendChild(age_filter_backdrop); + CONTAINER_STOPEVENT.appendChild(age_filter_backdrop); })(); /* zoom in/out */ @@ -233,7 +198,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 */ @@ -245,29 +210,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'); @@ -283,12 +248,12 @@ if (window.location !== window.parent.location) { i.classList.add('bi', 'bi-box-arrow-up-right'); btn.onclick = function() { - const coordinates = map.getView().getCenter(); + const coordinates = MAP.getView().getCenter(); const url = new URL(window.location.href); const searchParams = new URLSearchParams(url.hash.substring(1)); searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, '')); searchParams.set('y', coordinates[1].toFixed(2).replace(TRAILING_ZEROES, '')); - searchParams.set('z', map.getView().getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); + searchParams.set('z', MAP.getView().getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); url.hash = '#' + searchParams.toString(); return window.open(url.href, '_blank'); }; @@ -302,7 +267,7 @@ if (window.location === window.parent.location) { {id: 'age-filter', title: 'Filtrera objekt efter ålder', bi: 'clock-history'}, ].map(function(x) { const div = document.createElement('div'); - menu.appendChild(div); + MENU.appendChild(div); div.id = x.id + '-button'; div.classList.add('ol-unselectable', 'ol-control'); @@ -362,16 +327,16 @@ 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.addEventListener('enterfullscreen', function() { /* dispose popover as entering fullscreen messes up its position */ - disposePopover() + disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classInactive, classActive); @@ -383,7 +348,7 @@ 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() { /* dispose popover as is might overflow the viewport */ disposePopover(); @@ -397,7 +362,7 @@ if (window.location === window.parent.location) { if (exp !== undefined) { exp.classList.remove('d-none'); } - }) + }); } /* export/download button */ @@ -416,17 +381,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() { - map.once('rendercomplete', 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); @@ -447,14 +412,14 @@ if (window.location === window.parent.location) { }); }); - map.renderSync(); + MAP.renderSync(); }; } /* info button */ (function() { const div = document.createElement('div'); - menu.appendChild(div); + MENU.appendChild(div); div.id = 'info-button'; div.classList.add('ol-unselectable', 'ol-control'); @@ -569,7 +534,7 @@ if (window.location === window.parent.location) { /* show creation time of the MVT layers */ const li = document.createElement('li'); li.classList.add('list-group-item', 'text-muted'); - ul.appendChild(li) + ul.appendChild(li); const i = document.createElement('i'); i.classList.add('bi', 'bi-map'); li.appendChild(i); @@ -603,9 +568,9 @@ if (window.location === window.parent.location) { const li = document.createElement('li'); li.classList.add('list-group-item'); - ul.appendChild(li) + ul.appendChild(li); const h = document.createElement('h6'); - li.appendChild(h) + li.appendChild(h); if (x.description != null) { const t = document.createTextNode(x.description); h.appendChild(t); @@ -613,14 +578,14 @@ if (window.location === window.parent.location) { if (x.copyright != null) { const p = document.createElement('p'); - li.appendChild(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) + li.appendChild(p); p.appendChild(document.createTextNode('Licensvillkor: ')); const t = document.createTextNode(x.license.name); if (x.license.url == null) { @@ -636,7 +601,7 @@ if (window.location === window.parent.location) { if (x.product_url != null) { const p = document.createElement('p'); - li.appendChild(p) + li.appendChild(p); const t = document.createTextNode('Produktlänk '); const i = document.createElement('i'); i.classList.add('bi', 'bi-box-arrow-up-right'); @@ -675,7 +640,7 @@ if (window.location === window.parent.location) { const d = new Date(x.last_modified); const td = document.createTextNode(d.toLocaleDateString('sv-SE')); p.appendChild(td); - const t2 = document.createTextNode('.') + const t2 = document.createTextNode('.'); p.appendChild(t2); } }); @@ -686,9 +651,2046 @@ if (window.location === window.parent.location) { })(); /* we're all set, show the control container now */ -container.setAttribute('aria-hidden', 'false'); +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, + type: 'relative', + operator: '<=', + quantity: 1, + unit: 'y', + show_unknown: false, + _min_ts: null, + _max_ts: null, + getRelativeDate: function(quantity, unit) { + if (quantity == null || isNaN(quantity) || unit == null) { + return null; + } + /* use today noon localtime to avoid issues due to DST when substracting dates */ + const d = new Date(); + d.setHours(12, 0, 0, 0); + switch (unit) { + case 'd': + d.setDate(d.getDate() - quantity); + break; + case 'w': + d.setDate(d.getDate() - 7 * quantity); + break; + case 'm': + d.setMonth(d.getMonth() - quantity); + break; + case 'y': + d.setFullYear(d.getFullYear() - quantity); + break; + default: + return null; + } + return d; + }, + setupMinMax: function() { + this._min_ts = this._max_ts = null; + switch (this.type) { + case 'relative': { + const date = this.getRelativeDate(this.quantity, this.unit); + const prop = {'<=':'_min_ts', '>=':'_max_ts'}[this.operator]; + this[prop] = dateToTS(date); + break; + } + case 'interval': { + this._min_ts = dateToTS(this.from); + this._max_ts = dateToTS(this.to); + break; + } + } + }, + }); +})(); + +/* Layer style definitions */ +/* TODO: this should really be refactored… */ +const LAYERS = { + 'mrr.appr_ec': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 22, + fill: new Fill({ + color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + 'mrr.appl_ec': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 25, + fill: new Fill({ + color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [197, 14, 31, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + 'mrr.appr_met': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 24, + fill: new Fill({ + color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + 'mrr.appl_met': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 26, + fill: new Fill({ + color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [197, 14, 31, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + 'mrr.appr_ogd': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 24, + fill: new Fill({ + color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + 'mrr.appl_ogd': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 26, + fill: new Fill({ + color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [197, 14, 31, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + 'mrr.appr_dl': { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 20, + fill: new Fill({ + color: [228, 53, 45, Math.max((.2-1)/6 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + + 'svk.ledningar': { + legend: { zoomLevel: 5, type: 'linestring', reuse_canvas: true }, + style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { + return new Style({ + zIndex: 52, + stroke: new Stroke({ + color: 'black', + width: width, + }), + }); + }), + }, + 'svk.stolpar': { + legend: { zoomLevel: 5, type: 'point' }, + style: [undefined, undefined, undefined, undefined, undefined] + .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { + return new Style({ + zIndex: 51, + image: new CircleStyle({ + radius: radius, + fill: new Fill({ + color: 'black', + }), + }), + }); + })), + }, + 'svk.transmissionsnatsprojekt': { + legend: { zoomLevel: 5, type: 'linestring' }, + style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { + return new Style({ + zIndex: 53, + stroke: new Stroke({ + color: 'black', + width: width, + lineDash: [4 * width], + }), + }); + }), + }, + 'svk.stationer': { + legend: { zoomLevel: 3, type: 'point' }, + style: [3, 4, 5, 6, 7, 8.5, 10].map(function(radius) { + return new Style({ + zIndex: 50, + image: new RegularShape({ + radius: radius, + points: 4, + angle: Math.PI/4, + fill: new Fill({ + color: 'black', + }), + }), + }); + }) + .concat([.5, 1, 1.5, 2, 2].map(function(width) { + return new Style({ + zIndex: 50, + fill: new Fill({ + color: 'rgba(128, 128, 128, .7)', + }), + stroke: new Stroke({ + width: width, + color: 'rgb(0, 0, 0)', + }), + }); + })), + }, + + 'vbk.area_current': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [168, 198, 223, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [56, 96, 130, 1], + }), + }); + }), + }, + 'vbk.area_notcurrent': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [222, 163, 199, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [148, 55, 112, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + 'vbk.offshore_completed': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 17, + fill: new Fill({ + color: [38, 107, 29, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [38, 107, 29, 1], + }), + }); + }), + }, + 'vbk.offshore_approved': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 16, + fill: new Fill({ + color: [56, 160, 44, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [56, 160, 44, 1], + }), + }); + }), + }, + 'vbk.offshore_amended': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; + patternCanvas.width = width/2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(247, 105, 162, 1)'; + patternContext.beginPath(); + patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, 1.5*w, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 17, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: 2*w, + color: [247, 105, 162, 1], + lineDash: [8 * w], + }), + }); + }), + }, + 'vbk.offshore_rejected': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 11, + fill: new Fill({ + color: [227, 26, 28, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [227, 26, 28, 1], + }), + }); + }), + }, + 'vbk.offshore_appealed': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 15, + fill: new Fill({ + color: [177, 88, 40, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [177, 88, 40, 1], + }), + }); + }), + }, + 'vbk.offshore_applied': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 14, + fill: new Fill({ + color: [255, 127, 0, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [255, 128, 0, 1], + }), + }); + }), + }, + 'vbk.offshore_consultation': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 13, + fill: new Fill({ + color: [254, 217, 118, .65], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [254, 183, 82, 1], + }), + }); + }), + }, + 'vbk.offshore_investigation': { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; + patternCanvas.width = width*2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(68, 90, 166, 1)'; + patternContext.lineWidth = w; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 12, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: 2*w, + color: [68, 90, 166, 1], + lineDash: [8 * w], + }), + }); + }), + }, + 'vbk.offshore_revoked': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [105, 61, 154, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [105, 62, 153, 1], + }), + }); + }), + }, + 'vbk.station_completed': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-completed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + 'vbk.station_processed': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-processed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + 'vbk.station_approved': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-approved.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + 'vbk.station_revoked': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-revoked.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + 'vbk.station_rejected': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-rejected.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + 'vbk.station_dismounted': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-dismounted.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + 'vbk.station_appealed': { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-appealed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf + * */ + 'avverk.utford': { + legend: { zoomLevel: 7 }, + style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [204, 0, 0, 1], + }), + }); + }), + }, + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf + * */ + 'avverk.anmald': { + legend: { zoomLevel: 7 }, + style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: (width === undefined || width === 0) ? + new Fill({ color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)*.75] }) : + (function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const slope = 45 * Math.PI/180; + const spacing = z < 10 ? z*2 : 40; + const len = Math.hypot(1, slope); + const w = patternCanvas.width = Math.round(1/len + spacing); + const h = patternCanvas.height = Math.round(slope/len + spacing * slope); + + patternContext.fillStyle = 'rgba(255, 102, 102, .1)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(204, 0, 0, 1)'; + patternContext.lineWidth = Math.max(1, width/2); + patternContext.beginPath(); + patternContext.moveTo(0, h); + patternContext.lineTo(w, 0); + patternContext.moveTo(-w, h); + patternContext.lineTo(w, -h); + patternContext.moveTo(0, 2*h); + patternContext.lineTo(2*w, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Fill({ color: context.createPattern(patternCanvas, 'repeat') }); + })(), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [204, 0, 0, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + + 'skydd.tilltradesforbud': { + legend: { zoomLevel: 2 }, + style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { + return new Style({ + zIndex: 23, + fill: new Fill({ + /* transparent fill so clicking the inside of the polygon triggers a popover */ + /* XXX could also use a custom renderer but that doesn't seem to work */ + color: [0, 0, 0, 0], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [255, 0, 0, 1], + }), + }); + }), + }, + 'skydd.nationalpark': { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 55, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 22, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [0, 55, 0, 1], + }), + }); + }), + }, + 'skydd.naturreservat': { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 21, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [7, 181, 7, 1], + }), + }); + }), + }, + 'skydd.naturreservat_kommunalt': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 20, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [7, 181, 7, 1], + }), + }); + }), + }, + 'skydd.naturvardsomrade': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(176, 255, 176, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 19, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [176, 255, 176, 1], + }), + }); + }), + }, + 'skydd.djur_och_vaxtskyddsomrade': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 255, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 18, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [255, 255, 0, 1], + }), + }); + }), + }, + 'skydd.kulturreservat': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(154, 102, 255, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 17, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [154, 102, 255, 1], + }), + }); + }), + }, + 'skydd.vattenskyddsomrade': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 105, 212, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 16, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [0, 105, 212, 1], + }), + }); + }), + }, + 'skydd.landskapsbildsskyddsomrade': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(135, 110, 71, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 15, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [134, 110, 71, 1], + }), + }); + }), + }, + 'skydd.skogligt_biotopskyddsomrade': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(135, 90, 71, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 14, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [134, 90, 71, 1], + }), + }); + }), + }, + 'skydd.ovrigt_biotopskyddsomrade': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 95, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 13, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [255, 95, 0, 1], + }), + }); + }), + }, + 'skydd.naturminne_yta': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(113, 0, 116, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 12, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [134, 0, 116, 1], + }), + }); + }), + }, + 'skydd.naturminne_punkt': { + legend: { zoomLevel: 6, type: 'point' }, + style: [undefined, undefined, undefined, undefined].concat([3, 4, 6, 8, 12, 16, 20, 24].map(function(width) { + return new Style({ + zIndex: 12, + image: new CircleStyle({ + radius: width, + fill: new Fill({ + color: 'rgba(113, 0, 116, .5)', + }), + stroke: new Stroke({ + width: Math.log2(width)/2, + color: 'rgba(113, 0, 116, 1)', + }), + }), + }); + })) + }, + 'skydd.interimistiskt_forbud': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(168, 0, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 11, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [168, 0, 0, 1], + }), + }); + }), + }, + 'skydd.fageldirektivet': { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width*2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(230, 0, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + patternContext.beginPath(); + patternContext.lineWidth *= 6; + patternContext.moveTo(-.5*patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -.5*patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 1.5*patternCanvas.height); + patternContext.lineTo(1.5*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 10, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [230, 0, 0, 1], + }), + }); + }), + }, + 'skydd.habitatdirektivet': { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width*2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 77, 168, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + patternContext.beginPath(); + patternContext.lineWidth *= 6; + patternContext.moveTo(0, -.5*patternCanvas.height); + patternContext.lineTo(1.5*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-.5*patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 1.5*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 10, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [0, 77, 168, 1], + }), + }); + }), + }, + 'skydd.helcom': { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(130, 130, 130, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.5*patternCanvas.width, .5*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [130, 130, 130, 1], + }), + }); + }), + }, + 'skydd.ramsar': { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(195, 0, 255, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.25*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [195, 0, 255, 1], + }), + }); + }), + }, + 'skydd.ospar': { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.25*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [168, 0, 0, 1], + }), + }); + }), + }, + 'skydd.varldsarv': { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.75*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [168, 0, 0, 1], + }), + }); + }), + }, + 'skydd.biosfarsomraden': { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(131, 0, 219, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [131, 0, 219, 1], + }), + }); + }), + }, + 'skydd.naturvardsavtal': { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 21, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [255, 0, 197, 1], + }), + }); + }), + }, + 'skydd.naturvardsavtal_skogsstyrelsen': { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 20, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [255, 0, 197, 1], + }), + }); + }), + }, + 'skydd.atervatningsavtal': { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5, 6].map(function(width) { + return new Style({ + zIndex: 5, + fill: new Fill({ + color: [255, 115, 0, .4], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [255, 115, 0, 1], + }), + }); + }) + .concat([7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 115, 0, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 5, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [255, 115, 0, 1], + }), + }); + })), + }, + 'nv.naturvarde_sks': { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5].map(function(width) { + return new Style({ + zIndex: 6, + fill: new Fill({ + color: [255, 170, 0, .2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [255, 170, 0, .8], + }), + }); + }) + .concat([6, 7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 170, 0, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [255, 170, 0, 1], + }), + }); + })), + }, + 'nv.nyckelbiotop': { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5].map(function(width) { + return new Style({ + zIndex: 6, + fill: new Fill({ + color: [217, 148, 9, .2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [217, 148, 9, .8], + }), + }); + }) + .concat([6, 7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [217, 148, 9, 1], + }), + }); + })), + }, + 'nv.nyckelbiotop_storskogsbruk': { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5].map(function(width) { + return new Style({ + zIndex: 6, + fill: new Fill({ + color: [217, 148, 9, .2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [217, 148, 9, .8], + }), + }); + }) + .concat([6, 7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [217, 148, 9, 1], + }), + }); + })), + }, + 'nv.sumpskog': { + legend: { zoomLevel: 5 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const w = Math.max(1, width); + patternCanvas.width = z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 6 : 8; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(158, 200, 215, 1)'; + patternContext.lineWidth = w; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 5, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: w/2, + color: [158, 200, 215, 1], + }), + }); + }), + }, + 'nv.pagaende_naturreservatsbildning': { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.setLineDash([width/4, width/4]); + patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(width/4, 0); + patternContext.lineTo(width/4, patternCanvas.height); + patternContext.stroke(); + patternContext.beginPath(); + patternContext.lineDashOffset = width/4; + patternContext.moveTo(3*width/4, 0); + patternContext.lineTo(3*width/4, patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 10, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 3 : 4, + color: [7, 181, 7, 1], + lineDash: [width/8, width/4], + }), + }); + }), + }, + 'nv.snus': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 4, + fill: new Fill({ + color: [168,168,0,.2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [168,77,0,.75], + }), + }); + }), + }, + + 'ri.naturvard': { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(154, 230, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [154, 230, 0, 1], + }), + }); + }), + }, + 'ri.friluftsliv': { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 127, 232, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [0, 127, 232, 1], + }), + }); + }), + }, + 'ri.rorligt_friluftsliv': { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(187, 227, 212, .25)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(56, 151, 117, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [56, 151, 117, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + 'ri.obruten_kust': { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(227, 227, 187, .25)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(156, 158, 56, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [156, 158, 56, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + 'ri.obrutet_fjall': { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(255, 255, 209, .25)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(219, 183, 60, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [219, 183, 60, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + 'ri.skyddade_vattendrag': { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + return new Style({ + zIndex: 8, + fill: new Fill({ + color: [102, 157, 240, .25], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [41, 109, 197, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + + 'ren.betesomrade': { + legend: { zoomLevel: 0 }, + style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { + return new Style({ + zIndex: 4, + fill: new Fill({ + /* transparent fill so clicking the inside of the polygon triggers a popover */ + /* XXX could also use a custom renderer but that doesn't seem to work */ + color: [0, 0, 0, 0], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [179, 153, 102, 1], + }), + }); + }), + }, + 'ren.flyttled': { + legend: { zoomLevel: 2, type: 'linestring' }, + style: [.75, 1, 1.5, 2, 3, 4, 5, 5, 6, 7, 8, 10].map(function(width) { + return new Style({ + zIndex: 7, + stroke: new Stroke({ + width: 2*width, + color: [119, 99, 59, 1], + lineDash: [4 * width], + }), + }); + }), + }, + 'ren.riks_ren': { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = z < 4 ? 4 : z <= 5 ? 8 : Math.pow(2, Math.round(Math.log2(width) + 3)); + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(179, 153, 102, 1)'; + patternContext.lineWidth = Math.max(1, width/2); + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [179, 153, 102, 1], + }), + }); + }), + }, + 'ren.omr_riks': { + legend: { zoomLevel: 2 }, + style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { + return new Style({ + zIndex: 5, + fill: new Fill({ + color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [179, 153, 102, 1], + }), + }); + }), + }, + + /* Documentation at + * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf + * */ + 'misc.dammar': { + legend: { zoomLevel: 5, type: 'point' }, + style: [2, 3, 4, 4, 4, 6, 8, 8, 8, 10, 16, 32].map(function(width) { + return new Style({ + zIndex: 59, + image: new CircleStyle({ + radius: width, + fill: new Fill({ + color: 'rgb(219, 30, 42)', + }), + stroke: new Stroke({ + width: Math.log2(width) * 2/5, + color: 'rgb(128, 17, 25)', + }), + }), + }); + }), + }, + + 'misc.gigafactories': { + legend: { zoomLevel: 1, type: 'point' }, + style: [4, 6, 7, 8, 10, 12].map(function(width) { + return new Style({ + zIndex: 60, + image: new CircleStyle({ + radius: width, + fill: new Fill({ + color: 'rgb(152, 78, 163)', + }), + stroke: new Stroke({ + width: Math.log2(width) * 2/5, + color: 'rgb(119, 61, 128)', + }), + }), + }); + }) + .concat([1.5, 2, 2, 2, 2, 2].map(function(width) { + return new Style({ + zIndex: 58, + fill: new Fill({ + color: 'rgba(152, 78, 163, .4)', + }), + stroke: new Stroke({ + width: width, + color: 'rgb(119, 61, 128)', + }), + }); + })), + }, + + 'kskog.1' : { style: [ 56, 168, 0, .2] }, /* #1 Sannolikt kontinuitetsskog (preciserad) */ + 'kskog.2' : { style: [169, 0, 230, .2] }, /* #2 Sannolikt påverkad kontinuitetsskog (preciserad) */ + 'kskog.3' : { style: [152, 230, 0, .2] }, /* #3 Sannolikt kontinuitetsskog i fjällen (grövre precisering) */ + 'kskog.4' : { style: [ 76, 115, 0, .2] }, /* #4 Potentiell kontinuitetsskog (2015) */ +}; + +/* process URL parameters (other than 'basemap') */ +const STYLES = {}; +(function() { + const params = new URLSearchParams(window.location.hash.substring(1)); + const x = parseFloat(params.get('x')); + const y = parseFloat(params.get('y')); + if (!isNaN(x) && !isNaN(y)) { + MAP.getView().setCenter([x, y]); + } + const z = parseFloat(params.get('z')); + if (!isNaN(z)) { + MAP.getView().setZoom(z); + } + if (!params.has('layers') || (!params.get('layers').match(/^\s*$/) && + /* compat redirect/layer subst for old non-hierachical names */ + !params.get('layers').split(' ').some((l) => l.includes('.')))) { + params.set('layers', [ + 'svk.ledningar', + 'svk.stolpar', + 'svk.stationer', + 'svk.transmissionsnatsprojekt', + 'misc.gigafactories', + 'misc.dammar', + 'mrr.appr_ec', + 'mrr.appl_ec', + 'mrr.appr_ogd', + 'mrr.appl_ogd', + 'mrr.appr_met', + 'mrr.appl_met', + 'mrr.appr_dl', + 'vbk.area_current', + 'vbk.area_notcurrent', + ].join(' ')); + location.hash = '#' + params.toString(); + } + + /* map each known parameter to a callback processing its value */ + Object.entries({ + 'layers': function(value) { + const layersParams = value.split(' '); + Object.entries(LAYERS) + .filter(([key]) => layersParams.includes(key)) + .forEach(([key, lyr]) => STYLES[key] = lyr.style); + }, + + 'age-filter': function(value) { + /* eslint-disable-next-line no-useless-escape */ + const m0 = /^([ +\-]?)([0-9]+)([dwmy])$/.exec(value); + if (m0 != null) { + 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; + 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; + 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) { +MAP.getView().on('change', function(event) { const view = event.target; disposePopover(); @@ -700,6 +2702,107 @@ map.getView().on('change', function(event) { location.hash = '#' + searchParams.toString(); }); +/* add layers to the map */ +const mapLayers = (function() { + const baseurl = '/'; + const xyz = '/{z}/{x}/{y}.pbf'; + const tileGrid = createXYZ({ + extent: EXTENT, + tileSize: 1024, + maxResolution: 1024, /* = 1048576/1024 */ + minZoom: 0, + maxZoom: 7, + }); + const isVisible = function(groupname) { + return Object.keys(LAYERS).some((layername) => + layername.startsWith(groupname + '.') && STYLES[layername] != null); + }; + const canWebGL2 = !!document.createElement('canvas').getContext('webgl2'); + + /* Note: layers are added in the order below, so leave SvK and + * misc at the end so they show up on top of suface features */ + const rasterLayers = ['kskog']; + const vectorLayers = ['nv', 'mrr', 'skydd', 'ren', 'ri', 'avverk', 'vbk', 'svk', 'misc']; + const canFilterByAge = ['avverk', 'mrr', 'vbk']; /* layers for which features are dated */ + + const ret = {}; + if (!canWebGL2) { + rasterLayers.forEach((k) => ret[k] = null); + } else { + rasterLayers.forEach(function(k) { + ret[k] = new TileLayerGL({ + /* Naturvårdsverket has a WMS server we could use instead, but by serving it ourselves + * we can filter on he various kskog classes */ + source: new GeoTIFF({ + sources: [{ + url: baseurl + 'raster/' + k + '.tiff', + }], + normalize: false, + convertToRGB: false, + wrapX: false, + interpolate: false, + /* use the projection found in the source's metadata */ + }), + visible: false, + style: null, /* filled later */ + }); + MAP.addLayer(ret[k]); + }); + } + + vectorLayers.forEach(function(k) { + const canFilterByAge0 = canFilterByAge.includes(k); + ret[k] = new VectorTileLayer({ + source: new VectorTile({ + url: baseurl + 'tiles/' + k + xyz, + format: new MVT(), + projection: PROJECTION, + wrapX: false, + transition: 0, + tileGrid: tileGrid, + }), + /* XXX switch to 'hybrid' if there are perf issues; but that seems to + * put lines above points regardless of their respective z-index */ + renderMode: 'hybrid', + declutter: false, + visible: isVisible(k), + style: function(feature, resolution) { + /* WARN: very hot code path! */ + const properties = feature.getProperties(); + if (ageFilterSettings.active) { + /* TODO avoid doing this checks for each feature; instead, set up a + * different style function if ageFilterSettings.active */ + const ts = properties.ts; + if (ts == null) { + if (canFilterByAge0 && !ageFilterSettings.show_unknown) { + return null; + } + } else if ((ageFilterSettings._min_ts !== null && ts < ageFilterSettings._min_ts) || + (ageFilterSettings._max_ts !== null && ts > ageFilterSettings._max_ts)) { + return null; + } + } + const style = STYLES[k + '.' + properties.layer]; + if (!Array.isArray(style)) { + return style; + } else { + const maxi = style.length - 1; + const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); + /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ + const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); + // console.log(`resolution=${resolution}, z=${z}, i=${i}`); + return style[i]; + } + } + }); + ret[k].set('layerGroup', k, true); + ret[k].set('canFilterByAge', canFilterByAge0, true); + MAP.addLayer(ret[k]); + }); + return ret; +})(); + +/* layer hierarchy, for the layer selection, legend and info modal */ const layerHierarchy = [ { text: 'Transmissionsnät för el', @@ -1100,117 +3203,6 @@ const layerHierarchy = [ }, ]; -const styles = (function() { - const searchParams = new URLSearchParams(location.hash.substring(1)); - const layersParams = searchParams.has('layers') ? searchParams.get('layers').split(' ') : []; - return Object.fromEntries( - Object.entries(layers) - .filter(([key]) => layersParams.includes(key)) - .map(([key, lyr]) => [key, lyr.style]) - ); -})(); - -const mapLayers = (function() { - const baseurl = '/'; - const xyz = '/{z}/{x}/{y}.pbf'; - const tileGrid = createXYZ({ - extent: extent, - tileSize: 1024, - maxResolution: 1024, /* = 1048576/1024 */ - minZoom: 0, - maxZoom: 7, - }); - const isVisible = function(groupname) { - return Object.keys(layers).some((layername) => - layername.startsWith(groupname + '.') && styles[layername] !== undefined); - }; - const canWebGL2 = !!document.createElement('canvas').getContext('webgl2'); - - /* Note: layers are added in the order below, so leave SvK and - * misc at the end so they show up on top of suface features */ - const rasterLayers = ['kskog']; - const vectorLayers = ['nv', 'mrr', 'skydd', 'ren', 'ri', 'avverk', 'vbk', 'svk', 'misc']; - const canFilterByAge = ['avverk', 'mrr', 'vbk']; /* layers for which features are dated */ - - const ret = {}; - if (!canWebGL2) { - rasterLayers.forEach((k) => ret[k] = null); - } else { - rasterLayers.forEach(function(k) { - ret[k] = new TileLayerGL({ - /* Naturvårdsverket has a WMS server we could use instead, but by serving it ourselves - * we can filter on he various kskog classes */ - source: new GeoTIFF({ - sources: [{ - url: baseurl + 'raster/' + k + '.tiff', - }], - normalize: false, - convertToRGB: false, - wrapX: false, - interpolate: false, - /* use the projection found in the source's metadata */ - }), - visible: false, - style: null, /* filled later */ - }); - map.addLayer(ret[k]); - }); - } - - vectorLayers.forEach(function(k) { - const canFilterByAge0 = canFilterByAge.includes(k); - ret[k] = new VectorTileLayer({ - source: new VectorTile({ - url: baseurl + 'tiles/' + k + xyz, - format: new MVT(), - projection: projection, - wrapX: false, - transition: 0, - tileGrid: tileGrid, - }), - /* XXX switch to 'hybrid' if there are perf issues; but that seems to - * put lines above points regardless of their respective z-index */ - renderMode: 'hybrid', - declutter: false, - visible: isVisible(k), - style: function(feature, resolution) { - /* WARN: very hot code path! */ - const properties = feature.getProperties(); - if (age_filter_settings.active) { - /* TODO avoid doing this checks for each feature; instead, set up a - * different style function if age_filter_settings.active */ - const ts = properties.ts; - if (ts == null) { - if (canFilterByAge0 && !age_filter_settings.show_unknown) { - return null; - } - } else if ((age_filter_settings._min_ts !== null && ts < age_filter_settings._min_ts) || - (age_filter_settings._max_ts !== null && ts > age_filter_settings._max_ts)) { - return null; - } - } - const style = styles[k + '.' + properties.layer]; - if (!Array.isArray(style)) { - return style; - } else { - const maxi = style.length - 1; - const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); - /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ - const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); - // console.log(`resolution=${resolution}, z=${z}, i=${i}`); - return style[i]; - } - } - }); - ret[k].set('layerGroup', k, true); - ret[k].set('canFilterByAge', canFilterByAge0, true); - map.addLayer(ret[k]); - }); - - return ret; -})(); - - /* legend panel */ (function() { const modal = document.getElementById('map-legend-panel'); @@ -1263,11 +3255,11 @@ const mapLayers = (function() { .forEach(function(layer) { /* add symbols for each layer */ const layerGroup = layer.split('.', 1)[0]; - if (!layerGroup || !mapLayers[layerGroup] || !layers[layer] || !layers[layer].style) { + if (!layerGroup || !mapLayers[layerGroup] || LAYERS[layer]?.style == null) { console.log(`Could not find symbol for layer ${layer}, skipping`); return; } - const legend = layers[layer].legend || {}; + const legend = LAYERS[layer]?.legend ?? {}; if (canvas == null || !legend.reuse_canvas) { canvas = document.createElement('canvas'); div.appendChild(canvas); @@ -1279,15 +3271,15 @@ const mapLayers = (function() { if (mapLayers[layerGroup].getSource() instanceof GeoTIFF) { /* raster source */ render.setFillStrokeStyle(new Fill({ - color: layers[layer].style, + color: LAYERS[layer].style, })); return render.drawGeometry(symbols.polygon); } else if (mapLayers[layerGroup].getSource() instanceof VectorTile) { /* vector source */ - const style = Array.isArray(layers[layer].style) ? - layers[layer].style[legend.zoomLevel ?? 5] : - layers[layer].style; + const style = Array.isArray(LAYERS[layer].style) ? + LAYERS[layer].style[legend.zoomLevel ?? 5] : + LAYERS[layer].style; const legend_type = legend.type ?? 'polygon'; if (legend_type === 'point' && style.getImage(1) instanceof Icon && style.getImage(1).getSrc()) { /* use a new <img> element since .setStyle() returns the same one and doesn't work in that case */ @@ -1375,7 +3367,7 @@ const infoMetadataAccordions = []; const setIndeterminateAndChecked = function(list) { return list.forEach(function(elem) { - const layerStyles = elem._layers.map((lyr) => styles[lyr] !== undefined); + 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) { @@ -1399,15 +3391,15 @@ const infoMetadataAccordions = []; const result = {}; const nodata = [ 0, 0, 0, .0]; const kskog_palette = [nodata, nodata, nodata, nodata, nodata]; - Object.keys(layers).forEach(function(lyr) { + Object.keys(LAYERS).forEach(function(lyr) { const layerGroup = lyr.split('.', 1)[0]; if (result[layerGroup] === undefined) { result[layerGroup] = false; } - result[layerGroup] ||= styles[lyr] !== undefined; + result[layerGroup] ||= STYLES[lyr] !== undefined; if (layerGroup === 'kskog') { - const i = parseInt(lyr.slice(layerGroup.length + 1)) - kskog_palette[i] = styles[lyr] ?? nodata; + const i = parseInt(lyr.slice(layerGroup.length + 1)); + kskog_palette[i] = STYLES[lyr] ?? nodata; } }); const kskog = mapLayers['kskog']; @@ -1421,7 +3413,7 @@ const infoMetadataAccordions = []; mapLayers[lyr]?.setVisible(visible); }); const btn = document.getElementById('map-legend-button'); - if (Object.values(styles).some((v) => v !== null)) { + if (Object.values(STYLES).some((v) => v !== null)) { btn.classList.remove('disabled'); } else { btn.classList.add('disabled'); @@ -1438,14 +3430,14 @@ const infoMetadataAccordions = []; if (mapLayers[lyr.split('.', 1)[0]] == null) { return; /* keep unexisting layers (eg WebGL layers on a system without WebGL support) unselectable */ } - styles[lyr] = layers[lyr].style; + STYLES[lyr] = LAYERS[lyr].style; if (!layersParams.includes(lyr)) { layersParams.push(lyr); } }); } else { layerList.forEach(function(lyr) { - delete styles[lyr]; + delete STYLES[lyr]; }); layersParams = layersParams.filter((lyr) => !layerList.includes(lyr)); } @@ -1584,14 +3576,9 @@ const infoMetadataAccordions = []; 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'; }; })(); @@ -1643,7 +3630,1321 @@ const infoMetadataAccordions = []; })(); })(); -/* age filter panel */ +/* popup and feature overlays */ +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, {key: k, desc: o}]; + } else { + return [k, Object.assign(o, {key: k})]; + } + })); + }; + + const layers = { + svk: { + ledningar: { + title: 'Kraftledning (befintlig)', + fields: [ + { key: 'Placement', desc: 'Förläggning' }, + { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, + { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, + ], + }, + transmissionsnatsprojekt: { + title: 'Transmissionsnätsprojekt', + fields: [ + { key: 'Name', desc: 'Projektnamn' }, + { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, + { key: 'Url', desc: 'Länk', fn: formatLink }, + ], + }, + }, + + misc: { + gigafactories: { + title: 'Stor industrisatsning', + fields: [ + { key: 'Name', desc: 'Namn' }, + { key: 'Url', desc: 'Länk', fn: formatLink }, + ], + }, + dammar: { + /* Documentation at + * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf */ + title: 'Damm', + fields: [ + { key: 'DNamn', desc: 'Dammenhetens namn' }, + { key: 'Namn', desc: 'Dammanläggningens namn' }, + { key: 'LST_OBJID', desc: 'Länsnr', classes: ['feature-objid'] }, + { key: 'Status', desc: 'Status', fn: (v) => v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : '' }, + //{ key: 'Regleringstyp', desc: 'Regleringstyp' }, + { key: 'ByggAr', desc: 'Byggår' }, + { key: 'DammHojd', desc: 'Dammhöjd', unit: 'm' }, + { key: 'KronLangd', desc: 'Krönlängd', unit: 'm' }, + { key: 'Fiskvag', desc: 'Fiskväg', fn: (v) => + v === 1 ? 'Bassängtrappa' : + v === 2 ? 'Denilränna' : + v === 3 ? 'Slitsränna' : + v === 4 ? 'Omlöp' : + v === 5 ? 'Inlöp' : + v === 6 ? 'Ålledare' : + v === 7 ? 'Smoltränna' : + v === 8 ? 'Okänd typ' : + v === 9 ? 'Ingen' : + v === 10 ? 'Annan' : + null }, + { key: 'HARO', desc: 'Huvudavrinningsområdesnummer', classes: ['feature-objid'] }, + { key: 'Vattendistrikt', desc: 'Vattendistrikt', classes: ['feature-objid'] }, + { key: 'Verksamhet', desc: 'Verksamhet', fn: (v) => + v === 1 ? 'Kraftproduktion' : + v === 2 ? 'Industri' : + v === 3 ? 'Sjöfart' : + v === 4 ? 'Invallning' : + v === 5 ? 'Vattenförsörjning' : + v === 6 ? 'Spegeldamm' : + v === 7 ? 'Historisk' : + v === 8 ? 'Övrigt' : + null }, + { key: 'DG', desc: 'Högsta dämningsgräns', unit: 'm' }, + { key: 'SG', desc: 'Lägsta sänkningsgräns', unit: 'm' }, + { key: 'MY', desc: 'Magasinsyta', unit: 'km²' }, + { key: 'RV', desc: 'Reglerbar volym', unit: 'Mm³' }, + { key: 'Kommentar', desc: 'Kommentar' }, + ], + }, + }, + }; + + layers.mrr = {}; + (function() { + const fields = [ + { key: 'name', desc: 'Namn' }, + { key: 'mineral', desc: 'Koncessionsmineral', cond: (i) => i < 6 }, + { key: 'owners', desc: 'Ägare', cond: [0,2,4] }, + { key: 'owners', desc: 'Sökande', cond: [1,3,5] }, + { key: 'conc_name', desc: 'Tillhörande bearbetnings\u00ADkoncession(er)', cond: [6] }, + { key: 'licenceid', desc: 'Tillståndsid', classes: ['feature-attr-mrr-license-id'], cond: [0,2,4,6] }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + { key: 'validfrom', desc: 'Giltig från', cond: [0,2,4] }, + { key: 'validto', desc: 'Giltig till', cond: [0,2,4] }, + { key: 'diarynr', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, + { key: 'appl_date', desc: 'Ansökningsdatum' }, + { key: 'dec_date', desc: 'Beslutsdatum', cond: [0,2,4,6] }, + ]; + Object.entries({ + ec: 'Bearbetningskoncession', + met: 'Undersökningstillstånd, metaller och industrimineral', + ogd: 'Undersökningstillstånd, olja, gas och diamant', + }) + .flatMap(([k, title]) => [ + /* don't use Object.entries() to guaranty ordering */ + ['appr', 'beviljad'], /* even index */ + ['appl', 'ansökt'], /* odd index */ + ].map(([a,b]) => [a + '_' + k, title + ' \u2013 ' + b])) + .concat([['appr_dl', 'Markanvisning till koncession']]) /* index #6 */ + .forEach(([k, title], idx) => layers.mrr[k] = { title, fields: filterFields(idx, fields) }); + })(); + + layers.vbk = {}; + (function() { + const fieldMap = mkFieldMap({ + Projektnamn: 'Projektnamn', + OmrID: { desc: 'Områdes-ID', classes: ['feature-objid'] }, + AntalVerk: 'Aktuella verk', + AntalEjXY: 'Antal ej koordinatsatta verk', + Projektstatus: 'Projektstatus', + Diarienummer: 'Diarienummer', + geom_area: { desc: 'Areal', fn: 'area' }, + Calprod: { desc: 'Beräknad årsproduktion', unit: 'GWh' }, + PlaneradByggstart: 'Planerad byggstart', + PlaneratDrift: 'Planerat drifttagande', + AndringsansokanPagar: 'Ändringsansökan pågår', + UnderByggnation: 'Under byggnation', + Organisationsnamn: 'Verksamhetsutövare', + Organisationsnummer: { desc: 'Organisationsnummer', classes: ['feature-orgnr'] }, + SamradsunderlagInlamnat: 'Samrådsunderlag inlämnat', + AnsokanInlamnat: 'Tillståndsansökan inlämnad', + AnsokanAterkallad: 'Tillståndsansökan återkallad', + AnsokanBeviljad: 'Tillståndsansökan beviljad', + AnsokanAvslagen: 'Tillståndsansökan avslagen', + AnsokanOverklagad: 'Överklagad', + Natura2000_Ansokan: 'Natura2000 ansökan', + Natura2000_Beslutdatum: 'Natura2000 beslutsdatum', + Uppfort: 'Parken uppförd', + PlaneratAntalVerkMin: 'Planerat antal verk (min)', + PlaneratAntalVerkMax: 'Planerat antal verk (max)', + PlaneradHojdMin: { desc: 'Panerad totalhöjd (min)', unit: 'm' }, + PlaneradHojdMax: { desc: 'Panerad totalhöjd (max)', unit: 'm' }, + PlaneradProduktionMin: { desc: 'Planerad årsproduktion (min)', unit: 'GWh' }, + PlaneradProduktionMax: { desc: 'Planerad årsproduktion (max)', unit: 'GWh' }, + BeviljatAntalVerk: 'Beviljat antal verk', + UppfortAntalVerk: 'Uppfört antal verk', + BeviljadMaxhojd: { desc: 'Beviljad maxhöjd', unit: 'm' }, + InstalleradEffekt: { desc: 'Installerad effekt', unit: 'MW' }, + ElNamn: 'Elområde', + SenasteUppdaterat: 'Senast uppdaterat', + }); + + Object.entries({ + current: null, + notcurrent: ' \u2013 ej aktuell', + }) + .forEach(([k, title]) => layers.vbk['area_' + k] = { + title: 'Landbaserad projekteringsområde för vindkraft' + (title ?? ''), + fields: mapFields(k, fieldMap, [ + 'Projektnamn', + 'OmrID', + 'AntalVerk', + 'AntalEjXY', + 'geom_area', + 'Calprod', + 'PlaneradByggstart', + 'PlaneratDrift', + 'AndringsansokanPagar', + ['UnderByggnation', ['current']], + 'Organisationsnamn', + 'Organisationsnummer', + 'ElNamn', + 'SenasteUppdaterat', + ]), + }); + + [ + ['completed', /* 0 */ 'uppförd'], + ['approved', /* 1 */ 'tillståndsansökan beviljad'], + ['amended', /* 2 */ 'ändringsansökan'], + ['rejected', /* 3 */ 'tillståndsansökan avslagen'], + ['appealed', /* 4 */ 'överklagad'], + ['applied', /* 5 */ 'tillståndsansökan inlämnad'], + ['consultation', /* 6 */ 'samråd inför tillståndsansökan'], + ['investigation', /* 7 */ 'inledande undersökningar'], + ['revoked', /* 8 */ 'inte aktuell eller återkallad'], + ] + .forEach(([k, title], idx) => layers.vbk['offshore_' + k] = { + title: 'Havsbaserad vindkraft \u2013 ' + title, + fields: mapFields(idx, fieldMap, [ + 'Projektnamn', + 'OmrID', + 'Organisationsnamn', + 'Organisationsnummer', + 'Projektstatus', + 'Diarienummer', + ['AndringsansokanPagar', [1,2,4]], + 'geom_area', + ['SamradsunderlagInlamnat', (i) => i <= 6 || i === 8], + ['AnsokanInlamnat', (i) => i <= 5 || i === 8], + ['AnsokanAterkallad', [8]], + ['AnsokanBeviljad', [0,1,4,8]], + ['AnsokanAvslagen', [3,8]], + ['AnsokanOverklagad', [0,1,3,4,8]], + ['Natura2000_Ansokan', (i) => i !== 2], + ['Natura2000_Beslutdatum', (i) => i !== 2], + ['UnderByggnation', [1]], + ['PlaneratAntalVerkMin', (i) => i > 0], + ['PlaneratAntalVerkMax', (i) => i > 0], + ['PlaneradHojdMin', (i) => i > 0], + ['PlaneradHojdMax', (i) => i > 0], + ['PlaneradProduktionMin', (i) => i > 0], + ['PlaneradProduktionMax', (i) => i > 0], + ['PlaneradByggstart', (i) => i > 0], + ['Uppfort', [0,8]], + ['PlaneratDrift', (i) => i > 0], + ['BeviljatAntalVerk', [0,1,4,8]], + ['UppfortAntalVerk', [0,8]], + ['BeviljadMaxhojd', [0,1,4,8]], + ['InstalleradEffekt', [0]], + ['Calprod', [0]], + 'ElNamn', + 'SenasteUppdaterat', + ]), + }); + + Object.assign(fieldMap, mkFieldMap({ + VerkID: { desc: 'Verk-ID', classes: ['feature-objid'] }, + Status: 'Status', + Handlingstyp: 'Handlingstyp', + MB_Tillstand: 'Miljöbalken tillstånd tidsbegränsning', + Uppfort: 'Uppförandedatum',/* override previous def */ + Totalhojd: { desc: 'Totalhöjd', unit: 'm' }, + Navhojd: { desc: 'Navhöjd', unit: 'm' }, + Rotordiameter: { desc: 'Rotordiameter', unit: 'm' }, + Maxeffekt: { desc: 'Maxeffekt', unit: 'MW' }, + Fabrikat: 'Fabrikat', + Modell: 'Modell', + Placering: 'Placering', + })); + + [ + ['completed', /* 0 */ 'uppfört'], + ['approved', /* 1 */ 'beviljat'], + ['rejected', /* 2 */ 'avslagit/nekat'], + ['processed', /* 3 */ 'handlagt'], + ['dismounted', /* 4 */ 'nedmonterat'], + ['appealed', /* 5 */ 'överklagat'], + ['revoked', /* 6 */ 'inte aktuell eller återkallad'], + ] + .forEach(([k, title], idx) => layers.vbk['station_' + k] = { + title: 'Landbaserad vindkraftverk \u2013 ' + title, + fields: mapFields(idx, fieldMap, [ + 'VerkID', + 'OmrID', + 'Projektnamn', + 'Status', + 'Handlingstyp', + ['Uppfort', [0,4,6]], + 'MB_Tillstand', + 'Totalhojd', + 'Navhojd', + 'Rotordiameter', + 'Maxeffekt', + 'Calprod', + 'Fabrikat', + 'Modell', + 'Organisationsnamn', + 'Organisationsnummer', + 'Placering', + 'ElNamn', + 'SenasteUppdaterat', + ]), + }); + })(); + + layers.avverk = {}; + (function() { + const zeroIsNull = (v) => v > 0 ? v : null; + const fieldMap = mkFieldMap({ + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf + * and + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf + */ + Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, + ArendeAr: 'Registeringsår', + Inkomdatum: 'Inkom datum', + Skogstyp: 'Skogstyp', + Avvdatum: 'Datum för avverkning', + KallaDatum: 'Ursprung för datum för avverkning', + AnmaldHa: { desc: 'Areal anmält', unit: 'ha' }, + NatforHa: { desc: 'Areal naturlig föryngring', unit: 'ha', fn: zeroIsNull }, + SkogsodlHa: { desc: 'Areal plantering', unit: 'ha', fn: zeroIsNull }, + AvvSasong: 'Avverkningssäsong', + Avverktyp: 'Avverkningstyp', + ArendeStatus: 'Ärendestatus', + AvvHa: { desc: 'Avverkad areal', unit: 'ha' }, + geom_area: { desc: 'Areal för ytan', fn: 'area' }, + }); + + layers.avverk.utford = { + title: 'Utförd avverkning', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'ArendeAr', + 'Skogstyp', + 'AnmaldHa', + 'NatforHa', + 'Avverktyp', + 'Avvdatum', + 'KallaDatum', + 'geom_area', + ]), + }; + + layers.avverk.anmald = { + title: 'Avverkningsanmälansområde', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Inkomdatum', + 'ArendeAr', + 'AnmaldHa', + 'NatforHa', + 'SkogsodlHa', + 'AvvSasong', + 'ArendeStatus', + 'AvvHa', + ]), + }; + })(); + + layers.skydd = {}; + (function() { + const fieldMap = mkFieldMap({ + NVRID: { desc: 'NVR-ID', classes: ['feature-objid'] }, + FORSKRNAMN: 'Föreskriftsområde', + OBJEKTNAMN: 'Namn', + NAMN: 'Namn', + BESLSTAT: 'Beslutsstatus', + FORESKRTYP: 'Föreskriftstyp', + FORESKRIFT: 'Föreskriftssubtyp', + FRANDATUM: 'Från datum', + TILLDATUM: 'Till datum', + BESKRIVN: 'Beskrivning', + geom_area: { desc: 'Areal', fn: 'area' }, + + SKYDDSTYP: 'Skyddstyp', + BESLSTATUS: 'Beslutsstatus', + URSBESLDAT: 'Beslutsdatum (bildande)', + URSGALLDAT: 'Ursprungligt gällandedatum', + SENGALLDAT: 'Senaste gällandedatum', + FORVALTARE: 'Förvaltare', + IUCNKAT: 'IUCN-kategori', + DIARIENR: { desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, + LAGRUM: 'Lagrum', + BESLMYND: 'Beslutsmyndighet', + LAND_HA: { desc: 'Areal land', unit: 'ha' }, + VATTEN_HA: { desc: 'Areal vatten', unit: 'ha' }, + SKOG_HA: { desc: 'Skogsmarksareal', unit: 'ha' }, + + IKRAFTDATF: 'Ikraftträdandedatum föreskrifter', + TILLSYNSMH: 'Tillsynsmyndighet', + PROVNMHTIL: 'Prövningsmyndighet tillstånd', + PROVNMHDIS: 'Prövningsmyndighet dispens', + + NAME: 'Namn', + RAMSAR_ID: { desc: 'Ramsar-ID', classes: ['feature-objid'] }, + LEGAL_ACT: 'Rättsakt', + URSPR_BESL: 'Ursprungligt beslutsdatum', + SEN_BESLUT: 'Senaste beslutsdatum', + LINK: { desc: 'Länk', fn: formatLink }, + }); + + layers.skydd.tilltradesforbud = { + title: 'Tillträdesförbud', + fields: mapFields(fieldMap, [ + 'NVRID', + 'FORSKRNAMN', + 'OBJEKTNAMN', + 'BESLSTAT', + 'FORESKRTYP', + 'FORESKRIFT', + 'FRANDATUM', + 'TILLDATUM', + 'BESKRIVN', + 'geom_area', + ]), + }; + + /* Nationella skyddsformer från Naturvårdsregistret */ + const isSurface = (k) => !/_punkt$/.test(k); + Object.entries({ + nationalpark: 'Nationalpark', + naturreservat: 'Naturreservat', + naturreservat_kommunalt: 'Kommunalt naturreservat', + naturvardsomrade: 'Naturvårdsområde', + djur_och_vaxtskyddsomrade: 'Djur- och växtskyddsområde', + kulturreservat: 'Kulturreservat', + vattenskyddsomrade: 'Vattenskyddsområden', + landskapsbildsskyddsomrade: 'Landskapsbildsskyddsområde', + ovrigt_biotopskyddsomrade: 'Biotopskydd utanför skogsmark', + naturminne_yta: 'Naturminne (yta)', + naturminne_punkt: 'Naturminne (punkt)', + interimistiskt_forbud: 'Interimistiskt förbud', + }) + .forEach(([k, title]) => layers.skydd[k] = { + title: title, + fields: mapFields(k, fieldMap, [ + 'NVRID', + 'NAMN', + 'SKYDDSTYP', + 'BESLSTATUS', + 'URSBESLDAT', + ['URSGALLDAT', (k) => k !== 'vattenskyddsomrade'], + ['SENGALLDAT', (k) => k !== 'vattenskyddsomrade'], + ['FORVALTARE', (k) => k !== 'vattenskyddsomrade'], + ['IKRAFTDATF', (k) => k === 'vattenskyddsomrade'], + 'IUCNKAT', + 'DIARIENR', + 'LAGRUM', + 'BESLMYND', + ['TILLSYNSMH', (k) => k === 'vattenskyddsomrade'], + ['PROVNMHTIL', (k) => k === 'vattenskyddsomrade'], + ['PROVNMHDIS', (k) => k === 'vattenskyddsomrade'], + ['geom_area', isSurface], + ['LAND_HA', isSurface], + ['VATTEN_HA', isSurface], + ['SKOG_HA', isSurface], + ]), + }); + + /* Natura 2000-områden */ + (function() { + const fields = [ + { key: 'SITE_CODE', desc: 'Områdeskod', classes: ['feature-objid'] }, + { key: 'NAMN', desc: 'Namn' }, + { key: 'OMRADESTYP', desc: 'Områdestyp' }, + { key: 'UPPLAMNARE', desc: 'Uppgiftslämnare' }, + { key: 'SPA_DATUM', desc: 'SPA-datum' }, + { key: 'SCI_FORSL', desc: 'SCI-förslagsdatum' }, + { key: 'SCI_DATUM', desc: 'SCI-datum' }, + { key: 'SAC_DATUM', desc: 'SAC-datum' }, + fieldMap.geom_area, + { key: 'KVALITET', desc: 'Kvalitet' }, + { key: 'KARAKTAR', desc: 'Kännetecken för området' }, + { key: 'ARTER', desc: 'Arter' }, + { key: 'NATURTYPER', desc: 'Naturtyper' }, + { key: 'BEVPLAN', desc: 'Bevarandeplan', fn: formatLink }, + ]; + Object.entries({ + fageldirektivet: 'Fågeldirektivet (SPA)', + habitatdirektivet: 'Art- och habitatdirektivet (SCI)', + }) + .forEach(([k, title]) => layers.skydd[k] = { title, fields }); + })(); + + /* Områden med internationell status */ + layers.skydd.helcom = { + title: 'Marina skyddade områden (Helcom MPA)', + fields: mapFields(fieldMap, [ 'NAME', 'geom_area' ]), + }; + + layers.skydd.ramsar = { + title: 'Ramsar-områden (Våtmarkskonventionen)', + fields: mapFields(fieldMap, [ + 'RAMSAR_ID', + 'SKYDDSTYP', + 'NAMN', + 'geom_area', + 'LAND_HA', + 'VATTEN_HA', + 'SKOG_HA', + 'URSPR_BESL', + 'SEN_BESLUT', + 'LEGAL_ACT', + 'LINK', + ]), + }; + + layers.skydd.ospar = { + title: 'Marina skyddade områden (Ospar MPA)', + fields: [ + { key: 'ORIGIN', desc: 'Ursprung' }, + { key: 'NAMN_N2000', desc: 'N2000-namn' }, + { key: 'MPA_ID', desc: 'MPA-ID', classes: ['feature-objid'] }, + { key: 'MPA_NAMN', desc: 'MPA-namn' }, + { key: 'N2000_SITE', desc: 'N2000-ID', classes: ['feature-objid'] }, + fieldMap.geom_area, + ], + }; + + layers.skydd.varldsarv = { + title: 'Världsarv med mycket höga naturvärden (Unesco)', + fields: mapFields(fieldMap, [ 'NAMN', 'geom_area' ]), + }; + + layers.skydd.naturvardsavtal = { + title: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', + fields: [ + { key: 'ID', desc: 'ID', classes: ['feature-objid'] }, + { key: 'OBJNAMN', desc: 'Namn' }, + { key: 'FASTBET', desc: 'Fastighet', classes: ['feature-objid'] }, + { key: 'DATSTART', desc: 'Giltig från' }, + { key: 'DATSLUT', desc: 'Giltig till' }, + { key: 'DIARIENRNV', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, + { key: 'STATUS', desc: 'Satus' }, + fieldMap.geom_area, + ], + }; + })(); + + (function() { + const fieldMap = mkFieldMap({ + Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, + Biotyp: { desc: 'Biotopkategori' }, + Naturtyp: { desc: 'Skogstyp' }, + ArendeAr: { desc: 'Registeringsår' }, + geom_area: { desc: 'Areal', fn: 'area' }, + AreaProd: { desc: 'Skogsmarksareal', unit: 'ha' }, + Datbeslut: { desc: 'Beslutsdatum' }, + Url: { desc: 'Länk', fn: (v) => formatLink(v, 'Skogens Pärlor') }, + NvaTyp: 'Biotopkategori', + DatAvtal: 'Avtalsdatum', + Undertyp: 'Undertyp', + AvtalatDatum: 'Avtalat datum', + Objnamn: 'Objektnamn', + Datinv: 'Datum för fältinventering', + }); + + layers.skydd.skogligt_biotopskyddsomrade = { + title: 'Biotopskydd i skogsmark', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Biotyp', + 'Naturtyp', + 'ArendeAr', + 'geom_area', + 'AreaProd', + 'Datbeslut', + 'Url', + ]), + }; + layers.skydd.naturvardsavtal_skogsstyrelsen = { + title: 'Naturvårdsavtal (Skogsstyrelsen)', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'ArendeAr', + 'NvaTyp', + 'Naturtyp', + 'DatAvtal', + 'geom_area', + 'AreaProd', + 'Url', + 'Undertyp', + ]), + }; + + layers.skydd.atervatningsavtal = { + title: 'Återvätningsavtal', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'ArendeAr', + 'AvtalatDatum', + 'geom_area', + 'Url', + ]), + }; + + layers.nv = {}; + Object.assign(fieldMap, mkFieldMap(Object.fromEntries( + [1,2,3].map((i) => [`Biotop${i}`, `Biotoptyp #${i}`]).concat( + [1,2,3,4,5,6,7,8].map((i) => [`Beskrivn${i}`, `Nyckelord #${i} som beskriver objektet`]) + )))); + layers.nv.naturvarde_sks = { + title: 'Objekt med naturvärden (Skogsstyrelsen)', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Objnamn', + 'Datinv', + 'Biotop1', 'Biotop2', 'Biotop3', + 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', + 'geom_area', + 'Url', + ]), + }; + layers.nv.nyckelbiotop = { + title: 'Nyckelbiotop (Skogsstyrelsen)', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Objnamn', + 'Datinv', + 'Biotop1', 'Biotop2', 'Biotop3', + 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', 'Beskrivn4', 'Beskrivn5', 'Beskrivn6', 'Beskrivn7', 'Beskrivn8', + 'geom_area', + 'Url', + ]), + }; + layers.nv.nyckelbiotop_storskogsbruk = { + title: 'Nyckelbiotop (storskogsbruket)', + fields: [ + { key: 'Org', desc: 'Uppgifter lämnade av' }, + { key: 'InkomDatum', desc: 'Inkom datum' }, + fieldMap.geom_area, + fieldMap.Url, + ], + }; + + layers.nv.sumpskog = { + title: 'Sumpskog', + fields: [ + { key: 'Namn', desc: 'Objektnamn' }, + { key: 'Tradtext', desc: 'Skogstyp' }, + { key: 'Hydrtext', desc: 'Hydrologisk typ' }, + { key: 'Delklass', desc: 'Klass på delobjektet' }, + { key: 'Klassu', desc: 'Klass på objektet' }, + { key: 'Lovandel', desc: 'Andel löv' }, + { key: 'Andelva', desc: 'Andel öppet vatten' }, + { key: 'Krontakn', desc: 'Krontäckning' }, + { key: 'Huggklas', desc: 'Huggningsklass' }, + { key: 'Ingrepp', desc: 'Ingrepp på delobjekt (max 4)' }, + { key: 'Ingrpavv', desc: 'Grad av påverkan på delobjekt (max 4)' }, + { key: 'Objnyck', desc: 'Nyckelord på objektnivå' }, + { key: 'Delnyck', desc: 'Nyckelord på delobjektsnivå' }, + { key: 'Flygar', desc: 'Flygbildsår' }, + { key: 'Faltdat', desc: 'Datum för fältbesök' }, + { key: 'Invtekn', desc: 'Inventeringsteknik' }, + { key: 'Invdat', desc: 'Inventeringdatum' }, + { key: 'Ansvmynd', desc: 'Ansvarig myndighet' }, + fieldMap.geom_area, + fieldMap.Url, + ], + }; + })(); + + layers.nv.pagaende_naturreservatsbildning = { + title: 'Pågående naturreservatsbildning', + fields: [ + { key: 'NAMN', desc: 'Objektnamn' }, + /* XXX unclear what "GRANSJUST" means, just a guess */ + { key: 'GRANSJUST', desc: 'Senast justerat' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }; + + layers.nv.snus = { + title: 'Skyddsvärd statlig skog', + fields: [ + { key: 'NAMN', desc: 'Objektnamn' }, + { key: 'AR', desc: 'År' }, + { key: 'NATURGEOGR', desc: 'Naturgeografisk region', classes: ['feature-objid'] }, + { key: 'OBJEKTKATE', desc: 'Objektskategori', classes: ['feature-objid'] }, + { key: 'MARKAGARE', desc: 'Markägare' }, + { key: 'VARDEKARNA', desc: 'Areal värdekärna', unit: 'ha' }, + { key: 'UTV_MARK', desc: 'Areal utvecklingsmark', unit: 'ha' }, + { key: 'TOTAL_AREA', desc: 'Totalareal', unit: 'ha' }, + { key: 'LAND', desc: 'Areal land', unit: 'ha' }, + { key: 'VATTEN', desc: 'Areal vatten', unit: 'ha' }, + { key: 'PROD_SKOG', desc: 'Areal produktiv skogsmark', unit: 'ha' }, + { key: 'SKOG_O_FJG', desc: 'Areal produktiv skogsmark ovanför fjällnära gräns', unit: 'ha' }, + { key: 'SKOG_N_FJG', desc: 'Areal produktiv skogsmark nedanför fjällnära gräns', unit: 'ha' }, + { key: 'SKYDDSZON', desc: 'Areal skyddszon', unit: 'ha' }, + { key: 'ARRO_MARK', desc: 'Areal arronderingsmark', unit: 'ha' }, + { key: 'KRITERIER', desc: 'Kriterier för urval' }, + { key: 'BESKRIVN', desc: 'Beskrivning av området' }, + { key: 'LST_BEDOMN', desc: 'Länsstyrelsens bedömning' }, + { key: 'KALLOR', desc: 'Källor' }, + ], + }; + + layers.ri = {}; + (function() { + const fieldMap = mkFieldMap({ + NAMN: 'Namn', + SKYDD: 'Skydd', + AMNESOMRAD: 'Ämnesområde', + AMNESOMR: 'Ämnesområde', + OMRADESNR: { desc: 'Områdesnummer', classes: ['feature-objid'] }, + BESKRIVNIN: { desc: 'Beskrivning', fn: formatLink }, + LANK_VARDE: { desc: 'Länk värdebeskrivning', fn: formatLink }, + LAGRUM: 'Lagrum', + BESLUTSDAT: 'Beslutsdatum', + BESLDATUM: 'Beslutsdatum', + ARENDENR: { desc: 'Ärendenummer', classes: ['feature-attr-dnr'] }, + LANK_BESLU: { desc: 'Länk beslut', fn: formatLink }, + AKTIVITET: 'Aktivitet', + NATURTYP: 'Naturtyp', + ORGINALID: { desc: 'Original-ID', classes: ['feature-objid'] }, + RIKSID: { desc: 'Riks-ID', classes: ['feature-objid'] }, + geom_area: { desc: 'Areal', fn: 'area' }, + AREA_LAND_: { desc: 'Areal land', unit: 'ha' }, + AREA_VATTE: { desc: 'Areal vatten', unit: 'ha' }, + }); + layers.ri.naturvard = { + title: 'Riksintresse naturvård', + fields: mapFields(fieldMap, [ + 'NAMN', + 'SKYDD', + 'AMNESOMRAD', + 'BESKRIVNIN', + 'LAGRUM', + 'BESLUTSDAT', + 'ORGINALID', + 'RIKSID', + 'geom_area', + ]), + }; + layers.ri.friluftsliv = { + title: 'Riksintresse friluftsliv', + fields: mapFields(fieldMap, [ + 'NAMN', + 'SKYDD', + 'AMNESOMR', + 'OMRADESNR', + 'LANK_VARDE', + 'LAGRUM', + 'BESLDATUM', + 'ARENDENR', + 'LANK_BESLU', + 'AKTIVITET', + 'NATURTYP', + 'geom_area', + 'AREA_LAND_', + 'AREA_VATTE', + ]), + }; + + Object.assign(fieldMap, mkFieldMap({ + METODBESKR: 'Metodbeskrivning', + TILLKDATUM: 'Tillkomstdatum', + REVDATUM: 'Revisionsdatum', + ANM: 'Anmärkning', + OBJEKTLANK: { desc: 'Objektlänk', fn: formatLink }, + REFERENS: 'Referens', + OBJTYP: 'Objekttyp', + ORIGINALID: fieldMap.ORGINALID, + DIG_SKALA: { desc: 'Digitaliseringsskala', fn: (v) => v > 0 ? v : null }, + })); + [ + ['rorligt_friluftsliv', /* 0 */ 'rörligt friluftsliv (MB 4 kap 1§ och 2§)'], + ['obruten_kust', /* 1 */ 'obruten kust (MB 4 kap 3§)'], + ['obrutet_fjall', /* 2 */ 'obrutet fjäll (MB 4 kap 5§)'], + ['skyddade_vattendrag', /* 3 */ 'skyddade vattendrag (MB 4 kap 6§)'], + ] + .forEach(([k, title], idx) => layers.ri[k] = { + title: 'Riksintresse ' + title, + fields: mapFields(idx, fieldMap, [ + 'NAMN', + 'BESKRIVNIN', + 'METODBESKR', + 'TILLKDATUM', + 'REVDATUM', + ['OBJTYP', [1]], + ['ANM', [0,1,3]], + ['DIG_SKALA', [3]], + 'OBJEKTLANK', + 'geom_area', + 'ORIGINALID', + 'REFERENS', + ]), + }); + })(); + + layers.ren = { + betesomrade: { + title: 'Samebyarnas betesområde', + fields: [ + { key: 'NAMN', desc: 'Sameby' }, + { key: 'SAMEBY_TYP', desc: 'Samebys typ' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }, + flyttled: { + title: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', + fields: [ + { key: 'LED_ID', desc: 'Led-ID', classes: ['feature-objid'], fn: (v) => v > 0 ? v : null }, + { key: 'SAMEBY1', desc: 'Sameby #1' }, + { key: 'SAMEBY2', desc: 'Sameby #2' }, + { key: 'SAMEBY3', desc: 'Sameby #3' }, + { key: 'BESKRIVNIN', desc: 'Beskrivning' }, + { key: 'ARSTID', desc: 'Årstid' }, + { key: 'RIKSINTR', desc: 'Riksintresse' }, + { key: 'FAST_LED', desc: 'Fast led' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, + ], + }, + riks_ren: { + title: 'Riksintresse rennäring', + fields: [ + { key: 'LAGRUM', desc: 'Lagrum' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }, + omr_riks: { + title: '(Kärn)områden av riksintresse rennäring', + fields: [ + { key: 'OMR_NR', desc: 'Områdes-ID', classes: ['feature-objid'] }, + { key: 'LANK', desc: 'Länk' }, + { key: 'ARET_RUNT', desc: 'Årets runt' }, + { key: 'SAMEBY', desc: 'Sameby' }, + { key: 'ANSVARIG', desc: 'Ansvarig' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }, + }; + + /* format value to HTML */ + const formatValue = function(value, options) { + let unit = options?.unit; + if (options?.fn == null) { + /* no-op */ + } else if (typeof options.fn === 'function') { + value = options.fn(value); + } else if (options.fn === 'length' && typeof value === 'number' && unit == null) { + if (value < 1000) { + unit = 'm'; + } else { + value /= 1000; + value = Math.round(value*100) / 100; + unit = 'km'; + } + } else if (options.fn === 'area' && typeof value === 'number' && unit == null) { + if (value < 10000) { + unit = 'm²'; + } else if (value < 10000 * 10000) { + value /= 10000; + unit = 'ha'; + } else { + value /= 1000000; + unit = 'km²'; + } + value = Math.round(value*100) / 100; + } + if (value == null) { + return null; + } + if (value instanceof HTMLElement) { + return value; + } + switch (typeof value) { + case 'boolean': + return document.createTextNode(value ? 'Ja' : 'Nej'); + case 'string': + return document.createTextNode(value); + case 'number': + if (unit != null) { + return document.createTextNode(value.toLocaleString('sv-SE') + '\u202F' + unit); + } + return document.createTextNode(value.toString()); + default: + return null; + } + }; + + /* turn the properties into a fine <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: document.getElementById('popup'), + }); + MAP.addOverlay(popupOverlay); + + let featureOverlayLayer = null; + let overlayAttributes = [], + overlayAttrIdx = 0, + mapSources = {}; + /* clear the highlighted feature list and make the overlay layer invisible */ + const disposeFeatureOverlay = function() { + if (featureOverlayLayer?.getVisible?.()) { + featureOverlayLayer.setVisible(false); + featureOverlayLayer.changed(); + } + /* clear the overlay list */ + overlayAttributes = []; + overlayAttrIdx = 0; + mapSources = {}; + } + + let popover = null; + /* clear overlay layer and dispose popover */ + const disposePopover = function() { + disposeFeatureOverlay(); + if (popover?.tip != null) { + popover.dispose(); + } + }; + + /* initialize popover */ + featureOverlayLayer = new VectorTileLayer({ + zIndex: 65535, + declutter: false, + visible: false, + renderMode: 'vector', + style: null, + map: MAP, + }); + + const header = document.createElement('div'); + header.classList.add('d-flex'); + + const headerGrabbingArea = document.createElement('div'); + headerGrabbingArea.classList.add('flex-grow-1', 'grabbing-area', 'pe-2', 'me-2'); + header.appendChild(headerGrabbingArea); + + const pageNode = document.createElement('h6'); + headerGrabbingArea.appendChild(pageNode); + + headerGrabbingArea.onmousedown = function(event) { + /* move the popover around */ + if (event.button != 0) { + return; + } + const popoverTip = popover.tip; + if (popoverTip.classList.contains('popover-maximized')) { + return; + } + headerGrabbingArea.classList.add('grabbing-area-grabbed'); + + if (!popoverTip.classList.contains('popover-detached')) { + /* detach popover tip */ + popoverTip.classList.add('popover-detached'); + const rect = popoverTip.getBoundingClientRect(); + const style = popoverTip.style; + style.display = 'none'; /* avoid reflows between the following assignments */ + style.position = 'absolute'; + style.transform = ''; + style.inset = `${rect.top}px auto auto ${rect.left}px`; + style.display = ''; + } + + let clientX = event.clientX, clientY = event.clientY; + document.onmousemove = function(event) { + const offsetX = clientX - event.clientX; + const offsetY = clientY - event.clientY; + clientX = event.clientX; + clientY = event.clientY; + popoverTip.style.top = (popoverTip.offsetTop - offsetY).toString() + 'px'; + popoverTip.style.left = (popoverTip.offsetLeft - offsetX).toString() + 'px'; + }; + + document.onmouseup = function(event) { + /* done moving around */ + if (event.button != 0) { + return; + } + headerGrabbingArea.classList.remove('grabbing-area-grabbed'); + document.onmousemove = null; + document.onmouseup = null; + }; + }; + + /* current number page and total page count */ + const pageNum = document.createElement('span'); + const pageCount = document.createElement('span'); + pageNode.appendChild(document.createTextNode('Träff ')); + pageNode.appendChild(pageNum); + pageNode.appendChild(document.createTextNode(' av ')); + pageNode.appendChild(pageCount); + + /* highlight a feature */ + const featureOverlayStyle = new Style({ + stroke: new Stroke({ + color: 'rgba(0, 255, 255, .8)', + width: 3, + }), + }); + const highlightFeature = function(layer_group, layer, id) { + const source = mapSources[layer_group]; + if (source == null) { + return; + } + if (featureOverlayLayer.getSource() !== source) { + /* console.log('Updating source for feature overlay layer'); */ + featureOverlayLayer.setVisible(false); + featureOverlayLayer.setSource(source); + } + featureOverlayLayer.setStyle(function(feature) { + if (feature.getId() === id && feature.getProperties().layer === layer) { + return featureOverlayStyle; + } + }); + featureOverlayLayer.setVisible(true); + featureOverlayLayer.changed(); + }; + /* highlight the feature at index overlayAttrIdx within the CGI reply list */ + const refreshPopover = function() { + const attr = overlayAttributes[overlayAttrIdx]; + highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); + + pageNum.innerHTML = (overlayAttrIdx + 1).toString(); + const content = formatFeaturePropertiesToHTML(attr); + popover.tip.getElementsByClassName('popover-body')[0].replaceChildren(content); + }; + /* go back/forward in the overlayAttributes list */ + const onClickPageChange = function(event, offset) { + const btn = event.target; + if (btn.classList.contains('disabled') || popover?.tip == null) { + return; + } + if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) { + return; /* out of range */ + } + + overlayAttrIdx += offset; + if (overlayAttrIdx < 1) { + btnPrev.classList.add('disabled'); + } else { + btnPrev.classList.remove('disabled'); + } + if (overlayAttrIdx < overlayAttributes.length - 1) { + btnNext.classList.remove('disabled'); + } else { + btnNext.classList.add('disabled'); + } + + refreshPopover(); + setTimeout(function() { btn.blur() }, 100); + }; + + /* control buttons */ + const btnPrev = document.createElement('button'); + btnPrev.classList.add('popover-button', 'popover-button-prev'); + btnPrev.setAttribute('type', 'button'); + btnPrev.title = 'Föregående träff'; + btnPrev.setAttribute('aria-label', btnPrev.title); + btnPrev.onclick = function(event) { + return onClickPageChange(event, -1); + }; + + const btnNext = document.createElement('button'); + btnNext.classList.add('popover-button', 'popover-button-next'); + btnNext.setAttribute('type', 'button'); + btnNext.title = 'Nästa träff'; + btnNext.setAttribute('aria-label', btnNext.title); + btnNext.onclick = function(event) { + return onClickPageChange(event, +1); + }; + + const btnExpand = document.createElement('button'); + btnExpand.classList.add('popover-button', 'popover-button-expand'); + btnExpand.setAttribute('type', 'button'); + const btnExpandTitle = 'Förstora'; + const btnExpandTitle2 = 'Förminska'; + btnExpand.setAttribute('aria-label', btnExpand.title); + btnExpand.onclick = function() { /* maximize or reduce the popover */ + if (popover?.tip == null) { + return; + } + if (!popover.tip.classList.contains('popover-maximized')) { + popover.tip.classList.add('popover-maximized'); + btnExpand.classList.replace('popover-button-expand', 'popover-button-reduce'); + btnExpand.title = btnExpandTitle2; + btnExpand.setAttribute('aria-label', btnExpand.title); + } else { + popover.tip.classList.remove('popover-maximized'); + btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); + btnExpand.title = btnExpandTitle; + btnExpand.setAttribute('aria-label', btnExpand.title); + } + setTimeout(function() { btnExpand.blur() }, 100); + }; + + const btnClose = document.createElement('button'); + btnClose.classList.add('popover-button', 'popover-button-close'); + btnClose.setAttribute('type', 'button'); + btnClose.title = 'Stäng'; + btnClose.setAttribute('aria-label', btnClose.title); + btnClose.onclick = disposePopover; + + header.appendChild(btnPrev); + header.appendChild(btnNext); + header.appendChild(btnExpand); + header.appendChild(btnClose); + + MAP.on('singleclick', function(event) { + disposeFeatureOverlay(); + + /* dispose any pre-existing popover if not in detached mode */ + popover = Popover.getInstance(popupOverlay.element); + if (popover?.tip != null && !popover.tip.classList.contains('popover-detached')) { + popover.dispose(); + } + + const size = event.map.getSize(); + if (size[0] < 576 || size[1] < 576) { + return; /* skip popover if the map is too small */ + } + + /* unclear how many feature we'll find, don't render prev/next buttons for now */ + pageNode.classList.add('d-none'); + btnPrev.classList.add('d-none', 'disabled'); + btnNext.classList.add('d-none', 'disabled'); + + /* never start in maximized mode */ + if (popover?.tip != null) { + popover.tip.classList.remove('popover-maximized'); + } + btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); + btnExpand.title = btnExpandTitle; + btnExpand.setAttribute('aria-label', btnExpand.title); + + const fetch_body = []; + event.map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { + const layerGroup = layer.get('layerGroup'); + const layerName = feature.getProperties().layer; + mapSources[layerGroup] ??= layer.getSource(); + const def = layerName != null ? layers[layerGroup][layerName] : null; + if (def?.fields == null) { + /* skip layers which didn't opt-in for popover */ + return false; + } + if (fetch_body.length === 0) { + /* first feature in the list, mark cursor and detached popover as in-progress */ + document.body.classList.add('inprogress'); + popover?.tip?.classList?.add?.('inprogress'); + } + fetch_body.push({ + layer_group: layerGroup, + layer: layerName, + fid: feature.getId() ?? -1, + }); + if (fetch_body.length >= 100) { + return true; /* enough matches already, stop detection here */ + } + }, { + hitTolerance: 5, + checkWrapped: false, + layerFilter: (lyr) => lyr.get('layerGroup') != null, + }); + + if (fetch_body.length === 0) { + /* no feature at pixel (or only within layers which didn't opt-in for popover) */ + if (popover?.tip != null) { + /* dispose pre-detached popover */ + popover.dispose(); + } + return; + } + + fetch('/q', { + method: 'POST', + body: JSON.stringify(fetch_body), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + }) + .then(function(resp) { + if (resp.status === 200) { + return resp.json(); + } else { + throw new Error(`${resp.url} [${resp.status}]`); + } + }) + .then(function(data) { + /* the data is received from the CGI in the order it was sent */ + /* TODO optimizations on the CGI would break the above assumption, so the + * decoded JSON response would need to be reordered to match fetch_body */ + overlayAttributes = data; + if (overlayAttributes.length === 0) { + /* couldn't fetch any attribute for feature(s) at pixel */ + if (popover?.tip != null) { + /* dispose pre-detached popover */ + popover.dispose(); + } + return; + } + + pageCount.innerHTML = overlayAttributes.length.toString(); + if (overlayAttributes.length >= 2) { + /* render prev/pre buttons */ + btnNext.classList.remove('d-none', 'disabled'); + btnPrev.classList.remove('d-none'); + pageNode.classList.remove('d-none'); + } + if (popover?.tip == null) { + /* create a new popover (we're not already showing one in detached mode) */ + pageNum.innerHTML = (overlayAttrIdx + 1).toString(); + popupOverlay.setPosition(event.coordinate); + + const attr = overlayAttributes[0]; + highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); + popover = new Popover(popupOverlay.element, { + template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div>' + + '<div class="popover-header"></div><div class="popover-body"></div></div>', + title: header, + content: formatFeaturePropertiesToHTML(attr), + html: true, + placement: 'right', + fallbackPlacements: ['right', 'left', 'bottom', 'top'], + container: CONTAINER_STOPEVENT, + }); + popover.show(); + } + else if (popover.tip.classList.contains('popover-detached')) { + /* update existing detached mode popover */ + refreshPopover(); + popover.tip.classList.remove('inprogress'); + } + }) + .catch(function(e) { + console.log(e); + }) + .finally(function() { + /* remove in-progress marking on the cursor */ + document.body.classList.remove('inprogress'); + }); + }); + + return disposePopover; +})(); + +/* age filter dialog */ (function() { const panel = document.getElementById('age-filter-modal'); @@ -1834,7 +5135,7 @@ const infoMetadataAccordions = []; p.appendChild(document.createTextNode('.')); type_choice._update_helptext = function() { - const d = age_filter_settings.get_relative_date( + const d = ageFilterSettings.getRelativeDate( parseInt(type_choice.quantity.value, 10), type_choice.unit[0].value ); @@ -1957,7 +5258,7 @@ const infoMetadataAccordions = []; checkbox.type = 'checkbox'; checkbox.id = 'age-filter-show-unknown'; checkbox.setAttribute('role', 'switch'); - checkbox.checked = age_filter_settings.show_unknown; + checkbox.checked = ageFilterSettings.show_unknown; div.appendChild(checkbox); const lbl = document.createElement('label'); @@ -1988,7 +5289,7 @@ const infoMetadataAccordions = []; btn_cancel.onclick = function() { /* deactivate deactivate the filter but preserve its settings */ - age_filter_settings.active = false; + ageFilterSettings.active = false; Object.values(mapLayers).forEach(function(lyr) { if (lyr?.get('canFilterByAge')) { lyr.changed(); @@ -2007,29 +5308,29 @@ const infoMetadataAccordions = []; event.preventDefault(); const [filter_type, filter_settings] = Object.entries(type_choices).filter( (x) => x[1].radio.checked )[0]; let param; - age_filter_settings._min_ts = age_filter_settings._max_ts = null; + ageFilterSettings._min_ts = ageFilterSettings._max_ts = null; switch (filter_type) { case 'relative': { - const operator = age_filter_settings.operator = filter_settings.operator[0].value; - age_filter_settings.quantity = parseInt(filter_settings.quantity.value, 10); - age_filter_settings.unit = filter_settings.unit[0].value; + 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 += age_filter_settings.quantity.toString() + age_filter_settings.unit; + param += ageFilterSettings.quantity.toString() + ageFilterSettings.unit; break; } case 'interval': { - const date1 = age_filter_settings.from = parse_date(filter_settings.from.value); - const date2 = age_filter_settings.to = parse_date(filter_settings.to.value); + 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; } - age_filter_settings.type = filter_type; - age_filter_settings.show_unknown = show_unknown_age.checked; - age_filter_settings.setup_minmax(); - age_filter_settings.active = true; + ageFilterSettings.type = filter_type; + ageFilterSettings.show_unknown = show_unknown_age.checked; + ageFilterSettings.setupMinMax(); + ageFilterSettings.active = true; /* TODO auto update the filter passed midnight (if active) */ Object.values(mapLayers).forEach(function(lyr) { if (lyr?.get('canFilterByAge')) { @@ -2049,24 +5350,24 @@ const infoMetadataAccordions = []; * 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[age_filter_settings.type]; + const type_choice = type_choices[ageFilterSettings.type]; type_choice.radio.click(); - switch (age_filter_settings.type) { + switch (ageFilterSettings.type) { case 'relative': { Object.entries(type_choice.operator[1]).map(function([id, option]) { - option.selected = id === age_filter_settings.operator; + option.selected = id === ageFilterSettings.operator; }); - type_choice.quantity.value = age_filter_settings.quantity.toString(); + type_choice.quantity.value = ageFilterSettings.quantity.toString(); Object.entries(type_choice.unit[1]).map(function([id, option]) { - option.selected = id === age_filter_settings.unit; + option.selected = id === ageFilterSettings.unit; }); type_choice.quantity.dispatchEvent(new Event('change')); /* propagate to absolute */ break; } case 'interval': { - type_choice.from.value = format_date(age_filter_settings.from); - type_choice.to.value = format_date(age_filter_settings.to); + 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; } @@ -2084,7 +5385,7 @@ const infoMetadataAccordions = []; }; const btn = document.getElementById('age-filter-button').getElementsByTagName('button')[0]; - if (age_filter_settings.active) { + if (ageFilterSettings.active) { btn.classList.replace('btn-light', 'btn-dark'); } panel.addEventListener('show.bs.modal', function() { @@ -2100,7 +5401,7 @@ const infoMetadataAccordions = []; } }); panel.addEventListener('hidden.bs.modal', function() { - if (!age_filter_settings.active) { + if (!ageFilterSettings.active) { btn.classList.replace('btn-dark', 'btn-light'); } btn.setAttribute('aria-expanded', 'false'); diff --git a/src/layers.js b/src/layers.js deleted file mode 100644 index 688bb44..0000000 --- a/src/layers.js +++ /dev/null @@ -1,1908 +0,0 @@ -/*********************************************************************** - * Copyright © 2024-2025 Guilhem Moulin <info@guilhem.se> - * Vector and raster layer definitions - * - * 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 <https://www.gnu.org/licenses/>. - **********************************************************************/ - -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'; - -/* TODO: this should really be refactored… */ -export const layers = { - 'mrr.appr_ec': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 22, - fill: new Fill({ - color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - 'mrr.appl_ec': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 25, - fill: new Fill({ - color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [197, 14, 31, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - 'mrr.appr_met': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 24, - fill: new Fill({ - color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - 'mrr.appl_met': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 26, - fill: new Fill({ - color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [197, 14, 31, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - 'mrr.appr_ogd': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 24, - fill: new Fill({ - color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - 'mrr.appl_ogd': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 26, - fill: new Fill({ - color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [197, 14, 31, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - 'mrr.appr_dl': { - legend: { zoomLevel: 4 }, - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 20, - fill: new Fill({ - color: [228, 53, 45, Math.max((.2-1)/6 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - - 'svk.ledningar': { - legend: { zoomLevel: 5, type: 'linestring', reuse_canvas: true }, - style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { - return new Style({ - zIndex: 52, - stroke: new Stroke({ - color: 'black', - width: width, - }), - }); - }), - }, - 'svk.stolpar': { - legend: { zoomLevel: 5, type: 'point' }, - style: [undefined, undefined, undefined, undefined, undefined] - .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { - return new Style({ - zIndex: 51, - image: new CircleStyle({ - radius: radius, - fill: new Fill({ - color: 'black', - }), - }), - }); - })), - }, - 'svk.transmissionsnatsprojekt': { - legend: { zoomLevel: 5, type: 'linestring' }, - style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { - return new Style({ - zIndex: 53, - stroke: new Stroke({ - color: 'black', - width: width, - lineDash: [4 * width], - }), - }); - }), - }, - 'svk.stationer': { - legend: { zoomLevel: 3, type: 'point' }, - style: [3, 4, 5, 6, 7, 8.5, 10].map(function(radius) { - return new Style({ - zIndex: 50, - image: new RegularShape({ - radius: radius, - points: 4, - angle: Math.PI/4, - fill: new Fill({ - color: 'black', - }), - }), - }); - }) - .concat([.5, 1, 1.5, 2, 2].map(function(width) { - return new Style({ - zIndex: 50, - fill: new Fill({ - color: 'rgba(128, 128, 128, .7)', - }), - stroke: new Stroke({ - width: width, - color: 'rgb(0, 0, 0)', - }), - }); - })), - }, - - 'vbk.area_current': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [168, 198, 223, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [56, 96, 130, 1], - }), - }); - }), - }, - 'vbk.area_notcurrent': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [222, 163, 199, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [148, 55, 112, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - 'vbk.offshore_completed': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 17, - fill: new Fill({ - color: [38, 107, 29, .5], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [38, 107, 29, 1], - }), - }); - }), - }, - 'vbk.offshore_approved': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 16, - fill: new Fill({ - color: [56, 160, 44, .5], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [56, 160, 44, 1], - }), - }); - }), - }, - 'vbk.offshore_amended': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; - patternCanvas.width = width/2; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(247, 105, 162, 1)'; - patternContext.beginPath(); - patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, 1.5*w, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 17, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: 2*w, - color: [247, 105, 162, 1], - lineDash: [8 * w], - }), - }); - }), - }, - 'vbk.offshore_rejected': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 11, - fill: new Fill({ - color: [227, 26, 28, .5], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [227, 26, 28, 1], - }), - }); - }), - }, - 'vbk.offshore_appealed': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 15, - fill: new Fill({ - color: [177, 88, 40, .5], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [177, 88, 40, 1], - }), - }); - }), - }, - 'vbk.offshore_applied': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 14, - fill: new Fill({ - color: [255, 127, 0, .5], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [255, 128, 0, 1], - }), - }); - }), - }, - 'vbk.offshore_consultation': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 13, - fill: new Fill({ - color: [254, 217, 118, .65], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [254, 183, 82, 1], - }), - }); - }), - }, - 'vbk.offshore_investigation': { - legend: { zoomLevel: 1 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; - patternCanvas.width = width*2; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(68, 90, 166, 1)'; - patternContext.lineWidth = w; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 12, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: 2*w, - color: [68, 90, 166, 1], - lineDash: [8 * w], - }), - }); - }), - }, - 'vbk.offshore_revoked': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [105, 61, 154, .5], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [105, 62, 153, 1], - }), - }); - }), - }, - 'vbk.station_completed': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-completed.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - 'vbk.station_processed': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-processed.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - 'vbk.station_approved': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-approved.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - 'vbk.station_revoked': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-revoked.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - 'vbk.station_rejected': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-rejected.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - 'vbk.station_dismounted': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-dismounted.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - 'vbk.station_appealed': { - legend: { zoomLevel: 7, type: 'point' }, - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-appealed.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - - /* Documentation at - * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf - * */ - 'avverk.utford': { - legend: { zoomLevel: 7 }, - style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [204, 0, 0, 1], - }), - }); - }), - }, - /* Documentation at - * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf - * */ - 'avverk.anmald': { - legend: { zoomLevel: 7 }, - style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: (width === undefined || width === 0) ? - new Fill({ color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)*.75] }) : - (function() { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - const slope = 45 * Math.PI/180; - const spacing = z < 10 ? z*2 : 40; - const len = Math.hypot(1, slope); - const w = patternCanvas.width = Math.round(1/len + spacing) - const h = patternCanvas.height = Math.round(slope/len + spacing * slope); - - patternContext.fillStyle = 'rgba(255, 102, 102, .1)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(204, 0, 0, 1)'; - patternContext.lineWidth = Math.max(1, width/2); - patternContext.beginPath(); - patternContext.moveTo(0, h); - patternContext.lineTo(w, 0); - patternContext.moveTo(-w, h); - patternContext.lineTo(w, -h); - patternContext.moveTo(0, 2*h); - patternContext.lineTo(2*w, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Fill({ color: context.createPattern(patternCanvas, 'repeat') }); - })(), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [204, 0, 0, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - - 'skydd.tilltradesforbud': { - legend: { zoomLevel: 2 }, - style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { - return new Style({ - zIndex: 23, - fill: new Fill({ - /* transparent fill so clicking the inside of the polygon triggers a popover */ - /* XXX could also use a custom renderer but that doesn't seem to work */ - color: [0, 0, 0, 0], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [255, 0, 0, 1], - }), - }); - }), - }, - 'skydd.nationalpark': { - legend: { zoomLevel: 1 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 55, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 22, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [0, 55, 0, 1], - }), - }); - }), - }, - 'skydd.naturreservat': { - legend: { zoomLevel: 1 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 21, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [7, 181, 7, 1], - }), - }); - }), - }, - 'skydd.naturreservat_kommunalt': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 20, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [7, 181, 7, 1], - }), - }); - }), - }, - 'skydd.naturvardsomrade': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(176, 255, 176, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 19, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [176, 255, 176, 1], - }), - }); - }), - }, - 'skydd.djur_och_vaxtskyddsomrade': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 255, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 18, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 255, 0, 1], - }), - }); - }), - }, - 'skydd.kulturreservat': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(154, 102, 255, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 17, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [154, 102, 255, 1], - }), - }); - }), - }, - 'skydd.vattenskyddsomrade': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 105, 212, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 16, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [0, 105, 212, 1], - }), - }); - }), - }, - 'skydd.landskapsbildsskyddsomrade': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(135, 110, 71, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 15, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [134, 110, 71, 1], - }), - }); - }), - }, - 'skydd.skogligt_biotopskyddsomrade': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(135, 90, 71, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 14, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [134, 90, 71, 1], - }), - }); - }), - }, - 'skydd.ovrigt_biotopskyddsomrade': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 95, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 13, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [255, 95, 0, 1], - }), - }); - }), - }, - 'skydd.naturminne_yta': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(113, 0, 116, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 12, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [134, 0, 116, 1], - }), - }); - }), - }, - 'skydd.naturminne_punkt': { - legend: { zoomLevel: 6, type: 'point' }, - style: [undefined, undefined, undefined, undefined].concat([3, 4, 6, 8, 12, 16, 20, 24].map(function(width) { - return new Style({ - zIndex: 12, - image: new CircleStyle({ - radius: width, - fill: new Fill({ - color: 'rgba(113, 0, 116, .5)', - }), - stroke: new Stroke({ - width: Math.log2(width)/2, - color: 'rgba(113, 0, 116, 1)', - }), - }), - }); - })) - }, - 'skydd.interimistiskt_forbud': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(168, 0, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 11, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [168, 0, 0, 1], - }), - }); - }), - }, - 'skydd.fageldirektivet': { - legend: { zoomLevel: 1 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width*2; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(230, 0, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - patternContext.beginPath(); - patternContext.lineWidth *= 6; - patternContext.moveTo(-.5*patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -.5*patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 1.5*patternCanvas.height); - patternContext.lineTo(1.5*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 10, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [230, 0, 0, 1], - }), - }); - }), - }, - 'skydd.habitatdirektivet': { - legend: { zoomLevel: 1 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width*2; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 77, 168, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - patternContext.beginPath(); - patternContext.lineWidth *= 6; - patternContext.moveTo(0, -.5*patternCanvas.height); - patternContext.lineTo(1.5*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-.5*patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 1.5*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 10, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [0, 77, 168, 1], - }), - }); - }), - }, - 'skydd.helcom': { - legend: { zoomLevel: 1 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(130, 130, 130, 1)'; - const r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.5*patternCanvas.width, .5*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [130, 130, 130, 1], - }), - }); - }), - }, - 'skydd.ramsar': { - legend: { zoomLevel: 1 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(195, 0, 255, 1)'; - const r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.25*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [195, 0, 255, 1], - }), - }); - }), - }, - 'skydd.ospar': { - legend: { zoomLevel: 1 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; - const r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.25*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [168, 0, 0, 1], - }), - }); - }), - }, - 'skydd.varldsarv': { - legend: { zoomLevel: 1 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; - const r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.75*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [168, 0, 0, 1], - }), - }); - }), - }, - 'skydd.biosfarsomraden': { - legend: { zoomLevel: 1 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(131, 0, 219, 1)'; - const r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [131, 0, 219, 1], - }), - }); - }), - }, - 'skydd.naturvardsavtal': { - legend: { zoomLevel: 1 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 21, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [255, 0, 197, 1], - }), - }); - }), - }, - 'skydd.naturvardsavtal_skogsstyrelsen': { - legend: { zoomLevel: 2 }, - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 20, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [255, 0, 197, 1], - }), - }); - }), - }, - 'skydd.atervatningsavtal': { - legend: { zoomLevel: 0 }, - style: [0, 1, 2, 3, 4, 5, 6].map(function(width) { - return new Style({ - zIndex: 5, - fill: new Fill({ - color: [255, 115, 0, .4], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: .5, - color: [255, 115, 0, 1], - }), - }); - }) - .concat([7, 8, 9, 10, 11].map(function() { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = 16; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 115, 0, 1)'; - patternContext.lineWidth = 1; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 5, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: new Stroke({ - width: 1.5, - color: [255, 115, 0, 1], - }), - }); - })), - }, - 'nv.naturvarde_sks': { - legend: { zoomLevel: 0 }, - style: [0, 1, 2, 3, 4, 5].map(function(width) { - return new Style({ - zIndex: 6, - fill: new Fill({ - color: [255, 170, 0, .2], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: .5, - color: [255, 170, 0, .8], - }), - }); - }) - .concat([6, 7, 8, 9, 10, 11].map(function() { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = 16; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 170, 0, 1)'; - patternContext.lineWidth = 1; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 6, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: new Stroke({ - width: 1.5, - color: [255, 170, 0, 1], - }), - }); - })), - }, - 'nv.nyckelbiotop': { - legend: { zoomLevel: 0 }, - style: [0, 1, 2, 3, 4, 5].map(function(width) { - return new Style({ - zIndex: 6, - fill: new Fill({ - color: [217, 148, 9, .2], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: .5, - color: [217, 148, 9, .8], - }), - }); - }) - .concat([6, 7, 8, 9, 10, 11].map(function() { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = 16; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; - patternContext.lineWidth = 1; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 6, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: new Stroke({ - width: 1.5, - color: [217, 148, 9, 1], - }), - }); - })), - }, - 'nv.nyckelbiotop_storskogsbruk': { - legend: { zoomLevel: 0 }, - style: [0, 1, 2, 3, 4, 5].map(function(width) { - return new Style({ - zIndex: 6, - fill: new Fill({ - color: [217, 148, 9, .2], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: .5, - color: [217, 148, 9, .8], - }), - }); - }) - .concat([6, 7, 8, 9, 10, 11].map(function() { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = 16; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; - patternContext.lineWidth = 1; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 6, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: new Stroke({ - width: 1.5, - color: [217, 148, 9, 1], - }), - }); - })), - }, - 'nv.sumpskog': { - legend: { zoomLevel: 5 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - const w = Math.max(1, width); - patternCanvas.width = z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 6 : 8; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(158, 200, 215, 1)'; - patternContext.lineWidth = w; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 5, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: w/2, - color: [158, 200, 215, 1], - }), - }); - }), - }, - 'nv.pagaende_naturreservatsbildning': { - legend: { zoomLevel: 1 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.setLineDash([width/4, width/4]); - patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(width/4, 0); - patternContext.lineTo(width/4, patternCanvas.height); - patternContext.stroke(); - patternContext.beginPath(); - patternContext.lineDashOffset = width/4; - patternContext.moveTo(3*width/4, 0); - patternContext.lineTo(3*width/4, patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 10, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 3 : 4, - color: [7, 181, 7, 1], - lineDash: [width/8, width/4], - }), - }); - }), - }, - 'nv.snus': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { - return new Style({ - zIndex: 4, - fill: new Fill({ - color: [168,168,0,.2], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [168,77,0,.75], - }), - }); - }), - }, - - 'ri.naturvard': { - legend: { zoomLevel: 0 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(154, 230, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [154, 230, 0, 1], - }), - }); - }), - }, - 'ri.friluftsliv': { - legend: { zoomLevel: 0 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 127, 232, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [0, 127, 232, 1], - }), - }); - }), - }, - 'ri.rorligt_friluftsliv': { - legend: { zoomLevel: 0 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(187, 227, 212, .25)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(56, 151, 117, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [56, 151, 117, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - 'ri.obruten_kust': { - legend: { zoomLevel: 0 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(227, 227, 187, .25)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(156, 158, 56, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [156, 158, 56, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - 'ri.obrutet_fjall': { - legend: { zoomLevel: 0 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(255, 255, 209, .25)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(219, 183, 60, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [219, 183, 60, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - 'ri.skyddade_vattendrag': { - legend: { zoomLevel: 0 }, - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - return new Style({ - zIndex: 8, - fill: new Fill({ - color: [102, 157, 240, .25], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [41, 109, 197, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - - 'ren.betesomrade': { - legend: { zoomLevel: 0 }, - style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { - return new Style({ - zIndex: 4, - fill: new Fill({ - /* transparent fill so clicking the inside of the polygon triggers a popover */ - /* XXX could also use a custom renderer but that doesn't seem to work */ - color: [0, 0, 0, 0], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], - }), - }); - }), - }, - 'ren.flyttled': { - legend: { zoomLevel: 2, type: 'linestring' }, - style: [.75, 1, 1.5, 2, 3, 4, 5, 5, 6, 7, 8, 10].map(function(width) { - return new Style({ - zIndex: 7, - stroke: new Stroke({ - width: 2*width, - color: [119, 99, 59, 1], - lineDash: [4 * width], - }), - }); - }), - }, - 'ren.riks_ren': { - legend: { zoomLevel: 1 }, - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = z < 4 ? 4 : z <= 5 ? 8 : Math.pow(2, Math.round(Math.log2(width) + 3)); - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(179, 153, 102, 1)'; - patternContext.lineWidth = Math.max(1, width/2); - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 6, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], - }), - }); - }), - }, - 'ren.omr_riks': { - legend: { zoomLevel: 2 }, - style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { - return new Style({ - zIndex: 5, - fill: new Fill({ - color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], - }), - }); - }), - }, - - /* Documentation at - * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf - * */ - 'misc.dammar': { - legend: { zoomLevel: 5, type: 'point' }, - style: [2, 3, 4, 4, 4, 6, 8, 8, 8, 10, 16, 32].map(function(width) { - return new Style({ - zIndex: 59, - image: new CircleStyle({ - radius: width, - fill: new Fill({ - color: 'rgb(219, 30, 42)', - }), - stroke: new Stroke({ - width: Math.log2(width) * 2/5, - color: 'rgb(128, 17, 25)', - }), - }), - }); - }), - }, - - 'misc.gigafactories': { - legend: { zoomLevel: 1, type: 'point' }, - style: [4, 6, 7, 8, 10, 12].map(function(width) { - return new Style({ - zIndex: 60, - image: new CircleStyle({ - radius: width, - fill: new Fill({ - color: 'rgb(152, 78, 163)', - }), - stroke: new Stroke({ - width: Math.log2(width) * 2/5, - color: 'rgb(119, 61, 128)', - }), - }), - }); - }) - .concat([1.5, 2, 2, 2, 2, 2].map(function(width) { - return new Style({ - zIndex: 58, - fill: new Fill({ - color: 'rgba(152, 78, 163, .4)', - }), - stroke: new Stroke({ - width: width, - color: 'rgb(119, 61, 128)', - }), - }); - })), - }, - - 'kskog.1' : { style: [ 56, 168, 0, .2] }, /* #1 Sannolikt kontinuitetsskog (preciserad) */ - 'kskog.2' : { style: [169, 0, 230, .2] }, /* #2 Sannolikt påverkad kontinuitetsskog (preciserad) */ - 'kskog.3' : { style: [152, 230, 0, .2] }, /* #3 Sannolikt kontinuitetsskog i fjällen (grövre precisering) */ - 'kskog.4' : { style: [ 76, 115, 0, .2] }, /* #4 Potentiell kontinuitetsskog (2015) */ -}; diff --git a/src/map.js b/src/map.js deleted file mode 100644 index 5a751e6..0000000 --- a/src/map.js +++ /dev/null @@ -1,103 +0,0 @@ -/*********************************************************************** - * Copyright © 2024-2025 Guilhem Moulin <info@guilhem.se> - * Map base setup - * - * 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 <https://www.gnu.org/licenses/>. - **********************************************************************/ - -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 proj4 from 'proj4'; -import { get as getProjection } from 'ol/proj.js'; -import { register as registerProjection } from 'ol/proj/proj4.js'; - -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); - -export const projection = getProjection('EPSG:3006'); - -/* Lantmäteriet uses a tile-scheme where the origin (upper-left corner) is at - * N8500000 E-1200000 (SWEREF99 TM), where each tile is 256×256 pixels, and where - * the resolution at level 0 is 4096m per pixel (each side is 1048.576km long). - * - * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning_cache_v1.1.0.pdf - * https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf - * - * We set the extent to a 4×4 tiles square at level 2 (1024px = 1048.576km per - * side) somehow centered on Norrbotten and Västerbotten, and zoom in from there. - * This represent a TILEROW (x) offset of 5, and a TILECOL (y) offset of 2. - */ -export const extent = [110720, 6927136, 1159296, 7975712]; - -/* XXX using the topowebbcache WMTS is fine for testing (as it doesn't require - * authentication) but not in production in a public instance as doing so would - * violate its current terms of use (as of January 2024 it's not CC0 open data). - * See - * - * https://www.lantmateriet.se/sv/om-lantmateriet/Rattsinformation/upphovsratt-och-publicering-av-lantmateriets-geografiska-information/ - * https://www.lantmateriet.se/sv/kartor/vara-karttjanster/min-karta/#anchor-2 - * https://help.locusmap.eu/topic/support-for-swedish-lantmateriets-min-karta-wms - * - * More precise background maps might be available in the future as open data, - * though: - * - * https://www.lantmateriet.se/sv/om-lantmateriet/press/nyheter/lantmateriets-arbete-mot-oppna-data-i-full-gang/ - * https://ext-geodatakatalog.lansstyrelsen.se/GeodataKatalogen/srv/swe/catalog.search#/map uses - * https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/3c3a9cf47e7cb5ea24542d40d19698/?layer=topowebb&style=default&tilematrixset=3006&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=7&TileCol=237&TileRow=155 - */ -export 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, -}); - -export const map = new Map({ - controls: [], - view: view, - layers: [ - new TileLayer({ - source: baseMapSource - }), - ], -}); diff --git a/src/popover.js b/src/popover.js deleted file mode 100644 index 7080711..0000000 --- a/src/popover.js +++ /dev/null @@ -1,1348 +0,0 @@ -/*********************************************************************** - * Copyright © 2024-2025 Guilhem Moulin <info@guilhem.se> - * Popup and feature overlays - * - * 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 <https://www.gnu.org/licenses/>. - **********************************************************************/ - -import Overlay from 'ol/Overlay.js'; -import Stroke from 'ol/style/Stroke.js'; -import Style from 'ol/style/Style.js'; -import VectorTileLayer from 'ol/layer/VectorTile.js'; - -import { Popover } from 'bootstrap'; - -import { map } from './map.js'; - -/* 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, {key: k, desc: o}]; - } else { - return [k, Object.assign(o, {key: k})]; - } - })); -}; - -const LAYERS = { - svk: { - ledningar: { - title: 'Kraftledning (befintlig)', - fields: [ - { key: 'Placement', desc: 'Förläggning' }, - { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, - { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, - ], - }, - transmissionsnatsprojekt: { - title: 'Transmissionsnätsprojekt', - fields: [ - { key: 'Name', desc: 'Projektnamn' }, - { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, - { key: 'Url', desc: 'Länk', fn: formatLink }, - ], - }, - }, - - misc: { - gigafactories: { - title: 'Stor industrisatsning', - fields: [ - { key: 'Name', desc: 'Namn' }, - { key: 'Url', desc: 'Länk', fn: formatLink }, - ], - }, - dammar: { - /* Documentation at - * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf */ - title: 'Damm', - fields: [ - { key: 'DNamn', desc: 'Dammenhetens namn' }, - { key: 'Namn', desc: 'Dammanläggningens namn' }, - { key: 'LST_OBJID', desc: 'Länsnr', classes: ['feature-objid'] }, - { key: 'Status', desc: 'Status', fn: (v) => v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : '' }, - //{ key: 'Regleringstyp', desc: 'Regleringstyp' }, - { key: 'ByggAr', desc: 'Byggår' }, - { key: 'DammHojd', desc: 'Dammhöjd', unit: 'm' }, - { key: 'KronLangd', desc: 'Krönlängd', unit: 'm' }, - { key: 'Fiskvag', desc: 'Fiskväg', fn: (v) => - v === 1 ? 'Bassängtrappa' : - v === 2 ? 'Denilränna' : - v === 3 ? 'Slitsränna' : - v === 4 ? 'Omlöp' : - v === 5 ? 'Inlöp' : - v === 6 ? 'Ålledare' : - v === 7 ? 'Smoltränna' : - v === 8 ? 'Okänd typ' : - v === 9 ? 'Ingen' : - v === 10 ? 'Annan' : - null }, - { key: 'HARO', desc: 'Huvudavrinningsområdesnummer', classes: ['feature-objid'] }, - { key: 'Vattendistrikt', desc: 'Vattendistrikt', classes: ['feature-objid'] }, - { key: 'Verksamhet', desc: 'Verksamhet', fn: (v) => - v === 1 ? 'Kraftproduktion' : - v === 2 ? 'Industri' : - v === 3 ? 'Sjöfart' : - v === 4 ? 'Invallning' : - v === 5 ? 'Vattenförsörjning' : - v === 6 ? 'Spegeldamm' : - v === 7 ? 'Historisk' : - v === 8 ? 'Övrigt' : - null }, - { key: 'DG', desc: 'Högsta dämningsgräns', unit: 'm' }, - { key: 'SG', desc: 'Lägsta sänkningsgräns', unit: 'm' }, - { key: 'MY', desc: 'Magasinsyta', unit: 'km²' }, - { key: 'RV', desc: 'Reglerbar volym', unit: 'Mm³' }, - { key: 'Kommentar', desc: 'Kommentar' }, - ], - }, - }, -}; - - -LAYERS.mrr = {}; -(function() { - const fields = [ - { key: 'name', desc: 'Namn' }, - { key: 'mineral', desc: 'Koncessionsmineral', cond: (i) => i < 6 }, - { key: 'owners', desc: 'Ägare', cond: [0,2,4] }, - { key: 'owners', desc: 'Sökande', cond: [1,3,5] }, - { key: 'conc_name', desc: 'Tillhörande bearbetnings\u00ADkoncession(er)', cond: [6] }, - { key: 'licenceid', desc: 'Tillståndsid', classes: ['feature-attr-mrr-license-id'], cond: [0,2,4,6] }, - { key: 'geom_area', desc: 'Areal', fn: 'area' }, - { key: 'validfrom', desc: 'Giltig från', cond: [0,2,4] }, - { key: 'validto', desc: 'Giltig till', cond: [0,2,4] }, - { key: 'diarynr', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, - { key: 'appl_date', desc: 'Ansökningsdatum' }, - { key: 'dec_date', desc: 'Beslutsdatum', cond: [0,2,4,6] }, - ]; - Object.entries({ - ec: 'Bearbetningskoncession', - met: 'Undersökningstillstånd, metaller och industrimineral', - ogd: 'Undersökningstillstånd, olja, gas och diamant', - }) - .flatMap(([k, title]) => [ - /* don't use Object.entries() to guaranty ordering */ - ['appr', 'beviljad'], /* even index */ - ['appl', 'ansökt'], /* odd index */ - ].map(([a,b]) => [a + '_' + k, title + ' \u2013 ' + b])) - .concat([['appr_dl', 'Markanvisning till koncession']]) /* index #6 */ - .forEach(([k, title], idx) => LAYERS.mrr[k] = { title, fields: filterFields(idx, fields) }); -})(); - - -LAYERS.vbk = {}; -(function() { - const fieldMap = mkFieldMap({ - Projektnamn: 'Projektnamn', - OmrID: { desc: 'Områdes-ID', classes: ['feature-objid'] }, - AntalVerk: 'Aktuella verk', - AntalEjXY: 'Antal ej koordinatsatta verk', - Projektstatus: 'Projektstatus', - Diarienummer: 'Diarienummer', - geom_area: { desc: 'Areal', fn: 'area' }, - Calprod: { desc: 'Beräknad årsproduktion', unit: 'GWh' }, - PlaneradByggstart: 'Planerad byggstart', - PlaneratDrift: 'Planerat drifttagande', - AndringsansokanPagar: 'Ändringsansökan pågår', - UnderByggnation: 'Under byggnation', - Organisationsnamn: 'Verksamhetsutövare', - Organisationsnummer: { desc: 'Organisationsnummer', classes: ['feature-orgnr'] }, - SamradsunderlagInlamnat: 'Samrådsunderlag inlämnat', - AnsokanInlamnat: 'Tillståndsansökan inlämnad', - AnsokanAterkallad: 'Tillståndsansökan återkallad', - AnsokanBeviljad: 'Tillståndsansökan beviljad', - AnsokanAvslagen: 'Tillståndsansökan avslagen', - AnsokanOverklagad: 'Överklagad', - Natura2000_Ansokan: 'Natura2000 ansökan', - Natura2000_Beslutdatum: 'Natura2000 beslutsdatum', - Uppfort: 'Parken uppförd', - PlaneratAntalVerkMin: 'Planerat antal verk (min)', - PlaneratAntalVerkMax: 'Planerat antal verk (max)', - PlaneradHojdMin: { desc: 'Panerad totalhöjd (min)', unit: 'm' }, - PlaneradHojdMax: { desc: 'Panerad totalhöjd (max)', unit: 'm' }, - PlaneradProduktionMin: { desc: 'Planerad årsproduktion (min)', unit: 'GWh' }, - PlaneradProduktionMax: { desc: 'Planerad årsproduktion (max)', unit: 'GWh' }, - BeviljatAntalVerk: 'Beviljat antal verk', - UppfortAntalVerk: 'Uppfört antal verk', - BeviljadMaxhojd: { desc: 'Beviljad maxhöjd', unit: 'm' }, - InstalleradEffekt: { desc: 'Installerad effekt', unit: 'MW' }, - ElNamn: 'Elområde', - SenasteUppdaterat: 'Senast uppdaterat', - }); - - Object.entries({ - current: null, - notcurrent: ' \u2013 ej aktuell', - }) - .forEach(([k, title]) => LAYERS.vbk['area_' + k] = { - title: 'Landbaserad projekteringsområde för vindkraft' + (title ?? ''), - fields: mapFields(k, fieldMap, [ - 'Projektnamn', - 'OmrID', - 'AntalVerk', - 'AntalEjXY', - 'geom_area', - 'Calprod', - 'PlaneradByggstart', - 'PlaneratDrift', - 'AndringsansokanPagar', - ['UnderByggnation', ['current']], - 'Organisationsnamn', - 'Organisationsnummer', - 'ElNamn', - 'SenasteUppdaterat', - ]), - }); - - [ - ['completed', /* 0 */ 'uppförd'], - ['approved', /* 1 */ 'tillståndsansökan beviljad'], - ['amended', /* 2 */ 'ändringsansökan'], - ['rejected', /* 3 */ 'tillståndsansökan avslagen'], - ['appealed', /* 4 */ 'överklagad'], - ['applied', /* 5 */ 'tillståndsansökan inlämnad'], - ['consultation', /* 6 */ 'samråd inför tillståndsansökan'], - ['investigation', /* 7 */ 'inledande undersökningar'], - ['revoked', /* 8 */ 'inte aktuell eller återkallad'], - ] - .forEach(([k, title], idx) => LAYERS.vbk['offshore_' + k] = { - title: 'Havsbaserad vindkraft \u2013 ' + title, - fields: mapFields(idx, fieldMap, [ - 'Projektnamn', - 'OmrID', - 'Organisationsnamn', - 'Organisationsnummer', - 'Projektstatus', - 'Diarienummer', - ['AndringsansokanPagar', [1,2,4]], - 'geom_area', - ['SamradsunderlagInlamnat', (i) => i <= 6 || i === 8], - ['AnsokanInlamnat', (i) => i <= 5 || i === 8], - ['AnsokanAterkallad', [8]], - ['AnsokanBeviljad', [0,1,4,8]], - ['AnsokanAvslagen', [3,8]], - ['AnsokanOverklagad', [0,1,3,4,8]], - ['Natura2000_Ansokan', (i) => i !== 2], - ['Natura2000_Beslutdatum', (i) => i !== 2], - ['UnderByggnation', [1]], - ['PlaneratAntalVerkMin', (i) => i > 0], - ['PlaneratAntalVerkMax', (i) => i > 0], - ['PlaneradHojdMin', (i) => i > 0], - ['PlaneradHojdMax', (i) => i > 0], - ['PlaneradProduktionMin', (i) => i > 0], - ['PlaneradProduktionMax', (i) => i > 0], - ['PlaneradByggstart', (i) => i > 0], - ['Uppfort', [0,8]], - ['PlaneratDrift', (i) => i > 0], - ['BeviljatAntalVerk', [0,1,4,8]], - ['UppfortAntalVerk', [0,8]], - ['BeviljadMaxhojd', [0,1,4,8]], - ['InstalleradEffekt', [0]], - ['Calprod', [0]], - 'ElNamn', - 'SenasteUppdaterat', - ]), - }); - - Object.assign(fieldMap, mkFieldMap({ - VerkID: { desc: 'Verk-ID', classes: ['feature-objid'] }, - Status: 'Status', - Handlingstyp: 'Handlingstyp', - MB_Tillstand: 'Miljöbalken tillstånd tidsbegränsning', - Uppfort: 'Uppförandedatum',/* override previous def */ - Totalhojd: { desc: 'Totalhöjd', unit: 'm' }, - Navhojd: { desc: 'Navhöjd', unit: 'm' }, - Rotordiameter: { desc: 'Rotordiameter', unit: 'm' }, - Maxeffekt: { desc: 'Maxeffekt', unit: 'MW' }, - Fabrikat: 'Fabrikat', - Modell: 'Modell', - Placering: 'Placering', - })); - - [ - ['completed', /* 0 */ 'uppfört'], - ['approved', /* 1 */ 'beviljat'], - ['rejected', /* 2 */ 'avslagit/nekat'], - ['processed', /* 3 */ 'handlagt'], - ['dismounted', /* 4 */ 'nedmonterat'], - ['appealed', /* 5 */ 'överklagat'], - ['revoked', /* 6 */ 'inte aktuell eller återkallad'], - ] - .forEach(([k, title], idx) => LAYERS.vbk['station_' + k] = { - title: 'Landbaserad vindkraftverk \u2013 ' + title, - fields: mapFields(idx, fieldMap, [ - 'VerkID', - 'OmrID', - 'Projektnamn', - 'Status', - 'Handlingstyp', - ['Uppfort', [0,4,6]], - 'MB_Tillstand', - 'Totalhojd', - 'Navhojd', - 'Rotordiameter', - 'Maxeffekt', - 'Calprod', - 'Fabrikat', - 'Modell', - 'Organisationsnamn', - 'Organisationsnummer', - 'Placering', - 'ElNamn', - 'SenasteUppdaterat', - ]), - }); -})(); - - -LAYERS.avverk = {}; -(function() { - const zeroIsNull = (v) => v > 0 ? v : null; - const fieldMap = mkFieldMap({ - /* Documentation at - * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf - * and - * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf - */ - Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, - ArendeAr: 'Registeringsår', - Inkomdatum: 'Inkom datum', - Skogstyp: 'Skogstyp', - Avvdatum: 'Datum för avverkning', - KallaDatum: 'Ursprung för datum för avverkning', - AnmaldHa: { desc: 'Areal anmält', unit: 'ha' }, - NatforHa: { desc: 'Areal naturlig föryngring', unit: 'ha', fn: zeroIsNull }, - SkogsodlHa: { desc: 'Areal plantering', unit: 'ha', fn: zeroIsNull }, - AvvSasong: 'Avverkningssäsong', - Avverktyp: 'Avverkningstyp', - ArendeStatus: 'Ärendestatus', - AvvHa: { desc: 'Avverkad areal', unit: 'ha' }, - geom_area: { desc: 'Areal för ytan', fn: 'area' }, - }); - - LAYERS.avverk.utford = { - title: 'Utförd avverkning', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'ArendeAr', - 'Skogstyp', - 'AnmaldHa', - 'NatforHa', - 'Avverktyp', - 'Avvdatum', - 'KallaDatum', - 'geom_area', - ]), - }; - - LAYERS.avverk.anmald = { - title: 'Avverkningsanmälansområde', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'Inkomdatum', - 'ArendeAr', - 'AnmaldHa', - 'NatforHa', - 'SkogsodlHa', - 'AvvSasong', - 'ArendeStatus', - 'AvvHa', - ]), - }; -})(); - - -LAYERS.skydd = {}; -(function() { - const fieldMap = mkFieldMap({ - NVRID: { desc: 'NVR-ID', classes: ['feature-objid'] }, - FORSKRNAMN: 'Föreskriftsområde', - OBJEKTNAMN: 'Namn', - NAMN: 'Namn', - BESLSTAT: 'Beslutsstatus', - FORESKRTYP: 'Föreskriftstyp', - FORESKRIFT: 'Föreskriftssubtyp', - FRANDATUM: 'Från datum', - TILLDATUM: 'Till datum', - BESKRIVN: 'Beskrivning', - geom_area: { desc: 'Areal', fn: 'area' }, - - SKYDDSTYP: 'Skyddstyp', - BESLSTATUS: 'Beslutsstatus', - URSBESLDAT: 'Beslutsdatum (bildande)', - URSGALLDAT: 'Ursprungligt gällandedatum', - SENGALLDAT: 'Senaste gällandedatum', - FORVALTARE: 'Förvaltare', - IUCNKAT: 'IUCN-kategori', - DIARIENR: { desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, - LAGRUM: 'Lagrum', - BESLMYND: 'Beslutsmyndighet', - LAND_HA: { desc: 'Areal land', unit: 'ha' }, - VATTEN_HA: { desc: 'Areal vatten', unit: 'ha' }, - SKOG_HA: { desc: 'Skogsmarksareal', unit: 'ha' }, - - IKRAFTDATF: 'Ikraftträdandedatum föreskrifter', - TILLSYNSMH: 'Tillsynsmyndighet', - PROVNMHTIL: 'Prövningsmyndighet tillstånd', - PROVNMHDIS: 'Prövningsmyndighet dispens', - - NAME: 'Namn', - RAMSAR_ID: { desc: 'Ramsar-ID', classes: ['feature-objid'] }, - LEGAL_ACT: 'Rättsakt', - URSPR_BESL: 'Ursprungligt beslutsdatum', - SEN_BESLUT: 'Senaste beslutsdatum', - LINK: { desc: 'Länk', fn: formatLink }, - }); - - LAYERS.skydd.tilltradesforbud = { - title: 'Tillträdesförbud', - fields: mapFields(fieldMap, [ - 'NVRID', - 'FORSKRNAMN', - 'OBJEKTNAMN', - 'BESLSTAT', - 'FORESKRTYP', - 'FORESKRIFT', - 'FRANDATUM', - 'TILLDATUM', - 'BESKRIVN', - 'geom_area', - ]), - }; - - /* Nationella skyddsformer från Naturvårdsregistret */ - const isSurface = (k) => !/_punkt$/.test(k); - Object.entries({ - nationalpark: 'Nationalpark', - naturreservat: 'Naturreservat', - naturreservat_kommunalt: 'Kommunalt naturreservat', - naturvardsomrade: 'Naturvårdsområde', - djur_och_vaxtskyddsomrade: 'Djur- och växtskyddsområde', - kulturreservat: 'Kulturreservat', - vattenskyddsomrade: 'Vattenskyddsområden', - landskapsbildsskyddsomrade: 'Landskapsbildsskyddsområde', - ovrigt_biotopskyddsomrade: 'Biotopskydd utanför skogsmark', - naturminne_yta: 'Naturminne (yta)', - naturminne_punkt: 'Naturminne (punkt)', - interimistiskt_forbud: 'Interimistiskt förbud', - }) - .forEach(([k, title]) => LAYERS.skydd[k] = { - title: title, - fields: mapFields(k, fieldMap, [ - 'NVRID', - 'NAMN', - 'SKYDDSTYP', - 'BESLSTATUS', - 'URSBESLDAT', - ['URSGALLDAT', (k) => k !== 'vattenskyddsomrade'], - ['SENGALLDAT', (k) => k !== 'vattenskyddsomrade'], - ['FORVALTARE', (k) => k !== 'vattenskyddsomrade'], - ['IKRAFTDATF', (k) => k === 'vattenskyddsomrade'], - 'IUCNKAT', - 'DIARIENR', - 'LAGRUM', - 'BESLMYND', - ['TILLSYNSMH', (k) => k === 'vattenskyddsomrade'], - ['PROVNMHTIL', (k) => k === 'vattenskyddsomrade'], - ['PROVNMHDIS', (k) => k === 'vattenskyddsomrade'], - ['geom_area', isSurface], - ['LAND_HA', isSurface], - ['VATTEN_HA', isSurface], - ['SKOG_HA', isSurface], - ]), - }); - - /* Natura 2000-områden */ - (function() { - const fields = [ - { key: 'SITE_CODE', desc: 'Områdeskod', classes: ['feature-objid'] }, - { key: 'NAMN', desc: 'Namn' }, - { key: 'OMRADESTYP', desc: 'Områdestyp' }, - { key: 'UPPLAMNARE', desc: 'Uppgiftslämnare' }, - { key: 'SPA_DATUM', desc: 'SPA-datum' }, - { key: 'SCI_FORSL', desc: 'SCI-förslagsdatum' }, - { key: 'SCI_DATUM', desc: 'SCI-datum' }, - { key: 'SAC_DATUM', desc: 'SAC-datum' }, - fieldMap.geom_area, - { key: 'KVALITET', desc: 'Kvalitet' }, - { key: 'KARAKTAR', desc: 'Kännetecken för området' }, - { key: 'ARTER', desc: 'Arter' }, - { key: 'NATURTYPER', desc: 'Naturtyper' }, - { key: 'BEVPLAN', desc: 'Bevarandeplan', fn: formatLink }, - ]; - Object.entries({ - fageldirektivet: 'Fågeldirektivet (SPA)', - habitatdirektivet: 'Art- och habitatdirektivet (SCI)', - }) - .forEach(([k, title]) => LAYERS.skydd[k] = { title, fields }); - })(); - - /* Områden med internationell status */ - LAYERS.skydd.helcom = { - title: 'Marina skyddade områden (Helcom MPA)', - fields: mapFields(fieldMap, [ 'NAME', 'geom_area' ]), - }; - - LAYERS.skydd.ramsar = { - title: 'Ramsar-områden (Våtmarkskonventionen)', - fields: mapFields(fieldMap, [ - 'RAMSAR_ID', - 'SKYDDSTYP', - 'NAMN', - 'geom_area', - 'LAND_HA', - 'VATTEN_HA', - 'SKOG_HA', - 'URSPR_BESL', - 'SEN_BESLUT', - 'LEGAL_ACT', - 'LINK', - ]), - }; - - LAYERS.skydd.ospar = { - title: 'Marina skyddade områden (Ospar MPA)', - fields: [ - { key: 'ORIGIN', desc: 'Ursprung' }, - { key: 'NAMN_N2000', desc: 'N2000-namn' }, - { key: 'MPA_ID', desc: 'MPA-ID', classes: ['feature-objid'] }, - { key: 'MPA_NAMN', desc: 'MPA-namn' }, - { key: 'N2000_SITE', desc: 'N2000-ID', classes: ['feature-objid'] }, - fieldMap.geom_area, - ], - }; - - LAYERS.skydd.varldsarv = { - title: 'Världsarv med mycket höga naturvärden (Unesco)', - fields: mapFields(fieldMap, [ 'NAMN', 'geom_area' ]), - }; - - LAYERS.skydd.naturvardsavtal = { - title: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', - fields: [ - { key: 'ID', desc: 'ID', classes: ['feature-objid'] }, - { key: 'OBJNAMN', desc: 'Namn' }, - { key: 'FASTBET', desc: 'Fastighet', classes: ['feature-objid'] }, - { key: 'DATSTART', desc: 'Giltig från' }, - { key: 'DATSLUT', desc: 'Giltig till' }, - { key: 'DIARIENRNV', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, - { key: 'STATUS', desc: 'Satus' }, - fieldMap.geom_area, - ], - }; -})(); - -(function() { - const fieldMap = mkFieldMap({ - Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, - Biotyp: { desc: 'Biotopkategori' }, - Naturtyp: { desc: 'Skogstyp' }, - ArendeAr: { desc: 'Registeringsår' }, - geom_area: { desc: 'Areal', fn: 'area' }, - AreaProd: { desc: 'Skogsmarksareal', unit: 'ha' }, - Datbeslut: { desc: 'Beslutsdatum' }, - Url: { desc: 'Länk', fn: (v) => formatLink(v, 'Skogens Pärlor') }, - NvaTyp: 'Biotopkategori', - DatAvtal: 'Avtalsdatum', - Undertyp: 'Undertyp', - AvtalatDatum: 'Avtalat datum', - Objnamn: 'Objektnamn', - Datinv: 'Datum för fältinventering', - }); - - LAYERS.skydd.skogligt_biotopskyddsomrade = { - title: 'Biotopskydd i skogsmark', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'Biotyp', - 'Naturtyp', - 'ArendeAr', - 'geom_area', - 'AreaProd', - 'Datbeslut', - 'Url', - ]), - }; - LAYERS.skydd.naturvardsavtal_skogsstyrelsen = { - title: 'Naturvårdsavtal (Skogsstyrelsen)', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'ArendeAr', - 'NvaTyp', - 'Naturtyp', - 'DatAvtal', - 'geom_area', - 'AreaProd', - 'Url', - 'Undertyp', - ]), - }; - - LAYERS.skydd.atervatningsavtal = { - title: 'Återvätningsavtal', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'ArendeAr', - 'AvtalatDatum', - 'geom_area', - 'Url', - ]), - }; - - LAYERS.nv = {}; - Object.assign(fieldMap, mkFieldMap(Object.fromEntries( - [1,2,3].map((i) => [`Biotop${i}`, `Biotoptyp #${i}`]).concat( - [1,2,3,4,5,6,7,8].map((i) => [`Beskrivn${i}`, `Nyckelord #${i} som beskriver objektet`]) - )))); - LAYERS.nv.naturvarde_sks = { - title: 'Objekt med naturvärden (Skogsstyrelsen)', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'Objnamn', - 'Datinv', - 'Biotop1', 'Biotop2', 'Biotop3', - 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', - 'geom_area', - 'Url', - ]), - }; - LAYERS.nv.nyckelbiotop = { - title: 'Nyckelbiotop (Skogsstyrelsen)', - fields: mapFields(fieldMap, [ - 'Beteckn', - 'Objnamn', - 'Datinv', - 'Biotop1', 'Biotop2', 'Biotop3', - 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', 'Beskrivn4', 'Beskrivn5', 'Beskrivn6', 'Beskrivn7', 'Beskrivn8', - 'geom_area', - 'Url', - ]), - }; - LAYERS.nv.nyckelbiotop_storskogsbruk = { - title: 'Nyckelbiotop (storskogsbruket)', - fields: [ - { key: 'Org', desc: 'Uppgifter lämnade av' }, - { key: 'InkomDatum', desc: 'Inkom datum' }, - fieldMap.geom_area, - fieldMap.Url, - ], - }; - - LAYERS.nv.sumpskog = { - title: 'Sumpskog', - fields: [ - { key: 'Namn', desc: 'Objektnamn' }, - { key: 'Tradtext', desc: 'Skogstyp' }, - { key: 'Hydrtext', desc: 'Hydrologisk typ' }, - { key: 'Delklass', desc: 'Klass på delobjektet' }, - { key: 'Klassu', desc: 'Klass på objektet' }, - { key: 'Lovandel', desc: 'Andel löv' }, - { key: 'Andelva', desc: 'Andel öppet vatten' }, - { key: 'Krontakn', desc: 'Krontäckning' }, - { key: 'Huggklas', desc: 'Huggningsklass' }, - { key: 'Ingrepp', desc: 'Ingrepp på delobjekt (max 4)' }, - { key: 'Ingrpavv', desc: 'Grad av påverkan på delobjekt (max 4)' }, - { key: 'Objnyck', desc: 'Nyckelord på objektnivå' }, - { key: 'Delnyck', desc: 'Nyckelord på delobjektsnivå' }, - { key: 'Flygar', desc: 'Flygbildsår' }, - { key: 'Faltdat', desc: 'Datum för fältbesök' }, - { key: 'Invtekn', desc: 'Inventeringsteknik' }, - { key: 'Invdat', desc: 'Inventeringdatum' }, - { key: 'Ansvmynd', desc: 'Ansvarig myndighet' }, - fieldMap.geom_area, - fieldMap.Url, - ], - }; -})(); - -LAYERS.nv.pagaende_naturreservatsbildning = { - title: 'Pågående naturreservatsbildning', - fields: [ - { key: 'NAMN', desc: 'Objektnamn' }, - /* XXX unclear what "GRANSJUST" means, just a guess */ - { key: 'GRANSJUST', desc: 'Senast justerat' }, - { key: 'geom_area', desc: 'Areal', fn: 'area' }, - ], -}; - -LAYERS.nv.snus = { - title: 'Skyddsvärd statlig skog', - fields: [ - { key: 'NAMN', desc: 'Objektnamn' }, - { key: 'AR', desc: 'År' }, - { key: 'NATURGEOGR', desc: 'Naturgeografisk region', classes: ['feature-objid'] }, - { key: 'OBJEKTKATE', desc: 'Objektskategori', classes: ['feature-objid'] }, - { key: 'MARKAGARE', desc: 'Markägare' }, - { key: 'VARDEKARNA', desc: 'Areal värdekärna', unit: 'ha' }, - { key: 'UTV_MARK', desc: 'Areal utvecklingsmark', unit: 'ha' }, - { key: 'TOTAL_AREA', desc: 'Totalareal', unit: 'ha' }, - { key: 'LAND', desc: 'Areal land', unit: 'ha' }, - { key: 'VATTEN', desc: 'Areal vatten', unit: 'ha' }, - { key: 'PROD_SKOG', desc: 'Areal produktiv skogsmark', unit: 'ha' }, - { key: 'SKOG_O_FJG', desc: 'Areal produktiv skogsmark ovanför fjällnära gräns', unit: 'ha' }, - { key: 'SKOG_N_FJG', desc: 'Areal produktiv skogsmark nedanför fjällnära gräns', unit: 'ha' }, - { key: 'SKYDDSZON', desc: 'Areal skyddszon', unit: 'ha' }, - { key: 'ARRO_MARK', desc: 'Areal arronderingsmark', unit: 'ha' }, - { key: 'KRITERIER', desc: 'Kriterier för urval' }, - { key: 'BESKRIVN', desc: 'Beskrivning av området' }, - { key: 'LST_BEDOMN', desc: 'Länsstyrelsens bedömning' }, - { key: 'KALLOR', desc: 'Källor' }, - ], -}; - -LAYERS.ri = {}; -(function() { - const fieldMap = mkFieldMap({ - NAMN: 'Namn', - SKYDD: 'Skydd', - AMNESOMRAD: 'Ämnesområde', - AMNESOMR: 'Ämnesområde', - OMRADESNR: { desc: 'Områdesnummer', classes: ['feature-objid'] }, - BESKRIVNIN: { desc: 'Beskrivning', fn: formatLink }, - LANK_VARDE: { desc: 'Länk värdebeskrivning', fn: formatLink }, - LAGRUM: 'Lagrum', - BESLUTSDAT: 'Beslutsdatum', - BESLDATUM: 'Beslutsdatum', - ARENDENR: { desc: 'Ärendenummer', classes: ['feature-attr-dnr'] }, - LANK_BESLU: { desc: 'Länk beslut', fn: formatLink }, - AKTIVITET: 'Aktivitet', - NATURTYP: 'Naturtyp', - ORGINALID: { desc: 'Original-ID', classes: ['feature-objid'] }, - RIKSID: { desc: 'Riks-ID', classes: ['feature-objid'] }, - geom_area: { desc: 'Areal', fn: 'area' }, - AREA_LAND_: { desc: 'Areal land', unit: 'ha' }, - AREA_VATTE: { desc: 'Areal vatten', unit: 'ha' }, - }); - LAYERS.ri.naturvard = { - title: 'Riksintresse naturvård', - fields: mapFields(fieldMap, [ - 'NAMN', - 'SKYDD', - 'AMNESOMRAD', - 'BESKRIVNIN', - 'LAGRUM', - 'BESLUTSDAT', - 'ORGINALID', - 'RIKSID', - 'geom_area', - ]), - }; - LAYERS.ri.friluftsliv = { - title: 'Riksintresse friluftsliv', - fields: mapFields(fieldMap, [ - 'NAMN', - 'SKYDD', - 'AMNESOMR', - 'OMRADESNR', - 'LANK_VARDE', - 'LAGRUM', - 'BESLDATUM', - 'ARENDENR', - 'LANK_BESLU', - 'AKTIVITET', - 'NATURTYP', - 'geom_area', - 'AREA_LAND_', - 'AREA_VATTE', - ]), - }; - - Object.assign(fieldMap, mkFieldMap({ - METODBESKR: 'Metodbeskrivning', - TILLKDATUM: 'Tillkomstdatum', - REVDATUM: 'Revisionsdatum', - ANM: 'Anmärkning', - OBJEKTLANK: { desc: 'Objektlänk', fn: formatLink }, - REFERENS: 'Referens', - OBJTYP: 'Objekttyp', - ORIGINALID: fieldMap.ORGINALID, - DIG_SKALA: { desc: 'Digitaliseringsskala', fn: (v) => v > 0 ? v : null }, - })); - [ - ['rorligt_friluftsliv', /* 0 */ 'rörligt friluftsliv (MB 4 kap 1§ och 2§)'], - ['obruten_kust', /* 1 */ 'obruten kust (MB 4 kap 3§)'], - ['obrutet_fjall', /* 2 */ 'obrutet fjäll (MB 4 kap 5§)'], - ['skyddade_vattendrag', /* 3 */ 'skyddade vattendrag (MB 4 kap 6§)'], - ] - .forEach(([k, title], idx) => LAYERS.ri[k] = { - title: 'Riksintresse ' + title, - fields: mapFields(idx, fieldMap, [ - 'NAMN', - 'BESKRIVNIN', - 'METODBESKR', - 'TILLKDATUM', - 'REVDATUM', - ['OBJTYP', [1]], - ['ANM', [0,1,3]], - ['DIG_SKALA', [3]], - 'OBJEKTLANK', - 'geom_area', - 'ORIGINALID', - 'REFERENS', - ]), - }); -})(); - - -LAYERS.ren = { - betesomrade: { - title: 'Samebyarnas betesområde', - fields: [ - { key: 'NAMN', desc: 'Sameby' }, - { key: 'SAMEBY_TYP', desc: 'Samebys typ' }, - { key: 'SIGNATUR', desc: 'Signatur' }, - { key: 'AKTUALITET', desc: 'Aktualitet' }, - { key: 'geom_area', desc: 'Areal', fn: 'area' }, - ], - }, - flyttled: { - title: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', - fields: [ - { key: 'LED_ID', desc: 'Led-ID', classes: ['feature-objid'], fn: (v) => v > 0 ? v : null }, - { key: 'SAMEBY1', desc: 'Sameby #1' }, - { key: 'SAMEBY2', desc: 'Sameby #2' }, - { key: 'SAMEBY3', desc: 'Sameby #3' }, - { key: 'BESKRIVNIN', desc: 'Beskrivning' }, - { key: 'ARSTID', desc: 'Årstid' }, - { key: 'RIKSINTR', desc: 'Riksintresse' }, - { key: 'FAST_LED', desc: 'Fast led' }, - { key: 'AKTUALITET', desc: 'Aktualitet' }, - { key: 'SIGNATUR', desc: 'Signatur' }, - { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, - ], - }, - riks_ren: { - title: 'Riksintresse rennäring', - fields: [ - { key: 'LAGRUM', desc: 'Lagrum' }, - { key: 'AKTUALITET', desc: 'Aktualitet' }, - { key: 'SIGNATUR', desc: 'Signatur' }, - { key: 'geom_area', desc: 'Areal', fn: 'area' }, - ], - }, - omr_riks: { - title: '(Kärn)områden av riksintresse rennäring', - fields: [ - { key: 'OMR_NR', desc: 'Områdes-ID', classes: ['feature-objid'] }, - { key: 'LANK', desc: 'Länk' }, - { key: 'ARET_RUNT', desc: 'Årets runt' }, - { key: 'SAMEBY', desc: 'Sameby' }, - { key: 'ANSVARIG', desc: 'Ansvarig' }, - { key: 'AKTUALITET', desc: 'Aktualitet' }, - { key: 'SIGNATUR', desc: 'Signatur' }, - { key: 'geom_area', desc: 'Areal', fn: 'area' }, - ], - }, -}; - - - -/* format value to HTML */ -const formatValue = function(value, options) { - let unit = options?.unit; - if (options?.fn == null) { - /* no-op */ - } else if (typeof options.fn === 'function') { - value = options.fn(value); - } else if (options.fn === 'length' && typeof value === 'number' && unit == null) { - if (value < 1000) { - unit = 'm'; - } else { - value /= 1000; - value = Math.round(value*100) / 100; - unit = 'km'; - } - } else if (options.fn === 'area' && typeof value === 'number' && unit == null) { - if (value < 10000) { - unit = 'm²'; - } else if (value < 10000 * 10000) { - value /= 10000; - unit = 'ha'; - } else { - value /= 1000000; - unit = 'km²'; - } - value = Math.round(value*100) / 100; - } - if (value == null) { - return null; - } - if (value instanceof HTMLElement) { - return value; - } - switch (typeof value) { - case 'boolean': - return document.createTextNode(value ? 'Ja' : 'Nej'); - case 'string': - return document.createTextNode(value); - case 'number': - if (unit != null) { - return document.createTextNode(value.toLocaleString('sv-SE') + '\u202F' + unit); - } - return document.createTextNode(value.toString()); - default: - return null; - } -}; - -/* turn the properties into a fine <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 with the give map and HTML element */ -let popupOverlay = null; -(function() { - popupOverlay = new Overlay({ - stopEvent: true, - element: document.getElementById('popup'), - }); - map.addOverlay(popupOverlay); -})(); - -let featureOverlayLayer = null; -let overlayAttributes = [], - overlayAttrIdx = 0, - mapSources = {}; -/* Clear the highlighted feature list and make the overlay layer invisible */ -const disposeFeatureOverlay = function() { - if (featureOverlayLayer?.getVisible?.()) { - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - } - /* clear the overlay list */ - overlayAttributes = []; - overlayAttrIdx = 0; - mapSources = {}; -} - -let popover = null; -/* Clear overlay layer and dispose popover */ -export const disposePopover = function() { - disposeFeatureOverlay(); - if (popover?.tip != null) { - popover.dispose(); - } -}; - -/* Initialize popover on the given map */ -(function() { - featureOverlayLayer = new VectorTileLayer({ - zIndex: 65535, - declutter: false, - visible: false, - renderMode: 'vector', - style: null, - map: map, - }); - - const header = document.createElement('div'); - header.classList.add('d-flex'); - - const headerGrabbingArea = document.createElement('div'); - headerGrabbingArea.classList.add('flex-grow-1', 'grabbing-area', 'pe-2', 'me-2'); - header.appendChild(headerGrabbingArea); - - const pageNode = document.createElement('h6'); - headerGrabbingArea.appendChild(pageNode); - - headerGrabbingArea.onmousedown = function(event) { - /* move the popover around */ - if (event.button != 0) { - return; - } - const popoverTip = popover.tip; - if (popoverTip.classList.contains('popover-maximized')) { - return; - } - headerGrabbingArea.classList.add('grabbing-area-grabbed'); - - if (!popoverTip.classList.contains('popover-detached')) { - /* detach popover tip */ - popoverTip.classList.add('popover-detached'); - const rect = popoverTip.getBoundingClientRect(); - const style = popoverTip.style; - style.display = 'none'; /* avoid reflows between the following assignments */ - style.position = 'absolute'; - style.transform = ''; - style.inset = `${rect.top}px auto auto ${rect.left}px`; - style.display = ''; - } - - let clientX = event.clientX, clientY = event.clientY; - document.onmousemove = function(event) { - const offsetX = clientX - event.clientX; - const offsetY = clientY - event.clientY; - clientX = event.clientX; - clientY = event.clientY; - popoverTip.style.top = (popoverTip.offsetTop - offsetY).toString() + 'px'; - popoverTip.style.left = (popoverTip.offsetLeft - offsetX).toString() + 'px'; - }; - - document.onmouseup = function(event) { - /* done moving around */ - if (event.button != 0) { - return; - } - headerGrabbingArea.classList.remove('grabbing-area-grabbed'); - document.onmousemove = null; - document.onmouseup = null; - }; - }; - - /* current number page and total page count */ - const pageNum = document.createElement('span'); - const pageCount = document.createElement('span'); - pageNode.appendChild(document.createTextNode('Träff ')); - pageNode.appendChild(pageNum); - pageNode.appendChild(document.createTextNode(' av ')); - pageNode.appendChild(pageCount); - - /* highlight a feature */ - const featureOverlayStyle = new Style({ - stroke: new Stroke({ - color: 'rgba(0, 255, 255, .8)', - width: 3, - }), - }); - const highlightFeature = function(layer_group, layer, id) { - const source = mapSources[layer_group]; - if (source == null) { - return; - } - if (featureOverlayLayer.getSource() !== source) { - /* console.log('Updating source for feature overlay layer'); */ - featureOverlayLayer.setVisible(false); - featureOverlayLayer.setSource(source); - } - featureOverlayLayer.setStyle(function(feature) { - if (feature.getId() === id && feature.getProperties().layer === layer) { - return featureOverlayStyle; - } - }); - featureOverlayLayer.setVisible(true); - featureOverlayLayer.changed(); - }; - /* highlight the feature at index overlayAttrIdx within the CGI reply list */ - const refreshPopover = function() { - const attr = overlayAttributes[overlayAttrIdx]; - highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); - - pageNum.innerHTML = (overlayAttrIdx + 1).toString(); - const content = formatFeaturePropertiesToHTML(attr); - popover.tip.getElementsByClassName('popover-body')[0].replaceChildren(content); - }; - /* go back/forward in the overlayAttributes list */ - const onClickPageChange = function(event, offset) { - const btn = event.target; - if (btn.classList.contains('disabled') || popover?.tip == null) { - return; - } - if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) { - return; /* out of range */ - } - - overlayAttrIdx += offset; - if (overlayAttrIdx < 1) { - btnPrev.classList.add('disabled'); - } else { - btnPrev.classList.remove('disabled'); - } - if (overlayAttrIdx < overlayAttributes.length - 1) { - btnNext.classList.remove('disabled'); - } else { - btnNext.classList.add('disabled'); - } - - refreshPopover(); - setTimeout(function() { btn.blur() }, 100); - }; - - /* control buttons */ - const btnPrev = document.createElement('button'); - btnPrev.classList.add('popover-button', 'popover-button-prev'); - btnPrev.setAttribute('type', 'button'); - btnPrev.title = 'Föregående träff'; - btnPrev.setAttribute('aria-label', btnPrev.title); - btnPrev.onclick = function(event) { - return onClickPageChange(event, -1); - }; - - const btnNext = document.createElement('button'); - btnNext.classList.add('popover-button', 'popover-button-next'); - btnNext.setAttribute('type', 'button'); - btnNext.title = 'Nästa träff'; - btnNext.setAttribute('aria-label', btnNext.title); - btnNext.onclick = function(event) { - return onClickPageChange(event, +1); - }; - - const btnExpand = document.createElement('button'); - btnExpand.classList.add('popover-button', 'popover-button-expand'); - btnExpand.setAttribute('type', 'button'); - const btnExpandTitle = 'Förstora'; - const btnExpandTitle2 = 'Förminska'; - btnExpand.setAttribute('aria-label', btnExpand.title); - btnExpand.onclick = function() { /* maximize or reduce the popover */ - if (popover?.tip == null) { - return; - } - if (!popover.tip.classList.contains('popover-maximized')) { - popover.tip.classList.add('popover-maximized'); - btnExpand.classList.replace('popover-button-expand', 'popover-button-reduce'); - btnExpand.title = btnExpandTitle2; - btnExpand.setAttribute('aria-label', btnExpand.title); - } else { - popover.tip.classList.remove('popover-maximized'); - btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); - btnExpand.title = btnExpandTitle; - btnExpand.setAttribute('aria-label', btnExpand.title); - } - setTimeout(function() { btnExpand.blur() }, 100); - }; - - const btnClose = document.createElement('button'); - btnClose.classList.add('popover-button', 'popover-button-close'); - btnClose.setAttribute('type', 'button'); - btnClose.title = 'Stäng'; - btnClose.setAttribute('aria-label', btnClose.title); - btnClose.onclick = disposePopover; - - header.appendChild(btnPrev); - header.appendChild(btnNext); - header.appendChild(btnExpand); - header.appendChild(btnClose); - - const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; - map.on('singleclick', function(event) { - disposeFeatureOverlay(); - - /* dispose any pre-existing popover if not in detached mode */ - popover = Popover.getInstance(popupOverlay.element); - if (popover?.tip != null && !popover.tip.classList.contains('popover-detached')) { - popover.dispose(); - } - - const size = event.map.getSize(); - if (size[0] < 576 || size[1] < 576) { - return; /* skip popover if the map is too small */ - } - - /* unclear how many feature we'll find, don't render prev/next buttons for now */ - pageNode.classList.add('d-none'); - btnPrev.classList.add('d-none', 'disabled'); - btnNext.classList.add('d-none', 'disabled'); - - /* never start in maximized mode */ - if (popover?.tip != null) { - popover.tip.classList.remove('popover-maximized'); - } - btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); - btnExpand.title = btnExpandTitle; - btnExpand.setAttribute('aria-label', btnExpand.title); - - const fetch_body = []; - event.map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { - const layerGroup = layer.get('layerGroup'); - const layerName = feature.getProperties().layer; - mapSources[layerGroup] ??= layer.getSource(); - const def = layerName != null ? LAYERS[layerGroup][layerName] : null; - if (def?.fields == null) { - /* skip layers which didn't opt-in for popover */ - return false; - } - if (fetch_body.length === 0) { - /* first feature in the list, mark cursor and detached popover as in-progress */ - document.body.classList.add('inprogress'); - popover?.tip?.classList?.add?.('inprogress'); - } - fetch_body.push({ - layer_group: layerGroup, - layer: layerName, - fid: feature.getId() ?? -1, - }); - if (fetch_body.length >= 100) { - return true; /* enough matches already, stop detection here */ - } - }, { - hitTolerance: 5, - checkWrapped: false, - layerFilter: (lyr) => lyr.get('layerGroup') != null, - }); - - if (fetch_body.length === 0) { - /* no feature at pixel (or only within layers which didn't opt-in for popover) */ - if (popover?.tip != null) { - /* dispose pre-detached popover */ - popover.dispose(); - } - return; - } - - fetch('/q', { - method: 'POST', - body: JSON.stringify(fetch_body), - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - }) - .then(function(resp) { - if (resp.status === 200) { - return resp.json(); - } else { - throw new Error(`${resp.url} [${resp.status}]`); - } - }) - .then(function(data) { - /* the data is received from the CGI in the order it was sent */ - /* TODO optimizations on the CGI would break the above assumption, so the - * decoded JSON response would need to be reordered to match fetch_body */ - overlayAttributes = data; - if (overlayAttributes.length === 0) { - /* couldn't fetch any attribute for feature(s) at pixel */ - if (popover?.tip != null) { - /* dispose pre-detached popover */ - popover.dispose(); - } - return; - } - - pageCount.innerHTML = overlayAttributes.length.toString(); - if (overlayAttributes.length >= 2) { - /* render prev/pre buttons */ - btnNext.classList.remove('d-none', 'disabled'); - btnPrev.classList.remove('d-none'); - pageNode.classList.remove('d-none'); - } - if (popover?.tip == null) { - /* create a new popover (we're not already showing one in detached mode) */ - pageNum.innerHTML = (overlayAttrIdx + 1).toString(); - popupOverlay.setPosition(event.coordinate); - - const attr = overlayAttributes[0]; - highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); - popover = new Popover(popupOverlay.element, { - template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div>' + - '<div class="popover-header"></div><div class="popover-body"></div></div>', - title: header, - content: formatFeaturePropertiesToHTML(attr), - html: true, - placement: 'right', - fallbackPlacements: ['right', 'left', 'bottom', 'top'], - container: container0, - }); - popover.show(); - } - else if (popover.tip.classList.contains('popover-detached')) { - /* update existing detached mode popover */ - refreshPopover(); - popover.tip.classList.remove('inprogress'); - } - }) - .catch(function(e) { - console.log(e); - }) - .finally(function() { - /* remove in-progress marking on the cursor */ - document.body.classList.remove('inprogress'); - }); - }); -})(); diff --git a/src/style.css b/style.css index cf7fc86..cf7fc86 100644 --- a/src/style.css +++ b/style.css |