Skip to content

海量点

当需要在地图上显示大量点标记时(如数万、数十万甚至百万级别的点),需要使用特殊的优化策略来保证性能。SDK 提供了聚类、LOD(细节层次)等功能来优化海量点的渲染。

聚类显示

聚类是将相近的点合并显示,减少渲染负担的有效方法。

启用聚类

javascript
// 添加带聚类的 GeoJSON 源
sdk.addGeoJSON("mass-points-source", geojson, {
  cluster: true,           // 启用聚类
  clusterRadius: 50,        // 聚类半径(像素)
  clusterMaxZoom: 16,      // 最大聚类缩放级别
  clusterMinPoints: 3,     // 最小聚类点数
});

// 或者使用 GeoJSONManager 配置
sdk.geoJSONManager.setClusteringEnabled(true);
sdk.geoJSONManager.setClusteringConfig("mass-points-source", {
  enabled: true,
  radius: 50,
  maxZoom: 16,
  minPoints: 3,
});

聚类样式

javascript
// 添加聚类点图层
sdk.addLayer({
  id: "clusters",
  source: "mass-points-source",
  type: "circle",
  filter: ["has", "point_count"], // 只显示聚类点
  paint: {
    "circle-color": [
      "step",
      ["get", "point_count"],
      "#51bbd6",   // 小于100个点:蓝色
      100,
      "#f1f075",   // 100-750个点:黄色
      750,
      "#f28cb1",   // 大于750个点:粉色
    ],
    "circle-radius": [
      "step",
      ["get", "point_count"],
      20,   // 小于100个点:半径20
      100,
      30,   // 100-750个点:半径30
      750,
      40,   // 大于750个点:半径40
    ],
    "circle-opacity": 0.8,
    "circle-stroke-width": 2,
    "circle-stroke-color": "#fff",
  },
});

// 添加聚类数量标签
sdk.addLayer({
  id: "cluster-count",
  source: "mass-points-source",
  type: "symbol",
  filter: ["has", "point_count"],
  layout: {
    "text-field": "{point_count_abbreviated}", // 显示数量
    "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
    "text-size": 12,
  },
  paint: {
    "text-color": "#fff",
  },
});

单个点样式

javascript
// 添加单个点图层(非聚类点)
sdk.addLayer({
  id: "unclustered-point",
  source: "mass-points-source",
  type: "circle",
  filter: ["!", ["has", "point_count"]], // 非聚类点
  paint: {
    "circle-radius": 8,
    "circle-color": "#11b4da",
    "circle-stroke-width": 2,
    "circle-stroke-color": "#fff",
  },
});

点击聚类展开

javascript
// 点击聚类时放大并展开
sdk.on("click", "clusters", (e) => {
  const features = sdk.map.queryRenderedFeatures(e.point, {
    layers: ["clusters"],
  });
  const clusterId = features[0].properties.cluster_id;
  
  sdk.getSource("mass-points-source").getClusterExpansionZoom(
    clusterId,
    (err, zoom) => {
      if (err) return;
      
      sdk.flyTo({
        center: e.lngLat,
        zoom: zoom,
        duration: 500,
      });
    }
  );
});

LOD(细节层次)优化

LOD 根据缩放级别动态显示/隐藏图层,减少低缩放级别时的渲染负担。

启用 LOD

javascript
// 启用 LOD
sdk.layerManager.setLODEnabled(true);

// 配置特定图层的 LOD
sdk.layerManager.setLayerLODConfig("mass-points-layer", {
  minZoom: 10,        // 最小显示缩放级别
  maxZoom: 18,        // 最大显示缩放级别
  minPixelSize: 10,   // 最小像素大小
  maxPixelSize: 100,  // 最大像素大小
  cullingEnabled: true, // 启用视锥剔除
});

动态 LOD 配置

javascript
// 根据缩放级别动态调整
sdk.on("zoom", () => {
  const zoom = sdk.getZoom();
  
  if (zoom < 8) {
    // 低缩放级别:隐藏详细点
    sdk.hideLayers(["mass-points-layer"]);
  } else if (zoom < 12) {
    // 中等缩放级别:显示简化点
    sdk.showLayers(["mass-points-layer"]);
    sdk.updateLayerStyle("mass-points-layer", {
      paint: {
        "circle-radius": 5,
      },
    });
  } else {
    // 高缩放级别:显示完整点
    sdk.updateLayerStyle("mass-points-layer", {
      paint: {
        "circle-radius": 8,
      },
    });
  }
});

数据分片加载

对于超大数据集,可以采用分片加载策略。

按区域分片

javascript
// 根据地图边界动态加载数据
sdk.on("moveend", () => {
  const bounds = sdk.getBounds();
  loadPointsInBounds(bounds);
});

async function loadPointsInBounds(bounds) {
  const [minLng, minLat] = bounds[0];
  const [maxLng, maxLat] = bounds[1];
  
  // 请求当前视野范围内的数据
  const data = await fetch(
    `/api/points?minLng=${minLng}&minLat=${minLat}&maxLng=${maxLng}&maxLat=${maxLat}`
  ).then(r => r.json());
  
  // 更新源数据
  sdk.getSource("mass-points-source").setData(data);
}

按缩放级别分片

javascript
// 根据缩放级别加载不同详细程度的数据
sdk.on("zoomend", () => {
  const zoom = sdk.getZoom();
  
  if (zoom < 8) {
    // 低缩放级别:加载概览数据
    loadOverviewData();
  } else if (zoom < 12) {
    // 中等缩放级别:加载中等详细数据
    loadMediumDetailData();
  } else {
    // 高缩放级别:加载详细数据
    loadDetailData();
  }
});

