1/* 2 * Sognegrænsekort - entry-modul, fælles for sognegraenserv3.php og 3 * sognegraenserv4.php. 4 * 5 * Siderne adskiller sig kun ved, hvor lagene hentes fra: v3 bruger KML, v4 6 * sætter geoJson i konfigurationen og henter GeoJSON bygget på sognegrænserne 7 * fra DAGI (se layers.js). 8 * 9 * Læser konfigurationen fra PHP (#sg-config), indlæser Google Maps med den 10 * moderne bootstrap-loader (importLibrary) og binder lag, popups, panel, 11 * zoom-bånd og klik-navigation sammen. 12 * 13 * Zoom-bånd (som v2's zoomLevelArr): <=7 Stifter, 8 Provstier, 9 Sogne, 14 * >=10 Sogn+Kirker. 15 */ 16 17import { resolveIdsAt, EmptyLayerError } from './api.js'; 18import { MapLayers, LEVELS } from './layers.js'; 19import { buildStiftPopup, buildProvstiPopup, buildSognPopup, buildSognKirkerPopup } from './popup.js'; 20import { Panel } from './panel.js'; 21 22const cfg = JSON.parse(document.getElementById('sg-config').textContent); 23 24const DEFAULT_CENTER = { lat: 55.844217, lng: 10.59494 }; 25 26// Kortets tilstand - svarer til v2's globale variabler 27const state = { 28 level: -1, 29 stiftId: cfg.stiftId, 30 provstiId: cfg.provstiId, 31 sogneId: cfg.sogneId, 32 lastEvent: null, // 'map' = klik på selve kortet (centrér på klikpunktet) 33 clickPos: null, 34}; 35 36// Kasserer forældede async-svar ved hurtig zoom/klik 37let loadToken = 0; 38 39let map; 40let infoWindow; 41let clickMarker; 42let layers; 43let panel; 44 45/* Google Maps' officielle bootstrap-loader (inline udgave af "Dynamic Library Import") */ 46/* prettier-ignore */ 47(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loaded once."):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({ 48 key: cfg.mapsApiKey, 49 v: 'quarterly', 50 language: 'da', 51 region: 'DK', 52}); 53 54function bandForZoom(zoom) { 55 if (zoom <= cfg.zoomLevels[0]) return LEVELS.STIFTER; 56 if (zoom < cfg.zoomLevels[2]) return LEVELS.PROVSTIER; 57 if (zoom < cfg.zoomLevels[3]) return LEVELS.SOGNE; 58 return LEVELS.SOGN_KIRKER; 59} 60 61/* 62 * Zoom-intervaller pr. lag (v4: cfg.zoomRanges = [[min, max], ...] pr. niveau). 63 * Intervallerne overlapper, så et lag bliver stående, indtil man zoomer ud af 64 * netop dets interval - man kan altså komme tættere på stifter og provstier, 65 * før der skiftes. Ved skift vælges det nærmeste lag i zoom-retningen, hvis 66 * interval indeholder zoomet. Uden zoomRanges (v3) bruges de faste bånd. 67 * 68 * max er inklusiv; zoom kan være fraktioneret på vektorkort, så et interval 69 * [8, 11] dækker 8 <= zoom < 12. 70 */ 71function levelForZoom(zoom, current) { 72 const ranges = cfg.zoomRanges; 73 if (!Array.isArray(ranges) || ranges.length !== 4) { 74 return bandForZoom(zoom); 75 } 76 const inRange = (level) => zoom >= ranges[level][0] && zoom < ranges[level][1] + 1; 77 if (current >= 0 && inRange(current)) { 78 return current; 79 } 80 if (current >= 0 && zoom >= ranges[current][1] + 1) { 81 for (let level = current + 1; level < ranges.length; level++) { 82 if (inRange(level)) return level; 83 } 84 return LEVELS.SOGN_KIRKER; 85 } 86 if (current >= 0 && zoom < ranges[current][0]) { 87 for (let level = current - 1; level >= 0; level--) { 88 if (inRange(level)) return level; 89 } 90 return LEVELS.STIFTER; 91 } 92 for (let level = 0; level < ranges.length; level++) { 93 if (inRange(level)) return level; 94 } 95 return LEVELS.STIFTER; 96} 97 98/** 99 * Fælles lag-skift: henter laget, viser det og sætter center/zoom som v2. 100 * Falder tilbage til landsvisningen hvis laget er tomt. 101 */ 102async function showLayer(level, id, customZoom, panelView, panelId) { 103 const token = ++loadToken; 104 let geojson; 105 try { 106 geojson = await layers.load(level, id); 107 } catch (error) { 108 if (error instanceof EmptyLayerError && level !== LEVELS.STIFTER) { 109 // Som navigation, så landsvisningen også sætter zoom og centrum, 110 // selvom skiftet kom fra en zoom. 111 state.lastEvent = 'nav'; 112 await showStifter(); 113 return; 114 } 115 console.error('Kunne ikke indlæse kortlag', error); 116 return; 117 } 118 if (token !== loadToken) { 119 return; // et nyere lag-skift er i gang 120 } 121 122 infoWindow.close(); 123 const bounds = layers.display(level, geojson); 124 const previousLevel = state.level; 125 state.level = level; 126 127 // v4: skiftede laget, fordi brugeren zoomede, bestemmer brugeren selv zoom 128 // og centrum. Ellers ville kortet springe tilbage til lagets faste zoom, 129 // og de overlappende zoom-intervaller ville ikke kunne mærkes. 130 if (cfg.zoomRanges && state.lastEvent === 'zoom') { 131 if (panel) { 132 panel.showView(panelView, panelId); 133 } 134 return; 135 } 136 137 // Centrering som v2: klikposition ved kort-klik, ellers lagets midte 138 if (state.lastEvent === 'map' && level !== LEVELS.STIFTER) { 139 map.setCenter(state.clickPos); 140 } else if (!bounds.isEmpty()) { 141 map.setCenter(bounds.getCenter()); 142 } 143 144 // Zoom som v2: stifter/provstier har fast niveau; sogne følger customZoom; 145 // sogn+kirker sætter kun zoom når der kommes "oppefra" 146 switch (level) { 147 case LEVELS.STIFTER: 148 map.setZoom(cfg.zoomLevels[0]); 149 break; 150 case LEVELS.PROVSTIER: 151 map.setZoom(cfg.zoomLevels[1]); 152 break; 153 case LEVELS.SOGNE: 154 map.setZoom(customZoom !== -1 ? customZoom : cfg.zoomLevels[2]); 155 break; 156 case LEVELS.SOGN_KIRKER: 157 if (previousLevel < LEVELS.SOGN_KIRKER) { 158 map.setZoom(customZoom !== -1 ? customZoom : cfg.zoomLevels[3]); 159 } 160 break; 161 } 162 163 if (panel) { 164 panel.showView(panelView, panelId); 165 } 166} 167 168function showStifter() { 169 return showLayer(LEVELS.STIFTER, 0, -1, 'initielt', 0); 170} 171 172function showProvstier(id) { 173 if (id) { 174 state.stiftId = id; 175 } 176 return showLayer(LEVELS.PROVSTIER, state.stiftId, -1, 'stift', state.stiftId); 177} 178 179function showSogne(id, customZoom = -1) { 180 if (id) { 181 state.provstiId = id; 182 } 183 return showLayer(LEVELS.SOGNE, state.provstiId, customZoom, 'provsti', state.provstiId); 184} 185 186function showSognKirker(id, customZoom = -1) { 187 if (id) { 188 state.sogneId = id; 189 } 190 return showLayer(LEVELS.SOGN_KIRKER, state.sogneId, customZoom, 'sogn', state.sogneId); 191} 192 193function resetToCountry() { 194 state.lastEvent = 'nav'; 195 return showStifter(); 196} 197 198/* 199 * Navigationshandlinger til popups og panel. Popup-kaldene er parameterløse 200 * og bruger kortets aktuelle id-tilstand (som v2's onclick="getSogneLayer()"). 201 * Panelet kalder med eksplicit id. 202 */ 203const actions = { 204 showProvstier: (id) => { 205 state.lastEvent = 'nav'; 206 showProvstier(typeof id === 'number' ? id : 0); 207 }, 208 showSogne: (id) => { 209 state.lastEvent = 'nav'; 210 showSogne(typeof id === 'number' ? id : 0); 211 }, 212 showSognKirker: (id) => { 213 state.lastEvent = 'nav'; 214 showSognKirker(typeof id === 'number' ? id : 0); 215 }, 216 resetToCountry, 217}; 218 219/** Klik på en polygon/kirke i et Data-lag: opdatér id-tilstand og vis popup. */ 220function onFeatureClick(level, event) { 221 state.lastEvent = 'feature'; 222 const name = event.feature.getProperty('name'); 223 // togeojson leverer HTML-descriptions (CDATA) som { "@type": "html", value: "..." } 224 let rawDesc = event.feature.getProperty('description') || ''; 225 if (typeof rawDesc === 'object') { 226 rawDesc = rawDesc.value || ''; 227 } 228 const desc = String(rawDesc).split(';'); 229 while (desc.length < 7) { 230 desc.push(''); 231 } 232 233 let content; 234 switch (level) { 235 case LEVELS.STIFTER: 236 state.stiftId = name; 237 content = buildStiftPopup(desc, actions); 238 break; 239 case LEVELS.PROVSTIER: 240 state.provstiId = name; 241 content = buildProvstiPopup(desc, actions); 242 break; 243 case LEVELS.SOGNE: 244 state.sogneId = name; 245 content = buildSognPopup(desc, actions); 246 break; 247 case LEVELS.SOGN_KIRKER: 248 content = buildSognKirkerPopup(desc, actions); 249 break; 250 } 251 252 infoWindow.setContent(content); 253 infoWindow.setPosition(event.latLng); 254 infoWindow.open(map); 255} 256 257/** Zoom-skift: find bånd, slå id'er op for kortets centrum og skift lag. */ 258async function onZoomChanged() { 259 const band = levelForZoom(map.getZoom(), state.level); 260 if (band === state.level) { 261 return; 262 } 263 state.lastEvent = 'zoom'; 264 const ids = await resolveIdsAt(map.getCenter()); 265 // Zoomet kan have ændret sig mens opslaget kørte - er båndet ikke længere 266 // aktuelt, håndterer den seneste zoom_changed-hændelse skiftet 267 if (band !== levelForZoom(map.getZoom(), state.level) || band === state.level) { 268 return; 269 } 270 // Intet område ved kortets centrum (fx ude på havet): bliv på det aktuelle 271 // lag. Ellers hentes et tomt lag, og faldet tilbage til landsvisningen 272 // nulstiller den zoom, brugeren netop har lavet. 273 if (!ids && band !== LEVELS.STIFTER) { 274 return; 275 } 276 if (ids) { 277 Object.assign(state, ids); 278 } 279 switch (band) { 280 case LEVELS.STIFTER: 281 await showStifter(); 282 break; 283 case LEVELS.PROVSTIER: 284 await showProvstier(0); 285 break; 286 case LEVELS.SOGNE: 287 await showSogne(0); 288 break; 289 case LEVELS.SOGN_KIRKER: 290 await showSognKirker(0); 291 break; 292 } 293} 294 295/** Klik på selve kortet: markér punktet, centrér og skift til punktets område. */ 296async function onMapClick(event) { 297 state.lastEvent = 'map'; 298 state.clickPos = event.latLng; 299 clickMarker.position = event.latLng; 300 clickMarker.map = map; 301 map.setCenter(event.latLng); 302 303 const ids = await resolveIdsAt(event.latLng); 304 if (!ids) { 305 return; 306 } 307 switch (state.level) { 308 case LEVELS.PROVSTIER: 309 if (String(ids.stiftId) !== String(state.stiftId)) { 310 Object.assign(state, ids); 311 await showProvstier(0); 312 } 313 break; 314 case LEVELS.SOGNE: 315 if (String(ids.provstiId) !== String(state.provstiId)) { 316 Object.assign(state, ids); 317 await showSogne(0); 318 } 319 break; 320 case LEVELS.SOGN_KIRKER:
321 if (String(ids.sogneId) !== String(state.sogneId)) { 322 Object.assign(state, ids); 323 await showSognKirker(0); 324 } 325 break; 326 } 327 Object.assign(state, ids); 328} 329 330async function main() { 331 const { Map, InfoWindow } = await google.maps.importLibrary('maps'); 332 const { AdvancedMarkerElement } = await google.maps.importLibrary('marker'); 333 334 map = new Map(document.getElementById('map'), { 335 zoom: cfg.zoomLevels[0], 336 center: DEFAULT_CENTER, 337 mapTypeId: 'roadmap', 338 mapId: cfg.mapId, 339 clickableIcons: false, // klik på bynavne/POI'er skal ikke åbne Googles egen infoboks 340 }); 341 infoWindow = new InfoWindow(); 342 clickMarker = new AdvancedMarkerElement(); 343 layers = new MapLayers(map, onFeatureClick, cfg.geoJson === true); 344 panel = cfg.displayContent ? new Panel(document.getElementById('sg-panel'), actions) : null; 345 346 map.addListener('zoom_changed', onZoomChanged); 347 map.addListener('click', onMapClick); 348 349 // Debug-handle til test/fejlsøgning i browser-konsollen 350 window.sognegraenser = { map, layers, state, actions }; 351 352 // Startvisning ud fra URL-parametrene (som v2's switch i initialize()) 353 switch (cfg.type) { 354 case 'sogneId': 355 await showSognKirker(cfg.sogneId, cfg.customZoom); 356 break; 357 case 'provstiId': 358 await showSogne(cfg.provstiId, cfg.customZoom); 359 break; 360 case 'stiftId': 361 await showProvstier(cfg.stiftId); 362 break; 363 default: 364 await showStifter(); 365 } 366} 367 368main().then(r => {});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.