hejianhao
2026-05-06 6af2d7b06ec44d0b09c0b91244a495edc1ff9c6c
fix: 修复场站围栏显隐逻辑与地图中心点同步问题

- 提取 `findMatchedPolygon` 为公共方法,统一围栏命中逻辑
- 地图区域变化时同步场站围栏显隐状态,确保与吸附后中心点一致
- 运营区页面仅展示运营区围栏,过滤场站围栏
- 修复收藏按钮点击事件冒泡问题
- 调整路线摘要弹窗样式
9个文件已修改
405 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-location-flow.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-navigation.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-region-change.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/operating-area/index.tsx 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/site-list-item.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/Robotaxi-public-method.ts 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
@@ -54,6 +54,8 @@
  getCityLocation: (cityName: string) => Promise<any>;
  /** 地图数据刷新 */
  updateMapData: (lat: number, lng: number, cityCode?: string) => Promise<any>;
  /** 同步场站围栏显隐 */
  syncStationFenceVisibility: (lat: number, lng: number, isInside: boolean) => void;
  /** 城市就绪同步 */
  ensureSelectedCityReady: (lng: number, lat: number, skipCityEffect?: boolean, forceRefresh?: boolean) => Promise<any>;
  /** 设置当前站点 */
@@ -151,7 +153,8 @@
            longitude: loc.longitude
          });
          if (switchedCity?.code) {
            deps.updateMapData(loc.latitude, loc.longitude, switchedCity.code);
            const result = await deps.updateMapData(loc.latitude, loc.longitude, switchedCity.code);
            deps.syncStationFenceVisibility(loc.latitude, loc.longitude, !!result?.isInside);
          }
        } catch (error) {
          deps.skipNextLocationCenterRef.current = true;
@@ -172,11 +175,13 @@
      deps.shouldIgnoreRegionChange.current = true;
      const cachedCity = Taro.getStorageSync('selectedCity');
      if (cachedCity?.code) {
        deps.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code);
        const result = await deps.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code);
        deps.syncStationFenceVisibility(startSite.latitude, startSite.longitude, !!result?.isInside);
      } else {
        const cityInfo = await deps.ensureSelectedCityReady(startSite.longitude, startSite.latitude, true);
        if (cityInfo?.code) {
          deps.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code);
          const result = await deps.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code);
          deps.syncStationFenceVisibility(startSite.latitude, startSite.longitude, !!result?.isInside);
        }
      }
      return;
src/pages/robotaxi/home/hooks/use-location-flow.ts
@@ -12,12 +12,13 @@
  siteState: any;
  cityState: any;
  isLogin: boolean;
  onLocationSettled: (lat: number, lng: number, isInside: boolean) => void;
};
/**
 * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。
 */
export default function useLocationFlow({ mapState, siteState, cityState, isLogin }: Deps) {
export default function useLocationFlow({ mapState, siteState, cityState, isLogin, onLocationSettled }: Deps) {
  const updateByStartSite = async () => {
    const startSite = Taro.getStorageSync('startSite');
    if (!(startSite && startSite.latitude && startSite.longitude)) return false;
@@ -28,11 +29,13 @@
    mapState.shouldIgnoreRegionChange.current = true;
    const cachedCity = Taro.getStorageSync('selectedCity');
    if (cachedCity?.code) {
      await mapState.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code);
      const result = await mapState.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code);
      onLocationSettled(startSite.latitude, startSite.longitude, !!result?.isInside);
    } else {
      const cityInfo = await cityState.ensureSelectedCityReady(startSite.longitude, startSite.latitude, true);
      if (cityInfo?.code) {
        await mapState.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code);
        const result = await mapState.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code);
        onLocationSettled(startSite.latitude, startSite.longitude, !!result?.isInside);
      }
    }
    return true;
