基于百度地图的聚合排布运算

发布时间 2023-12-05 17:17:55作者: 卓扬

基于百度地图点位的聚合运算,在一定阈值(像素范围)内自动聚合,聚合后按照圆形重新均匀排布

一.聚合运算类

import _ from 'lodash';

/**
 * 获取一个扩展的视图范围,把上下左右都扩大一样的像素值。
 * @param {*} map BMap.Map的实例化对象
 * @param {*} bounds BMap.Bounds的实例化对象
 * @param {*} gridSize 要扩大的像素值
 * @returns
 */
const getExtendedBounds = function (map, bounds, gridSize) {
  bounds = cutBoundsInRange(bounds);
  const pixelNE = map.pointToPixel(bounds.getNorthEast());
  const pixelSW = map.pointToPixel(bounds.getSouthWest());
  pixelNE.x += gridSize;
  pixelNE.y -= gridSize;
  pixelSW.x -= gridSize;
  pixelSW.y += gridSize;
  const newNE = map.pixelToPoint(pixelNE);
  const newSW = map.pixelToPoint(pixelSW);
  return new BMapGL.Bounds(newSW, newNE);
};
/**
 * 按照百度地图支持的世界范围对bounds进行边界处理
 * @param {*} bounds BMap.Bounds的实例化对象
 * @returns 返回不越界的视图范围
 */
const cutBoundsInRange = function (bounds) {
  const maxX = _.clamp(bounds.getNorthEast().lng, -180, 180);
  const minX = _.clamp(bounds.getSouthWest().lng, -180, 180);
  const maxY = _.clamp(bounds.getNorthEast().lat, -74, 74);
  const minY = _.clamp(bounds.getSouthWest().lat, -74, 74);
  return new BMapGL.Bounds(new BMapGL.Point(minX, minY), new BMapGL.Point(maxX, maxY));
};
const isArray = function (source) {
  return '[object Array]' === Object.prototype.toString.call(source);
};
const indexOf = function (item, source) {
  let index = -1;
  if (isArray(source)) {
    if (source.indexOf) {
      index = source.indexOf(item);
    } else {
      for (let i = 0, m; (m = source[i]); i++) {
        if (m === item) {
          index = i;
          break;
        }
      }
    }
  }
  return index;
};

/**
 * 聚合集
 */
class MarkerClusterer {
  /**
   * MarkerClusterer
   * 用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能
   * @param {map} map 地图的一个实例。
   * @param {options} options 可选参数,可选项包括:<br />
   * gridSize {Number} 聚合计算时网格的像素大小,默认60<br />
   * onClustersChange {func} 聚合对象集变化的回调<br />
   */
  constructor(map, { gridSize, onClustersChange }) {
    if (!map) {
      return;
    }
    this._map = map;
    // 原始点集合
    this._orginMarkers = [];
    // 聚合集
    this._clusters = [];
    this._gridSize = gridSize || 60;
    this._onClustersChange = onClustersChange;
    this._map.addEventListener('zoomend', this._redraw);
    // this._map.addEventListener('moveend', this._redraw);
  }

  /**
   * 添加一组聚合的标记
   * @param {MyMarker[]} markers
   */
  addMarkers = (markers) => {
    _.each(markers, (marker) => {
      const index = indexOf(marker, this._orginMarkers);
      if (index === -1) {
        this._orginMarkers.push(marker);
      }
    });
    this._computeClusters();
  };

