{"version":3,"file":"PointClusterer.mjs","sources":["../../../../../../packages/sdk/utils/Dilution/PointClusterer.ts"],"sourcesContent":["/*\n * @Author: Yolo\n * @Date: 2024-04-24 10:06:05\n * @Last Modified by: Yolo\n * @Last Modified time: 2024-06-06 10:25:02\n * @Desc 坐标抽稀算法（正常使用效果还可以）\n */\nimport RBush from 'rbush'\nimport { GPS } from '@map-sdk/sdk/utils/GPS'\nimport type L from 'leaflet'\n\ninterface LatLng {\n  lat: number\n  lng: number\n}\n\ntype CollisionResolutionStrategy =\n  | 'nearest' // 选择与新点距离最近的已存在点\n  | 'average' // 选择与新点距离平均值最小的已存在点集，并以这些点的平均位置作为新点\n  | 'weightedAverage' // 选择与新点距离加权平均值最小的已存在点集，并以这些点的加权平均位置作为新点\n  | 'merge' // 将新点与所有与其发生碰撞的点合并为一个新的点，位置取所有碰撞点的中心\n  | 'discard' // 如果新点与已有点发生碰撞，忽略该新点，不添加到地图\n  | 'custom' // 用户自定义的碰撞处理策略\n\ninterface PointClustererOptions {\n  thresholdFactor: number // 默认抽稀阈值与缩放级别的比例\n  maxPointRange: number // 默认点位最大覆盖范围（单位：像素）\n  collisionResolution: CollisionResolutionStrategy // 默认碰撞处理策略（'nearest'：选择最近的已存在点）\n  callback?: ((...params: any) => void) | null\n  customCollisionResolver?: ((...params: any) => LatLng) | null\n}\n\ninterface RequiredProperty {\n  point: LatLng // 必须的属性\n  [key: string]: any // 索引签名，允许任意其他属性\n}\n\nexport class PointClusterer {\n  private map: L.Map\n  private originalPoints: LatLng[]\n  private options: PointClustererOptions\n  private rBush: RBush<{\n    minX: number\n    minY: number\n    maxX: number\n    maxY: number\n    point: LatLng\n  }>\n  private pointData: RequiredProperty[]\n\n  constructor(\n    map: L.Map,\n    originalPoints: RequiredProperty[],\n    options: PointClustererOptions\n  ) {\n    this.map = map\n    this.pointData = originalPoints\n    this.originalPoints = originalPoints.map((item) => item.point)\n    this.options = {\n      thresholdFactor: options.thresholdFactor ?? 5, // 默认抽稀阈值与缩放级别的比例\n      maxPointRange: options.maxPointRange ?? 80, // 默认点位最大覆盖范围（单位：像素）\n      collisionResolution: options.collisionResolution ?? 'nearest', // 默认碰撞处理策略（'nearest'：选择最近的已存在点）\n      callback: options.callback,\n      customCollisionResolver: options.customCollisionResolver,\n    }\n    this.rBush = new RBush() // 初始化空间索引\n\n    this.init()\n  }\n\n  private init(): void {\n    this.map.on('zoomend', () => this.updatePoints())\n    this.updatePoints()\n  }\n\n  private getProcessedData = (processedPoints: LatLng[]) => {\n    return this.pointData.filter((item) => processedPoints.includes(item.point))\n  }\n\n  private updatePoints(): void {\n    const currentZoom = this.map.getZoom()\n    const threshold = this.calculateThreshold(currentZoom)\n    // 获取当前地图边界 删选出相关数据\n    const bounds = this.map.getBounds()\n    const points = this.originalPoints.filter((item) => bounds.contains(item))\n    const simplifiedPoints = this.douglasPeucker(points, threshold)\n    const processedPoints = this.processPoints(\n      simplifiedPoints,\n      this.options.maxPointRange\n    )\n\n    this.options.callback &&\n      this.options.callback(this.getProcessedData(processedPoints))\n  }\n\n  private calculateThreshold(zoomLevel: number): number {\n    const maxZoom = this.map.getMaxZoom()\n    return 2 ** (maxZoom - zoomLevel) * this.options.thresholdFactor\n  }\n\n  private douglasPeucker(points: LatLng[], threshold: number): LatLng[] {\n    // 实现道格拉斯-普克算法，返回抽稀后的点集\n    // 注意：此处需用 TypeScript 实现该算法，返回类型为 LatLng[]\n    if (points.length < 3) {\n      return points // 如果点数小于3，直接返回原点集\n    }\n\n    let dmax = 0\n    let index = 0\n    for (let i = 1; i < points.length - 1; i++) {\n      const d = this.distanceToLineSegment(\n        points[0],\n        points[points.length - 1],\n        points[i]\n      )\n      if (d > dmax) {\n        index = i\n        dmax = d\n      }\n    }\n\n    if (dmax > threshold) {\n      const recResults1 = this.douglasPeucker(\n        points.slice(0, index + 1),\n        threshold\n      )\n      const recResults2 = this.douglasPeucker(\n        points.slice(index, points.length),\n        threshold\n      )\n      return [...recResults1.slice(0, -1), ...recResults2]\n    } else {\n      return [points[0], points[points.length - 1]]\n    }\n  }\n\n  private distanceToLineSegment(\n    start: LatLng,\n    end: LatLng,\n    point: LatLng\n  ): number {\n    const dx = end.lng - start.lng\n    const dy = end.lat - start.lat\n\n    if (dx === 0 && dy === 0) {\n      return this.distanceBetweenPoints(start, point)\n    }\n\n    const t =\n      ((point.lng - start.lng) * dx + (point.lat - start.lat) * dy) /\n      (dx * dx + dy * dy)\n\n    if (t < 0) {\n      return this.distanceBetweenPoints(start, point)\n    } else if (t > 1) {\n      return this.distanceBetweenPoints(end, point)\n    } else {\n      const projection = {\n        lat: start.lat + t * dy,\n        lng: start.lng + t * dx,\n      }\n      return this.distanceBetweenPoints(projection, point)\n    }\n  }\n  // 欧氏距离公式\n  // private distanceBetweenPoints(p1: LatLng, p2: LatLng): number {\n  //   const dx = p2.lng - p1.lng\n  //   const dy = p2.lat - p1.lat\n  //   return Math.sqrt(dx * dx + dy * dy)\n  // }\n  //\n  private distanceBetweenPoints(p1: LatLng, p2: LatLng): number {\n    return GPS.distance(p1.lat, p1.lng, p2.lat, p2.lng)\n  }\n  //  Haversine 公式计算地球上两点之间的距离（以公里为单位）\n  private processPoints(\n    simplifiedPoints: LatLng[],\n    maxPointRange: number\n  ): LatLng[] {\n    simplifiedPoints.forEach((point) => {\n      // 判断当前点位是否与已处理点位发生碰撞\n      const collidingPoints = this.rBush.search({\n        minX: point.lng - maxPointRange,\n        minY: point.lat - maxPointRange,\n        maxX: point.lng + maxPointRange,\n        maxY: point.lat + maxPointRange,\n      })\n      if (collidingPoints.length === 0) {\n        // 无碰撞，添加到已处理点集并更新空间索引\n        this.rBush.insert({\n          minX: point.lng,\n          minY: point.lat,\n          maxX: point.lng,\n          maxY: point.lat,\n          point,\n        })\n      } else {\n        // 发生碰撞，根据规则处理\n        const resolvedPoint = this.resolveCollision(\n          point,\n          collidingPoints,\n          this.options.collisionResolution\n        )\n        this.rBush.insert({\n          minX: resolvedPoint.lng,\n          minY: resolvedPoint.lat,\n          maxX: resolvedPoint.lng,\n          maxY: resolvedPoint.lat,\n          point: resolvedPoint,\n        })\n      }\n    })\n\n    return this.rBush.all().map((entry) => entry.point)\n  }\n\n  private resolveCollision(\n    newPoint: LatLng,\n    collidingPoints: any[],\n    strategy: CollisionResolutionStrategy\n  ): LatLng {\n    switch (strategy) {\n      case 'nearest':\n        return this.findNearest(collidingPoints, newPoint)\n      case 'average':\n        return this.resolveByAverage(newPoint, collidingPoints)\n      case 'weightedAverage':\n        return this.resolveByWeightedAverage(newPoint, collidingPoints)\n      case 'merge':\n        return this.resolveByMerge(newPoint, collidingPoints)\n      case 'discard':\n        return this.resolveByDiscard(newPoint)\n      case 'custom':\n        return this.resolveByCustom(newPoint, collidingPoints)\n      default:\n        throw new Error(\n          `Unsupported collision resolution strategy: ${strategy}`\n        )\n    }\n  }\n\n  /**\n   * 将新点与所有与其发生碰撞的点合并为一个新的点，位置取所有碰撞点的中心\n   * @param newPoint\n   * @param collidingPoints\n   * @returns\n   */\n  private resolveByMerge(newPoint: LatLng, collidingPoints: LatLng[]): LatLng {\n    const mergedPoint = {\n      lat: 0,\n      lng: 0,\n    }\n\n    for (const point of [newPoint, ...collidingPoints]) {\n      mergedPoint.lat += point.lat\n      mergedPoint.lng += point.lng\n    }\n\n    const count = collidingPoints.length + 1\n    mergedPoint.lat /= count\n    mergedPoint.lng /= count\n\n    return mergedPoint\n  }\n\n  /**\n   * 如果新点与已有点发生碰撞，忽略该新点，不添加到地图\n   * @param newPoint\n   * @param collidingPoints\n   * @returns\n   */\n  private resolveByDiscard(newPoint: LatLng): LatLng {\n    // 不做任何操作，直接返回新点，但在实际添加点到地图时会被忽略\n    return newPoint\n  }\n\n  /**\n   * 用户自定义的碰撞处理策略\n   * @param newPoint\n   * @param collidingPoints\n   * @returns\n   */\n  private resolveByCustom(newPoint: LatLng, collidingPoints: LatLng[]): LatLng {\n    if (typeof this.options.customCollisionResolver !== 'function') {\n      throw new TypeError(\n        '需要配置options 中 customCollisionResolver 参数 为回到函数模式'\n      )\n    }\n\n    return this.options.customCollisionResolver(newPoint, collidingPoints)\n  }\n\n  /**\n   * 选择与新点距离加权平均值最小的已存在点集，并以这些点的加权平均位置作为新点\n   * @param newPoint\n   * @param collidingPoints\n   * @returns\n   */\n  private resolveByWeightedAverage(\n    newPoint: LatLng,\n    collidingPoints: LatLng[]\n  ): LatLng {\n    const weightFunction = (point: LatLng) =>\n      1 / this.distanceBetweenPoints(newPoint, point) // 示例权重函数，距离越近权重越大\n    const sumLat = collidingPoints.reduce(\n      (sum, point) => sum + point.lat * weightFunction(point),\n      newPoint.lat\n    )\n    const sumLng = collidingPoints.reduce(\n      (sum, point) => sum + point.lng * weightFunction(point),\n      newPoint.lng\n    )\n    const sumWeight = collidingPoints.reduce(\n      (sum, point) => sum + weightFunction(point),\n      1\n    )\n\n    return {\n      lat: sumLat / sumWeight,\n      lng: sumLng / sumWeight,\n    }\n  }\n\n  /**\n   * 选择与新点距离平均值最小的已存在点集，并以这些点的平均位置作为新点\n   * @param newPoint\n   * @param collidingPoints\n   * @returns\n   */\n  private resolveByAverage(\n    newPoint: LatLng,\n    collidingPoints: LatLng[]\n  ): LatLng {\n    const sumLat = collidingPoints.reduce(\n      (sum, point) => sum + point.lat,\n      newPoint.lat\n    )\n    const sumLng = collidingPoints.reduce(\n      (sum, point) => sum + point.lng,\n      newPoint.lng\n    )\n    const count = collidingPoints.length + 1\n\n    return {\n      lat: sumLat / count,\n      lng: sumLng / count,\n    }\n  }\n  /**\n   *  选择与新点距离最近的已存在点\n   * @param collidingPoints\n   * @param point\n   * @returns\n   */\n  private findNearest(collidingPoints: LatLng[], point: LatLng): LatLng {\n    // 实现寻找最近点的功能，返回类型为 LatLng\n    let nearestPoint: LatLng | null = null\n    let minDistance = Number.POSITIVE_INFINITY\n    for (const candidate of collidingPoints) {\n      const { point: rubPoint } = candidate as any\n      const distance = this.distanceBetweenPoints(point, rubPoint)\n      if (distance < minDistance) {\n        minDistance = distance\n        nearestPoint = rubPoint\n      }\n    }\n\n    if (nearestPoint === null) {\n      throw new Error('No colliding points found')\n    }\n\n    return nearestPoint\n  }\n}\n\n// // 使用示例\n// const map = L.map('map').setView([0, 0], ½);\n// const originalPoints: LatLng[] = []; // 填充原始轨迹点数据\n// const pointClusterer = new PointClusterer(map, originalPoints, {\n//     thresholdFactor: 6,\n//     maxPointRange: 100,\n//     collisionResolution: 'nearest'\n// });\n"],"names":["PointClusterer","constructor","e","n","t","r","s","a","this","getProcessedData","pointData","filter","includes","point","map","originalPoints","o","options","thresholdFactor","maxPointRange","collisionResolution","callback","customCollisionResolver","rBush","l","init","on","updatePoints","getZoom","calculateThreshold","getBounds","contains","douglasPeucker","processPoints","getMaxZoom","length","distanceToLineSegment","slice","lng","lat","distanceBetweenPoints","u","distance","forEach","search","minX","minY","maxX","maxY","insert","resolveCollision","all","findNearest","resolveByAverage","resolveByWeightedAverage","resolveByMerge","resolveByDiscard","resolveByCustom","Error","TypeError","reduce","i","Number","POSITIVE_INFINITY"],"mappings":"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"}