const finiteCount = (value, fallback) => { const count = Number(value) return Number.isFinite(count) && count >= 0 ? count : fallback } export const clusterSizeClass = count => { if (count >= 100_000) return "is-xxl" if (count >= 10_000) return "is-xl" if (count >= 1_000) return "is-lg" return "is-md" } export const clusterCountLabel = (count, locale = "en") => { const value = finiteCount(count, 0) if (value < 1_000) return new Intl.NumberFormat(locale).format(value) return new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: value < 10_000 ? 1 : 0 }).format(value) } const clusterIconDiameter = count => { if (count >= 100_000) return 60 if (count >= 10_000) return 56 if (count >= 1_000) return 52 return 44 } const imageKeyPart = value => Array.from(value) .map(character => character.codePointAt(0).toString(16)) .join("-") export const clusterIconDescriptor = (point, locale = "en") => { const count = finiteCount(point?.count, 1) const urgent = finiteCount(point?.urgent_count, 0) > 0 const label = clusterCountLabel(count, locale) const diameter = clusterIconDiameter(count) return { id: `wnh-cluster-${urgent ? "urgent" : "normal"}-${diameter}-${imageKeyPart(label)}`, count, urgent, label, diameter } } export const clusterExpansionTarget = (currentZoom, requestedZoom, maximumZoom = 22) => { const current = finiteCount(currentZoom, 0) const requested = finiteCount(requestedZoom, current + 1) const nearestIntegerZoom = Math.round(current) const normalizedCurrent = Math.abs(current - nearestIntegerZoom) < 0.001 ? nearestIntegerZoom : current const nextIntegerZoom = Math.floor(normalizedCurrent) + 1 return Math.min(maximumZoom, Math.max(nextIntegerZoom, requested)) } const normalizedLongitude = longitude => { const wrapped = ((longitude + 180) % 360 + 360) % 360 - 180 return wrapped === -180 && longitude > 0 ? 180 : wrapped } export const clusterExpansionCenter = point => { const fallback = [Number(point?.longitude), Number(point?.latitude)] const bounds = point?.bounds const west = Number(bounds?.west) const south = Number(bounds?.south) const east = Number(bounds?.east) const north = Number(bounds?.north) if (![west, south, east, north].every(Number.isFinite)) return fallback const longitude = west <= east ? (west + east) / 2 : normalizedLongitude((west + east + 360) / 2) return [longitude, (south + north) / 2] } export const serverClusterDescriptor = point => { if ( point?.type !== "cluster" || !point.id || !Number.isFinite(Number(point.longitude)) || !Number.isFinite(Number(point.latitude)) ) { return null } return { key: `server:${point.id}`, count: finiteCount(point.count, 1), urgentCount: finiteCount(point.urgent_count, 0), longitude: Number(point.longitude), latitude: Number(point.latitude) } } export const pointFeatureCollection = (points, locale = "en") => ({ type: "FeatureCollection", features: points.map(point => { const clusterIcon = point.type === "cluster" ? clusterIconDescriptor(point, locale) : null return { type: "Feature", id: point.id, properties: { id: point.id, item_type: point.type || "request", title: point.title || "", location: point.location || "", exact: point.exact === true, radius_meters: Number.isFinite(point.radius_meters) ? point.radius_meters : 0, weight: finiteCount(point.count, 1), urgent_weight: finiteCount(point.urgent_count, 0), server_cluster_id: point.cluster_id || "", parent_cluster_id: point.parent_cluster_id || "", hierarchy_level: finiteCount(point.hierarchy_level, 0), expansion_zoom: finiteCount(point.expansion_zoom, 22), count_label: clusterIcon?.label || "", cluster_icon: clusterIcon?.id || "" }, geometry: { type: "Point", coordinates: [Number(point.longitude), Number(point.latitude)] } } }) }) export const pointSourceOptions = data => ({ type: "geojson", data, generateId: false, maxzoom: 22, cluster: false })