leaflet-velocity-scalar-csl
v1.0.8
Published
A custom layer for leaflet to visualise arbitrary velocities with ES module and UMD support
Maintainers
Readme
Leaflet Velocity 中文使用文档
一个用于在 Leaflet 地图上可视化风场、洋流等矢量数据的插件。
特性
- 🌊 支持风场和洋流数据可视化
- 🌡️ 支持温度、湿度、气压等标量数据可视化
- 🎨 粒子动画效果,直观展示矢量方向和强度
- 🎯 标量数据热力图渲染,支持自定义颜色方案
- 📱 响应式设计,支持移动端
- 🔧 灵活的配置选项
- 📦 支持 ES 模块和 UMD 两种引入方式
- ⚡ 轻量级,性能优化
📚 教程和示例
完整教程文档
- ScalarLayer 使用教程 - 标量数据可视化完整指南
在线示例
数据类型支持
- 矢量数据: 风场、洋流、流场等方向性数据
- 标量数据: 温度、湿度、气压、海拔、降水量等数值型数据
安装
NPM 安装
npm install leaflet-velocity-scalar-csl --saveCDN 引入
<!-- Leaflet 依赖 -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<!-- Leaflet Velocity 插件 -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-velocity-scalar/dist/leaflet-velocity-scalar.css" />
<script src="https://unpkg.com/leaflet-velocity-scalar/dist/leaflet-velocity-scalar.js"></script>使用方式
方式一:UMD 格式(传统 script 标签引入)
适用于传统的 HTML 页面开发:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<link rel="stylesheet" href="./dist/leaflet-velocity-scalar.css" />
</head>
<body>
<div id="map" style="height: 500px;"></div>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script src="./dist/leaflet-velocity-scalar.js"></script>
<script>
// 初始化地图
const map = L.map('map').setView([25, 121], 6);
// 添加底图
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// 加载风场数据
fetch('./demo/wind-gbr.json')
.then(response => response.json())
.then(data => {
// 创建风场图层
const velocityLayer = L.velocityLayer({
data: data,
lineWidth: 2,
displayOptions: {
velocityType: 'Wind',
displayPosition: 'bottomleft',
displayEmptyString: '无数据'
}
});
// 添加到地图
velocityLayer.addTo(map);
// 添加控制面板
const control = L.control.velocity().addTo(map);
});
</script>
</body>
</html>方式二:ES 模块引入
适用于现代前端框架(Vue、React、Angular 等):
基础引入
// 引入 Leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// 引入 Leaflet Velocity
import { velocityLayer, controlVelocity } from 'leaflet-velocity-scalar';
import 'leaflet-velocity-scalar/dist/leaflet-velocity-scalar.css';
// 初始化地图
const map = L.map('map').setView([25, 121], 6);
// 添加底图
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// 加载并添加风场数据
fetch('./wind-data.json')
.then(response => response.json())
.then(data => {
const velocity = velocityLayer({
data: data,
lineWidth: 2,
displayOptions: {
velocityType: 'Wind',
displayPosition: 'bottomleft'
}
});
velocity.addTo(map);
// 添加控制面板
const control = controlVelocity().addTo(map);
});按需引入
// 只引入需要的组件
import { VelocityLayer, ControlVelocity } from 'leaflet-velocity-scalar';
// 或者使用 API 获取函数
import { getLeafletVelocityAPI } from 'leaflet-velocity-scalar';
const api = getLeafletVelocityAPI();
const { velocityLayer, controlVelocity } = api;Vue 3 示例
风场图层 (VelocityLayer) 示例
<template>
<div id="map" ref="mapContainer" style="height: 500px;"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { velocityLayer } from 'leaflet-velocity-scalar';
import 'leaflet-velocity-scalar/dist/leaflet-velocity-scalar.css';
const mapContainer = ref(null);
let map = null;
onMounted(() => {
// 初始化地图
map = L.map(mapContainer.value).setView([25, 121], 6);
// 添加底图
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// 加载风场数据
loadWindData();
});
const loadWindData = async () => {
try {
const response = await fetch('./wind-data.json');
const data = await response.json();
const velocity = velocityLayer({
data: data,
lineWidth: 2,
displayOptions: {
velocityType: '风场',
displayPosition: 'bottomleft',
displayEmptyString: '无风场数据'
}
});
velocity.addTo(map);
} catch (error) {
console.error('加载风场数据失败:', error);
}
};
</script>标量场图层 (ScalarLayer) 示例
<template>
<div class="scalar-map-container">
<div id="map" ref="mapContainer" style="height: 500px; width: 100%;"></div>
<div class="controls">
<button @click="loadTemperatureData" :disabled="loading">
{{ loading ? '加载中...' : '加载温度数据' }}
</button>
<button @click="clearLayer" :disabled="!scalarLayerInstance">
清除图层
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import { scalarLayer } from 'leaflet-velocity-scalar'
import 'leaflet-velocity-scalar/dist/leaflet-velocity-scalar.css'
// 响应式数据
const mapContainer = ref(null)
const loading = ref(false)
const scalarLayerInstance = ref(null)
let map = null
// 模拟温度数据
const mockTemperatureData = [{
header: {
parameterCategory: 2,
parameterNumber: 2,
dx: 1.0,
dy: 1.0,
la1: 30.0,
la2: 20.0,
lo1: 115.0,
lo2: 125.0,
nx: 11,
ny: 11,
refTime: "2023-01-01 00:00:00"
},
data: [
273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283,
274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284,
// ... 更多数据点
]
}]
// 初始化地图
const initMap = () => {
if (!mapContainer.value) return
map = L.map(mapContainer.value, {
center: [25, 120],
zoom: 6,
zoomControl: true
})
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map)
}
// 加载温度数据
const loadTemperatureData = async () => {
if (!map) return
loading.value = true
try {
// 清除现有图层
if (scalarLayerInstance.value) {
map.removeLayer(scalarLayerInstance.value)
scalarLayerInstance.value = null
}
// 创建ScalarLayer实例
const layer = scalarLayer({
displayValues: true,
displayOptions: {
velocityType: "Temperature",
position: "bottomleft",
emptyString: "No temperature data"
},
minValue: 273, // 最小温度值 (0°C)
maxValue: 293, // 最大温度值 (20°C)
colorScale: [
[273, [0, 0, 255]], // 蓝色 (冷)
[283, [0, 255, 0]], // 绿色 (中等)
[293, [255, 0, 0]] // 红色 (热)
]
})
// 设置数据并添加到地图
layer.setData(mockTemperatureData)
layer.addTo(map)
scalarLayerInstance.value = layer
} catch (error) {
console.error('加载温度数据失败:', error)
} finally {
loading.value = false
}
}
// 清除图层
const clearLayer = () => {
if (scalarLayerInstance.value && map) {
map.removeLayer(scalarLayerInstance.value)
scalarLayerInstance.value = null
}
}
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (scalarLayerInstance.value && map) {
map.removeLayer(scalarLayerInstance.value)
}
if (map) {
map.remove()
}
})
</script>
<style scoped>
.scalar-map-container {
width: 100%;
height: 100%;
position: relative;
}
.controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.controls button {
margin: 0 5px;
padding: 8px 16px;
border: none;
border-radius: 3px;
background: #007cbb;
color: white;
cursor: pointer;
}
.controls button:hover:not(:disabled) {
background: #005a87;
}
.controls button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>React 示例
import React, { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { velocityLayer } from 'leaflet-velocity-scalar';
import 'leaflet-velocity-scalar/dist/leaflet-velocity-scalar.css';
const WindMap = () => {
const mapRef = useRef(null);
const mapInstance = useRef(null);
useEffect(() => {
if (!mapInstance.current) {
// 初始化地图
mapInstance.current = L.map(mapRef.current).setView([25, 121], 6);
// 添加底图
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
.addTo(mapInstance.current);
// 加载风场数据
loadWindData();
}
return () => {
if (mapInstance.current) {
mapInstance.current.remove();
}
};
}, []);
const loadWindData = async () => {
try {
const response = await fetch('./wind-data.json');
const data = await response.json();
const velocity = velocityLayer({
data: data,
lineWidth: 2,
displayOptions: {
velocityType: 'Wind',
displayPosition: 'bottomleft'
}
});
velocity.addTo(mapInstance.current);
} catch (error) {
console.error('Failed to load wind data:', error);
}
};
return <div ref={mapRef} style={{ height: '500px', width: '100%' }} />;
};
export default WindMap;配置选项
VelocityLayer 选项
const options = {
// 数据源
data: windData, // 风场数据(必需)
// 显示选项
displayOptions: {
velocityType: 'Wind', // 显示类型:'Wind'、'Ocean'、'Custom'
displayPosition: 'bottomleft', // 控制面板位置
displayEmptyString: '无数据', // 无数据时显示的文本
speedUnit: 'm/s', // 速度单位
directionString: '风向', // 方向标签
speedString: '风速' // 速度标签
},
// 粒子效果
lineWidth: 2, // 粒子轨迹宽度
colorScale: [
'rgb(36,104, 180)',
'rgb(60,157, 194)',
'rgb(128,205,193)',
'rgb(151,218,168)',
'rgb(198,231,181)',
'rgb(238,247,217)',
'rgb(255,238,159)',
'rgb(252,217,125)',
'rgb(255,182,119)',
'rgb(252,150,75)',
'rgb(250,112,52)',
'rgb(245,64,32)',
'rgb(237,45,28)',
'rgb(220,24,32)',
'rgb(180,0,35)'
], // 颜色映射
// 性能选项
maxVelocity: 10, // 最大速度值
velocityScale: 0.005, // 速度缩放因子
particleAge: 90, // 粒子生命周期
particleMultiplier: 1/300 // 粒子密度
};Control 选项
const controlOptions = {
position: 'bottomleft', // 控制面板位置
emptyString: '无数据', // 无数据显示文本
angleConvention: 'bearingCCW', // 角度约定
showCardinal: true, // 显示基数方向
velocityUnit: 'm/s', // 速度单位
directionString: '方向:', // 方向标签
speedString: '速度:' // 速度标签
};数据格式
风场数据需要符合以下 JSON 格式:
[
{
"header": {
"parameterCategory": 2,
"parameterNumber": 2,
"dx": 0.5,
"dy": 0.5,
"la1": 90.0,
"la2": -90.0,
"lo1": 0.0,
"lo2": 359.5,
"nx": 720,
"ny": 361
},
"data": [/* U 分量数据数组 */]
},
{
"header": {
"parameterCategory": 2,
"parameterNumber": 3,
"dx": 0.5,
"dy": 0.5,
"la1": 90.0,
"la2": -90.0,
"lo1": 0.0,
"lo2": 359.5,
"nx": 720,
"ny": 361
},
"data": [/* V 分量数据数组 */]
}
]数据说明
parameterNumber: 2- U 分量(东西方向)parameterNumber: 3- V 分量(南北方向)dx,dy- 网格间距(度)la1,la2- 纬度范围lo1,lo2- 经度范围nx,ny- 网格数量data- 对应的数值数组
API 参考
L.velocityLayer(options)
创建一个新的风场图层。
参数:
options(Object) - 配置选项
返回:
- VelocityLayer 实例
L.control.velocity(options)
创建一个风场控制面板。
参数:
options(Object) - 控制面板选项
返回:
- Control.Velocity 实例
实例方法
velocityLayer.setData(data)
更新风场数据。
参数:
data(Array) - 新的风场数据
velocityLayer.setOptions(options)
更新配置选项。
参数:
options(Object) - 新的配置选项
常见问题
Q: 如何处理大数据量的风场数据?
A: 可以通过以下方式优化性能:
const options = {
data: windData,
particleMultiplier: 1/500, // 减少粒子密度
particleAge: 60, // 缩短粒子生命周期
lineWidth: 1 // 减小线宽
};Q: 如何自定义颜色映射?
A: 修改 colorScale 选项:
const customColors = [
'#0000FF', // 蓝色 - 低速
'#00FF00', // 绿色 - 中速
'#FFFF00', // 黄色 - 高速
'#FF0000' // 红色 - 极高速
];
const velocityLayer = L.velocityLayer({
data: windData,
colorScale: customColors
});Q: 如何在移动端优化性能?
A: 针对移动端可以使用以下配置:
const mobileOptions = {
data: windData,
particleMultiplier: 1/800, // 更少的粒子
lineWidth: 1, // 更细的线条
particleAge: 45, // 更短的生命周期
velocityScale: 0.01 // 调整速度缩放
};浏览器支持
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
- 移动端浏览器
许可证
详见 LICENSE.md 文件。
贡献
欢迎提交 Issue 和 Pull Request!
更新日志
v2.1.4
- ✨ 新增 ES 模块支持
- 🔧 优化构建配置
- 📚 完善文档
- 🐛 修复已知问题
v2.1.3
- 🐛 修复性能问题
- 🔧 优化粒子渲染
v2.1.2
- ✨ 新增自定义颜色映射
- 🔧 改进数据处理逻辑
