Files
ln-bi/src/server/routes/hydrogen-heatmap-model.ts
T

148 lines
4.1 KiB
TypeScript

export type HydrogenHeatmapMetric = 'kg' | 'refuels' | 'vehicles';
export type HydrogenPayer = 'all' | 'lingniu' | 'customer';
export type HydrogenStationRecord = {
station_id: string;
station_name: string;
address: string;
longitude: string | number;
latitude: string | number;
kg: string | number;
refuel_count: string | number;
vehicle_count: string | number;
first_refuel: string;
last_refuel: string;
};
type HydrogenHeatmapQueryInput = {
startDate?: string;
endDate?: string;
query?: string;
payer?: string;
metric?: string;
};
type NearbyQueryInput = {
lng?: string;
lat?: string;
radiusKm?: string;
};
export const HYDROGEN_HEATMAP_DEFAULT_START = '2026-01-01';
export const HYDROGEN_HEATMAP_DEFAULT_END = '2026-07-13';
function isDate(value: string | undefined): value is string {
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
}
function parseMetric(value: string | undefined): HydrogenHeatmapMetric {
if (value === 'refuels' || value === 'vehicles') return value;
return 'kg';
}
function parsePayer(value: string | undefined): HydrogenPayer {
if (value === 'lingniu' || value === 'customer') return value;
return 'all';
}
export function parseHydrogenHeatmapQuery(input: HydrogenHeatmapQueryInput) {
return {
startDate: isDate(input.startDate) ? input.startDate : HYDROGEN_HEATMAP_DEFAULT_START,
endDate: isDate(input.endDate) ? input.endDate : HYDROGEN_HEATMAP_DEFAULT_END,
query: (input.query || '').trim(),
payer: parsePayer(input.payer),
metric: parseMetric(input.metric),
};
}
export function parseNearbyQuery(input: NearbyQueryInput) {
const longitude = Number(input.lng);
const latitude = Number(input.lat);
return {
center: Number.isFinite(longitude) && Number.isFinite(latitude)
? { lng: longitude, lat: latitude }
: null,
radiusKm: Math.min(200, Math.max(5, Number(input.radiusKm) || 50)),
};
}
export function hydrogenMetricValue(
row: HydrogenStationRecord,
metric: HydrogenHeatmapMetric,
): number {
if (metric === 'refuels') return Number(row.refuel_count) || 0;
if (metric === 'vehicles') return Number(row.vehicle_count) || 0;
return Number(row.kg) || 0;
}
export function serializeHydrogenStation(
row: HydrogenStationRecord,
metric: HydrogenHeatmapMetric,
) {
return {
stationId: row.station_id,
stationName: row.station_name,
address: row.address,
lng: Number(row.longitude),
lat: Number(row.latitude),
kg: Number(row.kg) || 0,
refuelCount: Number(row.refuel_count) || 0,
vehicleCount: Number(row.vehicle_count) || 0,
firstRefuel: row.first_refuel,
lastRefuel: row.last_refuel,
metricValue: hydrogenMetricValue(row, metric),
};
}
export function rankHydrogenStations(
stations: HydrogenStationRecord[],
metric: HydrogenHeatmapMetric,
): HydrogenStationRecord[] {
return [...stations].sort((left, right) => (
hydrogenMetricValue(right, metric) - hydrogenMetricValue(left, metric)
));
}
export function buildHydrogenPoints(
stations: HydrogenStationRecord[],
metric: HydrogenHeatmapMetric,
) {
const points = stations.map((row) => ({
lng: Number(row.longitude),
lat: Number(row.latitude),
count: hydrogenMetricValue(row, metric),
}));
return {
points,
max: Math.max(1, ...points.map((point) => point.count)),
};
}
export function haversineKm(
leftLng: number,
leftLat: number,
rightLng: number,
rightLat: number,
): number {
const radians = (degrees: number) => degrees * Math.PI / 180;
const earthRadius = 6371;
const dLat = radians(rightLat - leftLat);
const dLng = radians(rightLng - leftLng);
const a = Math.sin(dLat / 2) ** 2
+ Math.cos(radians(leftLat)) * Math.cos(radians(rightLat)) * Math.sin(dLng / 2) ** 2;
return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
export function filterHydrogenStationsByRadius(
stations: HydrogenStationRecord[],
center: { lng: number; lat: number },
radiusKm: number,
): HydrogenStationRecord[] {
return stations.filter((row) => haversineKm(
center.lng,
center.lat,
Number(row.longitude),
Number(row.latitude),
) <= radiusKm);
}