@@ -119,9 +122,12 @@
            forceCityRefresh
          );
          if (cityInfo?.code) {
            const { newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code);
            const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code);
            const nearestCandidates = sites.length > 0 ? sites : newMarkers;
            const { adsorbed, marker } = siteState.tryAdsorb(res.latitude, res.longitude, nearestCandidates);
            const finalLat = adsorbed && marker ? marker.latitude : res.latitude; // 最终纬度
            const finalLng = adsorbed && marker ? marker.longitude : res.longitude; // 最终经度
            onLocationSettled(finalLat, finalLng, !!isInside);
            if (adsorbed && updateCenter && marker) {
              setTimeout(() => {
                Taro.createMapContext('map').moveToLocation({
src/pages/robotaxi/home/hooks/use-navigation.ts
@@ -8,6 +8,7 @@
} from '../../../../utils/Robotaxi-public-method';
import { jumpTo } from '../../../../utils/jump';
import { getGeofence } from '../../../../robotaxi/api/home';
import { GeofenceType } from '../../../../robotaxi/constants/geofence';
type Deps = {
  /** 是否未登录 */
@@ -225,7 +226,7 @@
    let nextPolygons = deps.polygons;
    try {
      const res: any = await getGeofence({ cityCode: deps.selectedCity.code });
      const geofenceList = filterRenderableGeofence(res);
      const geofenceList = filterRenderableGeofence(res).filter((item) => Number(item?.type) === GeofenceType.OPERATING); // 查看运营区域时仅展示运营区
      if (geofenceList.length > 0) {
        const { polygons: nextFormattedPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(geofenceList); // 围栏格式统一复用公共实现
        nextPolygons = nextFormattedPolygons;
src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -14,6 +14,15 @@
 */
export default function useRegionChange({ mapState, siteState, toOrigin, onRegionBegin, onRegionSettled }: Deps) {
  /**
   * 按最终落点回传地图结算结果,保证场站围栏与吸附后的中心点一致。
   */
  const settleRegionResult = (payload: { lat: number; lon: number; isInside: boolean }) => {
    if (typeof onRegionSettled === 'function') {
      onRegionSettled(payload);
    }
  };
  /**
   * 通过中心点主动触发吸附判断。
   */
  const queryPointerByCenter = async (lat: number, lng: number) => {
@@ -22,11 +31,15 @@
    try {
      const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng);
      if (!isInside) {
        settleRegionResult({ lat, lon: lng, isInside: false });
        siteState.setBounce(false);
        return;
      }
      const nearestCandidates = sites.length > 0 ? sites : newMarkers;
      const { adsorbed } = siteState.tryAdsorb(lat, lng, nearestCandidates);
      const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
      const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度
      const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度
      settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true });
      setTimeout(async () => {
        siteState.setBounce(false);
        if (!adsorbed) {
@@ -66,11 +79,16 @@
        }
        try {
          const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng);
          if (typeof onRegionSettled === 'function') {
            onRegionSettled({ lat, lon: lng, isInside: !!isInside });
          if (!isInside) {
            settleRegionResult({ lat, lon: lng, isInside: false });
            siteState.setBounce(false);
            return;
          }
          const nearestCandidates = sites.length > 0 ? sites : newMarkers;
          const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
          const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度
          const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度
          settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true });
          if ((causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin) && marker) {
            mapCtx.moveToLocation({
              latitude: marker.latitude,
src/pages/robotaxi/home/index.tsx
@@ -25,9 +25,9 @@
import wsManager from '../../../utils/websocket';
import {
  filterRenderableGeofence,
  findMatchedPolygon,
  formatGeofenceByType,
  getCityLocation,
  mapOrderStatusToTripStatus,
  safeParse,
  getWeatherInfo
} from '../../../utils/Robotaxi-public-method';
@@ -71,6 +71,7 @@
import { getOrderDistance } from '../../../robotaxi/api/select-map-site';
import { checkOtp, toH5 } from '../../../utils/tools';
import { subscribeOperationCouponGroup } from '../../../utils/subscribe';
import { GeofenceType } from '../../../robotaxi/constants/geofence';
type HomeTripStatus = TripStatus | 0;
@@ -147,6 +148,10 @@
  const [isLogin, setIsLogin] = useState(false); // 是否登录
  const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部)
  const [temperatureUnit, setTemperatureUnit] = useState(DEFAULT_TEMPERATURE_UNIT); // 后台配置的温度单位
  const [operatingGeofenceList, setOperatingGeofenceList] = useState<any[]>([]); // 运营区围栏原始数据
  const [stationGeofenceList, setStationGeofenceList] = useState<any[]>([]); // 场站围栏原始数据
  const operatingGeofenceListRef = useRef<any[]>([]); // 运营区围栏最新缓存
  const stationGeofenceListRef = useRef<any[]>([]); // 场站围栏最新缓存
  const shouldShowOrderStatusCard = useMemo(() => tripStatus === 3 || Boolean(orderData?.id), [orderData?.id, tripStatus]); // 订单状态卡片展示时机
  const {
    boxHeight,
@@ -186,6 +191,56 @@
  const displayTemperatureUnit = useMemo(() => normalizeTemperatureUnit(temperatureUnit), [temperatureUnit]); // 页面展示温度单位
  const displayTemperature = useMemo(() => convertTemperatureByUnit(weatherData?.temperature, displayTemperatureUnit), [weatherData?.temperature, displayTemperatureUnit]); // 页面展示温度值
  /**
   * 按当前命中的场站围栏刷新地图覆盖物,运营区始终常驻展示。
   */
  const applyVisibleGeofence = (matchedStationGeofence: any | null, nextOperatingGeofenceList?: any[]) => {
    const operatingGeofenceSource = Array.isArray(nextOperatingGeofenceList)
      ? nextOperatingGeofenceList
      : operatingGeofenceListRef.current;
    const { polygons: operatingPolygons } = formatGeofenceByType(operatingGeofenceSource);
    const { polygons: stationPolygons, fencePolylines: stationFencePolylines } = formatGeofenceByType(
      matchedStationGeofence ? [matchedStationGeofence] : []
    );
    setPolygons([...operatingPolygons, ...stationPolygons]);
    setFencePolylines(stationFencePolylines);
  };
  /**
   * 根据大头针中心点切换场站围栏显隐。
   */
  const syncStationFenceVisibility = (lat: number, lng: number, isInside: boolean) => {
    const latestOperatingGeofenceList = operatingGeofenceListRef.current;
    const latestStationGeofenceList = stationGeofenceListRef.current;
    if (!Array.isArray(latestOperatingGeofenceList) || latestOperatingGeofenceList.length === 0) {
      return;
    }
    if (!isInside || !Array.isArray(latestStationGeofenceList) || latestStationGeofenceList.length === 0) {
      applyVisibleGeofence(null);
      return;
    }
    const matchedStationGeofence = findMatchedPolygon(
      { latitude: lat, longitude: lng },
      latestStationGeofenceList,
      0
    );
    applyVisibleGeofence(matchedStationGeofence);
  };
  /**
   * 同步运营区围栏 ref,避免异步回调读到旧值。
   */
  useEffect(() => {
    operatingGeofenceListRef.current = operatingGeofenceList;
  }, [operatingGeofenceList]);
  /**
   * 同步场站围栏 ref,保证初始化与吸附结算拿到最新数据。
   */
  useEffect(() => {
    stationGeofenceListRef.current = stationGeofenceList;
  }, [stationGeofenceList]);
  useEffect(() => {
    let timer: any = null;
@@ -278,13 +333,23 @@
    if (!selectedCity.code) return;
    const allowMapMove = !isLocationAuthDenied;
    getGeofence({ cityCode: selectedCity.code }).then((res: any) => {
      const geofenceList = filterRenderableGeofence(res);
      const nextOperatingGeofenceList = geofenceList.filter((item) => Number(item?.type) === GeofenceType.OPERATING); // 运营区围栏集合
      const nextStationGeofenceList = geofenceList.filter((item) => Number(item?.type) === GeofenceType.STATION); // 场站围栏集合
      setOperatingGeofenceList(nextOperatingGeofenceList);
      setStationGeofenceList(nextStationGeofenceList);
      if (geofenceList.length > 0) {
        const { polygons: newPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(geofenceList); // 围栏面和场站虚线边框统一走公共格式化
        setPolygons(newPolygons);
        setFencePolylines(nextFencePolylines);
        const matchedStationGeofence = (typeof latitude === 'number' && typeof longitude === 'number')
          ? findMatchedPolygon(
            { latitude, longitude },
            nextStationGeofenceList,
            0
          )
          : null;
        applyVisibleGeofence(matchedStationGeofence, nextOperatingGeofenceList);
      } else {
        setOperatingGeofenceList([]);
        setStationGeofenceList([]);
        setPolygons([]);
        setFencePolylines([]);
        if ((orderData || orderData?.id) && JSON.stringify(orderData) != '{}') return;
@@ -305,6 +370,8 @@
      }
    }).catch(err => {
      console.error('获取电子围栏失败', err);
      setOperatingGeofenceList([]);
      setStationGeofenceList([]);
      setPolygons([]);
      setFencePolylines([]);
      // 定位到城市中心
@@ -328,7 +395,8 @@
    mapState,
    siteState,
    cityState,
    isLogin
    isLogin,
    onLocationSettled: syncStationFenceVisibility
  }); // 定位流程编排
  const handleClosePrivacy = () => {
@@ -376,6 +444,7 @@
    shouldIgnoreRegionChange,
    getCityLocation,
    updateMapData,
    syncStationFenceVisibility,
    ensureSelectedCityReady,
    setCurrentSiteFromStart: setCurrentSite,
    firstEnterKey: FIRST_ENTER_KEY,
@@ -497,7 +566,10 @@
    siteState,
    toOrigin,
    onRegionBegin: handleVehicleRegionBegin,
    onRegionSettled: handleVehicleRegionSettled
    onRegionSettled: (payload) => {
      handleVehicleRegionSettled(payload);
      syncStationFenceVisibility(payload.lat, payload.lon, payload.isInside);
    }
  }); // 地图 region-change 编排
  const {
    ensureNetwork,
src/robotaxi/pages/begin-route/index.less
@@ -137,8 +137,8 @@
    font-size: 20rpx;
    font-weight: 500;
    color: #FFFFFF;
    line-height: 28rpx;
    padding: 10rpx 12rpx 9rpx 14rpx;
    line-height: 47rpx;
    padding: 0 12rpx 0 14rpx;
}
.route-summary-callout-arrow {
src/robotaxi/pages/operating-area/index.tsx
@@ -8,7 +8,7 @@
import { getOpenCity } from '../../api/select-city';
import { getGeofence } from '../../api/home';
import { extractPointsFromPolygons, getDistance } from '../../../utils/Robotaxi-public-method';
import { extractPointsFromPolygons, findMatchedPolygon, getDistance } from '../../../utils/Robotaxi-public-method';
export default function OperatingArea() {
  const [longitude, setLongitude] = useState<number | null>(null);
@@ -119,116 +119,13 @@
    setPolylines(updatePolylineStyle(geofenceList, polygonId));
  };
  const projectPoint = (point: { latitude: number; longitude: number }, referenceLatitude: number) => {
    const metersPerDegreeLat = 111320;
    const metersPerDegreeLng = 111320 * Math.cos(referenceLatitude * Math.PI / 180);
    return {
      x: point.longitude * metersPerDegreeLng,
      y: point.latitude * metersPerDegreeLat
    };
  };
  const getDistanceToSegment = (
    point: { latitude: number; longitude: number },
    start: { latitude: number; longitude: number },
    end: { latitude: number; longitude: number }
  ) => {
    const referenceLatitude = (point.latitude + start.latitude + end.latitude) / 3;
    const pointXY = projectPoint(point, referenceLatitude);
    const startXY = projectPoint(start, referenceLatitude);
    const endXY = projectPoint(end, referenceLatitude);
    const dx = endXY.x - startXY.x;
    const dy = endXY.y - startXY.y;
    const lengthSquared = dx * dx + dy * dy;
    if (lengthSquared === 0) {
      return Math.sqrt((pointXY.x - startXY.x) * (pointXY.x - startXY.x) + (pointXY.y - startXY.y) * (pointXY.y - startXY.y));
    }
    let ratio = ((pointXY.x - startXY.x) * dx + (pointXY.y - startXY.y) * dy) / lengthSquared;
    ratio = Math.max(0, Math.min(1, ratio));
    const closestX = startXY.x + ratio * dx;
    const closestY = startXY.y + ratio * dy;
    return Math.sqrt((pointXY.x - closestX) * (pointXY.x - closestX) + (pointXY.y - closestY) * (pointXY.y - closestY));
  };
  const isPointInPolygon = (
    point: { latitude: number; longitude: number },
    polygonPoints: Array<{ latitude: number; longitude: number }>
  ) => {
    if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
      return false;
    }
    let isInside = false;
    const pointX = point.longitude;
    const pointY = point.latitude;
    for (let index = 0, lastIndex = polygonPoints.length - 1; index < polygonPoints.length; lastIndex = index, index += 1) {
      const currentPoint = polygonPoints[index];
      const previousPoint = polygonPoints[lastIndex];
      const currentX = currentPoint.longitude;
      const currentY = currentPoint.latitude;
      const previousX = previousPoint.longitude;
      const previousY = previousPoint.latitude;
      const isIntersected = ((currentY > pointY) !== (previousY > pointY))
        && (pointX < ((previousX - currentX) * (pointY - currentY)) / ((previousY - currentY) || Number.EPSILON) + currentX);
      if (isIntersected) {
        isInside = !isInside;
      }
    }
    return isInside;
  };
  /**
   * 根据点击点命中围栏,优先命中内部,其次吸附到最近边界。
   */
  const findNearestPolygonIdByTap = (latitudeValue: number, longitudeValue: number) => {
    const tapPoint = { latitude: latitudeValue, longitude: longitudeValue };
    const insidePolygon = rawGeofenceData.find((item) => {
      const polygonPoints = item.polygonPoints || [];
      return isPointInPolygon(tapPoint, polygonPoints);
    });
    if (insidePolygon) {
      return insidePolygon.mapPolygonId;
    }
    let nearestPolygonId: number | null = null;
    let nearestDistance = Number.POSITIVE_INFINITY;
    rawGeofenceData.forEach((item) => {
      const polygonPoints = item.polygonPoints || [];
      if (polygonPoints.length < 2) {
        return;
      }
      let minDistanceToBorder = Number.POSITIVE_INFINITY;
      for (let index = 0; index < polygonPoints.length; index += 1) {
        const start = polygonPoints[index];
        const end = polygonPoints[(index + 1) % polygonPoints.length];
        const distanceToSegment = getDistanceToSegment(tapPoint, start, end);
        if (distanceToSegment < minDistanceToBorder) {
          minDistanceToBorder = distanceToSegment;
        }
      }
      const distanceToFirstPoint = getDistance(
        latitudeValue,
        longitudeValue,
        polygonPoints[0].latitude,
        polygonPoints[0].longitude
      );
      const matchDistance = Math.min(minDistanceToBorder, distanceToFirstPoint);
      if (matchDistance < nearestDistance) {
        nearestDistance = matchDistance;
        nearestPolygonId = item.mapPolygonId;
      }
    });
    return nearestDistance <= 180 ? nearestPolygonId : null;
    const matchedPolygon = findMatchedPolygon(tapPoint, rawGeofenceData, 180);
    return matchedPolygon?.mapPolygonId ?? null;
  };
  const handleSelectCity = (cityCode: string) => {
src/robotaxi/pages/site-search/components/site-list-item.tsx
@@ -175,7 +175,10 @@
        </View>
        {showDistance ?
          <View className='collect-box'>
            <Image onClick={handleCollect} className='collect-icon' src={isCollected ? starSrc : noStarSrc} />
            <Image onClick={(e) => {
              e.stopPropagation();
              handleCollect(e);
            }} className='collect-icon' src={isCollected ? starSrc : noStarSrc} />
            {renderHighlightedText(item.distance || '-', 'history-distance')}
          </View>
          : null}
src/utils/Robotaxi-public-method.ts
@@ -42,6 +42,149 @@
};
/**
 * 统一标准化围栏点位,兼容 `{ lat, lng }` 和 `{ latitude, longitude }` 两种结构。
 */
const normalizePolygonPoints = (points: any[] = []) => {
  return points
    .map((point) => ({
      latitude: Number(point?.latitude ?? point?.lat),
      longitude: Number(point?.longitude ?? point?.lng)
    }))
    .filter((point) => Number.isFinite(point.latitude) && Number.isFinite(point.longitude));
};
/**
 * 将经纬度投影到近似平面,便于计算点到边的距离。
 */
const projectPoint = (point: { latitude: number; longitude: number }, referenceLatitude: number) => {
  const metersPerDegreeLat = 111320;
  const metersPerDegreeLng = 111320 * Math.cos(referenceLatitude * Math.PI / 180);
  return {
    x: point.longitude * metersPerDegreeLng,
    y: point.latitude * metersPerDegreeLat
  };
};
/**
 * 计算点到线段的最短距离,单位为米。
 */
export const getDistanceToSegment = (
  point: { latitude: number; longitude: number },
  start: { latitude: number; longitude: number },
  end: { latitude: number; longitude: number }
) => {
  const referenceLatitude = (point.latitude + start.latitude + end.latitude) / 3;
  const pointXY = projectPoint(point, referenceLatitude);
  const startXY = projectPoint(start, referenceLatitude);
  const endXY = projectPoint(end, referenceLatitude);
  const dx = endXY.x - startXY.x;
  const dy = endXY.y - startXY.y;
  const lengthSquared = dx * dx + dy * dy;
  if (lengthSquared === 0) {
    return Math.sqrt((pointXY.x - startXY.x) * (pointXY.x - startXY.x) + (pointXY.y - startXY.y) * (pointXY.y - startXY.y));
  }
  let ratio = ((pointXY.x - startXY.x) * dx + (pointXY.y - startXY.y) * dy) / lengthSquared;
  ratio = Math.max(0, Math.min(1, ratio));
  const closestX = startXY.x + ratio * dx;
  const closestY = startXY.y + ratio * dy;
  return Math.sqrt((pointXY.x - closestX) * (pointXY.x - closestX) + (pointXY.y - closestY) * (pointXY.y - closestY));
};
/**
 * 判断点是否命中多边形内部。
 */
export const isPointInPolygon = (
  point: { latitude: number; longitude: number },
  polygonPoints: Array<{ latitude: number; longitude: number }>
) => {
  if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
    return false;
  }
  let isInside = false;
  const pointX = point.longitude;
  const pointY = point.latitude;
  for (let index = 0, lastIndex = polygonPoints.length - 1; index < polygonPoints.length; lastIndex = index, index += 1) {
    const currentPoint = polygonPoints[index];
    const previousPoint = polygonPoints[lastIndex];
    const currentX = currentPoint.longitude;
    const currentY = currentPoint.latitude;
    const previousX = previousPoint.longitude;
    const previousY = previousPoint.latitude;
    const isIntersected = ((currentY > pointY) !== (previousY > pointY))
      && (pointX < ((previousX - currentX) * (pointY - currentY)) / ((previousY - currentY) || Number.EPSILON) + currentX);
    if (isIntersected) {
      isInside = !isInside;
    }
  }
  return isInside;
};
/**
 * 查找当前点命中的围栏;未命中时可退化为最近围栏边界。
 */
export const findMatchedPolygon = (
  point: { latitude: number; longitude: number },
  polygonList: any[] = [],
  maxDistance = 180
) => {
  if (!Array.isArray(polygonList) || polygonList.length === 0) {
    return null;
  }
  const insidePolygon = polygonList.find((item) => {
    const polygonPoints = normalizePolygonPoints(item?.polygonPoints || item?.points || []);
    return isPointInPolygon(point, polygonPoints);
  });
  if (insidePolygon) {
    return insidePolygon;
  }
  let nearestPolygon: any = null;
  let nearestDistance = Number.POSITIVE_INFINITY;
  polygonList.forEach((item) => {
    const polygonPoints = normalizePolygonPoints(item?.polygonPoints || item?.points || []);
    if (polygonPoints.length < 2) {
      return;
    }
    let minDistanceToBorder = Number.POSITIVE_INFINITY;
    for (let index = 0; index < polygonPoints.length; index += 1) {
      const start = polygonPoints[index];
      const end = polygonPoints[(index + 1) % polygonPoints.length];
      const distanceToSegment = getDistanceToSegment(point, start, end);
      if (distanceToSegment < minDistanceToBorder) {
        minDistanceToBorder = distanceToSegment;
      }
    }
    const distanceToFirstPoint = getDistance(
      point.latitude,
      point.longitude,
      polygonPoints[0].latitude,
      polygonPoints[0].longitude
    );
    const matchDistance = Math.min(minDistanceToBorder, distanceToFirstPoint);
    if (matchDistance < nearestDistance) {
      nearestDistance = matchDistance;
      nearestPolygon = item;
    }
  });
  return nearestDistance <= maxDistance ? nearestPolygon : null;
};
/**
 * 根据站点空间分布估算推荐缩放等级(站点越分散,缩放越小)。
 */
export const getAutoScaleBySites = (sites: any[]) => {