PageSourceSearch

https://www.anthrazit.org/appl/js/swissmap/swissmap.js

js anthrazit.org collected 2026-10-01 14:15:25 UTC 20,780 bytes, 581 lines download raw bytes

1/*
2########################### anthrazit ag ############################
3### +-----------------------------------------------------------+ ###
4### |      Project Name: moPage                                 | ###
5### |      swisstopo / OpenLayers map runtime                   | ###
6### |      Replaces the Google Maps JS emitted by GoogleMap.php | ###
7### +-----------------------------------------------------------+ ###
8#####################################################################
9
10Consumes the config produced by SwissMapAPI::getMapJS() and builds an
11OpenLayers map on top of swisstopo WMTS base tiles.
12
13Public entry point:  SwissMap.init(config)
14Live instances are exposed on  window.swissMaps[mapId]  so that per-page
15listener code and the geometry library can reach the ol/Map instance.
16*/
17
18(function (global) {
19	'use strict';
20
21	if (typeof ol === 'undefined') {
22		console.error('SwissMap: OpenLayers (ol) is not loaded');
23		return;
24	}
25
26	global.swissMaps = global.swissMaps || {};
27
28	// swisstopo WMTS layers (EPSG:3857 / web mercator) and their tile format
29	var WMTS_BASE = 'https://wmts.geo.admin.ch/1.0.0/';
30	var LAYER_FORMAT = {
31		'ch.swisstopo.pixelkarte-farbe': 'jpeg',
32		'ch.swisstopo.pixelkarte-grau': 'jpeg',
33		'ch.swisstopo.swissimage': 'jpeg'
34	};
35
36	function wmtsUrl(layer) {
37		var fmt = LAYER_FORMAT[layer] || 'png';
38		return WMTS_BASE + layer + '/default/current/3857/{z}/{x}/{y}.' + fmt;
39	}
40
41	// swisstopo vector-tile styles (MapLibre spec) applied via ol-mapbox-style.
42	// maps the former raster layer ids onto the matching vector style.
43	var VECTOR_STYLE = {
44		'ch.swisstopo.pixelkarte-farbe': 'ch.swisstopo.lightbasemap.vt',
45		'ch.swisstopo.pixelkarte-grau': 'ch.swisstopo.lightbasemap.vt',
46		'ch.swisstopo.swissimage': 'ch.swisstopo.imagerybasemap.vt'
47	};
48
49	function vectorStyleUrl(baseId) {
50		var style = VECTOR_STYLE[baseId] || 'ch.swisstopo.lightbasemap.vt';
51		return 'https://vectortiles.geo.admin.ch/styles/' + style + '/style.json';
52	}
53
54	// build the swisstopo base layer: vector tiles (ol-mapbox-style) when available,
55	// otherwise fall back to the raster WMTS tiles. The swisstopo styles contain
56	// several sources (relief + base), so apply() into a Group renders them all
57	// (applyStyle on a single layer would only show the first source -> relief only).
58	function makeBaseLayer(baseId) {
59		baseId = baseId || 'ch.swisstopo.pixelkarte-farbe';
60		if (typeof olms !== 'undefined' && ol.layer.Group) {
61			var group = new ol.layer.Group();
62			olms.apply(group, vectorStyleUrl(baseId)).catch(function (e) {
63				console.error('SwissMap: vector style failed, check ol-mapbox-style', e);
64			});
65			return group;
66		}
67		return new ol.layer.Tile({
68			source: new ol.source.XYZ({
69				url: wmtsUrl(baseId),
70				attributions: '&copy; <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
71				crossOrigin: 'anonymous',
72				maxZoom: 27
73			})
74		});
75	}
76
77	// #rrggbb -> rgba(...) helper (mirrors hexToRgbA used by the old marker JS)
78	function hexToRgbA(hex, alpha) {
79		if (!hex) return 'rgba(0,0,0,' + (alpha || 1) + ')';
80		var c = hex.replace('#', '');
81		if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
82		var num = parseInt(c, 16);
83		return 'rgba(' + ((num >> 16) & 255) + ',' + ((num >> 8) & 255) + ',' + (num & 255) + ',' + (alpha == null ? 1 : alpha) + ')';
84	}
85
86	function lonLat(coords) {
87		return ol.proj.fromLonLat(coords); // [lon, lat] -> 3857
88	}
89
90	// --- vector styles used by the clustered marker path ---------------------
91	function singleFeatureStyle(feature) {
92		var m = feature.get('data');
93		if (m.icon && m.icon.url) {
94			var opt = { src: m.icon.url, anchor: [0.5, 1] };
95			if (m.icon.w && m.icon.h) { opt.width = m.icon.w; opt.height = m.icon.h; opt.anchor = [0.5, 0.5]; }
96			return new ol.style.Style({ image: new ol.style.Icon(opt) });
97		}
98		return new ol.style.Style({
99			image: new ol.style.Circle({
100				radius: 8,
101				fill: new ol.style.Fill({ color: m.color || '#EA4335' }),
102				stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
103			})
104		});
105	}
106
107	function clusterStyle(size) {
108		return new ol.style.Style({
109			image: new ol.style.Circle({
110				radius: 14,
111				fill: new ol.style.Fill({ color: '#3399CC' }),
112				stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
113			}),
114			text: new ol.style.Text({
115				text: String(size),
116				font: 'bold 12px sans-serif',
117				fill: new ol.style.Fill({ color: '#fff' })
118			})
119		});
120	}
121
122	/**
123	 * Build the DOM element for a marker, reproducing getCreateMarkerJS():
124	 * optional glow wrapper > icon wrapper (img) + label, or a default color pin.
125	 * Returns { el, positioning }.
126	 */
127	function buildMarkerElement(m) {
128		var label = null;
129		if (m.label) {
130			label = document.createElement('span');
131			label.className = 'gm-pin-label';
132			if (m.labelcolor) label.style.color = m.labelcolor;
133			label.textContent = m.label;
134		}
135
136		var pin;
137
138		if (m.icon && m.icon.url) {
139			var wrap = document.createElement('div');
140			wrap.className = 'mapmarkerimg antmarker';
141			var img = document.createElement('img');
142			img.src = m.icon.url;
143			if (m.icon.w && m.icon.h) {
144				wrap.style.width = m.icon.w + 'px';
145				wrap.style.height = m.icon.h + 'px';
146				img.style.width = m.icon.w + 'px';
147				img.style.height = m.icon.h + 'px';
148			}
149			wrap.appendChild(img);
150			if (label) wrap.appendChild(label);
151			pin = wrap;
152		} else {
153			// default teardrop pin, colored via CSS
154			pin = document.createElement('div');
155			pin.className = 'swissmap-pin';
156			pin.style.backgroundColor = m.color || '#EA4335';
157			pin.style.borderColor = m.color || '#EA4335';
158			if (label) pin.appendChild(label);
159		}
160
161		if (m.glow) {
162			var glow = document.createElement('div');
163			glow.className = 'mapmarkerglow antmarker';
164			glow.style.backgroundColor = hexToRgbA(m.glowcolor, 0.4);
165			glow.style.width = m.glow + 'px';
166			glow.style.height = m.glow + 'px';
167			if (m.icon && m.icon.url && m.icon.w) {
168				var offset = (m.glow - m.icon.w) / 2;
169				pin.style.position = 'relative';
170				pin.style.left = offset + 'px';
171				pin.style.top = offset + 'px';
172			}
173			glow.appendChild(pin);
174			pin = glow;
175		}
176
177		// glow halo is centred on the point; pins/icons anchor at their bottom tip
178		var positioning = m.glow ? 'center-center' : 'bottom-center';
179		return { el: pin, positioning: positioning };
180	}
181
182	// when the icon image has no known size yet, OL anchors it with height 0 and
183	// the image then loads downward -> re-apply the position once it has loaded
184	function repositionOnImgLoad(overlay, el) {
185		var img = el.querySelector ? el.querySelector('img') : null;
186		if (img && !img.complete) {
187			img.addEventListener('load', function () { overlay.setPosition(overlay.getPosition()); });
188		}
189	}
190
191	// map / aerial (Luftbild) base-layer toggle control (top-left, below zoom)
192	function addBaseSwitcher(map, baseLayer, aerialLayer) {
193		var el = document.createElement('div');
194		el.className = 'swissmap-baseswitcher ol-unselectable ol-control';
195		var btn = document.createElement('button');
196		btn.type = 'button';
197		btn.title = 'Luftbild';
198		btn.innerHTML = '<i class="fa fa-layer-group"></i>';
199		el.appendChild(btn);
200		var aerialOn = false;
201		btn.addEventListener('click', function () {
202			aerialOn = !aerialOn;
203			baseLayer.setVisible(!aerialOn);
204			aerialLayer.setVisible(aerialOn);
205			el.classList.toggle('active', aerialOn);
206			btn.title = aerialOn ? 'Karte' : 'Luftbild';
207		});
208		map.addControl(new ol.control.Control({ element: el }));
209	}
210
211	// center-to-device (geolocation) control incl. accuracy + position marker
212	function addGeolocate(map) {
213		var el = document.createElement('div');
214		el.className = 'jsMapCenterDeviceControlWrapper ol-unselectable ol-control';
215		var btn = document.createElement('button');
216		btn.type = 'button'; btn.title = 'Mein Standort';
217		btn.className = 'jsMapCenterDeviceControl';
218		el.appendChild(btn);
219		map.addControl(new ol.control.Control({ element: el }));
220
221		var geoloc = new ol.Geolocation({
222			trackingOptions: { enableHighAccuracy: true },
223			projection: map.getView().getProjection()
224		});
225		var accFeature = new ol.Feature();
226		var posFeature = new ol.Feature();
227		posFeature.setStyle(new ol.style.Style({
228			image: new ol.style.Circle({
229				radius: 7,
230				fill: new ol.style.Fill({ color: '#3399CC' }),
231				stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
232			})
233		}));
234		map.addLayer(new ol.layer.Vector({
235			source: new ol.source.Vector({ features: [accFeature, posFeature] }),
236			zIndex: 999
237		}));
238
239		var doCenter = false;
240		geoloc.on('change:accuracyGeometry', function () { accFeature.setGeometry(geoloc.getAccuracyGeometry()); });
241		geoloc.on('change:position', function () {
242			var c = geoloc.getPosition();
243			if (!c) return;
244			posFeature.setGeometry(new ol.geom.Point(c));
245			if (doCenter) {
246				doCenter = false;
247				map.getView().animate({ center: c, zoom: Math.max(map.getView().getZoom(), 16), duration: 300 });
248			}
249		});
250		geoloc.on('error', function () { btn.classList.remove('loading'); alert('Positionierung nicht möglich'); });
251		btn.addEventListener('click', function () {
252			doCenter = true;
253			btn.classList.add('loading');
254			geoloc.setTracking(true);
255			geoloc.once('change:position', function () { btn.classList.remove('loading'); });
256		});
257	}
258
259	function makePopup() {
260		// OL positions the overlay *element* via an inline transform, so we can't
261		// rely on CSS transform for centering on it. Use a zero-size anchor as the
262		// overlay element and center the actual (variable-width) popup on it via the
263		// popup's own transform (see .swissmap-popup in the stylesheet).
264		var anchor = document.createElement('div');
265		anchor.className = 'swissmap-popup-anchor';
266		var container = document.createElement('div');
267		container.className = 'swissmap-popup';
268		var closer = document.createElement('a');
269		closer.className = 'swissmap-popup-closer';
270		closer.href = '#';
271		closer.innerHTML = '&times;';
272		var content = document.createElement('div');
273		content.className = 'swissmap-popup-content';
274		container.appendChild(closer);
275		container.appendChild(content);
276		anchor.appendChild(container);
277		var overlay = new ol.Overlay({
278			element: anchor,
279			autoPan: { animation: { duration: 200 } },
280			stopEvent: true,
281			positioning: 'bottom-center',
282			offset: [0, -12]
283		});
284		closer.addEventListener('click', function (e) {
285			e.preventDefault();
286			overlay.setPosition(undefined);
287		});
288		return { overlay: overlay, content: content };
289	}
290
291	// event-name translation from the old google.maps events to ol/Map events
292	var EVENT_MAP = {
293		idle: 'moveend',
294		bounds_changed: 'moveend',
295		center_changed: 'moveend',
296		zoom_changed: 'moveend',
297		dragend: 'moveend',
298		tilesloaded: 'rendercomplete',
299		click: 'click'
300	};
301
302	/**
303	 * Thin wrapper handed to per-page listener callbacks so they don't need to
304	 * know OL internals. Mirrors the handful of google.Map methods used.
305	 */
306	function makeWrapper(map, popup) {
307		return {
308			ol: map,
309			getZoom: function () { return Math.round(map.getView().getZoom()); },
310			getCenter: function () { return ol.proj.toLonLat(map.getView().getCenter()); },
311			// bounds as {sw:[lon,lat], ne:[lon,lat]}
312			getBounds: function () {
313				var e = map.getView().calculateExtent(map.getSize());
314				return { sw: ol.proj.toLonLat([e[0], e[1]]), ne: ol.proj.toLonLat([e[2], e[3]]) };
315			},
316			setCenter: function (lon, lat) { map.getView().setCenter(lonLat([lon, lat])); },
317			setZoom: function (z) { map.getView().setZoom(z); },
318
319			// add a marker after init (used by dynamic POI loaders).
320			// opts may carry {label, labelcolor, color, glow, glowcolor}
321			addMarker: function (lon, lat, iconUrl, html, w, h, opts) {
322				opts = opts || {};
323				var built = buildMarkerElement({
324					icon: iconUrl ? { url: iconUrl, w: w, h: h } : null,
325					label: opts.label, labelcolor: opts.labelcolor,
326					color: opts.color, glow: opts.glow, glowcolor: opts.glowcolor
327				});
328				built.el.style.cursor = 'pointer';
329				var pos = lonLat([parseFloat(lon), parseFloat(lat)]);
330				var ov = new ol.Overlay({ element: built.el, position: pos, positioning: built.positioning, stopEvent: false });
331				map.addOverlay(ov);
332				repositionOnImgLoad(ov, built.el);
333				if (html && popup) {
334					built.el.addEventListener('click', function (e) {
335						e.stopPropagation();
336						popup.content.innerHTML = html;
337						popup.overlay.setPosition(pos);
338					});
339				}
340				return ov;
341			},
342			moveMarker: function (ov, lon, lat) { if (ov) ov.setPosition(lonLat([parseFloat(lon), parseFloat(lat)])); },
343			removeMarker: function (ov) { if (ov) map.removeOverlay(ov); },
344			showPopup: function (lon, lat, html) {
345				if (!popup) return;
346				popup.content.innerHTML = html;
347				popup.overlay.setPosition(lonLat([parseFloat(lon), parseFloat(lat)]));
348			},
349			// swisstopo forward search -> recenter (replaces google Geocoder in the pickers)
350			search: function (query, zoom) {
351				if (!query) return;
352				var url = 'https://api3.geo.admin.ch/rest/services/api/SearchServer?type=locations&sr=4326&limit=1&searchText=' + encodeURIComponent(query);
353				fetch(url).then(function (r) { return r.json(); }).then(function (data) {
354					var res = data && data.results && data.results[0];
355					if (res && res.attrs) {
356						map.getView().setCenter(lonLat([res.attrs.lon, res.attrs.lat]));
357						if (zoom) map.getView().setZoom(zoom);
358					}
359				}).catch(function () {});
360			}
361		};
362	}
363
364	var SwissMap = {
365
366		init: function (cfg) {
367			var el = document.getElementById(cfg.mapId);
368			if (!el) {
369				console.error('SwissMap: container #' + cfg.mapId + ' not found');
370				return;
371			}
372
373			var base = makeBaseLayer(cfg.base);
374			var layers = [base];
375
376			// optional aerial (Luftbild) layer for the base switcher
377			var aerial = null;
378			if (cfg.aerial) {
379				aerial = makeBaseLayer('ch.swisstopo.swissimage');
380				aerial.setVisible(false);
381				layers.push(aerial);
382			}
383
384			var controls = ol.control.defaults.defaults({
385				zoom: cfg.controls.zoom !== false,
386				attribution: true,
387				rotate: false
388			});
389			if (cfg.controls.scale !== false) controls.push(new ol.control.ScaleLine());
390			if (cfg.controls.fullscreen === true) controls.push(new ol.control.FullScreen());
391
392			// static maps (thumbnails) have no interactions at all
393			var interactions = cfg.static
394				? new ol.Collection()
395				: ol.interaction.defaults.defaults({ mouseWheelZoom: cfg.controls.mouseWheel !== false });
396
397			var view = new ol.View({
398				center: lonLat(cfg.center || [8.226692, 46.80121]),
399				zoom: cfg.zoom || 16,
400				maxZoom: 21
401			});
402
403			var map = new ol.Map({
404				target: el,
405				layers: layers,
406				controls: controls,
407				interactions: interactions,
408				view: view
409			});
410
411			if (aerial) addBaseSwitcher(map, base, aerial);
412			if (cfg.geolocate) addGeolocate(map);
413
414			var popup = null;
415			if (cfg.infoWindow !== false) {
416				popup = makePopup();
417				map.addOverlay(popup.overlay);
418			}
419
420			// --- ground overlay (georeferenced image) ---
421			if (cfg.overlay && cfg.overlay.img) {
422				var b = cfg.overlay.bounds; // [swLat, swLng, neLat, neLng]
423				var ext = ol.extent.boundingExtent([lonLat([b[1], b[0]]), lonLat([b[3], b[2]])]);
424				map.addLayer(new ol.layer.Image({
425					opacity: cfg.overlay.opacity ? cfg.overlay.opacity / 10 : 1,
426					source: new ol.source.ImageStatic({ url: cfg.overlay.img, imageExtent: ext, crossOrigin: 'anonymous' })
427				}));
428			}
429
430			// --- KML route overlays ---
431			(cfg.kml || []).forEach(function (url) {
432				map.addLayer(new ol.layer.Vector({
433					source: new ol.source.Vector({ url: url, format: new ol.format.KML({ extractStyles: true }) })
434				}));
435			});
436
437			// --- markers ---
438			if (cfg.clustering && cfg.markers && cfg.markers.length) {
439				// clustered vector markers
440				var features = cfg.markers.map(function (m) {
441					var f = new ol.Feature({ geometry: new ol.geom.Point(lonLat([parseFloat(m.lon), parseFloat(m.lat)])) });
442					f.set('data', m);
443					return f;
444				});
445				var clusterSource = new ol.source.Cluster({
446					distance: (cfg.clusterOptions && cfg.clusterOptions.distance) || 60,
447					source: new ol.source.Vector({ features: features })
448				});
449				var styleCache = {};
450				map.addLayer(new ol.layer.Vector({
451					source: clusterSource,
452					style: function (feature) {
453						var fs = feature.get('features');
454						if (fs.length === 1) return singleFeatureStyle(fs[0]);
455						if (!styleCache[fs.length]) styleCache[fs.length] = clusterStyle(fs.length);
456						return styleCache[fs.length];
457					}
458				}));
459				map.on('click', function (evt) {
460					map.forEachFeatureAtPixel(evt.pixel, function (feat) {
461						var fs = feat.get('features');
462						if (!fs) return false;
463						if (fs.length === 1) {
464							var m = fs[0].get('data');
465							if (m.html && popup) {
466								popup.content.innerHTML = m.html;
467								popup.overlay.setPosition(fs[0].getGeometry().getCoordinates());
468							}
469						} else {
470							var cext = ol.extent.createEmpty();
471							fs.forEach(function (ff) { ol.extent.extend(cext, ff.getGeometry().getExtent()); });
472							view.fit(cext, { duration: 300, padding: [40, 40, 40, 40], maxZoom: 18 });
473						}
474						return true;
475					});
476				});
477			} else {
478				// DOM overlay markers (faithful to the old renderer: icon/glow/label)
479				(cfg.markers || []).forEach(function (m) {
480					var built = buildMarkerElement(m);
481					var pos = lonLat([parseFloat(m.lon), parseFloat(m.lat)]);
482					var domOv = new ol.Overlay({
483						element: built.el,
484						position: pos,
485						positioning: built.positioning,
486						stopEvent: false
487					});
488					map.addOverlay(domOv);
489					repositionOnImgLoad(domOv, built.el);
490
491					if (m.html && popup) {
492						built.el.style.cursor = 'pointer';
493						var open = function (e) {
494							if (e) e.stopPropagation();
495							popup.content.innerHTML = m.html;
496							popup.overlay.setPosition(pos);
497						};
498						built.el.addEventListener('click', open);
499						(m.openers || []).forEach(function (id) {
500							var o = document.getElementById(id);
501							if (o) o.addEventListener('click', function (ev) { ev.preventDefault(); open(); });
502						});
503					}
504				});
505			}
506
507			// --- fit to marker bounds (replaces google fitBounds) ---
508			if (cfg.bounds && cfg.markers && cfg.markers.length > 1) {
509				var extent = ol.extent.boundingExtent([
510					lonLat([cfg.bounds[0], cfg.bounds[1]]),
511					lonLat([cfg.bounds[2], cfg.bounds[3]])
512				]);
513				var pad = cfg.boundsPadding != null ? cfg.boundsPadding : 40;
514				view.fit(extent, { size: map.getSize(), padding: [pad, pad, pad, pad], maxZoom: cfg.zoom || 18 });
515			}
516
517			// --- listeners bridge ---
518			var wrapper = makeWrapper(map, popup);
519			(cfg.listeners || []).forEach(function (l) {
520				var olEvent = EVENT_MAP[l.event] || l.event;
521				var fn = global[l.fn];
522				if (typeof fn !== 'function') return;
523				map.on(olEvent, function () {
524					global.setTimeout(function () { fn(wrapper); }, 50);
525				});
526				// google 'idle' also fired once after initial load -> replicate
527				if (l.event === 'idle') {
528					map.once('rendercomplete', function () {
529						global.setTimeout(function () { fn(wrapper); }, 50);
530					});
531				}
532			});
533
534			// optionally center on the device location (replaces google centerdevice)
535			if (cfg.centerdevice && global.navigator && navigator.geolocation) {
536				navigator.geolocation.getCurrentPosition(function (pos) {
537					view.animate({ center: lonLat([pos.coords.longitude, pos.coords.latitude]), duration: 300 });
538				});
539			}
540
541			// the container height is often set after init (addJSEof) -> keep size in sync
542			if (global.ResizeObserver) {
543				new global.ResizeObserver(function () { map.updateSize(); }).observe(el);
544			}
545			global.addEventListener('resize', function () { map.updateSize(); });
546			global.setTimeout(function () { map.updateSize(); }, 200);
547
548			global.swissMaps[cfg.mapId] = map;
549			global.swissMapWrappers = global.swissMapWrappers || {};
550			global.swissMapWrappers[cfg.mapId] = wrapper;
551			map.set('swissWrapper', wrapper);
552
553			// extension hook filled in by later feature modules (clustering, kml, overlay)
554			if (typeof SwissMap._afterInit === 'function') SwissMap._afterInit(map, cfg);
555
556			return map;
557		},
558
559		// shared helpers reused by the geometry library (geometrien.lib.js)
560		baseTileLayer: function (layerId) {
561			return makeBaseLayer(layerId);
562		},
563		baseLayerForStyle: function (styleName) {
564			var id = (styleName === 'gray' || styleName === 'dark' || styleName === 'uc')
565				? 'ch.swisstopo.pixelkarte-grau' : 'ch.swisstopo.pixelkarte-farbe';
566			return makeBaseLayer(id);
567		},
568		// add a hidden aerial layer + the map/aerial toggle control to any map
569		addAerialToggle: function (map, baseLayer) {
570			var aerial = makeBaseLayer('ch.swisstopo.swissimage');
571			aerial.setVisible(false);
572			map.addLayer(aerial);
573			addBaseSwitcher(map, baseLayer, aerial);
574			return aerial;
575		},
576		hexToRgbA: hexToRgbA
577	};
578
579	global.SwissMap = SwissMap;
580
581})(window);

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.