性能优化技巧

1. 简化样式

javascript
// 使用简单的样式减少渲染负担
sdk.addLayer({
  id: "mass-points",
  source: "mass-points-source",
  type: "circle",
  paint: {
    "circle-radius": 4,        // 较小的半径
    "circle-color": "#007bff", // 单一颜色
    "circle-opacity": 0.8,
    // 避免使用复杂表达式
  },
});

2. 限制可见范围

javascript
// 只显示视野范围内的点
sdk.addLayer({
  id: "mass-points",
  source: "mass-points-source",
  type: "circle",
  filter: [
    "all",
    [">=", ["get", "lng"], ["-", ["get", "minLng"], 0.1]],
    ["<=", ["get", "lng"], ["+", ["get", "maxLng"], 0.1]],
    [">=", ["get", "lat"], ["-", ["get", "minLat"], 0.1]],
    ["<=", ["get", "lat"], ["+", ["get", "maxLat"], 0.1]],
  ],
  paint: {
    "circle-radius": 4,
    "circle-color": "#007bff",
  },
});

3. 使用 Canvas 渲染

对于超大数据集,可以考虑使用 Canvas 2D 渲染:

javascript
// 使用 Canvas 渲染大量点
function renderPointsToCanvas(canvas, points, bounds) {
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  points.forEach(point => {
    const pixel = sdk.project([point.lng, point.lat]);
    ctx.fillStyle = "#007bff";
    ctx.beginPath();
    ctx.arc(pixel.x, pixel.y, 4, 0, Math.PI * 2);
    ctx.fill();
  });
}

4. 防抖更新

javascript
// 使用防抖减少更新频率
let updateTimeout;

sdk.on("moveend", () => {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(() => {
    updatePoints();
  }, 300); // 300ms 防抖
});

完整示例

javascript
let sdk;

async function initMap() {
  sdk = new navMap.MapSDK({
    container: "map",
    center: [116.39, 39.9],
    zoom: 10,
    performance: {
      lodEnabled: true,        // 启用 LOD
      clusteringEnabled: true,  // 启用聚类
    },
  });

  sdk.on("loadComplete", () => {
    setupMassPoints();
  });
}

async function setupMassPoints() {
  // 1. 生成或加载大量点数据
  const points = generateMassPoints(100000); // 10万个点
  
  const geojson = {
    type: "FeatureCollection",
    features: points.map((point, index) => ({
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: [point.lng, point.lat],
      },
      properties: {
        id: index,
        name: `Point ${index}`,
      },
    })),
  };

  // 2. 添加带聚类的源
  sdk.addGeoJSON("mass-points-source", geojson, {
    cluster: true,
    clusterRadius: 50,
    clusterMaxZoom: 16,
    clusterMinPoints: 3,
  });

  // 3. 添加聚类图层
  sdk.addLayer({
    id: "clusters",
    source: "mass-points-source",
    type: "circle",
    filter: ["has", "point_count"],
    paint: {
      "circle-color": [
        "step",
        ["get", "point_count"],
        "#51bbd6",
        100,
        "#f1f075",
        750,
        "#f28cb1",
      ],
      "circle-radius": [
        "step",
        ["get", "point_count"],
        20,
        100,
        30,
        750,
        40,
      ],
    },
  });

  // 4. 添加聚类标签
  sdk.addLayer({
    id: "cluster-count",
    source: "mass-points-source",
    type: "symbol",
    filter: ["has", "point_count"],
    layout: {
      "text-field": "{point_count_abbreviated}",
      "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
      "text-size": 12,
    },
  });

  // 5. 添加单个点图层
  sdk.addLayer({
    id: "unclustered-point",
    source: "mass-points-source",
    type: "circle",
    filter: ["!", ["has", "point_count"]],
    paint: {
      "circle-radius": 4,
      "circle-color": "#11b4da",
    },
  });

  // 6. 配置 LOD
  sdk.layerManager.setLayerLODConfig("mass-points-layer", {
    minZoom: 8,
    maxZoom: 18,
  });

  // 7. 点击聚类展开
  sdk.on("click", "clusters", (e) => {
    const features = sdk.map.queryRenderedFeatures(e.point, {
      layers: ["clusters"],
    });
    const clusterId = features[0].properties.cluster_id;
    
    sdk.getSource("mass-points-source").getClusterExpansionZoom(
      clusterId,
      (err, zoom) => {
        if (err) return;
        sdk.flyTo({
          center: e.lngLat,
          zoom: zoom,
          duration: 500,
        });
      }
    );
  });
}

function generateMassPoints(count) {
  const points = [];
  for (let i = 0; i < count; i++) {
    points.push({
      lng: 116 + Math.random() * 5,  // 116-121
      lat: 39 + Math.random() * 5,    // 39-44
    });
  }
  return points;
}

initMap();

性能监控

javascript
// 监控渲染性能
sdk.on("render", () => {
  const perfStats = sdk.getPerformanceStats();
  console.log("可见图层数:", perfStats.visibleLayers);
  console.log("LOD启用:", perfStats.lodEnabled);
  console.log("聚类启用:", perfStats.clusteringEnabled);
});

注意事项

  1. 数据量控制:单次加载的点数建议不超过 10 万
  2. 聚类配置:根据实际数据密度调整聚类参数
  3. LOD 配置:合理设置最小/最大缩放级别
  4. 样式简化:避免使用复杂表达式和动态样式
  5. 分片加载:超大数据集必须使用分片加载
  6. 性能监控:定期检查渲染性能,及时优化