who_need_help/assets/js/map_cluster_points.mjs

108 lines
3.0 KiB
JavaScript

const clusterCount = point => {
const count = Number(point?.count)
return Number.isFinite(count) && count > 0 ? count : 1
}
const urgentCount = point => {
const count = Number(point?.urgent_count)
return Number.isFinite(count) && count > 0 ? count : 0
}
export const clusterDiameterPixels = count => {
if (count >= 100_000) return 60
if (count >= 10_000) return 56
if (count >= 1_000) return 52
return 44
}
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"
}
const mergeDistance = (left, right, gap) =>
(clusterDiameterPixels(left.count) + clusterDiameterPixels(right.count)) / 2 + gap
const coordinatesFromPixel = (map, pixel) => {
const coordinates = map.unproject([pixel.x, pixel.y])
return {
longitude: Number(coordinates.lng ?? coordinates[0]),
latitude: Number(coordinates.lat ?? coordinates[1])
}
}
const nearbyMapPointGroups = (map, points, options = {}) => {
const gap = Number.isFinite(options.gap) ? Math.max(options.gap, 0) : 8
const groups = points.map(point => {
const pixel = map.project([point.longitude, point.latitude])
return {
points: [point],
x: pixel.x,
y: pixel.y,
count: clusterCount(point),
urgent_count: urgentCount(point)
}
})
let merged = true
while (merged) {
merged = false
outer:
for (let leftIndex = 0; leftIndex < groups.length; leftIndex += 1) {
for (let rightIndex = leftIndex + 1; rightIndex < groups.length; rightIndex += 1) {
const left = groups[leftIndex]
const right = groups[rightIndex]
const distance = Math.hypot(left.x - right.x, left.y - right.y)
if (distance >= mergeDistance(left, right, gap)) continue
const count = left.count + right.count
left.x = (left.x * left.count + right.x * right.count) / count
left.y = (left.y * left.count + right.y * right.count) / count
left.count = count
left.urgent_count += right.urgent_count
left.points.push(...right.points)
groups.splice(rightIndex, 1)
merged = true
break outer
}
}
}
return groups
}
const visualCluster = (map, group) => {
const coordinates = coordinatesFromPixel(map, group)
const firstId = group.points[0]?.id || "anonymous"
return {
type: "cluster",
id: `visual-cluster-${group.points.length}-${firstId}`,
count: group.count,
urgent_count: group.urgent_count,
longitude: coordinates.longitude,
latitude: coordinates.latitude
}
}
export const mergeNearbyMapClusters = (map, points, options = {}) =>
nearbyMapPointGroups(map, points, options).map(group => visualCluster(map, group))
export const mergeNearbyMapPoints = (map, points, options = {}) => {
return nearbyMapPointGroups(map, points, options).map(group => {
const point = group.points[0]
if (group.points.length === 1 && point?.type !== "cluster") return point
return visualCluster(map, group)
})
}