105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
|
|
/**
|
||
|
|
* Pure data-transform utilities for the map view (M2-T09).
|
||
|
|
* No leaflet imports — these functions are unit-testable in jsdom.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import type { LocationRecord, PooRecord } from '../records'
|
||
|
|
|
||
|
|
/** A heat point for L.heatLayer: [lat, lng, intensity]. */
|
||
|
|
export type HeatPoint = [number, number, number]
|
||
|
|
|
||
|
|
/** Map point with attached source record for click-to-edit. */
|
||
|
|
export interface LocationMapPoint {
|
||
|
|
lat: number
|
||
|
|
lng: number
|
||
|
|
record: LocationRecord
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PooMapPoint {
|
||
|
|
lat: number
|
||
|
|
lng: number
|
||
|
|
record: PooRecord
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Transforms
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert location records to heat points.
|
||
|
|
* All points get intensity=1; callers can adjust if needed.
|
||
|
|
*/
|
||
|
|
export function locationsToHeatPoints(records: LocationRecord[]): HeatPoint[] {
|
||
|
|
return records.map((r) => [r.latitude, r.longitude, 1])
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert poo records to heat points.
|
||
|
|
*/
|
||
|
|
export function pooToHeatPoints(records: PooRecord[]): HeatPoint[] {
|
||
|
|
return records.map((r) => [r.latitude, r.longitude, 1])
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert location records to map points (for scatter layer).
|
||
|
|
*/
|
||
|
|
export function locationsToMapPoints(records: LocationRecord[]): LocationMapPoint[] {
|
||
|
|
return records.map((r) => ({ lat: r.latitude, lng: r.longitude, record: r }))
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert poo records to map points (for scatter layer).
|
||
|
|
*/
|
||
|
|
export function pooToMapPoints(records: PooRecord[]): PooMapPoint[] {
|
||
|
|
return records.map((r) => ({ lat: r.latitude, lng: r.longitude, record: r }))
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Client-side time-window filter (for poo records — the endpoint has no server filter)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Filter poo records to those whose timestamp falls within [start, end] (inclusive).
|
||
|
|
* start and end are ISO8601 strings (e.g. "2026-01-01T00:00:00Z").
|
||
|
|
* If start or end is null, that bound is open (no filtering on that side).
|
||
|
|
*/
|
||
|
|
export function filterPooByTimeWindow(
|
||
|
|
records: PooRecord[],
|
||
|
|
start: string | null,
|
||
|
|
end: string | null,
|
||
|
|
): PooRecord[] {
|
||
|
|
if (!start && !end) return records
|
||
|
|
return records.filter((r) => {
|
||
|
|
const ts = r.timestamp
|
||
|
|
if (start && ts < start) return false
|
||
|
|
if (end && ts > end) return false
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Default time window helpers
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
/** Returns ISO8601 string for N days ago from now (UTC). */
|
||
|
|
export function daysAgoISO(days: number): string {
|
||
|
|
const d = new Date()
|
||
|
|
d.setUTCDate(d.getUTCDate() - days)
|
||
|
|
return d.toISOString()
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Returns ISO8601 string for now (UTC). */
|
||
|
|
export function nowISO(): string {
|
||
|
|
return new Date().toISOString()
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Compute a bounding center from an array of lat/lng points. Returns null if empty. */
|
||
|
|
export function computeCenter(
|
||
|
|
points: Array<{ lat: number; lng: number }>,
|
||
|
|
): [number, number] | null {
|
||
|
|
if (points.length === 0) return null
|
||
|
|
const sumLat = points.reduce((s, p) => s + p.lat, 0)
|
||
|
|
const sumLng = points.reduce((s, p) => s + p.lng, 0)
|
||
|
|
return [sumLat / points.length, sumLng / points.length]
|
||
|
|
}
|