PageSourceSearch

https://www.newportacademy.com/wp-content/plugins/newport-location…/js/map.js?ver=0.97527700%201789806336

js newportacademy.com collected 2026-09-24 09:03:33 UTC 23,331 bytes, 687 lines download raw bytes

1if ( typeof LmpMapRenderer !== 'function' ) {
2
3/**
4 * LmpMapRenderer
5 */
6class LmpMapRenderer {
7
8    isInDiviEditor = false;
9
10    storageKey = 'lmp-map-data';
11
12    restRoute = '/wp-json/lmp-map/v1/get-data';
13
14    localStorageObject = {
15        geoJson: {
16            modified: 0,
17            shouldUpdate: false,
18            data: {},
19        },
20        statesData: {
21            modified: 0,
22            shouldUpdate: false,
23            data: {},
24        },
25    };
26
27    mapsData = {}; // { [elementId]: { map, statesLayer, locationsLayer, pinCounts } }
28
29    infoWinPin = {
30        header: `<div class="info-win-location-header">Newport Academy Locations</div>`
31    }
32
33    constructor() {
34        if ( window !== window.parent && window.location.href.includes('et_pb_preview=true') ) {
35            this.isInDiviEditor = true;
36        }
37
38        document.addEventListener('DOMContentLoaded', () => this.init());
39    }
40
41    async init() {
42        // preflight
43        if ( ! await this.preflight() ) {
44            return;
45        }
46
47        // setup local storage
48        this.setupLocalData();
49
50        // check remote modifieds
51        if ( ! this.isInDiviEditor ) {
52            await this.checkRemoteModified();
53        }
54
55        // get / update remote data
56        await this.checkData();
57
58        // look for and loop maps
59        this.setupMaps();
60
61        // add GB preview change events
62        this.setupGutenberg();
63    }
64
65    async preflight() {
66      // check for remote geojson data
67      var check = await fetch(`${this.restRoute}`, {
68          method: 'POST',
69          headers: {
70              'Content-Type': 'application/json'
71          },
72          body: JSON.stringify({ key: 'geoJson' })
73      })
74      .then(res => {
75          if ( ! res.ok ) {
76              console.error(`Remote call not ok: ${res.status}`);
77              console.log(res);
78              return false;
79          }
80          return res.json();
81      })
82      .then(response => {
83          var response = response,
84              check = response?.geoJson?.data;
85
86          if ( ! response || ( check && Array.isArray(check) && check.length < 2 ) ) {
87              console.error('No remote location data');
88              return false;
89          } else {
90              return true;
91          }
92      })
93      .catch(err => {
94          console.error('No remote location data', err);
95          return false;
96      });
97
98      // render notice
99
100      if ( check === true ) { return true; }
101
102      this.maps = document.querySelectorAll('.lmp-map');
103
104      for (const mapElement of this.maps) {
105          mapElement.innerHTML = '<div style="text-align: center; height: 100%; display: grid; justify-content: center; align-content: center; background-color: red;">No data yet, please add meta fields to Locations!</div>';
106      }
107    }
108
109    setupMaps(reinit = false) {
110        this.maps = document.querySelectorAll('.lmp-map');
111
112        for (const mapElement of this.maps) {
113            //if ( reinit === true ) { mapElement.innerHtml = ''; }
114            const id = mapElement.id || `map-${Math.random().toString(36).substr(2, 9)}`;
115            mapElement.id = id;
116
117            this.mapsData[id] = { map: null, pinCounts: {}, statesLayer: null, locationsLayer: null };
118
119            this.updatePinCounts(id);
120            this.renderMap(mapElement);
121        }
122    }
123
124    setupGutenberg() {
125        const isInEditor = document.body.classList.contains('block-editor-page');
126
127        if ( isInEditor ) {
128            const { subscribe, select } = wp.data;
129
130            let previous = select('core/editor').getDeviceType();
131
132            subscribe(() => {
133                const current = select('core/editor').getDeviceType();
134
135                if ( current !== previous ) {
136                    previous = current;
137
138                    setTimeout(() => {
139                        this.setupMaps(true);
140                    }, 2000);
141                }
142            });
143        }
144    }
145
146    // Model / Data
147
148    setupLocalData() {
149        const cached = this.getLocalData();
150
151        if ( ! cached ) {
152            localStorage.setItem(this.storageKey, JSON.stringify(this.localStorageObject));
153        }
154    }
155
156    getLocalData() {
157        let cached = localStorage.getItem(this.storageKey) || false;
158
159        if ( cached && cached != '' ) {
160            cached = JSON.parse(cached);
161        }
162
163        return cached;
164    }
165
166    async checkRemoteModified() {
167        await this.getRemoteModified('geoJson');
168        await this.getRemoteModified('statesData');
169    }
170
171    async getRemoteModified(key = '') {
172        const cached = this.getLocalData();
173
174        await fetch(`${this.restRoute}`, {
175            method: 'POST',
176            headers: {
177                'Content-Type': 'application/json'
178            },
179            body: JSON.stringify({ key: `${key}Modified` })
180        })
181        .then(res => {
182            if ( ! res.ok ) { throw new Error(`HTTP error ${res.status}`); }
183            return res.json();
184        })
185        .then(response => {
186            var fileModifiedTime = response[`${key}Modified`]?.data;
187
188            if ( fileModifiedTime ) {
189                let newData = cached;
190                newData[key].modified = fileModifiedTime;
191                newData[key].shouldUpdate = true;
192                localStorage.setItem(this.storageKey, JSON.stringify(newData));
193                Promise.resolve(true);
194            } else {
195                throw new Error('Invalid response structure');
196            }
197        })
198        .catch(err => {
199            console.error('Error fetching remote data:', err);
200            return false;
201        });
202    }
203
204    async checkData() {
205        const cached = this.getLocalData();
206        let keys = ['geoJson', 'statesData'];
207
208        for (const id in keys) {
209            const key = keys[id];
210
211            if (
212                cached
213                && this.isObject(cached?.[key])
214                && Object.keys(cached?.[key]).length > 0
215                && ! this.shouldUpdateLocalData('key')
216            ) {
217                return Promise.resolve(cached);
218            } else {
219                await this.updateLocalData(key);
220            }
221        }
222    }
223
224    async updateLocalData(key = '') {
225        const cached = this.getLocalData();
226
227        return fetch(`${this.restRoute}`, {
228            method: 'POST',
229            headers: {
230                'Content-Type': 'application/json'
231            },
232            body: JSON.stringify({ key: `${key}` })
233        })
234        .then(res => {
235            if ( ! res.ok ) throw new Error(`HTTP error ${res.status}`);
236            return res.json();
237        })
238        .then(response => {
239            if ( response?.[key]?.data ) {
240                let newData = cached;
241                newData[key].data = response[key].data;
242                cached[key].shouldUpdate = false;
243                localStorage.setItem(this.storageKey, JSON.stringify(newData));
244
245                return Promise.resolve(true);
246            } else {
247                throw new Error('Invalid response structure');
248            }
249        })
250        .catch(err => {
251            console.error('Error fetching data:', err);
252            return null;
253        });
254    }
255
256    shouldUpdateLocalData(key = '') {
257        const cached = this.getLocalData();
258
259        if (
260            ! cached
261            || ! cached?.[key]?.modified
262            || ! cached[key].modified < 1
263            || !! cached[key].shouldUpdate
264        ) {
265            return true;
266        } else {
267            cached[key].shouldUpdate = false;
268            localStorage.setItem(this.storageKey, JSON.stringify(cached));
269        }
270    }
271
272    updatePinCounts(mapId) {
273        let cached = this.getLocalData();
274
275        const pinCounts = {};
276        let max = 0;
277
278        cached.geoJson.data.features.forEach(feature => {
279            if ( ['Point', 'Polygon'].indexOf(feature.geometry.type) > -1 ) {
280                const key = feature.properties.state || feature.properties.region;
281                if ( ! key ) { return; }
282
283                pinCounts[key] = (pinCounts[key] || 0) + 1;
284                max = Math.max(max, pinCounts[key]);
285            }
286        });
287
288        this.mapsData[mapId].pinCounts = pinCounts;
289        this.mapsData[mapId].maxPinCount = max;
290    }
291
292    // Views
293
294    renderMap(mapElement) {
295        const mapId = mapElement.id;
296        const map = L.map(mapElement, {
297          minZoom: 3,
298          maxZoom: 12,
299          maxBounds: [
300            [26.396308, -124.000000],
301            [49.384358, -66.934570]
302          ],
303          maxBoundsViscosity: 1.0,
304          attributionControl: false,
305          // Sleep Controls
306          sleep: true,                      // false if you want an unruly map
307          sleepTime: 5000,                  // time(ms) until map sleeps on mouseout
308          wakeTime: 1000,                   // time(ms) until map wakes on mouseover
309          sleepNote: true,                  // should the user receive wake instructions?
310          hoverToWake: true,                // should hovering wake the map? (non-touch devices only)
311          wakeMessage: 'Hover/Tap to Load', // a message to inform users about waking the map
312          sleepOpacity: .7,                 // opacity for the sleeping map
313        });
314
315        // Define the bounds for the continental US
316        const bounds = [
317            [23, -125], // Southwest corner
318            [49, -67]   // Northeast corner
319        ];
320
321        // Use fitBounds to automatically zoom and center the map
322        map.fitBounds(bounds);
323
324        // Leaflet Freezy
325        L.control.freezeMapControl({
326          // Whether to immediately freeze the map when the control is added.
327          freezeOnAdd: true,
328          // Opacity of the map container when it's frozen.
329          frozenMapOpacity: 0.5,
330          // Thaw the map when hovering the cursor over it for a certain duration.
331          // Default: Browser-dependent (true for Chromium-based, false for others)
332          hoverToThaw: true,
333          // Amount of time after which hovering thaws the map. [ms]
334          hoverToThawDuration: 1000,
335          // Freeze the map again when leaving the map container with the cursor for a certain
336          // duration.
337          leaveToFreeze: true,
338          // Amount of time after which to freeze the map when the cursor has left the map container.
339          // [ms]
340          leaveToFreezeDuration: 2000,
341          // Whether to display the 'Freeze' button when the map is thawed.
342          freezeButtonWhenThawed: false,
343          // Inner HTML of the 'Freeze' button.
344          freezeButtonInnerHtml: '🔒',
345          // Title (hover text) of the 'Freeze' button.
346          freezeButtonTitle: 'Deactivate map',
347          // Overlay content to show while the map is frozen. This can be either of:
348          //   undefined: Use the plugin's standard text overlay
349          //   function: Function to evaluate in order to obtain a DOM element tree.
350          //   null: Disable the overlay
351          frozenOverlay: undefined,
352        }).addTo(map);
353
354        this.mapsData[mapId].map = map;
355
356        L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png?key=cb1_2vtx_1_eaf1b1ea88f39991a92a992b', {
357            maxZoom: 12,
358            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attributions">CARTO</a>',
359        }).addTo(map);
360
361        const infoControl = this.createInfoWinPins(mapId, map);
362        this.createStatesLayer(mapId, map);
363        this.createLocationsLayer(mapId, map, infoControl);
364        //this.createInfoWinLegend(mapId, map);
365
366        const { statesLayer, locationsLayer } = this.mapsData[mapId];
367        const group = L.featureGroup([locationsLayer, statesLayer]); // ordered for stack
368
369        try {
370            const bounds = group.getBounds();
371
372            if ( bounds.isValid() ) {
373                if ( window.innerWidth > 1200 ) {
374                  map.setZoom(4.5);
375                  //map.fitBounds(bounds);
376                  //map.setZoom(3);
377                }
378            } else {
379                console.warn('Invalid bounds, using default view');
380            }
381        } catch ( error ) {
382            console.error('Error setting map bounds:', error);
383        }
384
385        // Bind state click events after layers are added
386        statesLayer.eachLayer(layer => {
387            const stateCode = layer.feature.properties.code;
388            layer.setStyle(this.style(layer.feature, this.mapsData[mapId].pinCounts[stateCode] || 0, mapId));
389            layer.off();
390            this.onEachStateFeature(layer.feature, layer, map, mapId);
391        });
392
393        // Add state legend content reset / close
394        this.setupStateLegendReset(mapId);
395    }
396
397    setupStateLegendReset(mapId = '') {
398        const map = document.getElementById(mapId);
399
400        if ( map ) {
401            map.addEventListener('click', (e) => {
402                if ( e.target && e.target.matches('.info-win-location-content__region-close')  ) {
403                  this.stateLegendReset(mapId);
404                }
405            });
406        }
407    }
408
409    stateLegendReset(mapId = 0) {
410        const map = document.getElementById(mapId);
411        const infowin = map.querySelector('.info-win-location-content');
412
413        if ( map && infowin ) {
414            // empty infowin
415            infowin.innerHTML = '';
416            // reset map
417            this.mapsData[mapId].map.setView([49.384358, -95.7129], 4);
418        }
419    }
420
421    createStatesLayer(mapId, map) {
422        let cached = this.getLocalData();
423
424        const statesLayer = L.geoJson(cached.statesData.data, {
425            style: feature => this.style(feature, this.mapsData[mapId].pinCounts[feature.properties.state] || 0, mapId)
426        });
427
428        statesLayer.addTo(map);
429        this.mapsData[mapId].statesLayer = statesLayer;
430    }
431
432    createLocationsLayer(mapId, map, infoControl) {
433        let cached = this.getLocalData();
434
435        const locationsLayer = L.geoJson(cached.geoJson.data, {
436            pointToLayer: (feature, latlng) => {
437                if ( latlng.lat && latlng.lng && latlng.lat !== 0 && latlng.lng !== 0 && ! isNaN(latlng.lat) && ! isNaN(latlng.lng) ) {
438                    // client wants no markers
439                    return L.circleMarker(latlng, { radius: 0, fill: false, stroke: false });
440                    /*
441                    return feature.properties.use_pin === '1'
442                        ? L.marker(latlng)
443                        : L.circleMarker(latlng, { radius: 8, fillColor: '#FCC13B', fillOpacity: 1 });
444                    */
445                }
446                console.warn('Invalid coordinates for feature:', feature);
447                return null;
448            },
449            style: feature =>
450                feature.geometry.type !== 'Point'
451                    ? this.style(feature, this.mapsData[mapId].pinCounts[feature.properties.state] || 0, mapId)
452                    : {},
453            filter: (feature) => {
454                if ( feature.geometry.type === 'Point' ) {
455                    const [lng, lat] = feature.geometry.coordinates;
456                    return lng !== 0 && lat !== 0 && !isNaN(lng) && !isNaN(lat);
457                }
458                return true;
459            }
460        });
461
462        locationsLayer.eachLayer(layer => {
463            this.onEachPinFeature(layer.feature, layer, map, infoControl);
464        });
465
466        locationsLayer.addTo(map).bringToFront();
467        this.mapsData[mapId].locationsLayer = locationsLayer;
468    }
469
470    // View - InfoWindows
471
472    createInfoWin(name, contentHTML) {
473        const div = L.DomUtil.create("div", name);
474        div.setAttribute('class', `info-win ${name}`);
475        div.innerHTML = contentHTML;
476
477        L.DomEvent.disableClickPropagation(div);
478        L.DomEvent.disableScrollPropagation(div);
479
480        return div;
481    }
482
483    createInfoWinLegend(mapId, map) {
484        const legend = L.control({ position: 'bottomleft' });
485
486        legend.onAdd = () => {
487            const max = this.mapsData[mapId]?.maxPinCount || 5;
488            const grades = Array.from({ length: max + 1 }, (_, i) => i);
489            let content = [
490                '<div class="info-win-legend__title">Locations in State</div>',
491                '<div class="info-win-legend__grid">',
492            ];
493
494            grades.forEach(grade => {
495                const opacity = 0.8;
496                content.push(`
497                    <div class="info-win-legend__item">
498                        <span>${grade}</span>
499                        <span style="background:${this.getColor(grade, mapId)}; opacity:${opacity};"></span>
500                    </div>
501                `);
502            });
503
504            content.push('</div>');
505            return this.createInfoWin('info-win-legend', content.join(''));
506        };
507
508        legend.addTo(map);
509    }
510
511    createInfoWinPins(mapId, map) {
512        const info = L.control({ position: 'topright' });
513
514        info.onAdd = () => {
515          const newBody =   `<div class="info-win-location-content">
516                              <div class="info-win-location-content__region">
517                                <span class="info-win-location-content__region-text default">
518                                  Click on the states in blue to explore our Newport Academy locations.
519                                </span>
520                                <span class="info-win-location-content__region-close">
521                                  <span></span>
522                                </span>
523                              </div>
524                            </div>`;
525            const content = `<div class="info-win-location__inner">${this.infoWinPin.header + newBody}</div>`;
526            return this.createInfoWin('info-win-location', content);
527        };
528
529        info.update = (props) => {
530            if ( ! props ) { return; }
531
532            const { pinCounts, locationsLayer } = this.mapsData[mapId];
533            this.updateInfo({ properties: props, geometry: { type: 'Point' } }, pinCounts, locationsLayer);
534        };
535
536        info.addTo(map);
537
538        return info;
539    }
540
541    createPinInfo(title = '', photo = '', url = '', category = '') {
542        if ( photo == '' ) {
543            photo = `${window.location.origin}/apple-touch-icon-180x180.png`;
544        }
545
546        return `
547        <div class="info-win-location-item">
548            <div class="info-win-location-item__img">
549                ${photo ? `<img src="${photo}" alt="Location photo of ${title}" />` : ''}
550            </div>
551            <div class="info-win-location-item__title">${title}</div>
552            ${category ? `<div class="info-win-location-item__category">${category}</div>` : ''}
553            <div class="info-win-location-item__link">
554                ${url ? `<a class="et_pb_button" href="${url}" target="_blank">Visit Location</a>` : ''}
555            </div>
556        </div>`;
557    }
558
559    updateInfo(feature, pinCounts, locationsLayer) {
560        const info = document.querySelector('.info-win-location__inner');
561        if ( ! info ) { return; }
562
563        let content = '';
564        let region = '';
565        let stateOrRegion = feature.properties.state || feature.properties.region || feature.properties.code || 'Unknown';
566        let category = feature.properties.category || '';
567
568        if ( feature.geometry.type === 'Point' ) {
569            content = this.createPinInfo(feature.properties.title, feature.properties.photo, feature.properties.url, category);
570        } else {
571            const pins = locationsLayer?.getLayers()
572                .filter(layer => {
573                    const props = layer.feature.properties;
574                    return props.state === stateOrRegion || props.region === stateOrRegion;
575                })
576                .map(layer => ({
577                    title: layer.feature.properties.title || '',
578                    photo: layer.feature.properties.photo || '',
579                    url: layer.feature.properties.url || '',
580                    category: layer.feature.properties.category || ''
581                }));
582
583            region = `<div class="info-win-location-content__region"><span class="info-win-location-content__region-text">${feature.properties.name}</span><span class="info-win-location-content__region-close"><span></div>`;
584            content = pins.map(pin => this.createPinInfo(pin.title, pin.photo, pin.url, pin.category)).join('');
585        }
586
587        info.innerHTML = `${this.infoWinPin.header}
588            <div class="info-win-location-content">
589                ${region}
590                <div class="info-win-location-grid">${content}</div>
591            </div>`;
592    }
593
594    // Event(ish)
595
596    onEachStateFeature(feature, layer, map, mapId) {
597        const { pinCounts, locationsLayer } = this.mapsData[mapId];
598        const stateCode = layer.feature.properties.code;
599        const pinCount = this.mapsData[mapId].pinCounts[stateCode];
600
601        layer.on({
602            mouseover: (e) => {
603                this.onHoverFeature(e, pinCount);
604            },
605            mouseout: (e) => {
606                this.resetHighlight(e, pinCount, mapId)
607            },
608            click: (e) => {
609              if ( pinCount > 0 && typeof pinCount === 'number' ) {
610                    this.updateInfo(feature, pinCounts, locationsLayer);
611                    this.zoomToFeature(e, map);
612                }
613            }
614        });
615    }
616
617    onEachPinFeature(feature, layer, map, infoControl) {
618        layer.on({
619            click: () => {
620                infoControl.update(feature.properties);
621            }
622        });
623    }
624
625    zoomToFeature(e, map) {
626        if ( e.target.getBounds ) {
627            map.fitBounds(e.target.getBounds());
628        }
629    }
630
631    onHoverFeature(e, pinCount = 0) {
632        var layer = e.target;
633
634        layer.setStyle({
635            weight: 2,
636            color: 'white',
637            dashArray: '',
638            fillColor: ( pinCount < 1 ) ? '#999999' : '#FCC13B', //#92A8D1
639            fillOpacity: 1
640        });
641
642        //layer.bringToFront();
643    }
644
645    // Utils
646
647    getColor(pinCount, mapId) {
648        const max = this.mapsData[mapId]?.maxPinCount || 5;
649        if ( max === 0 ) { return '#cccccc'; }
650
651        const t = Math.min(pinCount / max, 1);
652        const r = Math.round(204 + (39 - 204) * t);
653        const g = Math.round(204 + (41 - 204) * t);
654        const b = Math.round(204 + (82 - 204) * t);
655
656        return `rgb(${r},${g},${b})`;
657    }
658
659    style(feature, pinCount, mapId) {
660        return {
661            weight: 2,
662            opacity: 1,
663            color: 'white',
664            dashArray: '',
665            fillOpacity: 1, //(pinCount < 1) ? 0.95 : 0.8,
666            fillColor: (pinCount < 1) ? '#888888' : '#272952', // #4b70b3
667            //fillColor: this.getColor(pinCount, mapId)
668        };
669    }
670
671    resetHighlight(e, pinCount = 0, mapId = 0) {
672        this.mapsData[mapId].statesLayer.eachLayer((layer) => {
673            if ( layer === e.target ) {
674                const stateCode = layer.feature.properties.code;
675                layer.setStyle(this.style(layer.feature, this.mapsData[mapId].pinCounts[stateCode] || 0, mapId));
676            }
677        });
678    }
679
680    isObject(obj) {
681        return (obj && typeof obj === 'object' && ! Array.isArray(obj) );
682    }
683
684}
685
686new LmpMapRenderer();
687}

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.