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,
});
}
}