| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239 |
- const mapCanvas = document.getElementById("map-canvas");
- const mapShell = document.getElementById("map-shell");
- const mapTooltip = document.getElementById("map-tooltip");
- const mapStatus = document.getElementById("map-status");
- const entitySidebar = document.getElementById("entity-sidebar");
- const entityListTitle = document.getElementById("entity-list-title");
- const communeName = document.getElementById("commune-name");
- const communeTerritory = document.getElementById("commune-territory");
- const communeData = document.getElementById("commune-data");
- const communeModal = document.getElementById("commune-modal");
- const closeModalButton = document.getElementById("close-modal");
- const modalOpenTerritoryButton = document.getElementById("modal-open-territory");
- const modalTitle = document.getElementById("modal-title");
- const communeSearchInputDesktop = document.getElementById("commune-search");
- const communeSearchInputMobile = document.getElementById("commune-search-mobile");
- const communeListContainerDesktop = document.getElementById("commune-list");
- const communeListContainerMobile = document.getElementById("commune-list-mobile");
- const communeListCountDesktop = document.getElementById("commune-list-count");
- const communeListCountMobile = document.getElementById("commune-list-count-mobile");
- const openMobileCommuneListButton = document.getElementById("open-mobile-commune-list");
- const closeCommuneListModalButton = document.getElementById("close-commune-list-modal");
- const communeListModal = document.getElementById("commune-list-modal");
- const communeListModalTitle = document.getElementById("commune-list-modal-title");
- const territoryPanel = document.getElementById("territory-panel");
- const territorySummary = document.getElementById("territory-summary");
- const territoryList = document.getElementById("territory-list");
- const territoryToggleButton = document.getElementById("toggle-mobile-territories");
- const maskingDisclaimer = document.getElementById("masking-disclaimer");
- const maskingDisclaimerText = document.getElementById("masking-disclaimer-text");
- const openMaskingModalButton = document.getElementById("open-masking-modal");
- const maskingModal = document.getElementById("masking-modal");
- const closeMaskingModalButton = document.getElementById("close-masking-modal");
- const maskingModalSummary = document.getElementById("masking-modal-summary");
- const maskingModalData = document.getElementById("masking-modal-data");
- const COMMUNE_INDEX_URL = "./data/communes-index.json";
- const TERRITORIES_URL = "./data/territories.json";
- const MAX_SEARCH_RESULTS = 160;
- const BASE_PADDING = 28;
- const FILL_SEAL_WIDTH = 1.2;
- const BORDER_LINE_WIDTH = 0.85;
- const VECTOR_RENDER_SCALE_THRESHOLD = 2.2;
- const UNIT_MARKER_SIZE = 10.5;
- const UNIT_MARKER_CORNER_RADIUS = 2.8;
- const UNIT_MARKER_HOVER_SIZE = 12;
- const UNIT_MARKER_HIT_RADIUS = 10;
- const UNIT_FOCUS_SCALE = 14;
- const TERRITORY_DISPLAY_SCALE = {};
- const REGION_ENTITY_TYPE = "region";
- const DEPARTMENT_ENTITY_TYPE = "departement";
- const COMMUNE_ENTITY_TYPE = "commune";
- const UNIT_SEARCH_KIND = "unit";
- const ROUTE_TERRITORY_SEGMENT = "territoire";
- const ROUTE_ENTITY_SEGMENT = "entite";
- const ROUTE_UNIT_SEGMENT = "unite";
- const TERRITORY_BADGE_ASSETS = {
- "region-ile-de-france": {
- src: "./assets/territory-badges/region-ile-de-france.svg",
- },
- "region-centre-val-de-loire": {
- src: "./assets/territory-badges/region-centre-val-de-loire.svg",
- },
- "region-bourgogne-franche-comte": {
- src: "./assets/territory-badges/region-bourgogne-franche-comte.png",
- },
- "region-normandie": {
- src: "./assets/territory-badges/region-normandie.svg",
- },
- "region-hauts-de-france": {
- src: "./assets/territory-badges/region-hauts-de-france.svg",
- },
- "region-grand-est": {
- src: "./assets/territory-badges/region-grand-est.svg",
- },
- "region-pays-de-la-loire": {
- src: "./assets/territory-badges/region-pays-de-la-loire.png",
- },
- "region-bretagne": {
- src: "./assets/territory-badges/region-bretagne.svg",
- },
- "region-nouvelle-aquitaine": {
- src: "./assets/territory-badges/region-nouvelle-aquitaine.svg",
- },
- "region-occitanie": {
- src: "./assets/territory-badges/region-occitanie.svg",
- },
- "region-auvergne-rhone-alpes": {
- src: "./assets/territory-badges/region-auvergne-rhone-alpes.svg",
- },
- "region-provence-alpes-cote-d-azur": {
- src: "./assets/territory-badges/region-provence-alpes-cote-d-azur.svg",
- },
- "region-corse": {
- src: "./assets/territory-badges/region-corse.svg",
- },
- guadeloupe: {
- src: "./assets/territory-badges/guadeloupe.png",
- },
- martinique: {
- src: "./assets/territory-badges/martinique.png",
- },
- guyane: {
- src: "./assets/territory-badges/guyane.png",
- },
- "la-reunion": {
- src: "./assets/territory-badges/la-reunion.png",
- },
- mayotte: {
- src: "./assets/territory-badges/mayotte.png",
- },
- "saint-pierre-et-miquelon": {
- src: "./assets/territory-badges/saint-pierre-et-miquelon.png",
- },
- "saint-barthelemy": {
- src: "./assets/territory-badges/saint-barthelemy.svg",
- className: "territory-badge-flag",
- },
- "saint-martin": {
- src: "./assets/territory-badges/saint-martin.svg",
- className: "territory-badge-flag",
- },
- "polynesie-francaise": {
- src: "./assets/territory-badges/polynesie-francaise.png",
- },
- "nouvelle-caledonie": {
- src: "./assets/territory-badges/nouvelle-caledonie.png",
- },
- "wallis-et-futuna": {
- src: "./assets/territory-badges/wallis-et-futuna.png",
- },
- };
- const mapContext = mapCanvas.getContext("2d", { alpha: false });
- const hitCanvas = document.createElement("canvas");
- const hitContext = hitCanvas.getContext("2d", { alpha: false, willReadFrequently: true });
- const baseCanvas = document.createElement("canvas");
- const baseContext = baseCanvas.getContext("2d", { alpha: false });
- const themeStyles = getComputedStyle(document.documentElement);
- const themeColors = {
- background: themeStyles.getPropertyValue("--bg").trim(),
- communeFill: themeStyles.getPropertyValue("--commune-fill").trim(),
- communeHover: themeStyles.getPropertyValue("--commune-hover").trim(),
- communeSelected: themeStyles.getPropertyValue("--commune-selected").trim(),
- communeStroke: themeStyles.getPropertyValue("--commune-stroke").trim(),
- unitMarkerFill: themeStyles.getPropertyValue("--unit-marker-fill").trim() || "#394149",
- unitMarkerStroke: themeStyles.getPropertyValue("--unit-marker-stroke").trim() || "#07090b",
- unitMarkerHover: themeStyles.getPropertyValue("--unit-marker-hover").trim() || "#5a6672",
- };
- const state = {
- pixelRatio: window.devicePixelRatio || 1,
- width: 0,
- height: 0,
- communesIndex: [],
- communePostalCodesByCode: new Map(),
- searchEntries: [],
- territories: [],
- territoryByKey: new Map(),
- territoryDataByKey: new Map(),
- activeTerritoryKey: null,
- activeTerritoryData: null,
- hoveredPartKey: null,
- hoveredUnitKey: null,
- selectedCommuneId: null,
- selectedPartKey: null,
- selectedUnitKey: null,
- transform: d3.zoomIdentity,
- renderQueued: false,
- baseDirty: true,
- hitDirty: true,
- resizeTimer: null,
- routeHash: "",
- routeApplying: false,
- };
- const zoom = d3
- .zoom()
- .scaleExtent([1, 80])
- .on("start", () => hideTooltip())
- .on("zoom", (event) => {
- state.transform = event.transform;
- requestRender();
- });
- d3.select(mapCanvas).call(zoom).on("dblclick.zoom", null);
- function normalizeText(value) {
- return String(value || "")
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, " ")
- .trim();
- }
- function compareEntriesAlphabetically(left, right) {
- if (left.searchName !== right.searchName) {
- return left.searchName.localeCompare(right.searchName, "fr", { sensitivity: "base" });
- }
- if (left.name !== right.name) {
- return left.name.localeCompare(right.name, "fr", { sensitivity: "base" });
- }
- if (left.code !== right.code) {
- return left.code.localeCompare(right.code, "fr", { sensitivity: "base" });
- }
- return left.rawCode.localeCompare(right.rawCode, "fr", { sensitivity: "base" });
- }
- function compareLabelsAlphabetically(left, right) {
- return String(left || "").localeCompare(String(right || ""), "fr", { sensitivity: "base" });
- }
- function escapeHtml(value) {
- return String(value)
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
- }
- function hasTerritoryBadge(territoryKey) {
- return territoryKey === "france" || Boolean(TERRITORY_BADGE_ASSETS[territoryKey]);
- }
- function getTerritoryBadgeMarkup(territory, extraClass = "") {
- const classNames = ["territory-badge", extraClass];
- if (territory.key === "france") {
- classNames.push("territory-badge-flag");
- return `<span class="${classNames.filter(Boolean).join(" ")}" aria-hidden="true">🇫🇷</span>`;
- }
- const badge = TERRITORY_BADGE_ASSETS[territory.key];
- if (!badge) {
- return `<span class="territory-list-marker" aria-hidden="true">▦</span>`;
- }
- if (badge.className) {
- classNames.push(badge.className);
- }
- return `
- <span class="${classNames.filter(Boolean).join(" ")}" aria-hidden="true">
- <img class="territory-badge-image" src="${badge.src}" alt="" loading="lazy" decoding="async" />
- </span>
- `;
- }
- function getTerritoryBadgeMarkupByKey(territoryKey, extraClass = "") {
- if (!territoryKey || !hasTerritoryBadge(territoryKey)) {
- return "";
- }
- return getTerritoryBadgeMarkup(getTerritoryMeta(territoryKey) || { key: territoryKey }, extraClass);
- }
- function formatBadgeLabelMarkup(label, territoryKey, extraClass = "") {
- if (territoryKey === "france") {
- return escapeHtml(label);
- }
- const badgeMarkup = getTerritoryBadgeMarkupByKey(territoryKey, extraClass);
- const escapedLabel = escapeHtml(label);
- if (!badgeMarkup) {
- return escapedLabel;
- }
- return `<span class="modal-badge-label">${badgeMarkup}<span>${escapedLabel}</span></span>`;
- }
- function formatCount(value) {
- return new Intl.NumberFormat("fr-FR").format(Number(value || 0));
- }
- function formatEntityCount(value, singular, plural) {
- const numericValue = Number(value || 0);
- const label = numericValue === 1 ? singular : plural;
- return `${formatCount(numericValue)} ${label}`;
- }
- function formatPostalCodes(postalCodes) {
- if (!Array.isArray(postalCodes) || !postalCodes.length) {
- return "";
- }
- return postalCodes.join(", ");
- }
- function formatGroupementShortLabel(code) {
- if (!code) {
- return "";
- }
- return `GGD${String(code).toUpperCase()}`;
- }
- function getUnitInstitutionLabel(institution) {
- return institution === "PN" ? "Police nationale" : "Gendarmerie nationale";
- }
- function isGroupementCommuneView(territoryMeta) {
- return territoryMeta?.entityType === COMMUNE_ENTITY_TYPE && Boolean(territoryMeta?.groupementView);
- }
- function isUnitMarkerView(territoryMeta) {
- return isGroupementCommuneView(territoryMeta);
- }
- function buildCompetenceColorByServiceKey(serviceByKey) {
- const serviceKeys = Array.from(serviceByKey.entries())
- .filter(([, service]) => service?.i === "GN")
- .sort((left, right) => left[1].l.localeCompare(right[1].l, "fr", { sensitivity: "base" }))
- .map(([serviceKey]) => serviceKey);
- const colorByServiceKey = new Map();
- const total = serviceKeys.length || 1;
- serviceKeys.forEach((serviceKey, index) => {
- const hue = Math.round((index * 360) / total);
- colorByServiceKey.set(serviceKey, `hsl(${hue} 46% 56%)`);
- });
- return colorByServiceKey;
- }
- function getCommuneCompetenceFillColor(featureProperties, usesCompetenceColoring, colorByServiceKey) {
- if (!usesCompetenceColoring) {
- return themeColors.communeFill;
- }
- if (featureProperties.competenceInstitution !== "GN" || !featureProperties.competenceServiceKey) {
- return themeColors.communeFill;
- }
- return colorByServiceKey.get(featureProperties.competenceServiceKey) || themeColors.communeFill;
- }
- function getCommuneCompetenceLabel(featureProperties) {
- if (featureProperties.competenceMixedLabel) {
- return featureProperties.competenceMixedLabel;
- }
- if (featureProperties.competenceLabel) {
- return featureProperties.competenceLabel;
- }
- if (featureProperties.competenceInstitution === "PN") {
- return "Police nationale";
- }
- if (featureProperties.competenceInstitution === "UNK") {
- return "Compétence territoriale non renseignée";
- }
- return "";
- }
- function getCompetenceServiceKeys(featureProperties) {
- if (Array.isArray(featureProperties.competenceServiceKeys) && featureProperties.competenceServiceKeys.length) {
- return featureProperties.competenceServiceKeys;
- }
- if (featureProperties.competenceServiceKey) {
- return [featureProperties.competenceServiceKey];
- }
- return [];
- }
- function encodeHitColor(index) {
- return `rgb(${index & 255},${(index >> 8) & 255},${(index >> 16) & 255})`;
- }
- function getHitColorKey(pixel) {
- return `${pixel[0]},${pixel[1]},${pixel[2]}`;
- }
- function setStatus(message, visible = true) {
- mapStatus.textContent = message;
- mapStatus.hidden = !visible;
- }
- function showTooltip(item, clientX, clientY) {
- const label = typeof item === "string" ? item : item?.properties?.nom;
- if (!label) {
- hideTooltip();
- return;
- }
- mapTooltip.innerHTML = `<strong>${escapeHtml(label)}</strong>`;
- mapTooltip.style.left = `${clientX}px`;
- mapTooltip.style.top = `${clientY}px`;
- mapTooltip.classList.add("visible");
- }
- function hideTooltip() {
- mapTooltip.classList.remove("visible");
- }
- function getTerritoryMeta(key) {
- return state.territoryByKey.get(key) || null;
- }
- function getRegionTerritoryByCode(regionCode) {
- if (!regionCode) {
- return null;
- }
- return (
- state.territories.find(
- (territory) => territory.entityType === DEPARTMENT_ENTITY_TYPE && territory.regionCode === regionCode
- ) || null
- );
- }
- function getRegionLabel(regionCode) {
- return getRegionTerritoryByCode(regionCode)?.label || regionCode || "";
- }
- function capitalizeFirst(value) {
- const text = String(value || "");
- if (!text) {
- return "";
- }
- return text.charAt(0).toUpperCase() + text.slice(1);
- }
- function isFeminineEntity(entityType) {
- return entityType === COMMUNE_ENTITY_TYPE || entityType === REGION_ENTITY_TYPE;
- }
- function getEntityCopy(territoryKey = state.activeTerritoryKey) {
- const territoryMeta = getTerritoryMeta(territoryKey);
- const entityType = territoryMeta?.entityType || COMMUNE_ENTITY_TYPE;
- const singular = territoryMeta?.entityLabelSingular || "commune";
- const plural = territoryMeta?.entityLabelPlural || "communes";
- const isFeminine = isFeminineEntity(entityType);
- const article = isFeminine ? "une" : "un";
- const selectedSuffix = isFeminine ? "e" : "";
- const nonePrefix = isFeminine ? "Aucune" : "Aucun";
- const foundSuffix = isFeminine ? "e" : "";
- return {
- entityType,
- singular,
- plural,
- listTitle: capitalizeFirst(plural),
- searchTitle: `Rechercher ${article} ${singular}`,
- searchPlaceholder: "Nom, code, territoire...",
- selectedTitle: capitalizeFirst(singular),
- noneFoundLabel: `${nonePrefix} ${singular} trouvé${foundSuffix}.`,
- sidebarAria: `Liste des ${plural}`,
- };
- }
- function buildRouteHash(route = {}) {
- const territoryKey = route.territoryKey || state.activeTerritoryKey || "france";
- const unitKey = route.unitKey || "";
- const entityCode = route.entityCode || "";
- if (unitKey) {
- return `#/${ROUTE_TERRITORY_SEGMENT}/${encodeURIComponent(territoryKey)}/${ROUTE_UNIT_SEGMENT}/${encodeURIComponent(
- unitKey
- )}`;
- }
- if (entityCode) {
- return `#/${ROUTE_TERRITORY_SEGMENT}/${encodeURIComponent(
- territoryKey
- )}/${ROUTE_ENTITY_SEGMENT}/${encodeURIComponent(entityCode)}`;
- }
- return `#/${ROUTE_TERRITORY_SEGMENT}/${encodeURIComponent(territoryKey)}`;
- }
- function parseRouteHash(hash) {
- const sanitizedHash = String(hash || "").replace(/^#/, "").replace(/^\/+/, "");
- if (!sanitizedHash) {
- return { territoryKey: "france", entityCode: null, unitKey: null, valid: true };
- }
- const parts = sanitizedHash
- .split("/")
- .filter(Boolean)
- .map((part) => decodeURIComponent(part));
- if (parts[0] !== ROUTE_TERRITORY_SEGMENT || !parts[1]) {
- return { territoryKey: "france", entityCode: null, unitKey: null, valid: false };
- }
- if (parts.length === 2) {
- return { territoryKey: parts[1], entityCode: null, unitKey: null, valid: true };
- }
- if (parts.length === 4 && parts[2] === ROUTE_ENTITY_SEGMENT && parts[3]) {
- return { territoryKey: parts[1], entityCode: parts[3], unitKey: null, valid: true };
- }
- if (parts.length === 4 && parts[2] === ROUTE_UNIT_SEGMENT && parts[3]) {
- return { territoryKey: parts[1], entityCode: null, unitKey: parts[3], valid: true };
- }
- return { territoryKey: parts[1], entityCode: null, unitKey: null, valid: false };
- }
- function buildRouteUrl(hash) {
- return `${window.location.pathname}${window.location.search}${hash}`;
- }
- function getCurrentRouteState() {
- return {
- territoryKey: state.activeTerritoryKey || "france",
- entityCode: state.selectedUnitKey ? null : state.selectedCommuneId || null,
- unitKey: state.selectedUnitKey || null,
- };
- }
- function syncRouteFromState(options = {}) {
- if (state.routeApplying || !state.activeTerritoryKey) {
- return;
- }
- const nextHash = buildRouteHash(getCurrentRouteState());
- if (nextHash === state.routeHash) {
- return;
- }
- const method = options.replace ? "replaceState" : "pushState";
- history[method](null, "", buildRouteUrl(nextHash));
- state.routeHash = nextHash;
- }
- function applyEntityUi() {
- const territoryMeta = getTerritoryMeta(state.activeTerritoryKey);
- const entityCopy = getEntityCopy();
- entityListTitle.textContent = entityCopy.listTitle;
- entitySidebar.setAttribute("aria-label", entityCopy.sidebarAria);
- communeSearchInputDesktop.placeholder = entityCopy.searchPlaceholder;
- communeSearchInputMobile.placeholder = entityCopy.searchPlaceholder;
- communeListModalTitle.textContent = entityCopy.searchTitle;
- openMobileCommuneListButton.setAttribute("aria-label", entityCopy.searchTitle);
- modalTitle.textContent = entityCopy.selectedTitle;
- if (territoryMeta) {
- mapShell.setAttribute(
- "aria-label",
- `Carte interactive des ${entityCopy.plural} de ${territoryMeta.label}`
- );
- }
- }
- function getPreparedFeatureById(communeId) {
- return state.activeTerritoryData?.featureById?.get(communeId) || null;
- }
- function getPreparedPartByKey(partKey) {
- return state.activeTerritoryData?.partByKey?.get(partKey) || null;
- }
- function getPreparedFeatureFromPartKey(partKey) {
- const preparedPart = getPreparedPartByKey(partKey);
- return preparedPart ? getPreparedFeatureById(preparedPart.featureId) : null;
- }
- function getPreparedUnitByKey(unitKey) {
- if (!state.activeTerritoryData?.preparedUnits?.length || !unitKey) {
- return null;
- }
- return state.activeTerritoryData.preparedUnits.find((unit) => unit.key === unitKey) || null;
- }
- function getPointerPositions(event) {
- const rect = mapCanvas.getBoundingClientRect();
- const localX = event.clientX - rect.left;
- const localY = event.clientY - rect.top;
- const [worldX, worldY] = getProjectedPointer(localX, localY);
- const scaleX = state.width > 0 ? hitCanvas.width / state.width : 1;
- const scaleY = state.height > 0 ? hitCanvas.height / state.height : 1;
- return {
- rect,
- localX,
- localY,
- worldX,
- worldY,
- canvasX: Math.floor(worldX * scaleX),
- canvasY: Math.floor(worldY * scaleY),
- };
- }
- function getProjectedPointer(localX, localY) {
- return [
- (localX - state.transform.x) / state.transform.k,
- (localY - state.transform.y) / state.transform.k,
- ];
- }
- function isPointInRing(pointX, pointY, ring) {
- let inside = false;
- for (let index = 0, previousIndex = ring.length - 1; index < ring.length; previousIndex = index, index += 1) {
- const [x0, y0] = ring[index];
- const [x1, y1] = ring[previousIndex];
- const crosses =
- (y0 > pointY) !== (y1 > pointY) &&
- pointX < ((x1 - x0) * (pointY - y0)) / ((y1 - y0) || Number.EPSILON) + x0;
- if (crosses) {
- inside = !inside;
- }
- }
- return inside;
- }
- function isPointInPolygon(pointX, pointY, rings) {
- if (!rings.length || !isPointInRing(pointX, pointY, rings[0])) {
- return false;
- }
- for (let index = 1; index < rings.length; index += 1) {
- if (isPointInRing(pointX, pointY, rings[index])) {
- return false;
- }
- }
- return true;
- }
- function isPreparedPartUnderPointer(preparedPart, localX, localY) {
- if (!preparedPart) {
- return false;
- }
- const [projectedX, projectedY] = getProjectedPointer(localX, localY);
- return isPointInPolygon(projectedX, projectedY, preparedPart.projectedRings);
- }
- function getAssociatedCommunesForUnit(unit) {
- if (!unit?.serviceKey || !state.activeTerritoryData?.raw?.features?.length) {
- return [];
- }
- return state.activeTerritoryData.raw.features
- .filter((feature) => getCompetenceServiceKeys(feature.properties).includes(unit.serviceKey))
- .slice()
- .sort((left, right) => compareLabelsAlphabetically(left.properties.nom, right.properties.nom));
- }
- function getUnitsForFeature(feature) {
- const serviceKeys = getCompetenceServiceKeys(feature.properties);
- if (!serviceKeys.length || !state.activeTerritoryData?.unitByServiceKey) {
- return [];
- }
- return serviceKeys
- .map((serviceKey) => state.activeTerritoryData.unitByServiceKey.get(serviceKey))
- .filter(Boolean)
- .sort((left, right) => compareLabelsAlphabetically(left.name, right.name));
- }
- function getPreparedUnitFromPointer(event) {
- if (!state.activeTerritoryData?.preparedUnits?.length) {
- return null;
- }
- const { localX, localY } = getPointerPositions(event);
- let nearestUnit = null;
- let nearestDistanceSquared = Infinity;
- state.activeTerritoryData.preparedUnits.forEach((preparedUnit) => {
- const screenX = preparedUnit.worldX * state.transform.k + state.transform.x;
- const screenY = preparedUnit.worldY * state.transform.k + state.transform.y;
- const deltaX = localX - screenX;
- const deltaY = localY - screenY;
- const distanceSquared = deltaX * deltaX + deltaY * deltaY;
- if (distanceSquared <= UNIT_MARKER_HIT_RADIUS * UNIT_MARKER_HIT_RADIUS && distanceSquared < nearestDistanceSquared) {
- nearestUnit = preparedUnit;
- nearestDistanceSquared = distanceSquared;
- }
- });
- return nearestUnit;
- }
- function getProjectedAreaFromBounds(bounds) {
- const [[x0, y0], [x1, y1]] = bounds;
- return Math.max(0, x1 - x0) * Math.max(0, y1 - y0);
- }
- function isPlanarCoordinateSpace(territoryData) {
- return String(territoryData?.raw?.coordinateSpace || "").startsWith("planar");
- }
- function projectPoint(projection, point, isPlanar) {
- if (!isPlanar) {
- return projection(point);
- }
- const scale = projection.scale();
- const [translateX, translateY] = projection.translate();
- return [point[0] * scale + translateX, point[1] * scale + translateY];
- }
- function projectPolygonCoordinates(projection, polygonCoordinates, isPlanar = false) {
- const projectedRings = [];
- const bounds = [
- [Infinity, Infinity],
- [-Infinity, -Infinity],
- ];
- polygonCoordinates.forEach((ring) => {
- const projectedRing = [];
- ring.forEach((point) => {
- const projectedPoint = projectPoint(projection, point, isPlanar);
- if (!projectedPoint) {
- return;
- }
- projectedRing.push(projectedPoint);
- bounds[0][0] = Math.min(bounds[0][0], projectedPoint[0]);
- bounds[0][1] = Math.min(bounds[0][1], projectedPoint[1]);
- bounds[1][0] = Math.max(bounds[1][0], projectedPoint[0]);
- bounds[1][1] = Math.max(bounds[1][1], projectedPoint[1]);
- });
- if (projectedRing.length >= 3) {
- projectedRings.push(projectedRing);
- }
- });
- if (!projectedRings.length) {
- return null;
- }
- return { projectedRings, bounds };
- }
- function createTerritoryProjection(territoryData) {
- const [horizontalPadding, verticalPadding] = getFitPadding();
- const projection = isPlanarCoordinateSpace(territoryData) ? d3.geoIdentity() : d3.geoMercator();
- projection.fitExtent(
- [
- [horizontalPadding, verticalPadding],
- [state.width - horizontalPadding, state.height - 72],
- ],
- territoryData.raw
- );
- const displayScale = TERRITORY_DISPLAY_SCALE[territoryData.key];
- if (displayScale && displayScale > 1) {
- const currentScale = projection.scale();
- const [translateX, translateY] = projection.translate();
- const viewportCenterX = (horizontalPadding + (state.width - horizontalPadding)) / 2;
- const viewportCenterY = (verticalPadding + (state.height - 72)) / 2;
- projection.scale(currentScale * displayScale);
- projection.translate([
- viewportCenterX + (translateX - viewportCenterX) * displayScale,
- viewportCenterY + (translateY - viewportCenterY) * displayScale,
- ]);
- }
- return projection;
- }
- function buildPath2DFromPreparedParts(parts) {
- if (!window.Path2D || !parts?.length) {
- return null;
- }
- const path = new Path2D();
- parts.forEach((part) => {
- part.projectedRings.forEach((ring) => {
- if (!ring.length) {
- return;
- }
- path.moveTo(ring[0][0], ring[0][1]);
- for (let index = 1; index < ring.length; index += 1) {
- path.lineTo(ring[index][0], ring[index][1]);
- }
- path.closePath();
- });
- });
- return path;
- }
- function waitForNextPaint() {
- return new Promise((resolve) => {
- requestAnimationFrame(() => resolve());
- });
- }
- function traceProjectedPolygon(context, projectedRings) {
- context.beginPath();
- projectedRings.forEach((ring) => {
- if (!ring.length) {
- return;
- }
- context.moveTo(ring[0][0], ring[0][1]);
- for (let index = 1; index < ring.length; index += 1) {
- context.lineTo(ring[index][0], ring[index][1]);
- }
- context.closePath();
- });
- }
- function getFeaturePolygonParts(feature) {
- if (feature.geometry.type === "Polygon") {
- return [feature.geometry.coordinates];
- }
- return feature.geometry.coordinates;
- }
- function getUnionBounds(parts) {
- if (!parts.length) {
- return [
- [0, 0],
- [0, 0],
- ];
- }
- const initial = [
- [Infinity, Infinity],
- [-Infinity, -Infinity],
- ];
- return parts.reduce((accumulator, part) => {
- const [[x0, y0], [x1, y1]] = part.bounds;
- accumulator[0][0] = Math.min(accumulator[0][0], x0);
- accumulator[0][1] = Math.min(accumulator[0][1], y0);
- accumulator[1][0] = Math.max(accumulator[1][0], x1);
- accumulator[1][1] = Math.max(accumulator[1][1], y1);
- return accumulator;
- }, initial);
- }
- function buildDataRows(feature) {
- const territoryMeta = getTerritoryMeta(feature.properties.territoryKey);
- const entityType = feature.properties.kind || territoryMeta?.entityType || COMMUNE_ENTITY_TYPE;
- let rows = [];
- if (entityType === REGION_ENTITY_TYPE) {
- rows = [{ label: "Territoire", value: territoryMeta?.label || "France", territoryKey: territoryMeta?.key || "france" }];
- } else if (entityType === DEPARTMENT_ENTITY_TYPE) {
- rows = [
- { label: "Code groupement", value: feature.properties.code },
- { label: "Appellation courte", value: formatGroupementShortLabel(feature.properties.code) },
- { label: "Région de Gendarmerie", value: getRegionLabel(feature.properties.codeRegion) },
- { label: "Territoire", value: territoryMeta?.label || "France" },
- ];
- } else {
- const postalCodes = feature.properties.postalCodes || [];
- const postalCodeLabel = postalCodes.length > 1 ? "Codes postaux" : "Code postal";
- const regionTerritory = getRegionTerritoryByCode(feature.properties.codeRegion);
- const linkedUnits = getUnitsForFeature(feature);
- const competenceLabel = getCommuneCompetenceLabel(feature.properties);
- rows = [
- { label: postalCodeLabel, value: formatPostalCodes(postalCodes) || feature.properties.code },
- { label: "Groupement", value: feature.properties.codeDepartement },
- {
- label: "Région de Gendarmerie",
- value: getRegionLabel(feature.properties.codeRegion),
- territoryKey: regionTerritory?.key || null,
- },
- {
- label: "Territoire",
- value: territoryMeta?.label || "France",
- territoryKey: territoryMeta?.key || "france",
- },
- ];
- if (competenceLabel) {
- const unitLinksMarkup = linkedUnits.length
- ? linkedUnits
- .map(
- (unit) =>
- `<button type="button" class="unit-link-button" data-unit-service-key="${escapeHtml(
- unit.serviceKey
- )}">${escapeHtml(unit.name)}</button>`
- )
- .join('<span class="unit-link-separator"> / </span>')
- : "";
- rows.push(
- unitLinksMarkup
- ? { label: "Compétence territoriale", html: unitLinksMarkup }
- : { label: "Compétence territoriale", value: competenceLabel }
- );
- }
- }
- return rows
- .map(
- ({ label, value = "", html = "", territoryKey = null }) =>
- `<div class="country-data-row"><strong>${escapeHtml(label)}</strong><span>${
- html ||
- formatBadgeLabelMarkup(
- value,
- territoryKey,
- "modal-badge-inline"
- )
- }</span></div>`
- )
- .join("");
- }
- function buildUnitDataMarkup(unit) {
- const territoryMeta = getTerritoryMeta(state.activeTerritoryKey);
- const associatedCommunes = getAssociatedCommunesForUnit(unit);
- const rows = [
- { label: "Institution", value: getUnitInstitutionLabel(unit.institution) },
- { label: "Groupement", value: territoryMeta?.label || "" },
- { label: "Commune d'implantation", value: unit.hostCommune || "Non renseignée" },
- { label: "Téléphone", value: unit.telephone || "Non renseigné" },
- { label: "Adresse", value: unit.address || "Non renseignée" },
- ];
- if (unit.hours) {
- rows.push({ label: "Horaires", value: unit.hours });
- }
- rows.push({
- label: "Communes associées",
- value: formatEntityCount(associatedCommunes.length, "commune", "communes"),
- });
- const rowsMarkup = rows
- .map(
- ({ label, value }) =>
- `<div class="country-data-row"><strong>${escapeHtml(label)}</strong><span>${escapeHtml(value)}</span></div>`
- )
- .join("");
- const communesMarkup = associatedCommunes.length
- ? associatedCommunes
- .map((feature) => `<li>${escapeHtml(feature.properties.nom)}</li>`)
- .join("")
- : `<li>Aucune commune associée.</li>`;
- return `
- ${rowsMarkup}
- <div class="unit-communes-section">
- <strong>Liste des communes associées</strong>
- <ul class="unit-communes-list">${communesMarkup}</ul>
- </div>
- `;
- }
- function bindUnitLinks(container) {
- container.querySelectorAll("[data-unit-service-key]").forEach((element) => {
- element.addEventListener("click", () => {
- const serviceKey = element.getAttribute("data-unit-service-key");
- const unit = state.activeTerritoryData?.unitByServiceKey?.get(serviceKey || "");
- if (!unit) {
- return;
- }
- selectUnit(unit, { zoom: false, openModal: true });
- });
- });
- }
- function getFeatureOpenLabel(feature) {
- const entityType = feature.properties.kind;
- if (entityType === REGION_ENTITY_TYPE) {
- return "Ouvrir la région de gendarmerie";
- }
- if (entityType === DEPARTMENT_ENTITY_TYPE) {
- return "Ouvrir le groupement";
- }
- return "";
- }
- function updateModalAction(feature) {
- const childTerritoryKey = feature.properties.childTerritoryKey;
- if (!childTerritoryKey) {
- modalOpenTerritoryButton.hidden = true;
- modalOpenTerritoryButton.textContent = "";
- modalOpenTerritoryButton.onclick = null;
- return;
- }
- modalOpenTerritoryButton.hidden = false;
- modalOpenTerritoryButton.textContent = getFeatureOpenLabel(feature);
- modalOpenTerritoryButton.onclick = async () => {
- closeCommuneModal({ syncRoute: false });
- await activateTerritory(childTerritoryKey);
- };
- }
- function updateUnitModalAction(unit) {
- if (!unit?.url) {
- modalOpenTerritoryButton.hidden = true;
- modalOpenTerritoryButton.textContent = "";
- modalOpenTerritoryButton.onclick = null;
- return;
- }
- modalOpenTerritoryButton.hidden = false;
- modalOpenTerritoryButton.textContent = "Ouvrir la fiche officielle";
- modalOpenTerritoryButton.onclick = () => {
- window.open(unit.url, "_blank", "noopener,noreferrer");
- };
- }
- function openCommuneModal(feature) {
- const territoryMeta = getTerritoryMeta(feature.properties.territoryKey);
- const entityTerritoryKey =
- feature.properties.kind === REGION_ENTITY_TYPE ? feature.properties.childTerritoryKey || null : null;
- modalTitle.textContent = getEntityCopy(feature.properties.territoryKey).selectedTitle;
- communeName.innerHTML = formatBadgeLabelMarkup(feature.properties.nom, entityTerritoryKey, "modal-badge-heading");
- communeTerritory.innerHTML = formatBadgeLabelMarkup(
- territoryMeta ? `${territoryMeta.label}` : "France",
- territoryMeta?.key || "france",
- "modal-badge-inline"
- );
- communeData.innerHTML = buildDataRows(feature);
- bindUnitLinks(communeData);
- updateModalAction(feature);
- communeModal.classList.add("open");
- communeModal.setAttribute("aria-hidden", "false");
- }
- function openUnitModal(unit) {
- const territoryMeta = getTerritoryMeta(state.activeTerritoryKey);
- modalTitle.textContent = "Unité";
- communeName.textContent = unit.name;
- communeTerritory.textContent = territoryMeta ? territoryMeta.label : "";
- communeData.innerHTML = buildUnitDataMarkup(unit);
- updateUnitModalAction(unit);
- communeModal.classList.add("open");
- communeModal.setAttribute("aria-hidden", "false");
- }
- function closeCommuneModal(options = {}) {
- communeModal.classList.remove("open");
- communeModal.setAttribute("aria-hidden", "true");
- modalOpenTerritoryButton.hidden = true;
- modalOpenTerritoryButton.textContent = "";
- modalOpenTerritoryButton.onclick = null;
- if (options.clearSelection !== false) {
- state.selectedCommuneId = null;
- state.selectedPartKey = null;
- state.selectedUnitKey = null;
- renderCommuneResults(communeSearchInputDesktop.value);
- requestRender();
- }
- if (options.syncRoute !== false) {
- syncRouteFromState({ replace: Boolean(options.replaceRoute) });
- }
- }
- function closeCommuneListModal() {
- communeListModal.classList.remove("open");
- communeListModal.setAttribute("aria-hidden", "true");
- }
- function openCommuneListModal() {
- communeListModal.classList.add("open");
- communeListModal.setAttribute("aria-hidden", "false");
- communeSearchInputMobile.value = communeSearchInputDesktop.value;
- communeSearchInputMobile.focus();
- renderCommuneResults(communeSearchInputMobile.value);
- }
- function clearSearchInputs() {
- communeSearchInputDesktop.value = "";
- communeSearchInputMobile.value = "";
- }
- function updateTerritorySummary() {
- const territoryMeta = getTerritoryMeta(state.activeTerritoryKey);
- if (!territoryMeta) {
- territorySummary.textContent = "";
- return;
- }
- const entityCopy = getEntityCopy();
- territorySummary.textContent = `${territoryMeta.label} • ${formatEntityCount(
- territoryMeta.entityCount,
- entityCopy.singular,
- entityCopy.plural
- )} • survolez pour afficher le nom, cliquez pour sélectionner.`;
- }
- function closeMaskingModal() {
- maskingModal.classList.remove("open");
- maskingModal.setAttribute("aria-hidden", "true");
- }
- function buildMaskingModalMarkup(maskedData) {
- const communesMarkup = maskedData.communes.length
- ? maskedData.communes
- .map((commune) => {
- const postalCodes = commune.postalCodes.length ? ` • ${commune.postalCodes.join(", ")}` : "";
- return `<li><strong>${escapeHtml(commune.name)}</strong>${escapeHtml(postalCodes)}</li>`;
- })
- .join("")
- : "<li>Aucune commune masquée.</li>";
- const unitsMarkup = maskedData.units.length
- ? maskedData.units
- .map((unit) => {
- const associatedCommunes = unit.communeNames.join(", ");
- const institution = getUnitInstitutionLabel(unit.institution);
- return `<li><strong>${escapeHtml(unit.name)}</strong> (${escapeHtml(
- institution
- )})<br /><span>${escapeHtml(associatedCommunes)}</span></li>`;
- })
- .join("")
- : "<li>Aucune unité associée.</li>";
- return `
- <section class="masking-modal-block">
- <h3>Communes masquées</h3>
- <ul class="masking-modal-list">${communesMarkup}</ul>
- </section>
- <section class="masking-modal-block">
- <h3>Unités associées</h3>
- <ul class="masking-modal-list">${unitsMarkup}</ul>
- </section>
- `;
- }
- function openMaskingModal(options = {}) {
- const maskedData = state.activeTerritoryData?.maskedData;
- if (!maskedData?.communes?.length) {
- return;
- }
- const territoryMeta = getTerritoryMeta(state.activeTerritoryKey);
- const focusedCommune = options.focusCommuneCode
- ? maskedData.communeByCode.get(options.focusCommuneCode)
- : null;
- const focusLabel = focusedCommune ? ` (${focusedCommune.name})` : "";
- maskingModalSummary.textContent = `${formatEntityCount(
- maskedData.communes.length,
- "commune masquée",
- "communes masquées"
- )} dans ${territoryMeta?.label || "ce territoire"}${focusLabel}.`;
- maskingModalData.innerHTML = buildMaskingModalMarkup(maskedData);
- maskingModal.classList.add("open");
- maskingModal.setAttribute("aria-hidden", "false");
- }
- function updateMaskingDisclaimer() {
- const maskedData = state.activeTerritoryData?.maskedData;
- if (!maskedData?.communes?.length) {
- maskingDisclaimer.hidden = true;
- maskingDisclaimerText.textContent = "";
- return;
- }
- maskingDisclaimer.hidden = false;
- maskingDisclaimerText.textContent = `${formatEntityCount(
- maskedData.communes.length,
- "zone",
- "zones"
- )} masquée(s) sur la carte pour lisibilité.`;
- }
- function renderTerritoryList() {
- territoryList.innerHTML = "";
- state.territories
- .filter((territory) => territory.navVisible !== false)
- .forEach((territory) => {
- const item = document.createElement("li");
- const button = document.createElement("button");
- button.type = "button";
- button.className = "territory-list-item";
- if (territory.key === state.activeTerritoryKey) {
- button.classList.add("active");
- }
- button.innerHTML = `
- ${getTerritoryBadgeMarkup(territory)}
- <span class="territory-list-body">
- <span class="territory-list-title">${escapeHtml(territory.label)}</span>
- <span class="territory-list-meta">${escapeHtml(
- formatEntityCount(
- territory.entityCount,
- territory.entityLabelSingular,
- territory.entityLabelPlural
- )
- )}</span>
- </span>
- `;
- button.addEventListener("click", async () => {
- await activateTerritory(territory.key);
- if (window.innerWidth <= 760) {
- territoryPanel.classList.remove("mobile-open");
- territoryToggleButton.setAttribute("aria-expanded", "false");
- }
- });
- item.appendChild(button);
- territoryList.appendChild(item);
- });
- }
- function sortSearchResults(term, items) {
- const normalizedTerm = normalizeText(term);
- return items
- .map((entry) => {
- const nameIndex = entry.searchName.indexOf(normalizedTerm);
- const startsWith = nameIndex === 0 ? 0 : 1;
- const territoryBoost = entry.territoryKey === state.activeTerritoryKey ? 0 : 1;
- const compactTerm = term.trim().replace(/\s+/g, "");
- const codeBoost = entry.searchCode.replace(/[^a-z0-9]+/g, "").startsWith(compactTerm) ? 0 : 1;
- return { entry, startsWith, territoryBoost, codeBoost, nameIndex };
- })
- .sort((left, right) => {
- if (left.codeBoost !== right.codeBoost) {
- return left.codeBoost - right.codeBoost;
- }
- if (left.startsWith !== right.startsWith) {
- return left.startsWith - right.startsWith;
- }
- if (left.territoryBoost !== right.territoryBoost) {
- return left.territoryBoost - right.territoryBoost;
- }
- if (left.nameIndex !== right.nameIndex) {
- return left.nameIndex - right.nameIndex;
- }
- return compareEntriesAlphabetically(left.entry, right.entry);
- })
- .map((item) => item.entry)
- .slice(0, MAX_SEARCH_RESULTS);
- }
- function getSearchResults(term) {
- const normalizedTerm = normalizeText(term);
- if (!normalizedTerm) {
- return state.searchEntries
- .filter((entry) => entry.territoryKey === state.activeTerritoryKey)
- .sort(compareEntriesAlphabetically)
- .slice(0, MAX_SEARCH_RESULTS);
- }
- const matches = [];
- const activeUnitSearchEntries = state.activeTerritoryData?.unitSearchEntries || [];
- for (const entry of state.searchEntries) {
- if (
- entry.searchName.includes(normalizedTerm) ||
- entry.searchCode.replace(/[^a-z0-9]+/g, "").includes(normalizedTerm.replace(/\s+/g, ""))
- ) {
- matches.push(entry);
- }
- }
- for (const entry of activeUnitSearchEntries) {
- if (
- entry.searchName.includes(normalizedTerm) ||
- entry.searchCode.replace(/[^a-z0-9]+/g, "").includes(normalizedTerm.replace(/\s+/g, ""))
- ) {
- matches.push(entry);
- }
- }
- return sortSearchResults(term, matches);
- }
- function renderCommuneResults(term) {
- const results = getSearchResults(term);
- const normalizedTerm = normalizeText(term);
- const entityCopy = getEntityCopy();
- const resultCountLabel = normalizedTerm
- ? `${formatCount(results.length)} résultat(s) affiché(s)`
- : `${formatEntityCount(results.length, entityCopy.singular, entityCopy.plural)} du territoire affiché`;
- communeListCountDesktop.textContent = resultCountLabel;
- communeListCountMobile.textContent = resultCountLabel;
- const containers = [communeListContainerDesktop, communeListContainerMobile];
- containers.forEach((container) => {
- container.innerHTML = "";
- if (!results.length) {
- const empty = document.createElement("p");
- empty.className = "country-list-empty";
- empty.textContent = entityCopy.noneFoundLabel;
- container.appendChild(empty);
- return;
- }
- results.forEach((entry) => {
- const item = document.createElement("li");
- const button = document.createElement("button");
- const territoryMeta = getTerritoryMeta(entry.territoryKey);
- const territoryLabel = territoryMeta?.shortLabel || territoryMeta?.label || "France";
- const metaLabel = entry.displayCode
- ? `${entry.displayCode} • ${territoryLabel}${entry.isMasked ? " • zone masquée" : ""}`
- : `${territoryLabel}${entry.isMasked ? " • zone masquée" : ""}`;
- button.type = "button";
- button.className = "country-list-item";
- if (entry.isMasked) {
- button.classList.add("masked");
- }
- if (
- (entry.kind === UNIT_SEARCH_KIND && entry.id === state.selectedUnitKey) ||
- (entry.kind !== UNIT_SEARCH_KIND && entry.id === state.selectedCommuneId)
- ) {
- button.classList.add("active");
- }
- button.innerHTML = `
- <span class="country-list-flag">${entry.kind === UNIT_SEARCH_KIND ? "■" : entry.isMasked ? "◌" : "▣"}</span>
- <span class="country-list-body">
- <span class="country-list-title">${escapeHtml(entry.name)}</span>
- <span class="country-list-meta">${escapeHtml(metaLabel)}</span>
- </span>
- `;
- button.addEventListener("click", async () => {
- communeSearchInputDesktop.value = entry.name;
- communeSearchInputMobile.value = entry.name;
- closeCommuneListModal();
- if (entry.isMasked) {
- if (entry.territoryKey !== state.activeTerritoryKey) {
- await activateTerritory(entry.territoryKey, { syncRoute: false });
- }
- openMaskingModal({ focusCommuneCode: entry.id });
- return;
- }
- if (entry.kind === UNIT_SEARCH_KIND) {
- const unit = state.activeTerritoryData?.unitByServiceKey?.get(entry.id);
- if (unit) {
- selectUnit(unit, { zoom: true, openModal: true });
- }
- return;
- }
- if (entry.territoryKey === state.activeTerritoryKey) {
- updateSelection(entry.id, { zoom: true, openModal: true });
- return;
- }
- await activateTerritory(entry.territoryKey, { focusCommuneId: entry.id, openModal: true });
- });
- item.appendChild(button);
- container.appendChild(item);
- });
- });
- }
- async function fetchJson(url) {
- const response = await fetch(url);
- if (!response.ok) {
- throw new Error(`Impossible de charger ${url}`);
- }
- return response.json();
- }
- function prepareSearchEntries(entries) {
- return entries.map((entry) => ({
- id: entry.c,
- code: entry.k === COMMUNE_ENTITY_TYPE ? formatPostalCodes(entry.p) || entry.c : entry.c,
- displayCode:
- entry.k === REGION_ENTITY_TYPE
- ? ""
- : entry.k === COMMUNE_ENTITY_TYPE
- ? formatPostalCodes(entry.p) || entry.c
- : entry.c,
- rawCode: entry.c,
- name: entry.n,
- territoryKey: entry.t,
- kind: entry.k,
- isMasked: Boolean(entry.h),
- searchName: normalizeText(entry.n),
- searchCode:
- entry.k === COMMUNE_ENTITY_TYPE
- ? `${(entry.p || []).join(" ")} ${entry.c}`.trim()
- : entry.c,
- }));
- }
- function prepareUnitSearchEntries(units, territoryKey) {
- return units.map((unit) => ({
- id: unit.serviceKey,
- code: `${unit.institution}${unit.hostCommune ? ` • ${unit.hostCommune}` : ""}`,
- displayCode: `${unit.institution}${unit.hostCommune ? ` • ${unit.hostCommune}` : ""}`,
- rawCode: unit.serviceKey,
- name: unit.name,
- territoryKey,
- kind: UNIT_SEARCH_KIND,
- searchName: normalizeText(unit.name),
- searchCode: `${unit.hostCommune} ${unit.telephone} ${unit.serviceKey}`.trim(),
- }));
- }
- function resizeCanvases() {
- const nextWidth = Math.max(1, Math.round(mapShell.clientWidth));
- const nextHeight = Math.max(1, Math.round(mapShell.clientHeight));
- state.pixelRatio = window.devicePixelRatio || 1;
- state.width = nextWidth;
- state.height = nextHeight;
- [mapCanvas, hitCanvas].forEach((canvas) => {
- canvas.width = Math.round(nextWidth * state.pixelRatio);
- canvas.height = Math.round(nextHeight * state.pixelRatio);
- });
- baseCanvas.width = Math.round(nextWidth * state.pixelRatio);
- baseCanvas.height = Math.round(nextHeight * state.pixelRatio);
- state.baseDirty = true;
- state.hitDirty = true;
- if (state.activeTerritoryData) {
- prepareTerritoryForCurrentViewport(state.activeTerritoryData);
- }
- requestRender();
- }
- function getFitPadding() {
- const horizontalPadding = window.innerWidth <= 760 ? 22 : BASE_PADDING;
- const verticalPadding = window.innerWidth <= 760 ? 120 : BASE_PADDING;
- return [horizontalPadding, verticalPadding];
- }
- function traceRoundedMarker(context, centerX, centerY, size, radius) {
- const half = size / 2;
- const x = centerX - half;
- const y = centerY - half;
- const boundedRadius = Math.min(radius, half);
- context.beginPath();
- context.moveTo(x + boundedRadius, y);
- context.lineTo(x + size - boundedRadius, y);
- context.quadraticCurveTo(x + size, y, x + size, y + boundedRadius);
- context.lineTo(x + size, y + size - boundedRadius);
- context.quadraticCurveTo(x + size, y + size, x + size - boundedRadius, y + size);
- context.lineTo(x + boundedRadius, y + size);
- context.quadraticCurveTo(x, y + size, x, y + size - boundedRadius);
- context.lineTo(x, y + boundedRadius);
- context.quadraticCurveTo(x, y, x + boundedRadius, y);
- context.closePath();
- }
- function prepareTerritoryForCurrentViewport(territoryData) {
- const viewportKey = `${state.width}x${state.height}`;
- if (territoryData.preparedViewportKey === viewportKey && territoryData.preparedFeatures?.length) {
- state.baseDirty = true;
- state.hitDirty = true;
- return;
- }
- const projection = createTerritoryProjection(territoryData);
- const territoryMeta = getTerritoryMeta(territoryData.key);
- const usesPlanarCoordinates = isPlanarCoordinateSpace(territoryData);
- const featureById = new Map();
- const partByKey = new Map();
- const partKeyByHitColor = new Map();
- const preparedParts = [];
- const competenceColorByServiceKey = buildCompetenceColorByServiceKey(territoryData.serviceByKey);
- const usesCompetenceColoring = isGroupementCommuneView(territoryMeta);
- const usesUnitMarkers = isUnitMarkerView(territoryMeta);
- let nextPartKey = 1;
- territoryData.preparedFeatures = territoryData.raw.features.map((feature) => {
- const fillColor = getCommuneCompetenceFillColor(
- feature.properties,
- usesCompetenceColoring,
- competenceColorByServiceKey
- );
- const polygonParts = getFeaturePolygonParts(feature);
- const parts = polygonParts
- .map((polygonCoordinates) => {
- const projected = projectPolygonCoordinates(projection, polygonCoordinates, usesPlanarCoordinates);
- if (!projected) {
- return null;
- }
- const part = {
- key: nextPartKey,
- hitColor: encodeHitColor(nextPartKey),
- featureId: feature.properties.code,
- feature,
- fillColor,
- projectedRings: projected.projectedRings,
- bounds: projected.bounds,
- area: getProjectedAreaFromBounds(projected.bounds),
- };
- nextPartKey += 1;
- partByKey.set(part.key, part);
- partKeyByHitColor.set(part.hitColor.slice(4, -1), part.key);
- preparedParts.push(part);
- return part;
- })
- .filter(Boolean);
- const prepared = {
- feature,
- id: feature.properties.code,
- parts,
- fillColor,
- bounds: getUnionBounds(parts),
- primaryPartKey:
- parts
- .slice()
- .sort((left, right) => right.area - left.area)[0]?.key || null,
- };
- featureById.set(prepared.id, prepared);
- return prepared;
- });
- territoryData.territoryPath = buildPath2DFromPreparedParts(preparedParts);
- territoryData.featureById = featureById;
- territoryData.partByKey = partByKey;
- territoryData.partKeyByHitColor = partKeyByHitColor;
- territoryData.preparedParts = preparedParts;
- territoryData.preparedUnits = usesUnitMarkers
- ? territoryData.units
- .map((unit) => {
- const projectedPoint = projectPoint(projection, [unit.longitude, unit.latitude], usesPlanarCoordinates);
- if (!projectedPoint) {
- return null;
- }
- return {
- ...unit,
- worldX: projectedPoint[0],
- worldY: projectedPoint[1],
- };
- })
- .filter(Boolean)
- : [];
- territoryData.competenceColorByServiceKey = competenceColorByServiceKey;
- territoryData.usesCompetenceColoring = usesCompetenceColoring;
- territoryData.preparedViewportKey = viewportKey;
- state.baseDirty = true;
- state.hitDirty = true;
- }
- function applyCanvasScale(context) {
- context.scale(state.pixelRatio, state.pixelRatio);
- }
- function applyRenderTransform(context) {
- context.scale(state.pixelRatio, state.pixelRatio);
- context.translate(state.transform.x, state.transform.y);
- context.scale(state.transform.k, state.transform.k);
- }
- function getVisibleWorldBounds() {
- return [
- [(-state.transform.x) / state.transform.k, (-state.transform.y) / state.transform.k],
- [
- (state.width - state.transform.x) / state.transform.k,
- (state.height - state.transform.y) / state.transform.k,
- ],
- ];
- }
- function boundsIntersect(bounds, viewportBounds, padding = 0) {
- const [[x0, y0], [x1, y1]] = bounds;
- const [[vx0, vy0], [vx1, vy1]] = viewportBounds;
- return x1 >= vx0 - padding && x0 <= vx1 + padding && y1 >= vy0 - padding && y0 <= vy1 + padding;
- }
- function drawBaseLayer() {
- baseContext.setTransform(1, 0, 0, 1, 0, 0);
- baseContext.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
- baseContext.save();
- applyCanvasScale(baseContext);
- if (state.activeTerritoryData.territoryPath) {
- baseContext.clip(state.activeTerritoryData.territoryPath);
- }
- baseContext.lineJoin = "round";
- baseContext.lineCap = "round";
- baseContext.lineWidth = FILL_SEAL_WIDTH;
- state.activeTerritoryData.preparedParts.forEach((preparedPart) => {
- const fillColor = preparedPart.fillColor || themeColors.communeFill;
- baseContext.fillStyle = fillColor;
- baseContext.strokeStyle = fillColor;
- traceProjectedPolygon(baseContext, preparedPart.projectedRings);
- baseContext.fill();
- baseContext.stroke();
- });
- baseContext.restore();
- }
- function drawHitLayer() {
- hitContext.setTransform(1, 0, 0, 1, 0, 0);
- hitContext.clearRect(0, 0, hitCanvas.width, hitCanvas.height);
- hitContext.save();
- applyCanvasScale(hitContext);
- state.activeTerritoryData.preparedParts.forEach((preparedPart) => {
- hitContext.fillStyle = preparedPart.hitColor;
- traceProjectedPolygon(hitContext, preparedPart.projectedRings);
- hitContext.fill();
- });
- hitContext.restore();
- }
- function drawBaseVectorLayer(context) {
- const viewportBounds = getVisibleWorldBounds();
- const padding = Math.max(2, 8 / state.transform.k);
- context.lineJoin = "round";
- context.lineCap = "round";
- context.lineWidth = Math.max(0.08, FILL_SEAL_WIDTH / state.transform.k);
- state.activeTerritoryData.preparedParts.forEach((preparedPart) => {
- if (!boundsIntersect(preparedPart.bounds, viewportBounds, padding)) {
- return;
- }
- const fillColor = preparedPart.fillColor || themeColors.communeFill;
- context.fillStyle = fillColor;
- context.strokeStyle = fillColor;
- traceProjectedPolygon(context, preparedPart.projectedRings);
- context.fill();
- context.stroke();
- });
- }
- function drawFeatureOverlay(context, preparedFeature, fillColor) {
- if (!preparedFeature?.parts?.length) {
- return;
- }
- context.fillStyle = fillColor;
- preparedFeature.parts.forEach((preparedPart) => {
- traceProjectedPolygon(context, preparedPart.projectedRings);
- context.fill();
- });
- }
- function drawVisibleBoundaries(context) {
- const viewportBounds = getVisibleWorldBounds();
- const padding = Math.max(2, 8 / state.transform.k);
- context.lineJoin = "round";
- context.lineCap = "round";
- context.strokeStyle = themeColors.communeStroke;
- context.lineWidth = Math.max(0.3, BORDER_LINE_WIDTH / state.transform.k);
- state.activeTerritoryData.preparedParts.forEach((preparedPart) => {
- if (boundsIntersect(preparedPart.bounds, viewportBounds, padding)) {
- traceProjectedPolygon(context, preparedPart.projectedRings);
- context.stroke();
- }
- });
- }
- function drawUnitMarkers(context) {
- if (!state.activeTerritoryData?.preparedUnits?.length) {
- return;
- }
- const orderedUnits = state.activeTerritoryData.preparedUnits
- .slice()
- .sort((left, right) => {
- const leftPriority = left.key === state.hoveredUnitKey || left.key === state.selectedUnitKey ? 1 : 0;
- const rightPriority = right.key === state.hoveredUnitKey || right.key === state.selectedUnitKey ? 1 : 0;
- return leftPriority - rightPriority;
- });
- orderedUnits.forEach((preparedUnit) => {
- const isHovered = preparedUnit.key === state.hoveredUnitKey;
- const isSelected = preparedUnit.key === state.selectedUnitKey;
- const markerSize = (isHovered || isSelected ? UNIT_MARKER_HOVER_SIZE : UNIT_MARKER_SIZE) / state.transform.k;
- const cornerRadius = UNIT_MARKER_CORNER_RADIUS / state.transform.k;
- traceRoundedMarker(context, preparedUnit.worldX, preparedUnit.worldY, markerSize, cornerRadius);
- context.fillStyle = isHovered || isSelected ? themeColors.unitMarkerHover : themeColors.unitMarkerFill;
- context.fill();
- context.lineWidth = Math.max(0.9, 1.55 / state.transform.k);
- context.strokeStyle = themeColors.unitMarkerStroke;
- context.stroke();
- });
- }
- function renderMap() {
- state.renderQueued = false;
- mapContext.setTransform(1, 0, 0, 1, 0, 0);
- mapContext.fillStyle = themeColors.background;
- mapContext.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
- if (!state.activeTerritoryData?.preparedFeatures?.length) {
- return;
- }
- if (state.baseDirty) {
- drawBaseLayer();
- state.baseDirty = false;
- }
- if (state.hitDirty) {
- drawHitLayer();
- state.hitDirty = false;
- }
- mapContext.save();
- applyRenderTransform(mapContext);
- const usesVectorBaseLayer = state.transform.k > VECTOR_RENDER_SCALE_THRESHOLD;
- if (usesVectorBaseLayer) {
- drawBaseVectorLayer(mapContext);
- } else {
- mapContext.imageSmoothingEnabled = false;
- mapContext.drawImage(baseCanvas, 0, 0, state.width, state.height);
- mapContext.imageSmoothingEnabled = true;
- }
- const selectedPreparedFeature = getPreparedFeatureById(state.selectedCommuneId);
- const hoveredPreparedFeature = getPreparedFeatureFromPartKey(state.hoveredPartKey);
- drawFeatureOverlay(mapContext, selectedPreparedFeature, themeColors.communeSelected);
- if (!selectedPreparedFeature || hoveredPreparedFeature?.id !== selectedPreparedFeature.id) {
- drawFeatureOverlay(mapContext, hoveredPreparedFeature, themeColors.communeHover);
- }
- drawVisibleBoundaries(mapContext);
- drawUnitMarkers(mapContext);
- mapContext.restore();
- }
- function requestRender() {
- if (state.renderQueued) {
- return;
- }
- state.renderQueued = true;
- requestAnimationFrame(renderMap);
- }
- async function loadTerritoryData(territoryKey) {
- if (state.territoryDataByKey.has(territoryKey)) {
- return state.territoryDataByKey.get(territoryKey);
- }
- const territoryMeta = getTerritoryMeta(territoryKey);
- if (!territoryMeta) {
- throw new Error(`Territoire inconnu: ${territoryKey}`);
- }
- setStatus(`Chargement de ${territoryMeta.label}...`);
- const raw = await fetchJson(territoryMeta.file);
- const serviceByKey = new Map(Object.entries(raw.services || {}));
- const enhanced = {
- key: territoryKey,
- raw,
- units: (raw.units || []).map((unit) => ({
- key: unit.s,
- serviceKey: unit.s,
- institution: unit.i || "GN",
- name: unit.n,
- longitude: Number(unit.x),
- latitude: Number(unit.y),
- communeCode: unit.c || "",
- hostCommune: unit.m || "",
- address: unit.a || "",
- telephone: unit.t || "",
- hours: unit.h || "",
- url: unit.u || "",
- })),
- serviceByKey,
- unitByServiceKey: new Map(),
- unitSearchEntries: [],
- preparedFeatures: [],
- preparedUnits: [],
- territoryPath: null,
- featureById: new Map(),
- partByKey: new Map(),
- partKeyByHitColor: new Map(),
- preparedParts: [],
- competenceColorByServiceKey: new Map(),
- usesCompetenceColoring: isGroupementCommuneView(territoryMeta),
- preparedViewportKey: null,
- maskedData: {
- communes: [],
- units: [],
- communeByCode: new Map(),
- },
- };
- raw.features = raw.features.map((feature) => ({
- type: "Feature",
- properties: {
- code: feature.properties.c,
- nom: feature.properties.n,
- codeDepartement: feature.properties.d,
- codeRegion: feature.properties.r,
- kind: feature.properties.k || territoryMeta.entityType,
- territoryKey,
- childTerritoryKey: feature.properties.x || null,
- postalCodes: state.communePostalCodesByCode.get(feature.properties.c) || [],
- competenceInstitution: feature.properties.i || "",
- competenceServiceKey: feature.properties.u || "",
- competenceServiceKeys: feature.properties.v || (feature.properties.u ? [feature.properties.u] : []),
- competenceMixedLabel: feature.properties.m || "",
- competenceLabel:
- (feature.properties.u && serviceByKey.get(feature.properties.u)?.l) || feature.properties.m || "",
- },
- geometry: feature.geometry,
- }));
- const rawMasked = raw.masked || {};
- const maskedCommunes = (rawMasked.communes || []).map((commune) => ({
- code: commune.c,
- name: commune.n,
- postalCodes: commune.p || [],
- }));
- const maskedUnits = (rawMasked.units || []).map((unit) => ({
- serviceKey: unit.s,
- institution: unit.i || "UNK",
- name: unit.n || unit.s,
- communeCodes: unit.c || [],
- communeNames: unit.m || [],
- }));
- enhanced.maskedData = {
- communes: maskedCommunes
- .slice()
- .sort((left, right) => compareLabelsAlphabetically(left.name, right.name)),
- units: maskedUnits
- .slice()
- .sort((left, right) => compareLabelsAlphabetically(left.name, right.name)),
- communeByCode: new Map(maskedCommunes.map((commune) => [commune.code, commune])),
- };
- enhanced.unitByServiceKey = new Map(enhanced.units.map((unit) => [unit.serviceKey, unit]));
- enhanced.unitSearchEntries = prepareUnitSearchEntries(enhanced.units, territoryKey);
- state.territoryDataByKey.set(territoryKey, enhanced);
- return enhanced;
- }
- function syncZoomTransform(transform) {
- d3.select(mapCanvas).call(zoom.transform, transform);
- }
- function zoomToPreparedFeature(prepared) {
- if (!prepared) {
- return;
- }
- const [[x0, y0], [x1, y1]] = prepared.bounds;
- const dx = Math.max(1, x1 - x0);
- const dy = Math.max(1, y1 - y0);
- const scale = Math.max(
- 1,
- Math.min(48, 0.82 / Math.max(dx / state.width, dy / state.height))
- );
- const translateX = state.width / 2 - scale * (x0 + x1) / 2;
- const translateY = state.height / 2 - scale * (y0 + y1) / 2;
- syncZoomTransform(d3.zoomIdentity.translate(translateX, translateY).scale(scale));
- }
- function zoomToPreparedUnit(unit) {
- if (!unit) {
- return;
- }
- const scale = UNIT_FOCUS_SCALE;
- const translateX = state.width / 2 - scale * unit.worldX;
- const translateY = state.height / 2 - scale * unit.worldY;
- syncZoomTransform(d3.zoomIdentity.translate(translateX, translateY).scale(scale));
- }
- function selectUnit(unit, options = {}) {
- const preparedUnit = getPreparedUnitByKey(unit?.key || unit?.serviceKey) || unit;
- if (!preparedUnit) {
- return;
- }
- state.selectedCommuneId = null;
- state.selectedPartKey = null;
- state.selectedUnitKey = preparedUnit.key;
- if (options.zoom) {
- zoomToPreparedUnit(preparedUnit);
- }
- requestRender();
- if (options.openModal) {
- openUnitModal(preparedUnit);
- }
- if (options.syncRoute !== false) {
- syncRouteFromState({ replace: Boolean(options.replaceRoute) });
- }
- }
- function updateSelection(communeId, options = {}) {
- state.selectedCommuneId = communeId;
- state.selectedPartKey = options.partKey || null;
- state.selectedUnitKey = null;
- if (communeId) {
- const preparedFeature = getPreparedFeatureById(communeId);
- if (preparedFeature && !state.selectedPartKey) {
- state.selectedPartKey = preparedFeature.primaryPartKey;
- }
- const preparedTarget =
- options.zoomTarget === "part"
- ? getPreparedPartByKey(state.selectedPartKey)
- : preparedFeature;
- if (preparedTarget && options.zoom !== false) {
- zoomToPreparedFeature(preparedTarget);
- }
- if (preparedFeature && options.openModal) {
- openCommuneModal(preparedFeature.feature);
- }
- } else {
- state.selectedPartKey = null;
- }
- renderCommuneResults(communeSearchInputDesktop.value);
- requestRender();
- if (options.syncRoute !== false) {
- syncRouteFromState({ replace: Boolean(options.replaceRoute) });
- }
- }
- async function activateTerritory(territoryKey, options = {}) {
- const sameTerritory = territoryKey === state.activeTerritoryKey;
- const territoryMeta = getTerritoryMeta(territoryKey);
- const territoryData = await loadTerritoryData(territoryKey);
- const viewportKey = `${state.width}x${state.height}`;
- const needsPreparation =
- territoryData.preparedViewportKey !== viewportKey || !territoryData.preparedFeatures?.length;
- if (needsPreparation) {
- setStatus(`Préparation de ${territoryMeta?.label || territoryKey}...`);
- await waitForNextPaint();
- await new Promise((resolve) => setTimeout(resolve, 0));
- }
- if (!sameTerritory) {
- closeCommuneModal({ syncRoute: false, clearSelection: false });
- closeMaskingModal();
- }
- state.activeTerritoryKey = territoryKey;
- state.activeTerritoryData = territoryData;
- prepareTerritoryForCurrentViewport(territoryData);
- state.hoveredPartKey = null;
- state.hoveredUnitKey = null;
- hideTooltip();
- applyEntityUi();
- updateTerritorySummary();
- updateMaskingDisclaimer();
- renderTerritoryList();
- if (!sameTerritory) {
- clearSearchInputs();
- state.selectedUnitKey = null;
- syncZoomTransform(d3.zoomIdentity);
- }
- if (needsPreparation) {
- await waitForNextPaint();
- }
- setStatus("", false);
- if (options.focusCommuneId) {
- updateSelection(options.focusCommuneId, {
- zoom: options.zoom !== false,
- openModal: Boolean(options.openModal),
- syncRoute: options.syncRoute,
- replaceRoute: options.replaceRoute,
- });
- } else if (!sameTerritory) {
- updateSelection(null, { zoom: false, syncRoute: false });
- if (options.syncRoute !== false) {
- syncRouteFromState({ replace: Boolean(options.replaceRoute) });
- }
- } else {
- requestRender();
- if (options.syncRoute !== false) {
- syncRouteFromState({ replace: Boolean(options.replaceRoute) });
- }
- }
- }
- async function applyRouteFromHash(hash, options = {}) {
- const requestedHash = String(hash || "");
- const parsedRoute = parseRouteHash(requestedHash);
- let territoryKey = parsedRoute.territoryKey || "france";
- let shouldReplaceRoute = Boolean(options.replace) || !parsedRoute.valid;
- if (!state.territoryByKey.has(territoryKey)) {
- territoryKey = "france";
- shouldReplaceRoute = true;
- }
- state.routeApplying = true;
- try {
- await activateTerritory(territoryKey, { syncRoute: false });
- if (parsedRoute.unitKey) {
- const unit = state.activeTerritoryData?.unitByServiceKey?.get(parsedRoute.unitKey);
- if (unit) {
- selectUnit(unit, { zoom: false, openModal: true, syncRoute: false });
- } else {
- closeCommuneModal({ syncRoute: false });
- shouldReplaceRoute = true;
- }
- } else if (parsedRoute.entityCode) {
- const preparedFeature = getPreparedFeatureById(parsedRoute.entityCode);
- if (preparedFeature) {
- updateSelection(parsedRoute.entityCode, {
- zoom: false,
- openModal: true,
- syncRoute: false,
- });
- } else {
- closeCommuneModal({ syncRoute: false });
- shouldReplaceRoute = true;
- }
- } else {
- closeCommuneModal({ syncRoute: false });
- }
- } finally {
- state.routeApplying = false;
- }
- const finalHash = buildRouteHash(getCurrentRouteState());
- if (shouldReplaceRoute || finalHash !== requestedHash) {
- history.replaceState(null, "", buildRouteUrl(finalHash));
- }
- state.routeHash = finalHash;
- }
- async function handleBrowserRouteChange() {
- const nextHash = window.location.hash || "";
- if (nextHash === state.routeHash) {
- return;
- }
- await applyRouteFromHash(nextHash, { replace: !nextHash });
- }
- function getPreparedFeatureFromPointer(event) {
- if (state.hitDirty) {
- drawHitLayer();
- state.hitDirty = false;
- }
- const { localX, localY, canvasX, canvasY } = getPointerPositions(event);
- if (canvasX < 0 || canvasY < 0 || canvasX >= hitCanvas.width || canvasY >= hitCanvas.height) {
- return null;
- }
- const sampleOffsets = [
- [0, 0],
- [1, 0],
- [-1, 0],
- [0, 1],
- [0, -1],
- [1, 1],
- [1, -1],
- [-1, 1],
- [-1, -1],
- ];
- const testedKeys = new Set();
- for (const [offsetX, offsetY] of sampleOffsets) {
- const sampleX = canvasX + offsetX;
- const sampleY = canvasY + offsetY;
- if (sampleX < 0 || sampleY < 0 || sampleX >= hitCanvas.width || sampleY >= hitCanvas.height) {
- continue;
- }
- const pixel = hitContext.getImageData(sampleX, sampleY, 1, 1).data;
- const hitKey = state.activeTerritoryData?.partKeyByHitColor?.get(getHitColorKey(pixel)) || null;
- if (!hitKey || testedKeys.has(hitKey)) {
- continue;
- }
- testedKeys.add(hitKey);
- const preparedPart = getPreparedPartByKey(hitKey);
- if (isPreparedPartUnderPointer(preparedPart, localX, localY)) {
- return preparedPart;
- }
- }
- return null;
- }
- function handlePointerMove(event) {
- if (!state.activeTerritoryData) {
- return;
- }
- const preparedUnit = getPreparedUnitFromPointer(event);
- if (preparedUnit) {
- const nextHoverUnitKey = preparedUnit.key || null;
- const needsRender =
- nextHoverUnitKey !== state.hoveredUnitKey || state.hoveredPartKey !== null;
- state.hoveredUnitKey = nextHoverUnitKey;
- state.hoveredPartKey = null;
- if (needsRender) {
- requestRender();
- }
- showTooltip(preparedUnit.name, event.clientX, event.clientY);
- return;
- }
- const preparedPart = getPreparedFeatureFromPointer(event);
- const nextHoverPartKey = preparedPart?.key || null;
- const nextHoverUnitKey = null;
- if (nextHoverPartKey !== state.hoveredPartKey || nextHoverUnitKey !== state.hoveredUnitKey) {
- state.hoveredPartKey = nextHoverPartKey;
- state.hoveredUnitKey = nextHoverUnitKey;
- requestRender();
- }
- if (preparedPart) {
- showTooltip(preparedPart.feature, event.clientX, event.clientY);
- } else {
- hideTooltip();
- }
- }
- function handlePointerLeave() {
- state.hoveredPartKey = null;
- state.hoveredUnitKey = null;
- hideTooltip();
- requestRender();
- }
- function handleMapClick(event) {
- const preparedUnit = getPreparedUnitFromPointer(event);
- if (preparedUnit) {
- state.hoveredPartKey = null;
- selectUnit(preparedUnit, { zoom: false, openModal: true });
- return;
- }
- const preparedPart = getPreparedFeatureFromPointer(event);
- if (!preparedPart) {
- return;
- }
- updateSelection(preparedPart.featureId, {
- partKey: preparedPart.key,
- zoom: true,
- zoomTarget: "part",
- openModal: true,
- });
- }
- function handleSearchInput(event) {
- const value = event.target.value;
- communeSearchInputDesktop.value = value;
- communeSearchInputMobile.value = value;
- renderCommuneResults(value);
- }
- async function bootstrap() {
- try {
- setStatus("Chargement des territoires...");
- const [territories, communesIndex] = await Promise.all([
- fetchJson(TERRITORIES_URL),
- fetchJson(COMMUNE_INDEX_URL),
- ]);
- state.territories = territories;
- state.territoryByKey = new Map(territories.map((territory) => [territory.key, territory]));
- state.communesIndex = communesIndex;
- state.communePostalCodesByCode = new Map(
- communesIndex
- .filter((entry) => entry.k === COMMUNE_ENTITY_TYPE)
- .map((entry) => [entry.c, entry.p || []])
- );
- state.searchEntries = prepareSearchEntries(communesIndex);
- resizeCanvases();
- await applyRouteFromHash(window.location.hash, { replace: !window.location.hash });
- renderCommuneResults("");
- setStatus("", false);
- } catch (error) {
- console.error(error);
- setStatus(
- "Impossible de charger la carte. Exécutez d'abord le script de génération des données puis servez le dossier en HTTP."
- );
- }
- }
- window.addEventListener("resize", () => {
- window.clearTimeout(state.resizeTimer);
- state.resizeTimer = window.setTimeout(resizeCanvases, 120);
- });
- window.addEventListener("popstate", () => {
- handleBrowserRouteChange().catch((error) => console.error(error));
- });
- window.addEventListener("hashchange", () => {
- handleBrowserRouteChange().catch((error) => console.error(error));
- });
- mapCanvas.addEventListener("pointermove", handlePointerMove);
- mapCanvas.addEventListener("pointerleave", handlePointerLeave);
- mapCanvas.addEventListener("click", handleMapClick);
- communeSearchInputDesktop.addEventListener("input", handleSearchInput);
- communeSearchInputMobile.addEventListener("input", handleSearchInput);
- openMobileCommuneListButton.addEventListener("click", openCommuneListModal);
- closeCommuneListModalButton.addEventListener("click", closeCommuneListModal);
- closeModalButton.addEventListener("click", () => closeCommuneModal());
- openMaskingModalButton.addEventListener("click", () => openMaskingModal());
- closeMaskingModalButton.addEventListener("click", closeMaskingModal);
- territoryToggleButton.addEventListener("click", () => {
- const nextExpanded = !territoryPanel.classList.contains("mobile-open");
- territoryPanel.classList.toggle("mobile-open", nextExpanded);
- territoryToggleButton.setAttribute("aria-expanded", String(nextExpanded));
- });
- communeListModal.addEventListener("click", (event) => {
- if (event.target === communeListModal) {
- closeCommuneListModal();
- }
- });
- communeModal.addEventListener("click", (event) => {
- if (event.target === communeModal) {
- closeCommuneModal();
- }
- });
- maskingModal.addEventListener("click", (event) => {
- if (event.target === maskingModal) {
- closeMaskingModal();
- }
- });
- document.addEventListener("keydown", (event) => {
- if (event.key === "Escape") {
- closeCommuneModal();
- closeCommuneListModal();
- closeMaskingModal();
- territoryPanel.classList.remove("mobile-open");
- territoryToggleButton.setAttribute("aria-expanded", "false");
- }
- });
- bootstrap();
|