Vue.component('tt-map', { props: { markersData: { type: Array, default: () => [] }, config: { type: Object, default: () => ({}) }, loading: { type: Boolean, default: false }, showLogo: { type: Boolean, default: false }, contextmenu: { type: Boolean, default: false } }, data: () => ({ map: null, markerLayer: null, nonClusteredLayer: null, tileLayers: { mapbox: { streets: null, satellite: null }, basemap: { streets: null, satellite: null } }, mapProvider: localStorage.getItem('tt-map-provider') || 'basemap', mapType: localStorage.getItem('tt-map-type') || 'streets', internalLoading: true, scriptsLoaded: false, showSettings: false, searchQuery: '', searchResults: [], isSearchLoading: false, showSearchResults: false, searchDebounce: null, selectedMarker: null, isLegendOpen: false, isMobile: window.innerWidth < 992, showMobileControls: false }), computed: { isLoading() { return this.internalLoading || this.loading; }, mapConfig() { const defaults = { center: [47.07, 15.44], zoom: 9, mapboxKey: window.TT_CONFIG?.MAPBOX_KEY, streetsTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', streetsTileId: 'mapbox/streets-v11', satelliteTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', satelliteTileId: 'mapbox/satellite-streets-v12', tileAttribution: '© Mapbox © OpenStreetMap', basemapStreetsTileUrl: 'https://maps.wien.gv.at/basemap/geolandbasemap/normal/google3857/{z}/{y}/{x}.png', basemapSatelliteTileUrl: 'https://maps.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/{z}/{y}/{x}.jpeg', basemapAttribution: 'Datenquelle: basemap.at', clusterOptions: {}, makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" } }; return { ...defaults, ...this.config }; } }, async mounted() { try { await this.loadScripts(); this.scriptsLoaded = true; this.initializeMap(); this.updateMarkers(); this.internalLoading = false; } catch (error) { console.error("Map Initialization Error:", error); this.internalLoading = false; } window.addEventListener('resize', this.handleResize); }, beforeDestroy() { window.removeEventListener('resize', this.handleResize); if (this.map) { this.map.off('zoomend moveend', this.updateTooltipVisibility); if (this.contextmenu) this.map.off('contextmenu', this.onMapContextMenu); this.map.remove(); this.map = null; } }, methods: { loadScripts() { const scripts = [ { type: 'link', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' }, { type: 'script', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' }, { type: 'script', url: 'https://unpkg.com/leaflet-makimarkers@3.1.0/Leaflet.MakiMarkers.js' }, { type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css' }, { type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css' }, { type: 'script', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js' } ]; const promises = scripts.map(s => new Promise((resolve, reject) => { let el; if (s.type === 'script') { el = document.createElement('script'); el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject; } else { el = document.createElement('link'); el.rel = 'stylesheet'; el.href = s.url; el.onload = resolve; el.onerror = () => { console.warn(`Could not load stylesheet: ${s.url}`); resolve(); }; } document.head.appendChild(el); })); return Promise.all(promises); }, initializeMap() { if (!this.scriptsLoaded || typeof L === 'undefined' || !this.mapConfig.mapboxKey) return; this.map = L.map(this.$refs.mapContainer, { preferCanvas: true, zoomControl: false }).setView(this.mapConfig.center, this.mapConfig.zoom); L.control.zoom({ position: 'topright' }).addTo(this.map); if (typeof L.MakiMarkers !== 'undefined') L.MakiMarkers.accessToken = this.mapConfig.mapboxKey; this.map.createPane('fcpPane'); this.map.getPane('fcpPane').style.zIndex = 599; this.tileLayers.mapbox.streets = L.tileLayer(this.mapConfig.streetsTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 22, id: this.mapConfig.streetsTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey }); this.tileLayers.mapbox.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 22, id: this.mapConfig.satelliteTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey }); this.tileLayers.basemap.streets = L.tileLayer(this.mapConfig.basemapStreetsTileUrl, { attribution: this.mapConfig.basemapAttribution, maxZoom: 19 }); this.tileLayers.basemap.satellite = L.tileLayer(this.mapConfig.basemapSatelliteTileUrl, { attribution: this.mapConfig.basemapAttribution, maxZoom: 19 }); this.setActiveTileLayer(); // --- PERFORMANCE OPTIMIZATION --- const clusterOptions = { ...this.mapConfig.clusterOptions, // Keep existing user-defined options chunkedLoading: true, // Add markers in chunks to avoid freezing maxClusterRadius: 100, // Increase clustering radius for better performance when zoomed out disableClusteringAtZoom: 18 // Disable clustering at high zoom levels }; this.markerLayer = L.markerClusterGroup(clusterOptions); // --- END PERFORMANCE OPTIMIZATION --- this.nonClusteredLayer = L.layerGroup(); this.map.addLayer(this.markerLayer); this.map.addLayer(this.nonClusteredLayer); this.map.on('zoomend moveend', this.updateTooltipVisibility); if (this.contextmenu) { this.map.on('contextmenu', this.onMapContextMenu); } if (this.showLogo) { const LogoControl = L.Control.extend({ onAdd: map => { const container = L.DomUtil.create('div', 'leaflet-control-logo'); container.innerHTML = ``; L.DomEvent.disableClickPropagation(container); return container; }}); new LogoControl({ position: 'bottomright' }).addTo(this.map); } this.$nextTick(() => { this.map.invalidateSize(); }); }, setActiveTileLayer() { if (!this.map) return; Object.values(this.tileLayers).forEach(p => Object.values(p).forEach(l => { if (l && this.map.hasLayer(l)) this.map.removeLayer(l); })); const activeLayer = this.tileLayers[this.mapProvider]?.[this.mapType]; if (activeLayer) activeLayer.addTo(this.map); else this.tileLayers.mapbox.streets.addTo(this.map); }, updateMarkers() { if (!this.map || !this.markerLayer || !this.scriptsLoaded) return; this.markerLayer.clearLayers(); this.nonClusteredLayer.clearLayers(); const markersToCluster = []; this.markersData.forEach(data => { if (data.lat == null || data.lng == null) return; let icon; if (data.options?.icon instanceof L.Icon) icon = data.options.icon; else if (data.options?.icon) icon = L.divIcon(data.options.icon); else if (typeof L.MakiMarkers !== 'undefined') icon = L.MakiMarkers.icon({ ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) }); const markerOptions = { icon }; if (data.options?.zIndexOffset) markerOptions.zIndexOffset = data.options.zIndexOffset; if (data.options?.noCluster) markerOptions.pane = 'fcpPane'; const marker = L.marker([data.lat, data.lng], markerOptions); if (data.hausnummerId) marker.tt_hausnummerId = data.hausnummerId; if (data.options?.tooltip) marker.tt_tooltip_options = data.options.tooltip; if (data.options?.popup) marker.bindPopup(data.options.popup); else if (data.options?.asyncPopupContent) { marker.bindPopup(() => ''); marker.on('popupopen', async e => e.popup.setContent(await data.options.asyncPopupContent(data)).update()); } if (data.options?.noCluster) this.nonClusteredLayer.addLayer(marker); else markersToCluster.push(marker); }); // The chunkedLoading option will automatically handle adding these without freezing if (markersToCluster.length > 0) this.markerLayer.addLayers(markersToCluster); this.updateTooltipVisibility(); const allMarkers = [...this.markerLayer.getLayers(), ...this.nonClusteredLayer.getLayers()]; if (allMarkers.length > 0) { const bounds = L.latLngBounds(allMarkers.map(m => m.getLatLng())); if (bounds.isValid()) this.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 17 }); } }, updateTooltipVisibility() { if (!this.map) return; const currentZoom = this.map.getZoom(); const processMarker = marker => { const tooltipOptions = marker.tt_tooltip_options; if (!tooltipOptions || !tooltipOptions.minZoom) return; const isBound = marker.getTooltip(); if (currentZoom >= tooltipOptions.minZoom) { if (!isBound) { const { content, minZoom, ...options } = tooltipOptions; marker.bindTooltip(content, options); if (options.permanent) marker.openTooltip(); } } else if (isBound) marker.unbindTooltip(); }; this.markerLayer.eachLayer(processMarker); this.nonClusteredLayer.eachLayer(processMarker); }, toggleMapType() { this.mapType = this.mapType === 'streets' ? 'satellite' : 'streets'; localStorage.setItem('tt-map-type', this.mapType); this.setActiveTileLayer(); }, setMapProvider(provider) { this.mapProvider = provider; localStorage.setItem('tt-map-provider', provider); this.setActiveTileLayer(); this.showSettings = false; }, handleResize() { this.isMobile = window.innerWidth < 992; if (this.map) this.map.invalidateSize(); }, searchLocations() { if (this.searchDebounce) clearTimeout(this.searchDebounce); if (this.searchQuery.length < 3) { this.searchResults = []; this.showSearchResults = true; return; } this.isSearchLoading = true; this.searchDebounce = setTimeout(async () => { try { const res = await axios.get('/Geocoding/autocomplete', { params: { q: this.searchQuery } }); this.searchResults = res.data; } catch (e) { this.searchResults = []; } finally { this.isSearchLoading = false; } }, 350); }, selectLocation(location) { this.searchQuery = location.text; this.showSearchResults = false; const [lat, lng] = location.value.split(',').map(Number); if (this.map && !isNaN(lat) && !isNaN(lng)) { this.map.flyTo([lat, lng], 17, { duration: 0.5 }); if (this.selectedMarker) this.selectedMarker.remove(); const icon = L.divIcon({ html: `
`, className: '', iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -14] }); this.selectedMarker = L.marker([lat, lng], { icon }).addTo(this.map).bindPopup(`${location.text}`).openPopup(); this.searchQuery = ''; this.searchResults = []; setTimeout(() => { if (this.selectedMarker) { this.selectedMarker.remove(); this.selectedMarker = null; } }, 10000); } }, clearSearch() { this.searchQuery = ''; this.searchResults = []; this.showSearchResults = false; if (this.selectedMarker) { this.selectedMarker.remove(); this.selectedMarker = null; } }, handleSearchFocus() { this.showSearchResults = true; }, handleSearchBlur() { setTimeout(() => { this.showSearchResults = false; }, 200); }, onMapContextMenu(e) { e.originalEvent.preventDefault(); this.$emit('contextmenu', e); }, }, watch: { markersData: { handler() { this.updateMarkers(); }, deep: true }, loading(newVal) { if (!newVal && this.map) this.$nextTick(() => this.map.invalidateSize()); } }, template: `
  • Loading...
    Suche läuft...
  • Bitte mind. 3 Zeichen eingeben
  • Keine Ergebnisse gefunden
  • {{ result.text }}
Legende
Filter & Legende
Filter
Legende
` });