React+百度地图高阶封装:hooks版轻量化地图解决方案

文章目录

    • 每日一句正能量
    • 摘要
    • [1. 背景与痛点分析](#1. 背景与痛点分析)
      • [1.1 传统地图集成的问题](#1.1 传统地图集成的问题)
      • [1.2 Hooks封装的优势](#1.2 Hooks封装的优势)
    • [2. 核心架构设计](#2. 核心架构设计)
      • [2.1 整体架构图](#2.1 整体架构图)
      • [2.2 模块划分](#2.2 模块划分)
    • [3. 核心Hooks实现](#3. 核心Hooks实现)
      • [3.1 useBMapLoader - 地图加载器](#3.1 useBMapLoader - 地图加载器)
      • [3.2 useMapInstance - 地图实例管理](#3.2 useMapInstance - 地图实例管理)
      • [3.3 useMapEvents - 地图事件管理](#3.3 useMapEvents - 地图事件管理)
      • [3.4 useMapOverlays - 覆盖物管理](#3.4 useMapOverlays - 覆盖物管理)
    • [4. 完整示例:地图组件封装](#4. 完整示例:地图组件封装)
      • [4.1 基础地图组件](#4.1 基础地图组件)
      • [4.2 高级地图组件(带控件和工具)](#4.2 高级地图组件(带控件和工具))
      • [4.3 自定义地图控件](#4.3 自定义地图控件)
      • [4.4 地图工具函数](#4.4 地图工具函数)

每日一句正能量

所谓门槛,能力够了就是门,能力不够就是坎。

与其抱怨门槛太高,不如承认那是自己能力的边界;而边界,是可以移动的。

摘要

在React项目中集成地图功能时,传统的组件封装方式往往面临状态管理混乱、生命周期耦合、性能优化困难等问题。本文提出一种基于React Hooks的百度地图高阶封装方案,通过自定义hooks实现地图核心功能的模块化、按需加载和自动清理,为React项目提供一套轻量化、工程化、可维护的地图解决方案。

1. 背景与痛点分析

1.1 传统地图集成的问题

在React项目中集成百度地图时,开发者通常面临以下挑战:

  1. 全局污染问题 :百度地图SDK会向全局注入BMap对象,在多页面应用中容易造成命名冲突
  2. 生命周期管理复杂:地图实例的创建、销毁需要手动管理,容易导致内存泄漏
  3. 状态同步困难:地图状态与React组件状态需要手动同步,代码耦合度高
  4. 性能优化不足:地图资源加载时机难以控制,影响页面首屏加载性能
  5. 代码复用性差:地图相关逻辑分散在各个组件中,难以抽象和复用

1.2 Hooks封装的优势

使用React Hooks封装地图功能具有以下优势:

  • 逻辑复用:将地图相关逻辑抽象为自定义hooks,实现跨组件复用
  • 状态隔离:每个hooks实例拥有独立的状态和作用域
  • 生命周期自动化 :利用useEffect自动管理地图实例的生命周期
  • 按需加载:通过动态导入实现地图SDK的按需加载
  • 类型安全:配合TypeScript提供完整的类型提示

2. 核心架构设计

2.1 整体架构图

#mermaid-svg-OkivnxnJToiTZSjo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OkivnxnJToiTZSjo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OkivnxnJToiTZSjo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OkivnxnJToiTZSjo .error-icon{fill:#552222;}#mermaid-svg-OkivnxnJToiTZSjo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OkivnxnJToiTZSjo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OkivnxnJToiTZSjo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OkivnxnJToiTZSjo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OkivnxnJToiTZSjo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OkivnxnJToiTZSjo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OkivnxnJToiTZSjo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OkivnxnJToiTZSjo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OkivnxnJToiTZSjo .marker.cross{stroke:#333333;}#mermaid-svg-OkivnxnJToiTZSjo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OkivnxnJToiTZSjo p{margin:0;}#mermaid-svg-OkivnxnJToiTZSjo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OkivnxnJToiTZSjo .cluster-label text{fill:#333;}#mermaid-svg-OkivnxnJToiTZSjo .cluster-label span{color:#333;}#mermaid-svg-OkivnxnJToiTZSjo .cluster-label span p{background-color:transparent;}#mermaid-svg-OkivnxnJToiTZSjo .label text,#mermaid-svg-OkivnxnJToiTZSjo span{fill:#333;color:#333;}#mermaid-svg-OkivnxnJToiTZSjo .node rect,#mermaid-svg-OkivnxnJToiTZSjo .node circle,#mermaid-svg-OkivnxnJToiTZSjo .node ellipse,#mermaid-svg-OkivnxnJToiTZSjo .node polygon,#mermaid-svg-OkivnxnJToiTZSjo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OkivnxnJToiTZSjo .rough-node .label text,#mermaid-svg-OkivnxnJToiTZSjo .node .label text,#mermaid-svg-OkivnxnJToiTZSjo .image-shape .label,#mermaid-svg-OkivnxnJToiTZSjo .icon-shape .label{text-anchor:middle;}#mermaid-svg-OkivnxnJToiTZSjo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OkivnxnJToiTZSjo .rough-node .label,#mermaid-svg-OkivnxnJToiTZSjo .node .label,#mermaid-svg-OkivnxnJToiTZSjo .image-shape .label,#mermaid-svg-OkivnxnJToiTZSjo .icon-shape .label{text-align:center;}#mermaid-svg-OkivnxnJToiTZSjo .node.clickable{cursor:pointer;}#mermaid-svg-OkivnxnJToiTZSjo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OkivnxnJToiTZSjo .arrowheadPath{fill:#333333;}#mermaid-svg-OkivnxnJToiTZSjo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OkivnxnJToiTZSjo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OkivnxnJToiTZSjo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OkivnxnJToiTZSjo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OkivnxnJToiTZSjo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OkivnxnJToiTZSjo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OkivnxnJToiTZSjo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OkivnxnJToiTZSjo .cluster text{fill:#333;}#mermaid-svg-OkivnxnJToiTZSjo .cluster span{color:#333;}#mermaid-svg-OkivnxnJToiTZSjo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OkivnxnJToiTZSjo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OkivnxnJToiTZSjo rect.text{fill:none;stroke-width:0;}#mermaid-svg-OkivnxnJToiTZSjo .icon-shape,#mermaid-svg-OkivnxnJToiTZSjo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OkivnxnJToiTZSjo .icon-shape p,#mermaid-svg-OkivnxnJToiTZSjo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OkivnxnJToiTZSjo .icon-shape .label rect,#mermaid-svg-OkivnxnJToiTZSjo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OkivnxnJToiTZSjo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OkivnxnJToiTZSjo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OkivnxnJToiTZSjo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 地图服务层
地图配置服务
坐标转换服务
地理编码服务
路径规划服务
地图Hooks层
useBMapLoader

地图加载器
useMapInstance

地图实例管理
useMapControls

地图控件管理
useMapEvents

地图事件管理
useMapOverlays

地图覆盖物管理
React应用层
百度地图SDK

2.2 模块划分

我们的hooks封装方案分为以下核心模块:

  1. 加载器模块:负责地图SDK的异步加载和初始化
  2. 实例模块:管理地图实例的创建、配置和销毁
  3. 控件模块:封装地图控件的添加、移除和交互
  4. 事件模块:统一管理地图事件的绑定和解绑
  5. 覆盖物模块:管理标记、折线、多边形等地图覆盖物
  6. 工具模块:提供坐标转换、地理编码等工具函数

3. 核心Hooks实现

3.1 useBMapLoader - 地图加载器

typescript 复制代码
import { useState, useEffect } from 'react';

interface BMapLoaderOptions {
  ak: string; // 百度地图AK
  version?: string; // SDK版本
  plugins?: string[]; // 需要加载的插件
}

interface BMapLoaderResult {
  BMap: typeof window.BMap | null;
  isLoading: boolean;
  error: Error | null;
}

/**
 * 百度地图SDK异步加载Hook
 */
export const useBMapLoader = (options: BMapLoaderOptions): BMapLoaderResult => {
  const [BMap, setBMap] = useState<typeof window.BMap | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let mounted = true;

    const loadBMap = async () => {
      try {
        // 检查是否已加载
        if (window.BMap) {
          if (mounted) {
            setBMap(window.BMap);
            setIsLoading(false);
          }
          return;
        }

        // 动态创建script标签
        const script = document.createElement('script');
        const { ak, version = '3.0', plugins = [] } = options;
        
        let url = `https://api.map.baidu.com/api?v=${version}&ak=${ak}&callback=initBMapCallback`;
        
        if (plugins.length > 0) {
          url += `&plugins=${plugins.join(',')}`;
        }

        script.src = url;
        script.async = true;
        script.defer = true;

        // 定义全局回调函数
        window.initBMapCallback = () => {
          if (mounted) {
            setBMap(window.BMap);
            setIsLoading(false);
          }
        };

        script.onerror = (err) => {
          if (mounted) {
            setError(new Error(`Failed to load Baidu Map SDK: ${err}`));
            setIsLoading(false);
          }
        };

        document.head.appendChild(script);

        // 清理函数
        return () => {
          mounted = false;
          if (script.parentNode) {
            script.parentNode.removeChild(script);
          }
          delete window.initBMapCallback;
        };
      } catch (err) {
        if (mounted) {
          setError(err as Error);
          setIsLoading(false);
        }
      }
    };

    loadBMap();

    return () => {
      mounted = false;
    };
  }, [options.ak, options.version, JSON.stringify(options.plugins)]);

  return { BMap, isLoading, error };
};

3.2 useMapInstance - 地图实例管理

typescript 复制代码
import { useState, useEffect, useRef, useCallback } from 'react';

interface MapInstanceOptions {
  container: string | HTMLElement; // 地图容器
  center: { lng: number; lat: number }; // 中心点坐标
  zoom: number; // 缩放级别
  minZoom?: number;
  maxZoom?: number;
  enableScrollWheelZoom?: boolean;
  enableDragging?: boolean;
  mapType?: 'BMAP_NORMAL_MAP' | 'BMAP_SATELLITE_MAP' | 'BMAP_HYBRID_MAP';
}

interface MapInstanceResult {
  map: BMap.Map | null;
  setCenter: (center: { lng: number; lat: number }) => void;
  setZoom: (zoom: number) => void;
  panTo: (point: { lng: number; lat: number }) => void;
  destroy: () => void;
}

/**
 * 地图实例管理Hook
 */
export const useMapInstance = (
  BMap: typeof window.BMap | null,
  options: MapInstanceOptions
): MapInstanceResult => {
  const [map, setMap] = useState<BMap.Map | null>(null);
  const mapRef = useRef<BMap.Map | null>(null);
  const containerRef = useRef<string | HTMLElement>(options.container);

  // 创建地图实例
  useEffect(() => {
    if (!BMap || !containerRef.current) return;

    const container =
      typeof containerRef.current === 'string'
        ? document.getElementById(containerRef.current)
        : containerRef.current;

    if (!container) {
      console.error('Map container not found');
      return;
    }

    // 创建地图实例
    const mapInstance = new BMap.Map(container);
    const point = new BMap.Point(options.center.lng, options.center.lat);
    
    mapInstance.centerAndZoom(point, options.zoom);
    mapInstance.enableScrollWheelZoom(options.enableScrollWheelZoom ?? true);
    mapInstance.enableDragging(options.enableDragging ?? true);

    // 设置地图类型
    if (options.mapType) {
      mapInstance.setMapType((BMap as any)[options.mapType]);
    }

    // 设置缩放范围
    if (options.minZoom !== undefined) {
      mapInstance.setMinZoom(options.minZoom);
    }
    if (options.maxZoom !== undefined) {
      mapInstance.setMaxZoom(options.maxZoom);
    }

    mapRef.current = mapInstance;
    setMap(mapInstance);

    // 清理函数
    return () => {
      if (mapRef.current) {
        mapRef.current.destroy();
        mapRef.current = null;
        setMap(null);
      }
    };
  }, [BMap]);

  // 更新中心点
  const setCenter = useCallback((center: { lng: number; lat: number }) => {
    if (!mapRef.current || !BMap) return;
    
    const point = new BMap.Point(center.lng, center.lat);
    mapRef.current.setCenter(point);
  }, [BMap]);

  // 更新缩放级别
  const setZoom = useCallback((zoom: number) => {
    if (!mapRef.current) return;
    mapRef.current.setZoom(zoom);
  }, []);

  // 平移到指定点
  const panTo = useCallback((point: { lng: number; lat: number }) => {
    if (!mapRef.current || !BMap) return;
    
    const bmapPoint = new BMap.Point(point.lng, point.lat);
    mapRef.current.panTo(bmapPoint);
  }, [BMap]);

  // 销毁地图
  const destroy = useCallback(() => {
    if (mapRef.current) {
      mapRef.current.destroy();
      mapRef.current = null;
      setMap(null);
    }
  }, []);

  return {
    map,
    setCenter,
    setZoom,
    panTo,
    destroy,
  };
};

3.3 useMapEvents - 地图事件管理

typescript 复制代码
import { useEffect, useRef } from 'react';

type MapEventType = 
  | 'click' | 'dblclick' | 'rightclick'
  | 'mousemove' | 'mouseover' | 'mouseout'
  | 'movestart' | 'moving' | 'moveend'
  | 'zoomstart' | 'zoomend' | 'dragstart' | 'dragend';

interface MapEventHandlers {
  [key: string]: (e: any) => void;
}

/**
 * 地图事件管理Hook
 */
export const useMapEvents = (
  map: BMap.Map | null,
  eventHandlers: MapEventHandlers
) => {
  const handlersRef = useRef<MapEventHandlers>(eventHandlers);
  const eventListenersRef = useRef<{ [key: string]: any }>({});

  // 更新事件处理器引用
  useEffect(() => {
    handlersRef.current = eventHandlers;
  }, [eventHandlers]);

  // 绑定和解绑事件
  useEffect(() => {
    if (!map) return;

    const currentListeners = eventListenersRef.current;

    // 绑定新事件
    Object.entries(handlersRef.current).forEach(([eventType, handler]) => {
      if (handler && typeof handler === 'function') {
        const listener = (e: any) => {
          handler(e);
        };
        
        // 移除旧的监听器(如果存在)
        if (currentListeners[eventType]) {
          map.removeEventListener(eventType as any, currentListeners[eventType]);
        }
        
        // 添加新的监听器
        map.addEventListener(eventType as any, listener);
        currentListeners[eventType] = listener;
      }
    });

    // 清理函数:移除所有事件监听器
    return () => {
      Object.entries(currentListeners).forEach(([eventType, listener]) => {
        if (map && listener) {
          map.removeEventListener(eventType as any, listener);
        }
      });
      eventListenersRef.current = {};
    };
  }, [map]);
};

3.4 useMapOverlays - 覆盖物管理

typescript 复制代码
import { useState, useEffect, useRef, useCallback } from 'react';

interface MarkerOptions {
  position: { lng: number; lat: number };
  title?: string;
  icon?: string;
  label?: string;
  draggable?: boolean;
}

interface PolylineOptions {
  path: Array<{ lng: number; lat: number }>;
  strokeColor?: string;
  strokeWeight?: number;
  strokeOpacity?: number;
}

/**
 * 地图覆盖物管理Hook
 */
export const useMapOverlays = (map: BMap.Map | null, BMap: typeof window.BMap | null) => {
  const [markers, setMarkers] = useState<BMap.Marker[]>([]);
  const [polylines, setPolylines] = useState<BMap.Polyline[]>([]);
  const overlaysRef = useRef<{
    markers: BMap.Marker[];
    polylines: BMap.Polyline[];
  }>({ markers: [], polylines: [] });

  // 添加标记
  const addMarker = useCallback((options: MarkerOptions) => {
    if (!map || !BMap || !options.position) return null;

    const point = new BMap.Point(options.position.lng, options.position.lat);
    const marker = new BMap.Marker(point, {
      title: options.title,
      enableDragging: options.draggable,
    });

    // 设置图标
    if (options.icon) {
      const icon = new BMap.Icon(options.icon, new BMap.Size(30, 30));
      marker.setIcon(icon);
    }

    // 设置标签
    if (options.label) {
      const label = new BMap.Label(options.label, {
        offset: new BMap.Size(20, -10),
      });
      marker.setLabel(label);
    }

    map.addOverlay(marker);
    overlaysRef.current.markers.push(marker);
    setMarkers([...overlaysRef.current.markers]);

    return marker;
  }, [map, BMap]);

  // 添加折线
  const addPolyline = useCallback((options: PolylineOptions) => {
    if (!map || !BMap || !options.path || options.path.length < 2) return null;

    const points = options.path.map(p => new BMap.Point(p.lng, p.lat));
    const polyline = new BMap.Polyline(points, {
      strokeColor: options.strokeColor || '#FF0000',
      strokeWeight: options.strokeWeight || 2,
      strokeOpacity: options.strokeOpacity || 0.8,
    });

    map.addOverlay(polyline);
    overlaysRef.current.polylines.push(polyline);
    setPolylines([...overlaysRef.current.polylines]);

    return polyline;
  }, [map, BMap]);

  // 清除所有覆盖物
  const clearAllOverlays = useCallback(() => {
    if (!map) return;

    // 清除标记
    overlaysRef.current.markers.forEach(marker => {
      map.removeOverlay(marker);
    });
    overlaysRef.current.markers = [];
    setMarkers([]);

    // 清除折线
    overlaysRef.current.polylines.forEach(polyline => {
      map.removeOverlay(polyline);
    });
    overlaysRef.current.polylines = [];
    setPolylines([]);
  }, [map]);

  // 组件卸载时清理
  useEffect(() => {
    return () => {
      clearAllOverlays();
    };
  }, [clearAllOverlays]);

  return {
    markers,
    polylines,
    addMarker,
    addPolyline,
    clearAllOverlays,
  };
};

4. 完整示例:地图组件封装

4.1 基础地图组件

tsx 复制代码
import React, { useMemo } from 'react';
import { useBMapLoader, useMapInstance, useMapEvents, useMapOverlays } from './hooks';

interface BMapComponentProps {
  ak: string;
  containerId: string;
  center: { lng: number; lat: number };
  zoom: number;
  onMapClick?: (e: any) => void;
  onMapMoveEnd?: (e: any) => void;
  markers?: Array<{
    position: { lng: number; lat: number };
    title?: string;
    icon?: string;
  }>;
}

const BMapComponent: React.FC<BMapComponentProps> = ({
  ak,
  containerId,
  center,
  zoom,
  onMapClick,
  onMapMoveEnd,
  markers = [],
}) => {
  // 1. 加载地图SDK
  const { BMap, isLoading, error } = useBMapLoader({
    ak,
    plugins: ['Mapv'],
  });

  // 2. 创建地图实例
  const { map } = useMapInstance(BMap, {
    container: containerId,
    center,
    zoom,
    enableScrollWheelZoom: true,
    enableDragging: true,
    mapType: 'BMAP_NORMAL_MAP',
  });

  // 3. 绑定地图事件
  useMapEvents(map, {
    click: onMapClick || (() => {}),
    moveend: onMapMoveEnd || (() => {}),
  });

  // 4. 管理覆盖物
  const { addMarker, clearAllOverlays } = useMapOverlays(map, BMap);

  // 5. 根据props添加标记
  useMemo(() => {
    if (!map || !BMap || markers.length === 0) return;

    clearAllOverlays();
    
    markers.forEach(markerConfig => {
      addMarker({
        position: markerConfig.position,
        title: markerConfig.title,
        icon: markerConfig.icon,
      });
    });
  }, [map, BMap, markers, addMarker, clearAllOverlays]);

  // 加载状态处理
  if (isLoading) {
    return (
      <div className="map-loading">
        <div className="loading-spinner"></div>
        <p>地图加载中...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="map-error">
        <p>地图加载失败: {error.message}</p>
        <button onClick={() => window.location.reload()}>重试</button>
      </div>
    );
  }

  // 6. 渲染地图容器
  return (
    <div className="bmap-container">
      <div 
        id={containerId} 
        style={{ 
          width: '100%', 
          height: '500px',
          borderRadius: '8px',
          overflow: 'hidden',
          boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
        }} 
      />
      
      {/* 地图控制面板 */}
      <div className="map-controls" style={{ marginTop: '16px' }}>
        <div className="map-info">
          <p>中心点: {center.lng.toFixed(6)}, {center.lat.toFixed(6)}</p>
          <p>缩放级别: {zoom}</p>
          <p>标记数量: {markers.length}</p>
        </div>
        
        <div className="map-actions" style={{ marginTop: '12px' }}>
          <button 
            onClick={() => {
              if (map) {
                map.setZoom(map.getZoom() + 1);
              }
            }}
            style={{ marginRight: '8px' }}
          >
            放大
          </button>
          <button 
            onClick={() => {
              if (map) {
                map.setZoom(map.getZoom() - 1);
              }
            }}
            style={{ marginRight: '8px' }}
          >
            缩小
          </button>
          <button 
            onClick={() => {
              if (map) {
                map.setCenter(new BMap.Point(center.lng, center.lat));
              }
            }}
          >
            回到中心
          </button>
        </div>
      </div>
    </div>
  );
};

// 样式定义(可提取到单独的CSS文件)
const styles = `
.bmap-container {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.map-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 500px;
  background: #f5f5f5;
  border-radius: 8px;
}

.loading-spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #e0e0e0;
  border-top: 4px solid #1890ff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 16px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.map-error {
  padding: 24px;
  text-align: center;
  background: #fff2f0;
  border: 1px solid #ffccc7;
  border-radius: 8px;
  color: #cf1322;
}

.map-error button {
  margin-top: 12px;
  padding: 8px 16px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.map-error button:hover {
  background: #40a9ff;
}

.map-controls {
  background: #fafafa;
  padding: 16px;
  border-radius: 8px;
  border: 1px solid #f0f0f0;
}

.map-info p {
  margin: 4px 0;
  color: #666;
  font-size: 14px;
}

.map-actions button {
  padding: 6px 12px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.map-actions button:hover {
  background: #40a9ff;
}

.map-actions button:active {
  background: #096dd9;
}
`;

// 注入样式
if (typeof document !== 'undefined') {
  const styleElement = document.createElement('style');
  styleElement.textContent = styles;
  document.head.appendChild(styleElement);
}

export default BMapComponent;

/**
 * 使用示例:
 * 
 * ```tsx
 * import React from 'react';
 * import BMapComponent from './BMapComponent';
 * 
 * const App = () => {
 *   const handleMapClick = (e: any) => {
 *     console.log('地图点击坐标:', e.point.lng, e.point.lat);
 *   };
 * 
 *   const handleMapMoveEnd = (e: any) => {
 *     console.log('地图移动结束');
 *   };
 * 
 *   const markers = [
 *     {
 *       position: { lng: 116.404, lat: 39.915 },
 *       title: '天安门',
 *       icon: 'https://api.map.baidu.com/images/marker_red.png'
 *     },
 *     {
 *       position: { lng: 116.414, lat: 39.925 },
 *       title: '故宫',
 *       icon: 'https://api.map.baidu.com/images/marker_blue.png'
 *     }
 *   ];
 * 
 *   return (
 *     <div>
 *       <h1>百度地图示例</h1>
 *       <BMapComponent
 *         ak="你的百度地图AK"
 *         containerId="myMap"
 *         center={{ lng: 116.404, lat: 39.915 }}
 *         zoom={15}
 *         onMapClick={handleMapClick}
 *         onMapMoveEnd={handleMapMoveEnd}
 *         markers={markers}
 *       />
 *     </div>
 *   );
 * };
 * 
 * export default App;
 * ```
 */

4.2 高级地图组件(带控件和工具)

tsx 复制代码
import React, { useState, useCallback } from 'react';
import BMapComponent from './BMapComponent';
import { useMapTools } from './hooks/useMapTools';

interface AdvancedBMapProps {
  ak: string;
  containerId: string;
  initialCenter: { lng: number; lat: number };
  initialZoom: number;
  showControls?: boolean;
  showTools?: boolean;
}

const AdvancedBMap: React.FC<AdvancedBMapProps> = ({
  ak,
  containerId,
  initialCenter,
  initialZoom,
  showControls = true,
  showTools = true,
}) => {
  const [center, setCenter] = useState(initialCenter);
  const [zoom, setZoom] = useState(initialZoom);
  const [markers, setMarkers] = useState<Array<{
    position: { lng: number; lat: number };
    title: string;
    icon?: string;
  }>>([]);
  
  const { calculateDistance, geocodeAddress, reverseGeocode } = useMapTools();

  const handleMapClick = useCallback((e: any) => {
    const point = e.point;
    console.log('点击坐标:', point.lng, point.lat);
    
    // 添加标记
    const newMarker = {
      position: { lng: point.lng, lat: point.lat },
      title: `标记${markers.length + 1}`,
      icon: 'https://api.map.baidu.com/images/marker_green.png'
    };
    
    setMarkers(prev => [...prev, newMarker]);
    
    // 反向地理编码
    reverseGeocode(point.lng, point.lat).then(address => {
      console.log('地址信息:', address);
    });
  }, [markers.length, reverseGeocode]);

  const handleMapMoveEnd = useCallback((e: any) => {
    const newCenter = e.target.getCenter();
    const newZoom = e.target.getZoom();
    setCenter({ lng: newCenter.lng, lat: newCenter.lat });
    setZoom(newZoom);
    console.log('地图移动结束,新中心点:', newCenter, '新缩放级别:', newZoom);
  }, []);

  const handleAddMarker = () => {
    const newMarker = {
      position: center,
      title: `自定义标记${markers.length + 1}`,
      icon: 'https://api.map.baidu.com/images/marker_red.png'
    };
    setMarkers(prev => [...prev, newMarker]);
  };

  const handleClearMarkers = () => {
    setMarkers([]);
  };

  const handleGeocode = async () => {
    const address = prompt('请输入地址:');
    if (address) {
      try {
        const result = await geocodeAddress(address);
        if (result) {
          setCenter(result);
          alert(`找到位置:${result.lng}, ${result.lat}`);
        }
      } catch (error) {
        alert('地址解析失败');
      }
    }
  };

  const handleCalculateDistance = () => {
    if (markers.length >= 2) {
      const distance = calculateDistance(
        markers[0].position,
        markers[1].position
      );
      alert(`两点距离:${distance.toFixed(2)} 公里`);
    } else {
      alert('请先添加至少两个标记点');
    }
  };

  return (
    <div className="advanced-bmap-container">
      <div className="toolbar">
        {showControls && (
          <>
            <button onClick={handleAddMarker} className="toolbar-btn">
              添加标记
            </button>
            <button onClick={handleClearMarkers} className="toolbar-btn">
              清除标记
            </button>
            <button onClick={handleGeocode} className="toolbar-btn">
              地址解析
            </button>
            <button onClick={handleCalculateDistance} className="toolbar-btn">
              计算距离
            </button>
          </>
        )}
        
        {showTools && (
          <div className="coordinate-display">
            <span>中心点: {center.lng.toFixed(6)}, {center.lat.toFixed(6)}</span>
            <span>缩放级别: {zoom}</span>
            <span>标记数量: {markers.length}</span>
          </div>
        )}
      </div>

      <BMapComponent
        ak={ak}
        containerId={containerId}
        center={center}
        zoom={zoom}
        onMapClick={handleMapClick}
        onMapMoveEnd={handleMapMoveEnd}
        markers={markers}
      />

      <div className="info-panel">
        <h3>地图信息</h3>
        <div className="info-content">
          <p><strong>当前中心点:</strong>{center.lng.toFixed(6)}, {center.lat.toFixed(6)}</p>
          <p><strong>当前缩放级别:</strong>{zoom}</p>
          <p><strong>标记点列表:</strong></p>
          <ul className="marker-list">
            {markers.map((marker, index) => (
              <li key={index}>
                标记{index + 1}: {marker.position.lng.toFixed(6)}, {marker.position.lat.toFixed(6)}
                <button 
                  onClick={() => {
                    const newMarkers = [...markers];
                    newMarkers.splice(index, 1);
                    setMarkers(newMarkers);
                  }}
                  className="remove-btn"
                >
                  删除
                </button>
              </li>
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
};

// 样式
const advancedStyles = `
.advanced-bmap-container {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.toolbar {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
  padding: 12px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e9ecef;
  flex-wrap: wrap;
  align-items: center;
}

.toolbar-btn {
  padding: 8px 16px;
  background: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.3s;
}

.toolbar-btn:hover {
  background: #40a9ff;
}

.toolbar-btn:active {
  background: #096dd9;
}

.coordinate-display {
  display: flex;
  gap: 16px;
  margin-left: auto;
  font-size: 14px;
  color: #666;
}

.coordinate-display span {
  padding: 4px 8px;
  background: white;
  border-radius: 4px;
  border: 1px solid #e8e8e8;
}

.info-panel {
  margin-top: 20px;
  padding: 16px;
  background: #fafafa;
  border-radius: 8px;
  border: 1px solid #f0f0f0;
}

.info-panel h3 {
  margin-top: 0;
  margin-bottom: 12px;
  color: #1890ff;
}

.info-content {
  font-size: 14px;
}

.info-content p {
  margin: 8px 0;
}

.marker-list {
  list-style: none;
  padding: 0;
  margin: 8px 0;
  max-height: 200px;
  overflow-y: auto;
}

.marker-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 12px;
  margin: 4px 0;
  background: white;
  border-radius: 4px;
  border: 1px solid #e8e8e8;
}

.remove-btn {
  padding: 2px 8px;
  background: #ff4d4f;
  color: white;
  border: none;
  border-radius: 2px;
  cursor: pointer;
  font-size: 12px;
}

.remove-btn:hover {
  background: #ff7875;
}

@media (max-width: 768px) {
  .toolbar {
    flex-direction: column;
    align-items: stretch;
  }
  
  .coordinate-display {
    margin-left: 0;
    margin-top: 8px;
    justify-content: space-between;
  }
}
`;

// 注入样式
if (typeof document !== 'undefined') {
  const styleElement = document.createElement('style');
  styleElement.textContent = advancedStyles;
  document.head.appendChild(styleElement);
}

export default AdvancedBMap;

4.3 自定义地图控件

tsx 复制代码
import React, { useEffect, useRef } from 'react';

interface CustomControlProps {
  map: BMap.Map | null;
  position?: BMap.ControlPosition;
  children: React.ReactNode;
  className?: string;
}

/**
 * 自定义地图控件组件
 */
const CustomControl: React.FC<CustomControlProps> = ({
  map,
  position = BMap.ControlPosition.TOP_RIGHT,
  children,
  className = '',
}) => {
  const controlRef = useRef<HTMLDivElement>(null);
  const controlInstanceRef = useRef<any>(null);

  useEffect(() => {
    if (!map || !controlRef.current) return;

    // 创建自定义控件
    const CustomControlClass = function(this: any) {
      this.defaultAnchor = position;
      this.defaultOffset = new BMap.Size(10, 10);
    };

    CustomControlClass.prototype = new BMap.Control();
    CustomControlClass.prototype.initialize = function(map: BMap.Map) {
      const div = controlRef.current;
      if (div) {
        map.getContainer().appendChild(div);
        return div;
      }
      return null;
    };

    // 创建控件实例
    const control = new (CustomControlClass as any)();
    map.addControl(control);
    controlInstanceRef.current = control;

    return () => {
      if (map && controlInstanceRef.current) {
        map.removeControl(controlInstanceRef.current);
      }
    };
  }, [map, position]);

  return (
    <div 
      ref={controlRef} 
      className={`custom-control ${className}`}
      style={{ display: map ? 'block' : 'none' }}
    >
      {children}
    </div>
  );
};

// 缩放控件
export const ZoomControl: React.FC<{ map: BMap.Map | null }> = ({ map }) => {
  const handleZoomIn = () => {
    if (map) {
      map.setZoom(map.getZoom() + 1);
    }
  };

  const handleZoomOut = () => {
    if (map) {
      map.setZoom(map.getZoom() - 1);
    }
  };

  return (
    <CustomControl map={map} position={BMap.ControlPosition.TOP_LEFT}>
      <div className="zoom-control">
        <button onClick={handleZoomIn} className="zoom-btn">
          +
        </button>
        <button onClick={handleZoomOut} className="zoom-btn">
          -
        </button>
      </div>
    </CustomControl>
  );
};

// 定位控件
export const LocationControl: React.FC<{ 
  map: BMap.Map | null;
  onLocate?: (position: { lng: number; lat: number }) => void;
}> = ({ map, onLocate }) => {
  const handleLocate = () => {
    if (!map || !navigator.geolocation) return;

    navigator.geolocation.getCurrentPosition(
      (position) => {
        const { longitude, latitude } = position.coords;
        const point = new BMap.Point(longitude, latitude);
        map.centerAndZoom(point, 15);
        
        // 添加定位标记
        const marker = new BMap.Marker(point);
        map.addOverlay(marker);
        
        if (onLocate) {
          onLocate({ lng: longitude, lat: latitude });
        }
      },
      (error) => {
        console.error('定位失败:', error);
        alert('定位失败,请检查浏览器权限设置');
      },
      { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
    );
  };

  return (
    <CustomControl map={map} position={BMap.ControlPosition.TOP_RIGHT}>
      <button onClick={handleLocate} className="location-btn">
        📍
      </button>
    </CustomControl>
  );
};

// 图层切换控件
export const LayerControl: React.FC<{ 
  map: BMap.Map | null;
  BMap: typeof window.BMap | null;
}> = ({ map, BMap }) => {
  const [currentLayer, setCurrentLayer] = React.useState<'normal' | 'satellite' | 'hybrid'>('normal');

  const switchLayer = (layerType: 'normal' | 'satellite' | 'hybrid') => {
    if (!map || !BMap) return;

    const layerMap = {
      normal: BMap.MapTypeId.NORMAL_MAP,
      satellite: BMap.MapTypeId.SATELLITE_MAP,
      hybrid: BMap.MapTypeId.HYBRID_MAP,
    };

    map.setMapType(layerMap[layerType]);
    setCurrentLayer(layerType);
  };

  return (
    <CustomControl map={map} position={BMap.ControlPosition.TOP_RIGHT}>
      <div className="layer-control">
        <button 
          onClick={() => switchLayer('normal')}
          className={`layer-btn ${currentLayer === 'normal' ? 'active' : ''}`}
        >
          地图
        </button>
        <button 
          onClick={() => switchLayer('satellite')}
          className={`layer-btn ${currentLayer === 'satellite' ? 'active' : ''}`}
        >
          卫星
        </button>
        <button 
          onClick={() => switchLayer('hybrid')}
          className={`layer-btn ${currentLayer === 'hybrid' ? 'active' : ''}`}
        >
          混合
        </button>
      </div>
    </CustomControl>
  );
};

// 控件样式
const controlStyles = `
.custom-control {
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  padding: 8px;
}

.zoom-control {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.zoom-btn {
  width: 32px;
  height: 32px;
  background: white;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  font-size: 18px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
}

.zoom-btn:hover {
  background: #f5f5f5;
  border-color: #1890ff;
}

.zoom-btn:active {
  background: #e6f7ff;
}

.location-btn {
  width: 40px;
  height: 40px;
  background: white;
  border: 1px solid #d9d9d9;
  border-radius: 50%;
  font-size: 20px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
}

.location-btn:hover {
  background: #f5f5f5;
  border-color: #1890ff;
  transform: scale(1.1);
}

.layer-control {
  display: flex;
  gap: 4px;
}

.layer-btn {
  padding: 6px 12px;
  background: white;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
  transition: all 0.3s;
}

.layer-btn:hover {
  background: #f5f5f5;
  border-color: #1890ff;
}

.layer-btn.active {
  background: #1890ff;
  color: white;
  border-color: #1890ff;
}
`;

// 注入样式
if (typeof document !== 'undefined') {
  const styleElement = document.createElement('style');
  styleElement.textContent = controlStyles;
  document.head.appendChild(styleElement);
}

4.4 地图工具函数

typescript 复制代码
/**
 * 地图工具函数集合
 * 提供常用的地图计算、坐标转换、地理编码等工具函数
 */

/**
 * 计算两点之间的距离(单位:公里)
 * 使用Haversine公式计算球面距离
 */
export const calculateDistance = (
  point1: { lng: number; lat: number },
  point2: { lng: number; lat: number }
): number => {
  const rad = (d: number) => d * Math.PI / 180.0;
  
  const lat1 = point1.lat;
  const lng1 = point1.lng;
  const lat2 = point2.lat;
  const lng2 = point2.lng;
  
  const radLat1 = rad(lat1);
  const radLat2 = rad(lat2);
  const a = radLat1 - radLat2;
  const b = rad(lng1) - rad(lng2);
  
  let s = 2 * Math.asin(Math.sqrt(
    Math.pow(Math.sin(a / 2), 2) +
    Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)
  ));
  
  s = s * 6378.137; // 地球半径(公里)
  s = Math.round(s * 10000) / 10000;
  
  return s;
};

/**
 * 地理编码:地址转坐标
 * @param address 详细地址
 * @param city 城市名(可选)
 * @returns 坐标点 { lng, lat }
 */
export const geocodeAddress = async (
  address: string,
  city?: string
): Promise<{ lng: number; lat: number } | null> => {
  return new Promise((resolve, reject) => {
    if (!window.BMap) {
      reject(new Error('百度地图SDK未加载'));
      return;
    }

    const myGeo = new BMap.Geocoder();
    
    myGeo.getPoint(
      address,
      (point: BMap.Point | null) => {
        if (point) {
          resolve({ lng: point.lng, lat: point.lat });
        } else {
          resolve(null);
        }
      },
      city
    );
  });
};

/**
 * 反向地理编码:坐标转地址
 * @param lng 经度
 * @param lat 纬度
 * @returns 地址信息
 */
export const reverseGeocode = async (
  lng: number,
  lat: number
): Promise<{
  address: string;
  province: string;
  city: string;
  district: string;
  street: string;
  streetNumber: string;
} | null> => {
  return new Promise((resolve, reject) => {
    if (!window.BMap) {
      reject(new Error('百度地图SDK未加载'));
      return;
    }

    const point = new BMap.Point(lng, lat);
    const geocoder = new BMap.Geocoder();
    
    geocoder.getLocation(point, (result: any) => {
      if (result) {
        resolve({
          address: result.address,
          province: result.addressComponents.province,
          city: result.addressComponents.city,
          district: result.addressComponents.district,
          street: result.addressComponents.street,
          streetNumber: result.addressComponents.streetNumber,
        });
      } else {
        resolve(null);
      }
    });
  });
};

/**
 * 坐标转换:百度坐标(BD09)与其他坐标系互转
 */
export const coordinateConverter = {
  /**
   * 百度坐标(BD09)转火星坐标(GCJ02)
   */
  bd09ToGcj02: (lng: number, lat: number): { lng: number; lat: number } => {
    const x = lng - 0.0065;
    const y = lat - 0.006;
    const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * Math.PI);
    const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * Math.PI);
    
    return {
      lng: z * Math.cos(theta),
      lat: z * Math.sin(theta),
    };
  },

  /**
   * 火星坐标(GCJ02)转百度坐标(BD09)
   */
  gcj02ToBd09: (lng: number, lat: number): { lng: number; lat: number } => {
    const x = lng;
    const y = lat;
    const z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * Math.PI);
    const theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * Math.PI);
    
    return {
      lng: z * Math.cos(theta) + 0.0065,
      lat: z * Math.sin(theta) + 0.006,
    };
  },

  /**
   * 百度坐标(BD09)转WGS84坐标
   */
  bd09ToWgs84: (lng: number, lat: number): { lng: number; lat: number } => {
    // 先转GCJ02,再转WGS84
    const gcj02 = coordinateConverter.bd09ToGcj02(lng, lat);
    return coordinateConverter.gcj02ToWgs84(gcj02.lng, gcj02.lat);
  },

  /**
   * 火星坐标(GCJ02)转WGS84坐标
   */
  gcj02ToWgs84: (lng: number, lat: number): { lng: number; lat: number } => {
    const a = 6378245.0;
    const ee = 0.00669342162296594323;
    
    const dLat = coordinateConverter.transformLat(lng - 105.0, lat - 35.0);
    const dLng = coordinateConverter.transformLng(lng - 105.0, lat - 35.0);
    
    const radLat = lat / 180.0 * Math.PI;
    let magic = Math.sin(radLat);
    magic = 1 - ee * magic * magic;
    const sqrtMagic = Math.sqrt(magic);
    
    dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
    dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI);
    
    return {
      lng: lng - dLng,
      lat: lat - dLat,
    };
  },

  // 内部转换函数
  transformLat: (lng: number, lat: number): number => {
    let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
    ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
    ret += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0;
    ret += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0;
    return ret;
  },

  transformLng: (lng: number, lat: number): number => {
    let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
    ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
    ret += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0;
    ret += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0;
    return ret;
  },
};

/**
 * 计算多边形面积(单位:平方米)
 * 使用鞋带公式(Shoelace formula)
 */
export const calculatePolygonArea = (points: Array<{ lng: number; lat: number }>): number => {
  if (points.length < 3) return 0;
  
  let area = 0;
  const n = points.length;
  
  for (let i = 0; i < n; i++) {
    const j = (i + 1) % n;
    area += points[i].lng * points[j].lat;
    area -= points[j].lng * points[i].lat;
  }
  
  area = Math.abs(area) / 2;
  
  // 将经纬度坐标转换为平面坐标的近似面积
  // 这里使用简化计算,实际应用中可能需要更精确的球面面积计算
  const earthRadius = 6371000; // 地球半径(米)
  const latRad = (points[0].lat * Math.PI) / 180;
  const scaleFactor = Math.cos(latRad);
  
  return area * earthRadius * earthRadius * scaleFactor * Math.PI / 180 * Math.PI / 180;
};

/**
 * 判断点是否在多边形内
 * 使用射线法(Ray Casting Algorithm)
 */
export const isPointInPolygon = (
  point: { lng: number; lat: number },
  polygon: Array<{ lng: number; lat: number }>
): boolean => {
  if (polygon.length < 3) return false;
  
  let inside = false;
  const x = point.lng;
  const y = point.lat;
  
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const xi = polygon[i].lng;
    const yi = polygon[i].lat;
    const xj = polygon[j].lng;
    const yj = polygon[j].lat;
    
    const intersect = ((yi > y) !== (yj > y)) &&
      (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
    
    if (intersect) inside = !inside;
  }
  
  return inside;
};

/**
 * 计算路径长度(单位:米)
 */
export const calculatePathLength = (path: Array<{ lng: number; lat: number }>): number => {
  if (path.length < 2) return 0;
  
  let totalDistance = 0;
  
  for (let i = 0; i < path.length - 1; i++) {
    const point1 = path[i];
    const point2 = path[i + 1];
    const distance = calculateDistance(point1, point2) * 1000; // 公里转米
    totalDistance += distance;
  }
  
  return totalDistance;
};

/**
 * 获取两点之间的中间点
 */
export const getMidpoint = (
  point1: { lng: number; lat: number },
  point2: { lng: number; lat: number }
): { lng: number; lat: number } => {
  return {
    lng: (point1.lng + point2.lng) / 2,
    lat: (point1.lat + point2.lat) / 2,
  };
};

/**
 * 格式化坐标显示
 */
export const formatCoordinate = (
  lng: number,
  lat: number,
  format: 'decimal' | 'dms' = 'decimal'
): string => {
  if (format === 'decimal') {
    return `${lng.toFixed(6)}, ${lat.toFixed(6)}`;
  }
  
  // 度分秒格式
  const lngDeg = Math.floor(Math.abs(lng));
  const lngMin = Math.floor((Math.abs(lng) - lngDeg) * 60);
  const lngSec = ((Math.abs(lng) - lngDeg - lngMin / 60) * 3600).toFixed(2);
  const lngDir = lng >= 0 ? 'E' : 'W';
  
  const latDeg = Math.floor(Math.abs(lat));
  const latMin = Math.floor((Math.abs(lat) - latDeg) * 60);
  const latSec = ((Math.abs(lat) - latDeg - latMin / 60) * 3600).toFixed(2);
  const latDir = lat >= 0 ? 'N' : 'S';
  
  return `${lngDeg}°${lngMin}'${lngSec}"${lngDir}, ${latDeg}°${latMin}'${latSec}"${latDir}`;
};

/**
 * 地图工具函数Hook封装
 */
export const useMapTools = () => {
  return {
    calculateDistance,
    geocodeAddress,
    reverseGeocode,
    coordinateConverter,
    calculatePolygonArea,
    isPointInPolygon,
    calculatePathLength,
    getMidpoint,
    formatCoordinate,
  };
};

/**
 * 使用示例:
 * 
 * ```typescript
 * import { useMapTools } from './utils/mapTools';
 * 
 * const MapComponent = () => {
 *   const tools = useMapTools();
 *   
 *   // 计算两点距离
 *   const distance = tools.calculateDistance(
 *     { lng: 116.404, lat: 39.915 },
 *     { lng: 116.414, lat: 39.925 }
 *   );
 *   console.log(`距离:${distance}公里`);
 *   
 *   // 地址解析
 *   const getCoordinate = async () => {
 *     const point = await tools.geocodeAddress('北京市天安门');
 *     if (point) {
 *       console.log('坐标:', point);
 *     }
 *   };
 *   
 *   // 坐标转换
 *   const bdPoint = { lng: 116.404, lat: 39.915 };
 *   const gcjPoint = tools.coordinateConverter.bd09ToGcj02(bdPoint.lng, bdPoint.lat);
 *   console.log('转换后坐标:', gcjPoint);
 *   
 *   return <div>地图组件</div>;
 * };
 * ```
 */

转载自:https://blog.csdn.net/u014727709/article/details/163345035

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
2301_768103499 天前
HarmonyOS趣味相机实战第31篇:图片文档List、详情弹层与删除状态闭环
list·harmonyos·arkts·状态管理·arkui
想你依然心痛13 天前
【共创季稿事节】HarmonyOS 6.1 悬浮页签重塑阅读 App 多任务体验实战
性能优化·状态管理·arkui·harmonyos 6.1·悬浮页签·多文档阅读·进度保持
换元不配限17 天前
Jetpack Compose 状态管理指南
android·状态管理·compose·状态·单向数据流·jetpack compose·状态容器
不才难以繁此生17 天前
ArkUI Tabs 切换后状态丢失怎么办:中式美食分类页怎么保留关键词、筛选和滚动位置
harmonyos·状态管理·arkui·tabs·中式美食
不才难以繁此生20 天前
ArkUI 筛选状态实战:中式美食列表页怎么让关键词、分类和排序不互相打架
harmonyos·arkts·状态管理·arkui·中式美食
不才难以繁此生23 天前
ArkUI 列表性能实战|中式美食菜谱列表如何拆组件、控重绘和稳住滑动体验
harmonyos·arkts·状态管理·foreach·arkui·列表性能·组件拆分
xiangji23 天前
开源完美模块组件化可扩展的Xml解析器Hand.ParseXml
xml·模块化·组件化·可扩展
晓杰'2 个月前
从0到1实现Balatro游戏后端(8):Skip Blind与Tag奖励机制设计与实现
后端·websocket·typescript·项目实战·nestjs·状态管理·游戏服务器
故渊at2 个月前
第二板块:Android 四大组件标准化学理 | 第十一篇:组件间通信(IPC)与 Binder 深度解析
android·binder·组件化·组件间通信