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
英文地址,請注明出處:http://en.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;為用戶提供更豐富、更自然的交互…