在气象领域中,对台风的准确可视化呈现对于灾害预警和防范具有重要意义。本文将深入探讨一段使用 JavaScript 实现台风可视化功能的代码。原本只是简单的绘制台风的路径,但是后面的需求要求显示台风各个历史节点的动画绘制,于是难度增加了,最后还是使用leaflet的插件(leaflet.motion)完成了动画需求,以下就来介绍我的动画实现方式,请多指教。
一、代码结构与模块导入
首先,代码通过一系列的 import
语句引入了所需的模块和库,如 typhone_type
、typhoon_direction
、typhoonCircle_colors
等,为后续的台风绘制和数据处理提供了基础支持。这个是内部的一个字典库,可以自己定义,这里就不放出来了。
二、useTyphoon
函数
这是核心的函数,用于处理台风数据与地图的交互。
数据存储与缓存
函数内部定义了多个变量用于存储台风绘制的要素集合、已绘制的台风 ID 缓存等。避免每次绘制都是全清,然后全部一起绘制的情况。
绘制台风相关要素
通过 drawTyphoonByCodes
方法,根据传入的台风 ID 数组,进行新增和删除台风要素的处理。对于新增的台风,依次绘制实况线、未来线、当前标记、标题等。
线的绘制与动画效果
motionPolyline
函数用于动态绘制线,并可设置线的样式和是否显示气流图标。这里就是使用leaflet的插件(leaflet.motion)实现的,也是实现动画的主要代码块。
清除台风要素
clearFeaturesByIds
函数用于删除指定 ID 的台风相关要素。
三、辅助函数
convertKtsToBeaufort
函数用于将风速单位从节(kts)转换为蒲福风级。
drawTitle
函数用于在台风标记上添加名称。
getTyphoonContent
函数根据台风 ID、节点数据和类型生成不同的弹出内容。
getTyphoonDirection
函数将方向编码转换为中文描述。
四、具体绘制函数
drawLiveLine
绘制台风实况线,并为节点添加标记和弹出内容。
drawCurrentMarker
标记台风当前所在位置,并设置弹出内容。
drawFutureLine
绘制未来线。
drawWindCircle
绘制不同等级的风圈。
这段代码通过精细的设计和功能划分,实现了在地图上对台风相关要素的动态绘制和管理,为气象数据的可视化展示提供了强大的支持。
五、数据
使用的数据比较难懂,竟然不是json而是一个数组,我也只能按这种数据格式组织以下的代码
[
"2927830": {
"name": "马力斯(MALIKSI)",
"data": [
2927830,
"MALIKSI",
"马力斯",
2402,
2402,
20240002,
"菲律宾语,快速的意思",
"stop",
[
[
2927836,
"202405300900",
1717059600000,
"TD",
112.2,
17.4,
1000,
15,
"N",
26,
[],
{
"BABJ": [
[
12,
"202405300900",
112.3,
20.2,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405300900",
112.1,
21.4,
995,
20,
"BABJ",
"TS"
],
[
36,
"202405300900",
112.8,
22.7,
1000,
15,
"BABJ",
"TD"
],
[
48,
"202405300900",
114.1,
23.7,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405301700",
"2024年05月30日17时00分",
null,
null
]
],
[
2927842,
"202405301200",
1717070400000,
"TD",
112.1,
17.8,
1000,
15,
"N",
24,
[],
{
"BABJ": [
[
12,
"202405301200",
112.2,
20.3,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405301200",
112.2,
21.7,
995,
20,
"BABJ",
"TS"
],
[
36,
"202405301200",
112.8,
22.9,
1000,
15,
"BABJ",
"TD"
],
[
48,
"202405301200",
114.7,
24.1,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405302000",
"2024年05月30日20时00分",
null,
null
]
],
[
2927848,
"202405301500",
1717081200000,
"TD",
111.9,
18.5,
1000,
15,
"N",
17,
[],
{
"BABJ": [
[
12,
"202405301500",
112,
20.3,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405301500",
112.4,
21.7,
995,
20,
"BABJ",
"TS"
],
[
36,
"202405301500",
113,
23,
1000,
15,
"BABJ",
"TD"
],
[
48,
"202405301500",
114.9,
24,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405302300",
"2024年05月30日23时00分",
null,
null
]
],
[
2927854,
"202405301800",
1717092000000,
"TD",
111.5,
18.9,
1000,
15,
"NNE",
15,
[],
{
"BABJ": [
[
12,
"202405301800",
112.1,
20.4,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405301800",
112.5,
22,
995,
20,
"BABJ",
"TS"
],
[
36,
"202405301800",
113.5,
23.3,
1000,
15,
"BABJ",
"TD"
],
[
48,
"202405301800",
115.6,
24.2,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405310200",
"2024年05月31日02时00分",
null,
null
]
],
[
2927859,
"202405302100",
1717102800000,
"TD",
111.5,
18.9,
1000,
15,
"NNE",
17,
[],
{
"BABJ": [
[
12,
"202405302100",
111.9,
20.6,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405302100",
112.3,
22.3,
998,
18,
"BABJ",
"TS"
],
[
36,
"202405302100",
113.8,
23.8,
1000,
15,
"BABJ",
"TD"
]
]
},
[
"202405310500",
"2024年05月31日05时00分",
null,
null
]
],
[
2927864,
"202405310000",
1717113600000,
"TD",
111.6,
19.1,
1000,
15,
"NNE",
19,
[],
{
"BABJ": [
[
12,
"202405310000",
112.1,
21,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405310000",
112.7,
22.8,
1000,
15,
"BABJ",
"TD"
],
[
36,
"202405310000",
114,
23.8,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405310800",
"2024年05月31日08时00分",
null,
null
]
],
[
2927869,
"202405310300",
1717124400000,
"TD",
111.9,
20.3,
1000,
15,
"NNE",
15,
[],
{
"BABJ": [
[
12,
"202405310300",
112.2,
21.8,
998,
18,
"BABJ",
"TS"
],
[
24,
"202405310300",
112.7,
23,
1000,
15,
"BABJ",
"TD"
],
[
36,
"202405310300",
114.1,
24,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405311100",
"2024年05月31日11时00分",
null,
null
]
],
[
2927917,
"202405310600",
1717135200000,
"TS",
111.9,
20.3,
998,
18,
"NNE",
15,
[
[
"30KTS",
80,
150,
120,
60,
2927917
]
],
{
"BABJ": [
[
6,
"202405310600",
111.9,
21.1,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405310600",
112.2,
21.8,
998,
18,
"BABJ",
"TS"
],
[
18,
"202405310600",
112.6,
22.7,
1000,
15,
"BABJ",
"TD"
],
[
24,
"202405310600",
113.1,
23.5,
1002,
13,
"BABJ",
"TD"
],
[
36,
"202405310600",
114.6,
24.5,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405311400",
"2024年05月31日14时00分",
null,
null
]
],
[
2927928,
"202405310700",
1717138800000,
"TS",
111.9,
20.3,
998,
18,
"NNE",
15,
[
[
"30KTS",
80,
150,
120,
60,
2927928
]
],
{
"BABJ": [
[
6,
"202405310700",
111.95,
21.2167,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405310700",
112.2667,
21.95,
998,
18,
"BABJ",
"TS"
],
[
18,
"202405310700",
112.6833,
22.8333,
1000,
14,
"BABJ",
"TD"
],
[
24,
"202405310700",
113.225,
23.5833,
1000,
13,
"BABJ",
"TD"
],
[
36,
"202405310700",
114.725,
24.5833,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202405311500",
"2024年05月31日15时00分",
null,
null
]
],
[
2927942,
"202405310800",
1717142400000,
"TS",
111.9,
20.3,
998,
18,
"NNE",
15,
[
[
"30KTS",
80,
150,
120,
60,
2927942
]
],
{
"BABJ": [
[
6,
"202405310800",
112,
21.3333,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405310800",
112.3333,
22.1,
998,
18,
"BABJ",
"TS"
],
[
18,
"202405310800",
112.7667,
22.9667,
1000,
14,
"BABJ",
"TD"
],
[
24,
"202405310800",
113.35,
23.6667,
1000,
13,
"BABJ",
"TD"
],
[
36,
"202405310800",
114.85,
24.6667,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202405311600",
"2024年05月31日16时00分",
null,
null
]
],
[
2927953,
"202405310900",
1717146000000,
"TS",
111.9,
20.4,
998,
18,
"N",
16,
[
[
"30KTS",
80,
150,
120,
60,
2927953
]
],
{
"BABJ": [
[
6,
"202405310900",
111.8,
21.2,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405310900",
112.2,
22.1,
1000,
15,
"BABJ",
"TD"
],
[
18,
"202405310900",
112.8,
23,
1002,
13,
"BABJ",
"TD"
],
[
24,
"202405310900",
113.3,
23.8,
1004,
12,
"BABJ",
"TD"
],
[
36,
"202405310900",
115.5,
24.7,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405311700",
"2024年05月31日17时00分",
null,
null
]
],
[
2928006,
"202405311000",
1717149600000,
"TS",
111.9,
20.5,
998,
18,
"N",
16,
[
[
"30KTS",
80,
150,
120,
60,
2928006
]
],
{
"BABJ": [
[
6,
"202405311000",
111.8667,
21.35,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405311000",
112.3,
22.25,
1000,
14,
"BABJ",
"TD"
],
[
18,
"202405311000",
112.8833,
23.1333,
1000,
13,
"BABJ",
"TD"
],
[
24,
"202405311000",
113.4833,
23.875,
1000,
12,
"BABJ",
"TD"
],
[
36,
"202405311000",
115.6833,
24.775,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202405311800",
"2024年05月31日18时00分",
null,
null
]
],
[
2928020,
"202405311100",
1717153200000,
"TS",
111.9,
20.5,
998,
18,
"N",
16,
[
[
"30KTS",
80,
150,
120,
60,
2928020
]
],
{
"BABJ": [
[
6,
"202405311100",
111.9333,
21.5,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405311100",
112.4,
22.4,
1000,
14,
"BABJ",
"TD"
],
[
18,
"202405311100",
112.9667,
23.2667,
1000,
12,
"BABJ",
"TD"
],
[
24,
"202405311100",
113.6667,
23.95,
1000,
12,
"BABJ",
"TD"
],
[
36,
"202405311100",
115.8667,
24.85,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202405311900",
"2024年05月31日19时00分",
null,
null
]
],
[
2928078,
"202405311200",
1717156800000,
"TS",
111.4,
20.7,
998,
18,
"NNE",
22,
[
[
"30KTS",
80,
150,
120,
60,
2928078
]
],
{
"BABJ": [
[
6,
"202405311200",
111.7,
21.9,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405311200",
112.2,
22.9,
1000,
15,
"BABJ",
"TD"
],
[
18,
"202405311200",
113,
23.8,
1002,
13,
"BABJ",
"TD"
],
[
24,
"202405311200",
113.9,
24.4,
1004,
12,
"BABJ",
"TD"
],
[
36,
"202405311200",
116.3,
24.9,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405312000",
"2024年05月31日20时00分",
null,
null
]
],
[
2928087,
"202405311300",
1717160400000,
"TS",
111.4,
20.9,
998,
18,
"NNE",
20,
[
[
"30KTS",
70,
150,
100,
50,
2928087
]
],
{
"BABJ": [
[
6,
"202405311300",
111.7833,
22.0667,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405311300",
112.3333,
23.05,
1000,
14,
"BABJ",
"TD"
],
[
18,
"202405311300",
113.15,
23.9,
1000,
13,
"BABJ",
"TD"
],
[
24,
"202405311300",
114.1,
24.4417,
1000,
12,
"BABJ",
"TD"
],
[
36,
"202405311300",
116.5,
24.9417,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202405312100",
"2024年05月31日21时00分",
null,
null
]
],
[
2928101,
"202405311400",
1717164000000,
"TS",
111.5,
21.1,
998,
18,
"NNE",
18,
[
[
"30KTS",
50,
150,
100,
40,
2928101
]
],
{
"BABJ": [
[
6,
"202405311400",
111.8667,
22.2333,
998,
18,
"BABJ",
"TS"
],
[
12,
"202405311400",
112.4667,
23.2,
1000,
14,
"BABJ",
"TD"
],
[
18,
"202405311400",
113.3,
24,
1000,
12,
"BABJ",
"TD"
],
[
24,
"202405311400",
114.3,
24.4833,
1000,
12,
"BABJ",
"TD"
],
[
36,
"202405311400",
116.7,
24.9833,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202405312200",
"2024年05月31日22时00分",
null,
null
]
],
[
2928144,
"202405311500",
1717167600000,
"TS",
111.5,
21.2,
998,
18,
"NNE",
22,
[
[
"30KTS",
50,
150,
100,
40,
2928144
]
],
{
"BABJ": [
[
6,
"202405311500",
111.9,
22.3,
1000,
15,
"BABJ",
"TD"
],
[
12,
"202405311500",
112.6,
23.3,
1000,
15,
"BABJ",
"TD"
],
[
18,
"202405311500",
113.4,
24.2,
1002,
13,
"BABJ",
"TD"
],
[
24,
"202405311500",
114.6,
24.9,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202405312300",
"2024年05月31日23时00分",
null,
null
]
],
[
2928154,
"202405311600",
1717171200000,
"TS",
111.5,
21.2,
998,
18,
"NNE",
22,
[
[
"30KTS",
30,
150,
100,
30,
2928154
]
],
{
"BABJ": [
[
6,
"202405311600",
112.0167,
22.4667,
998,
15,
"BABJ",
"TD"
],
[
12,
"202405311600",
112.7333,
23.45,
1000,
14,
"BABJ",
"TD"
],
[
18,
"202405311600",
113.6,
24.3167,
1000,
13,
"BABJ",
"TD"
],
[
24,
"202405311600",
114.8,
25.0167,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202406010000",
"2024年06月01日00时00分",
null,
null
]
],
[
2928165,
"202405311700",
1717174800000,
"TS",
111.7,
21.6,
998,
18,
"NNE",
18,
[],
{
"BABJ": [
[
6,
"202405311700",
112.1333,
22.6333,
998,
15,
"BABJ",
"TD"
],
[
12,
"202405311700",
112.8667,
23.6,
1000,
14,
"BABJ",
"TD"
],
[
18,
"202405311700",
113.8,
24.4333,
1000,
12,
"BABJ",
"TD"
],
[
24,
"202405311700",
115,
25.1333,
1000,
12,
"BABJ",
"TD"
]
]
},
[
"202406010100",
"2024年06月01日01时00分",
null,
null
]
],
[
2928203,
"202405311800",
1717178400000,
"TD",
111.7,
21.6,
1000,
15,
"NNE",
22,
[],
{
"BABJ": [
[
6,
"202405311800",
112.1,
22.7,
1000,
15,
"BABJ",
"TD"
],
[
12,
"202405311800",
112.9,
23.7,
1000,
15,
"BABJ",
"TD"
],
[
18,
"202405311800",
113.9,
24.5,
1002,
13,
"BABJ",
"TD"
],
[
24,
"202405311800",
115.2,
25.2,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202406010200",
"2024年06月01日02时00分",
null,
null
]
],
[
2928231,
"202405312100",
1717189200000,
"TD",
111.8,
22,
1000,
15,
"NNE",
21,
[
[
"30KTS",
30,
150,
100,
30,
2928231
]
],
{
"BABJ": [
[
6,
"202405312100",
112.4,
23,
1000,
15,
"BABJ",
"TD"
],
[
12,
"202405312100",
113.1,
23.9,
1002,
13,
"BABJ",
"TD"
],
[
18,
"202405312100",
114.1,
24.6,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202406010454",
"2024年06月01日04时54分",
null,
null
]
],
[
2928259,
"202406010000",
1717200000000,
"TD",
112.2,
22.6,
1000,
15,
"NNE",
20,
[],
{
"BABJ": [
[
6,
"202406010000",
112.6,
23.5,
1000,
15,
"BABJ",
"TD"
],
[
12,
"202406010000",
113.2,
24.4,
1002,
13,
"BABJ",
"TD"
],
[
18,
"202406010000",
114.6,
25.1,
1004,
12,
"BABJ",
"TD"
]
]
},
[
"202406010800",
"2024年06月01日08时00分",
null,
null
]
],
[
2928279,
"202406010300",
1717210800000,
"TD",
112.4,
22.8,
1000,
15,
"NE",
20,
[],
{
"BABJ": [
[
6,
"202406010300",
112.9,
23.9,
1000,
13,
"BABJ",
"TD"
],
[
12,
"202406010300",
113.9,
24.6,
1002,
12,
"BABJ",
"TD"
]
]
},
[
"202406011100",
"2024年06月01日11时00分",
null,
null
]
],
[
2928297,
"202406010600",
1717221600000,
"TD",
112.6,
23.3,
1002,
13,
"NE",
24,
[],
{
"BABJ": [
[
6,
"202406010600",
113.4,
24.3,
1002,
13,
"BABJ",
"TD"
],
[
12,
"202406010600",
114.6,
25,
1003,
12,
"BABJ",
"TD"
]
]
},
[
"202406011400",
"2024年06月01日14时00分",
null,
null
]
]
],
null
]
}
]
六、完整代码
import {typhone_type, typhoon_direction, typhoonCircle_colors} from "@/utils/leaflet/L.typhoon";
import '@/utils/leaflet/L.typhoon.js'
import moment from "moment";
import 'leaflet.animatedmarker/src/AnimatedMarker';
import 'leaflet.motion/dist/leaflmotion';
/**
* 风相关的hook
* @param typhoonData 台风数据
* @param map 地图实例
* @param duration 绘制间隔时间
* @returns {{drawTyphoonByCodes: drawTyphoonByCodes, clearFeaturesByIds: clearFeaturesByIds}}
*/
export function useTyphoon(typhoonData, map, duration = 10) {
//所有台风绘制的要素group的集合
let allTyphoonList = {};
//所有台风绘制的要素group的集合
let allFeatureGroup = {};
//已经绘制的台风id集合(缓存)
let typhoonIds_cache = [];
/**
* 根据台风数据的code,绘制台风相关的要素
* @param ids 台风id数组
*/
function drawTyphoonByCodes(ids = []) {
//新增的台风id集合
let addIds = ids.filter(id => !typhoonIds_cache.includes(id));
//删除的台风id集合
let deleteIds = typhoonIds_cache.filter(id => !ids.includes(id));
if (deleteIds.length > 0) {
clearFeaturesByIds(deleteIds);
}
addIds.forEach(id => {
allTyphoonList = {
...allTyphoonList, [id]: {
liveLine: null, futureLine: null, currentMarker: null, windCircle: null, title: null
}
}
drawFutureLine(id);
drawLiveLine(id);
drawCurrentMarker(id);
drawTitle(id);
});
typhoonIds_cache = [...typhoonIds_cache, ...addIds];
Object.entries(allTyphoonList).forEach(([id, typhoon]) => {
let {liveLine, futureLine, title, currentMarker, windCircle} = typhoon || {};
if (!(liveLine && title && currentMarker) || !addIds.includes(id)) {
return;
}
futureLine = futureLine || [];
//当前这个台风的featureGroup
let currentFeatureGroup = allFeatureGroup?.[id];
if (!currentFeatureGroup) {
currentFeatureGroup = L.featureGroup().addTo(map);
allFeatureGroup = {...allFeatureGroup, [id]: currentFeatureGroup};
}
//添加线的动画效果(实况)
let liveSeqGroup = L.motion.seq([...liveLine], {
auto: false,
}).addTo(currentFeatureGroup);
//添加线的动画效果(预报)
let futureSeqGroup = L.motion.seq([...futureLine], {
auto: false,
}).addTo(currentFeatureGroup);
//动态添加圆形气流图标(实况)
liveSeqGroup.on(L.Motion.Event.Section, (e) => {
let {sourceTarget} = e;
let {_activeLayer: {options: {circleMarker}}} = sourceTarget;
if (circleMarker) {
circleMarker.addTo(currentFeatureGroup);
}
});
//实况绘制完成,开始绘制预报
liveSeqGroup.on(L.Motion.Event.Ended, (e) => {
//添加当前台风的实况圆形气流图标
currentMarker.addTo(currentFeatureGroup);
futureSeqGroup.motionStart();
});
//动态添加圆形气流图标(未来)
futureSeqGroup.on(L.Motion.Event.Section, (e) => {
let {sourceTarget} = e;
let {_activeLayer: {options: {circleMarker}}} = sourceTarget;
if (circleMarker) {
circleMarker.addTo(currentFeatureGroup);
}
if (windCircle?.length > 0) {
windCircle.forEach((feature) => {
currentFeatureGroup.addLayer(feature);
});
}
});
title.addTo(currentFeatureGroup);
map.flyTo(title?.getLatLng());
setTimeout(() => {
liveSeqGroup.motionStart();
}, 500);
});
}
/**
* 获取风的级别
* @param kts 风速(kts)
* @returns {number}
*/
function convertKtsToBeaufort(kts) {
if (kts >= 0 && kts <= 1) {
return 0;
} else if (kts <= 3) {
return 1;
} else if (kts <= 6) {
return 2;
} else if (kts <= 10) {
return 3;
} else if (kts <= 16) {
return 4;
} else if (kts <= 21) {
return 5;
} else if (kts <= 27) {
return 6;
} else if (kts <= 33) {
return 7;
} else if (kts <= 40) {
return 8;
} else if (kts <= 47) {
return 9;
} else if (kts <= 55) {
return 10;
} else if (kts <= 63) {
return 11;
} else {
return 12;
}
}
/**
* 动态绘制线
* @param latLngs 线的坐标数组
* @param options 线的样式
* @param showMarker 是否显示气流图标
* @returns {*}
*/
function motionPolyline(latLngs = [], options = {color: "transparent"}, showMarker = true) {
return L.motion.polyline(latLngs, options, {
autoStart: false, duration: duration, easing: L.Motion.Ease.easeInOutQuart
}, showMarker ? {
removeOnEnd: true, showMarker: false, icon: L.icon({
iconUrl: require('@/assets/typhoon/typhoon.gif'), size: [50, 50], iconAnchor: [25, 25]
}),
} : null);
}
/**
* 删除指定id的台风相关的要素
* @param ids 台风id数组
*/
function clearFeaturesByIds(ids = []) {
if (ids.length === 0) {
Object.values(allFeatureGroup).forEach(typhoonGroup => {
if (typhoonGroup) {
typhoonGroup.clearLayers();
typhoonGroup.eachLayer((feature) => {
typhoonGroup.removeLayer(feature);
});
}
});
allTyphoonList = {};
typhoonIds_cache = [];
return;
}
ids.forEach(id => {
let typhoonGroup = allFeatureGroup?.[id];
if (typhoonGroup) {
typhoonGroup.clearLayers();
typhoonGroup.eachLayer((feature) => {
typhoonGroup.removeLayer(feature);
});
delete allTyphoonList[id];
typhoonIds_cache = typhoonIds_cache.filter(item => item !== id);
}
});
}
/**
* 台风名称添加到marker上
* @param id 台风id
*/
function drawTitle(id) {
const data = typhoonData?.[id]?.data;
if (data) {
const center = [data?.[8]?.[0]?.[5], data?.[8]?.[0]?.[4]];
let [, code, name] = data;
let titleStr = `<div style="font-size: 14px;font-weight: bold;color: #333;text-align: center;white-space: nowrap;text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;">${name}(${code})</div>`;
const title = L.divIcon({
html: titleStr, className: 'title-text', iconSize: [10, 14], iconAnchor: [-10, 7],
});
allTyphoonList[id].title = L.marker(center, {icon: title});
}
}
/**
* 获取台风节点的popup内容
* @param id 台风id
* @param pointData 节点数据
* @param type
* @returns {string}
*/
function getTyphoonContent(id, pointData, type) {
//当前这个台风的信息
const currentTyphoonData = typhoonData?.[id]?.data;
if (!currentTyphoonData) {
return '';
}
//这个台风实况节点的时间
const tbj = pointData?.[2] ? moment(pointData[2]).format('YYYY年MM月DD日HH时') : null;
let content;
//台风英文名称
let typhoonECName = currentTyphoonData[1];
//台风名称
let typhoonName = currentTyphoonData[2];
//台风编号
let typhoonNum = currentTyphoonData[4];
//节点中心位置
let centerPosition = [`${pointData[5]?.toFixed(1)}N/,${pointData[4]?.toFixed(1)}E`];
//节点最大风速
let maxWindSpeed = `${pointData[7]}米/秒`;
//节点中心气压
let centerPressure = `${pointData[6]}百帕`;
//节点移动方向
let moveDirection = `${getTyphoonDirection(pointData[8])}`;
//节点移动速度
let moveSpeed = `${pointData[9]}公里/小时`;
//如果是台风预测节点
if (type === 'FutureMarker') {
//台风实况路径节点数据集合
const points = currentTyphoonData?.[8] || [];
//最后一个实况节点数据
let lastPoint = points[points.length - 1];
//预报机构
let forecastInstitution = pointData[6] ? (pointData[6] === 'BABJ' ? '中央气象台' : data[6]) : '';
let _d = moment(lastPoint[2] + pointData[0] * 3600000).format('YYYY年MM月DD日HH时');
centerPosition = `${pointData[3].toFixed(1)}N/${pointData[2].toFixed(1)}E`;
maxWindSpeed = `${pointData[5]}米/秒`;
centerPressure = `${pointData[4]}百帕`;
content = '<div class=\'customLeafletPopup\'>' + '<div class=\'popupTitle\'>' + typhoonNum + (typhoonName ? typhoonName : typhoonECName) + '</div>' + '<table style = \'font-family: 微软雅黑; font-weight: normal; margin-top:5px;font-size: 12px;\' cellspacing=\'0\' cellpadding=\'0\' width=\'170px\'>' + '<tr><td class="label" width=\'65px\'>预报机构:</td><td>' + forecastInstitution + '</td></tr>' + '<tr><td class="label">到达时间:</td><td>' + _d + '</td></tr>' + '<tr><td class="label">中心位置:</td><td>' + centerPosition + '</td></tr>' + '<tr><td class="label">最大风速:</td><td>' + maxWindSpeed + '</td></tr>' + '<tr><td class="label">中心气压:</td><td>' + centerPressure + '</td></tr>' + '</table></div>';
} else if (type === 'CurrentMarker') {
let windCircleInfo = pointData?.[10];
let windCircleList = windCircleInfo.map((item, index) => {
let [windLevel, eastNorth, eastSouth, westSouth, westNorth] = item;
windLevel = windLevel.toUpperCase().replace('KTS', '');
windLevel = convertKtsToBeaufort(windLevel);
return `<div class="flex" style="background: rgba(6, 22, 67, 0.12);padding: 4px 0;border-radius: 4px;margin-bottom: 4px;">
<div class="circle-level" style="color:${typhoonCircle_colors[{
7: 0, 10: 1, 12: 2
}[windLevel]]?.color};font-family: MiSans,serif;font-size: 16px;font-weight: 500;line-height: normal;letter-spacing: 0;margin-right: 10px;text-align: right;">
<div style="width: 40px;font-weight: 500;line-height: 35px;">${windLevel}级</div>
</div>
<div style="margin-right: 24px;text-align: center;">
<div style="font-family: MiSans,serif;font-size: 12px;font-weight: normal;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.6);">东北</div>
<div style="text-align:center;font-family: MiSans,serif;font-size: 14px;font-weight: 600;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.9);width: 30px;">${eastNorth}</div>
</div>
<div style="margin-right: 24px;text-align: center;;">
<div style="font-family: MiSans,serif;font-size: 12px;font-weight: normal;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.6);">东南</div>
<div style="text-align:center;font-family: MiSans,serif;font-size: 14px;font-weight: 600;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.9);width: 30px;">${eastSouth}</div>
</div>
<div style="margin-right: 24px;text-align: center;;">
<div style="font-family: MiSans,serif;font-size: 12px;font-weight: normal;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.6);">西南</div>
<div style="text-align:center;font-family: MiSans,serif;font-size: 14px;font-weight: 600;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.9);width: 30px;">${westSouth}</div>
</div>
<div style="margin-right: 24px;text-align: center;;">
<div style="font-family: MiSans,serif;font-size: 12px;font-weight: normal;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.6);">西北</div>
<div style="text-align:center;font-family: MiSans,serif;font-size: 14px;font-weight: 600;line-height: normal;letter-spacing: 0;color: rgba(255, 255, 255, 0.9);width: 30px;">${westNorth}</div>
</div>
</div>`;
}).join('');
content = '<div class=\'customLeafletPopup\'> ' + '<div class=\'popupTitle\'>' + typhoonNum + (typhoonName ? typhoonName : typhoonECName) + '</div>' + '<div style="height: 267px;overflow-y: auto;" class="customScrollbarSmall"><table style = \'font-family: 微软雅黑; font-weight: normal; margin-top:5px;font-size: 12px;\' cellspacing = \'0\' cellpadding = \'0\' width = \'200px\' >' + '<tr><td class="label" width=\'65px\'>过去时间:</td><td>' + tbj + '</td></tr >' + '<tr><td class="label">中心位置:</td><td > ' + centerPosition + '</td ></tr> ' + '<tr><td class="label">最大风速:</td><td>' + maxWindSpeed + '</td></tr > ' + '<tr><td class="label">中心气压:</td><td>' + centerPressure + '</td></tr > ' + '<tr><td class="label">移动方向:</td><td>' + moveDirection + '</td></tr> ' + '<tr><td class="label">移动速度:</td><td>' + moveSpeed + '</td></tr > ' + '</table>'+'<div style="height: 1px;background: linear-gradient(90deg, rgba(19, 106, 255, 0) 0%, rgba(19, 141, 255, 0.9922) 53%, rgba(19, 232, 255, 0) 99%);margin: 8px 0;"></div>' +'<div style="color: rgba(255, 255, 255, 0.6);">风圈半径</div>'+ windCircleList+'</div>';
} else {
content = '<div class=\'customLeafletPopup\'> ' + '<div class=\'popupTitle\'>' + typhoonNum + (typhoonName ? typhoonName : typhoonECName) + '</div>' + '<table style = \'font-family: 微软雅黑; font-weight: normal; margin-top:5px;font-size: 12px;\' cellspacing = \'0\' cellpadding = \'0\' width = \'170px\' >' + '<tr><td class="label" width=\'65px\'>过去时间:</td><td>' + tbj + '</td></tr >' + '<tr><td class="label">中心位置:</td><td > ' + centerPosition + '</td ></tr> ' + '<tr><td class="label">最大风速:</td><td>' + maxWindSpeed + '</td></tr > ' + '<tr><td class="label">中心气压:</td><td>' + centerPressure + '</td></tr > ' + '<tr><td class="label">移动方向:</td><td>' + moveDirection + '</td></tr> ' + '<tr><td class="label">移动速度:</td><td>' + moveSpeed + '</td></tr > ' + '</table></div>';
}
return content;
}
/**
* 获取中文的方向描述
* @param direction
* @returns {*}
*/
function getTyphoonDirection(direction) {
return direction?.split('').map(d => {
return typhoon_direction[d];
}).filter(item => !!item).join('');
}
/**
* 绘制台风实况线(实线)
* @param id 台风id
*/
function drawLiveLine(id) {
const data = typhoonData?.[id]?.data;
if (!data) {
return;
}
//台风实况路径节点数据集合
const points = data?.[8] || [];
allTyphoonList[id].liveLine = points.map((pointData, index) => {
//当前节点坐标
const point = [pointData[5], pointData[4]];
//上一个坐标
const point_pre = index > 0 ? [points[index - 1]?.[5], points[index - 1]?.[4]] : [...point];
//热带气旋类型
let type = pointData?.[3];
type = type?.toUpperCase();
//节点标记
const circleMarker = L.circleMarker(point, {
radius: 6, color: '#ffffff', weight: 2, opacity: 1, fillColor: typhone_type?.[type], fillOpacity: 1,
});
let popupContent = getTyphoonContent(id, pointData, 'LiveMarker');
circleMarker.bindPopup(popupContent).openPopup(map);
circleMarker.on('mouseover', e => {
circleMarker.setRadius(8);
});
circleMarker.on('mouseout', e => {
circleMarker.setRadius(6);
});
return motionPolyline([point_pre, point], {
weight: 2,
color: typhone_type?.[type] || 'white',
fillColor: typhone_type?.[type],
opacity: 1,
circleMarker
});
}).filter(line => !!line);
}
/**
* 标记台风当前所在位置(gif图标 )
* @param id
*/
function drawCurrentMarker(id) {
const data = typhoonData?.[id]?.data;
if (!data) {
return;
}
//台风实况路径节点数据集合
const points = data?.[8] || [];
//最后一个节点数据
let lastPoint = points[points.length - 1];
//绘制风圈(风圈数据存在最后一个实况节点)
if (lastPoint?.[10]?.length > 0) {
drawWindCircle(id, lastPoint, lastPoint?.[10]);
}
//当前台风所在位置
const position = [lastPoint?.[5], lastPoint?.[4]];
//台风动图(旋转的gif)
const typhoonGifMarker = L.marker(position, {
icon: L.icon({
iconUrl: require('@/assets/typhoon/typhoon.gif'), size: [50, 50], iconAnchor: [25, 25]
})
});
const popupContent = getTyphoonContent(id, lastPoint, 'CurrentMarker');
typhoonGifMarker.bindPopup(popupContent).openPopup(map);
allTyphoonList[id].currentMarker = typhoonGifMarker;
}
/**
* 绘制未来线
* @param id
*/
function drawFutureLine(id) {
const data = typhoonData?.[id]?.data;
if (!data) {
return;
}
//台风实况路径节点数据集合
const points = data?.[8] || [];
//最后一个实况节点数据
let lastPoint = points[points.length - 1];
//台风未来节点数据
const futurePointList = lastPoint?.[11] || [];
//最后一个实况节点的位置就是未来线的起点
let path = [[lastPoint[5], lastPoint[4]]];
Object.entries(futurePointList).forEach(([key, positionList = []]) => {
//未来节点之间的线段集合
allTyphoonList[id].futureLine = positionList.map((positionData, index) => {
//当前节点坐标
const currentPoint = [positionData[3], positionData[2]];
//热带气旋类型
let type = positionData?.[7];
type = type?.toUpperCase();
//节点标记
const circleMarker = L.circleMarker(currentPoint, {
radius: 6, color: '#ffffff', weight: 2, opacity: 1, fillColor: typhone_type?.[type], fillOpacity: 1
});
let popupContent = getTyphoonContent(id, positionData, 'FutureMarker');
circleMarker.bindPopup(popupContent).openPopup(map);
circleMarker.on('mouseover', e => {
circleMarker.setRadius(8);
});
circleMarker.on('mouseout', e => {
circleMarker.setRadius(6);
});
//轨迹线
let line = motionPolyline([...path, currentPoint], {
weight: 2,
color: typhone_type?.[type],
fillColor: typhone_type?.[type],
opacity: 1,
dashArray: ['5', '5'],
circleMarker
}, false);
path = [[...currentPoint]];
return line;
}).filter(line => !!line);
});
}
/**
* 绘制不同等级的风圈
* @param id
* @param cdata
* @param winC
*/
function drawWindCircle(id, cdata, winC = []) {
//风圈的位置坐标
let position = [cdata?.[5], cdata?.[4]];
//遍历生成风圈
allTyphoonList[id].windCircle = winC.map((windData, i) => {
let [, ne, se, nw, sw] = windData;
const windCircle = {ne, se, nw, sw};
const options = {
...typhoonCircle_colors[i], clickable: true, radius: 100, className: 'windCircleMarker'
};
return L.typhoon(position, windCircle, options, map);
});
}
return {drawTyphoonByCodes, clearFeaturesByIds};
}