@@ -11,6 +11,14 @@ import { Link, useNavigate, useSearchParams } from "react-router-dom"
1111import { Button } from "@/components/ui/button"
1212import { DeckOverlay } from "@/components/deck-overlay"
1313import { MapTopBar } from "@/components/map-top-bar"
14+ import {
15+ BASEMAP_KEYS ,
16+ BASEMAP_LABELS ,
17+ BASEMAP_STYLES ,
18+ isBasemap ,
19+ syncBasemapStyle ,
20+ type Basemap ,
21+ } from "@/lib/basemaps"
1422import { useChoropleth } from "@/hooks/useChoropleth"
1523import {
1624 HIGH_ZOOM_THRESHOLD ,
@@ -72,7 +80,7 @@ const POINTS_ZOOM = 11
7280const HEAT_PRICE_DOMAIN : [ number , number ] = [ 1500 , 6000 ]
7381
7482// --- État dans l'URL (partage/refresh, audit N4) ----------------------------
75- // v=lat,lng,zoom ; repr= ; type= ; poids= ; iso=1
83+ // v=lat,lng,zoom ; repr= ; type= ; poids= ; iso=1 ; fond= (commun avec /map)
7684
7785function parseViewParam ( v : string | null ) : MapViewState | null {
7886 if ( ! v ) return null
@@ -119,6 +127,10 @@ export default function DvfMap() {
119127 params . get ( "poids" ) === "ventes" ? "ventes" : "prix" ,
120128 )
121129 const [ contours , setContours ] = useState ( ( ) => params . get ( "iso" ) === "1" )
130+ const [ basemap , setBasemap ] = useState < Basemap > ( ( ) => {
131+ const f = params . get ( "fond" )
132+ return isBasemap ( f ) ? f : "clair"
133+ } )
122134
123135 // Filtres du store initialisés depuis l'URL (une fois, au montage).
124136 useEffect ( ( ) => {
@@ -151,8 +163,9 @@ export default function DvfMap() {
151163 if ( heatWeight !== "prix" ) next . set ( "poids" , heatWeight )
152164 if ( contours ) next . set ( "iso" , "1" )
153165 }
166+ if ( basemap !== "clair" ) next . set ( "fond" , basemap )
154167 setParams ( next , { replace : true } )
155- } , [ debouncedView , representation , typeLocal , heatWeight , contours , setParams ] )
168+ } , [ debouncedView , representation , typeLocal , heatWeight , contours , basemap , setParams ] )
156169
157170 // Dézoom manuel : le fil d'Ariane se tronque au niveau réellement visible.
158171 useEffect ( ( ) => {
@@ -452,12 +465,16 @@ export default function DvfMap() {
452465 fillBeforeId ,
453466 ] )
454467
455- // Au (re)chargement du style, mémorise le 1er calque de symboles : c'est le
456- // beforeId sous lequel glisser les couches deck (labels au-dessus). Idempotent
457- // (même id -> pas de re-render), style Positron unique sur cette page.
458- const onStyleData = ( map : MaplibreMap ) => {
459- const symbol = ( map . getStyle ( ) ?. layers ?? [ ] ) . find ( ( l ) => l . type === "symbol" )
460- setFillBeforeId ( symbol ?. id )
468+ // À chaque (re)chargement de style — initial OU changement de fond — labels FR
469+ // + mémorisation du 1er calque de symboles (beforeId), cf. lib/basemaps.
470+ const onStyleData = ( map : MaplibreMap ) => setFillBeforeId ( syncBasemapStyle ( map ) )
471+
472+ // Changement de fond : détacher le beforeId dans le même commit — l'ancien id
473+ // n'existe pas (encore) dans le style suivant, deck ré-ajouterait les couches
474+ // sous un calque fantôme ; le styledata du nouveau style le re-fournit.
475+ const changeBasemap = ( b : Basemap ) => {
476+ setFillBeforeId ( undefined )
477+ setBasemap ( b )
461478 }
462479
463480 // Remplace le getCursor de DeckGL : pointer au survol d'une entité pickable,
@@ -476,7 +493,9 @@ export default function DvfMap() {
476493 < MapTopBar
477494 extra = {
478495 < Button variant = "ghost" size = "sm" asChild className = "max-md:hidden" >
479- < Link to = { `/map?v=${ viewParam ( debouncedView ) } ` } >
496+ < Link
497+ to = { `/map?v=${ viewParam ( debouncedView ) } ${ basemap !== "clair" ? `&fond=${ basemap } ` : "" } ` }
498+ >
480499 Voir en qualité de vie
481500 </ Link >
482501 </ Button >
@@ -485,7 +504,10 @@ export default function DvfMap() {
485504 < Map
486505 ref = { mapRef }
487506 initialViewState = { initialView }
488- mapStyle = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
507+ mapStyle = { BASEMAP_STYLES [ basemap ] }
508+ // Rechargement complet au changement de fond (les styles Carto partagent
509+ // les mêmes ids ; le diff laisserait des libellés anglais résiduels).
510+ styleDiffing = { false }
489511 onMove = { ( e ) => setViewState ( e . viewState ) }
490512 onStyleData = { ( e ) => onStyleData ( e . target ) }
491513 style = { { width : "100%" , height : "100%" } }
@@ -594,6 +616,25 @@ export default function DvfMap() {
594616 </ div >
595617 ) }
596618
619+ { /* Fond de carte (fond= dans l'URL, commun avec /map). */ }
620+ < div className = "mt-2 flex flex-wrap items-center gap-1.5 border-t pt-2 text-xs" >
621+ < span className = "text-muted-foreground" > Fond</ span >
622+ { BASEMAP_KEYS . map ( ( b ) => (
623+ < button
624+ key = { b }
625+ type = "button"
626+ onClick = { ( ) => changeBasemap ( b ) }
627+ className = {
628+ b === basemap
629+ ? "rounded border border-accent bg-accent/10 px-2 py-0.5 font-semibold text-accent"
630+ : "rounded border border-input px-2 py-0.5 text-muted-foreground hover:bg-muted"
631+ }
632+ >
633+ { BASEMAP_LABELS [ b ] }
634+ </ button >
635+ ) ) }
636+ </ div >
637+
597638 { ! heat && mesh !== "communes" && (
598639 < p className = "mt-2 max-w-52 text-xs text-muted-foreground" >
599640 Cliquez sur une { mesh === "regions" ? "région" : "département" } pour
0 commit comments