PageSourceSearch

https://coconutcreek.gov/ResourcePackages/CoconutCreek/assets/dist…/v-638954455138121672/map-interactivity.js

js coconutcreek.gov collected 2026-10-09 04:56:37 UTC 31,160 bytes, 813 lines download raw bytes

1let map;
2let markers = []; // To store all markers
3const allLocations = {}; // To store all locations with their group key
4
5// Define initMap in the global scope so Google Maps API can call it
6window.initMap = async function() { // Make initMap async
7  // Default map center (e.g., center of your area of interest)
8  const defaultCenter = { lat: 26.2445, lng: -80.2587 }; // Example: Coconut Creek, FL
9
10  const { Map } = await google.maps.importLibrary("maps");
11  const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
12
13  map = new Map(document.getElementById("map"), {
14    zoomControl: true,
15    center: defaultCenter,
16    mapId: "COCONUT_CREEK_DEMO_MAP" // Recommended for Advanced Markers
17  });
18
19  // Now that the map is initialized, set up markers, listeners, etc.
20  await initializeMapFeatures(AdvancedMarkerElement); // Await the async function
21};
22
23// This function contains the core logic for setting up map features
24async function initializeMapFeatures(AdvancedMarkerElement) { // Make initializeMapFeatures async
25  const groupSelectorsContainer = document.getElementById('map-group-selectors');
26  const infoContentElement = document.getElementById('info-content');
27  const mapElement = document.getElementById('map'); // Check for map element as well
28  let activeMarkerWrapper = null; // Variable to keep track of the active marker
29
30  // Define feature icon mapping
31  const featureIcons = {
32    'Kids Playground': 'rec.svg',
33    'Walk/Jog Path': 'walking.svg',
34    'Athletic Field': 'field.svg',
35    'Football Field': 'football.svg',
36    'Baseball': 'baseball.svg',
37    'Dog Walk Area': 'dog-walking.svg',
38    'Restrooms': 'man-woman.svg',
39    'Picnic Area': 'picnic.svg',
40    'Pavilion Rental': 'pavilion.svg',
41    'Hockey Rink': 'hockey.svg',
42    'Tennis Courts': 'tennis.svg',
43    'Nature Preserve': 'nature.svg',
44    'Lake/Canal View': 'canal.svg',
45    'Bird Watching': 'bird.svg'
46  };
47
48  if (!groupSelectorsContainer || !infoContentElement || !mapElement) {
49    console.error('Map interactivity elements not found. Ensure #map-group-selectors, #info-content, and #map exist.');
50    return;
51  }
52
53  // Define all locations first
54  const allLocationsList = [
55    { 
56      id: 'loc1', 
57      image: '../img/locations/image1.jpg',
58      name: 'Tradewinds Park', 
59      address: '3600 W Sample Rd, <br>Coconut Creek, FL 33073', 
60      times: 'Mon - Thu: 7:00 AM - 10:00 PM, <br>Fri: 7:00 AM - 9:00 PM<br>Sat: 8:00 PM - 9:00 PM<br>Sun: 8:00 AM - 8:00 PM',
61      url: '/dummy-link',
62      features: {
63        'Kids Playground': true,
64        'Walk/Jog Path': true,
65        'Athletic Field': true,
66        'Football Field': true,
67        'Baseball': true,
68        'Dog Walk Area': true,
69        'Restrooms': true,
70        'Picnic Area': true,
71        'Pavilion Rental': true,
72        'Hockey Rink': true,
73        'Tennis Courts': true,
74        'Nature Preserve': true,
75        'Lake/Canal View': true,
76        'Bird Watching': true
77      },
78      lat: 26.2598, lng: -80.1938,
79      categories: ['group2'] // City Parks
80    },
81    { 
82      id: 'loc2',
83      image: '../img/locations/image2.jpg',
84      name: 'Butterfly World',
85      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
86      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
87      features: {
88
89      },
90      lat: 26.2600,
91      lng: -80.1900,
92      categories: ['group2'] // Greenways & Trails
93    },
94    { 
95      id: 'loc2',
96      image: '../img/locations/image2.jpg',
97      name: 'Butterfly World',
98      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
99      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
100      features: {
101
102      },
103      lat: 26.2600,
104      lng: -80.1900,
105      categories: ['group2'] // Greenways & Trails
106    },
107    { 
108      id: 'loc2',
109      image: '../img/locations/image2.jpg',
110      name: 'Butterfly World',
111      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
112      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
113      features: {
114
115      },
116      lat: 26.2600,
117      lng: -80.1900,
118      categories: ['group2'] // Greenways & Trails
119    },
120    { 
121      id: 'loc2',
122      image: '../img/locations/image2.jpg',
123      name: 'Butterfly World',
124      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
125      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
126      features: {
127
128      },
129      lat: 26.2600,
130      lng: -80.1900,
131      categories: ['group2'] // Greenways & Trails
132    },
133    { 
134      id: 'loc2',
135      image: '../img/locations/image2.jpg',
136      name: 'Butterfly World',
137      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
138      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
139      features: {
140
141      },
142      lat: 26.2600,
143      lng: -80.1900,
144      categories: ['group2'] // Greenways & Trails
145    },
146    { 
147      id: 'loc2',
148      image: '../img/locations/image2.jpg',
149      name: 'Butterfly World',
150      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
151      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
152      features: {
153
154      },
155      lat: 26.2600,
156      lng: -80.1900,
157      categories: ['group2'] // Greenways & Trails
158    },
159    { 
160      id: 'loc2',
161      image: '../img/locations/image2.jpg',
162      name: 'Butterfly World',
163      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
164      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
165      features: {
166
167      },
168      lat: 26.2600,
169      lng: -80.1900,
170      categories: ['group2'] // Greenways & Trails
171    },
172    { 
173      id: 'loc2',
174      image: '../img/locations/image2.jpg',
175      name: 'Butterfly World',
176      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
177      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
178      features: {
179
180      },
181      lat: 26.2600,
182      lng: -80.1900,
183      categories: ['group2'] // Greenways & Trails
184    },
185    { 
186      id: 'loc2',
187      image: '../img/locations/image2.jpg',
188      name: 'Butterfly World',
189      address: '3600 W Sample Rd, Coconut Creek, FL 33073',
190      times: 'Mon - Sun: 9:00 AM - 5:00 PM',
191      features: {
192
193      },
194      lat: 26.2600,
195      lng: -80.1900,
196      categories: ['group2'] // Greenways & Trails
197    },
198    { 
199      id: 'loc13',
200      image: '../img/locations/image3.jpg',
201      name: 'Coconut Creek Community Center',
202      address: '1010 Coconut Creek Blvd, Coconut Creek, FL 33066',
203      times: 'Mon - Fri: 8:00 AM - 10:00 PM, <br>Sat - Sun: 9:00 AM - 6:00 PM',
204      url: '/dummy-link',
205      features: {
206        'Kids Playground': true,
207        'Athletic Field': true,
208        'Restrooms': true,
209        'Picnic Area': true,
210        'Tennis Courts': true
211      },
212      lat: 26.2520,
213      lng: -80.1850,
214      categories: ['group2'] // Recreation Centers
215    },
216    { 
217      id: 'loc14',
218      image: '../img/locations/image4.jpg',
219      name: 'Creek Side Park',
220      address: '2200 NW 40th Ave, Coconut Creek, FL 33066',
221      times: 'Daily: 6:00 AM - 10:00 PM',
222      url: '/dummy-link',
223      features: {
224        'Kids Playground': true,
225        'Walk/Jog Path': true,
226        'Dog Walk Area': true,
227        'Restrooms': true,
228        'Picnic Area': true,
229        'Nature Preserve': true,
230        'Lake/Canal View': true
231      },
232      lat: 26.2650,
233      lng: -80.1920,
234      categories: ['group2'] // Athletic Fields
235    },
236    { 
237      id: 'loc3',
238      image: '../img/locations/image3.jpg',
239      name: 'Seminole Casino Coconut Creek',
240      address: '5550 NW 40th St, Coconut Creek, FL 33073',
241      times: 'Open 24 Hours lorem ipsum Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam excepturi, odit ratione quas itaque maiores placeat numquam nobis explicabo impedit iusto! Alias recusandae hic laudantium maxime excepturi ea commodi sunt. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam excepturi, odit ratione quas itaque maiores placeat numquam nobis explicabo impedit iusto! Alias recusandae hic laudantium maxime excepturi ea commodi sunt. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam excepturi, odit ratione quas itaque maiores placeat numquam nobis explicabo impedit iusto! Alias recusandae hic laudantium maxime excepturi ea commodi sunt. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam excepturi, odit ratione quas itaque maiores placeat numquam nobis explicabo impedit iusto! Alias recusandae hic laudantium maxime excepturi ea commodi sunt.',
242      features: {
243        'Kids Playground': true,
244        'Walk/Jog Path': true,
245        'Athletic Field': true,
246        'Baseball': true,
247        'Bird Watching': true
248      },
249      lat: 26.2786, lng: -80.1987,
250      categories: ['group3'] // Greenways & Trails
251    },
252    { 
253      id: 'loc4',
254      image: '../img/locations/image5.jpg',
255      name: 'Promenade at Coconut Creek',
256      address: '4443 Lyons Rd, Coconut Creek, FL 33073',
257      times: 'Mon - Sat: 10:00 AM - 9:00 PM, <br>Sun: 12:00 PM - 6:00 PM',
258      features: {
259        'Kids Playground': true,
260        'Walk/Jog Path': true,
261        'Athletic Field': true,
262        'Baseball': true,
263        'Dog Walk Area': true,
264        'Restrooms': true,
265        'Picnic Area': true,
266        'Nature Preserve': true,
267        'Lake/Canal View': true,
268        'Bird Watching': true
269      },
270      lat: 26.2850,
271      lng: -80.1760,
272      categories: ['group4'] // Playgrounds
273    },
274    { 
275      id: 'loc5',
276      image: '../img/locations/image6.jpg',
277      name: 'Windmill Park',
278      address: '1234 Windmill Dr, Coconut Creek, FL 33066',
279      times: 'Daily: 6:00 AM - 8:00 PM',
280      url: '/dummy-link',
281      features: {
282        'Kids Playground': true,
283        'Athletic Field': true,
284        'Restrooms': true,
285        'Picnic Area': true
286      },
287      lat: 26.2580,
288      lng: -80.1950,
289      categories: ['group5'] // Recreation Centers
290    },
291    { 
292      id: 'loc6',
293      image: '../img/locations/image7.jpg',
294      name: 'Palm Beach Park',
295      address: '5678 Palm Beach Blvd, Coconut Creek, FL 33073',
296      times: 'Daily: 5:00 AM - 10:00 PM',
297      url: '/dummy-link',
298      features: {
299        'Walk/Jog Path': true,
300        'Athletic Field': true,
301        'Tennis Courts': true,
302        'Restrooms': true
303      },
304      lat: 26.2720,
305      lng: -80.1800,
306      categories: ['group5'] // Athletic Fields
307    }
308  ];
309
310  // Build locationsData by organizing locations into groups
311  const locationsData = {};
312  
313  // Initialize all groups
314  ['group2', 'group3', 'group4', 'group5', 'group6', 'group7', 'group8', 'group9', 'group10', 'group11', 'group12'].forEach(groupKey => {
315    locationsData[groupKey] = [];
316  });
317
318  // Populate groups based on categories
319  allLocationsList.forEach(location => {
320    location.categories.forEach(category => {
321      if (locationsData[category]) {
322        // Add the location with the current category as its groupKey for this instance
323        locationsData[category].push({
324          ...location,
325          groupKey: category
326        });
327      }
328    });
329  });
330
331  // Fetch the SVG content for the map pin
332  let pinSvgString = '';
333  try {
334    const response = await fetch('../img/location-pin.svg'); // Path relative to the HTML file
335    if (!response.ok) {
336      throw new Error(`HTTP error! status: ${response.status}`);
337    }
338    pinSvgString = await response.text();
339  } catch (error) {
340    console.error('Failed to fetch map pin SVG:', error);
341    // Provide a simple fallback SVG string in case of fetch failure
342    pinSvgString = '<svg viewBox="0 0 24 24" width="28" height="41" fill="currentColor" style="display:block;"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"></path></svg>';
343  }
344
345  // Populate allLocations and create markers
346  // Create markers only once per unique location ID to avoid duplicates
347  const createdMarkers = new Set();
348  
349  for (const groupKey in locationsData) {
350    locationsData[groupKey].forEach(location => {
351      // Store location in allLocations if not already stored
352      if (!allLocations[location.id]) {
353        allLocations[location.id] = { ...location };
354      }
355      
356      // Create marker only if we haven't created one for this location ID yet
357      if (!createdMarkers.has(location.id)) {
358        // Create a div element to hold the SVG pin
359        const pinElement = document.createElement('div');
360        pinElement.innerHTML = pinSvgString;
361        pinElement.style.width = '28px'; 
362        pinElement.style.height = '41px';
363        pinElement.style.cursor = 'pointer'; 
364
365        const svgElement = pinElement.querySelector('svg');
366        if (svgElement) {
367          svgElement.style.width = '100%';
368          svgElement.style.height = '100%';
369          svgElement.style.display = 'block';
370        }
371
372        const markerElement = new AdvancedMarkerElement({
373          map: null, // Start with markers hidden
374          position: { lat: location.lat, lng: location.lng },
375          title: location.name,
376          content: pinElement, // Use the div containing the SVG
377        });
378        
379        // Store marker and its associated data
380        const markerWrapper = {
381          element: markerElement,
382          customInfo: location,
383          visible: false // Our custom visibility flag
384        };
385
386        // Add click listener directly to the marker element
387        markerElement.addListener('click', () => {
388          if (activeMarkerWrapper && activeMarkerWrapper.element.content) {
389            activeMarkerWrapper.element.content.classList.remove('active-pin');
390          }
391          if (markerWrapper.element.content) {
392            markerWrapper.element.content.classList.add('active-pin');
393          }
394          activeMarkerWrapper = markerWrapper;
395          
396          // Hide all markers first
397          markers.forEach(marker => {
398            marker.element.map = null;
399            marker.visible = false;
400          });
401          
402          // Show only this specific marker
403          markerWrapper.element.map = map;
404          markerWrapper.visible = true;
405          
406          // Center map on the marker
407          map.setCenter(markerWrapper.element.position);
408          map.setZoom(16);
409          
410          // Find the best group to highlight for this location
411          const bestGroupKey = findBestGroupForLocation(location.id);
412          selectGroupButton(bestGroupKey);
413          
414          // Show only this individual location's details
415          updateInfoPanel(location);
416        });
417
418        markers.push(markerWrapper);
419        createdMarkers.add(location.id);
420      }
421    });
422  }
423
424  // Helper function to add hover interactions between pins and cards
425  function addHoverInteractions() {
426    markers.forEach(markerWrapper => {
427      const locationId = markerWrapper.customInfo.id;
428      const pinElement = markerWrapper.element.content;
429      
430      if (pinElement) {
431        // Remove existing listeners to avoid duplicates
432        pinElement.onmouseenter = null;
433        pinElement.onmouseleave = null;
434        
435        // Add hover listeners directly to the pin element
436        pinElement.addEventListener('mouseenter', () => {
437          pinElement.classList.add('hover-pin');
438          // Find and highlight corresponding location card
439          const locationCard = document.querySelector(`[data-location-id="${locationId}"]`);
440          if (locationCard) {
441            locationCard.classList.add('hover-state');
442          }
443        });
444
445        pinElement.addEventListener('mouseleave', () => {
446          pinElement.classList.remove('hover-pin');
447          // Remove highlight from corresponding location card
448          const locationCard = document.querySelector(`[data-location-id="${locationId}"]`);
449          if (locationCard) {
450            locationCard.classList.remove('hover-state');
451          }
452        });
453      }
454    });
455  }
456
457  function updateInfoPanel(location) {
458    if (location) {
459      let imageHtml = '';
460      if (location.image) {
461        imageHtml = `<figure class="w-full mx-auto overflow-hidden mb-4"><img src="${location.image}" alt="${location.name}" width="520" height="350" class="object-cover w-full h-auto"></figure>`; 
462      }
463
464      let featuresHtml = '';
465      if (location.features && typeof location.features === 'object') {
466        featuresHtml = Object.keys(location.features)
467          .filter(feature => location.features[feature] === true)
468          .map(feature => {
469            const iconFilename = featureIcons[feature];
470            const iconPath = iconFilename ? `../svg/Icons/features/${iconFilename}` : '';
471            const iconTag = iconPath ? `<span class="w-14 h-14 roun
471ded-full grid items-center justify-center border-2 border-black xl:w-18 xl:h-18"><img src="${iconPath}" alt="${feature} icon" class="feature-list--icon"></span>` : '<span class="mr-2">[?]</span>';
472            return `
473              <li class="grid justify-center justify-items-center my-1 gap-1 text-center w-[calc(33.333%-1rem)] xl:w-[calc(25%-1rem)]">
474                ${iconTag}
475                ${feature}
476              </li>
477            `;
478          }).join('');
479      }
480
481      infoContentElement.innerHTML = `
482        <div>
483          ${imageHtml}
484          <div class="py-6 px-5">
485            <h3 class="mb-5 lg:mb-10">${location.name}</h3>
486            <p class="text-sm mt-1">${location.address}</p>
487            ${location.times ? `<p class="text-sm mt-1">${location.times}</p>` : ''}
488            ${featuresHtml ? `
489            <div>
490              <p class="font-bold mt-4">Features and Amenities</p>
491              <ul class="feature-list list-none flex flex-wrap justify-start gap-4">
492                ${featuresHtml}
493              </ul>
494            </div>
495            ` : ''}
496            ${location.url ? `
497              <a href="${location.url}" class="btn mt-4">
498                Learn more about ${location.name}
499                <i class="icon-chevron"></i>
500              </a>
501            ` : ''}
502          </div>
503        </div>
504      `;
505    } else {
506      infoContentElement.innerHTML = '<p class="text-gray-700">Select a group or click a pin on the map to see details here.</p>';
507    }
508  }
509
510  function updateInfoPanelWithAllLocations(selectedGroupKey) {
511    if (!infoContentElement) return;
512    
513    const groupLocations = locationsData[selectedGroupKey] || [];
514    
515    if (groupLocations.length === 0) {
516      infoContentElement.innerHTML = '<p class="text-gray-700">No locations found for this group.</p>';
517      return;
518    }
519
520    const locationCards = groupLocations.map((location, index) => {
521      let imageHtml = '';
522      if (location.image) {
523        imageHtml = `<figure class="w-full mx-auto overflow-hidden"><img src="${location.image}" alt="${location.name}" width="520" height="350" class="object-cover w-full h-auto"></figure>`; 
524      }
525
526      // Add active class to the first location card
527      const activeClass = index === 0 ? ' active-location-card' : '';
528
529      return `
530        <div class="flex location-card border-b-[1px] border-b-black cursor-pointer px-3.5 py-5 gap-4 hover:bg-orange-200 hover:text-white transition-all duration-300${activeClass}" data-location-id="${location.id}">
531          <div class="w-[70%]">
532            <h3 class="text-lg font-semibold mb-2">${location.name}</h3>
533            <p class="text-sm mb-0">${location.address}</p>
534          </div>
535          <div class="w-[30%]">
536            ${imageHtml}
537          </div>
538        </div>
539      `;
540    }).join('');
541
542    infoContentElement.innerHTML = `
543
544          ${locationCards}
545
546    `;
547
548    // Add click handlers to location cards
549    const locationCardElements = infoContentElement.querySelectorAll('.location-card');
550    locationCardElements.forEach(card => {
551      card.addEventListener('click', (e) => {
552        e.preventDefault();
553        const locationId = card.dataset.locationId;
554        const location = allLocations[locationId];
555        
556        if (location) {
557          // Remove active state from all location cards
558          locationCardElements.forEach(c => c.classList.remove('active-location-card'));
559          // Add active state to clicked card
560          card.classList.add('active-location-card');
561          
562          // Find and activate the corresponding marker
563          const markerWrapper = markers.find(m => m.customInfo.id === locationId);
564          if (markerWrapper) {
565            // Clear previous active marker
566            if (activeMarkerWrapper && activeMarkerWrapper.element.content) {
567              activeMarkerWrapper.element.content.classList.remove('active-pin');
568            }
569            
570            // Set new active marker
571            if (markerWrapper.element.content) {
572              markerWrapper.element.content.classList.add('active-pin');
573            }
574            activeMarkerWrapper = markerWrapper;
575            
576            // Hide all markers first
577            markers.forEach(marker => {
578              marker.element.map = null;
579              marker.visible = false;
580            });
581            
582            // Show only this specific marker
583            markerWrapper.element.map = map;
584            markerWrapper.visible = true;
585            
586            // Center map on the marker
587            map.setCenter(markerWrapper.element.position);
588            map.setZoom(16);
589            
590            // Find the best group to highlight for this location
591            const bestGroupKey = findBestGroupForLocation(locationId);
592            selectGroupButton(bestGroupKey);
593            
594            // Show only this individual location's details
595            updateInfoPanel(location);
596          }
597        }
598      });
599
600      // Add hover listeners for location cards
601      card.addEventListener('mouseenter', () => {
602        const locationId = card.dataset.locationId;
603        card.classList.add('hover-state');
604        
605        // Find and highlight corresponding marker
606        const markerWrapper = markers.find(m => m.customInfo.id === locationId);
607        if (markerWrapper && markerWrapper.element.content) {
608          markerWrapper.element.content.classList.add('hover-pin');
609        }
610      });
611
612      card.addEventListener('mouseleave', () => {
613        const locationId = card.dataset.locationId;
614        card.classList.remove('hover-state');
615        
616        // Remove highlight from corresponding marker
617        const markerWrapper = markers.find(m => m.customInfo.id === locationId);
618        if (markerWrapper && markerWrapper.element.content) {
619          markerWrapper.element.content.classList.remove('hover-pin');
620        }
621      });
622    });
623    
624    // Add hover interactions between pins and cards
625    addHoverInteractions();
626  }
627
628  function selectGroupButton(groupKey) {
629    const buttons = groupSelectorsContainer.querySelectorAll('[data-group-button]');
630    buttons.forEach(button => {
631      if (button.dataset.groupKey === groupKey) {
632        button.classList.add('active');
633      } else {
634        button.classList.remove('active');
635      }
636    });
637  }
638
639  function handleGroupSelected(selectedGroupKey) {
640    const bounds = new google.maps.LatLngBounds();
641    let firstLocationInGroup = null;
642    let hasVisibleMarkersInGroup = false;
643    let newActiveMarkerForGroup = null;
644
645    // Clear previous active marker's style
646    if (activeMarkerWrapper && activeMarkerWrapper.element.content) {
647      activeMarkerWrapper.element.content.classList.remove('active-pin');
648    }
649
650    // Get locations that belong to the selected group
651    const selectedGroupLocations = locationsData[selectedGroupKey] || [];
652    const selectedLocationIds = selectedGroupLocations.map(loc => loc.id);
653
654    markers.forEach(markerWrapper => {
655      // Show marker if its location belongs to the selected group
656      if (selectedLocationIds.includes(markerWrapper.customInfo.id)) {
657        markerWrapper.element.map = map; // Show marker by assigning to map
658        markerWrapper.visible = true;
659        bounds.extend(markerWrapper.element.position); // Use position from AdvancedMarkerElement
660        if (!firstLocationInGroup) {
661          firstLocationInGroup = markerWrapper.customInfo;
662          newActiveMarkerForGroup = markerWrapper; // This will be the new active marker for the group
663        }
664        hasVisibleMarkersInGroup = true;
665      } else {
666        markerWrapper.element.map = null; // Hide marker by removing from map
667        markerWrapper.visible = false;
668      }
669    });
670
671    if (hasVisibleMarkersInGroup && newActiveMarkerForGroup) {
672      map.fitBounds(bounds);
673      // Prevent zooming too far if only one marker
674      if (bounds.getNorthEast().equals(bounds.getSouthWest())) {
675         map.setZoom(15); // Preferred zoom level for a single marker
676      } else {
677         // Adjust zoom after fitBounds to zoom in a bit more
678         setTimeout(() => {
679           const currentZoom = map.getZoom();
680           map.setZoom(currentZoom + 0); // Zoom in one level more than fitBounds
681         }, 100);
682      }
683      
684      // Show all locations in the group
685      updateInfoPanelWithAllLocations(selectedGroupKey);
686      
687      // Set the new active marker and apply its style for the first location
688      if (newActiveMarkerForGroup.element.content) {
689        newActiveMarkerForGroup.element.content.classList.add('active-pin');
690      }
691      activeMarkerWrapper = newActiveMarkerForGroup;
692      
693      // After the info panel is updated, ensure the first location card also has active state
694      setTimeout(() => {
695        const firstLocationCard = infoContentElement.querySelector('.location-card');
696        if (firstLocationCard) {
697          firstLocationCard.classList.add('active-location-card');
698        }
699        // Re-add hover interactions after cards are created
700        addHoverInteractions();
701      }, 50);
702      
703      // console.log(`Group ${selectedGroupKey} selected. Displaying first location:`, firstLocationInGroup);
704    } else {
705      updateInfoPanelWithAllLocations(selectedGroupKey); // Show empty state for group
706      activeMarkerWrapper = null; // No active marker if group is empty or no visible markers
707      // console.log(`Group ${selectedGroupKey} selected, but no locations found or all are hidden.`);
708    }
709
710    // Highlight the active button
711    selectGroupButton(selectedGroupKey);
712  }
713
714  function findBestGroupForLocation(locationId) {
715    // Find the location in our original data
716    const location = allLocationsList.find(loc => loc.id === locationId);
717    if (!location || !location.categories || location.categories.length === 0) {
718      return 'group2'; // fallback to group2
719    }
720    
721    // Return the first category for this location
722    return location.categories[0];
723  }
724
725  if (groupSelectorsContainer) {
726    const allGroupButtons = groupSelectorsContainer.querySelectorAll('[data-group-button]');
727    const toggleMobileButton = document.getElementById('group-toggle-mobile');
728    const additionalButtonsWrapper = document.getElementById('additional-group-buttons');
729    const toggleArrowDown = toggleMobileButton ? toggleMobileButton.querySelector('.icon-chevron') : null;
730
731    let firstGroupKey = null;
732
733    if (!allGroupButtons.length) {
734      console.error('No group selector buttons found with data-group-button attribute.');
735    }
736
737    allGroupButtons.forEach((button, index) => {
738      const groupKey = button.dataset.groupKey; // Read directly from data-group-key attribute
739
740      if (groupKey) {
741        button.dataset.group = groupKey; // Set data-group for consistency with handleGroupSelected
742        if (index === 0) {
743          firstGroupKey = groupKey;
744        }
745
746        button.addEventListener('click', () => {
747          handleGroupSelected(button.dataset.group);
748          // If on mobile and additional buttons are open, and a button within it is clicked, consider closing it.
749          if (window.innerWidth < 768 && additionalButtonsWrapper && additionalButtonsWrapper.style.maxHeight !== '0px') {
750            // Example: toggleAdditionalButtons(false); // if you want to close it after selection
751          }
752        });
753      } else {
754        // Log a warning if a button that is supposed to be a group selector is missing the key
755        console.warn('Button found without a data-group-key attribute, it will not function for group selection:', button);
756      }
757    });
758
759    function toggleAdditionalButtons(forceOpen) {
760      const isCurrentlyOpen = additionalButtonsWrapper.style.maxHeight !== '0px';
761      const open = forceOpen !== undefined ? forceOpen : !isCurrentlyOpen;
762
763      if (open) {
764        additionalButtonsWrapper.style.maxHeight = additionalButtonsWrapper.scrollHeight + "px";
765        additionalButtonsWrapper.classList.add('is-open'); // Add class when open
766        if (toggleArrowDown) toggleArrowDown.classList.add('active');
767      } else {
768        additionalButtonsWrapper.style.maxHeight = '0px';
769        additionalButtonsWrapper.classList.remove('is-open'); // Remove class when closed
770        if (toggleArrowDown) toggleArrowDown.classList.remove('active');
771      }
772    }
773
774    if (toggleMobileButton && additionalButtonsWrapper) {
775      // Ensure correct initial state on mobile (collapsed)
776      if (window.innerWidth < 768) {
777        additionalButtonsWrapper.style.maxHeight = '0px';
778      } else {
779        // On desktop, ensure it's expanded (or rather, not constrained by max-height for md:contents to work)
780        additionalButtonsWrapper.style.maxHeight = 'none'; 
781      }
782      
783      toggleMobileButton.addEventListener('click', () => toggleAdditionalButtons());
784
785      // Adjust on resize
786      window.addEventListener('resize', () => {
787        if (window.innerWidth >= 768) {
788          additionalButtonsWrapper.style.maxHeight = 'none'; 
789          if (toggleArrowDown) toggleArrowDown.classList.remove('active');
790        } else {
791          if (additionalButtonsWrapper.style.maxHeight === 'none') { 
792            additionalButtonsWrapper.style.maxHeight = '0px';
793            if (toggleArrowDown) toggleArrowDown.classList.remove('active');
794          }
795        }
796      });
797    }
798
799    // Initialize with the first group selected
800    if (firstGroupKey && locationsData[firstGroupKey]) {
801      handleGroupSelected(firstGroupKey);
802    } else if (Object.keys(locationsData).length > 0) {
803      const firstDataGroupKey = Object.keys(locationsData)[0];
804      if (locationsData[firstDataGroupKey]) {
805        handleGroupSelected(firstDataGroupKey);
806      }
807    } else {
808      updateInfoPanel(null);
809    }
810  } else {
811    console.error('Group selectors container not found.');
812  }
813}

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.