Vue.component('tt-map', { props: { markersData: { type: Array, default: () => [] // Expecting [{ lat: Number, lng: Number, options: { maki?: Object, popup?: String, asyncPopupContent?: Function } }, ...] }, config: { type: Object, default: () => ({}) // User overrides for defaults }, loading: { type: Boolean, default: false } }, data() { return { map: null, markerLayer: null, tileLayers: { streets: null, satellite: null }, mapType: localStorage.getItem('tt-map-type') || 'streets', // Default to 'streets' or stored preference internalLoading: true, scriptsLoaded: false, }; }, computed: { isLoading() { return this.internalLoading || this.loading; }, mapConfig() { const defaults = { center: [46.9, 15.4995], zoom: 11, 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', // Or 'mapbox/satellite-v9' tileAttribution: '© Mapbox © OpenStreetMap', clusterOptions: {}, makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" } }; return { ...defaults, ...this.config }; // Merge user config over defaults } }, 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; } }, 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; resolve(); } if (el) document.head.appendChild(el); else reject(); })); return Promise.all(promises); }, initializeMap() { if (!this.scriptsLoaded || !L || !L.MarkerClusterGroup || !L.MakiMarkers || !this.mapConfig.mapboxKey) return; this.map = L.map(this.$refs.mapContainer, { preferCanvas: true }).setView(this.mapConfig.center, this.mapConfig.zoom); L.MakiMarkers.accessToken = this.mapConfig.mapboxKey; this.tileLayers.streets = L.tileLayer(this.mapConfig.streetsTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.streetsTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey }); this.tileLayers.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.satelliteTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey }); this.tileLayers[this.mapType].addTo(this.map); // Add initial layer based on preference this.markerLayer = L.markerClusterGroup(this.mapConfig.clusterOptions); this.map.addLayer(this.markerLayer); // Invalidate size after initial load if container might not have been ready this.$nextTick(() => { this.map.invalidateSize(); }); // Add resize listener window.addEventListener('resize', this.handleResize); }, updateMarkers() { if (!this.map || !this.markerLayer || !this.scriptsLoaded) return; this.markerLayer.clearLayers(); const markersToAdd = []; this.markersData.forEach(data => { if (data.lat != null && data.lng != null) { const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) }; const icon = L.MakiMarkers.icon(makiOptions); const marker = L.marker([data.lat, data.lng], { icon: icon }); if (data.options?.popup) { marker.bindPopup(data.options.popup); } else if (data.options?.asyncPopupContent && typeof data.options.asyncPopupContent === 'function') { marker.bindPopup(() => ''); // Initial content marker.on('popupopen', async (e) => { const popup = e.popup; try { const content = await data.options.asyncPopupContent(data); // Pass marker data to function popup.setContent(content); } catch (error) { console.error("Error loading popup content:", error); popup.setContent('
Failed to load content.
'); } popup.update(); // Adjust size }); } if (data.options?.tooltip) marker.bindTooltip(data.options.tooltip); markersToAdd.push(marker); } }); if (markersToAdd.length > 0) this.markerLayer.addLayers(markersToAdd); }, toggleMapType() { this.map.removeLayer(this.tileLayers[this.mapType]); this.mapType = this.mapType === 'streets' ? 'satellite' : 'streets'; this.tileLayers[this.mapType].addTo(this.map); localStorage.setItem('tt-map-type', this.mapType); }, handleResize() { if (this.map) { // Use debounce if resize events fire too rapidly this.map.invalidateSize(); } } }, watch: { markersData: { handler() { this.updateMarkers(); }, deep: true }, loading(newVal) { // Optional: Invalidate map size when loading finishes, in case container size changed if (!newVal && this.map) { this.$nextTick(() => this.map.invalidateSize()); } } }, beforeDestroy() { window.removeEventListener('resize', this.handleResize); if (this.map) { this.map.remove(); this.map = null; } }, template: `
` });