fix: handle all ES geo_point formats from Cesium+

Cesium+ stores geoPoint in multiple Elasticsearch formats (object,
string "lat,lon", array [lon,lat]). Using z.object() caused a ZodError
that silently swallowed the entire Cesium+ response, leaving geoMap
empty and displaying 0 geolocalized transactions.

Replace the strict Zod schema with z.unknown() and a parseGeoPoint()
helper that normalizes all three formats. Also add [GéoFlux] debug
logs to DataService to trace keyMap/duniterKeys/geoMap pipeline steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-22 18:12:15 +01:00
parent 9265517c52
commit a288a5b2e7
2 changed files with 39 additions and 12 deletions

View File

@@ -47,14 +47,16 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
let keyMap = new Map<string, string>();
try {
keyMap = await getIdentityKeyMap();
console.log('[GéoFlux] keyMap:', keyMap.size, 'entrées');
} catch (err) {
console.warn('Identity key map indisponible :', err);
console.error('[GéoFlux] Identity key map ERREUR:', err);
}
// Clés Duniter uniques des émetteurs
const duniterKeys = [...new Set(
rawTransfers.map((t) => keyMap.get(t.fromId)).filter(Boolean) as string[]
)];
console.log('[GéoFlux] duniterKeys:', duniterKeys.length);
// Résolution géo par clé Duniter (_id Cesium+)
let geoMap = new Map<string, { lat: number; lng: number; city: string }>();
@@ -63,8 +65,9 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
for (const [key, p] of profiles) {
geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city });
}
console.log('[GéoFlux] geoMap:', geoMap.size, 'entrées');
} catch (err) {
console.warn('Cesium+ indisponible :', err);
console.error('[GéoFlux] Cesium+ ERREUR:', err);
}
// Seules les transactions avec un profil géo entrent dans le heatmap

View File

@@ -22,15 +22,13 @@ export interface GeoProfile {
lng: number;
}
// geoPoint accepte n'importe quel type — Cesium+ utilise plusieurs formats ES geo_point
const HitSchema = z.object({
_id: z.string(),
_source: z.object({
title: z.string().optional(),
city: z.string().optional(),
geoPoint: z.object({
lat: z.coerce.number().min(-90).max(90),
lon: z.coerce.number().min(-180).max(180),
}).optional(),
geoPoint: z.unknown().optional(),
}),
});
@@ -40,6 +38,30 @@ const SearchResponseSchema = z.object({
}),
});
/** Normalise les différents formats Elasticsearch geo_point → {lat, lng} ou null */
function parseGeoPoint(raw: unknown): { lat: number; lng: number } | null {
if (raw == null) return null;
// Format objet { lat, lon }
if (typeof raw === 'object' && !Array.isArray(raw)) {
const g = raw as Record<string, unknown>;
const lat = Number(g.lat);
const lon = Number(g.lon);
if (isFinite(lat) && isFinite(lon) && lat >= -90 && lat <= 90) return { lat, lng: lon };
}
// Format string "lat,lon"
if (typeof raw === 'string') {
const [a, b] = raw.split(',').map((s) => Number(s.trim()));
if (isFinite(a) && isFinite(b) && a >= -90 && a <= 90) return { lat: a, lng: b };
}
// Format tableau [lon, lat] (ES geo_point array)
if (Array.isArray(raw) && raw.length === 2) {
const lon = Number(raw[0]);
const lat = Number(raw[1]);
if (isFinite(lat) && isFinite(lon) && lat >= -90 && lat <= 90) return { lat, lng: lon };
}
return null;
}
/**
* Résout les coordonnées par clé Duniter (Cesium+ _id).
* Plus fiable que par nom car la clé est unique et indépendante de la casse.
@@ -70,12 +92,13 @@ export async function resolveGeoByKeys(
const result = new Map<string, GeoProfile>();
for (const hit of parsed.hits.hits) {
const src = hit._source;
if (!src.geoPoint) continue;
const geo = parseGeoPoint(src.geoPoint);
if (!geo) continue;
result.set(hit._id, {
name: src.title ?? '',
city: src.city ?? 'Inconnue',
lat: src.geoPoint.lat,
lng: src.geoPoint.lon,
lat: geo.lat,
lng: geo.lng,
});
}
return result;
@@ -127,12 +150,13 @@ export async function resolveGeoByNames(
const result = new Map<string, GeoProfile>();
for (const hit of parsed.hits.hits) {
const src = hit._source;
if (src.geoPoint && src.title) {
const geo = parseGeoPoint(src.geoPoint);
if (geo && src.title) {
result.set(src.title.toLowerCase(), {
name: src.title,
city: src.city ?? 'Inconnue',
lat: src.geoPoint.lat,
lng: src.geoPoint.lon,
lat: geo.lat,
lng: geo.lng,
});
}
}