  /**
   * 根据所给定的标记计算聚合
   */
  _computeClusters = () => {
    // 获取地图边界
    const mapBounds = this._map.getBounds();
    // 扩展视图边界
    const extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize);
    // 遍历所有的点并判断是否需要聚合
    _.each(this._orginMarkers, (marker) => {
      //在视图扩展边界之内
      if (extendedBounds.containsPoint(marker.getPosition())) {
        let distance = Infinity; // 无限大
        let matchCluster = null; // 最匹配的聚合对象
        // 查找跟当前marker最匹配的聚合对象
        _.each(this._clusters, (cluster) => {
          // 在聚合对象网格范围内查找最近的聚合对象
          if (cluster.isMarkerInClusterBounds(marker)) {
            // 获取marker与聚合对象中心的距离,单位米
            const d = this._map.getDistance(cluster.getCenter(), marker.getPosition());
            // 按照距离最近的聚合对象进行匹配
            if (d < distance) {
              distance = d;
              matchCluster = cluster;
            }
          }
        });
        // 当前marker是否有适配的聚合对象
        if (matchCluster) {
          matchCluster.pushMarker(marker);
        } else {
          // 无匹配聚合对象则新建聚合对象
          const cluster = new Cluster(this, '聚合' + this._clusters.length);
          cluster.pushMarker(marker);
          this._clusters.push(cluster);
        }
      }
    });
    // 改变后的聚合对象返回
    this._onClustersChange && this._onClustersChange(this._clusters);
  };

  /**
   * 清除所有聚合对象
   */
  _clearAllClusters = () => {
    _.each(this._clusters, (cluster) => {
      // 聚合对象清除内部
      cluster.remove();
    });
    // 清除所有聚合对象
    this._clusters.length = 0;
  };

  /**
   * 销毁
   */
  dispose = () => {
    this._clearAllClusters();
    this._orginMarkers.length = 0;
    this._clusters.length = 0;
    this._map.removeEventListener('zoomend', this._redraw);
    this._map = null;
  };

  /**
   * 重绘
   */
  _redraw = () => {
    // 清除聚合
    this._clearAllClusters();
    // 计算聚合
    this._computeClusters();
  };

  getGridSize = () => {
    return this._gridSize;
  };

  getMap = () => {
    return this._map;
  };
}

/**
 * 聚合对象
 */
class Cluster {
  // 网格边界
  _gridBounds = null;
  // 聚合对象中心
  _center = null;
  // 聚合对象中marker集合
  _markers = [];
  // 聚合对象名称
  _name = null;
  // 中心指示线
  _lines = [];

  constructor(markerClusterer, name) {
    this._name = name;
    this._markerClusterer = markerClusterer;
    this._map = markerClusterer.getMap();
  }

  /**
   * marker加入本聚合
   * @param {MyMarker} marker
   * @returns
   */
  pushMarker = (marker) => {
    if (this.isMarkerInCluster(marker)) {
      return false;
    }

    // 加入marker集合
    this._markers.push(marker);
    // 加入marker后重新计算中心点
    if (!this._center) {
      this._center = marker.getPosition();
    } else {
      const l = this._markers.length + 1;
      const lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l;
      const lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l;
      this._center = new BMapGL.Point(lng, lat);
    }
    // 更新边界
    this.updateGridBounds();

    const len = this._markers.length;
    // 小于2不成聚合
    if (len >= 2) {
      // 重新按圆形分配位置
      const radian = 360 / len; // 等分圆形的弧度
      const radiu = this._markerClusterer.getGridSize(); // 像素半径
      this._lines.length = 0;
      _.each(this._markers, (marker, i) => {
        const currentRadian = radian * i;
        // 转为像素
        const xy = this._map.pointToPixel(this._center);
        const newXY = {
          x: xy.x + radiu * Math.cos(currentRadian),
          y: xy.y - radiu * Math.sin(currentRadian),
        };

        const newPos = this._map.pixelToPoint(newXY);
        marker.setPosition(newPos);

        // 指示线
        this._lines.push([this._center, newPos]);
      });
    } else {
      // 清除位置偏移
      marker.setPosition(null);
    }
  };

  /**
   * 是否已参与本聚合
   * @param {MyMarker} marker
   * @returns
   */
  isMarkerInCluster = (marker) => {
    if (this._markers.indexOf) {
      return this._markers.indexOf(marker) != -1;
    } else {
      for (let i = 0, m; (m = this._markers[i]); i++) {
        if (m === marker) {
          return true;
        }
      }
    }
    return false;
  };

  /**
   * 是否在本聚合网格范围中
   * @param {MyMarker} marker
   * @returns
   */
  isMarkerInClusterBounds = (marker) => {
    return this._gridBounds.containsPoint(marker.getPosition());
  };

  /**
   * 更新本聚合的网格范围
   */
  updateGridBounds = () => {
    const bounds = new BMapGL.Bounds(this._center, this._center);
    this._gridBounds = getExtendedBounds(this._map, bounds, this._markerClusterer.getGridSize());
  };

  /**
   * 清除本聚合所有元素
   */
  remove = () => {
    this._gridBounds = null;
    this._center = null;
    this._name = null;
    this._markers.length = 0;
    this._lines.length = 0;
    this._map = null;
    this._markerClusterer = null;
  };

