Contenu de la page /sources — par pays, les sources d’altitude, de chemins et de fonds de carte, leur résolution et leur licence.
stableÀ valider
Écran ou section propre à Pathou, pas un composant du design system : à ne pas réutiliser ailleurs. Tous les écrans et sections
// Titre : Sources des données
import DataSources from '@pathou/components/DataSources';
export default function Exemple() {
return (
<div className="ip-main" style={{ width: 720, maxHeight: 680, overflowY: 'auto' }}>
<DataSources />
</div>
);
}
Installation
Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.
import { BE_WA_ATTRIBUTION } from '../lib/elevation/be-wa';
import { DE_BKG_ATTRIBUTION } from '../lib/elevation/de-bkg';
import { REAL_PROFILE } from '../lib/sources/profile';
import { SIDE_MAX_RES_M } from '../lib/sources/thresholds';
import { COUNTRY_LABEL, DE_AUTRES_LANDER, REGIONS, REGION_COUNTRY, REGION_LABEL, REGION_SHORT, type CountryCode, type Region, type RegionProfile, type Support } from '../lib/sources/types';
type Source = [label: string, url: string];
type Kind = 'elevation' | 'extraWays';
/** Source de ce type dans le pays ; pour l'altitude, seulement si elle est assez fine (`maxResM`). */
const sourceIn = (r: Region, kind: Kind, maxResM = Infinity) => {
const p = REAL_PROFILE.regions[r] as RegionProfile | undefined;
return kind === 'elevation' && (p?.elevation?.resolutionM ?? Infinity) > maxResM ? undefined : p?.[kind];
};
/** Régions du profil groupées par pays, dans l'ordre de première apparition. */
const GROUPS: [CountryCode, Region[]][] = [];
for (const r of REGIONS.filter((x) => REAL_PROFILE.regions[x])) {
const g = GROUPS.find(([c]) => c === REGION_COUNTRY[r]);
if (g) g[1].push(r);
else GROUPS.push([REGION_COUNTRY[r], [r]]);
}
/** Pays où le profil de l'app a une source de ce type ; un pays à plusieurs régions les nomme entre parenthèses. */
const countries = (kind: Kind, maxResM?: number) =>
GROUPS.flatMap(([c, rs]) => {
const on = rs.filter((r) => sourceIn(r, kind, maxResM));
if (!on.length) return [];
return [rs.length > 1 ? `${COUNTRY_LABEL[c]} (${on.map((r) => REGION_SHORT[r]).join(', ')})` : REGION_LABEL[on[0]]];
}).join(', ');
/** Sources de ce type, sans doublon. */
const sourcesOf = (kind: Kind, maxResM?: number): Source[] => {
const seen = new Map<string, Source>();
for (const r of REGIONS) {
const s = sourceIn(r, kind, maxResM);
if (s) seen.set(s.id, [s.label, s.url]);
}
return [...seen.values()];
};
/** Résolutions extrêmes des sources d'altitude (m). */
const resolutions = REGIONS.flatMap((r) => REAL_PROFILE.regions[r]?.elevation?.resolutionM ?? []);
/** Année des mentions des services lus en direct : celle de la consultation. */
const YEAR = new Date().getFullYear();
/** Mention exigée par la licence, citée avant elle (dl-de/by-2-0 de la Rhénanie-Palatinat ; mentions d’usage en Hesse et en NRW ; mention du BKG pour basemap.de). */
const ATTRIBUTION: Record<string, string> = {
'de-he-dgm1': '© HVBG',
'de-rp-dgm1': `© GeoBasis-DE / LVermGeoRP (${YEAR})`,
'de-nw-dgm1': '© Geobasis NRW',
'be-wa-lidar': BE_WA_ATTRIBUTION,
'be-vl-dhmv': 'Bron: Digitaal Vlaanderen',
'de-bkg-dgm5': DE_BKG_ATTRIBUTION,
};
/** Contours des Länder et de l’Allemagne : VG250 du BKG, lu au WFS en 2026 (`scripts/regions/build.mjs`, `outlines.mjs`). */
export const VG250_ATTRIBUTION = '© GeoBasis-DE / BKG (2026)';
/** Mention de basemap.de demandée par le BKG (GetCapabilities). */
export const BASEMAP_DE_ATTRIBUTION = `© GeoBasis-DE / BKG (${YEAR}) CC BY 4.0`;
/** Chaque fonction de l'app et les sources (ouvertes, sans compte) dont elle dépend. */
export const DATA_SOURCES: { feature: string; sources: Source[]; note?: string }[] = [
{
feature: 'Altitude, pentes et dénivelé',
sources: sourcesOf('elevation'),
note: `altitude précise (${Math.min(...resolutions)} à ${Math.max(...resolutions)} m selon le pays) : ${countries('elevation')} ; ailleurs, altitude du GPX ou relief mondial`,
},
{ feature: 'Relief 3D', sources: [['Mapzen / AWS Terrain Tiles', 'https://registry.opendata.aws/terrain-tiles/']] },
{
feature: 'Chemins, surfaces, cotation VTT (S0–S5), accès, obstacles',
sources: [
['OpenStreetMap', 'https://www.openstreetmap.org/copyright'],
['Overpass API', 'https://overpass-api.de'],
],
},
{ feature: 'Passages absents d’OpenStreetMap', sources: sourcesOf('extraWays'), note: `${countries('extraWays')} uniquement` },
// Dévers mesuré seulement avec une altitude assez fine (TINITALY, à 10 m, ne sert pas)
{
feature: 'Dévers et sentiers en balcon',
sources: sourcesOf('elevation', SIDE_MAX_RES_M),
note: `${countries('elevation', SIDE_MAX_RES_M)} uniquement`,
},
{
feature: 'Fonds de carte',
sources: [
['IGN (photo aérienne, plan)', 'https://geoservices.ign.fr/'],
['IGN España (PNOA, MTN)', 'https://www.ign.es/web/ign/portal/ide-area-nodo-ide-ign'],
['swisstopo (SWISSIMAGE, carte nationale ; OGD)', 'https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html'],
['DGT (photo aérienne, Portugal)', 'https://www.dgterritorio.gov.pt/dados-abertos'],
[`basemap.de (carte de l’Allemagne\u00a0; ${BASEMAP_DE_ATTRIBUTION})`, 'https://basemap.de/'],
['Geobasis NRW (photo aérienne, Rhénanie-du-Nord-Westphalie)', 'https://www.bezreg-koeln.nrw.de/geobasis-nrw'],
['Digitaal Vlaanderen (photo aérienne, Flandre et Bruxelles\u00a0; Bron: Digitaal Vlaanderen)', 'https://www.geopunt.be'],
['OpenTopoMap', 'https://opentopomap.org/about'],
['CyclOSM', 'https://www.cyclosm.org/'],
['Esri World Imagery', 'https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9'],
],
},
{
feature: 'Contours des Länder (Allemagne)',
sources: [['VG250 (BKG)', 'https://gdz.bkg.bund.de/']],
note: `${VG250_ATTRIBUTION}, dl-de/by-2-0`,
},
{
feature: 'Contours des Régions (Belgique)',
sources: [
['SPW (CC BY 4.0)', 'https://geoportail.wallonie.be'],
['Digitaal Vlaanderen (VRBG)', 'https://www.geopunt.be'],
['UrbIS (CC0)', 'https://bric.brussels'],
],
},
{ feature: 'Itinéraires VTT balisés', sources: [['Waymarked Trails', 'https://mtb.waymarkedtrails.org/']] },
{ feature: 'Traces GPS publiques', sources: [['OpenStreetMap', 'https://www.openstreetmap.org/traces']] },
{ feature: "Points d'eau et points d'intérêt", sources: [['OpenStreetMap', 'https://www.openstreetmap.org/copyright']] },
{ feature: 'Photos au sol', sources: [['Panoramax', 'https://panoramax.fr/']], note: 'photos CC BY-SA des contributeurs' },
{ feature: 'Temps estimé', sources: [], note: 'calcul de l’app (pente, type de voie, niveau) ou ta propre vitesse' },
];
/** Licence de chaque source nationale, citée à côté de son nom. */
const LICENCE: Record<string, string> = {
'ign-rge-alti': 'Licence Ouverte 2.0',
'ign-lidar-hd': 'Licence Ouverte 2.0',
'ign-bdtopo': 'Licence Ouverte 2.0',
'es-mdt05': 'CC BY 4.0',
'es-btn': 'CC BY 4.0',
'ch-swissalti3d': 'OGD',
'it-tinitaly': 'CC BY 4.0',
'pt-mdt5': 'CC BY 4.0',
'de-he-dgm1': 'dl-de/zero-2-0',
'de-nw-dgm1': 'dl-de/zero-2-0',
'de-rp-dgm1': 'dl-de/by-2-0',
'be-wa-lidar': 'CC BY 4.0',
'be-vl-dhmv': 'Modellicentie voor gratis hergebruik',
'de-bkg-dgm5': 'licence bêta du BKG (usage d’essai, aucun stockage)',
};
/** Fournisseur des fonds nationaux de chaque pays (photo aérienne, carte topo), quand le profil les propose. */
export const BASEMAP_OF: Partial<Record<Region, { ortho?: string; plan?: string }>> = {
fr: { ortho: 'IGN : photo aérienne', plan: 'IGN : plan' },
es: { ortho: 'IGN España : PNOA', plan: 'IGN España : MTN' },
ad: { plan: 'IGN España : MTN' },
pt: { ortho: 'DGT : photo aérienne' },
ch: { ortho: 'swisstopo : SWISSIMAGE', plan: 'swisstopo : carte nationale' },
'de-he': { plan: 'basemap.de : carte' },
'de-rp': { plan: 'basemap.de : carte' },
'de-nw': { ortho: 'Geobasis NRW : photo aérienne', plan: 'basemap.de : carte' },
'de-autres': { plan: 'basemap.de\u00a0: carte' },
'be-vl': { ortho: 'Digitaal Vlaanderen\u00a0: photo aérienne' },
};
/** Réserve sur une source, dite à côté du pays. */
const COUNTRY_NOTE: Partial<Record<CountryCode, string>> = {
it: 'raccord d’altitude vérifié à un seul col frontalier (Larche)',
de: 'Licence de phase bêta de basemap.de 3D, «\u00a0à des fins de test\u00a0»\u00a0; Pathou lit les tuiles à l’exécution et n’en garde rien. Statut définitif demandé au BKG',
};
type Cell = 'elevation' | 'extraWays' | 'basemaps';
/** Pourquoi une donnée manque (ou reste partielle) dans un pays : constats des recherches de la feuille de route. */
/** `elsewhere` : ce que devient le reste d'un pays couvert en partie. */
type Reasons = Partial<Record<Cell | 'elsewhere', string>>;
const GAP_REASON: Partial<Record<CountryCode | 'ailleurs', Reasons>> = {
es: {
extraWays: 'pistes seulement, pas de sentiers',
},
ad: {
extraWays: 'pas de base nationale lisible en direct identifiée',
basemaps: 'photo aérienne : pas couverte par l’orthophoto espagnole PNOA',
},
it: {
extraWays: 'pas de base nationale ouverte des sentiers ; ceux du CAI sont déjà en grande partie dans OpenStreetMap',
basemaps: 'pas d’orthophoto nationale récente et ouverte',
},
pt: {
extraWays: 'pas de base ouverte utile : carte 1:10 000 de la DGT sous licence, pistes forestières (DGT) hors des montagnes et sans revêtement',
},
ch: {
extraWays: 'OpenStreetMap déjà très complet en Suisse',
},
de: {
extraWays: 'réseau ATKIS (basemap.de) prévu, richesse à mesurer',
basemaps: 'photo aérienne hors de la Rhénanie-du-Nord-Westphalie\u00a0: pas de service vérifié (repli satellite)',
},
be: {
extraWays: 'OpenStreetMap déjà très complet en Belgique',
basemaps: 'Wallonie\u00a0: photo aérienne pas encore intégrée\u00a0; carte topographique du NGI\u00a0: usage non commercial seulement',
},
ailleurs: { elevation: 'pays non pris en charge', extraWays: 'pays non pris en charge', basemaps: 'pays non pris en charge' },
};
const SUPPORT_LABEL: Record<Support, string> = { complet: 'complet', beta: 'bêta', degrade: 'dégradé' };
/** Statut le plus favorable des régions d'un pays. */
export const bestSupport = (regions: Region[]): Support =>
(['complet', 'beta', 'degrade'] as const).find((s) => regions.some((r) => REAL_PROFILE.regions[r]?.support === s)) ?? 'degrade';
interface CountrySource {
/** Land (ou région) de la source, quand le pays en a plusieurs */
land?: string;
label: string;
url: string;
licence?: string;
/** mention exigée, citée avant la licence */
attribution?: string;
resolutionM?: number;
}
export interface CountryRow {
/** null : « ailleurs » */
code: CountryCode | null;
regions: Region[];
country: string;
support: string | null;
/** une source par région dotée ; vide : relief mondial */
elevation: CountrySource[];
extraWays: CountrySource | null;
basemaps: string[];
note?: string;
/** pourquoi une donnée manque ou reste partielle */
reasons: Reasons;
}
/** Nom d’une région dans la ligne de son pays : les autres Länder sont nommés un à un, jamais regroupés. */
const landOf = (r: Region) => (r === 'de-autres' ? `${REGION_SHORT[r]} (${DE_AUTRES_LANDER.join(', ')})` : REGION_SHORT[r]);
const src = (s: { id: string; label: string; url: string; resolutionM?: number } | undefined): CountrySource | null =>
s
? {
label: s.label,
url: s.url,
licence: LICENCE[s.id],
...(ATTRIBUTION[s.id] && { attribution: ATTRIBUTION[s.id] }),
...(s.resolutionM !== undefined && { resolutionM: s.resolutionM }),
}
: null;
/** Sources de chaque pays du profil de l'app (une ligne par pays, ses régions réunies), puis « ailleurs ». */
export const COUNTRY_ROWS: CountryRow[] = [
...GROUPS.map(([c, rs]): CountryRow => {
const ps = rs.map((r) => REAL_PROFILE.regions[r] as RegionProfile);
// photo aérienne puis carte, union des régions sans doublon
const basemaps = (['ortho', 'plan'] as const).flatMap((k) => rs.flatMap((r, i) => (ps[i].basemaps.includes(k) && BASEMAP_OF[r]?.[k]) || []));
return {
code: c,
regions: rs,
country: COUNTRY_LABEL[c],
support: SUPPORT_LABEL[bestSupport(rs)],
elevation: rs.flatMap((r, i) => {
const s = src(ps[i].elevation);
return s ? [rs.length > 1 ? { land: landOf(r), ...s } : s] : [];
}),
extraWays: ps.map((p) => src(p.extraWays)).find((s) => s) ?? null,
basemaps: [...new Set(basemaps)],
...(COUNTRY_NOTE[c] && { note: COUNTRY_NOTE[c] }),
reasons: GAP_REASON[c] ?? {},
};
}),
{ code: null, regions: [], country: 'Ailleurs', support: null, elevation: [], extraWays: null, basemaps: [], reasons: GAP_REASON.ailleurs ?? {} },
];
/** Sources d'une case, chacune précédée de son Land s'il y en a plusieurs ; absente, la case dit pourquoi (ou le repli, puis pourquoi). */
function SourceCell({ s, fallback, note }: { s: CountrySource | CountrySource[] | null; fallback?: string; note?: string }) {
const list = s ? [s].flat() : [];
if (!list.length) return <td className="muted">{[fallback, note].filter(Boolean).join(' · ') || '—'}</td>;
return (
<td>
{list.map((x, k) => (
<span key={x.label}>
{k > 0 && '\u00a0; '}
{x.land && `${x.land}\u00a0: `}
<a href={x.url} target="_blank" rel="noopener">
{x.label}
</a>
{x.resolutionM !== undefined && `, ${x.resolutionM} m`}
{(x.attribution || x.licence) && <span className="muted"> ({[x.attribution, x.licence].filter(Boolean).join(', ')})</span>}
</span>
))}
{note && <span className="muted"> · {note}</span>}
</td>
);
}
function CountryTable() {
return (
<table className="data-countries">
<caption>Par pays</caption>
<thead>
<tr>
<th scope="col">Pays</th>
<th scope="col">Statut</th>
<th scope="col">Altitude</th>
<th scope="col">Chemins hors OpenStreetMap</th>
<th scope="col">Fonds nationaux</th>
</tr>
</thead>
<tbody>
{COUNTRY_ROWS.map((r) => (
<tr key={r.country}>
<th scope="row">{r.country}</th>
<td>{r.support ?? '—'}</td>
<SourceCell s={r.elevation} fallback="altitude du GPX ou relief mondial" note={[r.note, r.reasons.elevation].filter(Boolean).join(' · ')} />
<SourceCell s={r.extraWays} note={r.reasons.extraWays} />
<td className={r.basemaps.length ? undefined : 'muted'}>
{r.basemaps.length ? r.basemaps.join(', ') : 'fonds mondiaux'}
{r.reasons.basemaps && <span className="muted"> · {r.reasons.basemaps}</span>}
</td>
</tr>
))}
</tbody>
</table>
);
}
export default function DataSources() {
return (
<section className="data-sources">
<CountryTable />
<dl>
{DATA_SOURCES.map((d) => (
<div key={d.feature}>
<dt>{d.feature}</dt>
<dd>
{d.sources.map(([label, url], k) => (
<span key={label}>
{k > 0 && ', '}
<a href={url} target="_blank" rel="noopener">
{label}
</a>
</span>
))}
{d.note && <span className="muted">{d.sources.length ? ` · ${d.note}` : d.note}</span>}
</dd>
</div>
))}
</dl>
<p className="muted small">
Tout est calculé dans ton navigateur : ton fichier GPX n'est envoyé ni stocké nulle part. Seules les coordonnées de la trace sont transmises aux services
ci-dessus pour obtenir l'altitude, les chemins et les photos. Une mesure d'audience anonyme et sans cookie (Umami) compte les visites et les
actions (trace chargée, niveau choisi…), jamais le contenu ni l'emplacement de la trace.
</p>
</section>
);
}
Utilisation
import DataSources from './components/DataSources';
<DataSources />Guidelines
- MUSTCiter chaque source avec sa licence et son attribution exacte.
- MUSTTirer la liste des sources du profil de données (lib/sources), jamais la recopier.
Référence de l’API
Aucune prop.