高级用法

访问底层 MapLibre GL 地图实例,使用 MapLibre GL JS API 的任何功能。

你可以使用 refuseMap hook 访问底层 MapLibre GL 地图实例。

💡 提示:查看MapLibre GL JS 文档获取完整的方法和事件列表。

使用 Ref

访问地图实例最简单的方式。使用 ref 从事件处理器或 effect 中调用地图方法:

MyMapComponent.tsx
123456789101112131415161718
import { Map, type MapRef } from class="tk-string">"@/components/ui/map";
import { useRef } from class="tk-string">"react";

function MyMapComponent() {
  const mapRef = useRef<MapRef>(null);

  const handleFlyTo = () => {
    class=class="tk-string">"tk-comment">// 通过 ref 访问 MapLibre GL 地图实例
    mapRef.current?.flyTo({ center: [-74, 40.7], zoom: 12 });
  };

  return (
    <>
      <button onClick={handleFlyTo}>飞往纽约</button>
      <Map ref={mapRef} center={[-74, 40.7]} zoom={10} />
    </>
  );
}

使用 Hook

对于在 Map 内渲染的子组件,使用 useMap hook 访问地图实例并监听事件:

MapEventListener.tsx
12345678910111213141516171819202122232425
import { Map, useMap } from class="tk-string">"@/components/ui/map";
import { useEffect } from class="tk-string">"react";

class=class="tk-string">"tk-comment">// 对于 Map 内的子组件,使用 useMap hook
function MapEventListener() {
  const { map, isLoaded } = useMap();

  useEffect(() => {
    if (!map || !isLoaded) return;

    const handleClick = (e) => {
      console.log(class="tk-string">"点击位置:", e.lngLat);
    };

    map.on(class="tk-string">"click", handleClick);
    return () => map.off(class="tk-string">"click", handleClick);
  }, [map, isLoaded]);

  return null;
}

class=class="tk-string">"tk-comment">// 用法
<Map center={[-74, 40.7]} zoom={10}>
  <MapEventListener />
</Map>

自定义控件

创建自定义控件来操作地图的倾斜和方向,并监听地图事件显示实时值:

AdvancedUsageExample.tsx
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
class="tk-string">"use client";

import { useEffect, useState } from class="tk-string">"react";
import { Map, useMap } from class="tk-string">"@/components/ui/map";
import { Button } from class="tk-string">"@/components/ui/button";
import { RotateCcw, Mountain } from class="tk-string">"lucide-react";

function MapController() {
  const { map, isLoaded } = useMap();
  const [pitch, setPitch] = useState(0);
  const [bearing, setBearing] = useState(0);

  useEffect(() => {
    if (!map || !isLoaded) return;

    const handleMove = () => {
      setPitch(Math.round(map.getPitch()));
      setBearing(Math.round(map.getBearing()));
    };

    map.on(class="tk-string">"move", handleMove);
    return () => {
      map.off(class="tk-string">"move", handleMove);
    };
  }, [map, isLoaded]);

  const handle3DView = () => {
    map?.easeTo({
      pitch: 60,
      bearing: -20,
      duration: 1000,
    });
  };

  const handleReset = () => {
    map?.easeTo({
      pitch: 0,
      bearing: 0,
      duration: 1000,
    });
  };

  if (!isLoaded) return null;

  return (
    <div className=class="tk-string">"absolute top-3 left-3 z-10 flex flex-col gap-2">
      <div className=class="tk-string">"flex gap-2">
        <Button size=class="tk-string">"sm" variant=class="tk-string">"secondary" onClick={handle3DView}>
          <Mountain className=class="tk-string">"mr-1.5 size-4" />
          3D 视图
        </Button>
        <Button size=class="tk-string">"sm" variant=class="tk-string">"secondary" onClick={handleReset}>
          <RotateCcw className=class="tk-string">"mr-1.5 size-4" />
          重置
        </Button>
      </div>
      <div className=class="tk-string">"bg-background/90 rounded-md border px-3 py-2 font-mono text-xs backdrop-blur">
        <div>倾斜:{pitch}°</div>
        <div>方向:{bearing}°</div>
      </div>
    </div>
  );
}

export function AdvancedUsageExample() {
  return (
    <div className=class="tk-string">"h-[420px] w-full">
      <Map center={[-73.9857, 40.7484]} zoom={15}>
        <MapController />
      </Map>
    </div>
  );
}

自定义 GeoJSON 图层

添加自定义 GeoJSON 数据为图层,支持填充和轮廓样式及悬停交互:

CustomLayer.tsx
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
class="tk-string">"use client";

import { useCallback, useEffect, useState } from class="tk-string">"react";
import { Map, MapControls, useMap } from class="tk-string">"@/components/ui/map";
import { Button } from class="tk-string">"@/components/ui/button";
import { Layers, X } from class="tk-string">"lucide-react";

const geojsonData = {
  type: class="tk-string">"FeatureCollection" as const,
  features: [
    {
      type: class="tk-string">"Feature" as const,
      properties: { name: class="tk-string">"中央公园", type: class="tk-string">"park" },
      geometry: {
        type: class="tk-string">"Polygon" as const,
        coordinates: [[
          [-73.9731, 40.7644],
          [-73.9819, 40.7681],
          [-73.958, 40.8006],
          [-73.9493, 40.7969],
          [-73.9731, 40.7644],
        ]],
      },
    },
  ],
};

