diff --git a/src/components/StatsPanel.tsx b/src/components/StatsPanel.tsx index b7a9ea1..cd03720 100644 --- a/src/components/StatsPanel.tsx +++ b/src/components/StatsPanel.tsx @@ -1,5 +1,6 @@ import { useRef } from 'react'; import type { PeriodStats } from '../services/DataService'; +import { countryCodeToFlag } from '../services/adapters/CesiumAdapter'; interface StatsPanelProps { stats: PeriodStats | null; @@ -120,7 +121,9 @@ export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelPro > {MEDALS[i]}
-

{city.name}

+

+ {countryCodeToFlag(city.countryCode)}{city.countryCode ? ' ' : ''}{city.name} +

{city.count} tx

diff --git a/src/data/mockData.ts b/src/data/mockData.ts index b63b9c1..fa019a9 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -5,6 +5,7 @@ export interface Transaction { lng: number; amount: number; // Ğ1 (pas en centimes) city: string; + countryCode: string; // ISO 3166-1 alpha-2, ex: "FR" fromKey: string; // SS58 Ğ1v2 : préfixe "g1", ~50 chars toKey: string; } @@ -75,6 +76,7 @@ function generateTransactions(count: number, maxAgeMs: number): Transaction[] { lng, amount, city: city.name, + countryCode: 'FR', fromKey: generateKey(), toKey: generateKey(), }); @@ -94,10 +96,10 @@ export function computeStats(transactions: Transaction[]) { const totalVolume = transactions.reduce((sum, tx) => sum + tx.amount, 0); const transactionCount = transactions.length; - const cityVolumes: Record = {}; + const cityVolumes: Record = {}; for (const tx of transactions) { if (!cityVolumes[tx.city]) { - cityVolumes[tx.city] = { volume: 0, count: 0 }; + cityVolumes[tx.city] = { volume: 0, count: 0, countryCode: tx.countryCode ?? '' }; } cityVolumes[tx.city].volume += tx.amount; cityVolumes[tx.city].count += 1; diff --git a/src/services/DataService.ts b/src/services/DataService.ts index ccb40d0..56f62c7 100644 --- a/src/services/DataService.ts +++ b/src/services/DataService.ts @@ -56,11 +56,11 @@ async function fetchLiveTransactions(periodDays: number): Promise<{ )]; // Résolution géo par clé Duniter (_id Cesium+) - let geoMap = new Map(); + let geoMap = new Map(); try { const profiles = await resolveGeoByKeys(duniterKeys); for (const [key, p] of profiles) { - geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city }); + geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city, countryCode: p.countryCode }); } } catch (err) { console.warn('Cesium+ indisponible :', err); @@ -75,14 +75,15 @@ async function fetchLiveTransactions(periodDays: number): Promise<{ if (!geo) continue; geolocated.push({ - id: t.id, - timestamp: t.timestamp, - lat: geo.lat, - lng: geo.lng, - amount: t.amount, - city: geo.city, - fromKey: t.fromId, - toKey: t.toId, + id: t.id, + timestamp: t.timestamp, + lat: geo.lat, + lng: geo.lng, + amount: t.amount, + city: geo.city, + countryCode: geo.countryCode, + fromKey: t.fromId, + toKey: t.toId, }); } @@ -96,7 +97,7 @@ export interface PeriodStats { totalVolume: number; transactionCount: number; // total blockchain (y compris non-géolocalisés) geoCount: number; // transactions visibles sur la carte - topCities: { name: string; volume: number; count: number }[]; + topCities: { name: string; volume: number; count: number; countryCode: string }[]; } export interface DataResult { diff --git a/src/services/adapters/CesiumAdapter.ts b/src/services/adapters/CesiumAdapter.ts index 816d02e..632e486 100644 --- a/src/services/adapters/CesiumAdapter.ts +++ b/src/services/adapters/CesiumAdapter.ts @@ -16,10 +16,46 @@ import { z } from 'zod'; export const CESIUM_ENDPOINT = 'https://g1.data.e-is.pro'; export interface GeoProfile { - name: string; // nom d'identité Ğ1 (ex: "Anikka") - city: string; - lat: number; - lng: number; + name: string; // nom d'identité Ğ1 (ex: "Anikka") + city: string; + countryCode: string; // ISO 3166-1 alpha-2, ex: "FR" + lat: number; + lng: number; +} + +// Détection de pays par bounding box (pays présents dans la communauté Ğ1) +const COUNTRY_BOXES: { code: string; latMin: number; latMax: number; lngMin: number; lngMax: number }[] = [ + { code: 'FR', latMin: 41.3, latMax: 51.1, lngMin: -5.1, lngMax: 9.6 }, + { code: 'BE', latMin: 49.5, latMax: 51.5, lngMin: 2.5, lngMax: 6.4 }, + { code: 'CH', latMin: 45.8, latMax: 47.8, lngMin: 5.9, lngMax: 10.5 }, + { code: 'LU', latMin: 49.4, latMax: 50.2, lngMin: 5.7, lngMax: 6.5 }, + { code: 'DE', latMin: 47.3, latMax: 55.1, lngMin: 6.0, lngMax: 15.0 }, + { code: 'ES', latMin: 35.9, latMax: 43.8, lngMin: -9.3, lngMax: 4.3 }, + { code: 'PT', latMin: 36.8, latMax: 42.2, lngMin: -9.5, lngMax: -6.2 }, + { code: 'IT', latMin: 36.6, latMax: 47.1, lngMin: 6.6, lngMax: 18.5 }, + { code: 'GB', latMin: 49.9, latMax: 60.9, lngMin: -8.2, lngMax: 1.8 }, + { code: 'NL', latMin: 50.7, latMax: 53.6, lngMin: 3.3, lngMax: 7.2 }, + { code: 'MA', latMin: 27.6, latMax: 35.9, lngMin: -13.2, lngMax: -1.0 }, + { code: 'TN', latMin: 30.2, latMax: 37.5, lngMin: 7.5, lngMax: 11.6 }, + { code: 'SN', latMin: 12.3, latMax: 16.7, lngMin: -17.5, lngMax: -11.4 }, + { code: 'CA', latMin: 41.7, latMax: 83.1, lngMin: -141.0,lngMax: -52.6 }, + { code: 'BR', latMin: -33.7, latMax: 5.3, lngMin: -73.9, lngMax: -34.8 }, +]; + +function latLngToCountryCode(lat: number, lng: number): string { + // France métropolitaine en premier (cas le plus fréquent) + for (const b of COUNTRY_BOXES) { + if (lat >= b.latMin && lat <= b.latMax && lng >= b.lngMin && lng <= b.lngMax) return b.code; + } + return ''; +} + +/** Convertit un code ISO 3166-1 alpha-2 en emoji drapeau */ +export function countryCodeToFlag(code: string): string { + if (!code || code.length !== 2) return ''; + return String.fromCodePoint( + ...code.toUpperCase().split('').map((c) => 0x1F1E0 - 65 + c.charCodeAt(0)) + ); } // geoPoint accepte n'importe quel type — Cesium+ utilise plusieurs formats ES geo_point @@ -95,10 +131,11 @@ export async function resolveGeoByKeys( const geo = parseGeoPoint(src.geoPoint); if (!geo) continue; result.set(hit._id, { - name: src.title ?? '', - city: src.city ?? 'Inconnue', - lat: geo.lat, - lng: geo.lng, + name: src.title ?? '', + city: src.city ?? 'Inconnue', + countryCode: latLngToCountryCode(geo.lat, geo.lng), + lat: geo.lat, + lng: geo.lng, }); } return result; @@ -153,10 +190,11 @@ export async function resolveGeoByNames( const geo = parseGeoPoint(src.geoPoint); if (geo && src.title) { result.set(src.title.toLowerCase(), { - name: src.title, - city: src.city ?? 'Inconnue', - lat: geo.lat, - lng: geo.lng, + name: src.title, + city: src.city ?? 'Inconnue', + countryCode: latLngToCountryCode(geo.lat, geo.lng), + lat: geo.lat, + lng: geo.lng, }); } }