  /**
   * 获取本聚合对象边界
   * @returns
   */
  getBounds = () => {
    const bounds = new BMapGL.Bounds(this._center, this._center);
    _.each(this._markers, (marker) => {
      bounds.extend(marker.getPosition());
    });
    return bounds;
  };

  /**
   * 获取本聚合对象中心
   * @returns
   */
  getCenter = () => {
    return this._center;
  };

  get isReal() {
    return this._markers.length > 1;
  }
}

/**
 * 标记对象
 */
class MyMarker {
  // 原始位置
  originPosition = null;
  // 新位置
  newPosition = null;

  constructor(point, key, title) {
    this.originPosition = new BMapGL.Point(point.lng, point.lat);
    this._title = title;
    this._key = key;
  }

  /**
   * 设置新位置
   * @param {BMapGL.Point} point
   */
  setPosition = (point) => {
    this.newPosition = point;
  };

  /**
   * 获取原始位置
   * @returns
   */
  getPosition = () => {
    return this.originPosition;
  };

  /**
   * 获取最终位置
   * @returns
   */
  getFinalPosition = () => {
    return this.newPosition || this.originPosition;
  };

  get key() {
    return this._key;
  }
}

export { MarkerClusterer, MyMarker };

二.React中使用

import icon from '@/assets/icon/0.png';
import { Space } from 'antd';
import moment from 'moment';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { CustomOverlay, Polyline } from 'react-bmapgl';
import styles from './Overlays.less';
import { MarkerClusterer, MyMarker } from './markerClusterer';

const pointFunc = (value) => {
  if (value) {
    const v = value.split(',');
    return { lng: v[0], lat: v[1] };
  } else {
    return { lng: 121.698163, lat: 31.730767 };
  }
};

const badge = (count) => {
  if (count > 0) {
    return <span className={styles.badge}>{count > 100 ? '99+' : count}</span>;
  }
};

const Index = ({ map, list, onMarkerClick }) => {
  const [current, setCurrent] = useState(null);
  const [areas, setAreas] = useState([]);
  const [lines, setLines] = useState([]);
  const clusterRef = useRef();

  useEffect(() => {
    clusterRef.current = new MarkerClusterer(map, {
      onClustersChange: (data) => {
        const _areas = [];
        let _lines = [];
        _.each(data, (o) => {
          _.each(o._markers, (m) => {
            const area = _.find(list, { guid: m.key });
            if (area) {
              _areas.push({ ...area, position: m.getFinalPosition() });
            }
          });
          _lines = _lines.concat(o._lines);
        });
        setAreas(_areas);
        setLines(_lines);
      },
    });

    return () => {
      clusterRef.current.dispose();
      clusterRef.current = null;
    };
  }, []);

  useEffect(() => {
    if (clusterRef.current && list) {
      const markers = _.map(list, (o) => new MyMarker(pointFunc(o.position), o.guid, o.name));
      clusterRef.current.addMarkers(markers);
    }
  }, [list]);

  const renderMarker = useMemo(() => {
    const markers = _.map(areas, (o, i) => (
      <CustomOverlay
        position={o.position}
        offset={new BMapGL.Size(-5, 38)}
        unit="px"
        zIndex={current === i.toString() ? 99 : 2}
        key={i}
      >
        <div
          className={styles.marker}
          onMouseOver={() => {
            setCurrent(i.toString());
          }}
          onMouseOut={() => {
            setCurrent(null);
          }}
          onClick={() => {
            onMarkerClick && onMarkerClick(o);
          }}
        >
          <img width={25} src={icon} />
          <div className={styles.label}>
            <Space>
              {o.name} {badge(o.count)}
            </Space>
            {o.areaSpeed && (
              <div>
                <span>速度:{o.areaSpeed.speed}</span>
                <span style={{ marginLeft: '10px' }}>
                  {moment(o.areaSpeed.time).format('MM-DD HH:mm')}
                </span>
              </div>
            )}
          </div>
        </div>
      </CustomOverlay>
    ));
    return markers;
  }, [areas, current]);

  const renderLine = useMemo(() => {
    return _.map(lines, (o, i) => <Polyline path={o} strokeWeight={1} key={i} />);
  }, [lines]);

  return (
    <>
      {renderMarker}
      {renderLine}
    </>
  );
};

export default memo(Index);

三.示例图