function CustomLayer() {
  const { map, isLoaded } = useMap();
  const [isLayerVisible, setIsLayerVisible] = useState(false);
  const [hoveredPark, setHoveredPark] = useState<string | null>(null);

  const addLayers = useCallback(() => {
    if (!map) return;
    if (!map.getSource(class="tk-string">"parks")) {
      map.addSource(class="tk-string">"parks", { type: class="tk-string">"geojson", data: geojsonData });
    }
    if (!map.getLayer(class="tk-string">"parks-fill")) {
      map.addLayer({
        id: class="tk-string">"parks-fill",
        type: class="tk-string">"fill",
        source: class="tk-string">"parks",
        paint: {
          class="tk-string">"fill-color": class="tk-string">"#22c55e",
          class="tk-string">"fill-opacity": 0.4,
        },
        layout: {
          visibility: isLayerVisible ? class="tk-string">"visible" : class="tk-string">"none",
        },
      });
    }
  }, [map, isLayerVisible]);

  useEffect(() => {
    if (!map || !isLoaded) return;
    addLayers();

    const handleMouseMove = (e) => {
      const features = map.queryRenderedFeatures(e.point, {
        layers: [class="tk-string">"parks-fill"],
      });
      if (features.length > 0) {
        setHoveredPark(features[0].properties?.name || null);
      }
    };

    map.on(class="tk-string">"mousemove", class="tk-string">"parks-fill", handleMouseMove);
    return () => {
      map.off(class="tk-string">"mousemove", class="tk-string">"parks-fill", handleMouseMove);
    };
  }, [map, isLoaded, isLayerVisible]);

  const toggleLayer = () => {
    if (!map) return;
    const visibility = isLayerVisible ? class="tk-string">"none" : class="tk-string">"visible";
    map.setLayoutProperty(class="tk-string">"parks-fill", class="tk-string">"visibility", visibility);
    setIsLayerVisible(!isLayerVisible);
  };

  return (
    <>
      <div className=class="tk-string">"absolute top-3 left-3 z-10">
        <Button size=class="tk-string">"sm" variant={isLayerVisible ? class="tk-string">"default" : class="tk-string">"secondary"} onClick={toggleLayer}>
          {isLayerVisible ? <X className=class="tk-string">"mr-1.5 size-4" /> : <Layers className=class="tk-string">"mr-1.5 size-4" />}
          {isLayerVisible ? class="tk-string">"隐藏公园" : class="tk-string">"显示公园"}
        </Button>
      </div>
      {hoveredPark && (
        <div className=class="tk-string">"bg-background/90 absolute bottom-3 left-3 z-10 rounded-md border px-3 py-2 text-sm font-medium backdrop-blur">
          {hoveredPark}
        </div>
      )}
    </>
  );
}

通过图层渲染标记(性能优化)

当需要显示数百或数千个标记时,使用 GeoJSON 图层代替基于 DOM 的MapMarker 组件。 这种方式在 WebGL 画布上渲染标记,性能显著提升。

MarkersLayer.tsx
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
class="tk-string">"use client";

import { useEffect, useState, useId } from class="tk-string">"react";
import { Map, MapPopup, useMap } from class="tk-string">"@/components/ui/map";

function MarkersLayer() {
  const { map, isLoaded } = useMap();
  const id = useId();
  const sourceId = class="tk-string">`markers-source-${id}`;
  const layerId = class="tk-string">`markers-layer-${id}`;
  const [selectedPoint, setSelectedPoint] = useState(null);

  useEffect(() => {
    if (!map || !isLoaded) return;

    map.addSource(sourceId, {
      type: class="tk-string">"geojson",
      data: pointsData, class=class="tk-string">"tk-comment">// 包含 200 个点的 FeatureCollection
    });

    map.addLayer({
      id: layerId,
      type: class="tk-string">"circle",
      source: sourceId,
      paint: {
        class="tk-string">"circle-radius": 6,
        class="tk-string">"circle-color": class="tk-string">"#3b82f6",
        class="tk-string">"circle-stroke-width": 2,
        class="tk-string">"circle-stroke-color": class="tk-string">"#ffffff",
      },
    });

    const handleClick = (e) => {
      if (!e.features?.length) return;
      const feature = e.features[0];
      const coords = feature.geometry.coordinates;
      setSelectedPoint({
        id: feature.properties?.id,
        name: feature.properties?.name,
        coordinates: coords,
      });
    };

    map.on(class="tk-string">"click", layerId, handleClick);
    return () => {
      map.off(class="tk-string">"click", layerId, handleClick);
      if (map.getLayer(layerId)) map.removeLayer(layerId);
      if (map.getSource(sourceId)) map.removeSource(sourceId);
    };
  }, [map, isLoaded]);

  return (
    <>
      {selectedPoint && (
        <MapPopup
          longitude={selectedPoint.coordinates[0]}
          latitude={selectedPoint.coordinates[1]}
          onClose={() => setSelectedPoint(null)}
          closeButton
        >
          <div className=class="tk-string">"min-w-24">
            <p className=class="tk-string">"font-medium">{selectedPoint.name}</p>
          </div>
        </MapPopup>
      )}
    </>
  );
}