界面实现
说明
接口实现将用到如下技术:
- vue+vite+router
- element
- fetch
- echart
1. 工程初始化
工程的初始化参照WEB基础之Vue3部分完成。
1.1 地图初始化
地图初始化主要完成底图的加载、地图控件的加载以及地图的初始化配置,为方便应用,将其封装为了一个Vue的组件,实现代码如下:
vue
<template>
<div class="map" ref="map"></div>
</template>
<script>
let map;
export default {
name: "MapComponent",
mounted() {
this.pageLoaded()
},
props: {
center: {
type: Array,
default: () => [103.75254, 37.06996]
},
zoom: {
type: Number,
default: () => 3.3
},
},
watch: {
center() {
map.flyTo({center: this.center, zoom: this.zoom});
},
zoom() {
map.flyTo({center: this.center, zoom: this.zoom});
}
},
methods: {
pageLoaded() {
const dom = this.$refs.map
const style = {
name: 'my-style',
version: 8,
sources: {
'amap-vec': {
type: 'raster',
"scheme": "xyz",
"tileSize": 256,
tiles: [
'http://Webrd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8',
'http://Webrd02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8',
'http://Webrd03.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8',
'http://Webrd04.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8',
]
}
},
layers: [
{
id: 'amap-vec',
type: 'raster',
source: 'amap-vec'
}
]
}
map = new mapboxgl.Map({
container: dom, // container ID
style,
center: this.center, // starting position [lng, lat]
zoom: this.zoom, // starting zoom
doubleClickZoom: true,
dragPan: true,
hash: false,
attribute: false
});
map.on('load', () => {
this.$emit('map-loaded', map)
})
}
}
}
</script>
<style scoped lang="scss">
.map {
width: 100%;
height: 100%;
}
</style>
2. 功能实现
2.1 添加警戒线
台风境界线分为48小时警戒线和24小时警戒线,在48警戒线右侧的时候,记录台风的间隔是6小时,过了48小时警戒线,记录的间隔就减小到3小时,在实际生产和使用中有着非常重要的意义。
js
function addWarnLines() {
const lineData = [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[105, 0], [113, 4.5], [119, 11], [119, 18], [127, 22], [127, 34]]
},
"properties": {
"color": "blue",
"dashArray": [1, 0],
'label': '24小时警戒线'
}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [ [105, 0], [120, 0], [132, 15], [132, 34]
]
},
"properties": {
"color": "green",
"dashArray": [4, 2],
'label': '48小时警戒线'
}
}
]
this.map.addSource('source-warn-lines', {
"type": "geojson",
"data": new Geojson(lineData)
});
this.map.addLayer({
id: 'layer-warn-lines',
source: 'source-warn-lines',
type: 'line',
paint: {
'line-color': ['get', 'color'],
'line-width': 2,
'line-opacity': 0.8,
'line-dasharray': ['get', 'dashArray']
}
})
}
2.2 台风列表
以列表的方式展示产生的台风,在列表中展示台风编号、台风中文名称、英文名称,同时支持通过年来筛选台风列表数据。
vue
<template>
<div class="typhoon-list">
<div class="title">
台风列表
<div class="year-select">
<el-select v-model="selectYear" class="m-2" placeholder="Select" size="small">
<el-option
v-for="item in yearList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<el-table
class="list"
:data="typhoonList"
height="250"
style="width: 100%"
@select="selectChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="tfbh" label="台风编号" />
<el-table-column prop="name" label="中文名称" />
<el-table-column prop="ename" label="英文名称" />
</el-table>
</div>
</template>
<script>
let map;
export default {
name: "TyphoonList",
mounted() {
this.getYearList()
},
data() {
return {
typhoonList: [],
selectTyphoons: [],
selected: { tfbh: -1 },
yearList: [],
selectYear: 0
}
},
watch: {
selectYear() {
this.getTyphoonList()
}
},
methods: {
getYearList() {
const url = 'http://data.istrongcloud.com/v2/data/complex/years.json'
fetch(url).then(res => res.json()).then(res => {
this.yearList = res.map(r => {
return {value: r.year, label: r.year + '年'}
})
this.selectYear = this.yearList[0].value
})
},
getTyphoonList() {
const url = `https://data.istrongcloud.com/v2/data/complex/${this.selectYear}.json`
fetch(url).then(res => res.json()).then(res => {
this.typhoonList = res
})
},
selectChange(selection, row) {
this.selectTyphoons = selection
const isChecked = row.tfbh !== this.selected.tfbh
this.selected = isChecked ? row : { tfbh: -1 }
this.$emit('check-typhoon', row.tfbh, isChecked)
}
}
}
</script>
<style scoped lang="scss">
.typhoon-list {
position: absolute;
top: 20px;
right: 20px;
z-index: 99;
background-color: rgba(255, 255, 255, 1);
width: 380px;
font-size: 14px;
.title {
padding: 16px 12px;
font-weight: bold;
border-bottom: 1px solid #ccc;
.year-select {
float: right;
margin-top: -4px;
}
}
.list {
padding: 0;
}
}
</style>
2.3 台风预报
台风预报功能,是在台风移动到某个位置时,对台风相关信息的预测,具体功能及实现如下。
- 根据不同的预报强度展示预报结果
js
// 添加预报图层
map.addLayer({
id: 'typhoon-points-forc-' + this.tfbh,
source: 'source-points-' + this.tfbh,
type: 'circle',
filter: ['==', 'index', -1],
paint: {
'circle-radius': 4,
'circle-color': ['get', 'color'],
'circle-stroke-color': '#6a6a6a',
'circle-stroke-width': 1
}
});
forecast.forEach(forc => {
const sets = forc.sets
const pointsForc = forc.points
const coords = [[lng, lat]]
pointsForc.forEach(pointForc => {
pointForc.index = index
pointForc.color = this.getColor(pointForc.power)
pointForc.type = 'forc'
const geomForc = new Geometry('Point', coord)
const featureForc = new Feature(pointForc, geomForc)
pointFeatures.push(featureForc)
})
})
- 不同预报机构的预报数据展示;
js
map.addLayer({
id: 'typhoon-path-forc-' + this.tfbh,
source: 'source-lines-' + this.tfbh,
type: 'line',
paint: {
'line-width': 2,
'line-dasharray': [2, 2],
'line-color': [
'match',
['get', 'sets'],
'中国', '#f5000e',
'中国香港', '#6533b5',
'中国台湾', '#1f46b0',
'韩国', '#41c1f6',
'菲律宾', '#000',
'美国', '#3187d6',
'#f600ad'
]
}
});
forecast.forEach(forc => {
const sets = forc.sets
const pointsForc = forc.points
const coords = [[lng, lat]]
pointsForc.forEach(pointForc => {
const coord = [pointForc.lng, pointForc.lat]
coords.push(coord)
})
const _geom = new Geometry('LineString', coords)
const _feat = new Feature({
index: index,
type: 'forc',
sets: sets
}, _geom)
linesFeatures.push(_feat)
})
- 预报信息的展示
js
const forcLayer = 'typhoon-points-forc-' + this.tfbh
map.on('mouseover', forcLayer, e => {
map.getCanvasContainer().style.cursor = 'pointer'
const { properties } = e.features[0]
const dict = [
{"name":"移向", code: "move_dir", unit: ''},
{"name":"移速", code: "move_speed", unit: 'm/s'},
{"name":"压强", code: "pressure", unit: '百帕'},
{"name":"七级风圈", code: "radius7", unit: '千米'},
{"name":"十级风圈", code: "radius10", unit: '千米'},
{"name":"十二级风圈", code: "radius12", unit: '千米'},
{"name":"移速", code: "speed", unit: 'm/s'},
{"name":"经过时间", code: "time", unit: ''},
]
const pos = [properties.lng, properties.lat]
let content = `
<h4 class="field-header">${that.typhoonData.
<div class="field-item"><label class="field-
`
dict.forEach(d => {
const {code, name, unit} = d
let value = properties[code]
value = value ? value + unit : '/'
content += `<div class="field-item"><label class
})
this.popup = new mapboxgl.Popup({
offset: [0, -5],
anchor: 'bottom',
className: 'my-popup',
closeButton: false
}).setLngLat(pos).setHTML(content).addTo(map);
});
map.on('mouseout', forcLayer, e => {
map.getCanvasContainer().style.cursor = ''
if(this.popup) this.popup.remove()
});
2.4 台风实况
台风实况,是台风在移动的过程中,根据一定的规则记录(48小时境界线右侧每隔6小时,左侧每隔3小时)的台风的信息,包括:台风位置、台风强度、移动方向、移动速度、风速大小、大气压强、台风影响范围(台风风圈)、等信息。因此台风实况应包括:
- 根据不同的强度或风速大小,展示实况位置;
js
// 添加台风实况图层
map.addLayer({
id: 'typhoon-points-live-' + this.tfbh,
source: 'source-points-' + this.tfbh,
type: 'circle',
filter: ['==', 'index', -1],
paint: {
'circle-radius': 4,
'circle-color': ['get', 'color'],
'circle-stroke-color': '#6a6a6a',
'circle-stroke-width': 1
}
});
// 处理实况点数据
points.forEach((point, index) => {
point.index = index
point.type = 'live'
point.color = this.getColor(point.power)
const {lng, lat, forecast} = point
const geom = new Geometry('Point', [lng, lat])
const feature = new Feature(point, geom)
pointFeatures.push(feature)
});
- 根据台风风圈,绘制台风的影响范围;
js
// 添加风圈图层
map.addLayer({
id: 'typhoon-circle-' + this.tfbh,
source: 'source-circle-' + this.tfbh,
type: 'fill',
paint: {
'fill-color': ['get', 'color'],
'fill-opacity': 0.2
}
});
map.addLayer({
id: 'typhoon-circle-line-' + this.tfbh,
source: 'source-circle-' + this.tfbh,
type: 'line',
paint: {
'line-color': ['get', 'color'],
'line-width': 2
}
});
// 计算台风风圈
const {radius7_quad, radius10_quad, radius12_quad} = point
if(radius7_quad.ne && radius7_quad.ne > 0) {
const coords = this.getCircle([lng, lat], radius7_quad
const feature = new Feature({
color: '#00bab2',
index: index
}, new Geometry('Polygon', [coords]))
circleFeatures.push(feature)
}
if(radius10_quad.ne && radius10_quad.ne > 0) {
const coords = this.getCircle([lng, lat], radius10_qua
const feature = new Feature({
color: '#ffff00',
index: index
}, new Geometry('Polygon', [coords]))
circleFeatures.push(feature)
}
if(radius12_quad.ne && radius12_quad.ne > 0) {
const coords = this.getCircle([lng, lat], radius12_qua
const feature = new Feature({
color: '#da7341',
index: index
}, new Geometry('Polygon', [coords]))
circleFeatures.push(feature)
}
// 计算台风风圈
getCircle(center, radiusData) {
if(!radiusData.ne) return
center = proj4(proj4('EPSG:4326'), proj4('EPSG:3857'), center);
let latlngs = [];
let _angInterval = 6;
let _pointNums = 360 / (_angInterval * 4);
let quadrant = {
// 逆时针算角度
'0': 'ne',
'1': 'nw',
'2': 'sw',
'3': 'se'
};
for (let i = 0; i < 4; i++) {
let _r = parseFloat(radiusData[quadrant[i]]) * 1000; // 单位是km
if (!_r) _r = 0;
for (let j = i * _pointNums; j <= (i + 1) * _pointNums; j++) {
let _ang = _angInterval * j;
let x = center[0] + _r * Math.cos((_ang * Math.PI) / 180);
let y = center[1] + _r * Math.sin((_ang * Math.PI) / 180);
const coord = proj4(proj4('EPSG:3857'), proj4('EPSG:4326'), [x, y]);
latlngs.push(coord);
}
}
return latlngs
}
- 根据实况点连接而成的实况路径
js
//台风实况路径图层
map.addLayer({
id: 'typhoon-path-live-' + this.tfbh,
source: 'source-lines-' + this.tfbh,
type: 'line',
paint: {
'line-color': '#6a6a6a',
'line-width': 2
}
});
points.forEach((point, index) => {
// 实况线
if(index > 0) {
const coords = []
for (let i = 0; i <= index; i++) {
const _points = points[i]
const _lat = _points.lat
const _lng = _points.lng
coords.push([_lng, _lat])
}
const _geom = new Geometry('LineString', coords)
const _feature = new Feature({
index: index,
type: 'live'
}, _geom)
linesFeatures.push(_feature)
}
});
- 展示实况位置处台风的信息
js
const liveLayer = 'typhoon-points-live-' + this.tfbh
const that = this
map.on('mouseover', liveLayer, e => {
map.getCanvasContainer().style.cursor = 'pointer'
const { properties } = e.features[0]
const dict = [
{"name":"移向", code: "move_dir", unit: ''},
{"name":"移速", code: "move_speed", unit: 'm/s'},
{"name":"压强", code: "pressure", unit: '百帕'},
{"name":"七级风圈", code: "radius7", unit: '千米'},
{"name":"十级风圈", code: "radius10", unit: '千米'},
{"name":"十二级风圈", code: "radius12", unit: '千米'},
{"name":"移速", code: "speed", unit: 'm/s'},
{"name":"经过时间", code: "time", unit: ''},
]
const pos = [properties.lng, properties.lat]
let content = `
<h4 class="field-header">${that.typhoonData.name}${that.typhoonData.tfbh}</h4>
<div class="field-item"><label class="field-label">中心位置:</label>${pos.join(', ')}</div>
`
dict.forEach(d => {
const {code, name, unit} = d
let value = properties[code]
value = value ? value + unit : '/'
content += `<div class="field-item"><label class="field-label">${name}:</label>${value}</div>
})
this.popup = new mapboxgl.Popup({
offset: [0, -5],
anchor: 'bottom',
className: 'my-popup',
closeButton: false
}).setLngLat(pos).setHTML(content).addTo(map);
});
map.on('mouseout', liveLayer, e => {
map.getCanvasContainer().style.cursor = ''
if(this.popup) this.popup.remove()
});
2.5 风速、气压图
风速气压图,是将台风移动过程中的风速和气压分别以统计图的方式展示,方便了解台风变化的规律。
vue
<div class="chart-panel">
<div class="title">
气压图
</div>
<div class="chart" ref="chart1"></div>
<div class="title">
风速图
</div>
<div class="chart" ref="chart2"></div>
</div>
getTyphoonData() {
const url = `http://data.istrongcloud.com/v2/data/complex/${this.tfbh}.json`
fetch(url).then(res => res.json()).then(res => {
this.typhoonData = res[0].points
let times = [], ws = [], prs = []
this.typhoonData.forEach(d => {
times.push(new Date(d.time).format('yyyy-MM-dd hh:mm'))
ws.push(d.speed)
prs.push(d.pressure)
})
chart1.setOption(this.getChartOption({
name: '气压',
unit: 'mPa'
}, times, prs));
chart2.setOption(this.getChartOption({
name: '风速',
unit: 'm/s'
}, times, ws));
})
},
getChartOption(option, times, datas) {
return {
grid: {
left: "5%",
right: "5%",
top: "15",
bottom: "2%",
containLabel: true,
},
tooltip: {
show: true,
trigger: "axis",
transitionDuration: 0,
axisPointer: {
type: "line",
lineStyle: {
color: "rgba(50, 216, 205, 1)",
},
},
},
xAxis: [
{
type: "category",
boundaryGap: 0,
axisLine: {
show: false,
},
axisLabel: {
color: "#A1A7B3",
formatter:function(value) {
return new Date(value).format('hh:ss')
}
},
splitLine: {
show: false,
},
axisTick: {
show: false,
},
data: times
},
],
yAxis: [
{
type: "value",
name: `单位(${option.unit})`,
nameTextStyle: {
color: "#fff",
align: "right"
},
splitLine: {
show: false,
lineStyle: {
color: "#A1A7B3",
type: "dashed",
},
},
axisLine: {
show: false,
},
axisLabel: {
show: true,
textStyle: {
color: "#A1A7B3",
},
},
axisTick: {
show: false
},
},
],
series: [
{
name: option.name,
type: "line",
smooth: true,
symbolSize: 5,
showSymbol: false,
itemStyle: {
normal: {
color: "#23D0C4",
lineStyle: {
color: '#38cf8f',
width: 2
},
},
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1,
[
{ offset: 0, color: 'rgba(51, 192, 132, 1)' },
{ offset: 0.5, color: 'rgba(51, 192, 132, .5)' },
{ offset: 1, color: 'rgba(51, 192, 132, .1)' }
],
false
),
},
},
data: datas
},
]
}
}
<style scoped lang="scss">
.chart-panel {
width: 20rem;
overflow: hidden;
position: absolute;
top: 2rem;
left: 2rem;
background-color: white;
padding: 1rem;
font-size: 14px;
.title {
font-weight: bold;
padding: 0.4rem 0;
&:first-child {
padding-top: 0;
}
}
.chart {
width: 100%;
height: 7rem;
&.table {
height: auto;
}
}
}
</style>
2.6 城市测距
城市测距,是根据台风的当前位置,计算与城市之间的距离,并对距离比较近的城市做出预警。
vue
<div class="title">
城市测距
</div>
<div class="chart table">
<el-table
:data="cityDistance"
height="250"
style="width: 100%"
>
<el-table-column prop="city" label="城市" width="180" />
<el-table-column label="距离">
<template #default="scope">
{{ scope.row.dis + 'km' }}
</template>
</el-table-column>
</el-table>
</div>
getCityDistance() {
const url = '../../data/city-distance.json'
fetch(url).then(res => res.json()).then(res => {
this.cityDistance = res
})
}
最终实现后效果如下:
示例: ❐ 查看