SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.0 KB · 109 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/MapInner.tsx : partie Mapbox de la carte de la fiche (chargée5//   paresseusement) — KaSpotlightMap (Ka Maps) + lignes de métro + couche des6//   lieux filtrés (cercles colorés par catégorie + étiquettes) + caméra qui7//   englobe les lieux affichés.8// -----------------------------------------------------------------------------9import { useEffect, useMemo } from "react";10import "mapbox-gl/dist/mapbox-gl.css";11import "@groupe-ka/ka-maps/styles.css";12import type { MapProperty } from "@groupe-ka/ka-maps";13import { KaBrandBadge, KaSpotlightMap, useKaMap } from "@groupe-ka/ka-maps/react";14import type mapboxgl from "mapbox-gl";15import type { Listing } from "../api";16import MetroLignes from "../components/MetroLignes";17import { louKaMapTheme } from "../kamaps/theme";18import { MAPBOX_TOKEN } from "../kamaps/config";19import type { Categorie, Lieu } from "./InteractiveMap";2021const SRC = "lk-lieux";22const ORANGE = "#ff6a00";2324function LieuxLayer({ lieux, categories, center }: { lieux: Lieu[]; categories: Categorie[]; center: [number, number] }) {25  const ka = useKaMap();26  const couleurs = useMemo(() => {27    const m: unknown[] = ["match", ["get", "cat"]];28    for (const c of categories) m.push(c.key, c.color);29    m.push("#666");30    return m;31  }, [categories]);3233  useEffect(() => {34    if (!ka) return;35    const map = (ka as unknown as { map: mapboxgl.Map }).map;36    if (!map) return;37    const data = {38      type: "FeatureCollection" as const,39      features: lieux.map((x) => ({40        type: "Feature" as const, properties: { cat: x.cat, name: x.name },41        geometry: { type: "Point" as const, coordinates: [x.lng, x.lat] },42      })),43    };44    const ensure = () => {45      const src = map.getSource(SRC) as mapboxgl.GeoJSONSource | undefined;46      if (src) { src.setData(data); return; }47      map.addSource(SRC, { type: "geojson", data });48      map.addLayer({49        id: `${SRC}-halo`, type: "circle", source: SRC,50        paint: { "circle-radius": 9, "circle-color": "#ffffff", "circle-opacity": 0.95 },51      });52      map.addLayer({53        id: `${SRC}-dot`, type: "circle", source: SRC,54        paint: { "circle-radius": 6, "circle-color": couleurs as mapboxgl.ExpressionSpecification },55      });56      map.addLayer({57        id: `${SRC}-lbl`, type: "symbol", source: SRC,58        layout: {59          "text-field": ["get", "name"], "text-size": 11, "text-offset": [0, 1.1], "text-anchor": "top",60          "text-font": ["DIN Pro Medium", "Arial Unicode MS Regular"], "text-optional": true,61        },62        paint: { "text-color": "#141814", "text-halo-color": "#ffffff", "text-halo-width": 1.4 },63      });64    };65    const apply = () => { try { ensure(); } catch { /* style pas prêt */ } };66    if (map.isStyleLoaded()) apply();67    map.on("style.load", apply);68    map.on("load", apply);69    // caméra : englober les lieux + l'immeuble ; sans lieu → retour sur l'immeuble70    if (lieux.length > 0) {71      let w = center[0], e = center[0], s = center[1], n = center[1];72      const proches = lieux.filter((x) => x.dist_m <= 2000);73      for (const x of proches.length ? proches : lieux) { w = Math.min(w, x.lng); e = Math.max(e, x.lng); s = Math.min(s, x.lat); n = Math.max(n, x.lat); }74      map.fitBounds([[w, s], [e, n]], { padding: { top: 60, bottom: 40, left: 40, right: 40 }, pitch: 30, maxZoom: 16, duration: 700 });75    } else {76      map.easeTo({ center, zoom: 17, pitch: 62, duration: 700 });77    }78    return () => { map.off("style.load", apply); map.off("load", apply); };79  }, [ka, lieux, couleurs, center]);8081  useEffect(() => () => {82    if (!ka) return;83    const map = (ka as unknown as { map: mapboxgl.Map }).map;84    try {85      for (const id of [`${SRC}-lbl`, `${SRC}-dot`, `${SRC}-halo`]) if (map.getLayer(id)) map.removeLayer(id);86      if (map.getSource(SRC)) map.removeSource(SRC);87    } catch { /* carte détruite */ }88  }, [ka]);89  return null;90}9192export default function MapInner({ l, lieux, categories }: { l: Listing; lieux: Lieu[]; categories: Categorie[] }) {93  const property = useMemo<MapProperty>(() => ({94    id: l.uid, appSource: "lou-ka", latitude: l.lat as number, longitude: l.lng as number,95    kind: "listing", listingType: "rent", price: l.price ?? undefined,96    propertyType: l.unit_type || undefined, address: l.address || l.title || undefined,97    city: l.city || undefined, thumbnailUrl: l.images?.[0],98  }), [l.uid, l.lat, l.lng, l.price, l.unit_type, l.address, l.title, l.city, l.images]);99  const center = useMemo<[number, number]>(() => [l.lng as number, l.lat as number], [l.lat, l.lng]);100101  return (102    <KaSpotlightMap theme={louKaMapTheme} mapboxToken={MAPBOX_TOKEN} property={property} buildingColor={ORANGE}>103      <KaBrandBadge />104      <MetroLignes />105      <LieuxLayer lieux={lieux} categories={categories} center={center} />106    </KaSpotlightMap>107  );108}109