import { createClusterIndex } from './mapCluster.js' import { syncFeatureMarkers } from './mapMarkerSync.js' import { cotCategory, formatCotPopup, getCotIconHtml } from './cotDisplay.js' const ICON_SIZE = 28 const CLUSTER = createClusterIndex({ radius: 50, maxZoom: 14, minPoints: 2 }) function escapeHtml(text) { const div = document.createElement('div') div.textContent = text return div.innerHTML } export function entitiesToFeatures(entities) { return (entities || []) .filter(e => typeof e?.lat === 'number' && typeof e?.lng === 'number' && e?.id) .map(e => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [e.lng, e.lat] }, properties: { entity: e, cotCategory: cotCategory(e.type) }, })) } export function loadCotCluster(entities) { CLUSTER.load(entitiesToFeatures(entities)) } export function getCotClusters(view) { return CLUSTER.query(view) } function featureKey(feature) { const props = feature.properties ?? {} if (props.cluster) return `c:${props.cluster_id}` const id = props.entity?.id return id != null ? `e:${id}` : null } function clusterIcon(L, count) { const size = count < 10 ? 28 : count < 100 ? 34 : 40 return L.divIcon({ className: 'cot-cluster-icon', html: `${count}`, iconSize: [size, size], iconAnchor: [size / 2, size / 2], }) } function entityIcon(L, entity) { const { html, className } = getCotIconHtml(entity) return L.divIcon({ className: `poi-div-icon cot-entity-icon ${className}`, html, iconSize: [ICON_SIZE, ICON_SIZE], iconAnchor: [ICON_SIZE / 2, ICON_SIZE / 2], }) } function coords(feature) { const [lng, lat] = feature.geometry.coordinates return { lat, lng } } function attachClusterClick(marker, feature, map) { marker.on('click', () => { const { lat, lng } = coords(feature) const props = feature.properties ?? {} const zoom = props.expansionZoom ?? map.getZoom() + 2 map.setView([lat, lng], Math.min(zoom, 19), { animate: true }) }) } export function createCotLayer(L, map) { return L.layerGroup().addTo(map) } export function syncCotLayer(L, map, layer, features) { syncFeatureMarkers(layer, features, { keyFor: featureKey, create: (feature) => { const { lat, lng } = coords(feature) const props = feature.properties ?? {} const isCluster = Boolean(props.cluster) const icon = isCluster ? clusterIcon(L, props.point_count) : entityIcon(L, props.entity) const marker = L.marker([lat, lng], { icon }) if (isCluster) attachClusterClick(marker, feature, map) else if (props.entity) { marker.bindPopup( formatCotPopup(props.entity, escapeHtml), { className: 'kestrel-live-popup-wrap', maxWidth: 360 }, ) } return marker }, update: (marker, feature) => { const { lat, lng } = coords(feature) const props = feature.properties ?? {} const isCluster = Boolean(props.cluster) marker.setLatLng([lat, lng]) const icon = isCluster ? clusterIcon(L, props.point_count) : entityIcon(L, props.entity) marker.setIcon(icon) if (!isCluster && props.entity) { marker.setPopupContent(formatCotPopup(props.entity, escapeHtml)) } }, }) }