前言
假装已经完成了vue3和Openlayers10开发环境的搭建,如果有需要,可搜索vue+Openlayers环境搭建的相关文章。
本示例基于Vue3 和 Openlayers10 的环境,实现台风轨迹和台风圈的效果。
一、安装插件
安装Element-plus插件,其实只在台风列表的地方用到了el-checkbox,可根据实际需要取舍。
//安装Element-plus插件
npm install element-plus --save
安装成功后,在vue项目的package.json文件中会有Element-plus插件的依赖,如下图。
二、代码展示
项目中main.js的代码:
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
项目中App.vue的代码:
<template>
<HomePage></HomePage>
</template>
<script>
import HomePage from './views/HomePage.vue'
export default {
name: 'App',
components: {
HomePage,
},
}
</script>
项目中.\views\HomePage.vue的代码:
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px">Vue+Openlayers 开发示例目录</el-aside>
<el-container>
<el-main>
<Typhoon></Typhoon>
</el-main>
<!--<el-footer>@King空 @格子问道</el-footer>-->
</el-container>
</el-container>
</div>
</template>
<script>
/* eslint-disable */
import OlWind from '../components/Ol_Wind.vue'
export default {
name: 'HomePage',
components: {
Typhoon
},
}
</script>
<style scoped>
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
background-color: #E9EEF3;
color: #333;
text-align: center;
}
body>.el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>
提示:为了方便初始化地图对象和底图,我新建了一个继承至Map的BaseMap类,放在.\src\components\BaseMap.js里。
项目中.\src\components\BaseMap.js的代码如下:
/* eslint-disable */
import "ol/ol.css";
import * as olProj from "ol/proj";
import { Map, View } from "ol";
import Tile from "ol/layer/Tile";
import XYZ from "ol/source/XYZ";
/**
* @classdesc
* The map is the core component of OpenLayers. For a map to render, a view,
* one or more layers, and a target container are needed:
*
* import Map from 'ol/Map.js';
* import View from 'ol/View.js';
* import TileLayer from 'ol/layer/Tile.js';
* import OSM from 'ol/source/OSM.js';
*
* const map = new Map({
* view: new View({
* center: [0, 0],
* zoom: 1,
* }),
* layers: [
* new TileLayer({
* source: new OSM(),
* }),
* ],
* target: 'map',
* });
*
* The above snippet creates a map using a {@link module:ol/layer/Tile~TileLayer} to
* display {@link module:ol/source/OSM~OSM} OSM data and render it to a DOM
* element with the id `map`.
*
* The constructor places a viewport container (with CSS class name
* `ol-viewport`) in the target element (see `getViewport()`), and then two
* further elements within the viewport: one with CSS class name
* `ol-overlaycontainer-stopevent` for controls and some overlays, and one with
* CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent`
* option of {@link module:ol/Overlay~Overlay} for the difference). The map
* itself is placed in a further element within the viewport.
*
* Layers are stored as a {@link module:ol/Collection~Collection} in
* layerGroups. A top-level group is provided by the library. This is what is
* accessed by `getLayerGroup` and `setLayerGroup`. Layers entered in the
* options are added to this group, and `addLayer` and `removeLayer` change the
* layer collection in the group. `getLayers` is a convenience function for
* `getLayerGroup().getLayers()`. Note that {@link module:ol/layer/Group~LayerGroup}
* is a subclass of {@link module:ol/layer/Base~BaseLayer}, so layers entered in the
* options or added with `addLayer` can be groups, which can contain further
* groups, and so on.
*
* @fires import("./MapBrowserEvent.js").MapBrowserEvent
* @fires import("./MapEvent.js").MapEvent
* @fires import("./render/Event.js").default#precompose
* @fires import("./render/Event.js").default#postcompose
* @fires import("./render/Event.js").default#rendercomplete
* @api
*/
class BaseMap extends Map {
/**
* @param {MapOptions} [options] Map options.
*/
constructor(options) {
//console.log("BaseMap 构造函数");
super(options);
//添加基础底图图层
this.addLayer(
new Tile({
source: new XYZ({
//高德地图
url: "https://webrd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8",
}),
})
);
//设置View
this.setView(
new View({
projection: "EPSG:3857",
center: olProj.transform([110, 30], "EPSG:4326", "EPSG:3857"),
zoom: 3,
})
);
}
}
export default BaseMap;
至此,准备工作已完成,该示例中最主要的台风组件.\src\components\Typhoon.vue的代码如下:
<template>
<div class="header">
<!--利用台风的实况和预报数据,实现台风按轨迹移动的效果-->
<h2>Vue3 + Openlayers10 实现台风轨迹和风圈效果</h2>
<el-checkbox :label="item.name" v-for="item in tyhoonList" :value="item.tfid" :key="item.tfid"
@change="loadTyphoon(item.tfid)">
{{ item.name }} ( {{item.enname}} : {{item.tfid}} )
</el-checkbox>
</div>
<div id="vue-openlayers"></div>
</template>
<script>
/* eslint-disable */
import * as ol from 'ol';
import { source, layer, vector, geom, style, } from 'ol';
import VectorSource from 'ol/source/Vector.js';
import VectorLayer from 'ol/layer/Vector.js';
import { Style as olStyle, Fill as olFill, Text as olText, Stroke as olStroke, Icon as olIcon } from 'ol/style';
import * as olProj from 'ol/proj';
import Feature from 'ol/Feature.js';
import { LineString, Polygon, Point, Circle } from "ol/geom";
//导入自定义基础地图
import BaseMap from './BaseMap.js';
//本地台风列表数据Json文件
import tyhoonactivity from "../assets/typhoon/data/tyhoonactivity.json";
//2024年13号台风贝碧嘉的数据Json文件
import typhoon_202413 from "../assets/typhoon/data/typhoon_202413_BEBINCA.json";
//2024年14号台风普拉桑的数据Json文件
import typhoon_202414 from "../assets/typhoon/data/typhoon_202414_PULASAN.json";
import imaget01 from "../assets/typhoon/images/t-01.png";
import imaget02 from "../assets/typhoon/images/t-02.png";
import imaget03 from "../assets/typhoon/images/t-03.png";
import imaget04 from "../assets/typhoon/images/t-04.png";
import imaget05 from "../assets/typhoon/images/t-05.png";
import imaget06 from "../assets/typhoon/images/t-06.png";
export default {
name: 'Typhoon',
data() {
return {
map: null,
//台风列表
tyhoonList: null,
//台风信息详情
typhoonDetail: null,
//24小时警戒线
stromAlertLineLayer24: null,
//48小时警戒线
stromAlertLineLayer48: null,
//台风路径图层
tfRouteLayer: null,
//台风圈图层
tfCircleLayer7: null,
//台风圈图层
tfCircleLayer10: null,
//台风圈图层
tfCircleLayer12: null,
//台风路径线样式
tfLine_olStroke: new olStroke({
color: "blue",
width: 2,
}),
//台风风圈样式
tfCircleStyles: {
//7级
tfCircleStyle7: new olStyle({
fill: new olFill({
color: "rgb(230, 165, 53, 0.2)", // 黄色半透明填充
}),
stroke: new olStroke({
color: "rgb(230, 165, 53)", // 黄色半透明填充
width: 1,
}),
text: new olText({
font: "bold 14px 微软雅黑",
text: "七级风圈",
offsetX: 10,
offsetY: 15,
fill: new olFill({
color: "rgba(230, 165, 53)",
}),
}),
}),
//10级
tfCircleStyle10: new olStyle({
fill: new olFill({
color: "rgb(230, 165, 53, 0.5)", // 黄色半透明填充
}),
stroke: new olStroke({
color: "rgb(230, 165, 53)",
width: 1,
}),
text: new olText({
font: "bold 14px 微软雅黑",
text: "十级风圈",
offsetX: 10,
offsetY: 15,
fill: new olFill({
color: "rgb(230, 165, 53)",
}),
}),
}),
//12级
tfCircleStyle12: new olStyle({
fill: new olFill({
color: "rgb(230, 165, 53)", // 黄色半透明填充
}),
stroke: new olStroke({
color: "rgb(230, 165, 53)", // 黄色半透明填充
width: 1,
}),
text: new olText({
font: "bold 14px 微软雅黑",
text: "十二级风圈",
offsetX: 10,
offsetY: 15,
fill: new olFill({
color: "rgba(231,45,32,0.16)",
}),
}),
}),
},
}
},
methods: {
//初始化地图
initMap() {
//创建基础地图
this.map = new BaseMap({
target: "vue-openlayers",
});
//绘制台风24、48小时警戒线
this.DrawStromAlertLineLayer24(true, this.map);
this.DrawStromAlertLineLayer48(true, this.map);
//直接用本地样例数据
this.tyhoonList = tyhoonactivity;
//初始化台风路径图层
this.tfRouteLayer = new VectorLayer({
source: new VectorSource(),
map: this.map,
style: this.getTyphoonPointStyleByFeature,
});
//初始化风圈图层
this.tfCircleLayer7 = new VectorLayer({
source: new VectorSource(),
map: this.map,
style: this.tfCircleStyles.tfCircleStyle7,
});
this.tfCircleLayer10 = new VectorLayer({
source: new VectorSource(),
map: this.map,
style: this.tfCircleStyles.tfCircleStyle10,
});
this.tfCircleLayer12 = new VectorLayer({
source: new VectorSource(),
map: this.map,
style: this.tfCircleStyles.tfCircleStyle12,
});
},
//台风登陆警戒线24、48小时线
//24 小时警戒线(0°N, 105°E;4.5°N, 113°E;11°N, 119°E;18°N, 119°E;22°N, 127°E;34°N, 127°E)
//48 小时警戒线(0°N, 105°E; 0°N, 120°E;15°N, 132°E;34°N, 132°E)
//加载台风24小时警戒线
DrawStromAlertLineLayer24(visible, map) {
let alertLine24 = [[127, 34], [127, 22], [119, 18], [113, 4.5], [105, 0]];
let stromAlertLineLayer24Visible = visible;
if (stromAlertLineLayer24Visible) {
for (var i = 0; i < alertLine24.length; i++) {
alertLine24[i] = olProj.transform(alertLine24[i], 'EPSG:4326', 'EPSG:3857');
}
let featureLine24 = new Feature({
name: '24小时警戒线',
geometry: new LineString(alertLine24),
});
let vectorLine24 = new VectorSource({});
vectorLine24.addFeature(featureLine24);
this.stromAlertLineLayer24 = new VectorLayer({
source: vectorLine24,
style: new olStyle({
fill: new olFill({ color: [237, 41, 51], weight: 4 }),
stroke: new olStroke({ color: [237, 41, 51], width: 2 }),
text: new olText({
font: 'Italic bold 12px 思源黑体',
text: '24 小 时 警 戒 线',
fill: new olFill({
color: [237, 41, 51],
}),
placement: 'line',// 标注设置为沿线方向排列
textAlign: 'left',
textBaseline: 'bottom',
rotateWithView: true,
rotation: 90,
}),
}),
});
if (this.stromAlertLineLayer24 != undefined) {
map.removeLayer(this.stromAlertLineLayer24);
}
map.addLayer(this.stromAlertLineLayer24);
}
else {
if (this.stromAlertLineLayer24 != undefined) {
map.removeLayer(this.stromAlertLineLayer24);
}
}
},
//加载台风48小时警戒线
DrawStromAlertLineLayer48(visible, map) {
let alertLine48 = [[132, 34], [132, 15], [120, 0], [105, 0]];
let stromAlertLineLayer48Visible = visible;
if (stromAlertLineLayer48Visible) {
for (var i = 0; i < alertLine48.length; i++) {
alertLine48[i] = olProj.transform(alertLine48[i], 'EPSG:4326', 'EPSG:3857');
}
let featureLine48 = new Feature({
name: '48小时警戒线',
geometry: new LineString(alertLine48),
});
let vectorLine48 = new VectorSource({});
vectorLine48.addFeature(featureLine48);
this.stromAlertLineLayer48 = new VectorLayer({
source: vectorLine48,
style: new olStyle({
fill: new olFill({ color: 'blue', weight: 4 }),
stroke: new olStroke({ color: 'blue', width: 2, lineDash: [6, 6] }),
text: new olText({
font: 'Italic bold 12px 思源黑体',
text: '48 小 时 警 戒 线',
fill: new olFill({
color: 'blue',
}),
placement: 'line',// 标注设置为沿线方向排列
textAlign: 'left',
textBaseline: 'bottom',
rotateWithView: true,
rotation: 90,
}),
}),
});
if (this.stromAlertLineLayer48 != undefined) {
map.removeLayer(this.stromAlertLineLayer48);
}
map.addLayer(this.stromAlertLineLayer48);
}
else {
if (this.stromAlertLineLayer48 != undefined) {
map.removeLayer(this.stromAlertLineLayer48);
}
}
},
/***
* /@description 加载台风
* /@function
* /@param ftid 台风ID
* /@returns
***/
loadTyphoon(tfid) {
//直接用本地样例数据
this.typhoonDetail = (tfid == '202413') ? typhoon_202413 : typhoon_202414;
let js = JSON.stringify(this.typhoonDetail);
let jsO = JSON.parse(js);
//绘制台风
this.DrawTyphoon(jsO);
},
/***
* @description 刷新绘制台风图层
*
***/
DrawTyphoon(tfDetails) {
//台风路径header信息
let tfid = tfDetails.tfid,
name = tfDetails.name,
enname = tfDetails.enname,
isactive = tfDetails.isactive,
starttime = tfDetails.starttime,
endtime = tfDetails.endtime,
warnlevel = tfDetails.warnlevel,
centerlng = tfDetails.centerlng,
centerlat = tfDetails.centerlat;
let tfPoints = new Array();
let tfPointsFeature = new Array();
for (let i = 0; i < tfDetails.points.length; i++) {
let tfPoint = tfDetails.points[i];
//台风路径点详细信息
let time = tfPoint.time,//: 2024-09-10 20:00:00,
lng = tfPoint.lng,//: 145.40, 经度
lat = tfPoint.lat,//: 12.40, 纬度
strong = tfPoint.strong,//: 热带风暴,
power = tfPoint.power,//: 8,
speed = tfPoint.power,//: 18,
pressure = tfPoint.pressure,//: 998,
movespeed = tfPoint.pressure,//: 26,
movedirection = tfPoint.movedirection,//: 北西,
radius7 = tfPoint.radius7,//: 240|220|220|220,
radius10 = tfPoint.radius10,
radius12 = tfPoint.radius12,
forecast = tfPoint.forecast;
tfPoints.push(olProj.transform([tfPoint.lng, tfPoint.lat], 'EPSG:4326', 'EPSG:3857'));
//将台风路径点详细信息加入台风路径点的集合
tfPointsFeature.push(new Feature({
id: tfid + "-" + i,
geometry: new Point(olProj.transform([tfPoint.lng, tfPoint.lat], 'EPSG:4326', 'EPSG:3857')),
name: tfDetails.name,
time: tfPoint.time,//: 2024-09-10 20:00:00,
lng: tfPoint.lng,//: 145.40, 经度
lat: tfPoint.lat,//: 12.40, 纬度
strong: tfPoint.strong,//: 热带风暴,
power: tfPoint.power,//: 8,
speed: tfPoint.power,//: 18,
pressure: tfPoint.pressure,//: 998,
movespeed: tfPoint.pressure,//: 26,
movedirection: tfPoint.movedirection,//: 北西,
radius7: tfPoint.radius7,//: 240|220|220|220,
radius10: tfPoint.radius10,//: ,
radius12: tfPoint.radius12,//: ,
forecast: tfPoint.forecast,//:
}));
}
//将台风路径点加入路径线
let tfRoute = new LineString(tfPoints);
//台风路径Feature
let tfRouteFeature = new Feature({ geometry: tfRoute });
//台风图层的数据源
let layerSource_tfRoute = this.tfRouteLayer.getSource();
layerSource_tfRoute.addFeature(tfRouteFeature); //添加路径线
layerSource_tfRoute.addFeatures(tfPointsFeature); //添加路径点
//在台风路径上随便找一个点,画出相应的台风圈
let lastFeature = tfPointsFeature[tfPointsFeature.length - 42];
//画台风圈
TyphoonCircle.Draw(this.tfCircleLayer7, [lastFeature.get('lng'), lastFeature.get("lat")], lastFeature.get("radius7"));
TyphoonCircle.Draw(this.tfCircleLayer10, [lastFeature.get('lng'), lastFeature.get("lat")], lastFeature.get("radius10"));
TyphoonCircle.Draw(this.tfCircleLayer12, [lastFeature.get('lng'), lastFeature.get("lat")], lastFeature.get("radius12"));
},
//获取台风路径点和路径线的Style样式对象
getTyphoonPointStyleByFeature(feature) {
let img = undefined;
switch (feature.get('strong')) {
case "热带低压": img = imaget01;
break;
case "热带风暴": img = imaget02;
break;
case "强热带风暴": img = imaget03;
break;
case "台风": img = imaget04;
break;
case "强台风": img = imaget05;
break;
case "超强台风": img = imaget06;
break;
default:
img = imaget01;
break;
}
return new olStyle({
//线
stroke: this.tfLine_olStroke,
//点
image: new olIcon({
src: img,
}),
});
},
},
mounted() {
this.initMap();
},
}
/***
* @classdesc 台风圈类
***/
class TyphoonCircle {
/***
* @description 构造函数
* @param {*} vectorLayer 扇形所在图层
* @param {*} center 扇形圆心点的经纬度坐标(EPSG:4326)
* @param {*} radius 风圈半径 ("radius7": "220|260|180|260", 风圈格式:东北|东南|西北|西南,具体值为风圈半径,单位:公里。)
***/
constructor(vectorLayer, center, radius) {
this.layer = vectorLayer;
this.center = olProj.fromLonLat(center); //将经纬度转换为平面投影坐标
this.radius = radius;
}
/***
* @description 获取台风圈配置信息的对象
* @param {*} center 台风圈圆心的经纬度坐标(EPSG:4326)
* @param {*} radius 台风圈半径("radius7": "220|260|180|260", 风圈格式:东北|东南|西北|西南,具体值为风圈半径,单位:公里。)
**/
static getTyphoonCircleConfig(center, radius) {
//拆分风圈四个象限的半径
let radiuses = radius.split("|");
if (radiuses != undefined && radiuses.length == 4) {
let config = {
x: center[0], //台风圈中心点经度
y: center[1], //台风圈中心点维度
//扇形风场
sector: {
EN: { r: radiuses[0], startAngle: 0, endAngle: 90 }, //第一象限
WN: { r: radiuses[2], startAngle: 90, endAngle: 180 }, //第二象限
WS: { r: radiuses[3], startAngle: 180, endAngle: 270 }, //第三象限
ES: { r: radiuses[1], startAngle: 270, endAngle: 360 }, //第四象限
},
}
return config;
}
}
/***
* @description 绘制方法
* @param {*} vectorLayer 台风圈所在图层
* @param {*} center 台风圈圆心的经纬度坐标(EPSG:4326)
* @param {*} radius 风圈半径 ("radius7": "220|260|180|260", 风圈格式:东北|东南|西北|西南,具体值为风圈半径,单位:公里。)
***/
static Draw(vectorLayer, center, radius) {
//获取台风圈的配置信息
let config = TyphoonCircle.getTyphoonCircleConfig(center, radius);
if (config != undefined) {
let sectorEN = new TyphoonSector(center, config.sector.EN.r, config.sector.EN.startAngle, config.sector.EN.endAngle).getFeature();
let sectorES = new TyphoonSector(center, config.sector.ES.r, config.sector.ES.startAngle, config.sector.ES.endAngle).getFeature();
let sectorWN = new TyphoonSector(center, config.sector.WN.r, config.sector.WN.startAngle, config.sector.WN.endAngle).getFeature();
let sectorWS = new TyphoonSector(center, config.sector.WS.r, config.sector.WS.startAngle, config.sector.WS.endAngle).getFeature();
//将四个象限的台风圈扇形添加到台风图层的数据源里
const vectorSource = vectorLayer.getSource();
vectorSource.addFeatures([sectorEN, sectorES, sectorWN, sectorWS]);
}
}
}
class TyphoonSector {
/***
* @description 构造函数
* @param {*} center 扇形圆心点的经纬度坐标(EPSG:4326)
* @param {*} radius 扇形半径(单位:公里。)
* @param {*} startAngle 扇形的起始角度
* @param {*} endAngle 扇形的截止角度
*
***/
constructor(center, radius, startAngle, endAngle) {
this.id = Math.random().toString(36).substr(2, 9); //生成一个随机字符串作为 id
this.feature = undefined;
this.center = olProj.fromLonLat(center); //将经纬度转换为平面投影坐标
this.radius = radius * 1000; //将半径从公里转成米
this.startAngle = startAngle;
this.endAngle = endAngle;
//画扇形
this.draw();
}
get angleRange() {
return Math.abs(this.endAngle - this.startAngle);
}
get segments() {
return Math.max(2, this.angleRange); // 每度角有一个点
}
get arcPoints() {
const points = [];
for (let i = 0; i <= this.segments; i++) {
const angle = ((this.endAngle - this.startAngle) * i) / this.segments;
const point = this.calculatePointOnCircle(this.startAngle + angle);
points.push(point);
}
return [this.center, ...points, this.center];
}
// 计算圆上的点
calculatePointOnCircle(angleInDegrees) {
let angle = (angleInDegrees * Math.PI) / 180; //转换为弧度
const x = this.center[0] + this.radius * Math.cos(angle);
const y = this.center[1] + this.radius * Math.sin(angle);
return [x, y];
}
getFeature() {
return this.feature;
}
angle2Radian(angle) {
return (angle * Math.PI) / 180;
}
//画扇形
draw() {
this.startAngle = this.startAngle;
this.endAngle = this.endAngle;
// 创建扇形的边界线
// 创建一个LineString对象来表示扇形的边界线
const line = new LineString(this.arcPoints);
// 使用LineString对象的坐标创建一个Polygon对象来表示扇形区域
const polygon = new Polygon([line.getCoordinates()]);
polygon.rotate(this.angle2Radian(0), this.center);
// 创建一个Feature对象,并将Polygon对象作为几何信息传入
this.feature = new Feature({ geometry: polygon });
// 为当前特征设置一个唯一的标识符
this.feature.setId(this.id);
}
}
</script>
<style scoped>
#vue-openlayers {
width: auto;
margin: auto;
height: 600px;
border: 2px solid #4845e490;
}
.header {
width: auto;
margin: auto;
text-align: center;
border: 2px solid #4845e490;
}
</style>
提示:在Typhoon.vue代码里用到的台风列表和台风详情数据文件已上传,可自行下载并放到相应目录,当前示例是放在了assets/typhoon/data/目录下,使用时通过import引入。
//本地台风列表数据Json文件
import tyhoonactivity from "../assets/typhoon/data/tyhoonactivity.json";
//2024年13号台风贝碧嘉的数据Json文件
import typhoon_202413 from "../assets/typhoon/data/typhoon_202413_BEBINCA.json";
//2024年14号台风普拉桑的数据Json文件
import typhoon_202414 from "../assets/typhoon/data/typhoon_202414_PULASAN.json";
提示:本示例中的台风轨迹和台风圈都一次性完全绘制出来的效果,如果想做实时的台风跟踪监控效果,可以考虑动态加载台风数据并绘制最新轨迹、风圈以及预报的效果。
总结
本示例基于 Vue3 和 Openlayers10 开发,相关插件也均采用能够兼容的版本,可根据自身需要适当取舍。