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

OpenLayers 综合案例-台风风场模拟

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

OpenLayers 综合案例-台风风场模拟

Vue 3 + OpenLayers 实现的 WebGIS 应用提供了完整的台风风场模拟功能

主要功能

  1. 台风的完整路径以红色虚线在地图上绘制,路径上的历史和预测点用绿色圆点标记
  2. 当前台风中心点使用一个自定义图标(风圈.png)表示,并会沿着预设路径移动
  3. 应用通过大量蓝色线条模拟风场粒子,这些粒子围绕台风中心以漩涡状向内移动,并带有阻尼效果,模拟真实的台风风场
  4. 风场动画使用 requestAnimationFrame 实现平滑的粒子运动
  5. 用户在地图上按下鼠标时,台风路径动画会暂停;鼠标抬起时,动画会恢复,方便用户观察地图

在这里插入图片描述

MP4效果动画链接地址

技术栈

该环境下代码即拿即用

Vue 3.5.13+
Openlayers 10.5.0+
Vite 6.3.5+
<template><div class="container"><div id="map" class="map"></div><!-- 控制面板 --><div class="controls-panel"><h2 class="panel-title">图层控制</h2><!-- 台风路径不再有开关,始终显示 --><label class="checkbox-label"><!-- 仅作显示,无实际控制功能 --><input type="checkbox" checked disabled class="checkbox-input" /><span class="checkbox-text">台风路径 (始终显示)</span></label></div><!-- 台风信息卡片 --><div v-if="selectedTyphoon" class="info-card"><h3 class="card-title">{{ selectedTyphoon.name }} - {{ selectedTyphoon.time }}</h3><div class="card-content"><p><strong>中心位置:</strong> 东经{{ selectedTyphoon.lon }}° 北纬{{selectedTyphoon.lat}}°</p><p><strong>中心气压:</strong> {{ selectedTyphoon.pressure }}百帕</p><p><strong>风速风力:</strong> {{ selectedTyphoon.windSpeed }}米/秒,{{ selectedTyphoon.windForce }}级({{ selectedTyphoon.windType }})</p><p><strong>当前位置:</strong> {{ selectedTyphoon.currentLocation }}</p></div></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, watch } from "vue";
import Map from "ol/Map.js";
import View from "ol/View.js";
import TileLayer from "ol/layer/Tile.js";
import VectorLayer from "ol/layer/Vector.js";
import XYZ from "ol/source/XYZ.js";
import VectorSource from "ol/source/Vector.js";
import Feature from "ol/Feature.js";
import Point from "ol/geom/Point.js";
import LineString from "ol/geom/LineString.js";
import Circle from "ol/geom/Circle.js";
import { fromLonLat } from "ol/proj.js"; // 用于经纬度坐标与地图投影坐标之间的转换
import { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style.js"; // OpenLayers 样式相关模块,新增 Icon
import "ol/ol.css"; // OpenLayers 默认样式// OpenLayers 地图实例
let map = null;
// 台风路径和中心点图层
let typhoonTrackLayer = null;
// 风场模拟图层
let windFieldLayer = null;
// 风场数据源,用于存储风场粒子要素
let windFieldSource = null;
// 风场动画帧请求ID,用于取消 requestAnimationFrame 动画
let animationFrameId = null;
// 台风路径动画的定时器ID,用于清除 setInterval 定时器
let typhoonAnimationIntervalId = null;// 当前选中的台风信息,用于信息卡片显示,响应式更新
const selectedTyphoon = ref(null);
// 台风路径动画的当前索引,用于追踪台风在路径上的位置
const typhoonPathIndex = ref(0);// 模拟台风数据
const typhoonData = {name: "竹节草", // 台风名称// 模拟的台风路径点数组,每个点包含经纬度、时间、气压、风速风力等信息path: [{lon: 118.0,lat: 20.0,time: "7月28日0时",pressure: 1000,windSpeed: 15,windForce: 7,windType: "热带低压",currentLocation: "位于南海中部",},{lon: 119.5,lat: 21.5,time: "7月28日8时",pressure: 998,windSpeed: 16,windForce: 7,windType: "热带低压",currentLocation: "位于南海北部",},{lon: 121.0,lat: 23.0,time: "7月28日16时",pressure: 995,windSpeed: 17,windForce: 8,windType: "热带风暴",currentLocation: "位于巴士海峡",},{lon: 122.5,lat: 24.5,time: "7月29日0时",pressure: 993,windSpeed: 18,windForce: 8,windType: "热带风暴",currentLocation: "位于台湾以东洋面",},{lon: 123.8,lat: 26.0,time: "7月29日4时",pressure: 992,windSpeed: 18,windForce: 8,windType: "热带风暴",currentLocation: "位于台湾东北部洋面",},{lon: 124.9,lat: 27.3,time: "7月29日8时",pressure: 992,windSpeed: 18,windForce: 8,windType: "热带风暴",currentLocation: "当前位于东海,距离登陆点上海市东南方向549公里",}, // 当前位置{lon: 126.0,lat: 28.5,time: "7月29日16时",pressure: 990,windSpeed: 20,windForce: 9,windType: "热带风暴",currentLocation: "预计向西北方向移动",}, // 预测{lon: 127.5,lat: 29.5,time: "7月30日0时",pressure: 988,windSpeed: 22,windForce: 9,windType: "热带风暴",currentLocation: "预计在东海北部",},{lon: 129.0,lat: 30.0,time: "7月30日8时",pressure: 985,windSpeed: 25,windForce: 10,windType: "强热带风暴",currentLocation: "预计在黄海南部",},],windRadius: 300000, // 7级风圈半径,单位米
};// 风场粒子数组,存储每个粒子的属性和对应的 OpenLayers Feature
const windParticles = [];
// 风场粒子数量
const NUM_WIND_PARTICLES = 1500; // 增加粒子数量以提高密度
// 风场粒子线段长度(模拟),增加长度使其更明显
const WIND_PARTICLE_LENGTH = 10000; // 米 (10公里)// 风场行为的新常量(实现更强的漩涡和向内拉力)
const WIND_FIELD_MAX_RADIUS = typhoonData.windRadius * 3.5; // 粒子最大半径 (1050公里)
const WIND_FIELD_MIN_RADIUS = typhoonData.windRadius * 0.05; // 粒子最小半径 (15公里),允许粒子更靠近中心
const INWARD_ACCELERATION = 2.0; // 每帧粒子向内拉力的强度(已增加)
const SWIRL_ACCELERATION = 4.0; // 每帧粒子漩涡(切向力)的强度(已增加)
const MAX_PARTICLE_SPEED = 1000; // 粒子最大速度(米/帧)(已增加)
const MIN_PARTICLE_SPEED = 100; // 粒子最小速度(米/帧)(已增加)
const DRAG_FACTOR = 0.97; // 阻尼/拖曳因子,防止无限加速(略微减小阻力)// 用于更轻松更新的点要素引用
let typhoonPointFeatures = [];
let windCircleFeature = null;// 台风中心点图片 URL,请替换为您的图片路径
const TYPHOON_ICON_URL = "/src/assets/风圈.png"; // 示例占位符图片onMounted(() => {// 基础瓦片图层 (高德地图)const gaodeLayer = 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}",crossOrigin: "anonymous", // 允许跨域加载瓦片,避免图片加载问题}),});// 台风路径图层的数据源const typhoonSource = new VectorSource();typhoonTrackLayer = new VectorLayer({source: typhoonSource,// 根据要素类型设置不同的样式style: (feature) => {if (feature.get("type") === "path") {// 台风路径线样式return new Style({stroke: new Stroke({color: "red",width: 3,lineDash: [10, 10], // 虚线效果}),});} else if (feature.get("type") === "current") {// 当前台风中心点样式(使用circle代替)// return new Style({//   image: new CircleStyle({//     radius: 10,//     fill: new Fill({ color: "rgba(0, 150, 255, 0.8)" }), // 蓝色填充//     stroke: new Stroke({ color: "white", width: 2 }), // 白色边框//   }),// });// 当前台风中心点样式 (使用图片)return new Style({image: new Icon({anchor: [0.5, 0.5], // 图片中心与坐标点对齐anchorXUnits: "fraction",anchorYUnits: "fraction",src: TYPHOON_ICON_URL, // 您的图片 URLscale: 3.5, // 根据图片实际大小调整缩放比例}),});} else if (feature.get("type") === "point") {// 路径上的历史/预测点样式return new Style({image: new CircleStyle({radius: 5,fill: new Fill({ color: "rgba(0, 200, 0, 0.8)" }), // 绿色填充stroke: new Stroke({ color: "white", width: 1 }), // 白色边框}),});} else if (feature.get("type") === "windCircle") {// 风圈样式return new Style({stroke: new Stroke({color: "rgba(0, 255, 0, 0.6)", // 绿色边框width: 2,}),fill: new Fill({color: "rgba(0, 255, 0, 0.1)", // 半透明绿色填充}),});}return null;},});// 初始化台风路径和点要素const initialPathCoordinates = typhoonData.path.map((p) =>fromLonLat([p.lon, p.lat]));// 创建台风路径线要素const pathFeature = new Feature({geometry: new LineString(initialPathCoordinates),type: "path",});typhoonSource.addFeature(pathFeature);// 创建台风路径上的所有点要素并存储引用typhoonData.path.forEach((p, i) => {const pointFeature = new Feature({geometry: new Point(fromLonLat([p.lon, p.lat])),type: "point", // 初始都设为普通点});typhoonSource.addFeature(pointFeature);typhoonPointFeatures.push(pointFeature);});// 获取当前台风中心点要素和风圈要素的引用,方便后续动画更新if (typhoonPointFeatures[typhoonPathIndex.value]) {typhoonPointFeatures[typhoonPathIndex.value].set("type", "current"); // 设置初始当前点类型}// 创建风圈要素windCircleFeature = new Feature({geometry: new Circle(fromLonLat([typhoonData.path[typhoonPathIndex.value].lon,typhoonData.path[typhoonPathIndex.value].lat,]),typhoonData.windRadius),type: "windCircle",});typhoonSource.addFeature(windCircleFeature);// 初始化台风信息卡片显示为路径的第一个点数据selectedTyphoon.value = {...typhoonData.path[typhoonPathIndex.value],name: typhoonData.name,};// 风场图层的数据源windFieldSource = new VectorSource();windFieldLayer = new VectorLayer({source: windFieldSource,style: new Style({stroke: new Stroke({color: "rgba(0, 150, 255, 0.8)", // 风线颜色略微加深width: 2, // 增加风线宽度}),}),});// 初始化风场粒子,并添加到风场数据源for (let i = 0; i < NUM_WIND_PARTICLES; i++) {resetWindParticle(i); // 调用重置函数,它会创建并添加新的Feature}// 创建地图实例map = new Map({layers: [gaodeLayer, typhoonTrackLayer, windFieldLayer], // 添加所有图层到地图target: "map", // 地图容器的DOM元素IDview: new View({center: fromLonLat([typhoonData.path[typhoonPathIndex.value].lon,typhoonData.path[typhoonPathIndex.value].lat,]), // 初始地图中心为台风起始点zoom: 6, // 初始缩放级别maxZoom: 18, // 最大缩放级别}),});// 鼠标按下时停止定时器,抬起时启用定时器map.on("pointerdown", () => {if (typhoonAnimationIntervalId) {clearInterval(typhoonAnimationIntervalId);typhoonAnimationIntervalId = null;}});map.on("pointerup", () => {if (!typhoonAnimationIntervalId) {startTyphoonAnimation(); // 恢复台风路径动画}});// 初始状态下,启动风场动画startWindAnimation();// 启动台风路径动画startTyphoonAnimation();
});// 组件卸载时清理资源,防止内存泄漏
onUnmounted(() => {if (map) {map.setTarget(null); // 解除地图与DOM元素的绑定map = null;}stopWindAnimation(); // 停止风场动画stopTyphoonAnimation(); // 停止台风路径动画
});/*** 重置单个风场粒子位置* 当粒子超出范围或需要重新生成时调用* @param {number} index - 粒子在 windParticles 数组中的索引*/
function resetWindParticle(index) {const currentTyphoonCenter = fromLonLat([selectedTyphoon.value.lon,selectedTyphoon.value.lat,]);// 在环形区域 [最小半径, 最大半径] 内生成随机位置const angle = Math.random() * 2 * Math.PI;const distance =WIND_FIELD_MIN_RADIUS +Math.random() * (WIND_FIELD_MAX_RADIUS - WIND_FIELD_MIN_RADIUS);const startX = currentTyphoonCenter[0] + distance * Math.cos(angle);const startY = currentTyphoonCenter[1] + distance * Math.sin(angle);// 初始速度(在范围内随机速度,随机方向)let initialSpeed =MIN_PARTICLE_SPEED +Math.random() * (MAX_PARTICLE_SPEED - MIN_PARTICLE_SPEED);let initialAngle = Math.random() * 2 * Math.PI;let initialVx = initialSpeed * Math.cos(initialAngle);let initialVy = initialSpeed * Math.sin(initialAngle);// 应用一些初始的向内/漩涡趋势,以实现即时视觉效果const dxToCenter = currentTyphoonCenter[0] - startX;const dyToCenter = currentTyphoonCenter[1] - startY;const distToCenter = Math.sqrt(dxToCenter * dxToCenter + dyToCenter * dyToCenter);if (distToCenter > 0) {// 初始向内拉力initialVx += (dxToCenter / distToCenter) * INWARD_ACCELERATION * 5; // 增强初始向内力initialVy += (dyToCenter / distToCenter) * INWARD_ACCELERATION * 5;// 初始漩涡initialVx += (-dyToCenter / distToCenter) * SWIRL_ACCELERATION * 5; // 增强初始漩涡力initialVy += (dxToCenter / distToCenter) * SWIRL_ACCELERATION * 5;}const particleRenderAngle = Math.atan2(initialVy, initialVx);const endX = startX + WIND_PARTICLE_LENGTH * Math.cos(particleRenderAngle);const endY = startY + WIND_PARTICLE_LENGTH * Math.sin(particleRenderAngle);if (windParticles[index]) {windParticles[index].x = startX;windParticles[index].y = startY;windParticles[index].vx = initialVx;windParticles[index].vy = initialVy;windParticles[index].feature.getGeometry().setCoordinates([[startX, startY],[endX, endY],]);} else {const feature = new Feature({geometry: new LineString([[startX, startY],[endX, endY],]),});windParticles.push({feature: feature, // 存储 Feature 对象x: startX,y: startY,vx: initialVx,vy: initialVy,});windFieldSource.addFeature(feature); // 将新创建的 Feature 添加到数据源}
}/*** 风场粒子动画循环函数* 使用 requestAnimationFrame 实现平滑动画*/
function animateWindField() {// 获取当前台风中心点,风场粒子会围绕此中心移动const currentTyphoonCenter = fromLonLat([selectedTyphoon.value.lon,selectedTyphoon.value.lat,]);windParticles.forEach((p, index) => {const dx = currentTyphoonCenter[0] - p.x;const dy = currentTyphoonCenter[1] - p.y;const distance = Math.sqrt(dx * dx + dy * dy);// 如果粒子太近或太远,则重置if (distance < WIND_FIELD_MIN_RADIUS || distance > WIND_FIELD_MAX_RADIUS) {resetWindParticle(index);return; // 本帧跳过此粒子的后续处理}// 计算力(加速度)let ax = 0;let ay = 0;if (distance > 0) {// 向内拉力(越靠近中心越强)// 使用更平滑的向内力衰减,可能类似反平方定律const inwardFactor = INWARD_ACCELERATION / (distance / 50000 + 1); // 调整除数以控制力强度ax += (dx / distance) * inwardFactor;ay += (dy / distance) * inwardFactor;// 切向漩涡(顺时针,越靠近中心越强)const swirlFactor = SWIRL_ACCELERATION / (distance / 50000 + 1); // 调整除数以控制力强度ax += (-dy / distance) * swirlFactor; // 垂直于 (dx, dy)ay += (dx / distance) * swirlFactor;}// 更新速度p.vx += ax;p.vy += ay;// 应用阻力/阻尼p.vx *= DRAG_FACTOR;p.vy *= DRAG_FACTOR;// 限制速度const currentSpeed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);if (currentSpeed > MAX_PARTICLE_SPEED) {p.vx = (p.vx / currentSpeed) * MAX_PARTICLE_SPEED;p.vy = (p.vy / currentSpeed) * MAX_PARTICLE_SPEED;} else if (currentSpeed < MIN_PARTICLE_SPEED && currentSpeed > 0) {// 确保粒子不会完全停止,除非速度为0p.vx = (p.vx / currentSpeed) * MIN_PARTICLE_SPEED;p.vy = (p.vy / currentSpeed) * MIN_PARTICLE_SPEED;}// 更新位置p.x += p.vx;p.y += p.vy;// 更新要素几何形状(线段方向基于当前速度)const particleAngle = Math.atan2(p.vy, p.vx);const endX = p.x + WIND_PARTICLE_LENGTH * Math.cos(particleAngle);const endY = p.y + WIND_PARTICLE_LENGTH * Math.sin(particleAngle);p.feature.getGeometry().setCoordinates([[p.x, p.y],[endX, endY],]);});windFieldSource.changed(); // 强制重绘风场图层,显示粒子移动效果animationFrameId = requestAnimationFrame(animateWindField); // 请求下一帧动画
}/*** 启动风场动画*/
function startWindAnimation() {if (!animationFrameId) {// 避免重复启动动画animateWindField();}
}/*** 停止风场动画*/
function stopWindAnimation() {if (animationFrameId) {cancelAnimationFrame(animationFrameId); // 取消当前的动画帧请求animationFrameId = null;}
}/*** 启动台风路径动画* 使用 setInterval 定时器模拟台风的移动*/
function startTyphoonAnimation() {// 每隔1秒更新一次台风位置typhoonAnimationIntervalId = setInterval(() => {// 获取上一个当前点要素并重置其类型if (typhoonPointFeatures[typhoonPathIndex.value]) {typhoonPointFeatures[typhoonPathIndex.value].set("type", "point");}// 更新台风路径索引,循环遍历路径typhoonPathIndex.value =(typhoonPathIndex.value + 1) % typhoonData.path.length;const currentPointData = typhoonData.path[typhoonPathIndex.value];const newCenterCoordinates = fromLonLat([currentPointData.lon,currentPointData.lat,]);// 更新台风信息卡片显示的数据selectedTyphoon.value = { ...currentPointData, name: typhoonData.name };// 设置新的当前点要素类型if (typhoonPointFeatures[typhoonPathIndex.value]) {typhoonPointFeatures[typhoonPathIndex.value].set("type", "current");}// 更新风圈位置if (windCircleFeature) {windCircleFeature.setGeometry(new Circle(newCenterCoordinates, typhoonData.windRadius));}// 强制重绘台风路径图层,显示移动的台风中心和风圈typhoonTrackLayer.getSource().changed();// 确保风场粒子根据新的台风中心重置// 遍历所有风场粒子,确保它们对新的台风中心做出反应windParticles.forEach((p, index) => {// 当台风移动时,粒子也应调整// 相对于新的台风中心重新初始化粒子,以防止视觉“跳跃”const oldCenter = fromLonLat([typhoonData.path[(typhoonPathIndex.value - 1 + typhoonData.path.length) %typhoonData.path.length].lon,typhoonData.path[(typhoonPathIndex.value - 1 + typhoonData.path.length) %typhoonData.path.length].lat,]);// 计算粒子相对于旧中心的位置const relativeX = p.x - oldCenter[0];const relativeY = p.y - oldCenter[1];// 将相同的相对位置应用于新中心p.x = newCenterCoordinates[0] + relativeX;p.y = newCenterCoordinates[1] + relativeY;// 重置速度,避免携带可能与新力冲突的旧动量const initialSpeed =MIN_PARTICLE_SPEED +Math.random() * (MAX_PARTICLE_SPEED - MIN_PARTICLE_SPEED);const initialAngle = Math.atan2(p.vy, p.vx); // 保持大致方向p.vx = initialSpeed * Math.cos(initialAngle);p.vy = initialSpeed * Math.sin(initialAngle);// 更新其要素几何形状const particleAngle = Math.atan2(p.vy, p.vx);const endX = p.x + WIND_PARTICLE_LENGTH * Math.cos(particleAngle);const endY = p.y + WIND_PARTICLE_LENGTH * Math.sin(particleAngle);p.feature.getGeometry().setCoordinates([[p.x, p.y],[endX, endY],]);});windFieldSource.changed(); // 强制重绘风场图层}, 1000); // 每1秒更新一次台风位置
}/*** 停止台风路径动画*/
function stopTyphoonAnimation() {if (typhoonAnimationIntervalId) {clearInterval(typhoonAnimationIntervalId); // 清除定时器typhoonAnimationIntervalId = null;}
}
</script><style scoped>
/* 容器样式 */
.container {position: relative;width: 100vw; /* 视口宽度 */height: 100vh; /* 视口高度 */overflow: hidden; /* 隐藏溢出内容 */
}/* 地图容器样式 */
.map {width: 100%;height: 100%;background-color: #e0f2f7; /* 浅蓝色背景,模拟海洋区域 */
}/* 控制面板样式 */
.controls-panel {position: absolute;top: 16px; /* 距离顶部16px */right: 16px; /* 距离右侧16px */background-color: rgba(255, 255, 255, 0.9); /* 半透明白色背景 */padding: 16px; /* 内边距 */border-radius: 8px; /* 圆角 */box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 阴影效果 */z-index: 10; /* 确保在地图上方 */display: flex;flex-direction: column; /* 垂直布局 */gap: 12px; /* 元素间距 */
}/* 面板标题样式 */
.panel-title {font-size: 1.125rem; /* 字体大小 */font-weight: 600; /* 字体粗细 */color: #2d3748; /* 深灰色字体 */margin-bottom: 8px; /* 底部外边距 */
}/* 复选框标签样式 */
.checkbox-label {display: flex;align-items: center; /* 垂直居中对齐 */cursor: pointer; /* 鼠标指针变为手型 */
}/* 复选框输入框样式 */
.checkbox-input {height: 20px;width: 20px;accent-color: #2563eb; /* 改变复选框的颜色 */border-radius: 4px;border: 1px solid #ccc;appearance: none; /* 隐藏默认复选框样式 */-webkit-appearance: none; /* 兼容WebKit浏览器 */outline: none; /* 移除焦点轮廓 */cursor: pointer;position: relative;
}/* 复选框选中时的样式 */
.checkbox-input:checked {background-color: #2563eb; /* 选中时背景色 */border-color: #2563eb; /* 选中时边框色 */
}/* 复选框选中时显示对勾 */
.checkbox-input:checked::before {content: "✔"; /* 对勾符号 */display: block;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%); /* 居中对勾 */font-size: 14px;color: white; /* 对勾颜色 */
}/* 复选框文本样式 */
.checkbox-text {margin-left: 8px; /* 左侧外边距 */color: #4a5568; /* 灰色字体 */
}/* 信息卡片样式 */
.info-card {position: absolute;bottom: 16px; /* 距离底部16px */left: 16px; /* 距离左侧16px */background-color: rgba(255, 255, 255, 0.9); /* 半透明白色背景 */padding: 24px; /* 内边距 */border-radius: 8px; /* 圆角 */box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 阴影效果 */z-index: 10; /* 确保在地图上方 */width: 384px; /* 固定宽度 */
}/* 卡片标题样式 */
.card-title {font-size: 1.25rem; /* 字体大小 */font-weight: 700; /* 字体粗细 */color: #1d4ed8; /* 蓝色字体 */margin-bottom: 12px; /* 底部外边距 */
}/* 卡片内容样式 */
.card-content {display: flex;flex-direction: column; /* 垂直布局 */gap: 8px; /* 元素间距 */color: #4a5568; /* 灰色字体 */
}/* 卡片内容中加粗文本的样式 */
.card-content strong {font-weight: bold;
}
</style>

如果暂时没有图片,没关系,将完整代码中这段代码注释解开

// return new Style({
//   image: new CircleStyle({
//     radius: 10,
//     fill: new Fill({ color: "rgba(0, 150, 255, 0.8)" }), // 蓝色填充
//     stroke: new Stroke({ color: "white", width: 2 }), // 白色边框
//   }),
// });

在这里插入图片描述

http://www.dtcms.com/a/305035.html

相关文章:

  • MIAOYUN参编信通院《大模型API服务性能测试标准》
  • 营销活动效果分析与策略优化
  • 【JVM】常见的 Java 垃圾回收算法以及常见的垃圾回收器介绍及选型
  • ARM7微处理器的核心优势
  • Docker入门基础使用
  • 【vue(2)插槽】
  • ZKmall开源商城中台架构实践:API 网关与服务治理如何撑起电商技术骨架
  • vue3实战九、vue3+vue-cropper实现头像修改
  • 【Linux】批量处理多个用户的 sudo 权限问题
  • 【STM32开发】-基础开发笔记(STM32F103,HAL库开发)
  • 【ComfyUI学习笔记04】案例学习:局部重绘 - 上
  • 墨者:XPath注入漏洞实战
  • 第二十五节 MATLAB矩阵的加法和减法、除法(左,右)矩阵
  • Arduino声控RGB矩阵音乐节奏灯DIY全攻略
  • 解密数据结构之二叉树
  • Android11平台下rk3568的ATGM332D定位模块适配
  • 全志T507平台GPIO 控制(二)
  • OpenCV图像算数运算可莉版
  • bash命令创建新conda环境
  • Kubernetes自动扩容方案
  • 力扣-104. 二叉树的最大深度
  • Linux系统的虚拟控制台介绍(桌面卡死的拯救方案)
  • 深入探索爬虫与自动化脚本:释放效率的利器
  • 手写简易Spring框架
  • 万字详解——OSI七层模型:网络通信的完整架构解析
  • mysql 之多表
  • others-Facebook落地页自建归因逻辑
  • 如何快速把Clickhouse数据同步到Mysql
  • 解决百度网盘双击没反应打不开的问题
  • Element Plus常见基础组件(二)