PageSourceSearch

https://sogn.dk/maps/js/sognegraenser/app.js

js sogn.dk collected 2026-10-02 07:41:28 UTC 11,250 bytes, 368 lines download raw bytes

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.