SPB Git forge

spb/ka-maps

Public
6commits 1branches 0releases
448.0 KBsize
maindefault branch
29 days agolast push
TypeScript 87.7% CSS 12.3%
5.2 KB · 163 lines tsx
Raw Blame History
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Groupe Ka / Ka Maps5 *6 * React bindings. <KaMapView> owns the KaMap lifecycle (StrictMode-safe),7 * exposes it through context, and renders app-provided overlays (previews,8 * legends, controls) as normal React children — no HTML-string popups.9 */1011import {12  createContext,13  useContext,14  useEffect,15  useRef,16  useState,17  type CSSProperties,18  type ReactElement,19  type ReactNode,20} from "react";21import { KaMap, type KaMapOptions } from "../core/KaMap.js";22import type { KaMapMode } from "../theming/tokens.js";23import type { MapProperty } from "../types/index.js";2425const KaMapContext = createContext<KaMap | null>(null);2627/** Access the KaMap engine from any child of <KaMapView>. */28export function useKaMap(): KaMap | null {29  return useContext(KaMapContext);30}3132export interface KaMapViewProps33  extends Omit<KaMapOptions, "container" | "mode"> {34  mode?: KaMapMode;35  className?: string;36  style?: CSSProperties;37  /** Static data path (no adapter): render these properties directly. */38  properties?: MapProperty[];39  /** App-owned filter payload forwarded to the adapter. */40  filters?: Record<string, unknown>;41  onSelect?: (property: MapProperty | null, origin: "map" | "app") => void;42  onHover?: (property: MapProperty | null) => void;43  onData?: (count: number, totalCount?: number) => void;44  onLoading?: (loading: boolean) => void;45  onSearchAreaDirty?: (dirty: boolean) => void;46  onMoveEnd?: (47    center: { lat: number; lng: number },48    zoom: number,49    byUser: boolean,50  ) => void;51  /** Outil de dessin : polygone posé/effacé, ou tracé en cours. */52  onDraw?: (polygon: [number, number][] | null, drawing: boolean) => void;53  /** Survol d'un cluster (fourchette de prix) — null à la sortie. */54  onClusterHover?: (55    info: { count: number; min: number | null; max: number | null; x: number; y: number } | null,56  ) => void;57  children?: ReactNode;58}5960export function KaMapView(props: KaMapViewProps): ReactElement {61  const containerRef = useRef<HTMLDivElement | null>(null);62  const [engine, setEngine] = useState<KaMap | null>(null);6364  // Latest callbacks in refs so the engine effect never re-runs for them.65  const callbacks = useRef(props);66  callbacks.current = props;6768  useEffect(() => {69    const container = containerRef.current;70    if (!container) return;7172    const p = callbacks.current;73    const map = new KaMap({74      container,75      theme: p.theme,76      mapboxToken: p.mapboxToken,77      mode: p.mode,78      adapter: p.adapter,79      basemap: p.basemap,80      center: p.center,81      zoom: p.zoom,82      pitch: p.pitch,83      minZoom: p.minZoom,84      maxZoom: p.maxZoom,85      searchMode: p.searchMode,86      query: p.query,87      cooperativeGestures: p.cooperativeGestures,88      navControl: p.navControl,89      cluster: p.cluster,90    });9192    const offs = [93      map.events.on("select", ({ propertyId, origin }) => {94        callbacks.current.onSelect?.(95          propertyId ? map.getProperty(propertyId) ?? null : null,96          origin,97        );98      }),99      map.events.on("hover", ({ propertyId }) => {100        callbacks.current.onHover?.(101          propertyId ? map.getProperty(propertyId) ?? null : null,102        );103      }),104      map.events.on("data", ({ count, totalCount }) =>105        callbacks.current.onData?.(count, totalCount),106      ),107      map.events.on("loading", ({ loading }) =>108        callbacks.current.onLoading?.(loading),109      ),110      map.events.on("searchAreaDirty", ({ dirty }) =>111        callbacks.current.onSearchAreaDirty?.(dirty),112      ),113      map.events.on("moveend", ({ center, zoom, byUser }) =>114        callbacks.current.onMoveEnd?.(center, zoom, byUser),115      ),116      map.events.on("draw", ({ polygon, drawing }) =>117        callbacks.current.onDraw?.(polygon, drawing),118      ),119      map.events.on("clusterHover", ({ info }) =>120        callbacks.current.onClusterHover?.(info),121      ),122    ];123124    setEngine(map);125    return () => {126      for (const off of offs) off();127      setEngine(null);128      map.destroy();129    };130    // The engine is created once per mount; theme/adapter swaps remount.131    // eslint-disable-next-line react-hooks/exhaustive-deps132  }, [props.theme.id, props.adapter?.id]);133134  // Static data path.135  useEffect(() => {136    if (engine && props.properties) engine.setProperties(props.properties);137  }, [engine, props.properties]);138139  // Filters → adapter refetch.140  const filtersKey = props.filters ? JSON.stringify(props.filters) : "";141  useEffect(() => {142    if (engine && props.adapter) engine.setFilters(props.filters);143    // eslint-disable-next-line react-hooks/exhaustive-deps144  }, [engine, filtersKey]);145146  // Light/dark swaps restyle in place.147  useEffect(() => {148    if (engine && props.mode) engine.setMode(props.mode);149  }, [engine, props.mode]);150151  return (152    <div153      className={`ka-map ${props.className ?? ""}`}154      style={{ position: "relative", width: "100%", height: "100%", ...props.style }}155    >156      <div ref={containerRef} className="ka-map-canvas" style={{ position: "absolute", inset: 0 }} />157      <KaMapContext.Provider value={engine}>158        {engine ? props.children : null}159      </KaMapContext.Provider>160    </div>161  );162}163