app.js was bundling Chart.js (~200 KB), Cytoscape (~500 KB) and four heavy hooks (SensorChart, NetworkMap, WeathermapViewer, SitesMap) into a single 1.25 MB blob that every page paid for. Extract each into its own module under assets/js/hooks/ and reference them through the existing lazyHook() wrapper so the implementation is fetched only when a hook actually mounts. esbuild --splitting then factors the shared cytoscape vendor blob into a chunk that's only loaded on topology / weathermap pages. After-bundle layout (minified): app.js 297.8 KB (was ~1250 KB; -76%) chunk-LJGWK7RH (cyto) 555.1 KB loaded on topology pages only sensor_chart 325.3 KB loaded on sensor pages only network_map 24.4 KB topology hook glue weathermap 13.0 KB weathermap hook glue sites_map 4.1 KB site map hook glue coverage_hooks 25.0 KB coverage hook glue LeafletMap also moved to sites_map.ts and now uses ensureLeaflet instead of polling for window.L with setTimeout.
152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
// Lazy-loaded sites map hook (Leaflet). Leaflet itself is loaded on
|
|
// demand via ensureLeaflet (no bundling), so this chunk is small.
|
|
|
|
import { ensureLeaflet } from "../lib/leaflet"
|
|
|
|
declare const L: any
|
|
|
|
export const SitesMap = {
|
|
map: null as any,
|
|
markers: null as any,
|
|
|
|
mounted(this: any) {
|
|
ensureLeaflet().then(() => {
|
|
const sitesData = JSON.parse(this.el.dataset.sites || '[]')
|
|
this.initializeLeaflet(sitesData)
|
|
|
|
this.handleEvent("site_clicked", (payload: any) => {
|
|
this.pushEvent("site_clicked", payload)
|
|
})
|
|
}).catch((e: any) => {
|
|
console.error('SitesMap: Leaflet failed to load', e)
|
|
})
|
|
},
|
|
|
|
updated(this: any) {
|
|
if (this.map) {
|
|
const sitesData = JSON.parse(this.el.dataset.sites || '[]')
|
|
this.updateSites(sitesData)
|
|
}
|
|
},
|
|
|
|
destroyed(this: any) {
|
|
if (this.map) {
|
|
this.map.remove()
|
|
this.map = null
|
|
this.markers = null
|
|
}
|
|
},
|
|
|
|
initializeLeaflet(this: any, sites: any[]) {
|
|
this.map = L.map(this.el, {
|
|
zoomControl: true,
|
|
scrollWheelZoom: true
|
|
})
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
maxZoom: 18
|
|
}).addTo(this.map)
|
|
|
|
this.updateSites(sites)
|
|
},
|
|
|
|
updateSites(this: any, sites: any[]) {
|
|
if (!this.map) return
|
|
|
|
if (this.markers) {
|
|
this.map.removeLayer(this.markers)
|
|
}
|
|
|
|
this.markers = L.layerGroup()
|
|
|
|
if (sites.length === 0) {
|
|
this.map.setView([39.8283, -98.5795], 4)
|
|
this.markers.addTo(this.map)
|
|
return
|
|
}
|
|
|
|
const bounds = L.latLngBounds([])
|
|
|
|
sites.forEach((site: any) => {
|
|
if (site.latitude && site.longitude) {
|
|
const marker = L.marker([site.latitude, site.longitude])
|
|
|
|
let popupContent = `<div class="p-2">
|
|
<h3 class="font-semibold text-lg mb-1">${site.name}</h3>`
|
|
|
|
if (site.description) {
|
|
popupContent += `<p class="text-sm text-gray-600 mb-1">${site.description}</p>`
|
|
}
|
|
if (site.address) {
|
|
popupContent += `<p class="text-xs text-gray-500 mb-1">${site.address}</p>`
|
|
}
|
|
|
|
popupContent += `<p class="text-xs text-gray-500 mb-2">
|
|
${site.latitude.toFixed(4)}, ${site.longitude.toFixed(4)}
|
|
</p>`
|
|
|
|
if (site.device_count > 0) {
|
|
popupContent += `<p class="text-xs text-blue-600 mb-2">
|
|
${site.device_count} device${site.device_count !== 1 ? 's' : ''}
|
|
</p>`
|
|
}
|
|
|
|
popupContent += `<button
|
|
data-site-id="${site.id}"
|
|
data-action="view-site"
|
|
class="text-xs bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700"
|
|
>
|
|
View Site →
|
|
</button></div>`
|
|
|
|
marker.bindPopup(popupContent)
|
|
marker.on('popupopen', () => {
|
|
const button = marker.getPopup()?.getElement()?.querySelector('[data-action="view-site"]') as HTMLElement
|
|
if (button) {
|
|
button.addEventListener('click', () => {
|
|
this.pushEvent("site_clicked", { site_id: button.dataset.siteId })
|
|
})
|
|
}
|
|
})
|
|
this.markers.addLayer(marker)
|
|
bounds.extend([site.latitude, site.longitude])
|
|
}
|
|
})
|
|
|
|
this.markers.addTo(this.map)
|
|
|
|
if (bounds.isValid()) {
|
|
this.map.fitBounds(bounds, { padding: [20, 20] })
|
|
}
|
|
}
|
|
}
|
|
|
|
// Single-marker map for site detail page. Loads Leaflet via ensureLeaflet
|
|
// rather than relying on a global script polled with setTimeout.
|
|
export const LeafletMap = {
|
|
map: null as any,
|
|
mounted(this: any) {
|
|
const lat = parseFloat(this.el.dataset.lat)
|
|
const lng = parseFloat(this.el.dataset.lng)
|
|
const zoom = parseInt(this.el.dataset.zoom || '14')
|
|
const title = this.el.dataset.markerTitle || ''
|
|
if (isNaN(lat) || isNaN(lng)) return
|
|
|
|
ensureLeaflet().then(() => {
|
|
this.map = L.map(this.el, { scrollWheelZoom: false, zoomControl: true })
|
|
this.map.setView([lat, lng], zoom)
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap'
|
|
}).addTo(this.map)
|
|
L.marker([lat, lng]).addTo(this.map).bindPopup(title)
|
|
setTimeout(() => this.map.invalidateSize(), 200)
|
|
}).catch((e: any) => {
|
|
console.error('LeafletMap: Leaflet failed to load', e)
|
|
})
|
|
},
|
|
destroyed(this: any) {
|
|
if (this.map) { this.map.remove(); this.map = null }
|
|
}
|
|
}
|