| |
1 | +# Ka Maps — framework cartographique de Groupe Ka |
| |
2 | + |
| |
3 | +**Auteur : Simon-Pierre Boucher — contact@spboucher.ai** |
| |
4 | + |
| |
5 | +Ka Maps est le moteur géographique partagé des applications Groupe Ka. |
| |
6 | +Implémenté une fois ici, consommé par **Lou-Ka**, **Immo-Ka** et |
| |
7 | +**Vrai-Prix** — chaque app n'apporte que son **thème** et son |
| |
8 | +**adaptateur de données**. |
| |
9 | + |
| |
10 | +``` |
| |
11 | +Lou-Ka / Immo-Ka / Vrai-Prix (frontends) |
| |
12 | + │ thème + adaptateur + configuration |
| |
13 | + ▼ |
| |
14 | + Ka Maps ← ce dépôt (~/Desktop/ka-maps) |
| |
15 | + │ |
| |
16 | + ▼ |
| |
17 | + Mapbox GL JS v3 — style Mapbox Standard (3D) |
| |
18 | +``` |
| |
19 | + |
| |
20 | +## Empaquetage (décision d'architecture) |
| |
21 | + |
| |
22 | +Trois dépôts séparés → paquet local `@groupe-ka/ka-maps` : |
| |
23 | + |
| |
24 | +| App | Consommation | Pourquoi | |
| |
25 | +|---|---|---| |
| |
26 | +| Lou-Ka (`lou-ka/frontend`) | `file:../../ka-maps` (lien) + `resolve.dedupe` Vite | itération à chaud | |
| |
27 | +| Immo-Ka (`agent-courtage/frontend`) | idem | idem | |
| |
28 | +| Vrai-Prix (Next 16) | `file:../../ka-maps/groupe-ka-ka-maps-0.1.0.tgz` | Turbopack ne résout pas les liens hors racine | |
| |
29 | + |
| |
30 | +Après toute modification du framework : |
| |
31 | +`npm run build` (les apps Vite la voient immédiatement) puis |
| |
32 | +`npm run pack:tarball` + `npm install` dans vrai-prix. |
| |
33 | + |
| |
34 | +⚠️ Les apps Vite doivent déduper `react`, `react-dom`, `mapbox-gl` |
| |
35 | +(`resolve.dedupe`) et pointer `paths` tsconfig vers **leurs** |
| |
36 | +`@types/react` — sinon double React (crash hooks) et double moteur GL. |
| |
37 | + |
| |
38 | +## Architecture des sources |
| |
39 | + |
| |
40 | +``` |
| |
41 | +src/ |
| |
42 | + types/ MapProperty, KaDataAdapter, BBox, KaMapState, |
| |
43 | + GeographicMarketSummary, KaLensStats, HeatmapMetric… |
| |
44 | + core/ KaMap (moteur), KaEventHub (événements centralisés) |
| |
45 | + layers/ propertyLayer (pastilles + grappes), registry (couches par app) |
| |
46 | + services/ BoundsQueryScheduler (debounce, AbortController, cache LRU, |
| |
47 | + livraison monotone — jamais une réponse périmée) |
| |
48 | + styles/ kaBaseStyle (Mapbox Standard + réglages immobiliers), ka-maps.css |
| |
49 | + theming/ KaMapTheme (jetons par app), palettes |
| |
50 | + utils/ format (prix fr-CA), geo (bbox, haversine, GeoJSON), |
| |
51 | + url (caméra partageable), lens (Ka Lens) |
| |
52 | + react/ KaMapView, useKaMap, SearchAreaControl, PropertyPreview, |
| |
53 | + ResultCount, LoadingIndicator, LocateControl, Tilt3DControl, |
| |
54 | + KaBrandBadge |
| |
55 | +``` |
| |
56 | + |
| |
57 | +## Le contrat d'intégration (ajouter une app Groupe Ka) |
| |
58 | + |
| |
59 | +1. **Adaptateur** — comment vos données deviennent des `MapProperty` : |
| |
60 | + |
| |
61 | +```ts |
| |
62 | +const monAdapter: KaDataAdapter = { |
| |
63 | + id: "mon-app-items", |
| |
64 | + appSource: "mon-app", |
| |
65 | + async fetchInBounds({ bbox, zoom, filters, signal }) { |
| |
66 | + const res = await fetch(`/api/…?bbox=${bboxToString(bbox)}`, { signal }); |
| |
67 | + return { properties: (await res.json()).map(toMapProperty), totalCount }; |
| |
68 | + }, |
| |
69 | +}; |
| |
70 | +``` |
| |
71 | + |
| |
72 | +2. **Thème** — `KaMapTheme` : accent, familles de pastilles |
| |
73 | + (`sale`/`rent`/`valuation`/`highlight` × normal/sélection), grappe. |
| |
74 | +3. **Montage** : |
| |
75 | + |
| |
76 | +```tsx |
| |
77 | +<KaMapView theme={theme} adapter={adapter} mapboxToken={TOKEN} |
| |
78 | + filters={filters} pitch={50} cluster={{ maxZoom: 15 }}> |
| |
79 | + <KaBrandBadge /> <Tilt3DControl /> <SearchAreaControl /> |
| |
80 | + <LoadingIndicator /> <ResultCount /> |
| |
81 | + <PropertyPreview render={(p) => <MaCarte p={p} />} /> |
| |
82 | +</KaMapView> |
| |
83 | +``` |
| |
84 | + |
| |
85 | +4. **Jetons CSS** — sur `.ka-map` : `--ka-accent`, `--ka-surface`, |
| |
86 | + `--ka-ink`, `--ka-line`, `--ka-radius`, `--ka-shadow`, `--ka-font`. |
| |
87 | + |
| |
88 | +## Rendu des propriétés |
| |
89 | + |
| |
90 | +- **Aucun marqueur DOM** : source GeoJSON + couches symbole/cercle GPU — |
| |
91 | + tenue à 100 000+ points. |
| |
92 | +- **Pastilles de prix** : images canvas 9-slice étirables par famille × |
| |
93 | + état (le 9-slice n'est pas supporté sur les icônes SDF). `icon-image` |
| |
94 | + est une propriété *layout* (feature-state interdit) : la sélection est |
| |
95 | + réinjectée par `setLayoutProperty`. |
| |
96 | +- **Grappes** : clustering natif, compte + **valeur moyenne indicative** |
| |
97 | + (`≈`) via `clusterProperties` (somme/nombre) — `valueClamp` borne la |
| |
98 | + contribution de chaque point (un prix aberrant ne pollue pas la bulle). |
| |
99 | + La médiane exacte n'est pas réductible par supercluster ; elle est |
| |
100 | + disponible côté Ka Lens. |
| |
101 | +- **États** : `hovered`/`selected`/`dimmed` par feature-state (peinture). |
| |
102 | + |
| |
103 | +## Pièges connus du moteur (payés une fois, documentés ici) |
| |
104 | + |
| |
105 | +- **Polices** : les couches symbole doivent utiliser des polices du |
| |
106 | + serveur de glyphes Mapbox (`DIN Pro …`, `Arial Unicode MS …`). Une |
| |
107 | + police inconnue fait échouer le parsing des tuiles → **source vide, |
| |
108 | + silencieuse**. |
| |
109 | +- **Style à imports (Standard)** : installer les couches sur `load`, |
| |
110 | + config basemap ensuite ; auto-réparation (vérification + reconstruction |
| |
111 | + avec id de source rotatif) intégrée à `KaMap`. |
| |
112 | +- **`clusterMaxZoom` entier** obligatoire (sinon `Invalid array length` |
| |
113 | + dans le worker) — arrondi par `KaMap`. |
| |
114 | +- **`promoteId: undefined`** rejeté par la validation Mapbox. |
| |
115 | +- `getClusterExpansionZoom` est à **callback** (pas une promesse). |
| |
116 | + |
| |
117 | +## Fond de carte |
| |
118 | + |
| |
119 | +`KA_STYLE_URL` = `mapbox://styles/mapbox/standard` + |
| |
120 | +`applyKaBasemapConfig` : thème `faded` (les prix dominent), POI/transit |
| |
121 | +masqués, `lightPreset` day/dusk = Ka Light/Ka Dark (`setMode`, sans |
| |
122 | +rechargement de style). 3D native (bâtiments, repères) ; inclinaison par |
| |
123 | +défaut 50°, contrôle `Tilt3DControl` (2D/3D). Jeton public `pk.…` fourni |
| |
124 | +par l'app (`mapboxToken`) — jamais de secret serveur dans le navigateur. |
| |
125 | +Attribution Mapbox/OSM repliée en ⓘ (jamais retirée), logo conservé. |
| |
126 | + |
| |
127 | +## Synchronisation carte ↔ résultats |
| |
128 | + |
| |
129 | +`BoundsQueryScheduler` : `moveend` → debounce → adaptateur → rendu. |
| |
130 | +Mode `manual` : le viewport divergent affiche « Rechercher dans cette |
| |
131 | +zone » (tolérance 15 %) ; mode `auto` : requête à chaque déplacement |
| |
132 | +posé. Annulation `AbortController`, cache LRU (clé bbox+zoom+filtres), |
| |
133 | +livraison strictement monotone. |
| |
134 | + |
| |
135 | +## Couches, agrégats, Ka Lens (fondations) |
| |
136 | + |
| |
137 | +- `layers/registry.ts` : vocabulaire complet (PROPERTY/MARKET/LAND/ |
| |
138 | + LIFESTYLE/INVESTMENT) ; `buildLayerRegistry` marque ce que chaque app |
| |
139 | + supporte — rien d'autre n'est exposé en production. |
| |
140 | +- `GeographicMarketSummary` : contrat des agrégats par géographie |
| |
141 | + (province → quartier) pour Ka Market Pulse — les API restent à |
| |
142 | + implémenter par app (aucune valeur fictive). |
| |
143 | +- `utils/lens.ts` : `computeLensStats` (médianes, mix de types, parts de |
| |
144 | + baisses/90 j+) sur les propriétés réellement chargées ; |
| |
145 | + `propertiesInBBox` / `propertiesInPolygon` (ray casting) pour la |
| |
146 | + sélection Ka Lens ; `KaMap.setDimmedExcept` atténue le reste. |
| |
147 | +- `HeatmapMetric` : contrat de la future infra heatmap. |
| |
148 | + |
| |
149 | +## Évolution serveur prévue (architecturé, non déployé) |
| |
150 | + |
| |
151 | +``` |
| |
152 | +SQLite (bbox + index (lat,lng)) ← aujourd'hui, les 3 apps |
| |
153 | +PostgreSQL + PostGIS (GIST, ST_Intersects) ← quand la volumétrie l'exige |
| |
154 | +Martin → tuiles vectorielles MVT → CDN ← /api/map/…/{z}/{x}/{y}.pbf |
| |
155 | +``` |
| |
156 | + |
| |
157 | +Le client est prêt : remplacer la source GeoJSON par une source |
| |
158 | +`vector` + `source-layer` ne touche ni les couches ni les apps. |
| |
159 | + |
| |
160 | +## Commandes |
| |
161 | + |
| |
162 | +`npm run build` · `npm run typecheck` · `npm test` (30 tests : formats |
| |
163 | +fr-CA, bbox/URL, scheduler — debounce/annulation/cache/monotonie —, Ka |
| |
164 | +Lens) · `npm run pack:tarball`. |