PageSourceSearch

https://www.visionsource-bittnervision.com/scripts/map-handler.js

js visionsource-bittnervision.com collected 2026-10-08 00:09:24 UTC 16,507 bytes, 470 lines download raw bytes

1import mapSettings, { syncMapStyleWithTheme } from "../scripts/config-map.js";
2import mapStyles from "../scripts/config-map-styles.js";
3import { loadGoogleMapsApi } from "../scripts/map-loader.js";
4import { createHtmlMarker, createTraditionalMarker } from "../scripts/map-marker-factory.js";
5import { createInfoWindowContent, setupInfoWindowInteractions } from "../scripts/map-info-window-factory.js";
6import { addLocationSelectorControl, addStyleSwitcherControl, focusLocation } from "../scripts/map-controls.js";
7
8export default function mapHandler() {
9  return {
10    mapsInitialized: false,
11    mapElements: {},
12    isDarkMode: false, // Track dark mode state
13    advancedMarkersSupported: false,
14
15    async init() {
16      // Find all map containers on the page
17      const mapContainers = document.querySelectorAll(".google-map-container");
18      if (mapContainers.length === 0) return;
19
20      // Check if page is in dark mode
21      this.isDarkMode = document.documentElement.classList.contains("dark");
22
23      // Sync map style with current theme
24      syncMapStyleWithTheme(this.isDarkMode);
25
26      // Show loading indicators
27      mapContainers.forEach((container) => {
28        const loadingEl = container.querySelector(".map-loading");
29        if (loadingEl) loadingEl.classList.remove("hidden");
30      });
31
32      try {
33        // Load Google Maps script and wait for it to be ready
34        await loadGoogleMapsApi();
35        this.mapsInitialized = true;
36
37        // Check if Advanced Markers are supported
38        this.advancedMarkersSupported =
39          window.google &&
40          window.google.maps &&
41          window.google.maps.marker &&
42          window.google.maps.marker.AdvancedMarkerElement;
43
44
45        this.initializeAllMaps();
46      } catch (error) {
47        console.error("Failed to load Google Maps API:", error);
48      }
49
50      // Listen for theme changes
51      this.setupThemeChangeListener();
52    },
53
54    // Listen for theme changes (dark/light mode toggle)
55    setupThemeChangeListener() {
56      window.addEventListener("themechange", (e) => {
57        this.isDarkMode = e.detail.theme === "dark";
58        syncMapStyleWithTheme(this.isDarkMode);
59        this.updateAllMapStyles();
60      });
61    },
62
63    // Update styles on all existing maps
64    updateAllMapStyles() {
65      for (const mapId in this.mapElements) {
66        const { map } = this.mapElements[mapId];
67        if (map) {
68          map.setOptions({ styles: mapSettings.mapOptions.styles });
69        }
70      }
71    },
72
73    setMapStyle(styleId) {
74      // Update the map settings with the new style
75      if (mapSettings.setMapStyle(styleId)) {
76        // Update all existing maps with the new style
77        this.updateAllMapStyles();
78        return true;
79      }
80      return false;
81    },
82
83    initializeAllMaps() {
84      try {
85      
86
87        if (!this.mapsInitialized) {
88          console.error("Attempted to initialize maps before API was loaded.");
89          return;
90        }
91
92        const mapContainers = document.querySelectorAll(
93          ".google-map-container",
94        );
95        if (!mapContainers.length) {
96          console.log("No map containers found");
97          return;
98        }
99
100        mapContainers.forEach((container, index) => {
101          try {
102            this.initializeSingleMap(container, index);
103          } catch (error) {
104            console.error(`Error initializing map ${index}:`, error);
105          }
106        });
107
108        // Handle window resize
109        window.addEventListener("resize", this.handleResize.bind(this));
110      } catch (error) {
111        console.error("Critical error in map initialization:", error);
112      }
113    },
114
115    async initializeSingleMap(container, index) {
116
117      const specifiedStyle = container.dataset.mapStyle;
118
119      const showMultiple = container.dataset.multiple === "true";
120
121      const disableControls = container.dataset.disableControls === "true";
122      const noInfoWindow = container.dataset.noInfoWindow === "true";
123
124      let locationKeys = [];
125
126      if (showMultiple) {
127        locationKeys = Object.keys(Alpine.store('locationsData') || {}).filter(
128          (key) =>
129            key.startsWith("location") &&
130            Alpine.store('locationsData')[key]?.coordinates,
131        );
132      } else {
133        // Get single location
134        locationKeys = [container.dataset.location || "location1"];
135      }
136
137      if (!locationKeys.length) {
138        console.error("No valid locations found");
139        return;
140      }
141
142      // Create map
143      const mapElement = container.querySelector(".map-canvas");
144      if (!mapElement) {
145        console.error("Map canvas element not found in container");
146        return;
147      }
148
149      const mapId = `map-${index}`;
150      mapElement.id = mapId;
151
152      // First valid location data for initial center
153      let firstValidLocation = null;
154
155      // Process all locations to get valid coordinates
156      const validLocations = locationKeys
157        .map((key) => {
158          const locationData = Alpine.store('locationsData')?.[key];
159          if (!locationData) {
160            console.warn(`Location data not found for key: ${key}`);
161            return null;
162          }
163
164          const coordinates = this.parseCoordinates(locationData.coordinates);
165          if (!coordinates) {
166            console.warn(`Invalid coordinates for location: ${key}`);
167            return null;
168          }
169
170          if (!firstValidLocation)
171            firstValidLocation = { locationData, coordinates };
172          return { locationData, coordinates, key };
173        })
174        .filter(Boolean);
175
176      if (validLocations.length === 0 || !firstValidLocation) {
177        console.error("No valid coordinates found in locations");
178        return;
179      }
180
181      try {
182        // Create map options from scratch instead of spreading
183        const mapOptions = {
184          zoom: mapSettings.mapOptions.zoom,
185          center: firstValidLocation.coordinates,
186          mapTypeControl: disableControls ? false : mapSettings.mapOptions.mapTypeControl,
187          streetViewControl: disableControls ? false : mapSettings.mapOptions.streetViewControl,
188          fullscreenControl: disableControls ? false : mapSettings.mapOptions.fullscreenControl,
189          clickableIcons: false,
190        };
191        if (disableControls) {
192          // disableDefaultUI removes zoom / fullscreen / street view / map type; cameraControl
193          // is the vector-map tilt/rotate control, disabled explicitly for good measure.
194          mapOptions.disableDefaultUI = true;
195          mapOptions.zoomControl = false;
196          mapOptions.cameraControl = false;
197          mapOptions.keyboardShortcuts = false;
198        }
199
200        // Determine styling mode and marker type
201        const mapId = mapSettings.getMapId();
202        const useAdvancedMarkers =
203          mapSettings.shouldUseAdvancedMarkers() &&
204          this.advancedMarkersSupported;
205
206        if (mapId) {
207          // Cloud-based styling mode
208          mapOptions.mapId = mapId;
209          console.log("Using cloud-based styling with map ID:", mapId);
210        } else {
211          // Local styling mode
212          const styleId = specifiedStyle || mapSettings.currentStyle;
213          mapOptions.styles = mapStyles[styleId];
214          //console.log("Using local style:", styleId);
215        }
216
217        // Create the map
218        const map = new window.google.maps.Map(mapElement, mapOptions);
219
220        // Create bounds object AFTER map has been initialized
221        const bounds = new window.google.maps.LatLngBounds();
222
223        // Add style switcher control if enabled
224        if (container.dataset.showStyleSwitcher === "true") {
225          addStyleSwitcherControl(map, (styleId) => this.setMapStyle(styleId));
226        }
227
228        // Add markers for each location
229        const markers = [];
230        const infoWindows = [];
231
232        // Use for...of to support async/await
233        for (const { locationData, coordinates, key } of validLocations) {
234          // Add to bounds
235          bounds.extend(coordinates);
236
237          // Create marker based on settings
238          let marker;
239
240          // Use HTML marker if enabled and supported
241          if (useAdvancedMarkers) {
242            marker = createHtmlMarker(map, coordinates, locationData);
243          } else {
244            // Fallback to traditional marker (now async)
245            marker = await createTraditionalMarker(
246              map,
247              coordinates,
248              locationData,
249            );
250          }
251
252          // Create the info window — skipped entirely for no-info-window maps (e.g. the
253          // contact mini map), where selecting a marker just recenters on it with no box.
254          let infoWindow = null;
255
256          if (noInfoWindow) {
257            const focusKey = key;
258            if (useAdvancedMarkers) {
259              const markerElement = marker.content;
260              if (markerElement) {
261                markerElement.addEventListener("click", (e) => {
262                  e.stopPropagation();
263                  window.royaMapFocus?.[focusKey]?.();
264                });
265              }
266              marker.addListener("gmp-click", () => window.royaMapFocus?.[focusKey]?.());
267            } else {
268              marker.addListener("click", () => window.royaMapFocus?.[focusKey]?.());
269            }
270          } else {
271            infoWindow = new window.google.maps.InfoWindow({
272              content: createInfoWindowContent(locationData),
273              maxWidth: mapSettings.infoWindowOptions.maxWidth,
274            });
275
276            const openInfoWindow = () => {
277              infoWindows.forEach((iw) => iw && iw.close());
278              setTimeout(() => {
279                if (useAdvancedMarkers) {
280                  infoWindow.open({ anchor: marker, map: map });
281                } else {
282                  infoWindow.open(map, marker);
283                }
284                window.google.maps.event.addListener(infoWindow, "domready", () => {
285                  setupInfoWindowInteractions(locationData);
286                });
287              }, 50);
288            };
289
290            if (useAdvancedMarkers) {
291              const markerElement = marker.content;
292              if (markerElement) {
293                markerElement.addEventListener("click", (e) => {
294                  e.stopPropagation();
295                  openInfoWindow();
296                });
297              }
298              marker.addListener("gmp-click", openInfoWindow);
299            } else {
300              marker.addListener("click", openInfoWindow);
301            }
302          }
303
304          markers.push(marker);
305          infoWindows.push(infoWindow);
306        }
307
308        // If multiple locations, fit the map to show all markers
309        if (validLocations.length > 1) {
310          console.log("Fitting map to show all markers");
311
312          // Ensure bounds is not empty
313          if (!bounds.isEmpty()) {
314            map.fitBounds(bounds);
315
316            // Add a small padding to the bounds and limit max zoom
317            window.google.maps.event.addListenerOnce(map, "idle", () => {
318              if (map.getZoom() > 15) map.setZoom(15);
319            });
320
321            // Add location selector if there are multiple locations — unless this map
322            // opted out of map controls (data-disable-controls), in which case the
323            // built-in selector box is skipped entirely (e.g. the Vision Source V2 map
324            // and contact mini map provide their own location UI).
325            if (!disableControls) {
326              addLocationSelectorControl(
327                map,
328                validLocations,
329                markers,
330                infoWindows,
331              );
332            }
333          } else {
334            console.warn("Bounds is empty - cannot fit to bounds");
335          }
336        }
337
338        // Open first info window automatically if configured to do so
339        if (mapSettings.infoWindowOptions.autoOpen && markers.length === 1 && !noInfoWindow) {
340          setTimeout(() => {
341            if (useAdvancedMarkers) {
342              // For advanced markers
343              infoWindows[0].open({
344                anchor: markers[0],
345                map: map,
346              });
347            } else {
348              // For traditional markers
349              infoWindows[0].open(map, markers[0]);
350            }
351
352            // Add click listeners to buttons after info window is open
353            window.google.maps.event.addListener(
354              infoWindows[0],
355              "domready",
356              () => {
357                setupInfoWindowInteractions(
358                  validLocations[0].locationData,
359                );
360              },
361            );
362          }, mapSettings.infoWindowOptions.autoOpenDelay);
363        }
364
365        // Store map for later reference
366        this.mapElements[mapId] = {
367          map,
368          markers,
369          infoWindows,
370          bounds,
371          isMultiple: showMultiple,
372          validLocations,
373        };
374
375        // Expose a key-based focus hook so a custom location UI (e.g. the Vision
376        // Source accordion) can trigger the exact same pan/zoom/info-window as the
377        // built-in selector. Keyed by location key, so it is index-mismatch safe.
378        window.royaMapFocus = window.royaMapFocus || {};
379        validLocations.forEach((loc, i) => {
380          window.royaMapFocus[loc.key] = () => focusLocation(map, markers, infoWindows, i);
381        });
382
383        // The map's container can reflow shortly after init (images above the locations
384        // section finish loading, the accordion renders), which leaves Google Maps' cached
385        // size/projection stale until the first window resize. That staleness is what made
386        // the FIRST location click render the overview with the info window half off-screen.
387        // Re-sync the size (view unchanged) a few times so the map is correct before the
388        // user interacts, and again once everything has loaded.
389        [200, 700, 1500, 3000].forEach((delay) =>
390          setTimeout(() => window.google.maps.event.trigger(map, "resize"), delay),
391        );
392        window.addEventListener("load", () =>
393          window.google.maps.event.trigger(map, "resize"),
394        );
395
396        // Hide loading indicator once map is ready
397        const loadingEl = container.querySelector(".map-loading");
398        if (loadingEl) loadingEl.style.display = "none";
399
400        // Reset to original global style after map initialization
401        mapSettings.mapOptions.styles = mapStyles[mapSettings.currentStyle];
402      } catch (error) {
403        console.error("Error creating map:", error);
404        console.error(error.stack);
405
406        // Try to display a more useful error message on the map
407        const loadingEl = container.querySelector(".map-loading");
408        if (loadingEl) {
409          loadingEl.innerHTML = `
410            <div class="text-center">
411              <div class="text-red-500 font-semibold">Error loading map</div>
412              <p class="mt-2 text-sm text-primary-foreground">Please refresh the page or try again later.</p>
413            </div>
414          `;
415        }
416      }
417    },
418
419    parseCoordinates(coordinatesString) {
420      if (!coordinatesString) return null;
421
422      try {
423        const [lat, lng] = coordinatesString
424          .split(",")
425          .map((coord) => Number.parseFloat(coord.trim()));
426        if (isNaN(lat) || isNaN(lng)) throw new Error("Invalid coordinates");
427        return { lat, lng };
428      } catch (error) {
429        console.error("Error parsing coordinates:", error);
430        return null;
431      }
432    },
433
434    handleResize() {
435      for (const mapId in this.mapElements) {
436        const { map, markers, bounds, isMultiple } = this.mapElements[mapId];
437
438        if (!map) continue;
439
440        // Trigger resize event
441        window.google.maps.event.trigger(map, "resize");
442
443        // If multiple markers and we have bounds, fit to bounds
444        if (
445          isMultiple &&
446          markers &&
447          markers.length > 1 &&
448          bounds &&
449          !bounds.isEmpty()
450        ) {
451          map.fitBounds(bounds);
452
453          // Limit max zoom
454          const zoom = map.getZoom();
455          if (zoom > 15) {
456            map.setZoom(15);
457          }
458        }
459        // Otherwise center on single marker
460        else if (markers && markers.length === 1) {
461          // Handle both traditional and advanced markers
462          const position = markers[0].getPosition
463            ? markers[0].getPosition()
464            : markers[0].position;
465          map.setCenter(position);
466        }
467      }
468    },
469  };
470}

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.