| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945 |
- const mapContainer = d3.select("#map");
- const modal = document.getElementById("country-modal");
- const countryName = document.getElementById("country-name");
- const countryCategory = document.getElementById("country-category");
- const countryData = document.getElementById("country-data");
- const closeModalButton = document.getElementById("close-modal");
- const legendContainer = document.getElementById("category-legend");
- const countrySearchInputDesktop = document.getElementById("country-search");
- const countryListContainerDesktop = document.getElementById("country-list");
- const countryListCountDesktop = document.getElementById("country-list-count");
- const countrySearchInputMobile = document.getElementById("country-search-mobile");
- const countryListContainerMobile = document.getElementById("country-list-mobile");
- const countryListCountMobile = document.getElementById("country-list-count-mobile");
- const openMobileCountryListButton = document.getElementById("open-mobile-country-list");
- const closeCountryListModalButton = document.getElementById("close-country-list-modal");
- const countryListModal = document.getElementById("country-list-modal");
- const legendToggleButton = document.getElementById("toggle-mobile-legend");
- const mapTooltip = document.getElementById("map-tooltip");
- const worldGeoJsonUrl =
- "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_map_units.geojson";
- const rawPolicyDataUrl = "./znep_gta_pays_etrangers.txt";
- const legacyColorByCode = {
- VERT: "#22c55e",
- BLEU: "#3b82f6",
- ORANGE: "#f59e0b",
- ROUGE: "#ef4444",
- };
- const colorByAuthorizationCode = {
- "10": "#22c55e",
- "21": "#facc15",
- "22": "#f59e0b",
- "23": "#ef4444",
- "30": "#0a0a0a",
- "99": "#ef4444",
- };
- const explicitCategoryTitleByCode = {
- "10": "Libre circulation (sans formalité)",
- "21": "Autorisation ministérielle renforcée",
- "22": "Autorisation préalable + avis PSD",
- "23": "Autorisation CFA / autorité assimilée",
- "30": "Interdiction de séjour",
- "99": "Autorisation selon zone de destination",
- };
- const procedureSummaryByAuthorizationCode = {
- "10": "Déplacement possible sans autorisation préalable spécifique.",
- "21": "Autorisation ministérielle obligatoire avant départ.",
- "22": "Autorisation préalable obligatoire avec avis PSD.",
- "23": "Autorisation CFA (ou autorité assimilée) obligatoire avant départ.",
- "30": "Destination interdite : déplacement non autorisé.",
- "99": "Autorisation variable selon la zone/pays de destination.",
- };
- const noteServiceUpdateDate = "20260316";
- const noteServiceProcedureReference = `ANNEXE II à la circulaire n" l 1 400/ARM/SGA/DRH-MD/SR-RH/SDFM/FM5
- du 30 juillet 2024.
- Message n"5}l397lARM/CMlAIP du 13 mars2026`;
- const defaultCountryColor = "#6b7280";
- const forcedPolicyIsoByMatchedParentIso = {
- GB: ["GI"],
- NO: ["BV"],
- US: ["UM"],
- };
- const mapIsoToPolicyIsoAlias = {
- "CN-TW": "TW",
- TL: "TP",
- CW: "AN",
- SX: "AN",
- BQ: "AN",
- };
- const mapNameToPolicyIsoAlias = {
- antigua: "AG",
- barbuda: "AG",
- "federation of bosnia and herzegovina": "BA",
- "republic srpska": "BA",
- "flemish region": "BE",
- "walloon region": "BE",
- brussels: "BE",
- gaza: "PS",
- "west bank": "PS",
- "united states minor outlying islands": "UM",
- "autonomous region of bougainville": "PG",
- bougainville: "PG",
- azores: "PT",
- madeira: "PT",
- wales: "GB",
- scotland: "GB",
- england: "GB",
- "northern ireland": "GB",
- "nouvelle caledonie": "FR",
- "new caledonia": "FR",
- "polynesie francaise": "FR",
- "french polynesia": "FR",
- "saint barthelemy": "FR",
- "saint martin": "FR",
- "saint pierre et miquelon": "FR",
- "saint pierre and miquelon": "FR",
- "terres australes et antarctiques francaises": "FR",
- "french southern and antarctic lands": "FR",
- "wallis et futuna": "FR",
- "wallis and futuna": "FR",
- "chypre du nord": "CY",
- "northern cyprus": "CY",
- somaliland: "SO",
- "glacier de siachen": "IN",
- "siachen glacier": "IN",
- };
- let width = 0;
- let height = 0;
- let countries = [];
- let countryShadowPaths = null;
- let countryPaths = null;
- let countryPolicyByIso2 = new Map();
- let countryPolicyByName = new Map();
- let categoryByCode = new Map();
- let lastAnalysis = null;
- let countryEntries = [];
- let countryByKey = new Map();
- const svg = mapContainer.append("svg").attr("aria-hidden", "true");
- const zoomLayer = svg.append("g");
- const countryShadowLayer = zoomLayer
- .append("g")
- .attr("class", "country-shadow-layer")
- .attr("transform", "translate(1.8,1.8)");
- const countryLayer = zoomLayer.append("g");
- const projection = d3.geoRobinson ? d3.geoRobinson() : d3.geoEqualEarth();
- const path = d3.geoPath(projection);
- let currentTransform = d3.zoomIdentity;
- const zoom = d3
- .zoom()
- .scaleExtent([1, 40])
- .on("start", () => {
- hideCountryTooltip();
- })
- .on("zoom", (event) => {
- currentTransform = event.transform;
- zoomLayer.attr("transform", currentTransform);
- });
- function normalizeCountryName(name) {
- return String(name || "")
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, " ")
- .trim();
- }
- function getCountryLabel(feature) {
- return (
- feature.properties?.NAME_FR ||
- feature.properties?.NAME_EN ||
- feature.properties?.NAME ||
- "Pays inconnu"
- );
- }
- function getCountryIso2(feature) {
- const rawIso2 = getCountryIso2Raw(feature);
- if (!rawIso2) {
- return "";
- }
- return mapIsoToPolicyIsoAlias[rawIso2] || rawIso2;
- }
- function getCountryIso2Raw(feature) {
- const direct = String(feature.properties?.ISO_A2 || "").toUpperCase().trim();
- if (direct && direct !== "-99") {
- return direct;
- }
- const fallback = String(feature.properties?.ISO_A2_EH || "")
- .toUpperCase()
- .trim();
- if (fallback && fallback !== "-99") {
- return fallback;
- }
- return "";
- }
- function getFlagEmojiFromIso2(iso2) {
- const code = String(iso2 || "").toUpperCase();
- if (!/^[A-Z]{2}$/.test(code)) {
- return "🏳️";
- }
- const first = code.codePointAt(0) + 127397;
- const second = code.codePointAt(1) + 127397;
- return String.fromCodePoint(first, second);
- }
- function htmlToText(htmlString) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(String(htmlString || ""), "text/html");
- return (doc.body.textContent || "").replace(/\s+/g, " ").trim();
- }
- function formatDate(yyyymmdd) {
- const value = String(yyyymmdd || "");
- if (!/^\d{8}$/.test(value)) {
- return value;
- }
- if (value === "99991231") {
- return "Sans fin";
- }
- const year = value.slice(0, 4);
- const month = value.slice(4, 6);
- const day = value.slice(6, 8);
- return `${day}/${month}/${year}`;
- }
- function escapeHtml(value) {
- return String(value)
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll("\"", """)
- .replaceAll("'", "'");
- }
- function getCategoryDisplayLabel(code, fallbackLabel) {
- return (
- explicitCategoryTitleByCode[String(code || "")] ||
- fallbackLabel ||
- `Catégorie ${code}`
- );
- }
- function getCategoryColor(code, fallbackColorCode) {
- return (
- colorByAuthorizationCode[String(code || "")] ||
- legacyColorByCode[String(fallbackColorCode || "")] ||
- defaultCountryColor
- );
- }
- function summarizeProcedureText(rawText, categoryCode) {
- const code = String(categoryCode || "");
- const fallbackSummary = procedureSummaryByAuthorizationCode[code] || "";
- const cleaned = String(rawText || "").replace(/\s+/g, " ").trim();
- if (!cleaned) {
- return fallbackSummary || "Non renseignée";
- }
- const fragments = cleaned
- .split(/[.;]\s+|\n+/)
- .map((value) => value.replace(/^[\s\-•\d).]+/, "").trim())
- .filter(Boolean);
- const keywords =
- /(autorisation|autorise|interdit|interdiction|avis|accord|demande|validation|cfa|psd|minist[eè]re|depart|départ|avant)/i;
- const selected = [];
- for (let i = 0; i < fragments.length; i += 1) {
- const fragment = fragments[i];
- if (fragment.length < 18) {
- continue;
- }
- if (!keywords.test(fragment) && selected.length > 0) {
- continue;
- }
- selected.push(fragment);
- if (selected.length >= 2) {
- break;
- }
- }
- if (selected.length === 0 && fragments.length > 0) {
- selected.push(fragments[0]);
- }
- let literalSummary = selected.join(" / ");
- if (literalSummary.length > 210) {
- literalSummary = `${literalSummary.slice(0, 207)}...`;
- }
- if (!fallbackSummary) {
- return literalSummary || "Non renseignée";
- }
- return literalSummary ? `${fallbackSummary} ${literalSummary}` : fallbackSummary;
- }
- function isConcernedByMarch2026Note(policy) {
- return (
- String(policy?.beginDate || "") === noteServiceUpdateDate &&
- String(policy?.updateDate || "") === noteServiceUpdateDate
- );
- }
- function parseFlatTable(flatTable) {
- if (!Array.isArray(flatTable) || flatTable.length < 2) {
- return [];
- }
- const columnCount = Number(flatTable[0]);
- if (!Number.isInteger(columnCount) || columnCount <= 0) {
- return [];
- }
- const headers = flatTable.slice(1, columnCount + 1);
- const rows = [];
- for (let index = columnCount + 1; index < flatTable.length; index += columnCount) {
- const row = {};
- for (let col = 0; col < columnCount; col += 1) {
- row[headers[col]] = flatTable[index + col] ?? "";
- }
- rows.push(row);
- }
- return rows;
- }
- function isBetterCountryRow(candidate, current) {
- const candidateActive = candidate.ENDDA === "99991231" ? 1 : 0;
- const currentActive = current.ENDDA === "99991231" ? 1 : 0;
- if (candidateActive !== currentActive) {
- return candidateActive > currentActive;
- }
- const candidateBegda = String(candidate.BEGDA || "");
- const currentBegda = String(current.BEGDA || "");
- if (candidateBegda !== currentBegda) {
- return candidateBegda > currentBegda;
- }
- const candidateAedtm = String(candidate.AEDTM || "");
- const currentAedtm = String(current.AEDTM || "");
- return candidateAedtm > currentAedtm;
- }
- function parsePolicyModel(rawText) {
- const parsedJson = JSON.parse(rawText);
- const rawCountryRows = parseFlatTable(parsedJson.modelmasterListData);
- const rawCategoryRows = parseFlatTable(parsedJson.modelmasterListCatData);
- const countryRowsByIso = new Map();
- rawCountryRows.forEach((row) => {
- const iso2 = String(row.LAND1 || "").toUpperCase().trim();
- if (!iso2) {
- return;
- }
- const current = countryRowsByIso.get(iso2);
- if (!current || isBetterCountryRow(row, current)) {
- countryRowsByIso.set(iso2, row);
- }
- });
- const countriesData = Array.from(countryRowsByIso.values()).map((row) => ({
- iso2: String(row.LAND1 || "").toUpperCase(),
- countryNameFr: String(row.LANDX50 || ""),
- categoryCode: String(row.CATEGORIE || ""),
- categoryLabel: String(row.LBL_CATEGORIE || ""),
- colorCode: String(row.COLOR || ""),
- beginDate: String(row.BEGDA || ""),
- endDate: String(row.ENDDA || ""),
- beginDateText: String(row.BEGDA_TXT || ""),
- endDateText: String(row.ENDDA_TXT || ""),
- updateDate: String(row.AEDTM || ""),
- iconPath: String(row.DESCRIPTIF || ""),
- }));
- const categoriesData = rawCategoryRows.map((row) => ({
- code: String(row.CATEGORIE || ""),
- label: String(row.LBL_CATEGORIE || ""),
- colorCode: String(row.COLOR || ""),
- beginDate: String(row.BEGDA || ""),
- endDate: String(row.ENDDA || ""),
- descriptionHtml: String(row.DESCRIPTIF_WDA || row.DESCRIPTIF || ""),
- descriptionText: htmlToText(row.DESCRIPTIF_WDA || row.DESCRIPTIF || ""),
- }));
- return {
- countriesData,
- categoriesData,
- };
- }
- function getCountryPolicyForFeature(feature) {
- const iso2 = getCountryIso2(feature);
- if (iso2 && countryPolicyByIso2.has(iso2)) {
- return countryPolicyByIso2.get(iso2);
- }
- const names = [
- feature.properties?.NAME_FR,
- feature.properties?.NAME_EN,
- feature.properties?.NAME,
- feature.properties?.ADMIN,
- feature.properties?.NAME_LONG,
- ];
- for (let i = 0; i < names.length; i += 1) {
- const normalized = normalizeCountryName(names[i]);
- if (normalized && countryPolicyByName.has(normalized)) {
- return countryPolicyByName.get(normalized);
- }
- const aliasIso = mapNameToPolicyIsoAlias[normalized];
- if (aliasIso && countryPolicyByIso2.has(aliasIso)) {
- return countryPolicyByIso2.get(aliasIso);
- }
- }
- return null;
- }
- function getCountryColor(feature) {
- const policy = getCountryPolicyForFeature(feature);
- if (!policy) {
- return defaultCountryColor;
- }
- return getCategoryColor(policy.categoryCode, policy.colorCode);
- }
- function moveCountryTooltip(event) {
- if (!mapTooltip.classList.contains("visible")) {
- return;
- }
- const [pointerX, pointerY] = d3.pointer(event, document.body);
- const rawX = Number.isFinite(event?.clientX) ? event.clientX : pointerX;
- const rawY = Number.isFinite(event?.clientY) ? event.clientY : pointerY;
- const offset = 14;
- const tooltipWidth = mapTooltip.offsetWidth || 160;
- const tooltipHeight = mapTooltip.offsetHeight || 36;
- const maxX = window.innerWidth - tooltipWidth - 8;
- const maxY = window.innerHeight - tooltipHeight - 8;
- const x = Math.max(8, Math.min(maxX, rawX + offset));
- const y = Math.max(8, Math.min(maxY, rawY + offset));
- mapTooltip.style.left = `${x}px`;
- mapTooltip.style.top = `${y}px`;
- }
- function showCountryTooltip(event, feature) {
- mapTooltip.textContent = getCountryLabel(feature);
- mapTooltip.classList.add("visible");
- mapTooltip.setAttribute("aria-hidden", "false");
- moveCountryTooltip(event);
- }
- function hideCountryTooltip() {
- mapTooltip.classList.remove("visible");
- mapTooltip.setAttribute("aria-hidden", "true");
- }
- function buildCountryEntries() {
- countryByKey = new Map(countries.map((feature) => [featureKey(feature), feature]));
- countryEntries = countries
- .map((feature) => {
- const policy = getCountryPolicyForFeature(feature);
- const mapIso2 = getCountryIso2Raw(feature);
- const iso2 = mapIso2 || policy?.iso2 || getCountryIso2(feature);
- return {
- key: featureKey(feature),
- name: getCountryLabel(feature),
- iso2,
- flag: getFlagEmojiFromIso2(iso2),
- };
- })
- .sort((a, b) => {
- return a.name.localeCompare(b.name, "fr", { sensitivity: "base" });
- });
- }
- function renderCountryList(filterValue = "") {
- const normalizedFilter = normalizeCountryName(filterValue);
- const filtered = countryEntries.filter((entry) => {
- const byName = normalizeCountryName(entry.name).includes(normalizedFilter);
- const byIso = String(entry.iso2 || "")
- .toLowerCase()
- .includes(normalizedFilter);
- return byName || byIso;
- });
- const countText = `${filtered.length} / ${countryEntries.length} pays`;
- countryListCountDesktop.textContent = countText;
- countryListCountMobile.textContent = countText;
- let listHtml = "";
- if (filtered.length === 0) {
- listHtml = '<li class="country-list-empty">Aucun pays trouvé.</li>';
- } else {
- listHtml = filtered
- .map((entry) => {
- return `<li><button type="button" class="country-list-item" data-country-key="${escapeHtml(
- entry.key
- )}"><span class="country-list-flag">${entry.flag}</span><span>${escapeHtml(
- entry.name
- )}</span></button></li>`;
- })
- .join("");
- }
- countryListContainerDesktop.innerHTML = listHtml;
- countryListContainerMobile.innerHTML = listHtml;
- }
- function focusCountryOnMap(feature) {
- const bounds = path.bounds(feature);
- const x0 = bounds?.[0]?.[0];
- const y0 = bounds?.[0]?.[1];
- const x1 = bounds?.[1]?.[0];
- const y1 = bounds?.[1]?.[1];
- if (![x0, y0, x1, y1].every(Number.isFinite)) {
- return;
- }
- const featureWidth = Math.max(1, x1 - x0);
- const featureHeight = Math.max(1, y1 - y0);
- const centerX = (x0 + x1) / 2;
- const centerY = (y0 + y1) / 2;
- // Zoom cible plus fort au clic, ajusté à la taille du pays.
- const autoScale = 0.62 / Math.max(featureWidth / width, featureHeight / height);
- const targetScale = Math.min(20, Math.max(currentTransform.k, 4.5, autoScale));
- const translateX = width / 2 - targetScale * centerX;
- const translateY = height / 2 - targetScale * centerY;
- const targetTransform = d3.zoomIdentity
- .translate(translateX, translateY)
- .scale(targetScale);
- svg.transition().duration(420).call(zoom.transform, targetTransform);
- }
- function openCountryListModal() {
- countryListModal.classList.add("open");
- countryListModal.setAttribute("aria-hidden", "false");
- countrySearchInputMobile.focus();
- }
- function closeCountryListModal() {
- countryListModal.classList.remove("open");
- countryListModal.setAttribute("aria-hidden", "true");
- }
- function closeLegendOnMobile() {
- legendContainer.classList.remove("mobile-open");
- legendToggleButton.setAttribute("aria-expanded", "false");
- }
- function handleCountryListClick(event) {
- const button = event.target.closest("button[data-country-key]");
- if (!button) {
- return;
- }
- const feature = countryByKey.get(button.dataset.countryKey);
- if (!feature) {
- return;
- }
- focusCountryOnMap(feature);
- openModalForFeature(feature);
- closeCountryListModal();
- }
- function initCountryListEvents() {
- const syncAndRender = (value, source) => {
- const nextValue = value || "";
- if (source !== "desktop" && countrySearchInputDesktop.value !== nextValue) {
- countrySearchInputDesktop.value = nextValue;
- }
- if (source !== "mobile" && countrySearchInputMobile.value !== nextValue) {
- countrySearchInputMobile.value = nextValue;
- }
- renderCountryList(nextValue);
- };
- countrySearchInputDesktop.addEventListener("input", (event) => {
- syncAndRender(event.target.value, "desktop");
- });
- countrySearchInputMobile.addEventListener("input", (event) => {
- syncAndRender(event.target.value, "mobile");
- });
- countryListContainerDesktop.addEventListener("click", handleCountryListClick);
- countryListContainerMobile.addEventListener("click", handleCountryListClick);
- openMobileCountryListButton.addEventListener("click", openCountryListModal);
- closeCountryListModalButton.addEventListener("click", closeCountryListModal);
- countryListModal.addEventListener("click", (event) => {
- if (event.target === countryListModal) {
- closeCountryListModal();
- }
- });
- legendToggleButton.addEventListener("click", () => {
- const isOpen = legendContainer.classList.toggle("mobile-open");
- legendToggleButton.setAttribute("aria-expanded", String(isOpen));
- });
- }
- function updateLegend() {
- if (!lastAnalysis) {
- legendContainer.innerHTML = "";
- return;
- }
- const categories = [...lastAnalysis.categoryCounts.entries()].sort((a, b) => {
- if (a[0] === "30" && b[0] !== "30") {
- return 1;
- }
- if (b[0] === "30" && a[0] !== "30") {
- return -1;
- }
- return Number(a[0]) - Number(b[0]);
- });
- const items = categories
- .map(([code, count]) => {
- const category = categoryByCode.get(code);
- const label = getCategoryDisplayLabel(code, category?.label);
- const hex = getCategoryColor(code, category?.colorCode);
- return `<li><span class="legend-swatch" style="background:${hex}"></span><span>Code ${code} - ${label} (${count})</span></li>`;
- })
- .join("");
- legendContainer.innerHTML = `<h3>Légende catégories</h3><ul>${items}</ul>`;
- }
- function setCountryDataHtml(lines) {
- const rows = lines
- .map(
- ([label, value]) =>
- `<div class="country-data-row"><strong>${escapeHtml(
- label
- )}</strong><span>${escapeHtml(value)}</span></div>`
- )
- .join("");
- countryData.innerHTML = rows;
- }
- function openModalForFeature(feature) {
- const mapLabel = getCountryLabel(feature);
- const policy = getCountryPolicyForFeature(feature);
- const mapIso2 = getCountryIso2Raw(feature);
- const iso2 = mapIso2 || policy?.iso2 || getCountryIso2(feature);
- const flag = getFlagEmojiFromIso2(iso2);
- countryName.textContent = `${flag} ${mapLabel}`;
- if (!policy) {
- countryCategory.textContent = "Aucune donnée de catégorie pour ce pays.";
- setCountryDataHtml([
- ["Source", "Carte Natural Earth"],
- ["Code ISO2 carte", getCountryIso2(feature) || "N/A"],
- ]);
- } else {
- const category = categoryByCode.get(policy.categoryCode);
- const categoryLabel = getCategoryDisplayLabel(
- policy.categoryCode,
- category?.label || policy.categoryLabel
- );
- countryCategory.textContent = `${categoryLabel} (code ${policy.categoryCode})`;
- const procedureText = summarizeProcedureText(
- category?.descriptionText || "",
- policy.categoryCode
- );
- const fullProcedureText = isConcernedByMarch2026Note(policy)
- ? `${procedureText}\n\n${noteServiceProcedureReference}`
- : procedureText;
- setCountryDataHtml([
- ["Pays (donnée source)", policy.countryNameFr || mapLabel],
- ["Code ISO2", policy.iso2],
- ["Début validité", formatDate(policy.beginDate)],
- ["Fin validité", formatDate(policy.endDate)],
- ["Dernière maj", formatDate(policy.updateDate)],
- ["Procédure", fullProcedureText || "Non renseignée"],
- ]);
- }
- modal.classList.add("open");
- modal.setAttribute("aria-hidden", "false");
- }
- function closeModal() {
- modal.classList.remove("open");
- modal.setAttribute("aria-hidden", "true");
- }
- function handleCountryClick(event, feature) {
- if (event.defaultPrevented) {
- return;
- }
- if (event.currentTarget && typeof event.currentTarget.blur === "function") {
- event.currentTarget.blur();
- }
- focusCountryOnMap(feature);
- openModalForFeature(feature);
- }
- function featureKey(feature) {
- const iso3 = feature.properties?.ADM0_A3 || feature.properties?.ISO_A3 || "-";
- const name = feature.properties?.NAME_EN || feature.properties?.NAME || "unknown";
- return `${iso3}:${name}`;
- }
- function updateProjectionAndDrawing() {
- if (!countryPaths || !countryShadowPaths || countries.length === 0) {
- return;
- }
- const rect = mapContainer.node().getBoundingClientRect();
- width = Math.max(320, rect.width);
- height = Math.max(240, rect.height);
- svg.attr("viewBox", `0 0 ${width} ${height}`);
- projection.fitExtent(
- [
- [10, 10],
- [width - 10, height - 10],
- ],
- { type: "FeatureCollection", features: countries }
- );
- countryShadowPaths.attr("d", path);
- countryPaths.attr("d", path);
- zoom
- .extent([
- [0, 0],
- [width, height],
- ])
- .translateExtent([
- [-width, -height],
- [width * 2, height * 2],
- ]);
- svg.call(zoom).call(zoom.transform, currentTransform);
- }
- function analyzeMatching() {
- const categoryCounts = new Map();
- const matchedIso2 = new Set();
- let matchedMapCountries = 0;
- countryPolicyByIso2.forEach((policy) => {
- const categoryCode = policy.categoryCode || "N/A";
- categoryCounts.set(categoryCode, (categoryCounts.get(categoryCode) || 0) + 1);
- });
- countries.forEach((feature) => {
- const policy = getCountryPolicyForFeature(feature);
- if (!policy) {
- return;
- }
- matchedMapCountries += 1;
- matchedIso2.add(policy.iso2);
- });
- Object.entries(forcedPolicyIsoByMatchedParentIso).forEach(
- ([parentIso, forcedIsoList]) => {
- if (!matchedIso2.has(parentIso)) {
- return;
- }
- forcedIsoList.forEach((forcedIso) => {
- matchedIso2.add(forcedIso);
- });
- }
- );
- const totalPolicyCountries = countryPolicyByIso2.size;
- const unmatchedMapCountries = countries.filter((feature) => !getCountryPolicyForFeature(feature)).length;
- const unmatchedIso2 = [...countryPolicyByIso2.keys()].filter((iso2) => !matchedIso2.has(iso2));
- lastAnalysis = {
- totalPolicyCountries,
- matchedMapCountries,
- unmatchedMapCountries,
- categoryCounts,
- unmatchedIso2,
- };
- }
- Promise.all([d3.json(worldGeoJsonUrl), d3.text(rawPolicyDataUrl)])
- .then(([geoJson, rawPolicyText]) => {
- const parsedPolicy = parsePolicyModel(rawPolicyText);
- countryPolicyByIso2 = new Map(parsedPolicy.countriesData.map((item) => [item.iso2, item]));
- countryPolicyByName = new Map(
- parsedPolicy.countriesData
- .map((item) => [normalizeCountryName(item.countryNameFr), item])
- .filter(([name]) => Boolean(name))
- );
- categoryByCode = new Map(parsedPolicy.categoriesData.map((item) => [item.code, item]));
- countries = geoJson.features.filter(
- (feature) => feature.properties?.NAME_EN !== "Antarctica"
- );
- countryShadowPaths = countryShadowLayer
- .selectAll("path")
- .data(countries, featureKey)
- .join("path")
- .attr("class", "country-shadow");
- countryPaths = countryLayer
- .selectAll("path")
- .data(countries, featureKey)
- .join("path")
- .attr("class", "country")
- .attr("fill", getCountryColor)
- .attr("tabindex", 0)
- .on("pointerenter", (event, feature) => {
- showCountryTooltip(event, feature);
- })
- .on("pointermove", (event) => {
- moveCountryTooltip(event);
- })
- .on("pointerleave", () => {
- hideCountryTooltip();
- })
- .on("mouseenter", (event, feature) => {
- showCountryTooltip(event, feature);
- })
- .on("mouseover", (event, feature) => {
- showCountryTooltip(event, feature);
- })
- .on("mouseenter", (event, feature) => {
- showCountryTooltip(event, feature);
- })
- .on("mousemove", (event) => {
- moveCountryTooltip(event);
- })
- .on("mouseleave", () => {
- hideCountryTooltip();
- })
- .on("focus", (event, feature) => {
- showCountryTooltip(event, feature);
- })
- .on("click", handleCountryClick)
- .on("keydown", (event, feature) => {
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault();
- openModalForFeature(feature);
- }
- })
- .on("blur", () => {
- hideCountryTooltip();
- });
- countryPaths
- .selectAll("title")
- .data((feature) => [feature])
- .join("title")
- .text((feature) => getCountryLabel(feature));
- buildCountryEntries();
- renderCountryList();
- analyzeMatching();
- updateLegend();
- updateProjectionAndDrawing();
- })
- .catch((error) => {
- mapContainer
- .append("p")
- .style("color", "#b91c1c")
- .style("margin", "1rem")
- .text("Erreur lors du chargement de la carte ou des données.");
- // eslint-disable-next-line no-console
- console.error(error);
- });
- window.addEventListener("resize", () => {
- updateProjectionAndDrawing();
- hideCountryTooltip();
- if (window.innerWidth > 760) {
- closeCountryListModal();
- closeLegendOnMobile();
- }
- });
- initCountryListEvents();
- closeModalButton.addEventListener("click", closeModal);
- modal.addEventListener("click", (event) => {
- if (event.target === modal) {
- closeModal();
- }
- });
- document.addEventListener("keydown", (event) => {
- if (event.key !== "Escape") {
- return;
- }
- if (countryListModal.classList.contains("open")) {
- closeCountryListModal();
- return;
- }
- if (legendContainer.classList.contains("mobile-open")) {
- closeLegendOnMobile();
- return;
- }
- if (modal.classList.contains("open")) {
- closeModal();
- }
- });
|