1/** 2 * @file Initializes the map and all the corresponding components 3 */ 4 5import { initializeMap, handleClustering, getSlug } from './map.js'; 6import { initializeMiniMap } from './minimap.js'; 7 8import { addGeocoder } from './geocoder.js'; 9import { sortCenters } from './centers.js'; 10 11import { updateCenters } from './centers.js'; 12import { updateCentersList } from './list.js'; 13import { updateDetails, openAllPanels } from './details.js'; 14import { updateIsochrone, updateExtendedIsochrone } from './isochrone.js'; 15 16import { createElement } from './utilities.js'; 17import { print } from './print.js'; 18 19import { statesByPostalCode } from './list.js'; 20 21// Have to be Included in the view 22// import { theme } from '../data/theme.js'; 23 24window.mapboxgl.accessToken = theme.map.api; 25 26//To control styles from application end 27document.body.classList.add('mapbox-locator'); 28 29setupTheme(theme); 30 31let selectedPlace = null; 32let isochrone_mode = 'time'; 33let currentLocationMarker = null; 34 35const geographicCenter = [-98.5, 37.5]; 36 37let map; 38 39// Callback when point selected 40const selectCenterCallback = (center, userFlow) => { 41 // Add center id to params 42 const url = new URL(window.location.href); 43 url.searchParams.set('center', center.id); 44 url.searchParams.delete('search'); 45 window.history.pushState(null, '', url.toString()); 46 47 const slug = getSlug(center.name); 48 49 // Remove active class from markers 50 document.querySelectorAll('.locator__marker--active').forEach((marker) => { 51 marker.classList.remove('locator__marker--active'); 52 }); 53 54 // Add active class to marker 55 document.querySelectorAll(`[data-center='${slug}']`).forEach((marker) => { 56 marker.classList.add('locator__marker--active'); 57 }); 58 59 updateDetails(map, theme, center, selectedPlace, userFlow); 60 61 map.flyTo({ center: center.location, zoom: 8.5 }); 62}; 63 64// Load data 65//configure the ftech centger API URL from view 66const centers = await fetch(theme.centersApi).then((r) => r.json()); 67 68// Initialize map 69map = await initializeMap('locator-map', theme, geographicCenter, centers, selectCenterCallback); 70 71initializeMiniMap(map, centers); 72 73// Add centers to sidebar 74await updateCenters('locator-centers', centers, selectedPlace, selectCenterCallback); 75 76// Add centers to list view 77await updateCentersList('locator-list', centers, selectedPlace, selectCenterCallback); 78 79// Add geocoder 80const geocoder = await addGeocoder(map, 'locator-search', centers, async (place, placeName) => { 81 selectedPlace = place; 82 83 const url = new URL(window.location.href); 84 url.searchParams.set('search', placeName); 85 window.history.pushState(null, '', url.toString()); 86 87 updateWithLocation(place, placeName); 88}); 89 90// If search parameter is present, search for it 91const urlParams = new URLSearchParams(window.location.search); 92const search = urlParams.get('search'); 93const center = urlParams.get('center'); 94 95if (center) { 96 const selectedCenter = centers.find((c) => center === c.id); 97 98 if (selectedCenter) { 99 selectCenterCallback(selectedCenter); 100 } 101} else if (search) { 102 geocoder.query(search); 103 104 const input = document.querySelector('#locator-search input'); 105 106 // Blur input after 500ms 107 setTimeout(() => { 108 if (input) input.blur(); 109 }, 1000); 110} 111 112initializeClose(); 113initializeListSearch(); 114initializeLocate(); 115initializeModeButtons(); 116initializeResetLocation(); 117 118// Add copy 119 120const copyButton = document.querySelector('#centers-copy-link'); 121 122copyButton.addEventListener('click', () => { 123 const currentURL = window.location.href; 124 copyButton.focus(); 125 navigator.clipboard.writeText(currentURL); 126 127 copyButton.innerHTML = copyButton.innerHTML.replace('Copy link', 'Copied!'); 128 129 setTimeout(() => { 130 copyButton.innerHTML = copyButton.innerHTML.replace('Copied!', 'Copy link'); 131 }, 3000); 132}); 133 134// Add print events 135document.querySelector('#centers-print').addEventListener('click', () => { 136 document.getElementById('centers-print').focus(); 137 print(map); 138}); 139 140/* window.addEventListener('beforeprint', (event) => { 141 if (document.querySelector('.locator__map')) { 142 const container = document.querySelector('.locator__map'); 143 container.style.width = '640px'; 144 container.style.height = '360px'; 145 map.resize(); 146 } 147}); 148 149window.addEventListener('afterprint', (event) => { 150 if (document.querySelector('.locator__map')) { 151 const container = document.querySelector('.locator__map'); 152 container.style.width = '100%'; 153 container.style.height = '100%'; 154 map.resize(); 155 } 156}); */ 157 158function setupTheme(theme) { 159 // Add CSS variables to locator element 160 const locator = document.getElementById('locator'); 161 locator.style.setProperty('--font', theme.font.family); 162 locator.style.setProperty('--primaryColor', theme.colors.primary); 163 locator.style.setProperty('--secondaryColor', theme.colors.secondary); 164 locator.style.setProperty('--secondaryLight', theme.colors.secondaryLight); 165 locator.style.setProperty('--secondaryLighter', theme.colors.secondaryLighter); 166 locator.style.setProperty('--secondaryLightest', theme.colors.secondaryLightest); 167 168 // Add logo to logo element 169 const logo = document.getElementById('locator-logo'); 170 logo.src = theme.logo.url; 171} 172 173/** 174 * Adds event listeners to the locate button and updates location when pressed 175 */ 176function initializeLocate() { 177 const findButton = document.getElementById('locator-find'); 178 179 findButton.addEventListener('click', () => { 180 if (window.navigator.geolocation) { 181 // Geolocation available 182 183 window.navigator.geolocation.getCurrentPosition((location) => { 184 selectedPlace = [location.coords.longitude, location.coords.latitude]; 185 186 updateWithLocation(selectedPlace); 187 }); 188 } 189 }); 190} 191 192function initializeModeButtons() { 193 const listModeButton = document.getElementById('locator-list-mode'); 194 const mapModeButton = document.getElementById('locator-map-mode'); 195 196 const locatorContainer = document.getElementById('locator'); 197 198 listModeButton.addEventListener('click', () => { 199 locatorContainer.classList.add('locator--mode-list'); 200 201 listModeButton.classList.add('active'); 202 mapModeButton.classList.remove('active'); 203 204 openAllPanels(); 205 }); 206 207 mapModeButton.addEventListener('click', () => { 208 locatorContainer.classList.remove('locator--mode-list'); 209 210 listModeButton.classList.remove('active'); 211 mapModeButton.classList.add('active'); 212 }); 213} 214 215/** 216 * Adds event listener to the reset button and resets the map app (reset
216s selected place, remove map marker, isochrones, route and clears geolocator) 217 */ 218function initializeResetLocation() { 219 const resetButton = document.getElementById('locator-reset'); 220 221 resetButton.addEventListener('click', () => { 222 selectedPlace = null; 223 224 // Remove has-location class 225 const locator = document.getElementById('locator'); 226 locator.classList.remove('locator--has-location'); 227 228 // Remove search center id from params 229 const url = new URL(window.location.href); 230 url.searchParams.delete('center'); 231 url.searchParams.delete('search'); 232 window.history.pushState(null, '', url.toString()); 233 234 // Reset geocoder 235 geocoder.clear(); 236 237 // Clear map layers 238 [ 239 'route', 240 'time_isochrone', 241 'extended_time_isochrone', 242 'distance_isochrone', 243 'extended_distance_isochrone' 244 ].forEach((source) => { 245 map.getSource(source).setData({ type: 'FeatureCollection', features: [] }); 246 }); 247 248 // Remove marker 249 if (currentLocationMarker) { 250 currentLocationMarker.remove(); 251 currentLocationMarker = null; 252 } 253 254 // Zoom out 255 map.flyTo({ center: geographicCenter, zoom: 3.25 }); 256 }); 257} 258 259/** 260 * Adds event listener to the close button and closes the details panel and removes active class from markers and removes route 261 */ 262function initializeClose() { 263 const closeButton = document.getElementById('locator-close'); 264 265 closeButton.addEventListener('click', () => { 266 // Remove search center id from params 267 const url = new URL(window.location.href); 268 url.searchParams.delete('center'); 269 url.searchParams.delete('search'); 270 window.history.pushState(null, '', url.toString()); 271 272 // Closing details panel 273 const locator = document.getElementById('locator'); 274 locator.classList.remove('opened'); 275 276 // Remove active class from markers 277 document.querySelectorAll('.locator__marker--active').forEach((marker) => { 278 marker.classList.remove('locator__marker--active'); 279 }); 280 281 // Remove route 282 ['route'].forEach((source) => { 283 map.getSource(source).setData({ type: 'FeatureCollection', features: [] }); 284 }); 285 }); 286} 287 288function initializeListSearch() { 289 const searchInput = document.querySelector('#locator-list-search'); 290 const searchReset = document.querySelector('#locator-list-reset'); 291 292 function search(searchText) { 293 let filteredCenters = centers; 294 295 if (searchText.length >= 2) { 296 filteredCenters = centers.filter((center) => { 297 const centerState = statesByPostalCode[center.state]; 298 299 return ( 300 center.name.toLowerCase().includes(searchText) || 301 centerState.toLowerCase().includes(searchText) 302 ); 303 }); 304 } 305 306 updateCentersList('locator-list', filteredCenters, selectedPlace, selectCenterCallback); 307 } 308 309 searchInput.addEventListener('input', (e) => { 310 console.log('search', e.target.value); 311 312 const searchText = e.target.value.toLowerCase(); 313 314 search(searchText); 315 if (e.target.value.length > 0) { 316 searchReset.style.display = "block"; 317 } else { 318 searchReset.style.display = "none"; 319 } 320 }); 321 322 searchReset.addEventListener('click', () => { 323 searchInput.value = ''; 324 325 search(''); 326 searchReset.style.display = "none"; 327 }); 328} 329 330/** 331 * Updates the map with the selected place, adds centers, adds locator pin, centers map, adds isochrones 332 * @param {Array} selectedPlace - [longitude, latitude] 333 * @param {string} selectedPlaceName - Name of the selected place 334 * @returns 335 */ 336function updateWithLocation(selectedPlace, selectedPlaceName) { 337 const locator = document.getElementById('locator'); 338 locator.classList.add('locator--has-location'); 339 340 // Update centers after selecting instantly without selectedPlace 341 updateCenters('locator-centers', centers, null, selectCenterCallback); 342 343 const closest = sortCenters(centers, selectedPlace); 344 const within25 = closest.filter((center) => center.distance < 25); 345 346 // Get times to centers 347 if (selectedPlace) { 348 // Update with selected place which will get travel times 349 updateCenters('locator-centers', centers, selectedPlace, selectCenterCallback); 350 } 351 352 let closestCenters = closest.slice(0, 3).map((center) => center.location); 353 354 const points = [selectedPlace, ...closest.slice(0, 3).map((center) => center.location)].reduce( 355 (a, c) => { 356 a[0].push(c[0]); 357 a[1].push(c[1]); 358 359 return a; 360 }, 361 [[], []] 362 ); 363 364 const extent = [ 365 [Math.min(...points[0]), Math.min(...points[1])], 366 [Math.max(...points[0]), Math.max(...points[1])] 367 ]; 368 369 if (within25.length >= 3) { 370 map.flyTo({ center: selectedPlace, zoom: 9 }); 371 } else if (window.innerWidth > 960) { 372 /* 373 map.fitBounds(extent, { 374 padding: { top: 180, bottom: 60, left: 420 + 12, right: 60 } 375 }); 376 */ 377 378 const maxX = Math.max( 379 ...closestCenters.map((center) => { 380 return Math.abs(selectedPlace[0] - center[0]); 381 }) 382 ); 383 384 const maxY = Math.max( 385 ...closestCenters.map((center) => { 386 return Math.abs(selectedPlace[1] - center[1]); 387 }) 388 ); 389 390 const newExtent = [ 391 [selectedPlace[0] - maxX, selectedPlace[1] - maxY], 392 [selectedPlace[0] + maxX, selectedPlace[1] + maxY] 393 ]; 394 395 map.fitBounds(newExtent, { 396 padding: { top: 180, bottom: 60, left: 420 + 12 + 60, right: 60 } 397 }); 398 } else { 399 map.flyTo({ center: selectedPlace, zoom: 5 }); 400 } 401 402 // Add location marker 403 if (!currentLocationMarker) { 404 const element = createElement( 405 `<div class='locator__marker'> 406 <div class='locator__marker__icon marker__icon--secondary'>
407 <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"> 408 <path d="M40.6897 17.8448C40.6897 23.8343 35.8343 28.6897 29.8448 28.6897C23.8554 28.6897 19 23.8343 19 17.8448C19 11.8554 23.8554 7 29.8448 7C35.8343 7 40.6897 11.8554 40.6897 17.8448Z" fill="var(--primaryColor)"/> 409 <path fill-rule="evenodd" clip-rule="evenodd" d="M29.2069 52.931L29.2069 28.6897H30.4828L30.4828 52.931L29.2069 52.931Z" fill="var(--primaryColor)"/> 410 </svg> 411 </div> 412 <div class='locator__map__popup locator__map__popup--your-location'> 413 <div class='locator__map__popup__contents'> 414 <span class='locator__map__popup--your-location'>${dictionary.yourLocation}</span> 415 <span class='locator__map__popup--your-location-details'></span> 416 </div> 417 </div> 418 </div>` 419 ); 420 421 currentLocationMarker = new mapboxgl.Marker(element).setLngLat(selectedPlace).addTo(map); 422 } else { 423 currentLocationMarker.setLngLat(selectedPlace); 424 } 425 426 // Update text in pin 427 const popup = document.querySelector('.locator__map__popup--your-location-details'); 428 popup.innerHTML = selectedPlaceName || ''; 429 430 // If no close centers, don't add isochrone 431 if (closest[0].distance > 100) return; 432 433 // Add isochrone to map after selecting location 434 updateIsochrone(map, selectedPlace, isochrone_mode); 435 updateExtendedIsochrone(map, selectedPlace, isochrone_mode); 436} 437 438// GTM custom events tracking 439// Locator search box click 440document.querySelector(".mapboxgl-ctrl-geocoder--input").addEventListener('click', () => { 441 //Push search box GTM when focused - Event level tracking 442 if (dictionary && dictionary.gtmSearchBox) { 443 var gtmSearchBox = Object.assign({}, dictionary.gtmSearchBox); 444 if (dictionary.fnGTMPushData) { 445 dictionary.fnGTMPushData(gtmSearchBox); 446 } 447 } 448}); 449// global events from ATC center details 450document.body.addEventListener("click", function (e) { 451 // Bio link 452 if (e.target && e.target.classList.contains("bio-link") && dictionary && dictionary.gtmBioLink) { 453 let yourLocation = "", 454 gtmBioLink = Object.assign({}, dictionary.gtmBioLink), 455 bioDetails = e.target.closest(".locator__detail__bio__details"), 456 bioName = bioDetails.querySelector(".locator__detail__bio__name").textContent.trim(), 457 cDetails = JSON.parse(document.querySelector("#locator-selected-details").getAttribute("data-cdetails")); 458 459 if (document.querySelector(".mapboxgl-ctrl-geocoder--input").value != "") { 460 yourLocation = " | " + document.querySelector(".mapboxgl-ctrl-geocoder--input").value; 461 } 462 463 if (gtmBioLink.eventLabel.indexOf("{BIO LINK CENTER DETAILS}") >= 0) { 464 let eventLabel = 465 cDetails.id + " | " 466 + cDetails.name + " | " 467 + cDetails.primaryAddress_State + " | " 468 + cDetails.primaryAddress_City + " | " 469 + cDetails.primaryAddress_Zip 470 + yourLocation; 471 // [ID] | [Practice Name] | [Practice State abbreviation] | [Practice City] | [Practice Zip] | [Value available in search bar filed] | Care team Member {CARE TEAM MEMBER NAME} 472 gtmBioLink.eventLabel = (gtmBioLink.eventLabel.replace("{BIO LINK CENTER DETAILS}", eventLabel)); 473 gtmBioLink.eventLabel = (gtmBioLink.eventLabel.replace("{CARE TEAM MEMBER NAME}", bioName)); 474 } else if (gtmBioLink.eventLabel.indexOf("{BIO name}") >= 0) { 475 gtmBioLink.eventLabel = (gtmBioLink.eventLabel.replace("{BIO name}", bioName)); 476 } else { 477 gtmBioLink.eventLabel = bioName; 478 } 479 480 if (dictionary.fnGTMPushData) { 481 dictionary.fnGTMPushData(gtmBioLink); 482 } 483 } 484 485 // Directions link 486 if (e.target && e.target.classList.contains("directions-link") && dictionary && dictionary.gtmDirectionLink) { 487 let yourLocation = "", 488 gtmDirectionLink = Object.assign({}, dictionary.gtmDirectionLink), 489 cDetails = JSON.parse(document.querySelector("#locator-selected-details").getAttribute("data-cdetails")); 490 491 if (document.querySelector(".mapboxgl-ctrl-geocoder--input").value != "") { 492 yourLocation = " | " + document.querySelector(".mapboxgl-ctrl-geocoder--input").value; 493 } 494 495 if (gtmDirectionLink.eventLabel.indexOf("{GET DIRECTIONS LINK CENTER DETAILS}") >= 0) { 496 let eventLabel = 497 cDetails.id + " | " 498 + cDetails.name + " | "
499 + cDetails.primaryAddress_State + " | " 500 + cDetails.primaryAddress_City + " | " 501 + cDetails.primaryAddress_Zip 502 + yourLocation; 503 // [ID] | [Practice Name] | [Practice State abbreviation] | [Practice City] | [Practice Zip] | [Value available in search bar filed] | Get Directions via Google Maps 504 gtmDirectionLink.eventLabel = (gtmDirectionLink.eventLabel.replace("{GET DIRECTIONS LINK CENTER DETAILS}", eventLabel)); 505 } else if (gtmDirectionLink.eventLabel.indexOf("{Practice Name}") >= 0) { 506 gtmDirectionLink.eventLabel = (gtmDirectionLink.eventLabel.replace("{Practice Name}", cDetails.name)); 507 } else { 508 gtmDirectionLink.eventLabel = cDetails.name; 509 } 510 511 if (dictionary.fnGTMPushData) { 512 dictionary.fnGTMPushData(gtmDirectionLink); 513 } 514 } 515 516 // Center link 517 if (e.target && e.target.classList.contains("center-link") && dictionary && dictionary.gtmCenterLink) { 518 let yourLocation = "", 519 gtmCenterLink = Object.assign({}, dictionary.gtmCenterLink), 520 cDetails = JSON.parse(document.querySelector("#locator-selected-details").getAttribute("data-cdetails")); 521 522 if (document.querySelector(".mapboxgl-ctrl-geocoder--input").value != "") { 523 yourLocation = " | " + document.querySelector(".mapboxgl-ctrl-geocoder--input").value; 524 } 525 526 if (gtmCenterLink.eventLabel.indexOf("{CENTER WEBSITE LINK CENTER DETAILS}") >= 0) { 527 let eventLabel = 528 cDetails.id + " | " 529 + cDetails.name + " | " 530 + cDetails.primaryAddress_State + " | " 531 + cDetails.primaryAddress_City + " | " 532 + cDetails.primaryAddress_Zip 533 + yourLocation; 534 // [ID] | [Practice Name] | [Practice State abbreviation] | [Practice City] | [Practice Zip] | [Value available in search bar filed] | Click Website Link 535 gtmCenterLink.eventLabel = (gtmCenterLink.eventLabel.replace("{CENTER WEBSITE LINK CENTER DETAILS}", eventLabel)); 536 } else if (gtmCenterLink.eventLabel.indexOf("{Center Link}") >= 0) { 537 gtmCenterLink.eventLabel = (gtmCenterLink.eventLabel.replace("{Center Link}", e.target.getAttribute("href"))); 538 } 539 540 if (dictionary.fnGTMPushData) { 541 dictionary.fnGTMPushData(gtmCenterLink); 542 } 543 } 544 545 // Center email 546 if (e.target && e.target.classList.contains("center-email") && dictionary && dictionary.gtmCenterEmail) { 547 let yourLocation = "", 548 gtmCenterEmail = Object.assign({}, dictionary.gtmCenterEmail), 549 cDetails = JSON.parse(document.querySelector("#locator-selected-details").getAttribute("data-cdetails")); 550 551 if (document.querySelector(".mapboxgl-ctrl-geocoder--input").value != "") { 552 yourLocation = " | " + document.querySelector(".mapboxgl-ctrl-geocoder--input").value; 553 } 554 555 if (gtmCenterEmail.eventLabel.indexOf("{CENTER EMAIL LINK CENTER DETAILS}") >= 0) { 556 let eventLabel = 557 cDetails.id + " | " 558 + cDetails.name + " | " 559 + cDetails.primaryAddress_State + " | " 560 + cDetails.primaryAddress_City + " | " 561 + cDetails.primaryAddress_Zip 562 + yourLocation; 563 // [ID] | [Practice Name] | [Practice State abbreviation] | [Practice City] | [Practice Zip] | [Value available in search bar filed] | ATC Email 564 gtmCenterEmail.eventLabel = (gtmCenterEmail.eventLabel.replace("{CENTER EMAIL LINK CENTER DETAILS}", eventLabel)); 565 } else if (gtmCenterEmail.eventLabel.indexOf("{Center Email}") >= 0) { 566 gtmCenterEmail.eventLabel = (gtmCenterEmail.eventLabel.replace("{Center Email}", cDetails.email || "")); 567 } else { 568 gtmCenterEmail.eventLabel = cDetails.email || ""; 569 } 570 571 if (dictionary.fnGTMPushData) { 572 dictionary.fnGTMPushData(gtmCenterEmail); 573 } 574 } 575 576 // Locator Print > Center Details 577 if (e.target && e.target.classList.contains("locator__print-center-details") && dictionary && dictionary.gtmPrintCenterDetails) { 578 let yourLocation = "", 579 gtmPrintCenterDetails = Object.assign({}, dictionary.gtmPrintCenterDetails), 580 cDetails = JSON.parse(document.querySelector("#locator-selected-details").getAttribute("data-cdetails")); 581 582 if (document.querySelector(".mapboxgl-ctrl-geocoder--input").value != "") { 583 yourLocation = " | " + document.querySelector(".mapboxgl-ctrl-geocoder--input").value; 584 } 585 586 if (gtmPrintCenterDetails.eventLabel.indexOf("{PRINT ATC VIEW CENTER DETAILS}") >= 0) { 587 let eventLabel = 588 cDetails.id + " | " 589 + cDetails.name + " | "
590 + cDetails.primaryAddress_State + " | " 591 + cDetails.primaryAddress_City + " | " 592 + cDetails.primaryAddress_Zip 593 + yourLocation; 594 // [ID] | [Practice Name] | [Practice State abbreviation] | [Practice City] | [Practice Zip] | [Value available in search bar filed] | Print Information for ATC View 595 gtmPrintCenterDetails.eventLabel = (gtmPrintCenterDetails.eventLabel.replace("{PRINT ATC VIEW CENTER DETAILS}", eventLabel)); 596 } 597 598 if (dictionary.fnGTMPushData) { 599 dictionary.fnGTMPushData(gtmPrintCenterDetails); 600 } 601 } 602 603 // Locator Print > Centers 604 if (e.target && e.target.classList.contains("locator__print-centers") && dictionary && dictionary.gtmPrintCenters) { 605 let yourLocation = "", 606 gtmPrintCenters = Object.assign({}, dictionary.gtmPrintCenters); 607 608 if (document.querySelector(".mapboxgl-ctrl-geocoder--input").value != "") { 609 yourLocation = document.querySelector(".mapboxgl-ctrl-geocoder--input").value; 610 } 611 612 if (gtmPrintCenters.eventLabel.indexOf("{PRINT LIST VIEW ATC CENTERS}") >= 0) { 613 // [Value available in search bar filed] | Print Information for List View 614 gtmPrintCenters.eventLabel = (gtmPrintCenters.eventLabel.replace("{PRINT LIST VIEW ATC CENTERS}", yourLocation)); 615 } 616 617 if (dictionary.fnGTMPushData) { 618 dictionary.fnGTMPushData(gtmPrintCenters); 619 } 620 } 621});
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.