diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | eslint.config.mjs | 27 | ||||
| -rw-r--r-- | example/example.html (renamed from example.html) | 4 | ||||
| -rw-r--r-- | example/style.css (renamed from style2.css) | 0 | ||||
| -rw-r--r-- | index.html | 212 | ||||
| -rw-r--r-- | main.js | 8019 | ||||
| -rw-r--r-- | package-lock.json | 1712 | ||||
| -rw-r--r-- | package.json | 12 | ||||
| -rw-r--r-- | style.css | 367 | ||||
| -rw-r--r-- | vite.config.js | 10 |
10 files changed, 7107 insertions, 3258 deletions
@@ -12,7 +12,7 @@ to preview the production site. # Author -© 2024 [Guilhem Moulin](https://guilhem.se). +© 2024-2025 [Guilhem Moulin](https://guilhem.se). # Licensing diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..72b17bd --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,27 @@ +import js from "@eslint/js"; +import globals from "globals"; +import css from "@eslint/css"; +import { defineConfig } from "eslint/config"; + + +export default defineConfig([ + { + files: ["src/*.js", "./*.js"], + plugins: { js }, + extends: ["js/recommended"], + languageOptions: { + globals: globals.browser, + }, + }, + { + files: ["src/*.css", "./*.css"], + plugins: { css }, + language: "css/css", + extends: ["css/recommended"], + rules: { + "css/no-important": "off", + /* too many false positives as of @eslint/css 0.9.0 */ + "css/no-invalid-properties": "off", + }, + }, +]); diff --git a/example.html b/example/example.html index 5dd0e01..d003598 100644 --- a/example.html +++ b/example/example.html @@ -4,7 +4,7 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hemsida</title> - <link rel="stylesheet" href="/style2.css"> + <link rel="stylesheet" href="/example/style.css"> </head> <body> <div id="wrapper"> @@ -15,7 +15,7 @@ man välja lager, ladda ner kartan som PNG-fil, och få information om de olika föremålen.</p> </div> - <iframe id="map" src="/#z=0&basemap=topowebb_nedtonad&layers=svk_lines+svk_pylons+svk_stations+svk_planned+mrr_appr_ec+mrr_appl_ec+mrr_appr_ogd+mrr_appl_ogd+mrr_appr_met+mrr_appl_met+mrr_appr_dl+mrr_appr_pc+vbk_area_current+vbk_station_completed+vbk_station_processed+vbk_station_approved+gigafactories" title="Webbkarta"></iframe> + <iframe id="map" src="/" title="Webbkarta"></iframe> </div> </body> </html> diff --git a/style2.css b/example/style.css index dfeaee5..dfeaee5 100644 --- a/style2.css +++ b/example/style.css @@ -11,70 +11,188 @@ <div id="map-control-container" aria-hidden="true"> <div id="layer-selection-panel"></div> <div id="map-legend-panel"></div> + <div id="measure-panel"></div> <div id="map-menu"></div> </div> <div id="popup"></div> - <div class="modal" id="modal-info" tabindex="-1" aria-hidden="true"> + <div class="modal" id="info-modal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg"> <div class="modal-content"> - <div class="modal-header py-2"> - <h5 class="my-0">Källor och licensinformation</h5> + <div class="modal-header"> + <div class="h5 m-0">Källor och licensinformation</div> <button type="button" class="btn-close" data-bs-dismiss="modal" title="Stäng" aria-label="Stäng"></button> </div> - <div class="modal-body pt-2"> - <ul class="mb-2"> - <li><i>Transmissionsnät för el</i> från - <a href="https://svk.se" target="_blank">Svenska Kraftnät (SvK)</a>. + <div id="info-body" class="modal-body"> + <div id="info-accordion" class="accordion accordion-flush"></div> + <ul class="list-group list-group-flush"> + <li class="list-group-item"> + <h6>Bakgrund kartor</h6> + <p>© <a href="https://lantmateriet.se" target="_blank">Lantmäteriet</a></p> + <p>Licensvillkor: <a href="https://www.lantmateriet.se/sv/geodata/vara-produkter/oppna-data/#anchor-1" target="_blank">CC0 1.0 Universiell</a></p> </li> - <li><i>Dammregistret</i> från - <a href="https://smhi.se" target="_blank">Sveriges meteorologiska och hydrologiska institut (SMHI)</a>, - CC-BY-4.0 (<a href="https://www.smhi.se/data/oppna-data/villkor-for-anvandning-1.30622" target="_blank">öppna data</a>). + <li class="list-group-item"> + <h6>Webbkartan</h6> + <p>© 2024-2025 Guilhem Moulin</p> + <p>Licensvillkor: <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">AGPLv3+</a></p> + <p class="small text-muted"><i class="bi bi-file-earmark-code"></i> + <a href="https://git.guilhem.org/KlimatanalysNorr/webmap" target="_blank">Källkod <i class="bi bi-box-arrow-up-right"></i></a></p> </li> - <li><i>Mineralrättsregistret</i> från - <a href="https://www.sgu.se/bergsstaten/" target="_blank">Bergsstaten</a>, - CC0 (<a href="https://creativecommons.org/publicdomain/zero/1.0/deed.sv" target="_blank">öppna data</a>). + <li class="list-group-item"> + <h6>Backend verktyg</h6> + <p>© 2024-2025 Guilhem Moulin</p> + <p>Licensvillkor: <a href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">GPLv3+</a> och + (endast CGI) <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">AGPLv3+</a></p> + <p class="small text-muted"><i class="bi bi-file-earmark-code"></i> + <a href="https://git.guilhem.org/KlimatanalysNorr/tools" target="_blank">Källkod <i class="bi bi-box-arrow-up-right"></i></a></p> </li> - <li><i>Vindbruk</i> från - <a href="https://www.energimyndigheten.se/energisystem-och-analys/elproduktion/vindkraft/vindbrukskollen/" target="_blank">Länsstyrelserna och Energimyndigheten</a>, - CC0 (<a href="https://ext-geodatakatalog-forv.lansstyrelsen.se/GeodataKatalogen/codelist/metadata/anvandningsrestriktioner.xml#CC01.0" target="_blank">öppna data</a>). - </li> - <li><i>Skogsbruk</i>, <i>Skogliga biotopskyddsområden</i> och <i>Naturvårdsavtal</i> från - <a href="https://skogsstyrelsen.se" target="_blank">Skogsstyrelsen</a>, - CC0 (<a href="https://www.skogsstyrelsen.se/sjalvservice/karttjanster/geodatatjanster/villkor-for-nyttjande-av-skogsstyrelsens-kartdatabaser/" target="_blank">öppna data</a>). + </ul> + <p class="small text-muted info-credits">Webbkartan är utvecklad av + <a data-mailto-b64="Z3VpbGhlbQ __AT__ ZnJpcG9zdA __DOT__ b3Jn" href="#" target="_blank" class="email-address-b64">Guilhem Moulin</a> + på uppdrag av + <a href="https://www.klimatanalysnorr.se" target="_blank">Klimatanalys Norr projektet</a>.</p> + </div> + </div> + </div> + </div> + <div class="modal" id="help-modal" tabindex="-1" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="btn-close" data-bs-dismiss="modal" title="Stäng" aria-label="Stäng"></button> + </div> + <div id="help-body" class="modal-body"> + <h3>Navigering</h3> + <p>Kartan är interaktiv och kontrolleras med musen. Tryck ner på + musens första knapp och dra kartan för att byta koordinater.</p> + <p>Zoomnivån kan styras med musens rullhjul, eller alternativt med + knapparna och skjutreglaget längst upp till vänster. Den aktuella + skalan visas längst ner till vänster på kartan.</p> + <p>Både koordinater och zoomnivå sparas i URL:en, så om du delar + URL:en med någon annan eller sparar den i din webbläsares bokmärken + kommer senare besök att landa på samma plats samt zoomnivå på + kartan.</p> + <p>Klickar du på ett objekt på kartan – exempelvis ett + undersökningstillstånd eller ett naturreservat (läs mer om lagerval + nedan) – så visar det valda objektet med en cyanfärgad kantlinje + samt dyker ett fönster upp med information om just det objektet. + Både informationen och geometrier kommer från ansvarig myndighet. + I fall det finns flera objekt i närheten av just platsen du klickade + på, så antalet träffar visas högst upp på informationsfönstret + (exempelvis ”<span class="fst-italic text-muted">Träff 1 av 3</span>”) och du + kan välja mellan dem genom att klicka på pilarna + <span class="popover-header"> + <button class="popover-button popover-button-prev text-muted help-button"></button>/<button class="popover-button popover-button-next text-muted help-button"></button> + </span> bredvid. + Informationsfönstret kan flyttas runt om det står i vägen, samt att + det förstoras om rutan är för liten.</p> + + <h3>Funktionsknappar</h3> + <p>Det finns ett antal funktionsknappar längst upp till + höger på kartan. Om du låter muspekaren vila på varje + knapp, dyker en hjälptext upp.</p> + + <ol id='help-describe-functions'> + <li data-for-button='layer-selection-button'> + <p>Här kan du välja mellan olika lager att ha på kartan. + Det finns massor med lager, och de grupperas ihop så att + det är enkelt att välja hela gruppen på en gång. Klickar + du på pilen + <span class="accordion"><span class="accordion-button collapsed help-button text-muted"></span></span> + kan du istället välja ett specifikt lager.</p> + <p>Längst ned finns knappar där du kan välja att se + administrativa gränser, samt välja mellan nedtonad eller färgad + bakgrundskarta.</p> + <p>Lager, precis som koordinater och zoomnivå, sparas i URL:en. + Så om du delar URL:en med någon annan eller sparar den i din + webbläsares bokmärken kommer senare besök att landa på samma vy + (eventuellt med uppdaterat underlag).</p> </li> - <li><i>Naturvårdsregistret</i> och <i>Naturvårdsavtal</i> från - <a href="https://www.naturvardsverket.se/" target="_blank">Naturvårdsverket</a>, - CC0 (<a href="https://geodata.naturvardsverket.se/nedladdning/naturvardsregistret/Naturvardsregistret_beskrivning_av_oppna_data.pdf" target="_blank">öppna data</a>). + + <li data-for-button='map-legend-button'> + <p>Här ser du symboler för alla lager som valts ut.</p> </li> - <li><i>Riksintresse naturvård</i> och <i>frilufsliv</i> från - <a href="https://www.naturvardsverket.se/" target="_blank">Naturvårdsverket</a> - och - <a href="https://www.lansstyrelsen.se/" target="_blank">Länsstyrelsen</a>, - CC-BY-4.0 (<a href="https://ext-geodatakatalog-forv.lansstyrelsen.se/GeodataKatalogen/codelist/metadata/anvandningsrestriktioner.xml#CCby4.0" target="_blank">öppna data</a>). + + <li data-for-button='measure-button'> + <p>Med det här verktyget kan du rita direkt på kartan och mäta + distanser eller ytor. Till exempel kan du lätt mäta avståndet + mellan en viss exploatering och ett skyddat område eller + kommungräns.</p> </li> - <li><i>Riksintresse Rennäringen</i> skikt från - <a href="https://sametinget.se" target="_blank">Sametinget</a>, - CC-BY-4.0 (<a href="https://ext-geodatakatalog-forv.lansstyrelsen.se/GeodataKatalogen/codelist/metadata/anvandningsrestriktioner.xml#CCby4.0" target="_blank">öppna data</a>). - <i>Samebyarnas betesområden</i> och <i>flyttled</i> från - © <a href="https://sametinget.se" target="_blank">Sametingets</a> - Rennäringens markanvändningsdatabas (IRENMARK). + + <li data-for-button='age-filter-button'> + <p>Med det här verktyget kan du filtrera bort gamla + exploateringar, vilket underlättar övervakning. Det är bra att + kunna fokusera på nya exploateringen, då det annars snabbt kan + kännas överväldigande.</p> + + <p>Om filtret är aktivt så blir knappen svart. Filterparametrar + sparas i URL:en, så en övervaknings-URL till ett specifik område + kan enkelt delas med andra eller bokmärkas.</p> </li> - <li>Bakgrund kartor från - © <a href="https://lantmateriet.se" target="_blank">Lantmäteriet</a>, CC0 - (<a href="https://www.lantmateriet.se/sv/geodata/vara-produkter/oppna-data/#anchor-1" target="_blank">öppna data</a>). + + <li data-for-button='fullscreen-toggle'> + <p>Genom att klicka på knappen kan du aktivera eller inaktivera + helskärmsläget.</p> + <li> + + <li data-for-button='export-to-image'> + <p>Genom att klicka på knappen kan du ladda ner den aktuella + kartvyn som en bild. Bilden kan då användas i en rapport eller + ett yttrande.</p> </li> - <li>Webbkartan: - © <a href="https://guilhem.se" target="_blank">Guilhem Moulin</a>, AGPLv3+. - <a href="https://git.guilhem.org/KlimatanalysNorr/webmap" target="_blank">Källkod</a>. + + <li data-for-button='info-button'> + <p>Här kan du se källor och licensvillkor för varje lager samt + själva kartverktyget. + För de flesta lager finns det också en produktlänk till den + ansvariga myndighetens webbplats.</p> + <p>Bredvid varje källa kan du se när den senast hämtades av + kartverktyget, samt när skikten generades på kartan (det vill + säga hur gammalt underlaget är på kartan). + Nedladdning samt uppdatering av underlag för kartan sker + automatiskt varje dag.</p> </li> - <li>Backend-verktyg: - © <a href="https://guilhem.se" target="_blank">Guilhem Moulin</a>, GPLv3+. - <a href="https://git.guilhem.org/KlimatanalysNorr/tools" target="_blank">Källkod</a>. + + <li data-for-button='help-button'> + <p>Om du klickar på den här knappen ser du det här + hjälpfönstret.</p> </li> - </ul> - <p class="small mb-0">Webbkartan är utvecklad av - <a href="https://guilhem.se" target="_blank">Guilhem Datakonsult</a> på uppdrag av - <a href="https://www.klimatanalysnorr.se" target="_blank">Klimatanalys Norr projektet</a>.</p> + </ol> + + <h3>Målgrupp och syftet</h3> + <p class="mb-2">Kartan kan användas som en hjälp i att välja vilka + exploateringar som är viktigast att bekämpa. Målgruppen för kartan + är individer och organisationer som vill:</p> + <ol> + <li>se kumulativa effekter av olika exploateringar i + norr</li> + <li>se var en viss exploatering ligger i förhållande + till ett område av intresse (ex formellt skydd, höga + naturvärden, riksintresse med flera)</li> + <li>övervaka de senaste exploateringarna i ett visst + område.</li> + </ol> + + <p>Viktigt att komma ihåg är att allt underlag i kartan + kommer från olika myndigheter (Bergsstaten, Skogsstyrelsen, + Länsstyrelsen, Naturvårdsverket med flera) och ingen modell + finns för att assistera i själva avvägningen. + Med andra ord så samlar kartan in befintliga underlag från + olika myndigheter istället för att själv presentera något + nytt, och den data kartan presenterar är därmed information + som kan ingå i rapporter och yttranden (och ingen kan hävda + att underlaget är påhittat).</p> + + <p>Underlagen för kartan uppdateras automatiskt varje dag. + Se ovan för detaljer om hur gammalt varje underlag är.</p> + + <h3>Buggrapporter och feedback</h3> + <p>Tveka inte att skicka ett + <a data-mailto-b64="Z3VpbGhlbQ __AT__ ZnJpcG9zdA __DOT__ b3Jn" href="#" target="_blank" class="link-secondary email-address-b64">mejl + <i class="bi bi-envelope-at"></i></a> + med önskemål, buggrapporter, förslag till + förbättring med flera. + Kartverktyget samt tillhörande verktyg är alla fri programvara.</p> </div> </div> </div> @@ -18,8 +18,10 @@ import Map from 'ol/Map.js'; import View from 'ol/View.js'; import TileLayer from 'ol/layer/Tile.js'; +import TileLayerGL from 'ol/layer/WebGLTile.js'; import WMTS from 'ol/source/WMTS.js'; +import GeoTIFF from 'ol/source/GeoTIFF.js'; import WMTSTileGrid from 'ol/tilegrid/WMTS.js'; import FullScreen from 'ol/control/FullScreen.js'; @@ -34,8 +36,15 @@ import VectorTileLayer from 'ol/layer/VectorTile.js'; import VectorTile from 'ol/source/VectorTile.js'; import { createXYZ } from 'ol/tilegrid.js'; +import { toContext } from 'ol/render.js'; +import Polygon from 'ol/geom/Polygon.js'; +import LineString from 'ol/geom/LineString.js'; +import Point from 'ol/geom/Point.js'; + import VectorLayer from 'ol/layer/Vector.js'; import VectorSource from 'ol/source/Vector.js'; +import Draw from 'ol/interaction/Draw.js'; +import { unByKey } from 'ol/Observable.js'; import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; @@ -44,6 +53,8 @@ import RegularShape from 'ol/style/RegularShape.js'; import Stroke from 'ol/style/Stroke.js'; import Style from 'ol/style/Style.js'; +import Geolocation from 'ol/Geolocation.js'; + import proj4 from 'proj4'; import { get as getProjection } from 'ol/proj.js'; import { register as registerProjection } from 'ol/proj/proj4.js'; @@ -51,12 +62,14 @@ import { register as registerProjection } from 'ol/proj/proj4.js'; import { Modal, Popover } from 'bootstrap'; import './style.css'; +"use strict"; proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'); registerProjection(proj4); -const projection = getProjection('EPSG:3006'); +const PROJECTION = getProjection('EPSG:3006'); +const LOCALE = 'sv-SE'; /* Lantmäteriet uses a tile-scheme where the origin (upper-left corner) is at * N8500000 E-1200000 (SWEREF99 TM), where each tile is 256×256 pixels, and where @@ -69,7 +82,7 @@ const projection = getProjection('EPSG:3006'); * side) somehow centered on Norrbotten and Västerbotten, and zoom in from there. * This represent a TILEROW (x) offset of 5, and a TILECOL (y) offset of 2. */ -const extent = [110720, 6927136, 1159296, 7975712]; +const EXTENT = [110720, 6927136, 1159296, 7975712]; /* XXX using the topowebbcache WMTS is fine for testing (as it doesn't require * authentication) but not in production in a public instance as doing so would @@ -87,82 +100,93 @@ const extent = [110720, 6927136, 1159296, 7975712]; * https://ext-geodatakatalog.lansstyrelsen.se/GeodataKatalogen/srv/swe/catalog.search#/map uses * https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/3c3a9cf47e7cb5ea24542d40d19698/?layer=topowebb&style=default&tilematrixset=3006&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=7&TileCol=237&TileRow=155 */ -const baseMapSource = new WMTS({ - url: undefined, - version: '1.0.0', - style: 'default', - matrixSet: '3006', - format: 'image/png', - tileGrid: new WMTSTileGrid({ - extent: extent, - // https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf - tileSize: 256, - origin: [-1200000, 8500000], - resolutions: [4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8], - matrixIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - }), - projection: projection, - wrapX: false, - crossOrigin: 'anonymous', -}); - - -const view = new View({ - projection: projection, - extent: extent, - showFullExtent: true, - /* center of the bbox of the Norrbotten and Västerbotten geometries */ - center: [694767.48, 7338176.57], - zoom: 1, - enableRotation: false, - resolutions: [1024, 512, 256, 128, 64, 32, 16, 8], - constrainResolution: false, -}); - -let baseMapLayer = 'topowebb'; -(function() { - const params = new URLSearchParams(window.location.hash.substring(1)); - const x = parseFloat(params.get('x')); - const y = parseFloat(params.get('y')); - if (!isNaN(x) && !isNaN(y)) { - view.setCenter([x, y]); - } - const z = parseFloat(params.get('z')); - if (!isNaN(z)) { - view.setZoom(z); - } - - if (params.has('basemap')) { - baseMapLayer = params.get('basemap'); - } - baseMapSource.setUrl(`https://minkarta.lantmateriet.se/map/topowebbcache?LAYER=${encodeURIComponent(baseMapLayer)}`); -})(); - - -const map = new Map({ - controls: [], - view: view, - layers: [ - new TileLayer({ - source: baseMapSource +const [BASEMAP, MAP] = (function() { + const param = 'basemap'; + const baseMap = Object.seal({ + _layer: new URLSearchParams(location.hash.substring(1))?.get?.(param) ?? 'topowebb_nedtonad', + get layer() { + return this._layer; + }, + get url() { + return 'https://minkarta.lantmateriet.se/map/topowebbcache?' + + 'LAYER=' + encodeURIComponent(this.layer); + }, + set layer(layername) { + this._layer = layername; + baseMapSource.setUrl(this.url); + const searchParams = new URLSearchParams(location.hash.substring(1)); + searchParams.set(param, layername); + location.hash = '#' + searchParams.toString(); + }, + }); + const baseMapSource = new WMTS({ + url: baseMap.url, + version: '1.0.0', + style: 'default', + matrixSet: '3006', + format: 'image/png', + tileGrid: new WMTSTileGrid({ + extent: EXTENT, + // https://www.lantmateriet.se/globalassets/geodata/geodatatjanster/tb_twk_visning-oversiktlig_v1.0.3.pdf + tileSize: 256, + origin: [-1200000, 8500000], + resolutions: [4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8], + matrixIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], }), - ], - target: document.getElementById('map'), -}); + projection: PROJECTION, + wrapX: false, + crossOrigin: 'anonymous', + }); -const popup = document.getElementById('popup'); + const view = new View({ + projection: PROJECTION, + extent: EXTENT, + showFullExtent: true, + enableRotation: false, + resolutions: [1024, 512, 256, 128, 64, 32, 16, 8], + constrainResolution: false, + }); + return [ + baseMap, + new Map({ + controls: [], + view: view, + layers: [ + new TileLayer({ + source: baseMapSource + }), + ], + target: document.getElementById('map'), + }), + ]; +})(); /* move the control container to the viewport */ -const container = document.getElementById('map-control-container'); +const CONTAINER_MAP = document.getElementById('map-control-container'); +const CONTAINER_STOPEVENT = MAP.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; (function() { - const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; - container0.appendChild(document.getElementById('zoom-control')); - container0.appendChild(container); - container0.appendChild(document.getElementById('modal-info')); - - const backdrop = document.createElement('div'); - container0.appendChild(backdrop); - backdrop.id = 'modal-info-backdrop'; + CONTAINER_STOPEVENT.appendChild(document.getElementById('zoom-control')); + CONTAINER_STOPEVENT.appendChild(CONTAINER_MAP); + CONTAINER_STOPEVENT.appendChild(document.getElementById('info-modal')); + CONTAINER_STOPEVENT.appendChild(document.getElementById('help-modal')); + + const info_backdrop = document.createElement('div'); + CONTAINER_STOPEVENT.appendChild(info_backdrop); + info_backdrop.id = 'info-modal-backdrop'; + + const help_backdrop = document.createElement('div'); + CONTAINER_STOPEVENT.appendChild(help_backdrop); + help_backdrop.id = 'help-modal-backdrop'; + + const age_filter = document.createElement('div'); + age_filter.id = 'age-filter-modal'; + age_filter.classList.add('modal'); + age_filter.setAttribute('tabindex', '-1'); + age_filter.setAttribute('aria-hidden', 'true'); + CONTAINER_STOPEVENT.appendChild(age_filter); + const age_filter_backdrop = document.createElement('div'); + age_filter_backdrop.id = 'age-filter-modal-backdrop'; + CONTAINER_STOPEVENT.appendChild(age_filter_backdrop); })(); /* zoom in/out */ @@ -185,7 +209,7 @@ const container = document.getElementById('map-control-container'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } - map.addControl(control); + MAP.addControl(control); })(); /* zoom slider */ @@ -197,29 +221,29 @@ const container = document.getElementById('map-control-container'); for (const btn of control.element.getElementsByTagName('button')) { btn.classList.add('btn', 'btn-light'); } - map.addControl(control); + MAP.addControl(control); })(); /* scale line */ (function() { - const size = map.getSize(); + const size = MAP.getSize(); const control = new ScaleLine({ units: 'metric', minWidth: 150, maxWidth: size[1] < 350 ? size[1] - 50 : 350, - target: container, + target: CONTAINER_MAP, }); control.element.classList.add('modal', 'modal-content'); - map.addControl(control); + MAP.addControl(control); })(); -const menu = document.getElementById('map-menu'); +const MENU = document.getElementById('map-menu'); const TRAILING_ZEROES = /\.?0*$/; /* "open in new tab" button */ if (window.location !== window.parent.location) { const div = document.createElement('div'); - menu.appendChild(div); + MENU.appendChild(div); div.classList.add('ol-unselectable', 'ol-control'); const btn = document.createElement('button'); @@ -234,13 +258,13 @@ if (window.location !== window.parent.location) { btn.appendChild(i); i.classList.add('bi', 'bi-box-arrow-up-right'); - btn.onclick = function(event) { - const coordinates = view.getCenter(); + btn.onclick = function() { + const coordinates = MAP.getView().getCenter(); const url = new URL(window.location.href); const searchParams = new URLSearchParams(url.hash.substring(1)); searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, '')); searchParams.set('y', coordinates[1].toFixed(2).replace(TRAILING_ZEROES, '')); - searchParams.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); + searchParams.set('z', MAP.getView().getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); url.hash = '#' + searchParams.toString(); return window.open(url.href, '_blank'); }; @@ -248,79 +272,53 @@ if (window.location !== window.parent.location) { /* layer selection button and legend */ if (window.location === window.parent.location) { - const btn = (function() { - const div = document.createElement('div'); - menu.appendChild(div); - div.id = 'layer-selection-button'; - div.classList.add('ol-unselectable', 'ol-control'); - - const btn = document.createElement('button'); - div.appendChild(btn); - btn.type = 'button'; - btn.title = 'Lagerval'; - btn.setAttribute('aria-label', btn.title); - btn.setAttribute('aria-expanded', 'false'); - btn.classList.add('btn', 'btn-light'); - - const i = document.createElement('i'); - btn.appendChild(i); - i.classList.add('bi', 'bi-stack'); - - return btn; - })(); - - const btn2 = (function() { - const div = document.createElement('div'); - menu.appendChild(div); - div.id = 'map-legend-button'; - div.classList.add('ol-unselectable', 'ol-control'); - - const btn = document.createElement('button'); - div.appendChild(btn); - btn.type = 'button'; - btn.title = 'Teckenförklaring'; - btn.setAttribute('aria-label', btn.title); - btn.setAttribute('aria-expanded', 'false'); - btn.classList.add('btn', 'btn-light'); - - const i = document.createElement('i'); - btn.appendChild(i); - i.classList.add('bi', 'bi-list-task'); - - return btn; - })(); - - const panel = document.getElementById('layer-selection-panel'); - btn.onclick = function(event) { - if (btn.getAttribute('aria-expanded') === 'true') { - panel.setAttribute('aria-hidden', 'true'); + const buttons = Object.fromEntries([ + {id: 'layer-selection', title: 'Lagerval', bi: 'stack'}, + {id: 'map-legend', title: 'Teckenförklaring', bi: 'list-task'}, + {id: 'measure', title: 'Mät i kartan', bi: 'rulers'}, + {id: 'age-filter', title: 'Filtrera objekt efter ålder', bi: 'clock-history'}, + ].map(function(x) { + const div = document.createElement('div'); + MENU.appendChild(div); + div.id = x.id + '-button'; + div.classList.add('ol-unselectable', 'ol-control'); + + const btn = document.createElement('button'); + div.appendChild(btn); + btn.type = 'button'; + btn.title = x.title; + btn.setAttribute('aria-label', btn.title); btn.setAttribute('aria-expanded', 'false'); - btn.classList.replace('btn-dark', 'btn-light'); - } else { - if (btn2.getAttribute('aria-expanded') === 'true') { - btn2.click(); - } - panel.setAttribute('aria-hidden', 'false'); - btn.setAttribute('aria-expanded', 'true'); - btn.classList.replace('btn-light', 'btn-dark'); - } - }; - - const panel2 = document.getElementById('map-legend-panel'); - btn2.onclick = function(event) { - if (btn2.getAttribute('aria-expanded') === 'true') { - panel2.setAttribute('aria-hidden', 'true'); - btn2.setAttribute('aria-expanded', 'false'); - btn2.classList.replace('btn-dark', 'btn-light'); - } else { - if (btn.getAttribute('aria-expanded') === 'true') { - btn.click(); - } - panel2.setAttribute('aria-hidden', 'false'); - btn2.setAttribute('aria-expanded', 'true'); - btn2.classList.replace('btn-light', 'btn-dark'); + btn.classList.add('btn', 'btn-light'); + + const i = document.createElement('i'); + btn.appendChild(i); + i.classList.add('bi', 'bi-' + x.bi); + return [x.id, btn] + })); + + Object.entries(buttons).forEach(function([id, btn]) { + const panel = document.getElementById(id + '-panel'); + if (panel != null) { + btn.onclick = function() { + if (btn.getAttribute('aria-expanded') === 'true') { + panel.setAttribute('aria-hidden', 'true'); + btn.setAttribute('aria-expanded', 'false'); + btn.classList.replace('btn-dark', 'btn-light'); + } else { + Object.values(buttons).forEach(function(btn2) { + /* close all other panels */ + if (!btn.isEqualNode(btn2) && btn2.getAttribute('aria-expanded') === 'true') { + btn2.click(); + } + }); + panel.setAttribute('aria-hidden', 'false'); + btn.setAttribute('aria-expanded', 'true'); + btn.classList.replace('btn-light', 'btn-dark'); + } + }; } - }; + }); } /* fullscreen control */ @@ -341,20 +339,17 @@ if (window.location === window.parent.location) { labelActive: labelActive, tipLabel: titleInactive, keys: true, - target: menu, - }) + target: MENU, + }); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.add('btn', classInactive); btn.setAttribute('aria-label', btn.title); - map.addControl(control); + MAP.addControl(control); + control.element.id = 'fullscreen-toggle'; /* for the help dialog */ control.addEventListener('enterfullscreen', function() { - featureOverlayLayer.setVisible(false); - const popover = Popover.getInstance(popup); - if (popover !== null) { - /* dispose popover as entering fullscreen messes up its position */ - popover.dispose(); - } + /* dispose popover as entering fullscreen messes up its position */ + disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classInactive, classActive); @@ -366,14 +361,10 @@ if (window.location === window.parent.location) { /* hide export button in fullscreen mode as it exits it */ exp.classList.add('d-none'); } - }) + }); control.addEventListener('leavefullscreen', function() { - featureOverlayLayer.setVisible(false); - const popover = Popover.getInstance(popup); - if (popover !== null) { - /* dispose popover as is might overflow the viewport */ - popover.dispose(); - } + /* dispose popover as is might overflow the viewport */ + disposePopover(); const btn = control.element.getElementsByTagName('button')[0]; btn.classList.replace(classActive, classInactive); @@ -384,7 +375,7 @@ if (window.location === window.parent.location) { if (exp !== undefined) { exp.classList.remove('d-none'); } - }) + }); } /* export/download button */ @@ -403,17 +394,17 @@ if (window.location === window.parent.location) { const i = document.createElement('i'); btn.appendChild(i); i.classList.add('bi', 'bi-download'); - menu.appendChild(div); + MENU.appendChild(div); - btn.onclick = function(event) { - map.once('rendercomplete', function() { + btn.onclick = function() { + MAP.once('rendercomplete', function() { const canvas0 = document.createElement('canvas'); - const size = map.getSize(); + const size = MAP.getSize(); canvas0.width = size[0]; canvas0.height = size[1]; const context = canvas0.getContext('2d'); - map.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer').forEach(function(canvas) { + MAP.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer').forEach(function(canvas) { if (canvas.width > 0) { const opacity = canvas.parentNode.style.opacity || canvas.style.opacity; context.globalAlpha = opacity === '' ? 1 : Number(opacity); @@ -434,2474 +425,2637 @@ if (window.location === window.parent.location) { }); }); - map.renderSync(); + MAP.renderSync(); }; } -/* info button */ +/* info and help buttons */ (function() { - const div = document.createElement('div'); - menu.appendChild(div); - div.id = 'info-button'; - div.classList.add('ol-unselectable', 'ol-control'); + const add_button = function(x) { + const div = document.createElement('div'); + MENU.appendChild(div); + div.id = x.id + '-button'; + div.classList.add('ol-unselectable', 'ol-control'); - const btn = document.createElement('button'); - div.appendChild(btn); - btn.type = 'button'; - btn.setAttribute('aria-expanded', 'false'); - btn.title = 'Visa information'; - btn.setAttribute('aria-label', btn.title); - btn.classList.add('btn', 'btn-light'); + const btn = document.createElement('button'); + div.appendChild(btn); + btn.type = 'button'; + btn.setAttribute('aria-expanded', 'false'); + btn.title = x.title; + btn.setAttribute('aria-label', btn.title); + btn.classList.add('btn', 'btn-light'); - const i = document.createElement('i'); - btn.appendChild(i); - i.classList.add('bi', 'bi-info-lg'); + const i = document.createElement('i'); + btn.appendChild(i); + i.classList.add('bi', 'bi-' + x.bi); - const panel = document.getElementById('modal-info'); - const modal = new Modal(panel, { - backdrop: false, - }); + const panel = document.getElementById(x.id + '-modal'); + const modal = new Modal(panel, { + backdrop: false, + }); - const backdrop = document.getElementById('modal-info-backdrop'); - backdrop.onclick = function(event) { - modal.hide(); - }; + const backdrop = document.getElementById(x.id + '-modal-backdrop'); + backdrop.onclick = function() { + modal.hide(); + }; - panel.addEventListener('show.bs.modal', function() { - backdrop.classList.add('modal-backdrop', 'show'); - btn.setAttribute('aria-expanded', 'true'); - btn.classList.replace('btn-light', 'btn-dark'); - }); - panel.addEventListener('hidden.bs.modal', function() { - btn.classList.replace('btn-dark', 'btn-light'); - btn.setAttribute('aria-expanded', 'false'); - backdrop.classList.remove('modal-backdrop', 'show'); - }); + panel.addEventListener('show.bs.modal', function() { + backdrop.classList.add('modal-backdrop', 'show'); + btn.setAttribute('aria-expanded', 'true'); + btn.classList.replace('btn-light', 'btn-dark'); + }); + panel.addEventListener('hide.bs.modal', function() { + /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */ + const activeElement = document.activeElement; + if (activeElement instanceof HTMLElement) { + activeElement.blur(); + } + }); - btn.onclick = function(event) { - modal.toggle(); - }; -})(); + panel.addEventListener('hidden.bs.modal', function() { + btn.classList.replace('btn-dark', 'btn-light'); + btn.setAttribute('aria-expanded', 'false'); + backdrop.classList.remove('modal-backdrop', 'show'); + }); -/* we're all set, show the control container now */ -container.setAttribute('aria-hidden', 'false'); + btn.onclick = function() { + modal.show(); + }; -view.on('change', function(event) { - featureOverlayLayer.setVisible(false); - const popover = Popover.getInstance(popup); - if (popover !== null) { - popover.dispose(); - } + /* de-obfuscate email address */ + const CLASSNAME = 'email-address-b64'; + const ATTRNAME = 'data-mailto-b64'; + for (const a of panel.getElementsByClassName(CLASSNAME)) { + if (a.tagName.toLowerCase() === 'a' && a.hasAttribute(ATTRNAME)) { + let href = 'mailto:'; + for (const part of a.getAttribute(ATTRNAME).split(/\s+/)) { + switch (part) { + case '__AT__': + href += '@'; + break; + case '__DOT__': + href += '.'; + break; + default: + href += atob(part); + } + } + a.classList.remove(CLASSNAME); + a.removeAttribute(ATTRNAME); + a.href = href; + } + } - const coordinates = view.getCenter(); - const searchParams = new URLSearchParams(location.hash.substring(1)); - searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, '')); - searchParams.set('y', coordinates[1].toFixed(2).replace(TRAILING_ZEROES, '')); - searchParams.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); - location.hash = '#' + searchParams.toString(); -}); + return [panel, btn, modal]; + }; + /* info button */ + (function() { + const [panel, btn, modal] = add_button({ + id: 'info', + title: 'Källor och licensinformation', + bi: 'info-lg', + }); -/* TODO: this should really be refactored… */ -const layers = { - mrr_appr_ec: { - popoverTitle: 'Bearbetningskoncession \u2013 beviljad', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Ägare', 'owners'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Giltig från', 'validfrom'], - ['Giltig till', 'validto'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 22, - fill: new Fill({ - color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - mrr_appl_ec: { - popoverTitle: 'Bearbetningskoncession \u2013 ansökt', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Sökande', 'owners'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Ansökningsdatum', 'appl_date'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 25, - fill: new Fill({ - color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [197, 14, 31, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - mrr_appr_met: { - popoverTitle: 'Undersökningstillstånd, metaller och industrimineral \u2013 beviljad', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Ägare', 'owners'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Giltig från', 'validfrom'], - ['Giltig till', 'validfrom'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 24, - fill: new Fill({ - color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - mrr_appl_met: { - popoverTitle: 'Undersökningstillstånd, metaller och industrimineral \u2013 ansökt', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Sökande', 'owners'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Ansökningsdatum', 'appl_date'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 26, - fill: new Fill({ - color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [197, 14, 31, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - mrr_appr_ogd: { - popoverTitle: 'Undersökningstillstånd, olja, gas och diamant \u2013 beviljad', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Ägare', 'owners'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Giltig från', 'validfrom'], - ['Giltig till', 'validto'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 24, - fill: new Fill({ - color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), - }); - }), - }, - mrr_appl_ogd: { - popoverTitle: 'Undersökningstillstånd, olja, gas och diamant \u2013 ansökt', - popover: [ - ['Namn', 'name'], - ['Koncessionsmineral', 'mineral'], - ['Sökande', 'owners'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Ansökningsdatum', 'appl_date'], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 26, - fill: new Fill({ - color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [197, 14, 31, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - mrr_appr_dl: { - popoverTitle: 'Markanvisning till koncession', - popover: [ - ['Namn', 'name'], - ['Tillhörande bearbetnings\u00ADkoncession(er)', 'conc_name'], - ['Tillståndsid', 'licenceid', { classes: ['feature-attr-mrr-license-id'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ['Diarienummer', 'diarynr', { classes: ['feature-attr-dnr'] }], - ['Ansökningsdatum', 'appl_date'], - ['Beslutsdatum', 'dec_date'], - //['Kommun', 'Municipality'], - //['Län', 'County'], - ['Senast uppdaterad', 'export_date'], - ], - style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 20, - fill: new Fill({ - color: [228, 53, 45, Math.max((.2-1)/6 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [151, 173, 23, 1], - }), + panel.addEventListener('hidden.bs.modal', function() { + infoMetadataAccordions.forEach(function(x, idx) { + /* collapse all accordions */ + const body = x.element.parentNode.parentNode; + const name = 'info-accordion-collapse-' + idx; + if (body.id === name) { + body.classList.remove('show'); + } + if (body.parentNode !== null) { + const headers = body.parentNode.getElementsByClassName('accordion-header'); + for (let i = 0; i < headers.length; i++) { + const buttons = headers[i].getElementsByClassName('accordion-button'); + for (let j = 0; j < buttons.length; j++) { + const btn = buttons[j]; + if (btn.getAttribute('data-bs-target') === '#' + name) { + btn.setAttribute('aria-expanded', 'false'); + btn.classList.add('collapsed'); + } + } + } + } + }); + }); + + const dateFormatter = new Intl.DateTimeFormat(LOCALE); + btn.onclick = function() { + infoMetadataAccordions.forEach((x) => x.element.replaceChildren()); + modal.show(); + Promise.allSettled(Object.entries(mapLayers).map(function([grp,lyr]) { + const baseurl = lyr?.getSource?.()?.get?.('baseurl'); + if (baseurl == null) { + return new Promise(() => { throw new Error(`Unknown source for "${grp}"`); }); + } + return fetch(new URL('metadata.json', baseurl)) + .then(function(resp0) { + if (resp0.status === 200) { + return resp0.json().then((x) => [grp,x]); + } else { + throw new Error(`${resp0.url} [${resp0.status}]`); + } + }); + })) + .then(function(rs) { + const metadata = Object.fromEntries(rs.filter(function(r) { + if (r.status === 'fulfilled') { + return true; + } else if (r.status === 'rejected') { + console.log(r.reason); + } + return false; + }).map((r) => r.value)); + + infoMetadataAccordions.forEach(function(x) { + const ul = x.element; + const groupnames = new Set(); + const last_updated = []; + x.items.forEach(function([groupname]) { + const layer_group = metadata[groupname]; + if (layer_group == null) { + return; + } + if (!groupnames.has(groupname)) { + groupnames.add(groupname); + if (layer_group.last_updated != null) { + last_updated.push(layer_group.last_updated); + } + } + }); + if (last_updated.length > 0) { + /* show creation time of the MVT layers */ + const li = document.createElement('li'); + li.classList.add('list-group-item', 'text-muted'); + ul.appendChild(li); + const i = document.createElement('i'); + i.classList.add('bi', 'bi-map'); + li.appendChild(i); + const t = document.createTextNode( + ' Lokalt skikt (vectiler) genererades ' + + last_updated + .sort() + .map((ts) => dateFormatter.format(new Date(ts))) + .join('; ') + '.' + ); + li.appendChild(t); + } + + const source_files = new Set(); + x.items.forEach(function([groupname, layername]) { + /* for each source file associated with the accordion header, show copyright, license and timing information */ + const layer_group = metadata[groupname]; + if (layer_group?.layers == null || layer_group?.source_files == null) { + return; + } + const def = layer_group.layers[layername]; + if (def?.source_files == null) { + return; + } + def.source_files.forEach(function(source_file) { + if (source_files.has(source_file)) { + return; + } + const x = layer_group.source_files[source_file]; + source_files.add(source_file); + + const li = document.createElement('li'); + li.classList.add('list-group-item'); + ul.appendChild(li); + const h = document.createElement('h6'); + li.appendChild(h); + if (x.description != null) { + const t = document.createTextNode(x.description); + h.appendChild(t); + } + + if (x.copyright != null) { + const p = document.createElement('p'); + li.appendChild(p); + const t = document.createTextNode(x.copyright); + p.appendChild(t); + } + + if (x.license != null) { + const p = document.createElement('p'); + li.appendChild(p); + p.appendChild(document.createTextNode('Licensvillkor: ')); + const t = document.createTextNode(x.license.name); + if (x.license.url == null) { + p.appendChild(t); + } else { + const a = document.createElement('a'); + a.href = x.license.url; + a.target = '_blank'; + a.appendChild(t); + p.appendChild(a); + } + } + + if (x.product_url != null) { + const p = document.createElement('p'); + li.appendChild(p); + const t = document.createTextNode('Produktlänk '); + const i = document.createElement('i'); + i.classList.add('bi', 'bi-box-arrow-up-right'); + const a = document.createElement('a'); + a.href = x.product_url; + a.target = '_blank'; + a.appendChild(t); + a.appendChild(i); + p.appendChild(a); + } + + if (x.last_modified != null) { + const p = document.createElement('p'); + p.classList.add('small', 'text-muted'); + li.appendChild(p); + const i = document.createElement('i'); + i.classList.add('bi', 'bi-file-earmark-code'); + p.appendChild(i); + p.appendChild(document.createTextNode(' ')); + const t0 = document.createTextNode('Källfil'); + if (x.url == null) { + p.appendChild(t0); + } else { + const a = document.createElement('a'); + p.appendChild(a); + const i = document.createElement('i'); + i.classList.add('bi', 'bi-box-arrow-up-right'); + a.appendChild(t0); + a.appendChild(document.createTextNode(' ')); + a.appendChild(i); + a.href = x.url; + a.target = '_blank'; + } + const t1 = document.createTextNode(' ändrades senast '); + p.appendChild(t1); + const td = document.createTextNode(dateFormatter.format(new Date(x.last_modified))); + p.appendChild(td); + const t2 = document.createTextNode('.'); + p.appendChild(t2); + } + }); + }); + }); }); - }), + }; + })(); + + /* help button */ + (function() { + const [panel] = add_button({ + id: 'help', + title: 'Hjälp med att använda kartan', + bi: 'question-circle', + }); + + /* Use the text from the .html file but ensure that buttons are + * listed in the same order as the menu, and spell titles out. This + * avoids duplication and avoids that things would get out of sync */ + const button_map = {}; + const ol = panel.querySelector('#help-describe-functions'); + if (ol != null && ol.tagName.toLowerCase() === 'ol') { + for (const li of ol.children) { + const id = li.getAttribute('data-for-button'); + if (id == null || id === '') { + continue; + } + button_map[id] = li; + } + } + + for (const node of MENU.children) { + if (node.id == null || node.id === '') { + continue + } + const btn = node.getElementsByTagName('button')[0]; + if (btn == null || btn.tagName.toLowerCase() !== 'button') { + continue; + } + const btn2 = btn.cloneNode(true); + const title = btn2.title; + btn2.id = btn2.title = ''; + for (const attr of btn.attributes) { + if (attr.name.toLowerCase().startsWith('aria-') || attr.name === 'id' || attr.name === 'title') { + btn2.removeAttribute(attr.name); + } + } + + const h = document.createElement('h6'); + h.classList.add('help-button-description'); + h.appendChild(btn2); + if (title != null && title != '') { + const t = document.createTextNode(title) + h.appendChild(t); + } + btn2.classList.add('help-button'); + + ol.insertAdjacentElement('beforebegin', h); + + const li = button_map[node.id]; + if (li != null) { + /* move <li>'s children (paragraphs) to the main text */ + while (li.children.length > 0) { + ol.insertAdjacentElement('beforebegin', li.firstElementChild); + } + } + } + ol.remove(); + })(); +})(); + +/* we're all set, show the control container now */ +CONTAINER_MAP.setAttribute('aria-hidden', 'false'); + +/* age filter settings state */ +const ageFilterSettings = (function() { + const dateToTS = function(d) { + if (d != null) { + /* number of days since 1970-01-01; take both dates at 00:00:00.0 UTC */ + return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())/86_400_000); + } + }; + return Object.seal({ + _active: false, + get active() { + return this._active; + }, + set active(b) { + ageFilterSetActive(b); + }, + type: 'relative', + operator: '<=', + quantity: 1, + unit: 'y', + from: null, + to: null, + show_unknown: false, + _min_ts: null, + _max_ts: null, + getRelativeDate: function(quantity, unit) { + if (quantity == null || isNaN(quantity) || unit == null) { + return null; + } + /* use today noon localtime to avoid issues due to DST when substracting dates */ + const d = new Date(); + d.setHours(12, 0, 0, 0); + switch (unit) { + case 'd': + d.setDate(d.getDate() - quantity); + break; + case 'w': + d.setDate(d.getDate() - 7 * quantity); + break; + case 'm': + d.setMonth(d.getMonth() - quantity); + break; + case 'y': + d.setFullYear(d.getFullYear() - quantity); + break; + default: + return null; + } + return d; + }, + setupMinMax: function() { + this._min_ts = this._max_ts = null; + switch (this.type) { + case 'relative': { + const date = this.getRelativeDate(this.quantity, this.unit); + const prop = {'<=':'_min_ts', '>=':'_max_ts'}[this.operator]; + this[prop] = dateToTS(date); + break; + } + case 'interval': { + this._min_ts = dateToTS(this.from); + this._max_ts = dateToTS(this.to); + break; + } + } + }, + }); +})(); + +/* Layer style definitions */ +/* TODO: this should really be refactored, but probably not worth doing before the switch to + * WebGL, which is currently blocking on https://github.com/openlayers/openlayers/issues/15807 + * and https://github.com/openlayers/openlayers/issues/16246 */ +const LAYERS = Object.seal({ + adm: { + lansyta: { + legend: { zoomLevel: 3, type: 'linestring' }, + style: [1.5, 2, 3, 3, 4, 4, 6, 6, 8, 8, 10, 10].map(function(width) { + return new Style({ + zIndex: 0, + fill: null, + stroke: new Stroke({ + width: width, + color: [212, 147, 208, 1], + }), + }); + }), + }, + kommunyta: { + legend: { zoomLevel: 3, type: 'linestring' }, + style: [2, 2, 3, 3, 4, 4, 6, 6, 8, 8, 10, 10].map(function(width) { + return new Style({ + zIndex: 0, + fill: null, + stroke: new Stroke({ + width: width/2, + color: [212, 147, 208, 1], + }), + }); + }), + }, }, - svk_ledningar: { - popoverTitle: 'Kraftledning (befintlig)', - popover: [ - ['Förläggning', 'Placement'], - ['Spänning', 'Voltage', { unit: 'kV' }], - ['Ledlängd', 'geom_length', { fn: 'length' }], - ], - style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { - return new Style({ - zIndex: 52, - stroke: new Stroke({ - color: 'black', - width: width, - }), - }); - }), + mrr: { + appr_ec: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 22, + fill: new Fill({ + color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + appl_ec: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 25, + fill: new Fill({ + color: [247, 170, 67, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [197, 14, 31, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + appr_met: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 24, + fill: new Fill({ + color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + appl_met: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 26, + fill: new Fill({ + color: [0, 0, 0, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [197, 14, 31, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + appr_ogd: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 24, + fill: new Fill({ + color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, + appl_ogd: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 26, + fill: new Fill({ + color: [30, 55, 87, Math.max((.2-.4)/4 * z + .4, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [197, 14, 31, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + appr_dl: { + legend: { zoomLevel: 4 }, + style: [0, .1, .5, .5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 20, + fill: new Fill({ + color: [228, 53, 45, Math.max((.2-1)/6 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [151, 173, 23, 1], + }), + }); + }), + }, }, - svk_stolpar: { - style: [undefined, undefined, undefined, undefined, undefined] - .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { + + svk: { + ledningar: { + legend: { zoomLevel: 5, type: 'linestring', reuse_canvas: true }, + style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { return new Style({ - zIndex: 51, - image: new CircleStyle({ + zIndex: 52, + stroke: new Stroke({ + color: 'black', + width: width, + }), + }); + }), + }, + stolpar: { + legend: { zoomLevel: 5, type: 'point' }, + style: [undefined, undefined, undefined, undefined, undefined] + .concat([3, 4, 5, 6, 8, 10, 15].map(function(radius) { + return new Style({ + zIndex: 51, + image: new CircleStyle({ + radius: radius, + fill: new Fill({ + color: 'black', + }), + }), + }); + })), + }, + transmissionsnatsprojekt: { + legend: { zoomLevel: 5, type: 'linestring' }, + style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { + return new Style({ + zIndex: 53, + stroke: new Stroke({ + color: 'black', + width: width, + lineDash: [4 * width], + }), + }); + }), + }, + stationer: { + legend: { zoomLevel: 3, type: 'point' }, + style: [3, 4, 5, 6, 7, 8.5, 10].map(function(radius) { + return new Style({ + zIndex: 50, + image: new RegularShape({ radius: radius, + points: 4, + angle: Math.PI/4, fill: new Fill({ color: 'black', }), }), }); - })), - }, - svk_transmissionsnatsprojekt: { - popoverTitle: 'Transmissionsnätsprojekt', - popover: [ - ['Projektnamn', 'Name'], - ['Spänning', 'Voltage', { unit: 'kV' }], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [1, 1.5, 2, 2, 2, 2, 3, 4, 5, 6, 8, 10].map(function(width) { - return new Style({ - zIndex: 53, - stroke: new Stroke({ - color: 'black', - width: width, - lineDash: [4 * width], - }), - }); - }), - }, - svk_stationer: { - style: [3, 4, 5, 6, 7, 8.5, 10].map(function(radius) { - return new Style({ - zIndex: 50, - image: new RegularShape({ - radius: radius, - points: 4, - angle: Math.PI/4, + }) + .concat([.5, 1, 1.5, 2, 2].map(function(width) { + return new Style({ + zIndex: 50, fill: new Fill({ - color: 'black', + color: 'rgba(128, 128, 128, .7)', }), - }), - }); - }) - .concat([.5, 1, 1.5, 2, 2].map(function(width) { - return new Style({ - zIndex: 50, - fill: new Fill({ - color: 'rgba(128, 128, 128, .7)', - }), - stroke: new Stroke({ - width: width, - color: 'rgb(0, 0, 0)', - }), - }); - })), + stroke: new Stroke({ + width: width, + color: 'rgb(0, 0, 0)', + }), + }); + })), + }, }, - vbk_area_current: { - popoverTitle: 'Projekteringsområde för vindbruk', - popover: [ - ['Projektnamn', 'Projektnamn'], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Aktuella verk', 'AntalVerk'], - ['Antal ej koordinatsatta verk', 'AntalEjXY'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Planerad byggstart', 'PlaneradByggstart'], - ['Planerat drifttagande', 'PlaneratDrift'], - ['Andringsansokan', 'AndringsansokanPagar'], - ['Under Byggnation', 'UnderByggnation'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Senast uppdaterat', 'SenasteUppdaterat'], - ], - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [168, 198, 223, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [56, 96, 130, 1], - }), - }); - }), - }, - vbk_area_notcurrent: { - popoverTitle: 'Projekteringsområde för vindbruk \u2013 ej aktuell', - popover: [ - ['Projektnamn', 'Projektnamn'], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Aktuella verk', 'AntalVerk'], - ['Antal ej koordinatsatta verk', 'AntalEjXY'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Planerad byggstart', 'PlaneradByggstart'], - ['Planerat drifttagande', 'PlaneratDrift'], - ['Andringsansokan', 'AndringsansokanPagar'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Senast uppdaterat', 'SenasteUppdaterat'], - ], - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [222, 163, 199, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [148, 55, 112, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), - }, - vbk_station_completed: { - popoverTitle: 'Vindkraftverk \u2013 uppfört', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Uppförandedatum', 'Uppfort'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-completed.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - vbk_station_processed: { - popoverTitle: 'Vindkraftverk \u2013 handlagt', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-processed.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - vbk_station_approved: { - popoverTitle: 'Vindkraftverk \u2013 beviljat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-approved.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - vbk_station_revoked: { - popoverTitle: 'Vindkraftverk \u2013 inte längre aktuell/återkallat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-revoked.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - vbk_station_rejected: { - popoverTitle: 'Vindkraftverk \u2013 avslagit/nekat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Miljöbalken tillstånd tidsbegränsning', 'MB_Tillstand'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-rejected.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - vbk_station_dismounted: { - popoverTitle: 'Vindkraftverk \u2013 nedmonterat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Uppförandedatum', 'Uppfort'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-dismounted.svg', - declutter: 'none', - scale: scale, - }), - }); - }), - }, - vbk_station_appealed: { - popoverTitle: 'Vindkraftverk \u2013 överklagat', - popover: [ - ['Verk-ID', 'VerkID', { classes: ['feature-objid'] }], - ['Områdes-ID', 'OmrID', { classes: ['feature-objid'] }], - ['Projektnamn', 'Projektnamn'], - ['Status', 'Status'], - ['Handlingstyp', 'Handlingstyp'], - ['Totalhöjd', 'Totalhojd', { unit: 'm' }], - ['Navhöjd', 'Navhojd', { unit: 'm' }], - ['Rotordiameter', 'Rotordiameter', { unit: 'm' }], - ['Maxeffekt', 'Maxeffekt', { unit: 'MW' }], - ['Beräknad årsproduktion', 'Calprod', { unit: 'GWh' }], - ['Fabrikat', 'Fabrikat'], - ['Modell', 'Modell'], - ['Organisationsnamn', 'Organisationsnamn'], - ['Organisationsnummer', 'Organisationsnummer', { classes: ['feature-orgnr'] }], - ['Placering', 'Placering'], - //['Kommun', 'KOMNAMN'], - //['Län', 'LANSNAMN'], - ['Elområde', 'ElNamn'], - ['Datum för senaste uppdatering av verk', 'SenasteUppdaterat'], - ], - style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { - return scale === undefined ? undefined : new Style({ - zIndex: 99, - image: new Icon({ - src: '/assets/icons/wind-turbine-appealed.svg', - declutter: 'none', - scale: scale, - }), - }); - }), + vbk: { + area_current: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [168, 198, 223, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [56, 96, 130, 1], + }), + }); + }), + }, + area_notcurrent: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [222, 163, 199, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [148, 55, 112, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, + offshore_completed: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 17, + fill: new Fill({ + color: [38, 107, 29, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [38, 107, 29, 1], + }), + }); + }), + }, + offshore_approved: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 16, + fill: new Fill({ + color: [56, 160, 44, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [56, 160, 44, 1], + }), + }); + }), + }, + offshore_amended: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; + patternCanvas.width = width/2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(247, 105, 162, 1)'; + patternContext.beginPath(); + patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, 1.5*w, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 17, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: 2*w, + color: [247, 105, 162, 1], + lineDash: [8 * w], + }), + }); + }), + }, + offshore_rejected: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 11, + fill: new Fill({ + color: [227, 26, 28, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [227, 26, 28, 1], + }), + }); + }), + }, + offshore_appealed: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 15, + fill: new Fill({ + color: [177, 88, 40, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [177, 88, 40, 1], + }), + }); + }), + }, + offshore_applied: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 14, + fill: new Fill({ + color: [255, 127, 0, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [255, 128, 0, 1], + }), + }); + }), + }, + offshore_consultation: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 13, + fill: new Fill({ + color: [254, 217, 118, .65], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [254, 183, 82, 1], + }), + }); + }), + }, + offshore_investigation: { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const w = z < 4 ? .5 : z <= 5 ? 1.5 : 4; + patternCanvas.width = width*2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(68, 90, 166, 1)'; + patternContext.lineWidth = w; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 12, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: 2*w, + color: [68, 90, 166, 1], + lineDash: [8 * w], + }), + }); + }), + }, + offshore_revoked: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [105, 61, 154, .5], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [105, 62, 153, 1], + }), + }); + }), + }, + turbine_completed: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-completed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_processed: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-processed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_approved: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-approved.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_revoked: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-revoked.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_rejected: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-rejected.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_dismounted: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-dismounted.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, + turbine_appealed: { + legend: { zoomLevel: 7, type: 'point' }, + style: [undefined, undefined, undefined, undefined, .125, .125, .25, .5, 1, 2, 4, 8].map(function(scale) { + return scale === undefined ? undefined : new Style({ + zIndex: 99, + image: new Icon({ + src: '/assets/icons/wind-turbine-appealed.svg', + declutter: 'none', + scale: scale, + }), + }); + }), + }, }, - /* Documentation at - * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf - * */ - sks_clearcut_comp: { - popoverTitle: 'Utförd avverkning', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Registeringsår', 'ArendeAr'], - ['Skogstyp', 'Skogstyp'], - ['Areal anmält', 'AnmaldHa', { unit: 'ha' }], - ['Areal naturlig föryngring', 'NatforHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - //['Areal plantering', 'SkogsodlHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - ['Avverkningstyp', 'Avverktyp'], - ['Datum för avverkning', 'Avvdatum'], - ['Ursprung för datum för avverkning', 'KallaDatum'], - //['Ursprung för areal avverkning', 'KallaAreal'], - //['Kommun', 'Kommun'], - //['Län', 'Lan'], - ['Areal för ytan', 'geom_area', { fn: 'area' }], - ], - style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: new Fill({ - color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [204, 0, 0, 1], - }), - }); - }), - }, - /* Documentation at - * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf - * */ - sks_clearcut_appl: { - popoverTitle: 'Avverkningsanmälansområde', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Inkom datum', 'Inkomdatum'], - ['Registeringsår', 'ArendeAr'], - //['Skogstyp', 'Skogstyp'], - ['Areal anmält', 'AnmaldHa', { unit: 'ha' }], - ['Areal naturlig föryngring', 'NatforHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - ['Areal plantering', 'SkogsodlHa', { unit: 'ha', fn: (v) => v === 0 ? '' : v }], - ['Avverkningssäsong', 'AvvSasong'], - //['Avverkningstyp', 'Avverktyp'], - //['Ändamål', 'Andamal'], - //['Kommun', 'Kommun'], - //['Län', 'Lan'], - ['Ärendestatus', 'ArendeStatus'], - ['Avverkad areal', 'AvvHa', { unit: 'ha' }], - ], - style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { - return new Style({ - zIndex: 10, - fill: (width === undefined || width === 0) ? - new Fill({ color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)*.75] }) : - (function() { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - const slope = 45 * Math.PI/180; - const spacing = z < 10 ? z*2 : 40; - const len = Math.hypot(1, slope); - const w = patternCanvas.width = Math.round(1/len + spacing) - const h = patternCanvas.height = Math.round(slope/len + spacing * slope); - - patternContext.fillStyle = 'rgba(255, 102, 102, .1)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(204, 0, 0, 1)'; - patternContext.lineWidth = Math.max(1, width/2); - patternContext.beginPath(); - patternContext.moveTo(0, h); - patternContext.lineTo(w, 0); - patternContext.moveTo(-w, h); - patternContext.lineTo(w, -h); - patternContext.moveTo(0, 2*h); - patternContext.lineTo(2*w, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Fill({ color: context.createPattern(patternCanvas, 'repeat') }); - })(), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [204, 0, 0, 1], - lineDash: width >= 1.5 ? [2 * width] : undefined, - }), - }); - }), + avverk: { + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf + * */ + utford: { + legend: { zoomLevel: 7 }, + style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: new Fill({ + color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [204, 0, 0, 1], + }), + }); + }), + }, + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf + * */ + anmald: { + legend: { zoomLevel: 7 }, + style: [0, 0, 0, 0, 0, .5, .75, 1, 1, 1, 1, 1].map(function(width, z) { + return new Style({ + zIndex: 10, + fill: (width === undefined || width === 0) ? + new Fill({ color: [255, 102, 102, Math.max((.2-1)/8 * z + 1, 0)*.75] }) : + (function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const slope = 45 * Math.PI/180; + const spacing = z < 10 ? z*2 : 40; + const len = Math.hypot(1, slope); + const w = patternCanvas.width = Math.round(1/len + spacing); + const h = patternCanvas.height = Math.round(slope/len + spacing * slope); + + patternContext.fillStyle = 'rgba(255, 102, 102, .1)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(204, 0, 0, 1)'; + patternContext.lineWidth = Math.max(1, width/2); + patternContext.beginPath(); + patternContext.moveTo(0, h); + patternContext.lineTo(w, 0); + patternContext.moveTo(-w, h); + patternContext.lineTo(w, -h); + patternContext.moveTo(0, 2*h); + patternContext.lineTo(2*w, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Fill({ color: context.createPattern(patternCanvas, 'repeat') }); + })(), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [204, 0, 0, 1], + lineDash: width >= 1.5 ? [2 * width] : undefined, + }), + }); + }), + }, }, - nvr_tilltradesforbud: { - popoverTitle: 'Tillträdesförbud', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Föreskriftsområde', 'FORSKRNAMN'], - ['Namn', 'OBJEKTNAMN'], - ['Beslutsstatus', 'BESLSTAT'], - ['Föreskriftstyp', 'FORESKRTYP'], - ['Föreskriftssubtyp', 'FORESKRIFT'], - ['Från datum', 'FRANDATUM'], - ['Till datum', 'TILLDATUM'], - ['Beskrivning', 'BESKRIVN'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width, z) { - return new Style({ - zIndex: 23, - fill: new Fill({ - /* transparent fill so clicking the inside of the polygon triggers a popover */ - /* XXX could also use a custom renderer but that doesn't seem to work */ - color: [0, 0, 0, 0], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [255, 0, 0, 1], - }), - }); - }), - }, - nvr_nationalpark: { - popoverTitle: 'Nationalpark', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 55, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 22, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [0, 55, 0, 1], - }), - }); - }), - }, - nvr_naturreservat: { - popoverTitle: 'Naturreservat', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 21, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [7, 181, 7, 1], - }), - }); - }), - }, - nvr_naturreservat_kommunalt: { - popoverTitle: 'Kommunalt naturreservat', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 20, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [7, 181, 7, 1], - }), - }); - }), - }, - nvr_naturvardsomrade: { - popoverTitle: 'Naturvårdsområde', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(176, 255, 176, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 19, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [176, 255, 176, 1], - }), - }); - }), - }, - nvr_djur_och_vaxtskyddsomrade: { - popoverTitle: 'Djur- och växtskyddsområde', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 255, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 18, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 255, 0, 1], - }), - }); - }), - }, - nvr_kulturreservat: { - popoverTitle: 'Kulturreservat', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(154, 102, 255, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 17, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [154, 102, 255, 1], - }), - }); - }), - }, - nvr_vattenskyddsomrade: { - popoverTitle: 'Vattenskyddsområden', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ikraftträdandedatum föreskrifter', 'IKRAFTDATF'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Tillsynsmyndighet', 'TILLSYNSMH'], - ['Prövningsmyndighet tillstånd', 'PROVNMHTIL'], - ['Prövningsmyndighet dispens', 'PROVNMHDIS'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 105, 212, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 16, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [0, 105, 212, 1], - }), - }); - }), - }, - nvr_landskapsbildsskyddsomrade: { - popoverTitle: 'Landskapsbildsskyddsområde', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(135, 110, 71, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 15, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [134, 110, 71, 1], - }), - }); - }), - }, - nvr_skogligt_biotopskyddsomrade: { - popoverTitle: 'Biotopskydd i skogsmark', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Biotopkategori', 'Biotyp'], - ['Skogstyp', 'Naturtyp'], - ['Registeringsår', 'ArendeAr'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Skogsmarksareal', 'AreaProd', { unit: 'ha' }], - ['Beslutsdatum', 'Datbeslut'], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(135, 90, 71, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 14, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [134, 90, 71, 1], - }), - }); - }), - }, - nvr_ovrigt_biotopskyddsomrade: { - popoverTitle: 'Biotopskydd utanför skogsmark', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 95, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 13, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 95, 0, 1], - }), - }); - }), - }, - nvr_naturminne_yta: { - popoverTitle: 'Naturminne (yta)', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(113, 0, 116, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 12, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [134, 0, 116, 1], - }), - }); - }), - }, - nvr_naturminne_punkt: { - popoverTitle: 'Naturminne (punkt)', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [undefined, undefined, undefined, undefined].concat([3, 4, 6, 8, 12, 16, 20, 24].map(function(width, z) { - return new Style({ - zIndex: 12, - image: new CircleStyle({ - radius: width, + skydd: { + tilltradesforbud: { + legend: { zoomLevel: 2 }, + style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { + return new Style({ + zIndex: 23, + fill: new Fill({ + /* transparent fill so clicking the inside of the polygon triggers a popover */ + /* XXX could also use a custom renderer but that doesn't seem to work */ + color: [0, 0, 0, 0], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [255, 0, 0, 1], + }), + }); + }), + }, + nationalpark: { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 55, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 22, fill: new Fill({ - color: 'rgba(113, 0, 116, .5)', + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [0, 55, 0, 1], + }), + }); + }), + }, + naturreservat: { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 21, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [7, 181, 7, 1], + }), + }); + }), + }, + naturreservat_kommunalt: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 20, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [7, 181, 7, 1], + }), + }); + }), + }, + naturvardsomrade: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(176, 255, 176, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 19, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [176, 255, 176, 1], + }), + }); + }), + }, + djur_och_vaxtskyddsomrade: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 255, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 18, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [255, 255, 0, 1], + }), + }); + }), + }, + kulturreservat: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(154, 102, 255, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 17, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [154, 102, 255, 1], + }), + }); + }), + }, + vattenskyddsomrade: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 105, 212, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 16, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [0, 105, 212, 1], + }), + }); + }), + }, + landskapsbildsskyddsomrade: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(135, 110, 71, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 15, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [134, 110, 71, 1], + }), + }); + }), + }, + skogligt_biotopskyddsomrade: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(135, 90, 71, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 14, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [134, 90, 71, 1], + }), + }); + }), + }, + ovrigt_biotopskyddsomrade: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 95, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 13, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [255, 95, 0, 1], + }), + }); + }), + }, + naturminne_yta: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(113, 0, 116, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 12, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [134, 0, 116, 1], + }), + }); + }), + }, + naturminne_punkt: { + legend: { zoomLevel: 6, type: 'point' }, + style: [undefined, undefined, undefined, undefined].concat([3, 4, 6, 8, 12, 16, 20, 24].map(function(width) { + return new Style({ + zIndex: 12, + image: new CircleStyle({ + radius: width, + fill: new Fill({ + color: 'rgba(113, 0, 116, .5)', + }), + stroke: new Stroke({ + width: Math.log2(width)/2, + color: 'rgba(113, 0, 116, 1)', + }), + }), + }); + })) + }, + interimistiskt_forbud: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(168, 0, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 11, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [168, 0, 0, 1], + }), + }); + }), + }, + fageldirektivet: { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width*2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(230, 0, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + patternContext.beginPath(); + patternContext.lineWidth *= 6; + patternContext.moveTo(-.5*patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -.5*patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 1.5*patternCanvas.height); + patternContext.lineTo(1.5*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 10, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [230, 0, 0, 1], + }), + }); + }), + }, + habitatdirektivet: { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width*2; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 77, 168, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + patternContext.beginPath(); + patternContext.lineWidth *= 6; + patternContext.moveTo(0, -.5*patternCanvas.height); + patternContext.lineTo(1.5*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-.5*patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 1.5*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 10, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [0, 77, 168, 1], + }), + }); + }), + }, + helcom: { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(130, 130, 130, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.5*patternCanvas.width, .5*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [130, 130, 130, 1], + }), + }); + }), + }, + ramsar: { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(195, 0, 255, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.25*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [195, 0, 255, 1], + }), + }); + }), + }, + ospar: { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.25*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [168, 0, 0, 1], + }), + }); + }), + }, + varldsarv: { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.75*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [168, 0, 0, 1], + }), + }); + }), + }, + biosfarsomraden: { + legend: { zoomLevel: 1 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(131, 0, 219, 1)'; + const r = z < 5 ? (z+1)*.75 : z*.5; + patternContext.beginPath(); + patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true); + patternContext.fill(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 9, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [131, 0, 219, 1], + }), + }); + }), + }, + naturvardsavtal: { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 21, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [255, 0, 197, 1], + }), + }); + }), + }, + naturvardsavtal_skogsstyrelsen: { + legend: { zoomLevel: 2 }, + style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 20, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 4 ? .5 : z <= 5 ? 1 : 2, + color: [255, 0, 197, 1], + }), + }); + }), + }, + atervatningsavtal: { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5, 6].map(function(width) { + return new Style({ + zIndex: 5, + fill: new Fill({ + color: [255, 115, 0, .4], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [255, 115, 0, 1], + }), + }); + }) + .concat([7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 115, 0, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 5, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), }), stroke: new Stroke({ - width: Math.log2(width)/2, - color: 'rgba(113, 0, 116, 1)', + width: 1.5, + color: [255, 115, 0, 1], }), - }), - }); - })) - }, - nvr_interimistiskt_forbud: { - popoverTitle: 'Interimistiskt förbud', - popover: [ - ['NVR-ID', 'NVRID', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Beslutsstatus', 'BESLSTATUS'], - ['Beslutsdatum (bildande)', 'URSBESLDAT'], - ['Ursprungligt gällandedatum', 'URSGALLDAT'], - ['Senaste gällandedatum', 'SENGALLDAT'], - ['Förvaltare', 'FORVALTARE'], - ['IUCN-kategori', 'IUCNKAT'], - ['Diarienummer', 'DIARIENR', { classes: ['feature-attr-dnr'] }], - ['Lagrum', 'LAGRUM'], - ['Beslutsmyndighet', 'BESLMYND'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(168, 0, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 11, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [168, 0, 0, 1], - }), - }); - }), - }, - nvr_fageldirektivet: { - popoverTitle: 'Fågeldirektivet (SPA)', - popover: [ - ['Områdeskod', 'SITE_CODE', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Områdestyp', 'OMRADESTYP'], - ['Uppgiftslämnare', 'UPPLAMNARE'], - ['SPA-datum', 'SPA_DATUM'], - ['SCI-förslagsdatum', 'SCI_FORSL'], - ['SCI-datum', 'SCI_DATUM'], - ['SAC-datum', 'SAC_DATUM'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Kvalitet', 'KVALITET'], - ['Kännetecken för området', 'KARAKTAR'], - ['Arter', 'ARTER'], - ['Naturtyper', 'NATURTYPER'], - ['Bevarandeplan', 'BEVPLAN', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width*2; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(230, 0, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - patternContext.beginPath(); - patternContext.lineWidth *= 6; - patternContext.moveTo(-.5*patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -.5*patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 1.5*patternCanvas.height); - patternContext.lineTo(1.5*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 10, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [230, 0, 0, 1], - }), - }); - }), - }, - nvr_habitatdirektivet: { - popoverTitle: 'Art- och habitatdirektivet (SCI)', - popover: [ - ['Områdeskod', 'SITE_CODE', { classes: ['feature-objid'] }], - ['Namn', 'NAMN'], - ['Områdestyp', 'OMRADESTYP'], - ['Uppgiftslämnare', 'UPPLAMNARE'], - ['SPA-datum', 'SPA_DATUM'], - ['SCI-förslagsdatum', 'SCI_FORSL'], - ['SCI-datum', 'SCI_DATUM'], - ['SAC-datum', 'SAC_DATUM'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Kvalitet', 'KVALITET'], - ['Kännetecken för området', 'KARAKTAR'], - ['Arter', 'ARTER'], - ['Naturtyper', 'NATURTYPER'], - ['Bevarandeplan', 'BEVPLAN', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width*2; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 77, 168, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - patternContext.beginPath(); - patternContext.lineWidth *= 6; - patternContext.moveTo(0, -.5*patternCanvas.height); - patternContext.lineTo(1.5*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-.5*patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 1.5*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 10, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 4 ? .5 : z <= 5 ? 1 : 2, - color: [0, 77, 168, 1], - }), - }); - }), - }, - nvr_helcom: { - popoverTitle: 'Marina skyddade områden (Helcom MPA)', - popover: [ - ['Namn', 'NAME'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(130, 130, 130, 1)'; - let r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.5*patternCanvas.width, .5*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [130, 130, 130, 1], - }), - }); - }), - }, - nvr_ramsar: { - popoverTitle: 'Ramsar-områden (Våtmarkskonventionen)', - popover: [ - ['Ramsar-ID', 'RAMSAR_ID', { classes: ['feature-objid'] }], - ['Skyddstyp', 'SKYDDSTYP'], - ['Namn', 'NAMN'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'LAND_HA', { unit: 'ha' }], - ['Areal vatten', 'VATTEN_HA', { unit: 'ha' }], - ['Skogsmarksareal', 'SKOG_HA', { unit: 'ha' }], - ['Ursprungligt beslutsdatum', 'URSPR_BESL'], - ['Senaste beslutsdatum', 'SEN_BESLUT'], - ['Rättsakt', 'LEGAL_ACT'], - ['Länk', 'LINK', { fn: function(v) { - if (v === undefined || v === null || v === '') { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(195, 0, 255, 1)'; - let r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.25*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [195, 0, 255, 1], - }), - }); - }), - }, - nvr_ospar: { - popoverTitle: 'Marina skyddade områden (Ospar MPA)', - popover: [ - ['Ursprung', 'ORIGIN'], - ['N2000-namn', 'NAMN_N2000'], - ['MPA-ID', 'MPA_ID', { classes: ['feature-objid'] }], - ['MPA-namn', 'MPA_NAMN'], - ['N2000-ID', 'N2000_SITE', { classes: ['feature-objid'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; - let r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.25*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [168, 0, 0, 1], - }), - }); - }), - }, - nvr_varldsarv: { - popoverTitle: 'Världsarv med mycket höga naturvärden (Unesco)', - popover: [ - ['Namn', 'NAMN'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(168, 0, 0, 1)'; - let r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.75*patternCanvas.width, .25*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [168, 0, 0, 1], - }), - }); - }), - }, - nvr_biosfarsomraden: { - popoverTitle: 'Biosfärsområde (Unesco)', - popover: [ - ['Namn', 'NAMN'], - ['Skyddstyp', 'SKYDDSTYP'], - ['Länk', 'LINK', { fn: function(v) { - if (v === undefined || v === null || v === '') { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(131, 0, 219, 1)'; - let r = z < 5 ? (z+1)*.75 : z*.5; - patternContext.beginPath(); - patternContext.arc(.75*patternCanvas.width, .75*patternCanvas.height, r, 0, 2*Math.PI, true) - patternContext.fill(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 9, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [131, 0, 219, 1], - }), - }); - }), - }, - nvr_naturvardsavtal: { - popoverTitle: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', - popover: [ - ['ID', 'ID', { classes: ['feature-objid'] }], - ['Namn', 'OBJNAMN'], - ['Fastighet', 'FASTBET', { classes: ['feature-objid'] }], - ['Giltig från', 'DATSTART'], - ['Giltig till', 'DATSLUT'], - ['Diarienummer', 'DIARIENRNV', { classes: ['feature-attr-dnr'] }], - ['Satus', 'STATUS'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 21, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 0, 197, 1], - }), - }); - }), - }, - nvr_naturvardsavtal_skogsstyrelsen: { - popoverTitle: 'Naturvårdsavtal (Skogsstyrelsen)', - popover: [ - ['Ärendebeteckning', 'Beteckn', { classes: ['feature-objid'] }], - ['Registeringsår', 'ArendeAr'], - ['Biotopkategori', 'NvaTyp'], - ['Skogstyp', 'Naturtyp'], - ['Avtalsdatum', 'DatAvtal'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Skogsmarksareal', 'AreaProd', { unit: 'ha' }], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Undertyp', 'Undertyp'], - ], - style: [4, 8, 16, 16, 32, 32, 64, 64, 64, 128, 128, 128].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(255, 0, 197, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 20, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [255, 0, 197, 1], - }), - }); - }), + }); + })), + }, }, - ri_naturvard: { - popoverTitle: 'Riksintresse naturvård', - popover: [ - ['Namn', 'NAMN'], - ['Skydd', 'SKYDD'], - ['Ämnesområde', 'AMNESOMRAD'], - ['Beskrivning', 'BESKRIVNIN', { fn: function(v) { - if (v === undefined || v === null || !(v.startsWith('http://') || v.startsWith('https://'))) { - return v; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Lagrum', 'LAGRUM'], - ['Beslutsdatum', 'BESLUTSDAT'], - ['Original-ID', 'ORGINALID', { classes: ['feature-objid'] }], - ['Riks-ID', 'RIKSID', { classes: ['feature-objid'] }], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(154, 230, 0, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [154, 230, 0, 1], - }), - }); - }), - }, - ri_friluftsliv: { - popoverTitle: 'Riksintresse friluftsliv', - popover: [ - ['Namn', 'NAMN'], - ['Skydd', 'SKYDD'], - ['Ämnesområde', 'AMNESOMR'], - ['Områdesnummer', 'OMRADESNR', { classes: ['feature-objid'] }], - ['Länk värdebeskrivning', 'LANK_VARDE', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Lagrum', 'LAGRUM'], - ['Beslutsdatum', 'BESLDATUM'], - ['Ärendenummer', 'ARENDENR', { classes: ['feature-attr-dnr'] }], - ['Länk, beslut', 'LANK_BESLU', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Aktivitet', 'AKTIVITET'], - ['Naturtyp', 'NATURTYP'], - ['Areal', 'geom_area', { fn: 'area' }], - ['Areal land', 'AREA_LAND_', { unit: 'ha' }], - ['Areal vatten', 'AREA_VATTE', { unit: 'ha' }], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(0, 127, 232, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, - color: [0, 127, 232, 1], - }), - }); - }), - }, - ri_rorligt_friluftsliv: { - popoverTitle: 'Riksintresse rörligt friluftsliv (MB 4 kap 1§ och 2§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - //['Metodbeskrivning', 'METODBESKR'], - //['Tillk. datum', 'TILLKDATUM'], - //['Rev. datum', 'REVDATUM'], - ['Anmärkning', 'ANM'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(187, 227, 212, .25)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(56, 151, 117, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, patternCanvas.height); - patternContext.lineTo(patternCanvas.width, -patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, 2*patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [56, 151, 117, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - ri_obruten_kust: { - popoverTitle: 'Riksintresse obruten kust (MB 4 kap 3§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - //['Metodbeskrivning', 'METODBESKR'], - //['Tillk. datum', 'TILLKDATUM'], - //['Rev. datum', 'REVDATUM'], - ['Anmärkning', 'ANM'], - ['Objekttyp', 'OBJTYP'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(227, 227, 187, .25)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(156, 158, 56, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [156, 158, 56, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - ri_obrutet_fjall: { - popoverTitle: 'Riksintresse obrutet fjäll (MB 4 kap 5§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - ['Metodbeskrivning', 'METODBESKR'], - ['Tillk. datum', 'TILLKDATUM'], - //['Rev. datum', 'REVDATUM'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = width; - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'rgba(255, 255, 209, .25)'; - patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); - patternContext.strokeStyle = 'rgba(219, 183, 60, 1)'; - patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(0, -patternCanvas.height); - patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); - patternContext.stroke(); - patternContext.moveTo(-patternCanvas.width, 0); - patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 8, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [219, 183, 60, 1], - lineDash: [width/4, width/3], - }), - }); - }), - }, - ri_skyddade_vattendrag: { - popoverTitle: 'Riksintresse skyddade vattendrag (MB 4 kap 6§)', - popover: [ - ['Namn', 'NAMN'], - //['Original-ID', 'ORIGINALID', { classes: ['feature-objid'] }], - ['Beskrivning', 'BESKRIVNIN'], - ['Metodbeskrivning', 'METODBESKR'], - ['Tillk. datum', 'TILLKDATUM'], - ['Rev. datum', 'REVDATUM'], - ['Anmärkning', 'ANM'], - ['Digitaliseringsskala', 'DIG_SKALA'], - ['Länk', 'OBJEKTLANK', { fn: function(v) { - if (v === null) { - return; - } - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ['Areal', 'geom_area', { fn: 'area' }], - ['Referens', 'REFERENS'], - ], - style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { - return new Style({ - zIndex: 8, - fill: new Fill({ - color: [102, 157, 240, .25], + nv: { + naturvarde_sks: { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5].map(function(width) { + return new Style({ + zIndex: 6, + fill: new Fill({ + color: [255, 170, 0, .2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [255, 170, 0, .8], + }), + }); + }) + .concat([6, 7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(255, 170, 0, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [255, 170, 0, 1], + }), + }); + })), + }, + nyckelbiotop: { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5].map(function(width) { + return new Style({ + zIndex: 6, + fill: new Fill({ + color: [217, 148, 9, .2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [217, 148, 9, .8], + }), + }); + }) + .concat([6, 7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [217, 148, 9, 1], + }), + }); + })), + }, + nyckelbiotop_storskogsbruk: { + legend: { zoomLevel: 0 }, + style: [0, 1, 2, 3, 4, 5].map(function(width) { + return new Style({ + zIndex: 6, + fill: new Fill({ + color: [217, 148, 9, .2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: .5, + color: [217, 148, 9, .8], + }), + }); + }) + .concat([6, 7, 8, 9, 10, 11].map(function() { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = 16; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(217, 148, 9, 1)'; + patternContext.lineWidth = 1; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: new Stroke({ + width: 1.5, + color: [217, 148, 9, 1], + }), + }); + })), + }, + sumpskog: { + legend: { zoomLevel: 5 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + const w = Math.max(1, width); + patternCanvas.width = z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 6 : 8; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(158, 200, 215, 1)'; + patternContext.lineWidth = w; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 5, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: w/2, + color: [158, 200, 215, 1], + }), + }); + }), + }, + pagaende_naturreservatsbildning: { + legend: { zoomLevel: 1 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.setLineDash([width/4, width/4]); + patternContext.strokeStyle = 'rgba(7, 181, 7, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(width/4, 0); + patternContext.lineTo(width/4, patternCanvas.height); + patternContext.stroke(); + patternContext.beginPath(); + patternContext.lineDashOffset = width/4; + patternContext.moveTo(3*width/4, 0); + patternContext.lineTo(3*width/4, patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 10, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 3 : 4, + color: [7, 181, 7, 1], + lineDash: [width/8, width/4], + }), + }); }), - stroke: width === 0 ? undefined : new Stroke({ - width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, - color: [41, 109, 197, 1], - lineDash: [width/4, width/3], + }, + snus: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width) { + return new Style({ + zIndex: 4, + fill: new Fill({ + color: [168,168,0,.2], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [168,77,0,.75], + }), + }); }), - }); - }), + }, }, - ren_betesomraden: { - popoverTitle: 'Samebyarnas betesområde', - popover: [ - ['Sameby', 'NAMN'], - ['Samebys typ', 'SAMEBY_TYP'], - ['Signatur', 'SIGNATUR'], - ['Aktualitet', 'AKTUALITET'], - ], - style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width, z) { - return new Style({ - zIndex: 4, - fill: new Fill({ - /* transparent fill so clicking the inside of the polygon triggers a popover */ - /* XXX could also use a custom renderer but that doesn't seem to work */ - color: [0, 0, 0, 0], - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], - }), - }); - }), - }, - ren_flyttled: { - popoverTitle: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', - popover: [ - ['Led-ID', 'LED_ID', { classes: ['feature-objid'], fn: (v) => v === 0 ? '' : v }], - ['Sameby #1', 'SAMEBY1'], - ['Sameby #2', 'SAMEBY2'], - ['Sameby #3', 'SAMEBY3'], - ['Beskrivning', 'BESKRIVNIN'], - ['Årstid', 'ARSTID'], - ['Riksintresse', 'RIKSINTR'], - ['Fast led', 'FAST_LED'], - ['Aktualitet', 'AKTUALITET'], - ['Signatur', 'SIGNATUR'], - ['Ledlängd', 'geom_length', { fn: 'length' }], - ], - style: [.75, 1, 1.5, 2, 3, 4, 5, 5, 6, 7, 8, 10].map(function(width, z) { - return new Style({ - zIndex: 7, - stroke: new Stroke({ - width: 2*width, - color: [119, 99, 59, 1], - lineDash: [4 * width], - }), - }); - }), - }, - ren_riks_ren: { - popoverTitle: 'Riksintresse rennäring', - popover: [ - ['Lagrum', 'LAGRUM'], - ['Aktualitet', 'AKTUALITET'], - ['Signatur', 'SIGNATUR'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { - const patternCanvas = document.createElement('canvas'); - const patternContext = patternCanvas.getContext('2d'); - patternCanvas.width = z < 4 ? 4 : z <= 5 ? 8 : Math.pow(2, Math.round(Math.log2(width) + 3)); - patternCanvas.height = patternCanvas.width; - patternContext.fillStyle = 'transparent'; - patternContext.strokeStyle = 'rgba(179, 153, 102, 1)'; - patternContext.lineWidth = Math.max(1, width/2); - patternContext.beginPath(); - patternContext.moveTo(0, 0); - patternContext.lineTo(patternCanvas.width, 0); - patternContext.stroke(); - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - return new Style({ - zIndex: 6, - fill: new Fill({ - color: context.createPattern(patternCanvas, 'repeat'), - }), - stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], - }), - }); - }), - }, - ren_omr_riks: { - popoverTitle: '(Kärn)områden av riksintresse rennäring', - popover: [ - ['Områdes-ID', 'OMR_NR', { classes: ['feature-objid'] }], - ['Länk', 'LANK'], - ['Årets runt', 'ARET_RUNT'], - ['Sameby', 'SAMEBY'], - ['Ansvarig', 'ANSVARIG'], - ['Aktualitet', 'AKTUALITET'], - ['Signatur', 'SIGNATUR'], - ['Areal', 'geom_area', { fn: 'area' }], - ], - style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { + ri: { + naturvard: { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(154, 230, 0, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [154, 230, 0, 1], + }), + }); + }), + }, + friluftsliv: { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(0, 127, 232, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 1 : z < 4 ? 2 : z <= 5 ? 4 : 8, + color: [0, 127, 232, 1], + }), + }); + }), + }, + rorligt_friluftsliv: { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(187, 227, 212, .25)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(56, 151, 117, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, patternCanvas.height); + patternContext.lineTo(patternCanvas.width, -patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, 2*patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [56, 151, 117, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + obruten_kust: { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(227, 227, 187, .25)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(156, 158, 56, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [156, 158, 56, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + obrutet_fjall: { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = width; + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'rgba(255, 255, 209, .25)'; + patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); + patternContext.strokeStyle = 'rgba(219, 183, 60, 1)'; + patternContext.lineWidth = z < 4 ? .5 : z <= 5 ? 1 : 2; + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(0, -patternCanvas.height); + patternContext.lineTo(2*patternCanvas.width, patternCanvas.height); + patternContext.stroke(); + patternContext.moveTo(-patternCanvas.width, 0); + patternContext.lineTo(patternCanvas.width, 2*patternCanvas.height); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 8, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [219, 183, 60, 1], + lineDash: [width/4, width/3], + }), + }); + }), + }, + skyddade_vattendrag: { + legend: { zoomLevel: 0 }, + style: [8, 16, 32, 32, 64, 64, 128, 128, 128, 256, 256, 256].map(function(width, z) { return new Style({ - zIndex: 5, + zIndex: 8, fill: new Fill({ - color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], + color: [102, 157, 240, .25], }), stroke: width === 0 ? undefined : new Stroke({ - width: width, - color: [179, 153, 102, 1], + width: z < 2 ? 2 : z < 4 ? 4 : z <= 5 ? 8 : 16, + color: [41, 109, 197, 1], + lineDash: [width/4, width/3], }), }); }), + }, }, - /* Documentation at - * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf - * */ - misc_dammar: { - popoverTitle: 'Damm', - popover: [ - ['Dammenhetens namn', 'DNamn'], - ['Dammanläggningens namn', 'Namn'], - ['Länsnr', 'LST_OBJID', { classes: ['feature-objid'] }], - ['Status', 'Status', { fn: (v) => v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : '' }], - //['Regleringstyp', 'Regleringstyp'], - ['Byggår', 'ByggAr'], - ['Dammhöjd', 'DammHojd', { unit: 'm' }], - ['Krönlängd', 'KronLangd', { unit: 'm' }], - ['Fiskväg', 'Fiskvag', { fn: (v) => - v === 1 ? 'Bassängtrappa' : - v === 2 ? 'Denilränna' : - v === 3 ? 'Slitsränna' : - v === 4 ? 'Omlöp' : - v === 5 ? 'Inlöp' : - v === 6 ? 'Ålledare' : - v === 7 ? 'Smoltränna' : - v === 8 ? 'Okänd typ' : - v === 9 ? 'Ingen' : - v === 10 ? 'Annan' : - '' }], - ['Huvudavrinningsområdesnummer', 'HARO', { classes: ['feature-objid'] } ], - ['Vattendistrikt', 'Vattendistrikt', { classes: ['feature-objid'] } ], - ['Verksamhet', 'Verksamhet', { fn: (v) => - v === 1 ? 'Kraftproduktion' : - v === 2 ? 'Industri' : - v === 3 ? 'Sjöfart' : - v === 4 ? 'Invallning' : - v === 5 ? 'Vattenförsörjning' : - v === 6 ? 'Spegeldamm' : - v === 7 ? 'Historisk' : - v === 8 ? 'Övrigt' : - '' }], - ['Högsta dämningsgräns', 'DG', { unit: 'm' }], - ['Lägsta sänkningsgräns', 'SG', { unit: 'm' }], - ['Magasinsyta', 'MY', { unit: 'km²' }], - ['Reglerbar volym', 'RV', { unit: 'Mm³' }], - ['Kommentar', 'Kommentar'], - ], - style: [2, 3, 4, 4, 4, 6, 8, 8, 8, 10, 16, 32].map(function(width) { - return new Style({ - zIndex: 59, - image: new CircleStyle({ - radius: width, + ren: { + betesomrade: { + legend: { zoomLevel: 0 }, + style: [1, 1.5, 2, 3, 3.5, 4, 5, 5, 6, 7, 8, 10].map(function(width) { + return new Style({ + zIndex: 4, fill: new Fill({ - color: 'rgb(219, 30, 42)', + /* transparent fill so clicking the inside of the polygon triggers a popover */ + /* XXX could also use a custom renderer but that doesn't seem to work */ + color: [0, 0, 0, 0], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [179, 153, 102, 1], }), + }); + }), + }, + flyttled: { + legend: { zoomLevel: 2, type: 'linestring' }, + style: [.75, 1, 1.5, 2, 3, 4, 5, 5, 6, 7, 8, 10].map(function(width) { + return new Style({ + zIndex: 7, stroke: new Stroke({ - width: Math.log2(width) * 2/5, - color: 'rgb(128, 17, 25)', + width: 2*width, + color: [119, 99, 59, 1], + lineDash: [4 * width], }), - }), - }); - }), + }); + }), + }, + riks_ren: { + legend: { zoomLevel: 1 }, + style: [.5, 1, 1.5, 1.5, 2, 2, 2.5, 2.5, 3, 3.5, 4, 5].map(function(width, z) { + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + patternCanvas.width = z < 4 ? 4 : z <= 5 ? 8 : Math.pow(2, Math.round(Math.log2(width) + 3)); + patternCanvas.height = patternCanvas.width; + patternContext.fillStyle = 'transparent'; + patternContext.strokeStyle = 'rgba(179, 153, 102, 1)'; + patternContext.lineWidth = Math.max(1, width/2); + patternContext.beginPath(); + patternContext.moveTo(0, 0); + patternContext.lineTo(patternCanvas.width, 0); + patternContext.stroke(); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + return new Style({ + zIndex: 6, + fill: new Fill({ + color: context.createPattern(patternCanvas, 'repeat'), + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [179, 153, 102, 1], + }), + }); + }), + }, + omr_riks: { + legend: { zoomLevel: 2 }, + style: [.5, .5, 1, 1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2].map(function(width, z) { + return new Style({ + zIndex: 5, + fill: new Fill({ + color: [203, 190, 163, Math.max((.3-.5)/8 * z + .5, 0)], + }), + stroke: width === 0 ? undefined : new Stroke({ + width: width, + color: [179, 153, 102, 1], + }), + }); + }), + }, }, - misc_gigafactories: { - popoverTitle: 'Stor industrisatsning', - popover: [ - ['Namn', 'Name'], - ['Länk', 'Url', { fn: function(v) { - const a = document.createElement('a'); - a.href = v; - a.target = '_blank'; - const i = document.createElement('i'); - i.classList.add('bi', 'bi-box-arrow-up-right'); - a.appendChild(i); - return a; - }}], - ], - style: [4, 6, 7, 8, 10, 12].map(function(width) { - return new Style({ - zIndex: 60, - image: new CircleStyle({ - radius: width, + misc: { + /* Documentation at + * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf + * */ + dammar: { + legend: { zoomLevel: 5, type: 'point' }, + style: [2, 3, 4, 4, 4, 6, 8, 8, 8, 10, 16, 32].map(function(width) { + return new Style({ + zIndex: 59, + image: new CircleStyle({ + radius: width, + fill: new Fill({ + color: 'rgb(219, 30, 42)', + }), + stroke: new Stroke({ + width: Math.log2(width) * 2/5, + color: 'rgb(128, 17, 25)', + }), + }), + }); + }), + }, + + gigafactories: { + legend: { zoomLevel: 1, type: 'point' }, + style: [4, 6, 7, 8, 10, 12].map(function(width) { + return new Style({ + zIndex: 60, + image: new CircleStyle({ + radius: width, + fill: new Fill({ + color: 'rgb(152, 78, 163)', + }), + stroke: new Stroke({ + width: Math.log2(width) * 2/5, + color: 'rgb(119, 61, 128)', + }), + }), + }); + }) + .concat([1.5, 2, 2, 2, 2, 2].map(function(width) { + return new Style({ + zIndex: 58, fill: new Fill({ - color: 'rgb(152, 78, 163)', + color: 'rgba(152, 78, 163, .4)', }), stroke: new Stroke({ - width: Math.log2(width) * 2/5, + width: width, color: 'rgb(119, 61, 128)', }), - }), - }); - }) - .concat([1.5, 2, 2, 2, 2, 2].map(function(width) { - return new Style({ - zIndex: 58, - fill: new Fill({ - color: 'rgba(152, 78, 163, .4)', - }), - stroke: new Stroke({ - width: width, - color: 'rgb(119, 61, 128)', - }), - }); - })), + }); + })), + }, + }, + + kskog: { + 1: { style: [ 56, 168, 0, .2] }, /* #1 Sannolikt kontinuitetsskog (preciserad) */ + 2: { style: [169, 0, 230, .2] }, /* #2 Sannolikt påverkad kontinuitetsskog (preciserad) */ + 3: { style: [152, 230, 0, .2] }, /* #3 Sannolikt kontinuitetsskog i fjällen (grövre precisering) */ + 4: { style: [ 76, 115, 0, .2] }, /* #4 Potentiell kontinuitetsskog (2015) */ + }, +}); + +/* process URL parameters (other than 'basemap') */ +const STYLES = Object.seal(Object.fromEntries(Object.entries(LAYERS).map(([k,ls]) => + [k, Object.seal(Object.fromEntries(Object.keys(ls).map((l) => [l, null])))]))); +(function() { + const view = MAP.getView(); + const params = new URLSearchParams(window.location.hash.substring(1)); + const x = parseFloat(params.get('x')), + y = parseFloat(params.get('y')), + z = parseFloat(params.get('z')); + if (!isNaN(x) && !isNaN(y)) { + view.setCenter([x, y]); + view.setZoom(isNaN(z) ? 1 : z); + } else { + /* center of the bbox of the Norrbotten and Västerbotten geometries */ + view.setCenter([694767.48, 7338176.57]); + view.setZoom(1); + const geolocation = new Geolocation({ + projection: view.getProjection(), + tracking: true, + }); + const evt_key = geolocation.on('change:position', function() { + const pos = geolocation.getPosition(); + if (pos == null) { + return; + } + /* ignore further geolocation position changes */ + unByKey(evt_key); + geolocation.setTracking(false); + + const params2 = new URLSearchParams(window.location.hash.substring(1)); + /* ignore geolocation result if coordinates have changed meanwhile */ + if (params2.has('x') || params2.has('y')) { + return; + } + /* ignore geolocation result if not within extent */ + if (EXTENT[0] > pos[0] || pos[0] > EXTENT[2] || EXTENT[1] > pos[1] || pos[1] > EXTENT[3]) { + return; + } + view.setCenter(pos); + params2.set('x', pos[0].toFixed(2).replace(TRAILING_ZEROES, '')); + params2.set('y', pos[1].toFixed(2).replace(TRAILING_ZEROES, '')); + if (!params2.has('z')) { + const accuracy = geolocation.getAccuracy(); + if (accuracy == null || accuracy < 0) { + view.setZoom(Math.max(view.getMinZoom(), 0)); + } else { + /* infer resolution from accuracy, up to zoom level 7 (8px/m) */ + const [width, height] = MAP.getSize(); + const res = 8. * accuracy / Math.min(width, height); + view.setResolution(Math.max(res, view.getResolutionForZoom(7))); + } + params2.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); + } + location.hash = '#' + params2.toString(); + }); + } + if (!params.has('layers') || (!params.get('layers').match(/^\s*$/) && + /* compat redirect/layer subst for old non-hierachical names */ + !params.get('layers').split(' ').some((l) => l.includes('.')))) { + params.set('layers', [ + 'svk.ledningar', + 'svk.stolpar', + 'svk.stationer', + 'svk.transmissionsnatsprojekt', + 'misc.gigafactories', + 'misc.dammar', + 'mrr.appr_ec', + 'mrr.appl_ec', + 'mrr.appr_ogd', + 'mrr.appl_ogd', + 'mrr.appr_met', + 'mrr.appl_met', + 'mrr.appr_dl', + 'vbk.area_current', + 'vbk.area_notcurrent', + ].join(' ')); + location.hash = '#' + params.toString(); + } + + /* map each known parameter to a callback processing its value */ + Object.entries({ + 'layers': function(value) { + const layersParams = value.split(' '); + Object.entries(LAYERS).forEach(([k, v]) => + Object.entries(v) + .filter(([l]) => layersParams.includes(k + '.' + l)) + .forEach(([l,x]) => STYLES[k][l] = x.style ?? null)); + }, + + 'age-filter': function(value) { + /* eslint-disable-next-line no-useless-escape */ + const m0 = /^([ +\-]?)([0-9]+)([dwmy])$/.exec(value); + if (m0 != null) { + /* handling relative parameter values add some complexity, but it's worth doing since + * users can bookmark the URL with e.g., age-filter=-1y and visit it later with the + * timeframe shifted forward, thus not having to reconfigure the age filter */ + ageFilterSettings.type = 'relative'; + ageFilterSettings.operator = (m0[1] === ' ' || m0[1] === '+' || m0[1] === '') ? '>=' + : m0[1] === '-' ? '<=' + : null; + ageFilterSettings.quantity = parseInt(m0[2], 10); + ageFilterSettings.unit = m0[3]; + ageFilterSettings.setupMinMax(); + ageFilterSettings._active = true; /* don't call the setter as it's not initialized yet */ + return; + } + const m1 = /^([0-9]{8})-([0-9]{8})$/.exec(value); /* YYYYMMDD */ + if (m1 != null) { + const parse_date = (m) => new Date( + parseInt(m.slice(0,4), 10), + parseInt(m.slice(4,6), 10)-1, + parseInt(m.slice(6,8), 10), + 12 /* use 12:00:00.0 like for the <input type="date"> */ + ); + ageFilterSettings.type = 'interval'; + ageFilterSettings.from = parse_date(m1[1]); + ageFilterSettings.to = parse_date(m1[2]); + ageFilterSettings.setupMinMax(); + ageFilterSettings._active = true; /* don't call the setter as it's not initialized yet */ + return; + } + //console.log(`Ignoring invalid value for 'age-filter' parameter: ${value}`); + }, + 'show-unknown-age': function(value) { + if (value === '0') { + ageFilterSettings.show_unknown = false; + } else if (value === '1') { + ageFilterSettings.show_unknown = true; + } + }, + }) + .forEach(function([param, cb]) { + if (params.has(param)) { + cb(params.get(param)); + } + }); +})(); + +MAP.getView().on('change', function(event) { + const view = event.target; + disposePopover(); + + const coordinates = view.getCenter(); + const searchParams = new URLSearchParams(location.hash.substring(1)); + searchParams.set('x', coordinates[0].toFixed(2).replace(TRAILING_ZEROES, '')); + searchParams.set('y', coordinates[1].toFixed(2).replace(TRAILING_ZEROES, '')); + searchParams.set('z', view.getZoom().toFixed(3).replace(TRAILING_ZEROES, '')); + location.hash = '#' + searchParams.toString(); +}); + +/* add layers to the map */ +const mapLayers = (function() { + const tileGrid = createXYZ({ + extent: EXTENT, + tileSize: 1024, + maxResolution: 1024, /* = 1048576/1024 */ + minZoom: 0, + maxZoom: 7, + }); + const isVisible = function(k) { + const styles = STYLES[k]; + return Object.keys(LAYERS[k]).some((l) => styles[l] != null); + }; + const canWebGL2 = !!document.createElement('canvas').getContext('webgl2'); + + /* Note: layers are added in the order below, so leave SvK and + * misc at the end so they show up on top of suface features */ + const rasterLayers = ['kskog']; + const vectorLayers = ['adm', 'nv', 'mrr', 'skydd', 'ren', 'ri', 'avverk', 'vbk', 'svk', 'misc']; + const canFilterByAge = ['avverk', 'mrr', 'vbk']; /* layers for which features are dated */ + + const ret = {}; + if (!canWebGL2) { + rasterLayers.forEach((k) => ret[k] = null); + } else { + rasterLayers.forEach(function(k) { + const baseurl = new URL('/raster/' + k + '/', window.location.toString()).toString(); + const source = new GeoTIFF({ + sources: [{ url: baseurl + encodeURIComponent(k) + '.tiff' }], + normalize: false, + convertToRGB: false, + wrapX: false, + interpolate: false, + /* use the projection found in the source's metadata */ + }); + /* GeoTIFF doesn't allow retrieving the URL later, so we manually store the baseurl instead */ + source.set('baseurl', baseurl, true); + ret[k] = new TileLayerGL({ + /* Naturvårdsverket has a WMS server we could use instead, but by serving it ourselves + * we can filter on he various kskog classes */ + source: source, + visible: false, + style: null, /* filled later */ + }); + MAP.addLayer(ret[k]); + }); } -}; + vectorLayers.forEach(function(k) { + const canFilterByAge0 = canFilterByAge.includes(k); + const styles = STYLES[k]; + const baseurl = new URL('/tiles/' + k + '/', window.location.toString()).toString(); + const source = new VectorTile({ + url: baseurl + '{z}/{x}/{y}.pbf', + format: new MVT(), + projection: PROJECTION, + wrapX: false, + transition: 0, + tileGrid: tileGrid, + }); + source.set('baseurl', baseurl, true); + ret[k] = new VectorTileLayer({ + source: source, + /* XXX switch to 'hybrid' if there are perf issues; but that seems to + * put lines above points regardless of their respective z-index */ + renderMode: 'hybrid', + declutter: false, + visible: isVisible(k), + style: function(feature, resolution) { + /* WARN: very hot code path! */ + const properties = feature.getProperties(); + if (ageFilterSettings.active) { + /* TODO avoid doing this checks for each feature; instead, set up a + * different style function if ageFilterSettings.active */ + const ts = properties.ts; + if (ts == null) { + if (canFilterByAge0 && !ageFilterSettings.show_unknown) { + return; + } + } else if ((ageFilterSettings._min_ts !== null && ts < ageFilterSettings._min_ts) || + (ageFilterSettings._max_ts !== null && ts > ageFilterSettings._max_ts)) { + return; + } + } + const style = styles[properties.layer]; + if (!Array.isArray(style)) { + return style; + } else { + const maxi = style.length - 1; + const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); + /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ + const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); + // console.log(`resolution=${resolution}, z=${z}, i=${i}`); + return style[i]; + } + } + }); + ret[k].set('layerGroup', k, true); + ret[k].set('canFilterByAge', canFilterByAge0, true); + MAP.addLayer(ret[k]); + }); + return ret; +})(); + +/* layer hierarchy, for the layer selection, legend and info modal */ const layerHierarchy = [ { text: 'Transmissionsnät för el', children: [ { text: 'Kraftledningar (befintliga)', - layer: ['svk_ledningar', 'svk_stolpar'], + layer: ['svk.stolpar', 'svk.ledningar'], }, { text: 'Stationer', - layer: 'svk_stationer', + layer: 'svk.stationer', }, { text: 'Transmissionsnätsprojekt ', - layer: 'svk_transmissionsnatsprojekt', + layer: 'svk.transmissionsnatsprojekt', }, ], }, { text: 'Stora industrisatsningar', - layer: 'misc_gigafactories', + layer: 'misc.gigafactories', }, { text: 'Dammar', - layer: 'misc_dammar', + layer: 'misc.dammar', }, { text: 'Mineralrättigheter', @@ -2911,11 +3065,11 @@ const layerHierarchy = [ children: [ { text: 'Beviljad', - layer: 'mrr_appr_ec', + layer: 'mrr.appr_ec', }, { text: 'Ansökt', - layer: 'mrr_appl_ec', + layer: 'mrr.appl_ec', }, ], }, @@ -2924,11 +3078,11 @@ const layerHierarchy = [ children: [ { text: 'Beviljad', - layer: 'mrr_appr_ogd', + layer: 'mrr.appr_ogd', }, { text: 'Ansökt', - layer: 'mrr_appl_ogd', + layer: 'mrr.appl_ogd', }, ], }, @@ -2937,17 +3091,17 @@ const layerHierarchy = [ children: [ { text: 'Beviljad', - layer: 'mrr_appr_met', + layer: 'mrr.appr_met', }, { text: 'Ansökt', - layer: 'mrr_appl_met', + layer: 'mrr.appl_met', }, ], }, { text: 'Markanvisningar till koncession', - layer: 'mrr_appr_dl', + layer: 'mrr.appr_dl', }, ], }, @@ -2955,48 +3109,89 @@ const layerHierarchy = [ text: 'Vindbruk', children: [ { - text: 'Projekteringsområden', + text: 'Landbaserade projekteringsområden', children: [ { text: 'Aktuella', - layer: 'vbk_area_current', + layer: 'vbk.area_current', }, { text: 'Ej aktuella', - layer: 'vbk_area_notcurrent', + layer: 'vbk.area_notcurrent', }, ], }, { - text: 'Vindkraftverk', + text: 'Landbaserade vindkraftverk', children: [ { text: 'Uppförda', - layer: 'vbk_station_completed', + layer: 'vbk.turbine_completed', + }, + { + text: 'Beviljade', + layer: 'vbk.turbine_approved', + }, + { + text: 'Avslagna/nekad', + layer: 'vbk.turbine_rejected', }, { text: 'Handläggs', - layer: 'vbk_station_processed', + layer: 'vbk.turbine_processed', }, { - text: 'Beviljade', - layer: 'vbk_station_approved', + text: 'Nedmonterade', + layer: 'vbk.turbine_dismounted', + }, + { + text: 'Överklagade', + layer: 'vbk.turbine_appealed', }, { text: 'Inte längre aktuella/återkallade', - layer: 'vbk_station_revoked', + layer: 'vbk.turbine_revoked', }, + ], + }, + { + text: 'Havsbaserad vindkraft', + children: [ { - text: 'Avslagna/nekad', - layer: 'vbk_station_rejected', + text: 'Uppförd', + layer: 'vbk.offshore_completed', }, { - text: 'Nedmonterade', - layer: 'vbk_station_dismounted', + text: 'Tillståndsansökan beviljad', + layer: 'vbk.offshore_approved', }, { - text: 'Överklagade', - layer: 'vbk_station_appealed', + text: 'Ändringsansökan', + layer: 'vbk.offshore_amended', + }, + { + text: 'Tillståndsansökan avslagen', + layer: 'vbk.offshore_rejected', + }, + { + text: 'Överklagad', + layer: 'vbk.offshore_appealed', + }, + { + text: 'Tillståndsansökan inlämnad', + layer: 'vbk.offshore_applied', + }, + { + text: 'Samråd inför tillståndsansökan', + layer: 'vbk.offshore_consultation', + }, + { + text: 'Inledande undersökningar', + layer: 'vbk.offshore_investigation', + }, + { + text: 'Inte längre aktuell/återkallad', + layer: 'vbk.offshore_revoked', }, ], }, @@ -3006,12 +3201,12 @@ const layerHierarchy = [ text: 'Skogsbruk', children: [ { - text: 'Uppförda (sedan 2000)', - layer: 'sks_clearcut_comp', + text: 'Utförda avverkningar', + layer: 'avverk.utford', }, { - text: 'Anmälda', - layer: 'sks_clearcut_appl', + text: 'Avverkningsanmälningar', + layer: 'avverk.anmald', }, ] }, @@ -3019,59 +3214,59 @@ const layerHierarchy = [ text: 'Skyddad natur', children: [ { - text: 'Nationella skyddsformer', + text: 'Nationella skyddsformer från Naturvårdsregistret', children: [ { text: 'Tillträdesförbud', - layer: 'nvr_tilltradesforbud', + layer: 'skydd.tilltradesforbud', }, { text: 'Nationalpark', - layer: 'nvr_nationalpark', + layer: 'skydd.nationalpark', }, { text: 'Naturreservat', - layer: 'nvr_naturreservat', + layer: 'skydd.naturreservat', }, { text: 'Kommunala naturreservat', - layer: 'nvr_naturreservat_kommunalt', + layer: 'skydd.naturreservat_kommunalt', }, { text: 'Naturvårdsområden', - layer: 'nvr_naturvardsomrade', + layer: 'skydd.naturvardsomrade', }, { text: 'Djur- och växtskyddsområden', - layer: 'nvr_djur_och_vaxtskyddsomrade', + layer: 'skydd.djur_och_vaxtskyddsomrade', }, { text: 'Kulturreservat', - layer: 'nvr_kulturreservat', + layer: 'skydd.kulturreservat', }, { text: 'Vattenskyddsområden', - layer: 'nvr_vattenskyddsomrade', + layer: 'skydd.vattenskyddsomrade', }, { text: 'Landskapsbildsskyddsområden', - layer: 'nvr_landskapsbildsskyddsomrade', + layer: 'skydd.landskapsbildsskyddsomrade', }, { text: 'Skogliga biotopskyddsområden', - layer: 'nvr_skogligt_biotopskyddsomrade', + layer: 'skydd.skogligt_biotopskyddsomrade', }, { text: 'Övriga biotopskyddsområden', - layer: 'nvr_ovrigt_biotopskyddsomrade', + layer: 'skydd.ovrigt_biotopskyddsomrade', }, { text: 'Naturminne', - layer: [ 'nvr_naturminne_yta', 'nvr_naturminne_punkt' ], + layer: [ 'skydd.naturminne_yta', 'skydd.naturminne_punkt' ], }, { text: 'Interimistiskt förbud', - layer: 'nvr_interimistiskt_forbud', + layer: 'skydd.interimistiskt_forbud', }, ], }, @@ -3080,11 +3275,11 @@ const layerHierarchy = [ children: [ { text: 'Fågeldirektivet (SPA)', - layer: 'nvr_fageldirektivet', + layer: 'skydd.fageldirektivet', }, { text: 'Art- och habitatdirektivet (SCI)', - layer: 'nvr_habitatdirektivet', + layer: 'skydd.habitatdirektivet', }, ], }, @@ -3093,38 +3288,92 @@ const layerHierarchy = [ children: [ { text: 'Marina skyddade områden (Helcom MPA)', - layer: 'nvr_helcom', + layer: 'skydd.helcom', }, { text: 'Ramsar-områden (Våtmarkskonventionen)', - layer: 'nvr_ramsar', + layer: 'skydd.ramsar', }, { text: 'Marina skyddade områden (Ospar MPA)', - layer: 'nvr_ospar', + layer: 'skydd.ospar', }, { text: 'Världsarv med mycket höga naturvärden (UNESCO)', - layer: 'nvr_varldsarv', + layer: 'skydd.varldsarv', }, { text: 'Biosfärsområden (UNESCO)', - layer: 'nvr_biosfarsomraden', + layer: 'skydd.biosfarsomraden', }, ], }, + { + text: 'Naturvårdsavtal', + children: [ + { + text: 'Naturvårdsverket, Länsstyrelserna', + layer: 'skydd.naturvardsavtal', + }, + { + text: 'Skogsstyrelsen', + layer: 'skydd.naturvardsavtal_skogsstyrelsen', + } + ] + }, + { + text: 'Återvätningsavtal', + layer: 'skydd.atervatningsavtal', + } ] }, { - text: 'Naturvårdsavtal', + text: 'Skogliga värden', children: [ { - text: 'Naturvårdsverket, Länsstyrelserna', - layer: 'nvr_naturvardsavtal', + text: 'Objekt med naturvärden (Skogsstyrelsen)', + layer: 'nv.naturvarde_sks', + }, + { + text: 'Nyckelbiotoper', + layer: 'nv.nyckelbiotop', + }, + { + text: 'Nyckelbiotoper storskogsbruket', + layer: 'nv.nyckelbiotop_storskogsbruk', + }, + { + text: 'Sumpskogar', + layer: 'nv.sumpskog', }, { - text: 'Skogsstyrelsen', - layer: 'nvr_naturvardsavtal_skogsstyrelsen', + text: 'Pågående naturreservatsbildning', + layer: 'nv.pagaende_naturreservatsbildning', + }, + { + text: 'Skyddsvärda statliga skogar', + layer: 'nv.snus', + }, + { + text: 'Sannolikt och potentiell kontinuitetsskog', + children: [ + { + text: 'Sannolikt kontinuitetsskog (preciserad)', + layer: 'kskog.1', + }, + { + text: 'Sannolikt påverkad kontinuitetsskog (preciserad)', + layer: 'kskog.2', + }, + { + text: 'Sannolikt kontinuitetsskog i fjällen (grövre precisering)', + layer: 'kskog.3', + }, + { + text: 'Potentiell kontinuitetsskog (2015)', + layer: 'kskog.4', + }, + ], } ] }, @@ -3133,27 +3382,27 @@ const layerHierarchy = [ children: [ { text: 'Naturvård', - layer: 'ri_naturvard', + layer: 'ri.naturvard', }, { text: 'Friluftsliv', - layer: 'ri_friluftsliv', + layer: 'ri.friluftsliv', }, { text: 'Rörligt friluftsliv', - layer: 'ri_rorligt_friluftsliv', + layer: 'ri.rorligt_friluftsliv', }, { text: 'Obruten kust', - layer: 'ri_obruten_kust', + layer: 'ri.obruten_kust', }, { text: 'Obrutet fjäll', - layer: 'ri_obrutet_fjall', + layer: 'ri.obrutet_fjall', }, { text: 'Skyddade vattendrag', - layer: 'ri_skyddade_vattendrag', + layer: 'ri.skyddade_vattendrag', }, ] }, @@ -3164,112 +3413,172 @@ const layerHierarchy = [ children: [ { text: 'Betesområden', - layer: 'ren_betesomraden', + layer: 'ren.betesomrade', }, { text: 'Flyttled', - layer: 'ren_flyttled', + layer: 'ren.flyttled', }, { text: 'Riksintressen', - layer: 'ren_riks_ren', + layer: 'ren.riks_ren', }, { text: '(Kärn)områden av riksintresse', - layer: 'ren_omr_riks', + layer: 'ren.omr_riks', }, ] }, + { + text: 'Administrativa gränser', + type: 'switch', + collapse_children: true, + children: [ + { + text: 'Länsgränser', + layer: 'adm.lansyta', + }, + { + text: 'Kommungränser', + layer: 'adm.kommunyta', + }, + ], + }, ]; -const styles = (function() { - const searchParams = new URLSearchParams(location.hash.substring(1)); - const layersParams = searchParams.has('layers') ? searchParams.get('layers').split(' ') : []; - return Object.keys(layers).reduce(function(result, key) { - if (layersParams.includes(key)) { - result[key] = layers[key].style; +/* legend panel */ +(function() { + const modal = document.getElementById('map-legend-panel'); + modal.classList.add('modal'); + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-hidden', 'true'); + + const content = document.createElement('div'); + modal.appendChild(content); + content.classList.add('modal-content'); + + const body = document.createElement('div'); + content.appendChild(body); + body.classList.add('modal-body'); + + const createLegend = function(ul, elem, classes) { + const li = document.createElement('li'); + li.classList.add('list-group-item'); + ul.appendChild(li); + + const t = document.createTextNode(elem.text); + if (elem.layer === undefined) { + li.appendChild(t); + li.classList.add('map-legend-header'); + } else { + li.classList.add('d-flex', 'flex-row'); + const div = document.createElement('div'); + div.classList.add('map-legend-symbol'); + li.appendChild(div); + const parent_height = 24; /* see CSS */ + const [width, height] = [32, 20]; + const symbols = { + polygon: new Polygon([[ + [0, (parent_height-height)/2], + [width, (parent_height-height)/2], + [width, (parent_height+height)/2], + [0, (parent_height+height)/2], + [0, (parent_height-height)/2], + ]]), + linestring: new LineString([ + [0, parent_height/2], + [width, parent_height/2] + ]), + point: new Point( + [width/2, parent_height/2] + ), + }; + let canvas, render; + (Array.isArray(elem.layer) ? elem.layer : [elem.layer]) + .forEach(function(layer) { + /* add symbols for each layer */ + const layerGroup = layer.split('.', 1).pop(); + const layerName = layer.slice(layerGroup.length + 1); + if (mapLayers[layerGroup] == null || LAYERS[layerGroup]?.[layerName]?.style == null) { + console.log(`Could not find symbol for layer ${layer}, skipping`); + return; + } + const legend = LAYERS[layerGroup][layerName]?.legend; + if (legend === null) { + return; /* layer has opted out from legend */ + } + if (canvas == null || !legend?.reuse_canvas) { + canvas = document.createElement('canvas'); + div.appendChild(canvas); + render = toContext(canvas.getContext('2d'), + { size:[width, parent_height] } + ); + } + + if (mapLayers[layerGroup].getSource() instanceof GeoTIFF) { + /* raster source */ + render.setFillStrokeStyle(new Fill({ + color: LAYERS[layerGroup][layerName].style, + })); + return render.drawGeometry(symbols.polygon); + } + else if (mapLayers[layerGroup].getSource() instanceof VectorTile) { + /* vector source */ + const style = Array.isArray(LAYERS[layerGroup][layerName].style) ? + LAYERS[layerGroup][layerName].style[legend?.zoomLevel ?? 5] : + LAYERS[layerGroup][layerName].style; + const legend_type = legend?.type ?? 'polygon'; + if (legend_type === 'point' && style.getImage(1) instanceof Icon && style.getImage(1).getSrc()) { + /* use a new <img> element since .setStyle() returns the same one and doesn't work in that case */ + const div2 = document.createElement('div'); + const img = document.createElement('img'); + div2.appendChild(img); + img.src = style.getImage(1).getSrc(); + img.height = height; + div.replaceChild(div2, canvas); + } else { + const style2 = style.clone(); + const stroke = style2.getStroke(); + if (legend_type === 'polygon' && stroke && stroke.getWidth() > 1) { + /* don't stroke too wide to avoid spilling over boxes above/below */ + stroke.setWidth(1); + } + render.setStyle(style2); + return render.drawGeometry(symbols[legend_type]); + } + } + else { + throw new Error(`Cannot show legend for ${layer}`); + } + }); + const span = document.createElement('div'); + span.appendChild(t); + li.appendChild(span); } - return result; - }, {}); -})(); -const [vectorLayers, featureOverlayLayer] = (function() { - const xyz = '/{z}/{x}/{y}.pbf'; - const tileGrid = createXYZ({ - extent: extent, - tileSize: 1024, - maxResolution: 1024, /* = 1048576/1024 */ - minZoom: 0, - maxZoom: 7, + elem._legend = li; + if (elem.children != null && elem.children.length > 0) { + if (classes.length > 0) { + li.classList.add(classes[0]); + classes = classes.slice(1); + } + const ul2 = document.createElement('ul'); + ul2.classList.add('list-group', 'list-group-flush'); + ul.appendChild(ul2); + elem.children.forEach((elem2) => createLegend(ul2, elem2, classes)); + } + }; + + const ul = document.createElement('ul'); + ul.classList.add('list-group', 'list-group-flush'); + body.appendChild(ul); + layerHierarchy.forEach(function(x) { + createLegend(ul, x, ['h4', 'h5', 'h6']); }); - return [ - Object.fromEntries( - ['mrr', 'nvr', 'ren', 'ri', 'sks', 'svk', 'vbk', 'misc'] - .map(function(k) { - let visible = false; - Object.keys(layers).forEach(function(lyr) { - if (lyr.startsWith(k + '_')) { - visible ||= styles[lyr] !== undefined; - } - }); - const vectorLayer = new VectorTileLayer({ - source: new VectorTile({ - url: '/tiles/' + k + xyz, - format: new MVT(), - projection: projection, - wrapX: false, - transition: 0, - tileGrid: tileGrid, - }), - /* XXX switch to 'hybrid' if there are perf issues; but that seems to - * put lines above points regardless of their respective z-index */ - renderMode: 'hybrid', - declutter: false, - visible: visible, - style: function(feature, resolution) { - const style = styles[k + '_' + feature.getProperties().layer]; - if (!Array.isArray(style)) { - return style; - } else { - const maxi = style.length - 1; - const z = 10 /* Math.log2(maxResolution) */ - Math.log2(resolution); - /* use Math.floor() as VectorTile.js calls getZForResolution(resolution, 1) */ - const i = z <= 0 ? 0 : z >= maxi ? maxi : Math.floor(z); - // console.log(`resolution=${resolution}, z=${z}, i=${i}`); - return style[i]; - } - }}); - vectorLayer.set('layerGroup', k, true); - map.addLayer(vectorLayer); - return [k, vectorLayer]; - })), - - /* We use a vector tile layer for featureOverlayLayer instead of a simple - * vector layer overlay, since we don't want to clip selected geometries at - * tile boundaries. It sounds overkill to load an entire tile layer to - * display a single feature, but this shouldn't cause much overhead in - * practice (the tiles are most likely cached already). */ - new VectorTileLayer({ - source: new VectorTile({ - urls: [], - format: new MVT(), - projection: projection, - wrapX: false, - transition: 0, - tileGrid: tileGrid, - }), - zIndex: 65535, - declutter: false, - visible: false, - renderMode: 'vector', - map: map, - style: null, - }), - ]; })(); - /* layer selection panel */ +const infoMetadataAccordions = []; (function() { const modal = document.getElementById('layer-selection-panel'); modal.classList.add('modal'); @@ -3294,7 +3603,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { elem._layers = elem.layer === undefined ? [] : Array.isArray(elem.layer) ? elem.layer : [elem.layer]; - if (elem.children !== undefined && elem.children.length > 0) { + if (elem.children != null && elem.children.length > 0) { collectLayers(elem.children); elem.children.forEach(function(child) { child._layers.forEach((l) => elem._layers.push(l)); @@ -3305,55 +3614,82 @@ const [vectorLayers, featureOverlayLayer] = (function() { const setIndeterminateAndChecked = function(list) { return list.forEach(function(elem) { - const layerStyles = elem._layers.map((lyr) => styles[lyr] !== undefined); + const layerStyles = elem._layers.map(function(lyr) { + const layerGroup = lyr.split('.', 1).pop(); + const layerName = lyr.slice(layerGroup.length + 1); + return STYLES[layerGroup]?.[layerName] != null; + }); elem._input.indeterminate = elem._layers.length <= 1 ? false : layerStyles.slice(1).some((v) => v !== layerStyles[0]); - if (!elem._input.indeterminate) { + if (elem._input.indeterminate) { + elem._legend.classList.remove('d-none'); + } else { /* keep checked value if indeterminate */ - elem._input.checked = layerStyles.every((v) => v); + const checked = layerStyles.every((v) => v); + elem._input.checked = checked; + if (checked) { + elem._legend.classList.remove('d-none'); + } else { + elem._legend.classList.add('d-none'); + } } - if (elem.children !== undefined && elem.children.length > 0) { + if (elem.children != null && elem.children.length > 0) { setIndeterminateAndChecked(elem.children); } }); }; const fixLayerVisibility = function() { - const result = {} - Object.keys(layers).forEach(function(lyr) { - const layerGroup = lyr.split('_', 1)[0]; - if (result[layerGroup] === undefined) { - result[layerGroup] = false; + Object.entries(mapLayers).forEach(function([k,lyr]) { + /* set palette for raster layers */ + if (lyr != null && lyr.getSource() instanceof GeoTIFF) { + const styles = STYLES[k] ?? {}; + const nodata = [0, 0, 0, .0]; + const indices = Object.keys(LAYERS[k]).map((v) => [parseInt(v, 10), v]); + const n = Math.max(...indices.map(([i]) => i)); + const palette = new Array(n+1).fill(nodata); + indices.forEach(([i,l]) => palette[i] = styles[l] ?? nodata); + /* XXX unfortunately calling .setStyle() makes the layer blink; using style variabse would + * be nicer, but `['palette', ['band', 1], [['var', 'val1'], …]]` fails as each palette index + * needs to be a color literal, even when `['var', 'val1']` evaluates to such, cf. + * https://github.com/openlayers/openlayers/blob/v10.6.0/src/ol/expr/expression.js#L976 */ + lyr.setStyle({ color: ['palette', ['band', 1], palette] }); } - result[layerGroup] ||= styles[lyr] !== undefined; }); - Object.entries(result).forEach(function([lyr, visible]) { - const v = vectorLayers[lyr]; - if (v !== undefined) { - //console.log(lyr, visible); - v.setVisible(visible); - } + Object.entries(LAYERS).forEach(function([k,v]) { + const styles = STYLES[k]; + const visible = Object.keys(v).some((l) => styles[l] != null); + // console.log(k, visible); + mapLayers[k]?.setVisible(visible); }); + const btn = document.getElementById('map-legend-button'); + if (Object.values(STYLES).some((v) => Object.values(v).some((v2) => v2 != null))) { + btn.classList.remove('disabled'); + } else { + btn.classList.add('disabled'); + } }; const onClickFunction = function(layerList, event) { - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - const popover = Popover.getInstance(popup); - if (popover !== null) { - popover.dispose(); - } + disposePopover(); const searchParams = new URLSearchParams(location.hash.substring(1)); let layersParams = searchParams.get('layers') || ''; layersParams = layersParams.match(/^\s*$/) ? [] : layersParams.split(' '); if (event.target.checked) { layerList.forEach(function(lyr) { - styles[lyr] = layers[lyr].style; - if (!layersParams.includes(lyr)) { - layersParams.push(lyr); + const layerGroup = lyr.split('.', 1).pop(); + if (mapLayers[layerGroup] != null) { + /* keep unexisting layers (eg WebGL layers on a system without WebGL support) unselectable */ + const layerName = lyr.slice(layerGroup.length + 1); + STYLES[layerGroup][layerName] = LAYERS[layerGroup][layerName]?.style ?? null; + if (!layersParams.includes(lyr)) { + layersParams.push(lyr); + } } }); } else { layerList.forEach(function(lyr) { - delete styles[lyr]; + const layerGroup = lyr.split('.', 1).pop(); + const layerName = lyr.slice(layerGroup.length + 1); + STYLES[layerGroup][layerName] = null; }); layersParams = layersParams.filter((lyr) => !layerList.includes(lyr)); } @@ -3361,9 +3697,9 @@ const [vectorLayers, featureOverlayLayer] = (function() { fixLayerVisibility(); layerList - .map((l) => l.split('_', 1)[0]) - .filter((v, i, arr) => arr.indexOf(v) === i) - .forEach((l) => vectorLayers[l].getSource().changed()); + .map((l) => l.split('.', 1)[0]) + .filter((v, i, arr) => mapLayers[v] != null && arr.indexOf(v) === i) + .forEach((l) => mapLayers[l].getSource().changed()); searchParams.set('layers', layersParams.join(' ')); location.hash = '#' + searchParams.toString(); @@ -3372,7 +3708,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { let layerId = 0; const addAccordionGroup = function(parentNode, children) { const ul = document.createElement('ul'); - parentNode.appendChild(ul); + parentNode?.appendChild?.(ul); ul.classList.add('list-group', 'list-group-flush'); children.forEach(function(child) { @@ -3398,7 +3734,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { const textNode = document.createTextNode(child.text); label.appendChild(textNode); - if (child.children !== undefined && child.children.length > 0) { + if (child.children != null && child.children.length > 0) { addAccordionGroup(li, child.children); } @@ -3453,8 +3789,18 @@ const [vectorLayers, featureOverlayLayer] = (function() { const text0 = document.createTextNode(x.text); label0.appendChild(text0); - if (x.children === undefined || x.children.length === 0) { - item.replaceChild(span0, header); + if (x.children == null || x.children.length === 0 || x.collapse_children) { + span0.removeAttribute('data-bs-toggle'); + span0.removeAttribute('data-bs-target'); + item.replaceChildren(span0); + if (x.type === 'switch') { + span0.classList.add('form-switch'); + input0.setAttribute('role', 'switch'); + } + if (x.children != null && x.children.length > 0) { + /* create inputs for the hash param logic but don't add them to the panel */ + addAccordionGroup(null, x.children); + } } else { const body = document.createElement('div'); collapse.appendChild(body); @@ -3492,44 +3838,1411 @@ const [vectorLayers, featureOverlayLayer] = (function() { label.setAttribute('for', input.id); label.innerHTML = 'Nedtonad bakgrund karta'; - input.checked = baseMapLayer === 'topowebb_nedtonad'; + input.checked = BASEMAP.layer === 'topowebb_nedtonad'; input.onchange = function(event) { - baseMapLayer = event.target.checked ? 'topowebb_nedtonad' : 'topowebb'; - baseMapSource.setUrl(`https://minkarta.lantmateriet.se/map/topowebbcache?LAYER=${encodeURIComponent(baseMapLayer)}`); - - const searchParams = new URLSearchParams(location.hash.substring(1)); - searchParams.set('basemap', baseMapLayer); - location.hash = '#' + searchParams.toString(); + BASEMAP.layer = event.target.checked ? 'topowebb_nedtonad' : 'topowebb'; }; })(); + + (function() { + const accordion = document.getElementById('info-accordion'); + layerHierarchy.forEach(function(x, idx) { + const item = document.createElement('div'); + accordion.appendChild(item); + item.classList.add('accordion-item'); + + const header = document.createElement('div'); + item.appendChild(header); + header.classList.add('accordion-header'); + + const btn = document.createElement('button'); + header.appendChild(btn); + + const collapse = document.createElement('div'); + item.appendChild(collapse); + collapse.id = 'info-accordion-collapse-' + idx; + collapse.classList.add('accordion-collapse', 'collapse'); + + btn.type = 'button'; + btn.setAttribute('data-bs-toggle', 'collapse'); + btn.setAttribute('data-bs-target', '#' + collapse.id); + btn.setAttribute('aria-expanded', 'false'); + btn.setAttribute('aria-controls', collapse.id); + btn.classList.add('accordion-button', 'collapsed'); + + const t = document.createTextNode(x.text); + btn.appendChild(t); + + const body = document.createElement('div'); + body.classList.add('accordion-body'); + collapse.appendChild(body); + + const ul = document.createElement('ul'); + ul.classList.add('list-group', 'list-group-flush'); + body.appendChild(ul); + + infoMetadataAccordions.push({ + element: ul, + items: x._layers.map(function(k) { + const groupname = k.split('.', 1).pop(); + return [ groupname, k.slice(groupname.length + 1) ]; + }), + }); + }); + })(); })(); -/* legend panel */ +/* measurement panel */ (function() { - const modal = document.getElementById('map-legend-panel'); - modal.classList.add('modal'); - modal.setAttribute('role', 'dialog'); - modal.setAttribute('aria-hidden', 'true'); + const value = document.createTextNode(''), + unit = document.createTextNode(''); + const reset = function() { + source.clear(true); + const f = { LineString: formatLength, Polygon: formatArea }[getMeasureMode()]; + if (f == null) { + value.nodeValue = unit.nodeValue = ''; + } else { + f(0); + } + }; - const content = document.createElement('div'); - modal.appendChild(content); - content.classList.add('modal-content'); + const source = new VectorSource({ + wrapX: false, + }); - const body = document.createElement('div'); - content.appendChild(body); - body.classList.add('modal-body'); - body.innerHTML = 'legend TODO'; + let draw; + const buttons = Object.fromEntries([ + { + id: 'cancel', + bi: 'trash', + title: 'Avbryt mätningen', + onclick: function() { + reset(); + Object.values(buttons).forEach((btn) => btn.disabled = true); + draw.abortDrawing(); + }, + }, + { + id: 'undo', + bi: 'arrow-counterclockwise', + title: 'Ta bort sista punkten', + onclick: function() { + draw.removeLastPoint(); + const n = { LineString: 2, Polygon: 3 }[getMeasureMode()] ?? Infinity; + draw.getOverlay().getSource().forEachFeature(function(feature) { + const geom = feature.getGeometry(); + if (geom.getType() === 'LineString') { + /* disable OK button if not enough points have been drawn (excluding cursor) */ + buttons.ok.disabled = geom.getCoordinates().length - 1 < n; + return true; /* stop iterating */ + } + }); + } + }, + { + id: 'ok', + bi: 'check-lg', + title: 'Slutför mätningen', + onclick: () => draw.finishDrawing(), + }, + ].map(function(x, idx, arr) { + const btn = document.createElement('button'); + btn.classList.add('btn', 'btn-outline-' + (idx < arr.length-1 ? 'secondary' : 'primary')); + btn.setAttribute('type', 'button'); + btn.title = x.title; + btn.setAttribute('aria-label', btn.title); + + const i = document.createElement('i'); + i.classList.add('bi', 'bi-' + x.bi); + btn.appendChild(i); + + btn.onclick = x.onclick; + return [x.id, btn]; + })); + + const formatLength = (function() { + const formatters = [ + { maximumFractionDigits: 0 }, + { maximumFractionDigits: 1 }, + { maximumFractionDigits: 2 }, + ] + .map((fmt) => new Intl.NumberFormat(LOCALE, { + ...fmt, + minimumFractionDigits: fmt.maximumFractionDigits ?? 0, + })); + return function(v) { + if (v <= 100) { /* ≤ 100 m */ + unit.nodeValue = 'm'; + value.nodeValue = formatters[1].format(v); + } else if (v <= 5_000) { /* ≤ 5 km */ + unit.nodeValue = 'm'; + value.nodeValue = formatters[0].format(v); + } else { + unit.nodeValue = 'km'; + value.nodeValue = formatters[2].format(v/1000); + } + }; + })(); + const formatArea = (function() { + const formatters = [ + { maximumFractionDigits: 1 }, + { maximumFractionDigits: 2 }, + ] + /* XXX would be nice to use Intl.NumberFormat()'s unit support, but m² and km² are not + * supported currently, see https://github.com/tc39/ecma402/issues/767 */ + .map((fmt) => new Intl.NumberFormat(LOCALE, { + ...fmt, + minimumFractionDigits: fmt.maximumFractionDigits ?? 0, + })); + return function(v) { + if (v < 10_000) { /* < 1 ha */ + unit.nodeValue = 'm²'; + value.nodeValue = formatters[0].format(v); + } else if (v < 100_000_000) { /* < 10000 ha (100 km²) */ + unit.nodeValue = 'ha'; + value.nodeValue = formatters[1].format(v/10_000); + } else { + unit.nodeValue = 'km²'; + v /= 1_000_000; + const i = v < 1_000_000 ? 1 : 0; /* ≥10⁶ km² overflows the box with 2 decimals */ + value.nodeValue = formatters[i].format(v); + } + }; + })(); + + const setup = (function() { + const styles = { + Point: new Style({ + image: new CircleStyle({ + radius: 6, + fill: new Fill({ + color: [0, 183, 255, 1], + }), + stroke: new Stroke({ + color: [255, 255, 255, 1], + width: .5, + }), + }), + }), + LineString: [ + new Style({ + stroke: new Stroke({ + color: [255, 255, 255, 1], + width: 4, + }), + }), + new Style({ + stroke: new Stroke({ + color: [0, 183, 255, 1], + width: 3, + lineDash: [10, 10], + }), + }), + ], + Polygon: new Style({ + fill: new Fill({ + color: [255, 255, 255, .5], + }), + }), + }; + const layer = new VectorLayer({ + source: source, + visible: false, + style: [ + new Style({ + fill: styles.Polygon.getFill(), + stroke: styles.LineString[0].getStroke(), + }), + (function() { + const s = styles.LineString[1].clone(); + s?.getStroke?.()?.setLineDash?.(null); + return s; + })(), + ], + map: MAP, + }); + + return function(geom_type) { + if (draw != null) { + draw.abortDrawing(); + MAP.removeInteraction(draw); + } + reset(); /* remove features when toggling between geom types */ + Object.values(buttons).forEach((btn) => btn.disabled = true); + if (geom_type == null) { + layer.setVisible(false); + return; + } + draw = new Draw({ + source: source, + type: geom_type, + style: function(feature) { + return styles[ feature.getGeometry().getType() ]; + }, + }); + MAP.addInteraction(draw); + layer.setVisible(true); + + let listener; + draw.on('drawstart', function(event) { + reset(); + buttons.undo.disabled = buttons.cancel.disabled = false; + const geom = event.feature.getGeometry(); + const [isComplete, measure] = { + LineString: [ + /* 2 points drawn + cursor */ + (g) => g.getCoordinates().length >= 3, + (g) => formatLength(g.getLength()), + ], + Polygon: [ + /* 3 points drawn + cursor + 1st point */ + (g) => g.getCoordinates()[0].length >= 5, + (g) => formatArea(g.getArea()), + ], + }[geom.getType()]; + const btnOK = buttons.ok; + listener = geom.on('change', function(event) { + if (btnOK.disabled && isComplete(geom)) { + btnOK.disabled = false; + } + measure(event.target); + }); + }); + draw.on('drawend', function() { + unByKey(listener); + buttons.ok.disabled = buttons.undo.disabled = true; + }); + draw.on('drawabort', function() { + unByKey(listener); + reset(); + Object.values(buttons).forEach((btn) => btn.disabled = true); + }); + }; + })(); + + const [body, btn_close] = (function() { + const modal = document.getElementById('measure-panel'); + modal.classList.add('modal'); + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-hidden', 'true'); + + const content = document.createElement('div'); + modal.appendChild(content); + content.classList.add('modal-content'); + + const header = document.createElement('div'); + content.appendChild(header); + header.classList.add('modal-header'); + + const title = document.createElement('div'); + title.classList.add('h5'); + title.innerHTML = 'Mät i kartan'; + header.appendChild(title); + + const btn_close = document.createElement('button'); + btn_close.classList.add('btn-close'); + btn_close.type = 'button'; + btn_close.title = 'Stäng'; + btn_close.setAttribute('aria-label', btn_close.title); + header.appendChild(btn_close); + + const body = document.createElement('div'); + content.appendChild(body); + body.classList.add('modal-body'); + return [body, btn_close]; + })(); + + const getMeasureMode = (function() { + const btn_group = document.createElement('div'); + btn_group.classList.add('btn-group'); + btn_group.setAttribute('role', 'group'); + btn_group.setAttribute('aria-label', 'Välj geometrityp'); + body.appendChild(btn_group); + + const radios = [ + { id: 'LineString', text: 'Distans' }, + { id: 'Polygon', text: 'Yta' }, + ].map(function(x, idx) { + const radio = document.createElement('input'); + radio.classList.add('btn-check'); + radio.type = 'radio'; + radio.checked = idx == 0; + radio.name = 'measure-geomtype'; + radio.id = 'measure-geomtype-' + x.id; + radio.setAttribute('aria-expanded', 'false'); + radio.setAttribute('autocomplete', 'off'); + btn_group.appendChild(radio); + + const lbl = document.createElement('label'); + lbl.classList.add('btn', 'btn-lg', 'btn-outline-dark'); + lbl.setAttribute('for', radio.id); + lbl.appendChild(document.createTextNode(x.text)); + btn_group.appendChild(lbl); + + radio.onclick = function() { + setup(x.id); + }; + return [x.id, radio]; + }); + + return () => radios.filter( (x) => x[1].checked )?.[0]?.[0]; + })(); + + (function() { + const div = document.createElement('div'); + div.classList.add('measure-value'); + body.appendChild(div); + const span0 = document.createElement('span'); + span0.appendChild(value); + div.appendChild(span0); + const span1 = document.createElement('span'); + span1.classList.add('measure-unit'); + span1.appendChild(unit); + div.appendChild(span1); + })(); + + (function() { + const btn_group = document.createElement('div'); + btn_group.classList.add('btn-group'); + btn_group.setAttribute('role', 'group'); + body.appendChild(btn_group); + + btn_group.appendChild(buttons.cancel); + btn_group.appendChild(buttons.undo); + btn_group.appendChild(buttons.ok); + })(); + + const button_menu = document.getElementById('measure-button') + .getElementsByTagName('button')[0]; + button_menu.addEventListener('click', function(event) { + if (event.currentTarget.getAttribute('aria-expanded') === 'true') { + disposePopover(); + setup(getMeasureMode()); + } else { + setup(null); + /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */ + const activeElement = document.activeElement; + if (activeElement.isEqualNode(btn_close)) { + activeElement.blur(); + } + } + }); + btn_close.onclick = () => button_menu.click(); })(); /* popup and feature overlays */ -(function() { +const disposePopover = (function() { + /* return an <a> tag with the given URL and optional text */ + const reURL = new RegExp('^https?://', 'i'); + const formatLink = function(url, text) { + if (url == null || typeof url !== 'string' || !reURL.test(url)) { + return url; + } + const a = document.createElement('a'); + a.href = url; + a.target = '_blank'; + if (text != null && text !== '') { + const t = document.createTextNode(text + ' '); + a.appendChild(t); + } + const i = document.createElement('i'); + i.classList.add('bi', 'bi-box-arrow-up-right'); + a.appendChild(i); + return a; + }; + + /* test a condition on the field maps */ + const condField = function(cond, k) { + if (Array.isArray(cond)) { + return cond.includes(k); + } + if (cond instanceof RegExp) { + return cond.test(k); + } + return cond(k); + }; + /* filter fields by condition */ + const filterFields = function(k, fields) { + return fields.map(function(v) { + if (v.cond == null || condField(v.cond, k)) { + return v; + } + }).filter((f) => f != null); + }; + /* filter fields using a pre-built map */ + const mapFields = function(k, fieldMap, fields) { + if (fields === undefined) { + return fieldMap.map((v) => k[v]); + } + return fields.map(function(v) { + if (!Array.isArray(v)) { + return fieldMap[v]; + } else if (condField(v[1], k)) { + return fieldMap[v[0]]; + } + }).filter((f) => f !== undefined); + }; + /* pre-build the field map so we don't need to duplicate objects accross layers */ + const mkFieldMap = function(fieldMap) { + return Object.fromEntries(Object.entries(fieldMap).map(function([k, o]) { + if (typeof o === 'string') { + return [k, Object.seal({key: k, desc: o})]; + } else { + return [k, Object.seal(Object.assign(o, {key: k}))]; + } + })); + }; + + const layers = { + svk: { + ledningar: { + title: 'Kraftledning (befintlig)', + fields: [ + { key: 'Placement', desc: 'Förläggning' }, + { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, + { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, + ], + }, + transmissionsnatsprojekt: { + title: 'Transmissionsnätsprojekt', + fields: [ + { key: 'Name', desc: 'Projektnamn' }, + { key: 'Voltage', desc: 'Spänning', unit: 'kV' }, + { key: 'Url', desc: 'Länk', fn: formatLink }, + ], + }, + }, + + misc: { + gigafactories: { + title: 'Stor industrisatsning', + fields: [ + { key: 'Name', desc: 'Namn' }, + { key: 'Url', desc: 'Länk', fn: formatLink }, + ], + }, + dammar: { + /* Documentation at + * https://www.smhi.se/polopoly_fs/1.34541!/dammprod%202013_3%2C%20beskrivning%2C%20SVAR2012_2.pdf */ + title: 'Damm', + fields: [ + { key: 'DNamn', desc: 'Dammenhetens namn' }, + { key: 'Namn', desc: 'Dammanläggningens namn' }, + { key: 'LST_OBJID', desc: 'Länsnr', classes: ['feature-objid'] }, + { key: 'Status', desc: 'Status', fn: (v) => v === 1 ? 'Befintlig damm' : v === 2 ? 'Fd. damm' : '' }, + //{ key: 'Regleringstyp', desc: 'Regleringstyp' }, + { key: 'ByggAr', desc: 'Byggår' }, + { key: 'DammHojd', desc: 'Dammhöjd', unit: 'm' }, + { key: 'KronLangd', desc: 'Krönlängd', unit: 'm' }, + { key: 'Fiskvag', desc: 'Fiskväg', fn: (v) => + v === 1 ? 'Bassängtrappa' : + v === 2 ? 'Denilränna' : + v === 3 ? 'Slitsränna' : + v === 4 ? 'Omlöp' : + v === 5 ? 'Inlöp' : + v === 6 ? 'Ålledare' : + v === 7 ? 'Smoltränna' : + v === 8 ? 'Okänd typ' : + v === 9 ? 'Ingen' : + v === 10 ? 'Annan' : + null }, + { key: 'HARO', desc: 'Huvudavrinningsområdesnummer', classes: ['feature-objid'] }, + { key: 'Vattendistrikt', desc: 'Vattendistrikt', classes: ['feature-objid'] }, + { key: 'Verksamhet', desc: 'Verksamhet', fn: (v) => + v === 1 ? 'Kraftproduktion' : + v === 2 ? 'Industri' : + v === 3 ? 'Sjöfart' : + v === 4 ? 'Invallning' : + v === 5 ? 'Vattenförsörjning' : + v === 6 ? 'Spegeldamm' : + v === 7 ? 'Historisk' : + v === 8 ? 'Övrigt' : + null }, + { key: 'DG', desc: 'Högsta dämningsgräns', unit: 'm' }, + { key: 'SG', desc: 'Lägsta sänkningsgräns', unit: 'm' }, + { key: 'MY', desc: 'Magasinsyta', unit: 'km²' }, + { key: 'RV', desc: 'Reglerbar volym', unit: 'Mm³' }, + { key: 'Kommentar', desc: 'Kommentar' }, + ], + }, + }, + }; + + layers.mrr = {}; + (function() { + const fields = [ + { key: 'name', desc: 'Namn' }, + { key: 'mineral', desc: 'Koncessionsmineral', cond: (i) => i < 6 }, + { key: 'owners', desc: 'Ägare', cond: [0,2,4] }, + { key: 'owners', desc: 'Sökande', cond: [1,3,5] }, + { key: 'conc_name', desc: 'Tillhörande bearbetnings\u00ADkoncession(er)', cond: [6] }, + { key: 'licenceid', desc: 'Tillståndsid', classes: ['feature-attr-mrr-license-id'], cond: [0,2,4,6] }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + { key: 'validfrom', desc: 'Giltig från', cond: [0,2,4] }, + { key: 'validto', desc: 'Giltig till', cond: [0,2,4] }, + { key: 'diarynr', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, + { key: 'appl_date', desc: 'Ansökningsdatum' }, + { key: 'dec_date', desc: 'Beslutsdatum', cond: [0,2,4,6] }, + ]; + Object.entries({ + ec: 'Bearbetningskoncession', + met: 'Undersökningstillstånd, metaller och industrimineral', + ogd: 'Undersökningstillstånd, olja, gas och diamant', + }) + .flatMap(([k, title]) => [ + /* don't use Object.entries() to guaranty ordering */ + ['appr', 'beviljad'], /* even index */ + ['appl', 'ansökt'], /* odd index */ + ].map(([a,b]) => [a + '_' + k, title + ' \u2013 ' + b])) + .concat([['appr_dl', 'Markanvisning till koncession']]) /* index #6 */ + .forEach(([k, title], idx) => layers.mrr[k] = { title, fields: filterFields(idx, fields) }); + })(); + + layers.vbk = {}; + (function() { + const fieldMap = mkFieldMap({ + Projektnamn: 'Projektnamn', + OmrID: { desc: 'Områdes-ID', classes: ['feature-objid'] }, + AntalVerk: 'Aktuella verk', + AntalEjXY: 'Antal ej koordinatsatta verk', + Projektstatus: 'Projektstatus', + Diarienummer: 'Diarienummer', + geom_area: { desc: 'Areal', fn: 'area' }, + Calprod: { desc: 'Beräknad årsproduktion', unit: 'GWh' }, + PlaneradByggstart: 'Planerad byggstart', + PlaneratDrift: 'Planerat drifttagande', + AndringsansokanPagar: 'Ändringsansökan pågår', + UnderByggnation: 'Under byggnation', + Organisationsnamn: 'Verksamhetsutövare', + Organisationsnummer: { desc: 'Organisationsnummer', classes: ['feature-orgnr'] }, + SamradsunderlagInlamnat: 'Samrådsunderlag inlämnat', + AnsokanInlamnat: 'Tillståndsansökan inlämnad', + AnsokanAterkallad: 'Tillståndsansökan återkallad', + AnsokanBeviljad: 'Tillståndsansökan beviljad', + AnsokanAvslagen: 'Tillståndsansökan avslagen', + AnsokanOverklagad: 'Överklagad', + Natura2000_Ansokan: 'Natura2000 ansökan', + Natura2000_Beslutdatum: 'Natura2000 beslutsdatum', + Uppfort: 'Parken uppförd', + PlaneratAntalVerkMin: 'Planerat antal verk (min)', + PlaneratAntalVerkMax: 'Planerat antal verk (max)', + PlaneradHojdMin: { desc: 'Panerad totalhöjd (min)', unit: 'm' }, + PlaneradHojdMax: { desc: 'Panerad totalhöjd (max)', unit: 'm' }, + PlaneradProduktionMin: { desc: 'Planerad årsproduktion (min)', unit: 'GWh' }, + PlaneradProduktionMax: { desc: 'Planerad årsproduktion (max)', unit: 'GWh' }, + BeviljatAntalVerk: 'Beviljat antal verk', + UppfortAntalVerk: 'Uppfört antal verk', + BeviljadMaxhojd: { desc: 'Beviljad maxhöjd', unit: 'm' }, + InstalleradEffekt: { desc: 'Installerad effekt', unit: 'MW' }, + ElNamn: 'Elområde', + SenasteUppdaterat: 'Senast uppdaterat', + }); + + Object.entries({ + current: null, + notcurrent: ' \u2013 ej aktuell', + }) + .forEach(([k, title]) => layers.vbk['area_' + k] = { + title: 'Landbaserad projekteringsområde för vindkraft' + (title ?? ''), + fields: mapFields(k, fieldMap, [ + 'Projektnamn', + 'OmrID', + 'AntalVerk', + 'AntalEjXY', + 'geom_area', + 'Calprod', + 'PlaneradByggstart', + 'PlaneratDrift', + 'AndringsansokanPagar', + ['UnderByggnation', ['current']], + 'Organisationsnamn', + 'Organisationsnummer', + 'ElNamn', + 'SenasteUppdaterat', + ]), + }); + + [ + ['completed', /* 0 */ 'uppförd'], + ['approved', /* 1 */ 'tillståndsansökan beviljad'], + ['amended', /* 2 */ 'ändringsansökan'], + ['rejected', /* 3 */ 'tillståndsansökan avslagen'], + ['appealed', /* 4 */ 'överklagad'], + ['applied', /* 5 */ 'tillståndsansökan inlämnad'], + ['consultation', /* 6 */ 'samråd inför tillståndsansökan'], + ['investigation', /* 7 */ 'inledande undersökningar'], + ['revoked', /* 8 */ 'inte aktuell eller återkallad'], + ] + .forEach(([k, title], idx) => layers.vbk['offshore_' + k] = { + title: 'Havsbaserad vindkraft \u2013 ' + title, + fields: mapFields(idx, fieldMap, [ + 'Projektnamn', + 'OmrID', + 'Organisationsnamn', + 'Organisationsnummer', + 'Projektstatus', + 'Diarienummer', + ['AndringsansokanPagar', [1,2,4]], + 'geom_area', + ['SamradsunderlagInlamnat', (i) => i <= 6 || i === 8], + ['AnsokanInlamnat', (i) => i <= 5 || i === 8], + ['AnsokanAterkallad', [8]], + ['AnsokanBeviljad', [0,1,4,8]], + ['AnsokanAvslagen', [3,8]], + ['AnsokanOverklagad', [0,1,3,4,8]], + ['Natura2000_Ansokan', (i) => i !== 2], + ['Natura2000_Beslutdatum', (i) => i !== 2], + ['UnderByggnation', [1]], + ['PlaneratAntalVerkMin', (i) => i > 0], + ['PlaneratAntalVerkMax', (i) => i > 0], + ['PlaneradHojdMin', (i) => i > 0], + ['PlaneradHojdMax', (i) => i > 0], + ['PlaneradProduktionMin', (i) => i > 0], + ['PlaneradProduktionMax', (i) => i > 0], + ['PlaneradByggstart', (i) => i > 0], + ['Uppfort', [0,8]], + ['PlaneratDrift', (i) => i > 0], + ['BeviljatAntalVerk', [0,1,4,8]], + ['UppfortAntalVerk', [0,8]], + ['BeviljadMaxhojd', [0,1,4,8]], + ['InstalleradEffekt', [0]], + ['Calprod', [0]], + 'ElNamn', + 'SenasteUppdaterat', + ]), + }); + + Object.assign(fieldMap, mkFieldMap({ + VerkID: { desc: 'Verk-ID', classes: ['feature-objid'] }, + Status: 'Status', + Handlingstyp: 'Handlingstyp', + MB_Tillstand: 'Miljöbalken tillstånd tidsbegränsning', + Uppfort: 'Uppförandedatum',/* override previous def */ + Totalhojd: { desc: 'Totalhöjd', unit: 'm' }, + Navhojd: { desc: 'Navhöjd', unit: 'm' }, + Rotordiameter: { desc: 'Rotordiameter', unit: 'm' }, + Maxeffekt: { desc: 'Maxeffekt', unit: 'MW' }, + Fabrikat: 'Fabrikat', + Modell: 'Modell', + Placering: 'Placering', + })); + + [ + ['completed', /* 0 */ 'uppfört'], + ['approved', /* 1 */ 'beviljat'], + ['rejected', /* 2 */ 'avslagit/nekat'], + ['processed', /* 3 */ 'handlagt'], + ['dismounted', /* 4 */ 'nedmonterat'], + ['appealed', /* 5 */ 'överklagat'], + ['revoked', /* 6 */ 'inte aktuell eller återkallad'], + ] + .forEach(([k, title], idx) => layers.vbk['turbine_' + k] = { + title: 'Landbaserad vindkraftverk \u2013 ' + title, + fields: mapFields(idx, fieldMap, [ + 'VerkID', + 'OmrID', + 'Projektnamn', + 'Status', + 'Handlingstyp', + ['Uppfort', [0,4,6]], + 'MB_Tillstand', + 'Totalhojd', + 'Navhojd', + 'Rotordiameter', + 'Maxeffekt', + 'Calprod', + 'Fabrikat', + 'Modell', + 'Organisationsnamn', + 'Organisationsnummer', + 'Placering', + 'ElNamn', + 'SenasteUppdaterat', + ]), + }); + })(); + + layers.avverk = {}; + (function() { + const zeroIsNull = (v) => v > 0 ? v : null; + const fieldMap = mkFieldMap({ + /* Documentation at + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/utforda-avverkningar---produktbeskrivning.pdf + * and + * https://www.skogsstyrelsen.se/globalassets/sjalvservice/karttjanster/geodatatjanster/produktbeskrivningar/yttre-granser-for-avverkningsanmalda-omraden---produktbeskrivning.pdf + */ + Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, + ArendeAr: 'Registeringsår', + Inkomdatum: 'Inkom datum', + Skogstyp: 'Skogstyp', + Avvdatum: 'Datum för avverkning', + KallaDatum: 'Ursprung för datum för avverkning', + AnmaldHa: { desc: 'Areal anmält', unit: 'ha' }, + NatforHa: { desc: 'Areal naturlig föryngring', unit: 'ha', fn: zeroIsNull }, + SkogsodlHa: { desc: 'Areal plantering', unit: 'ha', fn: zeroIsNull }, + AvvSasong: 'Avverkningssäsong', + Avverktyp: 'Avverkningstyp', + ArendeStatus: 'Ärendestatus', + AvvHa: { desc: 'Avverkad areal', unit: 'ha' }, + geom_area: { desc: 'Areal för ytan', fn: 'area' }, + }); + + layers.avverk.utford = { + title: 'Utförd avverkning', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'ArendeAr', + 'Skogstyp', + 'AnmaldHa', + 'NatforHa', + 'Avverktyp', + 'Avvdatum', + 'KallaDatum', + 'geom_area', + ]), + }; + + layers.avverk.anmald = { + title: 'Avverkningsanmälansområde', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Inkomdatum', + 'ArendeAr', + 'AnmaldHa', + 'NatforHa', + 'SkogsodlHa', + 'AvvSasong', + 'ArendeStatus', + 'AvvHa', + ]), + }; + })(); + + layers.skydd = {}; + (function() { + const fieldMap = mkFieldMap({ + NVRID: { desc: 'NVR-ID', classes: ['feature-objid'] }, + FORSKRNAMN: 'Föreskriftsområde', + OBJEKTNAMN: 'Namn', + NAMN: 'Namn', + BESLSTAT: 'Beslutsstatus', + FORESKRTYP: 'Föreskriftstyp', + FORESKRIFT: 'Föreskriftssubtyp', + FRANDATUM: 'Från datum', + TILLDATUM: 'Till datum', + BESKRIVN: 'Beskrivning', + geom_area: { desc: 'Areal', fn: 'area' }, + + SKYDDSTYP: 'Skyddstyp', + BESLSTATUS: 'Beslutsstatus', + URSBESLDAT: 'Beslutsdatum (bildande)', + URSGALLDAT: 'Ursprungligt gällandedatum', + SENGALLDAT: 'Senaste gällandedatum', + FORVALTARE: 'Förvaltare', + IUCNKAT: 'IUCN-kategori', + DIARIENR: { desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, + LAGRUM: 'Lagrum', + BESLMYND: 'Beslutsmyndighet', + LAND_HA: { desc: 'Areal land', unit: 'ha' }, + VATTEN_HA: { desc: 'Areal vatten', unit: 'ha' }, + SKOG_HA: { desc: 'Skogsmarksareal', unit: 'ha' }, + + IKRAFTDATF: 'Ikraftträdandedatum föreskrifter', + TILLSYNSMH: 'Tillsynsmyndighet', + PROVNMHTIL: 'Prövningsmyndighet tillstånd', + PROVNMHDIS: 'Prövningsmyndighet dispens', + + NAME: 'Namn', + RAMSAR_ID: { desc: 'Ramsar-ID', classes: ['feature-objid'] }, + LEGAL_ACT: 'Rättsakt', + URSPR_BESL: 'Ursprungligt beslutsdatum', + SEN_BESLUT: 'Senaste beslutsdatum', + LINK: { desc: 'Länk', fn: formatLink }, + }); + + layers.skydd.tilltradesforbud = { + title: 'Tillträdesförbud', + fields: mapFields(fieldMap, [ + 'NVRID', + 'FORSKRNAMN', + 'OBJEKTNAMN', + 'BESLSTAT', + 'FORESKRTYP', + 'FORESKRIFT', + 'FRANDATUM', + 'TILLDATUM', + 'BESKRIVN', + 'geom_area', + ]), + }; + + /* Nationella skyddsformer från Naturvårdsregistret */ + const isSurface = (k) => !/_punkt$/.test(k); + Object.entries({ + nationalpark: 'Nationalpark', + naturreservat: 'Naturreservat', + naturreservat_kommunalt: 'Kommunalt naturreservat', + naturvardsomrade: 'Naturvårdsområde', + djur_och_vaxtskyddsomrade: 'Djur- och växtskyddsområde', + kulturreservat: 'Kulturreservat', + vattenskyddsomrade: 'Vattenskyddsområden', + landskapsbildsskyddsomrade: 'Landskapsbildsskyddsområde', + ovrigt_biotopskyddsomrade: 'Biotopskydd utanför skogsmark', + naturminne_yta: 'Naturminne (yta)', + naturminne_punkt: 'Naturminne (punkt)', + interimistiskt_forbud: 'Interimistiskt förbud', + }) + .forEach(([k, title]) => layers.skydd[k] = { + title: title, + fields: mapFields(k, fieldMap, [ + 'NVRID', + 'NAMN', + 'SKYDDSTYP', + 'BESLSTATUS', + 'URSBESLDAT', + ['URSGALLDAT', (k) => k !== 'vattenskyddsomrade'], + ['SENGALLDAT', (k) => k !== 'vattenskyddsomrade'], + ['FORVALTARE', (k) => k !== 'vattenskyddsomrade'], + ['IKRAFTDATF', (k) => k === 'vattenskyddsomrade'], + 'IUCNKAT', + 'DIARIENR', + 'LAGRUM', + 'BESLMYND', + ['TILLSYNSMH', (k) => k === 'vattenskyddsomrade'], + ['PROVNMHTIL', (k) => k === 'vattenskyddsomrade'], + ['PROVNMHDIS', (k) => k === 'vattenskyddsomrade'], + ['geom_area', isSurface], + ['LAND_HA', isSurface], + ['VATTEN_HA', isSurface], + ['SKOG_HA', isSurface], + ]), + }); + + /* Natura 2000-områden */ + (function() { + const fields = [ + { key: 'SITE_CODE', desc: 'Områdeskod', classes: ['feature-objid'] }, + { key: 'NAMN', desc: 'Namn' }, + { key: 'OMRADESTYP', desc: 'Områdestyp' }, + { key: 'UPPLAMNARE', desc: 'Uppgiftslämnare' }, + { key: 'SPA_DATUM', desc: 'SPA-datum' }, + { key: 'SCI_FORSL', desc: 'SCI-förslagsdatum' }, + { key: 'SCI_DATUM', desc: 'SCI-datum' }, + { key: 'SAC_DATUM', desc: 'SAC-datum' }, + fieldMap.geom_area, + { key: 'KVALITET', desc: 'Kvalitet' }, + { key: 'KARAKTAR', desc: 'Kännetecken för området' }, + { key: 'ARTER', desc: 'Arter' }, + { key: 'NATURTYPER', desc: 'Naturtyper' }, + { key: 'BEVPLAN', desc: 'Bevarandeplan', fn: formatLink }, + ]; + Object.entries({ + fageldirektivet: 'Fågeldirektivet (SPA)', + habitatdirektivet: 'Art- och habitatdirektivet (SCI)', + }) + .forEach(([k, title]) => layers.skydd[k] = { title, fields }); + })(); + + /* Områden med internationell status */ + layers.skydd.helcom = { + title: 'Marina skyddade områden (Helcom MPA)', + fields: mapFields(fieldMap, [ 'NAME', 'geom_area' ]), + }; + + layers.skydd.ramsar = { + title: 'Ramsar-områden (Våtmarkskonventionen)', + fields: mapFields(fieldMap, [ + 'RAMSAR_ID', + 'SKYDDSTYP', + 'NAMN', + 'geom_area', + 'LAND_HA', + 'VATTEN_HA', + 'SKOG_HA', + 'URSPR_BESL', + 'SEN_BESLUT', + 'LEGAL_ACT', + 'LINK', + ]), + }; + + layers.skydd.ospar = { + title: 'Marina skyddade områden (Ospar MPA)', + fields: [ + { key: 'ORIGIN', desc: 'Ursprung' }, + { key: 'NAMN_N2000', desc: 'N2000-namn' }, + { key: 'MPA_ID', desc: 'MPA-ID', classes: ['feature-objid'] }, + { key: 'MPA_NAMN', desc: 'MPA-namn' }, + { key: 'N2000_SITE', desc: 'N2000-ID', classes: ['feature-objid'] }, + fieldMap.geom_area, + ], + }; + + layers.skydd.varldsarv = { + title: 'Världsarv med mycket höga naturvärden (Unesco)', + fields: mapFields(fieldMap, [ 'NAMN', 'geom_area' ]), + }; + + layers.skydd.biosfarsomraden = { + title: 'Biosfärsområden (UNESCO)', + fields: [ + { key: 'SKYDDSTYP', desc: 'Skyddstyp' }, + { key: 'NAMN', desc: 'Namn' }, + { key: 'LINK', desc: 'Länk', fn: formatLink }, + fieldMap.geom_area, + ], + }; + + layers.skydd.naturvardsavtal = { + title: 'Naturvårdsavtal (Naturvårdsverket, Länsstyrelsen)', + fields: [ + { key: 'ID', desc: 'ID', classes: ['feature-objid'] }, + { key: 'OBJNAMN', desc: 'Namn' }, + { key: 'FASTBET', desc: 'Fastighet', classes: ['feature-objid'] }, + { key: 'DATSTART', desc: 'Giltig från' }, + { key: 'DATSLUT', desc: 'Giltig till' }, + { key: 'DIARIENRNV', desc: 'Diarienummer', classes: ['feature-attr-dnr'] }, + { key: 'STATUS', desc: 'Satus' }, + fieldMap.geom_area, + ], + }; + })(); + + (function() { + const fieldMap = mkFieldMap({ + Beteckn: { desc: 'Ärendebeteckning', classes: ['feature-objid'] }, + Biotyp: { desc: 'Biotopkategori' }, + Naturtyp: { desc: 'Skogstyp' }, + ArendeAr: { desc: 'Registeringsår' }, + geom_area: { desc: 'Areal', fn: 'area' }, + AreaProd: { desc: 'Skogsmarksareal', unit: 'ha' }, + Datbeslut: { desc: 'Beslutsdatum' }, + Url: { desc: 'Länk', fn: (v) => formatLink(v, 'Skogens Pärlor') }, + NvaTyp: 'Biotopkategori', + DatAvtal: 'Avtalsdatum', + Undertyp: 'Undertyp', + AvtalatDatum: 'Avtalat datum', + Objnamn: 'Objektnamn', + Datinv: 'Datum för fältinventering', + }); + + layers.skydd.skogligt_biotopskyddsomrade = { + title: 'Biotopskydd i skogsmark', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Biotyp', + 'Naturtyp', + 'ArendeAr', + 'geom_area', + 'AreaProd', + 'Datbeslut', + 'Url', + ]), + }; + layers.skydd.naturvardsavtal_skogsstyrelsen = { + title: 'Naturvårdsavtal (Skogsstyrelsen)', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'ArendeAr', + 'NvaTyp', + 'Naturtyp', + 'DatAvtal', + 'geom_area', + 'AreaProd', + 'Url', + 'Undertyp', + ]), + }; + + layers.skydd.atervatningsavtal = { + title: 'Återvätningsavtal', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'ArendeAr', + 'AvtalatDatum', + 'geom_area', + 'Url', + ]), + }; + + layers.nv = {}; + Object.assign(fieldMap, mkFieldMap(Object.fromEntries( + [1,2,3].map((i) => [`Biotop${i}`, `Biotoptyp #${i}`]).concat( + [1,2,3,4,5,6,7,8].map((i) => [`Beskrivn${i}`, `Nyckelord #${i} som beskriver objektet`]) + )))); + layers.nv.naturvarde_sks = { + title: 'Objekt med naturvärden (Skogsstyrelsen)', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Objnamn', + 'Datinv', + 'Biotop1', 'Biotop2', 'Biotop3', + 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', + 'geom_area', + 'Url', + ]), + }; + layers.nv.nyckelbiotop = { + title: 'Nyckelbiotop (Skogsstyrelsen)', + fields: mapFields(fieldMap, [ + 'Beteckn', + 'Objnamn', + 'Datinv', + 'Biotop1', 'Biotop2', 'Biotop3', + 'Beskrivn1', 'Beskrivn2', 'Beskrivn3', 'Beskrivn4', 'Beskrivn5', 'Beskrivn6', 'Beskrivn7', 'Beskrivn8', + 'geom_area', + 'Url', + ]), + }; + layers.nv.nyckelbiotop_storskogsbruk = { + title: 'Nyckelbiotop (storskogsbruket)', + fields: [ + { key: 'Org', desc: 'Uppgifter lämnade av' }, + { key: 'InkomDatum', desc: 'Inkom datum' }, + fieldMap.geom_area, + fieldMap.Url, + ], + }; + + layers.nv.sumpskog = { + title: 'Sumpskog', + fields: [ + { key: 'Namn', desc: 'Objektnamn' }, + { key: 'Tradtext', desc: 'Skogstyp' }, + { key: 'Hydrtext', desc: 'Hydrologisk typ' }, + { key: 'Delklass', desc: 'Klass på delobjektet' }, + { key: 'Klassu', desc: 'Klass på objektet' }, + { key: 'Lovandel', desc: 'Andel löv' }, + { key: 'Andelva', desc: 'Andel öppet vatten' }, + { key: 'Krontakn', desc: 'Krontäckning' }, + { key: 'Huggklas', desc: 'Huggningsklass' }, + { key: 'Ingrepp', desc: 'Ingrepp på delobjekt (max 4)' }, + { key: 'Ingrpavv', desc: 'Grad av påverkan på delobjekt (max 4)' }, + { key: 'Objnyck', desc: 'Nyckelord på objektnivå' }, + { key: 'Delnyck', desc: 'Nyckelord på delobjektsnivå' }, + { key: 'Flygar', desc: 'Flygbildsår' }, + { key: 'Faltdat', desc: 'Datum för fältbesök' }, + { key: 'Invtekn', desc: 'Inventeringsteknik' }, + { key: 'Invdat', desc: 'Inventeringdatum' }, + { key: 'Ansvmynd', desc: 'Ansvarig myndighet' }, + fieldMap.geom_area, + fieldMap.Url, + ], + }; + })(); + + layers.nv.pagaende_naturreservatsbildning = { + title: 'Pågående naturreservatsbildning', + fields: [ + { key: 'NAMN', desc: 'Objektnamn' }, + /* XXX unclear what "GRANSJUST" means, just a guess */ + { key: 'GRANSJUST', desc: 'Senast justerat' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }; + + layers.nv.snus = { + title: 'Skyddsvärd statlig skog', + fields: [ + { key: 'NAMN', desc: 'Objektnamn' }, + { key: 'AR', desc: 'År' }, + { key: 'NATURGEOGR', desc: 'Naturgeografisk region', classes: ['feature-objid'] }, + { key: 'OBJEKTKATE', desc: 'Objektskategori', classes: ['feature-objid'] }, + { key: 'MARKAGARE', desc: 'Markägare' }, + { key: 'VARDEKARNA', desc: 'Areal värdekärna', unit: 'ha' }, + { key: 'UTV_MARK', desc: 'Areal utvecklingsmark', unit: 'ha' }, + { key: 'TOTAL_AREA', desc: 'Totalareal', unit: 'ha' }, + { key: 'LAND', desc: 'Areal land', unit: 'ha' }, + { key: 'VATTEN', desc: 'Areal vatten', unit: 'ha' }, + { key: 'PROD_SKOG', desc: 'Areal produktiv skogsmark', unit: 'ha' }, + { key: 'SKOG_O_FJG', desc: 'Areal produktiv skogsmark ovanför fjällnära gräns', unit: 'ha' }, + { key: 'SKOG_N_FJG', desc: 'Areal produktiv skogsmark nedanför fjällnära gräns', unit: 'ha' }, + { key: 'SKYDDSZON', desc: 'Areal skyddszon', unit: 'ha' }, + { key: 'ARRO_MARK', desc: 'Areal arronderingsmark', unit: 'ha' }, + { key: 'KRITERIER', desc: 'Kriterier för urval' }, + { key: 'BESKRIVN', desc: 'Beskrivning av området' }, + { key: 'LST_BEDOMN', desc: 'Länsstyrelsens bedömning' }, + { key: 'KALLOR', desc: 'Källor' }, + ], + }; + + layers.ri = {}; + (function() { + const fieldMap = mkFieldMap({ + NAMN: 'Namn', + SKYDD: 'Skydd', + AMNESOMRAD: 'Ämnesområde', + AMNESOMR: 'Ämnesområde', + OMRADESNR: { desc: 'Områdesnummer', classes: ['feature-objid'] }, + BESKRIVNIN: { desc: 'Beskrivning', fn: formatLink }, + LANK_VARDE: { desc: 'Länk värdebeskrivning', fn: formatLink }, + LAGRUM: 'Lagrum', + BESLUTSDAT: 'Beslutsdatum', + BESLDATUM: 'Beslutsdatum', + ARENDENR: { desc: 'Ärendenummer', classes: ['feature-attr-dnr'] }, + LANK_BESLU: { desc: 'Länk beslut', fn: formatLink }, + AKTIVITET: 'Aktivitet', + NATURTYP: 'Naturtyp', + ORGINALID: { desc: 'Original-ID', classes: ['feature-objid'] }, + RIKSID: { desc: 'Riks-ID', classes: ['feature-objid'] }, + geom_area: { desc: 'Areal', fn: 'area' }, + AREA_LAND_: { desc: 'Areal land', unit: 'ha' }, + AREA_VATTE: { desc: 'Areal vatten', unit: 'ha' }, + }); + layers.ri.naturvard = { + title: 'Riksintresse naturvård', + fields: mapFields(fieldMap, [ + 'NAMN', + 'SKYDD', + 'AMNESOMRAD', + 'BESKRIVNIN', + 'LAGRUM', + 'BESLUTSDAT', + 'ORGINALID', + 'RIKSID', + 'geom_area', + ]), + }; + layers.ri.friluftsliv = { + title: 'Riksintresse friluftsliv', + fields: mapFields(fieldMap, [ + 'NAMN', + 'SKYDD', + 'AMNESOMR', + 'OMRADESNR', + 'LANK_VARDE', + 'LAGRUM', + 'BESLDATUM', + 'ARENDENR', + 'LANK_BESLU', + 'AKTIVITET', + 'NATURTYP', + 'geom_area', + 'AREA_LAND_', + 'AREA_VATTE', + ]), + }; + + Object.assign(fieldMap, mkFieldMap({ + METODBESKR: 'Metodbeskrivning', + TILLKDATUM: 'Tillkomstdatum', + REVDATUM: 'Revisionsdatum', + ANM: 'Anmärkning', + OBJEKTLANK: { desc: 'Objektlänk', fn: formatLink }, + REFERENS: 'Referens', + OBJTYP: 'Objekttyp', + ORIGINALID: fieldMap.ORGINALID, + DIG_SKALA: { desc: 'Digitaliseringsskala', fn: (v) => v > 0 ? v : null }, + })); + [ + ['rorligt_friluftsliv', /* 0 */ 'rörligt friluftsliv (MB 4 kap 1§ och 2§)'], + ['obruten_kust', /* 1 */ 'obruten kust (MB 4 kap 3§)'], + ['obrutet_fjall', /* 2 */ 'obrutet fjäll (MB 4 kap 5§)'], + ['skyddade_vattendrag', /* 3 */ 'skyddade vattendrag (MB 4 kap 6§)'], + ] + .forEach(([k, title], idx) => layers.ri[k] = { + title: 'Riksintresse ' + title, + fields: mapFields(idx, fieldMap, [ + 'NAMN', + 'BESKRIVNIN', + 'METODBESKR', + 'TILLKDATUM', + 'REVDATUM', + ['OBJTYP', [1]], + ['ANM', [0,1,3]], + ['DIG_SKALA', [3]], + 'OBJEKTLANK', + 'geom_area', + 'ORIGINALID', + 'REFERENS', + ]), + }); + })(); + + layers.ren = { + betesomrade: { + title: 'Samebyarnas betesområde', + fields: [ + { key: 'NAMN', desc: 'Sameby' }, + { key: 'SAMEBY_TYP', desc: 'Samebys typ' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }, + flyttled: { + title: 'Samebyarnas markanvändningsredovisning \u2013 flyttled', + fields: [ + { key: 'LED_ID', desc: 'Led-ID', classes: ['feature-objid'], fn: (v) => v > 0 ? v : null }, + { key: 'SAMEBY1', desc: 'Sameby #1' }, + { key: 'SAMEBY2', desc: 'Sameby #2' }, + { key: 'SAMEBY3', desc: 'Sameby #3' }, + { key: 'BESKRIVNIN', desc: 'Beskrivning' }, + { key: 'ARSTID', desc: 'Årstid' }, + { key: 'RIKSINTR', desc: 'Riksintresse' }, + { key: 'FAST_LED', desc: 'Fast led' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'geom_length', desc: 'Ledlängd', fn: 'length' }, + ], + }, + riks_ren: { + title: 'Riksintresse rennäring', + fields: [ + { key: 'LAGRUM', desc: 'Lagrum' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }, + omr_riks: { + title: '(Kärn)områden av riksintresse rennäring', + fields: [ + { key: 'OMR_NR', desc: 'Områdes-ID', classes: ['feature-objid'] }, + { key: 'LANK', desc: 'Länk' }, + { key: 'ARET_RUNT', desc: 'Årets runt' }, + { key: 'SAMEBY', desc: 'Sameby' }, + { key: 'ANSVARIG', desc: 'Ansvarig' }, + { key: 'AKTUALITET', desc: 'Aktualitet' }, + { key: 'SIGNATUR', desc: 'Signatur' }, + { key: 'geom_area', desc: 'Areal', fn: 'area' }, + ], + }, + }; + + /* format value to HTML */ + const numberFormatters = [ + { }, + { maximumFractionDigits: 1 }, + { maximumFractionDigits: 2 }, + ] + /* XXX would be nice to use Intl.NumberFormat()'s unit support, but m² and km² are not + * supported currently, see https://github.com/tc39/ecma402/issues/767 */ + .map((fmt) => new Intl.NumberFormat(LOCALE, fmt)); + const unitSeparator = '\u00A0'; /* U+00A0 NO-BREAK SPACE */ + const formatValue = function(value, options) { + if (options?.fn == null) { + /* no-op */ + } else if (typeof options.fn === 'function') { + value = options.fn(value); + } else if (options.fn === 'length' && typeof value === 'number' && options?.unit == null) { + if (value <= 5_000) { /* ≤ 5 km */ + value = numberFormatters[1].format(value) + unitSeparator + 'm'; + } else { + value = numberFormatters[2].format(value/1000) + unitSeparator + 'km'; + } + } else if (options.fn === 'area' && typeof value === 'number' && options?.unit == null) { + if (value < 10_000) { /* < 1 ha */ + value = numberFormatters[1].format(value) + unitSeparator + 'm²'; + } else if (value < 100_000_000) { /* < 10000 ha (100 km²) */ + value = numberFormatters[2].format(value/10_000) + unitSeparator + 'ha'; + } else { + value = numberFormatters[2].format(value/1_000_000) + unitSeparator + 'km²'; + } + } + if (value == null) { + return null; + } + if (value instanceof HTMLElement) { + return value; + } + switch (typeof value) { + case 'boolean': + return document.createTextNode(value ? 'Ja' : 'Nej'); + case 'string': + return document.createTextNode(value); + case 'number': + if (options?.unit != null) { + return document.createTextNode(numberFormatters[0].format(value) + + unitSeparator + options.unit); + } + return document.createTextNode(value.toString()); + default: + return null; + } + }; + + /* turn the properties into a fine <table> */ + const formatFeaturePropertiesToHTML = function(properties) { + const table = document.createElement('table'); + table.classList.add('table', 'table-sm', 'table-borderless', 'table-hover'); + + const tbody = document.createElement('tbody'); + table.appendChild(tbody); + + const def = layers[properties.layer_group][properties.layer]; + def.fields.forEach(function(field) { + const tr = document.createElement('tr'); + tbody.appendChild(tr); + + const th = document.createElement('th'); + th.setAttribute('scope', 'row'); + tr.appendChild(th); + const textDesc = document.createTextNode(field.desc); + th.appendChild(textDesc); + + const td = document.createElement('td'); + tr.appendChild(td); + const v = formatValue(properties[field.key], field); + if (v != null) { + td.appendChild(v); + } + field.classes?.forEach?.((c) => td.classList.add(c)); + }); + + const content = document.createElement('div'); + if (def.title != null) { + const h = document.createElement('h6'); + content.appendChild(h); + const textNode = document.createTextNode(def.title); + h.appendChild(textNode); + } + + content.appendChild(table); + return content; + }; + + /* initialize popup overlay */ const popupOverlay = new Overlay({ stopEvent: true, - element: popup, + element: document.getElementById('popup'), }); - map.addOverlay(popupOverlay); + MAP.addOverlay(popupOverlay); + + let featureOverlayLayer = null; + let overlayAttributes = [], + overlayAttrIdx = 0, + mapSources = {}; + /* clear the highlighted feature list and make the overlay layer invisible */ + const disposeFeatureOverlay = function() { + if (featureOverlayLayer?.getVisible?.()) { + featureOverlayLayer.setVisible(false); + featureOverlayLayer.changed(); + } + /* clear the overlay list */ + overlayAttributes = []; + overlayAttrIdx = 0; + mapSources = {}; + } - let popover, overlayAttributes = [], overlayAttrIdx = 0; + let popover = null; + /* clear overlay layer and dispose popover */ + const disposePopover = function() { + disposeFeatureOverlay(); + if (popover?.tip != null) { + popover.dispose(); + } + }; + + /* initialize popover */ + featureOverlayLayer = new VectorTileLayer({ + zIndex: 65535, + declutter: false, + visible: false, + renderMode: 'vector', + style: null, + map: MAP, + }); const header = document.createElement('div'); header.classList.add('d-flex'); @@ -3538,23 +5251,24 @@ const [vectorLayers, featureOverlayLayer] = (function() { headerGrabbingArea.classList.add('flex-grow-1', 'grabbing-area', 'pe-2', 'me-2'); header.appendChild(headerGrabbingArea); + const pageNode = document.createElement('h6'); + headerGrabbingArea.appendChild(pageNode); + headerGrabbingArea.onmousedown = function(event) { + /* move the popover around */ if (event.button != 0) { return; } - const popoverTip = Popover.getInstance(popup).tip; + const popoverTip = popover.tip; if (popoverTip.classList.contains('popover-maximized')) { return; } - pageNode.classList.add('grabbing-area-grabbed'); + headerGrabbingArea.classList.add('grabbing-area-grabbed'); if (!popoverTip.classList.contains('popover-detached')) { /* detach popover tip */ popoverTip.classList.add('popover-detached'); const rect = popoverTip.getBoundingClientRect(); - const maxHeight = document.getElementById('map').getBoundingClientRect().height - 1 - rect.top - - popoverTip.getElementsByClassName('popover-header')[0].getBoundingClientRect().height; - const style = popoverTip.style; style.display = 'none'; /* avoid reflows between the following assignments */ style.position = 'absolute'; @@ -3574,18 +5288,17 @@ const [vectorLayers, featureOverlayLayer] = (function() { }; document.onmouseup = function(event) { + /* done moving around */ if (event.button != 0) { return; } - pageNode.classList.remove('grabbing-area-grabbed'); + headerGrabbingArea.classList.remove('grabbing-area-grabbed'); document.onmousemove = null; document.onmouseup = null; }; }; - const pageNode = document.createElement('h6'); - headerGrabbingArea.appendChild(pageNode); - + /* current number page and total page count */ const pageNum = document.createElement('span'); const pageCount = document.createElement('span'); pageNode.appendChild(document.createTextNode('Träff ')); @@ -3593,48 +5306,48 @@ const [vectorLayers, featureOverlayLayer] = (function() { pageNode.appendChild(document.createTextNode(' av ')); pageNode.appendChild(pageCount); + /* highlight a feature */ const featureOverlayStyle = new Style({ stroke: new Stroke({ color: 'rgba(0, 255, 255, .8)', width: 3, }), }); - const updateFeatureOverlayLayer = function(layer_group, layer, id) { - const lyr = vectorLayers[layer_group]; - if (lyr === undefined) { + const highlightFeature = function(layer_group, layer, id) { + const source = mapSources[layer_group]; + if (source == null) { return; } - const urls = lyr.getSource().getUrls(); - const source = featureOverlayLayer.getSource(); - if (source.getUrls().length < 1 || source.getUrls()[0] !== urls[0]) { + if (featureOverlayLayer.getSource() !== source) { + /* console.log('Updating source for feature overlay layer'); */ featureOverlayLayer.setVisible(false); - source.setUrls(urls); + featureOverlayLayer.setSource(source); } - featureOverlayLayer.setStyle(function(feature, resolution) { + featureOverlayLayer.setStyle(function(feature) { if (feature.getId() === id && feature.getProperties().layer === layer) { return featureOverlayStyle; - } else { - return undefined; } }); featureOverlayLayer.setVisible(true); featureOverlayLayer.changed(); }; + /* highlight the feature at index overlayAttrIdx within the CGI reply list */ const refreshPopover = function() { const attr = overlayAttributes[overlayAttrIdx]; - updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.ogc_fid); + highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); pageNum.innerHTML = (overlayAttrIdx + 1).toString(); const content = formatFeaturePropertiesToHTML(attr); popover.tip.getElementsByClassName('popover-body')[0].replaceChildren(content); }; + /* go back/forward in the overlayAttributes list */ const onClickPageChange = function(event, offset) { const btn = event.target; - if (btn.classList.contains('disabled') || popover === null || popover.tip === null) { + if (btn.classList.contains('disabled') || popover?.tip == null) { return; } if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) { - return; + return; /* out of range */ } overlayAttrIdx += offset; @@ -3653,6 +5366,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { setTimeout(function() { btn.blur() }, 100); }; + /* control buttons */ const btnPrev = document.createElement('button'); btnPrev.classList.add('popover-button', 'popover-button-prev'); btnPrev.setAttribute('type', 'button'); @@ -3677,8 +5391,8 @@ const [vectorLayers, featureOverlayLayer] = (function() { const btnExpandTitle = 'Förstora'; const btnExpandTitle2 = 'Förminska'; btnExpand.setAttribute('aria-label', btnExpand.title); - btnExpand.onclick = function(event) { - if (popover === null || popover.tip === null) { + btnExpand.onclick = function() { /* maximize or reduce the popover */ + if (popover?.tip == null) { return; } if (!popover.tip.classList.contains('popover-maximized')) { @@ -3700,119 +5414,30 @@ const [vectorLayers, featureOverlayLayer] = (function() { btnClose.setAttribute('type', 'button'); btnClose.title = 'Stäng'; btnClose.setAttribute('aria-label', btnClose.title); - btnClose.onclick = function(event) { - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - if (popover !== null) { - popover.dispose(); - } - }; + btnClose.onclick = disposePopover; header.appendChild(btnPrev); header.appendChild(btnNext); header.appendChild(btnExpand); header.appendChild(btnClose); - const formatFeaturePropertiesToHTML = function(properties) { - /* turn the properties into a fine table */ - const table = document.createElement('table'); - table.classList.add('table', 'table-sm', 'table-borderless', 'table-hover'); - - const tbody = document.createElement('tbody'); - table.appendChild(tbody); - - const def = layers[properties.layer_group + '_' + properties.layer]; - def.popover.forEach(function([desc, key, opts]) { - let v = properties[key]; - if (opts === undefined) { - opts = {}; - } - if (opts.fn !== undefined) { - if (opts.fn === 'length') { - if (v < 1000) { - opts.unit = 'm'; - } else { - v /= 1000.; - v = Math.round(v*100) / 100.; - opts.unit = 'km'; - } - } else if (opts.fn === 'area') { - if (v < 10000) { - opts.unit = 'm²'; - } else if (v < 10000 * 10000) { - v /= 10000.; - opts.unit = 'ha'; - } else { - v /= 1000000.; - opts.unit = 'km²'; - } - v = Math.round(v*100) / 100.; - } else { - v = opts.fn(v); - } - } - if (v === undefined || v === null) { - v = document.createTextNode(''); - } else if (!(v instanceof HTMLElement)) { - if (typeof(v) === 'number' && opts.unit !== undefined) { - v = v.toLocaleString('sv-SE'); - } else if (typeof(v) === 'boolean') { - v = v ? 'Ja' : 'Nej'; - } - if (opts.unit !== undefined && v !== '') { - v += '\u202F' + opts.unit; - } - v = document.createTextNode(v); - } - - const tr = document.createElement('tr'); - tbody.appendChild(tr); - - const td1 = document.createElement('td'); - tr.appendChild(td1); - const textDesc = document.createTextNode(desc); - td1.appendChild(textDesc); - - const td2 = document.createElement('td'); - tr.appendChild(td2); - td2.appendChild(v); - if (opts.classes !== undefined) { - opts.classes.forEach((c) => td2.classList.add(c)); - } - }); - - const content = document.createElement('div'); - if (def.popoverTitle !== undefined) { - const h = document.createElement('h6'); - content.appendChild(h); - const textNode = document.createTextNode(def.popoverTitle); - h.appendChild(textNode); + const measureButton = document.getElementById('measure-button') + .getElementsByTagName('button')[0]; + MAP.on('singleclick', function(event) { + if (measureButton.getAttribute('aria-expanded') === 'true') { + /* skip popover while measuring */ + return; } - - content.appendChild(table); - return content; - }; - - const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0]; - map.on('singleclick', function(event) { - /* clear the overlay list */ - featureOverlayLayer.setVisible(false); - featureOverlayLayer.changed(); - overlayAttributes = []; - overlayAttrIdx = 0; + disposeFeatureOverlay(); /* dispose any pre-existing popover if not in detached mode */ - popover = Popover.getInstance(popup); - if (popover !== null) { - const popoverTip = popover.tip; - if (popoverTip !== null && !popoverTip.classList.contains('popover-detached')) { - popover.dispose(); - } + popover = Popover.getInstance(popupOverlay.element); + if (popover?.tip != null && !popover.tip.classList.contains('popover-detached')) { + popover.dispose(); } - const size = map.getSize(); - if (size[0] < 576 || size[1] < 576) { - return; + if (window.innerWidth < 200) { + return; /* skip popover if the map is too small */ } /* unclear how many feature we'll find, don't render prev/next buttons for now */ @@ -3821,45 +5446,47 @@ const [vectorLayers, featureOverlayLayer] = (function() { btnNext.classList.add('d-none', 'disabled'); /* never start in maximized mode */ - if (popover !== null && popover.tip !== null) { + if (popover?.tip != null) { popover.tip.classList.remove('popover-maximized'); } btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand'); btnExpand.title = btnExpandTitle; btnExpand.setAttribute('aria-label', btnExpand.title); - const fetch_body = [] - map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { + const fetch_body = []; + event.map.forEachFeatureAtPixel(event.pixel, function(feature, layer) { const layerGroup = layer.get('layerGroup'); const layerName = feature.getProperties().layer; - const def = layers[layerGroup + '_' + layerName]; - if (def !== undefined && def.popover !== undefined) { + mapSources[layerGroup] ??= layer.getSource(); + const def = layerName != null ? layers[layerGroup]?.[layerName] : null; + if (def?.fields == null) { /* skip layers which didn't opt-in for popover */ - if (!fetch_body.length) { - document.body.classList.add('inprogress'); - if (popover !== null && popover.tip !== null) { - popover.tip.classList.add('inprogress'); - } - } - fetch_body.push({ - layer_group: layerGroup, - layer: layerName, - fid: feature.getId(), - }); - if (fetch_body.length >= 100) { - return true; /* enough matches already, stop detection here */ - } + return false; + } + if (fetch_body.length === 0) { + /* first feature in the list, mark cursor and detached popover as in-progress */ + document.body.classList.add('inprogress'); + popover?.tip?.classList?.add?.('inprogress'); + } + fetch_body.push({ + layer_group: layerGroup, + layer: layerName, + fid: feature.getId() ?? -1, + }); + if (fetch_body.length >= 100) { + return true; /* enough matches already, stop detection here */ } }, { hitTolerance: 5, checkWrapped: false, - layerFilter: (l) => l !== null && l.get('layerGroup') !== undefined, + layerFilter: (lyr) => lyr.get('layerGroup') != null, }); if (fetch_body.length === 0) { - if (popover !== null) { - /* dispose pre-detached popover */ - popover.dispose(); + /* no feature at pixel (or only within layers which didn't opt-in for popover) */ + if (popover?.tip != null) { + /* dispose pre-detached popover */ + popover.dispose(); } return; } @@ -3868,18 +5495,26 @@ const [vectorLayers, featureOverlayLayer] = (function() { method: 'POST', body: JSON.stringify(fetch_body), headers: { - 'Content-Type': 'application/json; charset=UTF-8' - } - }).then((resp) => resp.json()) + 'Content-Type': 'application/json; charset=UTF-8', + }, + }) + .then(function(resp) { + if (resp.status === 200) { + return resp.json(); + } else { + throw new Error(`${resp.url} [${resp.status}]`); + } + }) .then(function(data) { /* the data is received from the CGI in the order it was sent */ /* TODO optimizations on the CGI would break the above assumption, so the * decoded JSON response would need to be reordered to match fetch_body */ - overlayAttributes = data + overlayAttributes = data; if (overlayAttributes.length === 0) { - if (popover !== null) { - /* dispose pre-detached popover */ - popover.dispose(); + /* couldn't fetch any attribute for feature(s) at pixel */ + if (popover?.tip != null) { + /* dispose pre-detached popover */ + popover.dispose(); } return; } @@ -3891,14 +5526,14 @@ const [vectorLayers, featureOverlayLayer] = (function() { btnPrev.classList.remove('d-none'); pageNode.classList.remove('d-none'); } - if (popover === null || popover.tip === null) { + if (popover?.tip == null) { /* create a new popover (we're not already showing one in detached mode) */ pageNum.innerHTML = (overlayAttrIdx + 1).toString(); popupOverlay.setPosition(event.coordinate); const attr = overlayAttributes[0]; - updateFeatureOverlayLayer(attr.layer_group, attr.layer, attr.ogc_fid); - popover = new Popover(popup, { + highlightFeature(attr.layer_group, attr.layer, attr.ogc_fid); + popover = new Popover(popupOverlay.element, { template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div>' + '<div class="popover-header"></div><div class="popover-body"></div></div>', title: header, @@ -3906,7 +5541,7 @@ const [vectorLayers, featureOverlayLayer] = (function() { html: true, placement: 'right', fallbackPlacements: ['right', 'left', 'bottom', 'top'], - container: container0, + container: CONTAINER_STOPEVENT, }); popover.show(); } @@ -3916,8 +5551,552 @@ const [vectorLayers, featureOverlayLayer] = (function() { popover.tip.classList.remove('inprogress'); } }) + .catch(function(e) { + console.log(e); + }) .finally(function() { + /* remove in-progress marking on the cursor */ document.body.classList.remove('inprogress'); }); }); -}()); + + return disposePopover; +})(); + +/* age filter dialog */ +const ageFilterSetActive = (function() { + const panel = document.getElementById('age-filter-modal'); + + let updateHelpTextTimeoutID = null; + const updateHelpTextState = Object.seal({ value: null }); + const dialog_setup = (function() { + const dialog = document.createElement('div'); + dialog.classList.add('modal-dialog', 'modal-dialog-centered'); + panel.appendChild(dialog); + + const content = document.createElement('div'); + content.classList.add('modal-content'); + dialog.appendChild(content); + + const header = document.createElement('div'); + header.classList.add('modal-header'); + content.appendChild(header); + + const title = document.createElement('div'); + title.classList.add('h4', 'm-0'); + header.appendChild(title); + title.innerHTML = 'Filtrera objekt efter ålder'; + + const btn_close = document.createElement('button'); + btn_close.classList.add('btn-close'); + btn_close.type = 'button'; + btn_close.title = 'Stäng'; + btn_close.setAttribute('aria-label', btn_close.title); + btn_close.setAttribute('data-bs-dismiss', 'modal'); + header.appendChild(btn_close); + + const body = document.createElement('div'); + body.classList.add('modal-body'); + content.appendChild(body); + + const p = document.createElement('p'); + p.classList.add('small', 'text-muted', 'age-filter-infotext'); + body.appendChild(p); + p.appendChild(document.createTextNode('Här kan du t.ex. filtrera bort gamla objekt. ' + + 'I nulaget gäller filtret endast objekt inom ')); + ['mineralrättigheter', 'skogsbruk', 'vindbruk'].forEach(function(l, idx, arr) { + if (idx > 0) { + p.appendChild(document.createTextNode(idx < arr.length-1 ? ', ' : ' eller ')); + } + const i = document.createElement('i'); + i.innerHTML = l; + p.appendChild(i); + }); + p.appendChild(document.createTextNode(' skikter.')); + + const form = document.createElement('form'); + form.action = '#'; + form.id = 'age-filter-form'; + body.appendChild(form); + + const btn_group = document.createElement('div'); + btn_group.classList.add('btn-group'); + btn_group.setAttribute('role', 'group'); + btn_group.setAttribute('aria-label', 'Välj filtreringstyp'); + form.appendChild(btn_group); + + const type_choices = Object.fromEntries([ + { id: 'relative', text: 'Åldersgräns', desc: 'som är' }, + { id: 'interval', text: 'Datumintervall', desc: 'med datum' }, + ].map(function(x) { + const radio = document.createElement('input'); + radio.classList.add('btn-check'); + radio.type = 'radio'; + radio.name = 'age-filter-type'; + radio.id = 'age-filter-type-' + x.id; + radio.required = true; + radio.setAttribute('aria-expanded', 'false'); + radio.setAttribute('autocomplete', 'off'); + btn_group.appendChild(radio); + + const lbl = document.createElement('label'); + lbl.classList.add('btn', 'btn-primary'); + lbl.setAttribute('for', radio.id); + lbl.appendChild(document.createTextNode(x.text)); + btn_group.appendChild(lbl); + + const div = document.createElement('div'); + div.classList.add('d-none', 'age-filter-settings'); + div.setAttribute('aria-hidden', 'true'); + form.appendChild(div); + + const p = document.createElement('p'); + p.classList.add('age-filter-settings-desc'); + div.appendChild(p); + const t = document.createTextNode('Visa endast objekt ' + x.desc + ':'); + p.appendChild(t); + + return [x.id, { radio: radio, div: div } ]; + })); + + Object.values(type_choices).forEach(function(type_choice) { + type_choice.radio.onclick = function(event) { + const radio = event.target; + radio.checked = true; + radio.setAttribute('aria-expanded', 'true'); + + if (type_choice === type_choices.relative) { + type_choice._update_helptext(); + } + + Object.values(type_choices).forEach(function(x) { + const isSame = radio.isEqualNode(x.radio); + if (isSame) { + x.div.classList.remove('d-none'); + x.div.setAttribute('aria-hidden', 'false'); + } else { + x.div.classList.add('d-none'); + x.div.setAttribute('aria-hidden', 'true'); + x.radio.checked = false; + x.radio.setAttribute('aria-expanded', 'false'); + } + const inputs = x.div.getElementsByTagName('input'); + for (let i = 0; i < inputs.length; i++) { + inputs[i].required = isSame; + } + }); + }; + }); + + const create_select = function(parentNode, select_label, options) { + const select = document.createElement('select'); + select.classList.add('form-select'); + select.setAttribute('aria-label', select_label); + parentNode.appendChild(select); + return [ + select, + Object.fromEntries(options.map(function(x) { + const option = document.createElement('option'); + option.appendChild(document.createTextNode(x.text)); + option.value = x.id; + select.appendChild(option); + return [x.id, option]; + })) + ]; + }; + + const format_date = function(d, s) { /* YYYY-MM-DD or YYYYMMDD */ + return d.getFullYear() .toString().padStart(4, '0') + (s ?? '-') + + (d.getMonth()+1).toString().padStart(2, '0') + (s ?? '-') + + d.getDate() .toString().padStart(2, '0'); + }; + const parse_date = (m) => new Date( /* YYYY-MM-DD */ + parseInt(m.slice(0,4), 10), + parseInt(m.slice(5,7), 10)-1, + parseInt(m.slice(8,10),10), + 12 /* use 12:00:00.0 like for URL parameters */ + ); + + (function(type_choice) { + const div = document.createElement('div'); + div.classList.add('input-group'); + type_choice.div.appendChild(div); + + type_choice.operator = create_select(div, 'Under eller över åldersgränsen', [ + { id: '<=', text: 'Nyare' }, + { id: '>=', text: 'Äldre' }, + ]); + + const span = document.createElement('span'); + span.classList.add('input-group-text'); + div.appendChild(span); + span.innerHTML = 'än'; + + const input_quantity = type_choice.quantity = document.createElement('input'); + input_quantity.classList.add('form-control'); + input_quantity.type = 'number'; + input_quantity.min = 0; + input_quantity.max = 65535; + input_quantity.placeholder = 'Antal'; + input_quantity.setAttribute('aria-label', input_quantity.placeholder); + div.appendChild(input_quantity); + + type_choice.unit = create_select(div, 'Enhet', [ + { id: 'd', text: 'dagar' }, + { id: 'w', text: 'veckor' }, + { id: 'm', text: 'månader' }, + { id: 'y', text: 'år' }, + ]); + + const p = document.createElement('p'); + p.classList.add('small', 'text-muted', 'invisible'); + type_choice.div.appendChild(p); + p.appendChild(document.createTextNode('Det vill säga med datum ')); + const span_date = document.createElement('span'); + p.appendChild(span_date); + p.appendChild(document.createTextNode(' eller ')); + const span_direction = document.createElement('span'); + p.appendChild(span_direction); + p.appendChild(document.createTextNode('.')); + + type_choice._update_helptext = function() { + const d = ageFilterSettings.getRelativeDate( + parseInt(type_choice.quantity.value, 10), + type_choice.unit[0].value + ); + const operator = type_choice.operator[0].value; + if (d == null || operator === '') { + p.classList.add('invisible'); + return null; + } else { + /* update help text */ + span_date.innerHTML = format_date(d); + span_direction.innerHTML = { '>=':'tidigare', '<=':'senare' }[operator]; + p.classList.remove('invisible'); + return d; + } + }; + + type_choice.operator[0].onchange = input_quantity.onchange + = type_choice.unit[0].onchange + = function() { + const d = type_choice._update_helptext(); + if (d != null) { + /* propagate to interval tab */ + const operator = type_choice.operator[0].value; + const [d1, d2] = { '>=': [new Date(1900, 0, 1, 12), d], /* between 1900-01-01 and d */ + '<=': [d, new Date()], /* between d and today */ + }[operator]; + type_choices.interval.from.value = format_date(d1); + type_choices.interval.to.value = format_date(d2); + } + }; + })(type_choices.relative); + + (function(type_choice) { + const div = document.createElement('div'); + div.classList.add('input-group'); + type_choice.div.appendChild(div); + + const span1 = document.createElement('span'); + span1.classList.add('input-group-text'); + span1.innerHTML = 'Mellan'; + span1.id = 'age-filter-interval-from'; + div.appendChild(span1); + + const date1 = type_choice.from = document.createElement('input'); + date1.classList.add('form-control'); + date1.type = 'date'; + date1.setAttribute('aria-label', 'Intervallstart'); + date1.setAttribute('aria-describedby', span1.id); + div.appendChild(date1); + + const span2 = document.createElement('span'); + span2.classList.add('input-group-text'); + span2.innerHTML = 'och'; + span2.id = 'age-filter-interval-to'; + div.appendChild(span2); + + const date2 = type_choice.to = document.createElement('input'); + date2.classList.add('form-control'); + date2.type = 'date'; + date2.setAttribute('aria-label', 'Intervallstop'); + date2.setAttribute('aria-describedby', span2.id); + div.appendChild(date2); + + /* propagate to relative tab by trying to preserve the operator and unit */ + const propagate_to_relative = function() { + let d; + switch (type_choices.relative.operator[0].value) { + case '<=': + d = parse_date(date1.value); + break; + case '>=': + d = parse_date(date2.value); + break; + } + const delta = (new Date().getTime() - d.getTime()) / 86_400_000; + let v; + switch (type_choices.relative.unit[0].value) { + case 'd': + v = delta; + break; + case 'w': + v = delta/7; + break; + case 'm': + v = delta*12/365.25; + break; + case 'y': + v = delta/365.25; + break; + } + if (v != null) { + type_choices.relative.quantity.value = Math.round(v); + type_choices.relative._update_helptext(); + } + }; + + /* make sure that from_date ≤ to_date */ + date1.onchange = function() { + if (date1.value !== '' && (date2.value === '' || date1.value > date2.value)) { + date2.value = date1.value; + } + propagate_to_relative(); + }; + date2.onchange = function() { + if (date2.value !== '' && (date1.value === '' || date1.value > date2.value)) { + date1.value = date2.value; + } + propagate_to_relative(); + }; + })(type_choices.interval); + + const show_unknown_age = (function() { + const div = document.createElement('div'); + div.classList.add('form-check', 'form-switch'); + form.appendChild(div); + + const checkbox = document.createElement('input'); + checkbox.classList.add('form-check-input'); + checkbox.type = 'checkbox'; + checkbox.id = 'age-filter-show-unknown'; + checkbox.setAttribute('role', 'switch'); + checkbox.checked = ageFilterSettings.show_unknown; + div.appendChild(checkbox); + + const lbl = document.createElement('label'); + lbl.classList.add('form-check-label'); + lbl.setAttribute('for', checkbox.id); + lbl.innerHTML = 'Visa även objekt med okänd datum.'; + div.appendChild(lbl); + + return checkbox; + })(); + + const footer = document.createElement('div'); + footer.classList.add('modal-footer'); + content.appendChild(footer); + + const btn_cancel = document.createElement('button'); + btn_cancel.classList.add('btn', 'btn-secondary'); + btn_cancel.type = 'button'; + btn_cancel.innerHTML = 'Återställ'; + footer.appendChild(btn_cancel); + + const btn_apply = document.createElement('input'); + btn_apply.classList.add('btn', 'btn-primary'); + btn_apply.type = 'submit'; + btn_apply.value = 'Filter'; + btn_apply.setAttribute('form', form.id); + footer.appendChild(btn_apply); + + btn_cancel.onclick = function() { + /* deactivate deactivate the filter but preserve its settings */ + ageFilterSettings.active = false; + const params = new URLSearchParams(window.location.hash.substring(1)); + params.delete('age-filter'); + params.delete('show-unknown-age'); + location.hash = '#' + params.toString(); + + modal.hide(); + }; + + form.onsubmit = function(event) { + event.preventDefault(); + const [filter_type, filter_settings] = Object.entries(type_choices).filter( (x) => x[1].radio.checked )[0]; + let param; + ageFilterSettings._min_ts = ageFilterSettings._max_ts = null; + switch (filter_type) { + case 'relative': { + /* save relative value so it can be autoshifted later */ + const operator = ageFilterSettings.operator = filter_settings.operator[0].value; + ageFilterSettings.quantity = parseInt(filter_settings.quantity.value, 10); + ageFilterSettings.unit = filter_settings.unit[0].value; + param = {'<=':'-', '>=':''}[operator]; + param += ageFilterSettings.quantity.toString() + ageFilterSettings.unit; + break; + } + case 'interval': { + const date1 = ageFilterSettings.from = parse_date(filter_settings.from.value); + const date2 = ageFilterSettings.to = parse_date(filter_settings.to.value); + param = format_date(date1, '') + '-' + format_date(date2, ''); + break; + } + default: + return; + } + ageFilterSettings.type = filter_type; + ageFilterSettings.show_unknown = show_unknown_age.checked; + ageFilterSettings.active = true; + + const params = new URLSearchParams(window.location.hash.substring(1)); + params.set('age-filter', param); + params.set('show-unknown-age', show_unknown_age.checked ? '1' : '0'); + location.hash = '#' + params.toString(); + + modal.hide(); + }; + + const updateHelpTextTimeout = function cb() { + const type_choice = type_choices.relative; + const [ms, b] = getDelay(updateHelpTextState); + if (type_choice.radio.checked && b) { + type_choice._update_helptext(); + } + updateHelpTextTimeoutID = setTimeout(cb, ms); + }; + + /* Now that all elements have been added to the form, click the radio + * button to show the relevant <div>, mark its fields as required and + * fills them. The function is run whenever the modal is shown. */ + return function() { + const type_choice = type_choices[ageFilterSettings.type]; + type_choice.radio.click(); + + switch (ageFilterSettings.type) { + case 'relative': { + Object.entries(type_choice.operator[1]).map(function([id, option]) { + option.selected = id === ageFilterSettings.operator; + }); + type_choice.quantity.value = ageFilterSettings.quantity.toString(); + Object.entries(type_choice.unit[1]).map(function([id, option]) { + option.selected = id === ageFilterSettings.unit; + }); + type_choice.quantity.dispatchEvent(new Event('change')); /* propagate to absolute */ + break; + } + case 'interval': { + type_choice.from.value = format_date(ageFilterSettings.from); + type_choice.to.value = format_date(ageFilterSettings.to); + type_choice.from.dispatchEvent(new Event('change')); /* propagate to relative */ + break; + } + } + + if (updateHelpTextTimeoutID == null) { + const ms = getDelay(updateHelpTextState)[0]; + updateHelpTextTimeoutID = setTimeout(updateHelpTextTimeout, ms); + } + }; + })(); + + /* Retun the number of milliseconds left for the next full 15min at the + * wall clock, along with a boolean indicating whether we just passed + * midnight. + * Keep using setTimeout(,15min) since unlike setInterval() it allows us to avoid clock + * skews by adjusting delays */ + const getDelay = function(state) { + const i = 900, d = new Date(); /* at *:00,15,30,45:00.50 */ + const sec = (d.getHours()*60 + d.getMinutes())*60 + d.getSeconds(); + /* add 50ms to ensure formatting the date doesn't end up on the previous period */ + const ms = (Math.floor(sec/i)*i + i - sec)*1000 - d.getMilliseconds() + 50; + const v = state.value, v2 = d.getDate(); + const b = v == null || v !== v2; + if (b) { + state.value = v2; + } + return [ms, b]; + }; + + const modal = new Modal(panel, { + backdrop: false, + }); + + const backdrop = document.getElementById('age-filter-modal-backdrop'); + backdrop.onclick = function() { + modal.hide(); + }; + + const btn = document.getElementById('age-filter-button').getElementsByTagName('button')[0]; + if (ageFilterSettings.active) { + btn.classList.replace('btn-light', 'btn-dark'); + } + panel.addEventListener('show.bs.modal', function() { + backdrop.classList.add('modal-backdrop', 'show'); + btn.setAttribute('aria-expanded', 'true'); + btn.classList.replace('btn-light', 'btn-dark'); + }); + panel.addEventListener('hide.bs.modal', function() { + /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */ + const activeElement = document.activeElement; + if (activeElement instanceof HTMLElement) { + activeElement.blur(); + } + }); + panel.addEventListener('hidden.bs.modal', function() { + if (!ageFilterSettings.active) { + btn.classList.replace('btn-dark', 'btn-light'); + } + if (updateHelpTextTimeoutID != null) { + clearTimeout(updateHelpTextTimeoutID); + updateHelpTextState.value = null; + updateHelpTextTimeoutID = null; + } + btn.setAttribute('aria-expanded', 'false'); + backdrop.classList.remove('modal-backdrop', 'show'); + }); + + btn.onclick = function() { + disposePopover(); + dialog_setup(); + modal.show(); + }; + + return (function() { /* setter for ageFilterSettings.active */ + let timeoutID = null; + const state = { value: null }; + const fun = function cb() { + const [ms, b] = getDelay(state); + if (b && ageFilterSettings.type === 'relative') { + ageFilterSettings.setupMinMax(); + changed(); + } + timeoutID = setTimeout(cb, ms); + }; + const changed = function() { + Object.values(mapLayers) + .filter((lyr) => lyr?.get('canFilterByAge')) + .forEach((lyr) => lyr.changed()); + }; + const setter = function(active) { + if (active) { + ageFilterSettings.setupMinMax(); + } + ageFilterSettings._active = active; + changed(); + if (active && ageFilterSettings.type === 'relative') { + if (timeoutID == null) { + timeoutID = setTimeout(fun, getDelay(state)[0]); + } + } else if (timeoutID != null) { + clearTimeout(timeoutID); + state.value = null; + timeoutID = null; + } + }; + /* initial activation from param URL */ + setter(ageFilterSettings._active); + return setter; + })(); +})(); diff --git a/package-lock.json b/package-lock.json index 73f9e67..3ab4cba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,24 @@ "version": "0.0.1", "license": "AGPL-3.0-or-later", "dependencies": { + "@fontsource/inter": "^5.2.8", "bootstrap": "5.3.x", "bootstrap-icons": "1.13.x", - "ol": "10.5.x", - "proj4": "2.17.x" + "ol": "10.6.x", + "proj4": "2.19.x" }, "devDependencies": { - "vite": "^5.4.19" + "@eslint/css": "^0.9.0", + "@eslint/js": "^9.29.0", + "eslint": "^9.37.0", + "globals": "^16.4.0", + "vite": "^7.1.9" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -31,13 +36,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -47,13 +52,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -63,13 +68,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -79,13 +84,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -95,13 +100,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -111,13 +116,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -127,13 +132,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -143,13 +148,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -159,13 +164,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -175,13 +180,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -191,13 +196,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -207,13 +212,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -223,13 +228,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -239,13 +244,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -255,13 +260,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -271,13 +276,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -287,13 +292,29 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -303,13 +324,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -319,13 +356,29 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -335,13 +388,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -351,13 +404,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -367,13 +420,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -383,13 +436,266 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/css": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/css/-/css-0.9.0.tgz", + "integrity": "sha512-fq8hYnjipdzVDSU/bXqv7qlvdjDA27Nq7DhQXzlPElLlVon3lnKovIM/6HaUrq1bz7EPgRobr+vOhpeM6z0X4w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.14.0", + "@eslint/css-tree": "^3.6.1", + "@eslint/plugin-kit": "^0.3.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/css-tree": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-3.6.6.tgz", + "integrity": "sha512-C3YiJMY9OZyZ/3vEMFWJIesdGaRY6DmIYvmtyxMT934CbrOKqRs+Iw7NWSRlJQEaK4dPYy2lZ2y1zkaj8z0p5A==", + "dev": true, + "dependencies": { + "mdn-data": "2.23.0", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@petamoriken/float16": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz", - "integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==" + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==" }, "node_modules/@popperjs/core": { "version": "2.11.8", @@ -402,9 +708,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", - "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "cpu": [ "arm" ], @@ -415,9 +721,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", - "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", "cpu": [ "arm64" ], @@ -428,9 +734,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", - "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "cpu": [ "arm64" ], @@ -441,9 +747,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", - "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "cpu": [ "x64" ], @@ -454,9 +760,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", - "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "cpu": [ "arm64" ], @@ -467,9 +773,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", - "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "cpu": [ "x64" ], @@ -480,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", - "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "cpu": [ "arm" ], @@ -493,9 +799,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", - "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "cpu": [ "arm" ], @@ -506,9 +812,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", - "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "cpu": [ "arm64" ], @@ -519,9 +825,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", - "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "cpu": [ "arm64" ], @@ -531,10 +837,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", - "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "cpu": [ "loong64" ], @@ -544,10 +850,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", - "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "cpu": [ "ppc64" ], @@ -558,9 +864,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", - "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "cpu": [ "riscv64" ], @@ -571,9 +877,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", - "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "cpu": [ "riscv64" ], @@ -584,9 +890,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", - "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "cpu": [ "s390x" ], @@ -597,9 +903,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", - "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", "cpu": [ "x64" ], @@ -610,9 +916,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", - "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", "cpu": [ "x64" ], @@ -622,10 +928,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", - "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", "cpu": [ "arm64" ], @@ -636,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", - "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", "cpu": [ "ia32" ], @@ -648,10 +967,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", - "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", "cpu": [ "x64" ], @@ -662,9 +994,15 @@ ] }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/rbush": { @@ -672,10 +1010,74 @@ "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz", "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==" }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "node_modules/bootstrap": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", - "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", "funding": [ { "type": "github", @@ -705,49 +1107,414 @@ } ] }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/earcut": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", - "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint/node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -762,11 +1529,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/geographiclib-geodesic": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.1.1.tgz", - "integrity": "sha512-lkd8EUkPSByobWu9BPMHTdYA5AUZxOa8McmUNtBE9KrvUJEvSADnN6gTDmhXbi6NzdA16LtWLpSxLE/lIIRhyA==" - }, "node_modules/geotiff": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", @@ -785,16 +1547,207 @@ "node": ">=10.19" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/lerc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/mdn-data": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.23.0.tgz", + "integrity": "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==", + "dev": true + }, "node_modules/mgrs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==" }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -813,10 +1766,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/ol": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ol/-/ol-10.5.0.tgz", - "integrity": "sha512-nHFx8gkGmvYImsa7iKkwUnZidd5gn1XbMZd9GNOorvm9orjW9gQvT3Naw/MjIasVJ3cB9EJUdCGR2EFAulMHsQ==", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz", + "integrity": "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==", "dependencies": { "@types/rbush": "4.0.0", "earcut": "^3.0.0", @@ -829,16 +1788,93 @@ "url": "https://opencollective.com/openlayers" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-headers": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pbf": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", @@ -856,10 +1892,22 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -876,7 +1924,7 @@ } ], "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -884,14 +1932,25 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/proj4": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.17.0.tgz", - "integrity": "sha512-BqVoruVAOUgkw5U9Ns76+E2nHZG0Y42tbkC+0BpyqjhwPIai29hoivyQoyelEKFSfaV3zkR3NqPRD0EwPM4Wug==", + "version": "2.19.10", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.19.10.tgz", + "integrity": "sha512-uL6/C6kA8+ncJAEDmUeV8PmNJcTlRLDZZa4/87CzRpb8My4p+Ame4LhC4G3H/77z2icVqcu3nNL9h5buSdnY+g==", "dependencies": { - "geographiclib-geodesic": "^2.1.1", "mgrs": "1.0.0", "wkt-parser": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/ahocevar" } }, "node_modules/protocol-buffers-schema": { @@ -899,6 +1958,15 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/quick-lru": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", @@ -923,6 +1991,15 @@ "quickselect": "^3.0.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -932,12 +2009,12 @@ } }, "node_modules/rollup": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", - "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "dev": true, "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -947,29 +2024,52 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.1", - "@rollup/rollup-android-arm64": "4.41.1", - "@rollup/rollup-darwin-arm64": "4.41.1", - "@rollup/rollup-darwin-x64": "4.41.1", - "@rollup/rollup-freebsd-arm64": "4.41.1", - "@rollup/rollup-freebsd-x64": "4.41.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", - "@rollup/rollup-linux-arm-musleabihf": "4.41.1", - "@rollup/rollup-linux-arm64-gnu": "4.41.1", - "@rollup/rollup-linux-arm64-musl": "4.41.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-musl": "4.41.1", - "@rollup/rollup-linux-s390x-gnu": "4.41.1", - "@rollup/rollup-linux-x64-gnu": "4.41.1", - "@rollup/rollup-linux-x64-musl": "4.41.1", - "@rollup/rollup-win32-arm64-msvc": "4.41.1", - "@rollup/rollup-win32-ia32-msvc": "4.41.1", - "@rollup/rollup-win32-x64-msvc": "4.41.1", + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -979,21 +2079,85 @@ "node": ">=0.10.0" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1002,19 +2166,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -1035,6 +2205,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -1043,16 +2219,52 @@ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==" }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wkt-parser": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.2.tgz", "integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/xml-utils": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==" }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zstddec": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", diff --git a/package.json b/package.json index ab316e6..33a40a8 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,23 @@ "version": "0.0.1", "license": "AGPL-3.0-or-later", "scripts": { + "lint": "npx eslint --color --format stylish", "start": "vite", "build": "vite build", "serve": "vite preview" }, "devDependencies": { - "vite": "^5.4.19" + "@eslint/css": "^0.9.0", + "@eslint/js": "^9.29.0", + "eslint": "^9.37.0", + "globals": "^16.4.0", + "vite": "^7.1.9" }, "dependencies": { + "@fontsource/inter": "^5.2.8", "bootstrap": "5.3.x", "bootstrap-icons": "1.13.x", - "ol": "10.5.x", - "proj4": "2.17.x" + "ol": "10.6.x", + "proj4": "2.19.x" } } @@ -1,6 +1,7 @@ @import "~bootstrap/dist/css/bootstrap.css"; @import "~bootstrap-icons/font/bootstrap-icons.css"; @import "~ol/ol.css"; +@import "~fontsource/inter/latin-400.css"; html, body { margin: 0; @@ -154,9 +155,9 @@ body.inprogress { .ol-control { background: none; border: none; - outline: none; + outline: none; /* eslint-disable-line css/use-baseline */ } -.ol-control button.btn { +.ol-control button.btn, #help-body button.btn.help-button { --bs-btn-padding-x: 0.5rem; --bs-btn-padding-y: 0.5rem; display: block; @@ -178,7 +179,7 @@ body.inprogress { color: var(--bs-btn-color); background-color: var(--bs-btn-bg); border-color: var(--bs-btn-border-color); - outline: none; + outline: none; /* eslint-disable-line css/use-baseline */ } .ol-control button.btn:hover, .ol-control button.btn:focus-visible { @@ -186,7 +187,7 @@ body.inprogress { color: var(--bs-btn-hover-color); background-color: var(--bs-btn-hover-bg); border-color: var(--bs-btn-hover-border-color); - outline: none; + outline: none; /* eslint-disable-line css/use-baseline */ } .btn-light, .btn-dark { --bs-btn-border-color: var(--bs-btn-hover-border-color); @@ -194,24 +195,36 @@ body.inprogress { #map-control-container[aria-hidden="true"], #layer-selection-panel[aria-hidden="true"], -#map-legend-panel[aria-hidden="true"] { +#map-legend-panel[aria-hidden="true"], +#measure-panel[aria-hidden="true"] { display: none; } #layer-selection-panel, -#map-legend-panel { +#map-legend-panel, +#measure-panel { position: relative; - /*min-width: min-content;*/ - /*max-width: 35%;*/ - /*width: revert;*/ - max-width: 360px; margin-left: 0; margin-right: var(--map-menu-spacing); font-size: medium; z-index: 11; + height: max-content; + max-height: 100%; pointer-events: auto; --bs-modal-color: var(--bs-body-color); --bs-modal-padding: .75rem; } +#layer-selection-panel { + /*min-width: min-content;*/ + /*max-width: 35%;*/ + /*width: revert;*/ + max-width: 360px; +} +#map-legend-panel { + max-width: 410px; +} +#measure-panel { + max-width: 15em; +} @keyframes fade-in { from { opacity: 0; @@ -220,8 +233,14 @@ body.inprogress { opacity: 1; } } +#map-legend-panel .modal-body { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; /* eslint-disable-line css/use-baseline */ +} #layer-selection-panel[aria-hidden="false"], -#map-legend-panel[aria-hidden="false"] { +#map-legend-panel[aria-hidden="false"], +#measure-panel[aria-hidden="false"] { animation: fade-in .25s ease-in-out both; display: block; } @@ -243,17 +262,25 @@ body.inprogress { display: none; } #layer-selection-panel, - #map-legend-panel { + #map-legend-panel, + #measure-panel { margin: var(--map-menu-spacing) 0 0 auto; } } +#map-legend-button.disabled button { + color: rgb(from var(--bs-btn-color) r g b / calc(alpha*.25)); + pointer-events: none; +} @media screen and (max-width: 200px) { #layer-selection-button, #map-legend-button, + #measure-button, + #age-filter-button, #export-to-image, #layer-selection-panel, #map-legend-panel, + #measure-panel, #map-menu .ol-full-screen { display: none; } @@ -272,23 +299,150 @@ body.inprogress { --bs-btn-padding-y: 0.4rem; } } -#modal-info { - pointer-events: auto; +#info-modal, #help-modal { + /* close the modal when clicking the backdrop */ + pointer-events: none; -webkit-user-select: text; -moz-user-select: text; - user-select: text; + user-select: text; /* eslint-disable-line css/use-baseline */ + --bs-modal-header-padding: 1rem; + --modal-info-padding-x: var(--bs-modal-header-padding); + --modal-info-padding-y: .5rem; + --modal-info-bg-light: rgba(0, 0, 0, .08); +} +#help-modal { + --modal-info-padding-x: 1rem; + --modal-info-padding-y: 1rem; } -#modal-info .modal-body ul > li { - padding: 0.05rem 0; +#info-modal .modal-header, +#help-modal .modal-header { + padding: var(--modal-info-padding-y) var(--modal-info-padding-x); } -#modal-info .modal-body a { +#info-modal .list-group-item, +#info-modal { + --bs-list-group-border-width: 1px; +} +#info-accordion { + --bs-accordion-active-bg: var(--bs-accordion-bg); + --bs-accordion-active-color: var(--bs-body-color); + --bs-accordion-btn-padding-x: 0; + --bs-accordion-btn-padding-y: .025rem; + --bs-accordion-btn-focus-box-shadow: none; + --bs-accordion-body-padding-x: 0; + --bs-accordion-body-padding-y: 0; + --bs-accordion-btn-active-bg: var(--modal-info-bg-light); + padding: 0 var(--modal-info-padding-x); +} +#info-modal .accordion-item { + border: none; +} +#info-modal .accordion-header > .accordion-button[aria-expanded="false"]:hover { + background-color: rgb(from var(--modal-info-bg-light) r g b / calc(alpha*.4)); +} +#info-modal .accordion-header > .accordion-button[aria-expanded="true"] { + background-color: var(--bs-accordion-btn-active-bg); +} +#info-modal ul.list-group > li.list-group-item { + padding: .3rem 0; + border: none; +} +#info-modal ul.list-group > li.list-group-item:last-child { + padding-bottom: var(--modal-info-padding-y); +} +#info-body > ul.list-group { + margin-top: var(--modal-info-padding-y); + border-top: var(--bs-list-group-border-width) solid var(--modal-info-bg-light); + padding: 0 var(--modal-info-padding-x); +} +#info-modal ul.list-group > li.list-group-item:not(:first-child) { + border-top: var(--bs-list-group-border-width) solid var(--modal-info-bg-light); +} +#info-accordion > .accordion-item:not(:last-child) ul.list-group > li.list-group-item:last-child { + border-bottom: var(--bs-list-group-border-width) solid var(--modal-info-bg-light); +} +#info-modal .modal-body ul.list-group > li.list-group-item:not(.text-muted):hover { + background-color: rgb(from var(--modal-info-bg-light) r g b / calc(alpha*.4)); +} +#info-body { + padding: var(--modal-info-padding-y) 0; +} +#info-body > ul.list-group > li.list-group-item p, +#info-modal .accordion-body ul.list-group > li.list-group-item p { + margin: 0; +} +#info-body > ul.list-group > li.list-group-item h6, +#info-modal .accordion-body ul.list-group > li.list-group-item h6 { + margin: .05rem 0 .15rem 0; + font-size: 1.15rem; +} +#info-modal .modal-body a { color: inherit; text-decoration: none; } -#modal-info .modal-body a:hover { +#info-modal .modal-body a:hover { opacity: .8; text-decoration: underline; } +#info-modal .modal-body .info-credits { + border-top: var(--bs-list-group-border-width) solid var(--modal-info-bg-light); + padding: var(--modal-info-padding-y) var(--modal-info-padding-x) 0 var(--modal-info-padding-x); + margin: 0; +} +#help-body { + padding: var(--modal-info-padding-y) var(--modal-info-padding-x); + hyphens: auto; /* eslint-disable-line css/use-baseline */ +} +#help-body .popover-header { + -webkit-user-select: inherit; + -moz-user-select: inherit; + user-select: inherit; /* eslint-disable-line css/use-baseline */ + border: none; + margin: 0; + padding: 0; +} +#help-body .popover-header .popover-button { + height: var(--bs-body-font-size); + width: var(--bs-body-font-size); + padding: 0; + border: none; + vertical-align: middle; +} +#help-body button.btn.help-button { + --bs-btn-padding-x: .1rem; + --bs-btn-padding-y: .1rem; + width: 1.75rem; + height: 1.75rem; + display: inline-block; + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + margin: 0; +} +#help-body .help-button { + pointer-events: none; +} +#help-body .accordion .accordion-button { + display: inline; + padding: 0; + opacity: .75; +} +#help-body .accordion .accordion-button::after { + display: inline-block; + margin: 0; + height: 1rem; + vertical-align: middle; +} +#help-body .help-button-description > button.btn.help-button { + margin-right: .5rem; +} +#help-body p:last-child { + margin-bottom: 0; +} +@media (min-width: 992px) { + #help-body { + text-align: justify; + } +} .ol-overlaycontainer-stopevent .modal-backdrop.show { pointer-events: auto; @@ -337,18 +491,146 @@ body.inprogress { #layer-selection-panel .accordion-item .form-check > .form-check-label { display: inline; } +#map-legend-panel .list-group-flush > .list-group-item { + border: none; + padding: 0; +} +#map-legend-panel .modal-body > ul.list-group > li.list-group-item:not(.d-none) ~ li.list-group-item:not(.d-none) { + border-top: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); + margin-top: .5rem; + padding-top: .5rem; +} +#map-legend-panel .list-group .list-group-item.h4, +#map-legend-panel .list-group .list-group-item.h5, +#map-legend-panel .list-group .list-group-item.h6 { + margin: 0 0 .25rem 0; +} +#map-legend-panel .list-group .list-group-item:not(.d-none) ~ .list-group-item.h5:not(.d-none) { + margin-top: .125rem; +} +#map-legend-panel .list-group .list-group-item.h4 { + font-size: 1.3rem; +} +#map-legend-panel .list-group .list-group-item.h5 { + font-size: 1.05rem; +} +#map-legend-panel .list-group .list-group-item.h6 { + font-size: 1rem; +} +#map-legend-panel .list-group .list-group-item .map-legend-symbol { + /* 24px */ + height: calc(var(--bs-body-line-height) * var(--bs-body-font-size)) !important; +} +#map-legend-panel .list-group .list-group-item .map-legend-symbol > * { + width: 32px; + margin-right: .5rem; +} +#map-legend-panel .list-group .list-group-item .map-legend-symbol img { + margin-left: auto; + margin-right: auto; + display: block; +} +#map-legend-panel .list-group .list-group-item:not(.h4,.h5,.h6):hover { + background-color: var(--bs-list-group-action-hover-bg); +} + +#age-filter-modal .modal-body .btn-group { + width: 100%; +} +#age-filter-modal .age-filter-infotext { + margin: 0 0 var(--bs-modal-padding) 0; +} +#age-filter-form .age-filter-settings .age-filter-settings-desc { + margin: var(--bs-modal-padding) 0; +} +#age-filter-form .age-filter-settings { + margin: 0 0 var(--bs-modal-padding) 0; +} +#age-filter-form .age-filter-settings p.text-muted { + margin: 0; +} +#age-filter-modal p { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; /* eslint-disable-line css/use-baseline */ +} +#measure-panel .modal-header { + border-bottom: none; + padding: var(--bs-modal-padding); +} +#measure-panel .modal-header .h5 { + font-size: 1.25rem; + margin: 0; +} +#measure-panel .modal-body { + padding-top: 0; +} +#measure-panel .modal-body .btn-group > .btn.btn-lg { + --bs-btn-padding-y: .375rem; /* revert to non-large size */ +} +#age-filter-modal .btn-group > .btn-check:not(:checked) + label.btn:hover, +#measure-panel .btn-group > .btn-check:not(:checked) + label.btn:hover { + background: color-mix(in srgb, var(--bs-btn-bg) 35%, var(--bs-btn-hover-bg)); /* eslint-disable-line css/use-baseline */ + color: var(--bs-btn-hover-color); +} +#measure-panel .measure-value { + font-family: Inter; + font-variant-numeric: tabular-nums; + --measure-value-padding: .75rem; + --measure-value-border-width: 3px; + --measure-value-font-size: calc(var(--bs-body-font-size) * 1.5); + --bs-border-opacity: .75; + border-width: var(--measure-value-border-width); + border-style: solid; + border-color: rgb(from var(--bs-secondary) r g b / .75); + border-radius: var(--bs-border-radius-lg); + background: rgb(from var(--bs-secondary-bg-subtle) r g b / calc(alpha*.30)); + height: calc(var(--bs-body-line-height) * var(--bs-body-font-size) * var(--measure-value-font-size) + + 2*var(--measure-value-padding) + 2*var(--measure-value-border-width)); + padding: var(--measure-value-padding) var(--measure-value-padding); + margin: calc(var(--measure-value-padding)*1.5) 0; + display: flex; + justify-content: flex-end; /* overflow on the left */ + overflow: hidden; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; /* eslint-disable-line css/use-baseline */ +} +#measure-panel .measure-value :not(.measure-unit) { + font-size: var(--measure-value-font-size); + font-weight: 400; +} +#measure-panel .measure-value .measure-unit:before { + font-size: calc(.5*(var(--measure-value-font-size) + var(--bs-body-font-size))); + content: '\00A0'; /* U+00A0 NO-BREAK SPACE */ +} +#measure-panel .measure-value .measure-unit { + font-size: var(--bs-body-font-size); + line-height: calc(var(--bs-body-line-height) * var(--measure-value-font-size)); +} +#measure-panel .btn-group { + width: 100%; +} .popover { --bs-popover-header-padding-x: .75rem; --bs-popover-header-padding-y: .5rem; - --bs-popover-body-padding-x: .45rem; + --bs-popover-body-offset-x: .3rem; + --bs-popover-body-padding-x: calc(var(--bs-popover-header-padding-x) - var(--bs-popover-body-offset-x)); --bs-popover-body-padding-y: .5rem; --bs-popover-header-bg: var(--bs-popover-bg); --bs-popover-zindex: 1000; - --bs-popover-max-width: 450px; + --bs-popover-max-width: min(450px, 90vw); width: var(--bs-popover-max-width); pointer-events: auto; } +.popover.popover-maximized { + --bs-popover-header-padding-x: 1rem; + --bs-popover-header-padding-y: 1rem; + --bs-popover-body-padding-y: 1rem; + --bs-popover-body-offset-x: .5rem; + --bs-popover-font-size: 1rem; +} .popover.inprogress > .popover-header, .popover.inprogress > .popover-body { filter: opacity(70%); @@ -357,42 +639,63 @@ body.inprogress { .popover-header { -webkit-user-select: none; -moz-user-select: none; - user-select: none; + user-select: none; /* eslint-disable-line css/use-baseline */ --bs-heading-color: var(--bs-popover-header-color); } +.popover-header h5, .popover-header h6 { + line-height: var(--bs-body-line-height); + vertical-align: middle; + margin: 0; +} .popover-body { -webkit-user-select: text; -moz-user-select: text; - user-select: text; + user-select: text; /* eslint-disable-line css/use-baseline */ max-height: 40vh; overflow: auto auto; } +.popover.popover-maximized .popover-body { + max-height: calc(100% - 2*var(--bs-popover-header-padding-y) - var(--bs-body-line-height)*var(--bs-popover-font-size) - var(--bs-popover-border-width)); +} .popover-body h6, .popover-body h5 { - margin-bottom: .25rem; + margin-bottom: var(--bs-popover-body-padding-y); margin-left: calc(var(--bs-popover-header-padding-x) - var(--bs-popover-body-padding-x)); margin-right: calc(var(--bs-popover-header-padding-x) - var(--bs-popover-body-padding-x)); } +.popover.popover-maximized .popover-body h5, +.popover.popover-maximized .popover-body h6 { + font-size: calc(1.2 * var(--bs-popover-font-size)); +} .popover-body .table { --bs-table-bg: none; - --bs-table-hover-bg: rgba(0, 0, 0, 0.04); + --bs-table-hover-bg: rgba(0, 0, 0, .04); + --bs-table-bg-padding-y: .1rem; margin: 0; } -.popover-body table > tbody > tr > td:first-child { +.popover.popover-maximized .popover-body .table { + --bs-table-hover-bg: none; + --bs-table-bg-padding-y: .5rem; +} +.popover.popover-maximized .popover-body .table tr:nth-of-type(2n+1) { + background: rgba(0, 0, 0, .05); +} +.popover-body table > tbody > tr > th[scope="row"] { font-style: italic; - width: 9.75em; padding-right: .75em; + font-weight: normal; } +.popover .popover-body table > tbody > tr > th[scope="row"] { + width: 35%; +} +.popover-body table > tbody > tr > th[scope="row"], .popover-body table > tbody > tr > td { - padding: 0.1rem 0.3rem; + padding: var(--bs-table-bg-padding-y) var(--bs-popover-body-offset-x); } .feature-attr-mrr-license-id, .feature-attr-dnr, .feature-objid, .feature-orgnr { font-family: var(--bs-font-monospace); word-wrap: break-word; } -.popover-header h6 { - margin: 0; -} /* inspired from bootstrap's .btn-close */ .popover-header .popover-button { box-sizing: content-box; @@ -410,7 +713,7 @@ body.inprogress { opacity: .75; } .popover-header .popover-button:focus { - outline: 0; + outline: 0; /* eslint-disable-line css/use-baseline */ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); opacity: 1; } diff --git a/vite.config.js b/vite.config.js index 68076e9..2e513f9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,3 +1,6 @@ +/* eslint-disable */ +import path from 'path'; + export default { build: { sourcemap: true, @@ -18,9 +21,10 @@ export default { }, resolve: { alias: { - "~bootstrap": "node_modules/bootstrap", - "~bootstrap-icons": "node_modules/bootstrap-icons", - "~ol": "node_modules/ol", + "~bootstrap": path.resolve(__dirname, "node_modules/bootstrap"), + "~bootstrap-icons": path.resolve(__dirname, "node_modules/bootstrap-icons"), + "~ol": path.resolve(__dirname, "node_modules/ol"), + "~fontsource": path.resolve(__dirname, "node_modules/@fontsource"), } }, server: { |
