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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user