当前位置: 首页 > ds >正文

OpenLayers 综合案例-点位聚合

看过的知识不等于学会。唯有用心总结、系统记录,并通过温故知新反复实践,才能真正掌握一二
作为一名摸爬滚打三年的前端开发,开源社区给了我饭碗,我也将所学的知识体系回馈给大家,助你少走弯路!
OpenLayers、Leaflet 快速入门 ,每周保持更新 2 个案例
Cesium 快速入门,每周保持更新 4 个案例

OpenLayers 综合案例-点位聚合

Vue 3 + OpenLayers 实现的 WebGIS 应用提供了完整的点位聚合功能

主要功能

  1. 使用 Cluster 实现点数据的聚合展示
  2. 动态调整聚合距离
  3. 生成随机点数据
  4. 动画效果开关控制,动画无实际作用

在这里插入图片描述
MP4效果动画

<template><div class="cluster-map-container"><div ref="mapContainer" class="map"></div><div class="map-controls"><div class="control-section"><h3>点聚合控制</h3><div class="control-group"><button class="control-btn" @click="generateRandomPoints(100)"><span class="icon">🎲</span> 生成100点</button><button class="control-btn" @click="generateRandomPoints(500)"><span class="icon">🎲</span> 生成500点</button></div><div class="control-group"><button class="control-btn" @click="clearAllPoints"><span class="icon">🗑️</span> 清除所有点</button><button class="control-btn" @click="toggleAnimation"><span class="icon">{{ animationEnabled ? "⏸️" : "▶️" }}</span>{{ animationEnabled ? "暂停动画" : "开启动画" }}</button></div><div class="slider-group"><label for="distance">聚合距离: {{ clusterDistance }} 像素</label><inputtype="range"id="distance"min="10"max="200"v-model="clusterDistance"@input="updateClusterDistance"/></div></div><div class="stats-section"><h3>统计信息</h3><div class="stats-item"><div class="stats-label">总点数:</div><div class="stats-value">{{ totalPoints }}</div></div><div class="stats-item"><div class="stats-label">聚合点数:</div><div class="stats-value">{{ clusterCount }}</div></div><div class="stats-item"><div class="stats-label">显示比例:</div><div class="stats-value">{{ displayRatio }}%</div></div></div></div><div class="coordinates-display"><div class="coords-label">当前坐标:</div><div class="coords-value">{{ coordinates }}</div><div class="zoom-level">缩放级别: {{ currentZoom.toFixed(2) }}</div></div><div class="animation-points"><divv-for="(point, index) in animatedPoints":key="index"class="animated-point":style="{left: point.x + 'px',top: point.y + 'px',backgroundColor: point.color,}"></div></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, computed } from "vue";
import Map from "ol/Map";
import View from "ol/View";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import { XYZ, Vector as VectorSource, Cluster } from "ol/source";
import { Point } from "ol/geom";
import Feature from "ol/Feature";
import { Style, Fill, Stroke, Circle, Text } from "ol/style";
import { defaults as defaultControls, FullScreen, ScaleLine } from "ol/control";
import { fromLonLat, toLonLat } from "ol/proj";
import "ol/ol.css";// 地图实例
const map = ref(null);
const mapContainer = ref(null);
const vectorSource = ref(null);
const clusterSource = ref(null);// 坐标显示
const coordinates = ref("经度: 0.00, 纬度: 0.00");
const currentZoom = ref(0);
const clusterDistance = ref(60);
const animationEnabled = ref(true);
const animatedPoints = ref([]);
const animationInterval = ref(null);// 点数据统计
const totalPoints = ref(0);
const clusterCount = ref(0);// 计算显示比例
const displayRatio = computed(() => {if (totalPoints.value === 0) return 0;return ((clusterCount.value / totalPoints.value) * 100).toFixed(1);
});// 初始化地图
onMounted(() => {// 创建矢量数据源vectorSource.value = new VectorSource();// 创建聚合数据源clusterSource.value = new Cluster({source: vectorSource.value,distance: clusterDistance.value,});// 创建聚合图层样式const clusterStyle = (feature) => {const size = feature.get("features").length;const radius = Math.min(20 + Math.sqrt(size) * 5, 40);const color =size > 50? "#d32f2f": size > 20? "#f57c00": size > 5? "#1976d2": "#388e3c";return new Style({image: new Circle({radius: radius,fill: new Fill({ color: `${color}80` }),stroke: new Stroke({color: "#fff",width: 3,}),}),text: new Text({text: size.toString(),fill: new Fill({ color: "#fff" }),font: "bold 16px sans-serif",}),});};// 创建聚合图层const clusterLayer = new VectorLayer({source: clusterSource.value,style: clusterStyle,});// 创建高德地图图层const baseLayer = new TileLayer({source: new XYZ({url: "https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}",}),});// 创建地图map.value = new Map({target: mapContainer.value,layers: [baseLayer, clusterLayer],view: new View({center: fromLonLat([116.4, 39.9]), // 北京zoom: 10,}),controls: defaultControls().extend([new FullScreen(), new ScaleLine()]),});// 添加坐标显示事件map.value.on("pointermove", (event) => {const coord = toLonLat(event.coordinate);coordinates.value = `经度: ${coord[0].toFixed(4)}, 纬度: ${coord[1].toFixed(4)}`;});// 监听缩放变化map.value.getView().on("change:resolution", () => {currentZoom.value = map.value.getView().getZoom();updateClusterStats();});// 监听聚合源变化clusterSource.value.on("change", updateClusterStats);// 初始生成一些点generateRandomPoints(100);// 启动动画startAnimation();
});// 更新聚合统计信息
function updateClusterStats() {const features = clusterSource.value.getFeatures();clusterCount.value = features.length;// 计算所有聚合点包含的总点数let total = 0;features.forEach((feature) => {total += feature.get("features").length;});totalPoints.value = total;
}// 更新聚合距离
function updateClusterDistance() {clusterSource.value.setDistance(parseInt(clusterDistance.value));
}// 生成随机点
function generateRandomPoints(count) {const view = map.value.getView();const extent = view.calculateExtent(map.value.getSize());let points = []; // 优化后方法console.time("500点位生成所需时间");for (let i = 0; i < count; i++) {const x = extent[0] + Math.random() * (extent[2] - extent[0]);const y = extent[1] + Math.random() * (extent[3] - extent[1]);const point = new Feature({geometry: new Point([x, y]),id: `point-${Date.now()}-${i}`,});points.push(point); // 优化后方法// vectorSource.value.addFeature(point); // 开始方法}vectorSource.value.addFeatures(points); // 优化后方法console.timeEnd("500点位生成所需时间");// 触发动画效果if (animationEnabled.value) {animatePoints(count);}
}// 清除所有点
function clearAllPoints() {vectorSource.value.clear();totalPoints.value = 0;clusterCount.value = 0;animatedPoints.value = [];
}// 点动画效果
function animatePoints(count) {const newPoints = [];const colors = ["#FF5252","#FF4081","#E040FB","#7C4DFF","#536DFE","#448AFF","#40C4FF","#18FFFF","#64FFDA","#69F0AE",];for (let i = 0; i < Math.min(count, 50); i++) {newPoints.push({x: Math.random() * window.innerWidth,y: Math.random() * window.innerHeight,size: Math.random() * 20 + 10,color: colors[Math.floor(Math.random() * colors.length)],life: 100,});}animatedPoints.value = [...animatedPoints.value, ...newPoints];
}// 开始动画
function startAnimation() {if (animationInterval.value) clearInterval(animationInterval.value);animationInterval.value = setInterval(() => {if (!animationEnabled.value) return;// 更新动画点animatedPoints.value = animatedPoints.value.map((p) => ({ ...p, life: p.life - 2 })).filter((p) => p.life > 0);// 添加新点if (Math.random() > 0.7) {animatePoints(1);}}, 100);
}// 切换动画状态
function toggleAnimation() {animationEnabled.value = !animationEnabled.value;if (animationEnabled.value) {startAnimation();} else {if (animationInterval.value) {clearInterval(animationInterval.value);animationInterval.value = null;}}
}// 组件卸载时清理
onUnmounted(() => {if (map.value) {map.value.dispose();}if (animationInterval.value) {clearInterval(animationInterval.value);}
});
</script><style scoped>
.cluster-map-container {position: relative;width: 100vw;height: 100vh;overflow: hidden;background: linear-gradient(135deg, #1a237e, #4a148c);font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}.map {width: 100%;height: 100%;background: #0d47a1;
}.map-controls {position: absolute;top: 20px;right: 20px;background: rgba(255, 255, 255, 0.92);border-radius: 12px;padding: 20px;box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);z-index: 1;width: 320px;backdrop-filter: blur(5px);border: 1px solid rgba(255, 255, 255, 0.3);
}.control-section h3,
.stats-section h3 {margin-top: 0;margin-bottom: 15px;padding-bottom: 10px;border-bottom: 2px solid #3f51b5;color: #1a237e;font-size: 1.4rem;
}.control-group {display: grid;grid-template-columns: 1fr 1fr;gap: 10px;margin-bottom: 15px;
}.control-btn {padding: 12px 15px;border: none;border-radius: 8px;background: #3f51b5;color: white;font-weight: 600;cursor: pointer;transition: all 0.3s ease;display: flex;align-items: center;justify-content: center;gap: 8px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}.control-btn:hover {background: #303f9f;transform: translateY(-2px);box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}.control-btn .icon {font-size: 1.2rem;
}.slider-group {margin-top: 20px;padding: 10px 0;
}.slider-group label {display: block;margin-bottom: 10px;font-weight: 600;color: #1a237e;
}.slider-group input {width: 100%;height: 8px;border-radius: 4px;background: #e0e0e0;outline: none;-webkit-appearance: none;
}.slider-group input::-webkit-slider-thumb {-webkit-appearance: none;width: 20px;height: 20px;border-radius: 50%;background: #3f51b5;cursor: pointer;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}.stats-section {margin-top: 25px;padding-top: 20px;border-top: 1px solid #eee;
}.stats-item {display: flex;justify-content: space-between;padding: 10px 0;border-bottom: 1px solid #f5f5f5;
}.stats-label {font-weight: 500;color: #333;
}.stats-value {font-weight: 700;color: #3f51b5;font-size: 1.1rem;
}.coordinates-display {position: absolute;bottom: 40px;left: 20px;background: rgba(255, 255, 255, 0.92);border-radius: 10px;padding: 15px 20px;box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);z-index: 1;display: flex;flex-direction: column;gap: 5px;min-width: 260px;backdrop-filter: blur(5px);border: 1px solid rgba(255, 255, 255, 0.3);
}.coords-label {font-weight: 600;color: #3f51b5;font-size: 0.9rem;
}.coords-value {font-family: "Courier New", monospace;font-size: 1.1rem;color: #1a237e;font-weight: bold;
}.zoom-level {font-family: "Courier New", monospace;font-size: 1rem;color: #f57c00;font-weight: bold;margin-top: 5px;
}.animation-points {position: absolute;top: 0;left: 0;width: 100%;height: 100%;pointer-events: none;z-index: 0;
}.animated-point {position: absolute;width: 12px;height: 12px;border-radius: 50%;transform: translate(-50%, -50%);box-shadow: 0 0 15px currentColor;opacity: 0.7;animation: float 3s infinite ease-in-out;
}@keyframes float {0% {transform: translate(-50%, -50%) scale(1);opacity: 0.7;}50% {transform: translate(-50%, -60%) scale(1.2);opacity: 0.9;}100% {transform: translate(-50%, -50%) scale(1);opacity: 0.7;}
}
</style>
http://www.xdnf.cn/news/16545.html

相关文章:

  • 【通识】线性代数(Linear Algebra)
  • 深度学习在计算机视觉中的应用:对象检测
  • 从 .NET Framework 到 .NET 8:跨平台融合史诗与生态演进全景
  • 设计模式(十一)结构型:外观模式详解
  • ESP32步进电机控制实战:从原理到代码实现
  • JAVA秋招学习指南
  • 【Java实例】服务器IP一站式管理
  • linux 部署 flink 1.15.1 并提交作业
  • ios UIAppearance 协议
  • 元宇宙背景下治理模式:自治的乌托邦
  • 移植pbrt中的并行化到ray trace in weeks中
  • 268. 丢失的数字
  • RocksDB跳表MemTable优化揭秘
  • Java 集合进阶:从 Collection 接口到迭代器的实战指南
  • Containerd简介
  • 栈算法之【有效括号】
  • mybatis-plus从入门到入土(三):持久层接口之IService
  • Day 22: 复习
  • OTG原理讲解
  • 进制间的映射关系
  • 【RHCSA 问答题】第 12 章 安装和更新软件包
  • WorkManager vs Flow 适用场景分析
  • CSS变量与Houdini自定义属性:解锁样式编程新维度
  • [硬件电路-94]:模拟器件 - 信号耦合,让被放大信号与静态工作点的直流偏置信号完美的融合
  • 慧星云新增大模型服务:多款大模型轻松调用
  • 编程语言Java——核心技术篇(四)集合类详解
  • Go的内存管理和垃圾回收
  • 震网(Stuxnet):打开潘多拉魔盒的数字幽灵
  • 网络:基础概念
  • React入门指南——指北指南(第二节)