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.pswp.cn/news/916453.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/916453.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

测试老鸟整理,物流项目系统测试+测试点分析(一)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 物流项目&#xf…

好的编程语言设计是用简洁清晰的原语组合复杂功能

首先&#xff0c;函数命名要user friendly&#xff0c;比如最常用的控制台输入输出&#xff0c;input scanf gets read readln readline print println writeline… 我专门询问了chatgpt&#xff0c;让它给出流行度百分比最高的组合&#xff08;ai干这个最在行&#xff09;&…

基于springboot的在线购票系统/在线售票系统

用户&#xff1a;注册&#xff0c;登录&#xff0c;影院信息&#xff0c;即将上映&#xff0c;电影信息&#xff0c;新闻公告&#xff0c;取票管理&#xff0c;电影评价管理&#xff0c;我的收藏管理&#xff0c;个人中心管理员&#xff1a;登录&#xff0c;个人中心&#xff0…

Spring Boot项目打包部署常见问题解决方案

问题一&#xff1a;JAR包缺少主清单属性 问题描述 在使用 java -jar 命令启动Spring Boot项目时&#xff0c;遇到以下错误&#xff1a; demo-service.jar中没有主清单属性问题原因 pom.xml 中 spring-boot-maven-plugin 配置不正确打包时跳过了主清单文件的生成主类&#xff08…

【分享】外国使馆雷电综合防护系统改造方案(一)

1防雷项目设计思想&#xff1a;1.1设计依据&#xff1a;依据中国GB标准与部委颁发的设计规范的要求&#xff0c;该建筑物和大楼内之计算机房等设备都必须有完整完善之防护措施&#xff0c;保证该系统能正常运作。这包括电源供电系统、不间断供电系统&#xff0c;空调设备、电脑…

数据结构预备知识

在学习数据结构之前&#xff0c;有些知识是很有必要提前知道的&#xff0c;它们包括&#xff1a;集合框架、复杂度和泛型。本篇文章专门介绍这三个东西。1.集合框架1.1 什么是集合框架Java 集合框架(Java Collection Framework)&#xff0c;又被称为容器&#xff0c;是定义在 j…

【C++】数字cmath库常用函数

菜鸟传送门&#xff1a;https://www.runoob.com/cplusplus/cpp-numbers.html 作者废话&#xff1a;作为一个从业3年的JS人&#xff0c;现在重拾C&#xff0c;虽然众多语言都有很多相似之处&#xff08;至少算法&#xff0c;数学运算&#xff0c;数据结构等等那些都是相同的&…

神经网络(第二课第一周)

文章目录神经网络&#xff08;第二课第一周&#xff09;&#xff08;一&#xff09;神经网络的内涵&#xff08;二&#xff09;如何构建神经元层1、tensorflow如何处理数据&#xff08;Tensorflow 是由 Google 开发的机器学习包。&#xff09;2、详细的一些实验代码&#xff0c…

CCF-GESP 等级考试 2025年6月认证C++七级真题解析

1 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09;第1题 已知小写字母 b 的ASCII码为98&#xff0c;下列C代码的输出结果是&#xff08; &#xff09;。#include <iostream>using namespace std;int main() { char a b ^ 4; cout << a; …

【HarmonyOS】鸿蒙应用开发中常用的三方库介绍和使用示例

【HarmonyOS】鸿蒙应用开发中常用的三方库介绍和使用示例 截止到2025年&#xff0c;目前参考官方文档&#xff1a;访问 HarmonyOS三方库中心 。梳理了以下热门下载量和常用的三方库。 上述库的组合&#xff0c;可快速实现网络请求、UI搭建、状态管理等核心功能&#xff0c;显著…

SpringBoot 获取请求参数的常用注解

SpringBoot 提供了多种注解来方便地从 HTTP 请求中获取参数以下是主要的注解及其用法&#xff1a;1. RequestParam用于获取查询参数(URL 参数)&#xff0c;适用于 GET 请求或 POST 表单提交。GetMapping("/user") public String getUser(RequestParam("id"…

【Linux篇章】Socket 套接字,竟让 UDP 网络通信如此丝滑,成为一招致胜的秘籍!

本篇文章将带大家了解网络通信是如何进行的&#xff08;如包括网络字节序&#xff0c;端口号&#xff0c;协议等&#xff09; &#xff1b;再对socket套接字进行介绍&#xff1b;以及一些udp-socket相关网络通信接口的介绍及使用&#xff1b;最后进行对基于udp的网络通信&#…

GIF图像格式

你可能已经知道&#xff0c;GIF 是一种光栅图像文件格式&#xff0c;它在不损失图像质量的前提下提供压缩功能&#xff0c;并且支持动画和透明度。 GIF 是“Graphics Interchange Format&#xff08;图形交换格式&#xff09;”的缩写。由于其良好的兼容性以及在不同应用程序和…

D3.js的力导向图使用入门笔记

D3.js是一个用于数据可视化的JavaScript库,广泛应用于Web端的数据交互式图形展示 中文文档&#xff1a;入门 | D3 中文网 一、D3.js核心特点 1、核心思想 将数据绑定到DOM元素&#xff0c;通过数据动态生成/修改可视化图形。 2、应用场景 交互式图表&#xff1a;如动态条…

Zookeeper的分布式事务与原子性:深入解析与实践指南

引言在分布式系统架构中&#xff0c;事务管理和原子性保证一直是极具挑战性的核心问题。作为分布式协调服务的标杆&#xff0c;Apache Zookeeper提供了一套独特而强大的机制来处理分布式环境下的原子操作。本文将深入探讨Zookeeper如何实现分布式事务的原子性保证&#xff0c;分…

Lua(迭代器)

Lua 迭代器基础概念Lua 迭代器是一种允许遍历集合&#xff08;如数组、表&#xff09;元素的机制。迭代器通常由两个部分组成&#xff1a;迭代函数和状态控制变量。每次调用迭代函数会返回集合中的下一个元素。泛型 for 循环Lua 提供了泛型 for 循环来简化迭代器的使用。语法如…

发布 VS Code 扩展的流程:以颜色主题为例

发布 VS Code 扩展的流程&#xff1a;以颜色主题为例 引言&#xff1a;您的 VS Code 扩展在市场中的旅程 Visual Studio Code (VS Code) 的强大扩展性是其广受欢迎的核心原因之一&#xff0c;它允许开发者通过添加语言支持、调试器和各种开发工具来定制和增强其集成开发环境&…

C++ 多线程(一)

C 多线程&#xff08;一&#xff09;1.std中的thread API 介绍开启一个线程获取线程信息API交换两个线程2.向线程里传递参数的方法第一种方式&#xff08;在创建线程的构造函数后携带参数&#xff09;第二种方式&#xff08;Lambda&#xff09;第三种方式&#xff08;成员函数&…

自动驾驶训练-tub详解

在 Donkeycar 的环境里&#xff0c;“tub” 是一个很关键的术语&#xff0c;它代表的是存储训练数据的目录。这些数据主要来源于自动驾驶模型训练期间收集的图像和控制指令。 Tub 的构成 一个标准的 tub 目录包含以下两类文件&#xff1a; JSON 记录文件&#xff1a;其命名格式…

CVPR多模态破题密钥:跨模对齐,信息串供

关注gongzhonghao【CVPR顶会精选】当今数字化时代&#xff0c;多模态技术正迅速改变我们与信息互动的方式。多模态被定义为在特定语境中多种符号资源的共存与协同。这种技术通过整合不同模态的数据&#xff0c;如文本、图像、音频等&#xff0c;为用户提供更丰富、更自然的交互…