who_need_help/assets/js/map_cluster_features.mjs

144 lines
4.1 KiB
JavaScript

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
})