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 = '