PageSourceSearch

https://www.visionsource-esplineyes.com/scripts/map-controls.js

js visionsource-esplineyes.com collected 2026-10-08 09:40:13 UTC 8,241 bytes, 193 lines download raw bytes

1import mapSettings from "../scripts/config-map.js";
2
3/**
4 * Pan + zoom the map to a location, bounce its marker, and open its info window
5 * (which holds the location CTAs). Shared by the built-in selector and by any
6 * custom location UI (e.g. the Vision Source accordion) via window.royaMapFocus.
7 * @param {google.maps.Map} map
8 * @param {Array} markers
9 * @param {Array<google.maps.InfoWindow>} infoWindows
10 * @param {number} index - index of the location to focus
11 */
12export function focusLocation(map, markers, infoWindows, index) {
13  if (!map || !markers || !markers[index]) return;
14  const marker = markers[index];
15  const pos = marker.getPosition ? marker.getPosition() : marker.position;
16  // Newest click wins, so rapid clicks don't fight over the map.
17  const token = (window.__royaMapFocusToken = (window.__royaMapFocusToken || 0) + 1);
18  const isCurrent = () => window.__royaMapFocusToken === token;
19
20  // The info window is optional — some maps (e.g. the contact-page mini map) focus a
21  // location without opening any info box, just centering it.
22  const infoWindow = infoWindows && infoWindows[index];
23  if (infoWindows) infoWindows.forEach((iw) => iw && iw.close());
24
25  const center = () => {
26    // Vector (cloud-styled) maps: moveCamera drives the WebGL camera directly and
27    // repaints, where setZoom/setCenter can leave the first zoom-in unrendered.
28    if (typeof map.moveCamera === "function") {
29      map.moveCamera({ center: pos, zoom: 16 });
30    } else {
31      map.setZoom(16);
32      map.setCenter(pos);
33    }
34  };
35
36  if (marker.setAnimation) {
37    marker.setAnimation(window.google.maps.Animation.BOUNCE);
38    setTimeout(() => {
39      marker.setAnimation(null);
40    }, 1500);
41  } else if (marker.content) {
42    marker.content.animate(
43      [{ transform: "translateY(0)" }, { transform: "translateY(-20px)" }, { transform: "translateY(0)" }],
44      { duration: 700, iterations: 3, easing: "cubic-bezier(0.215, 0.610, 0.355, 1.000)" }
45    );
46  }
47
48  let opened = false;
49  const open = () => {
50    if (opened || !isCurrent()) return;
51    opened = true;
52    if (infoWindow) infoWindow.open({ map, anchor: marker });
53  };
54
55  // On the very first interaction after a page load the vector map's camera/projection can
56  // still be settling, so a single move lands off-target — the marker isn't centered and
57  // the info window opens off to the side (a real mouse drag wakes it up, and a second
58  // click works, because the map has settled by then). Re-assert the size and camera
59  // repeatedly as it settles — actively forcing it to render — then open the info window
60  // once it has caught up. Re-asserting continues briefly after opening, and the info
61  // window (anchored to the marker) follows, so it ends up centered regardless.
62  const settle = () => {
63    if (!isCurrent()) return;
64    window.google.maps.event.trigger(map, "resize");
65    center();
66  };
67  center();
68  [100, 300, 600, 1000, 1500, 2000].forEach((t) => setTimeout(settle, t));
69  window.google.maps.event.addListenerOnce(map, "idle", () => setTimeout(open, 700));
70  setTimeout(open, 2300); // overall safety net
71}
72
73/**
74 * Adds a location selector dropdown to the map.
75 * @param {google.maps.Map} map - The map instance.
76 * @param {Array} locations - Array of valid location objects.
77 * @param {Array<google.maps.Marker|google.maps.marker.AdvancedMarkerElement>} markers - Array of map markers.
78 * @param {Array<google.maps.InfoWindow>} infoWindows - Array of info windows.
79 */
80export function addLocationSelectorControl(map, locations, markers, infoWindows) {
81  const controlDiv = document.createElement("div");
82  controlDiv.className = "relative bg-primary/80 backdrop-blur-xs shadow-md p-2 m-4 xs:w-[280px] rounded max-md:!relative";
83
84  const controlUI = document.createElement("div");
85  controlUI.className = "flex flex-col";
86
87  const headerDiv = document.createElement("div");
88  headerDiv.className = "flex justify-between items-center mb-2 px-2";
89
90  const title = document.createElement("div");
91  title.textContent = "Our Locations";
92  title.className = "text-primary-foreground font-semibold text-sm";
93  headerDiv.appendChild(title);
94
95  const toggleButton = document.createElement("button");
96  toggleButton.textContent = "HIDE";
97  toggleButton.className = "text-xs font-bold text-accent hover:text-accent/80 transition-colors";
98  toggleButton.setAttribute("type", "button");
99  headerDiv.appendChild(toggleButton);
100
101  controlUI.appendChild(headerDiv);
102
103  const locationList = document.createElement("div");
104  locationList.id = "map-location-list";
105  locationList.className = "location-list max-h-60 overflow-y-auto";
106
107  locations.forEach((location, index) => {
108    const locationButton = document.createElement("button");
109    locationButton.className = "group w-full text-left px-3 py-2 text-sm hover:bg-secondary hover:text-secondary-foreground focus:outline-none focus:ring-1 focus:ring-inset focus:ring-accent transition-colors rounded my-1";
110    locationButton.innerHTML = `
111      <div class="flex items-center">
112        <span class="rounded-full bg-primary group-hover:bg-accent group-hover:text-accent-foreground text-primary-foreground h-6 w-6 flex items-center justify-center mr-2 text-xs font-bold">${index + 1}</span>
113        <div>
114          <span class="text-primary-foreground group-hover:text-secondary-foreground font-medium block">${location.locationData.name || `Location ${index + 1}`}</span>
115          <span class="text-xs text-primary-foreground/80 group-hover:text-secondary-foreground/80 block">${location.locationData.addressCity || ""}, ${location.locationData.addressState || ""}</span>
116        </div>
117      </div>
118    `;
119
120    locationButton.addEventListener("click", () => {
121      focusLocation(map, markers, infoWindows, index);
122      locationList.style.display = "none";
123      resetButton.style.display = "none";
124      toggleButton.textContent = "SHOW";
125    });
126
127    locationList.appendChild(locationButton);
128  });
129
130  controlUI.appendChild(locationList);
131  controlDiv.appendChild(controlUI);
132
133  const resetButton = document.createElement("button");
134  resetButton.className = "btn btn-wide btn-md btn-rounded btn-accent mt-2";
135  resetButton.textContent = "Show All Locations";
136  resetButton.addEventListener("click", () => {
137    infoWindows.forEach((iw) => iw.close());
138    const bounds = new window.google.maps.LatLngBounds();
139    markers.forEach((marker) => {
140      const position = marker.getPosition ? marker.getPosition() : marker.position;
141      bounds.extend(position);
142    });
143    map.fitBounds(bounds);
144    window.google.maps.event.addListenerOnce(map, "idle", () => {
145      if (map.getZoom() > 15) map.setZoom(15);
146    });
147  });
148
149  controlUI.appendChild(resetButton);
150
151  toggleButton.addEventListener("click", () => {
152    if (locationList.style.display === "none") {
153      locationList.style.display = "block";
154      resetButton.style.display = "block";
155      toggleButton.textContent = "HIDE";
156    } else {
157      locationList.style.display = "none";
158      resetButton.style.display = "none";
159      toggleButton.textContent = "SHOW";
160    }
161  });
162
163  map.controls[window.google.maps.ControlPosition.LEFT_TOP].push(controlDiv);
164}
165
166/**
167 * Adds a style switcher control to the map.
168 * @param {google.maps.Map} map - The map instance.
169 * @param {Function} setMapStyleCallback - Callback function to set the map style.
170 */
171export function addStyleSwitcherControl(map, setMapStyleCallback) {
172  const controlDiv = document.createElement("div");
173  controlDiv.className = "bg-white shadow-md rounded-lg p-2 m-2";
174  controlDiv.style.margin = "100px";
175
176  const select = document.createElement("select");
177  select.className = "text-sm px-2 py-1 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500";
178
179  mapSettings.availableStyles.forEach((style) => {
180    const option = document.createElement("option");
181    option.value = style.id;
182    option.textContent = style.name;
183    option.selected = style.id === mapSettings.currentStyle;
184    select.appendChild(option);
185  });
186
187  select.addEventListener("change", () => {
188    setMapStyleCallback(select.value);
189  });
190
191  controlDiv.appendChild(select);
192  map.controls[window.google.maps.ControlPosition.TOP_RIGHT].push(controlDiv);
193}

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.