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

OpenLayers:台风轨迹动画

台风轨迹动画是一种最常见的轨迹动画的应用方式,在许多气象、水利、应急相关的系统中都有类似的功能。最近我在学习了一些案例后自己也尝试实现了一个简单的台风轨迹动画。效果如下(数据源自温州台风网):

台风路径动画

一、功能概述

我所实现的台风轨迹动画大致可以拆解为以下的几个功能点:

1.路径点和路径线

用点和线来表示台风的运动路径。

路径点有一个特别的要求:要按照当时的台风强度设置路径点的颜色

台风强度与颜色的关系如下:

  const mapObj = {TD: "#02ff02", // 热带低压TS: "#0264ff", // 热带风暴STS: "#fffb05", // 强热带风暴TY: "#ffac05", // 台风STY: "#f00f00", // 强台风SuperTY: "#b10021", // 超强台风};

2.台风标识

使用一个图标来表示台风本身,通过台风图标在路径上的移动模拟台风运动的过程。

3.台风风圈

通过一个不规则的图形来表示台风风圈的影响范围。

关于台风风圈有两个点必需要实现说明一下。

第一,台风风圈在各个方向上的半径是不一样的,所以它不是一个圆。

以我所使用的这套数据为例,其中提供了台风风圈在东北、西北、东南、西南四个方向上的半径。

{"radius7_quad": {"ne": 150, //东北 "se": 260,//东南"sw": 260,//西南"nw": 100//西北},
}

第二,台风风圈分为radius7radius10radius12三种。它们是台风的“三围”分别指其12级、10级和7级风圈的半径大小,即在最大风速半径外,近地面风速衰减至32.7、24.5以及17.2m/s时离台风中心的距离。

除了这几个功能之外还可以尝试去绘制台风预报路径、添加一些信息提示框、封装一个台风的图层类等,感兴趣的可以自己去尝试一下。

二、功能实现

1.路径绘制

路径绘制非常简单,我手上数据就是以一个个的路径点为单位组织的,其中有每个路径点的坐标,而将路径点连起来就是一条路径线。

大致的代码如下:

let timer, source, typhoonLayer;let index = 0;timer && clearInterval(timer);
source && source.clear();// 创建图层
if (!typhoonLayer) {source = new VectorSource();typhoonLayer = new VectorLayer({id: "typhoonLayer",name: "台风路径_蝴蝶",source: source,});window.map.addLayer(typhoonLayer);
}timer = setInterval(() => {if (index >= mockData.points.length) {clearInterval(timer);return;}const pointItem = mockData.points[index];const position = [pointItem.lng, pointItem.lat];const lastPointItem = index > 0 ? mockData.points[index - 1] : null;const lastPointPosition = lastPointItem? [lastPointItem.lng, lastPointItem.lat]: null;// 绘制台风路径点const feature = new Feature({geometry: new Point(position),});const pointStyle = new Style({image: new Circle({fill: new Fill({color: judgeColorByWindLevel(pointItem.strong),}),stroke: new Stroke({color: "#000",width: 1,}),radius: 6,}),});feature.setStyle(pointStyle);feature.set("attribute", pointItem);source.addFeature(feature);// 绘制台风路径线if (index > 0) {const lineFeature = new Feature({geometry: new LineString([lastPointPosition, position]),});lineFeature.setStyle(new Style({stroke: new Stroke({color: "#595959",}),}));source.addFeature(lineFeature);}
},100)

2.台风标识

台风标识本质上是一张图片,想要在地图上显示一张图片,一般有两种方法:给一个点要素设置Icon样式,或者使用Overlay。如果台风标识是静态的那两种方法都可以,但是这里我希望我的台风标识可以转动起来,因此只能使用Overlay,这样就可以使用CSS动画来实现图片元素的旋转。

@keyframes rotate {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}
}.typhoon-flag {width: 62px;height: 62px;background: url("./typhoon.png") no-repeat;animation: rotate 0.5s linear infinite;
}

之后只要在计时器中修改Overlay的位置即可。

timer = setInterval(() => {//省略......// 绘制台风标识let typhoonFlag = window.map.getOverlayById("typhoonFlag");if (!typhoonFlag) {const typhoonFlagElement = document.createElement("div");typhoonFlagElement.className = "typhoon-flag";typhoonFlag = new Overlay({id: "typhoonFlag",name: "台风标识",element: typhoonFlagElement,positioning: "center-center",autoPan: true,});window.map.addOverlay(typhoonFlag);}typhoonFlag.setPosition(position);//省略......
},100)

3.绘制风圈

由于台风在不同方位的风力影响范围(即风圈半径)通常是不同的,因此在绘制风圈范围图形不能简单的画成一个正圆,而是一个四个象限中半径不同的特殊圆。

象限

风圈半径

第一象限(0° ~ 90°)

使用东北方向的半径(ne)

第一象限(90° ~ 180°)

使用东南方向的半径(se)

第一象限(180° ~ 270°)

使用西南方向的半径(sw)

第一象限(270° ~ 360°)

使用西北方向的半径(nw)

这个风圈图形非常特殊其中涉及到绘制弧线,OpenLayers中是没办法直接去绘制弧线的,这里我采用描点法实现弧线的绘制,将弧线拆解成一个个的散点而散点连起来就是弧线,因此只要计算出所有散点的坐标即可绘制出一条弧线。具体的代码如下:

// 绘制台风风圈
function drawWindCircle(source, center, radiusQuad, maxRadius, level) {let color = "#2196f329";let strokeColor = "#2196f3";let featureId = "windCircle7";switch (level) {case 10:color = "#ff980042";strokeColor = "#ff9800";featureId = "windCircle10";break;case 12:color = "#ff000042";strokeColor = "#ff0000";featureId = "windCircle12";break;default:break;}// 如果风圈已经存在,则删除const feature = source.getFeatureById(featureId);if (feature) {source.removeFeature(feature);}if (!maxRadius) return;const Configs = {CIRCLE_CENTER_X: center[0],CIRCLE_CENTER_Y: center[1],CIRCLE_R: {SE: radiusQuad.se / 100, //东南NE: radiusQuad.ne / 100, //东北NW: radiusQuad.nw / 100, //西北SW: radiusQuad.sw / 100  //西南}};const points = [];const _interval = 6;for (let i = 0; i < 360 / _interval; i++) {let _r = 0;let _ang = i * _interval;if (_ang > 0 && _ang <= 90) {_r = Configs.CIRCLE_R.NE;} else if (_ang > 90 && _ang <= 180) {_r = Configs.CIRCLE_R.SE;} else if (_ang > 180 && _ang <= 270) {_r = Configs.CIRCLE_R.SW;} else {_r = Configs.CIRCLE_R.SW;}const x = Configs.CIRCLE_CENTER_X + _r * Math.cos((_ang * Math.PI) / 180);const y = Configs.CIRCLE_CENTER_Y + _r * Math.sin((_ang * Math.PI) / 180);points.push([x, y]);}const polyFeature = new Feature({geometry: new Polygon([points]),});const style = new Style({fill: new Fill({color: color,}),//边框stroke: new Stroke({color: strokeColor,width: 1,}),image: new Circle({radius: 2,fill: new Fill({color: "#ff0000",}),}),});polyFeature.setId(featureId);polyFeature.setStyle(style);source.addFeature(polyFeature);return polyFeature;
}

完整代码

<template><divref="mapContainer"id="mapContainer"style="width: 100vw; height: 100vh"></div><activity-panel><el-button @click="drawTyphoon">开始</el-button></activity-panel>
</template><script setup>
import { onMounted, nextTick, ref } from "vue";
// ol custom API
import useInitMap from "@/utils/ol/useInitMap";
// ol API
import { Vector as VectorLayer } from "ol/layer";
import { Vector as VectorSource } from "ol/source";
import Feature from "ol/Feature";
import { Point, LineString, Polygon } from "ol/geom";
import { Style, Circle, Fill, Stroke, Text } from "ol/style";
import Overlay from "ol/Overlay";// 台风路径数据
import mockData from "./mockData.json";useInitMap({target: "mapContainer",zoom: 6,onLoadEnd: onLoadEnd,
});function onLoadEnd(map) {
}let timer, source, typhoonLayer;
async function drawTyphoon() {let index = 0;timer && clearInterval(timer);source && source.clear();if (!typhoonLayer) {source = new VectorSource();typhoonLayer = new VectorLayer({id: "typhoonLayer",name: "台风路径_蝴蝶",source: source,});window.map.addLayer(typhoonLayer);}timer = setInterval(() => {if (index >= mockData.points.length) {clearInterval(timer);return;}const pointItem = mockData.points[index];const position = [pointItem.lng, pointItem.lat];const lastPointItem = index > 0 ? mockData.points[index - 1] : null;const lastPointPosition = lastPointItem? [lastPointItem.lng, lastPointItem.lat]: null;// 绘制台风路径点const feature = new Feature({geometry: new Point(position),});const pointStyle = new Style({image: new Circle({fill: new Fill({color: judgeColorByWindLevel(pointItem.strong),}),stroke: new Stroke({color: "#000",width: 1,}),radius: 6,}),});if (index === 0) {pointStyle.setText(new Text({text: mockData.name,scale: 1.3,offsetY: 0,offsetX: 30,fill: new Fill({color: "#000",}),}));}feature.setStyle(pointStyle);feature.set("attribute", pointItem);source.addFeature(feature);// 绘制台风路径线if (index > 0) {const lineFeature = new Feature({geometry: new LineString([lastPointPosition, position]),});lineFeature.setStyle(new Style({stroke: new Stroke({color: "#595959",}),}));source.addFeature(lineFeature);}// 绘制台风风圈drawWindCircle(source,position,pointItem.radius7_quad,pointItem.radius7,7);drawWindCircle(source,position,pointItem.radius10_quad,pointItem.radius10,10);drawWindCircle(source,position,pointItem.radius12_quad,pointItem.radius12,12);// 绘制台风标识let typhoonFlag = window.map.getOverlayById("typhoonFlag");if (!typhoonFlag) {const typhoonFlagElement = document.createElement("div");typhoonFlagElement.className = "typhoon-flag";typhoonFlag = new Overlay({id: "typhoonFlag",name: "台风标识",element: typhoonFlagElement,positioning: "center-center",autoPan: true,});window.map.addOverlay(typhoonFlag);}typhoonFlag.setPosition(position);index++;}, 100);
}function judgeColorByWindLevel(strong) {const flag = strong.split(/[\(\)]/)[1];const mapObj = {TD: "#02ff02", // 热带低压TS: "#0264ff", // 热带风暴STS: "#fffb05", // 强热带风暴TY: "#ffac05", // 台风STY: "#f00f00", // 强台风SuperTY: "#b10021", // 超强台风};return mapObj[flag];
}/*** 绘制台风风圈* @param {VectorSource} source 图层数据源* @param {Array} center 中心点* @param {Object} radiusQuad 风圈半径对象* @param {Number} maxRadius 最大半径* @param { 7 | 10 | 12} level 风圈等级*/
function drawWindCircle(source, center, radiusQuad, maxRadius, level) {let color = "#2196f329";let strokeColor = "#2196f3";let featureId = "windCircle7";switch (level) {case 10:color = "#ff980042";strokeColor = "#ff9800";featureId = "windCircle10";break;case 12:color = "#ff000042";strokeColor = "#ff0000";featureId = "windCircle12";break;default:break;}// 如果风圈已经存在,则删除const feature = source.getFeatureById(featureId);if (feature) {source.removeFeature(feature);}if (!maxRadius) return;const Configs = {CIRCLE_CENTER_X: center[0],CIRCLE_CENTER_Y: center[1],CIRCLE_R: {SE: radiusQuad.se / 100, //东南NE: radiusQuad.ne / 100, //东北NW: radiusQuad.nw / 100, //西北SW: radiusQuad.sw / 100, //西南},};const points = [];const _interval = 6;for (let i = 0; i < 360 / _interval; i++) {let _r = 0;let _ang = i * _interval;if (_ang > 0 && _ang <= 90) {_r = Configs.CIRCLE_R.NE;} else if (_ang > 90 && _ang <= 180) {_r = Configs.CIRCLE_R.SE;} else if (_ang > 180 && _ang <= 270) {_r = Configs.CIRCLE_R.SW;} else {_r = Configs.CIRCLE_R.SW;}const x = Configs.CIRCLE_CENTER_X + _r * Math.cos((_ang * Math.PI) / 180);const y = Configs.CIRCLE_CENTER_Y + _r * Math.sin((_ang * Math.PI) / 180);points.push([x, y]);}const polyFeature = new Feature({geometry: new Polygon([points]),});const style = new Style({fill: new Fill({color: color,}),//边框stroke: new Stroke({color: strokeColor,width: 1,}),image: new Circle({radius: 2,fill: new Fill({color: "#ff0000",}),}),});polyFeature.setId(featureId);polyFeature.setStyle(style);source.addFeature(polyFeature);return polyFeature;
}
</script><style>
@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}
}.typhoon-flag {width: 62px;height: 62px;background: url("./typhoon.png") no-repeat;animation: spin 0.5s linear infinite;
}
</style>

参考资料

  1. openlayers4+中台风路径播放优化_openlayers 台风-CSDN博客
  2. Openlayers3中实现台风风圈绘制算法-CSDN博客
  3. Vue3 + Openlayers10示例 台风轨迹和台风圈_openlayers 台风-CSDN博客
  4. 温州台风网
  5. https://www.toutiao.com/article/7260332757472100923/?upstream_biz=doubao&source=m_redirect&wid=1750591887067

http://www.lqws.cn/news/495577.html

相关文章:

  • AI智能体——MCP 模型上下文协议
  • TestCafe 全解析:免费开源的 E2E 测试解决方案实战指南
  • Python datetime模块详解
  • SpringBoot中使用表单数据有效性检验
  • C#串口通讯实战指南
  • 前端跨域解决方案(7):Node中间件
  • C语言数组介绍 -- 一维数组和二维数组的创建、初始化、下标、遍历、存储,C99 变长数组
  • Linux笔记---线程控制
  • 容器技术入门与Docker环境部署指南
  • js逻辑:【增量更新机制】
  • 【LeetCode 热题 100】42. 接雨水——(解法一)前后缀分解
  • Profibus DP主站转EtherNet/IP从站总线协议转换网关
  • Auto-GPT vs ReAct:两种智能体思路对决
  • 开始读Learning PostgresSQL第二版
  • B端布局性能优化秘籍:如何让个性化页面加载速度提升
  • 实时反欺诈:基于 Spring Boot 与 Flink 构建信用卡风控系统
  • 【AI论文】扩展大型语言模型(LLM)智能体在测试时的计算量
  • 硬件工程师笔试面试高频考点汇总——(2025版)
  • 软件更新 | 从数据到模型,全面升级!TSMaster新版助力汽车研发新突破
  • 体育数据api接口,足球api篮球api电竞api,比赛赛事数据api
  • 火山引擎大模型未来发展趋势
  • QML\QtQuick\QtWidgets适合的场景及其优缺点
  • 开发上门按摩APP应具备哪些安全保障功能?
  • Java流程控制--判断结构
  • Java编程中的设计模式:单例模式的深度剖析
  • 智能生成分析报告系统在危化安全生产监测预警评估中的应用
  • 【Java高频面试问题】数据结构篇
  • springboot开发项目 SLF4J+Logback日志框架集成【最终篇】
  • 智慧园区数字孪生最佳交付实践:沉淀可复用场景模板,实现快速部署与定制化开发
  • 顶级思维方式——认知篇十一《传习录》笔记