|
|
@@ -0,0 +1,520 @@ |
|
|
|
<template> |
|
|
|
<n-drawer v-bind="getDrawerOptions" @update:show="handleDrawerColse"> |
|
|
|
<n-drawer-content closable title="轨迹回放"> |
|
|
|
<div class="main_container"> |
|
|
|
<div id="history-map" /> |
|
|
|
<!-- 视频播放 --> |
|
|
|
<div class="videobox"> |
|
|
|
<!-- 视频开关 --> |
|
|
|
<div |
|
|
|
class="flagbox" |
|
|
|
:class="videoShow == 'back' ? 'flagbox_open' : 'flaxbox_back'" |
|
|
|
> |
|
|
|
<span class="videotitle">巡检视频</span> |
|
|
|
<img |
|
|
|
v-if="videoShow == 'back'" |
|
|
|
src="@/assets/img/back.png" |
|
|
|
alt="" |
|
|
|
class="closedetail imageicon" |
|
|
|
@click="showVideo('open')" |
|
|
|
> |
|
|
|
<img |
|
|
|
v-else |
|
|
|
src="@/assets/img/open.png" |
|
|
|
alt="" |
|
|
|
class="closedetail imageicon" |
|
|
|
@click="showVideo('back')" |
|
|
|
> |
|
|
|
</div> |
|
|
|
<!-- 直播视频 --> |
|
|
|
<div |
|
|
|
class="video_content" |
|
|
|
:class="videoShow == 'back' ? 'video_show' : 'video_hidden'" |
|
|
|
> |
|
|
|
<VideoPlayer id="demand-video" ref="originRef" @video-status="handleVideoStatus" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</n-drawer-content> |
|
|
|
</n-drawer> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import { defineComponent, computed, reactive, toRefs, ref, watch, nextTick } from 'vue' |
|
|
|
import { Map, View, Feature } from 'ol' |
|
|
|
import 'ol/ol.css' |
|
|
|
import { Tile, Vector as VectorLayer } from 'ol/layer' |
|
|
|
import { Style, Icon, Text, Fill } from 'ol/style' |
|
|
|
import LineString from 'ol/geom/LineString' |
|
|
|
import Point from 'ol/geom/Point' |
|
|
|
import { XYZ, Vector as VectorSource } from 'ol/source' |
|
|
|
import { transform, fromLonLat } from 'ol/proj' |
|
|
|
import { getVectorContext } from 'ol/render' |
|
|
|
import * as control from 'ol/control' |
|
|
|
import { styleList } from '@/utils/style.js' |
|
|
|
import { getTrackList, emergencyRecord } from '@/api/task/index.js' |
|
|
|
import { airportList, getWarningInfo } from '@/api/dashboard/index.js' |
|
|
|
import uav_icon from '@/assets/images/airport.png' |
|
|
|
import warningIcon from '@/assets/gis/images/fire.png' |
|
|
|
// 视频组件 |
|
|
|
import VideoPlayer from '@/components/VideoPlayer/index.vue' |
|
|
|
import gcj02Mecator from '@/utils/map/gcj02Mecator.js' |
|
|
|
import { gcj02towgs84 } from '@/utils/coordinate-util.js' |
|
|
|
export default defineComponent({ |
|
|
|
name: 'FlightDrawer', |
|
|
|
components: { VideoPlayer }, |
|
|
|
props: { |
|
|
|
/* 可见 */ |
|
|
|
visible: { |
|
|
|
type: Boolean, |
|
|
|
default: false |
|
|
|
}, |
|
|
|
/* 选中的数据 */ |
|
|
|
data: { |
|
|
|
type: Object, |
|
|
|
default: () => {} |
|
|
|
} |
|
|
|
}, |
|
|
|
emits: { |
|
|
|
'update:visible': null |
|
|
|
}, |
|
|
|
setup(props, { emit }) { |
|
|
|
const originRef = ref(null) |
|
|
|
|
|
|
|
const data = reactive({ |
|
|
|
show: props.visible, |
|
|
|
mapData: null, |
|
|
|
mapView: null, |
|
|
|
liveTrackLayer: null, |
|
|
|
lineTrackLayer: null, |
|
|
|
trackLayer: null, |
|
|
|
trackInfo: null, |
|
|
|
socket: null, |
|
|
|
// 轨迹数据 格式 |
|
|
|
trackList: [ |
|
|
|
{ lng: '118.765591', lat: '32.050993' }, |
|
|
|
{ lng: '118.665591', lat: '32.150993' }, |
|
|
|
{ lng: '118.565591', lat: '32.070993' }, |
|
|
|
{ lng: '118.465591', lat: '32.020993' }, |
|
|
|
{ lng: '118.365591', lat: '32.110993' }, |
|
|
|
{ lng: '118.265591', lat: '32.080993' }, |
|
|
|
{ lng: '118.165591', lat: '32.150993' }, |
|
|
|
{ lng: '118.765591', lat: '32.150993' }, |
|
|
|
{ lng: '118.66551', lat: '32.050993' }, |
|
|
|
{ lng: '118.365591', lat: '32.060993' }, |
|
|
|
{ lng: '118.265591', lat: '32.180993' }, |
|
|
|
{ lng: '118.165591', lat: '32.050993' } |
|
|
|
], |
|
|
|
videoShow: 'back', // 视频展示开关 |
|
|
|
videoInfo: { |
|
|
|
url: null, |
|
|
|
currentTime: 0, |
|
|
|
duration: 100, |
|
|
|
isLive: false, |
|
|
|
status: 'init' |
|
|
|
}, |
|
|
|
vectorLayers: [] |
|
|
|
}) |
|
|
|
|
|
|
|
/* 获取抽屉的信息 */ |
|
|
|
const getDrawerOptions = computed(() => { |
|
|
|
return { |
|
|
|
show: props.visible, |
|
|
|
width: '100%', |
|
|
|
placement: 'right' |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
function initOriginPlayer() { |
|
|
|
data.videoInfo.status = 'init' |
|
|
|
const origin = { |
|
|
|
width: '100%', |
|
|
|
height: '446px', |
|
|
|
// 播放的是原视频而非 AI分析后的视频 |
|
|
|
source: props.data.videoUrl |
|
|
|
} |
|
|
|
originRef.value?.init(origin) |
|
|
|
setTimeout(() => { |
|
|
|
if (data.videoInfo.status === 'init') { |
|
|
|
initOriginPlayer() |
|
|
|
} |
|
|
|
}, 30000) |
|
|
|
} |
|
|
|
|
|
|
|
/* 获取轨迹数据 */ |
|
|
|
const getTrackData = async function() { |
|
|
|
const res = await getTrackList(props.data.id) |
|
|
|
const trackList = res.data |
|
|
|
data.trackList = trackList |
|
|
|
data.lineTrajectoryList = trackList |
|
|
|
return Promise.resolve({ |
|
|
|
trackList |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// 绘制机场图层 |
|
|
|
const drawAirport = async(id) => { |
|
|
|
const res = await airportList({ page: 1, limit: 100 }) |
|
|
|
if (res.code === 0) { |
|
|
|
if (res.data.length > 0) { |
|
|
|
for (let i = 0; i < res.data.length; i++) { |
|
|
|
if (id === res.data[i].id) { |
|
|
|
const airport = res.data[i] |
|
|
|
const lngLat = gcj02towgs84( |
|
|
|
parseFloat(airport.longitude), |
|
|
|
parseFloat(airport.latitude) |
|
|
|
) |
|
|
|
const feature = new Feature({ |
|
|
|
geometry: new Point(fromLonLat(lngLat)) |
|
|
|
}) |
|
|
|
// 要素设置样式 |
|
|
|
feature.setStyle( |
|
|
|
new Style({ |
|
|
|
image: new Icon({ |
|
|
|
src: uav_icon |
|
|
|
}), |
|
|
|
text: new Text({ |
|
|
|
// 文字内容 |
|
|
|
text: airport.name, |
|
|
|
// 位置 |
|
|
|
textAlign: 'center', |
|
|
|
// 基准线 |
|
|
|
textBaseline: 'top', |
|
|
|
offsetY: 30, |
|
|
|
// 文字样式 |
|
|
|
font: 'normal 20px Microsoft YaHei', |
|
|
|
backgroundFill: new Fill({ |
|
|
|
color: '#1890FF' |
|
|
|
}), |
|
|
|
padding: [3, 6, 3, 6], |
|
|
|
// 文字颜色 |
|
|
|
fill: new Fill({ |
|
|
|
color: '#fff' |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
) |
|
|
|
// 要素设置id |
|
|
|
feature.setId(airport.id) |
|
|
|
// 添加矢量图层 |
|
|
|
const airLayer = new VectorLayer({ |
|
|
|
source: new VectorSource({ |
|
|
|
features: [feature] |
|
|
|
}), |
|
|
|
visible: true |
|
|
|
}) |
|
|
|
data.mapData.addLayer(airLayer) |
|
|
|
data.vectorLayers.push(airLayer) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const getEmergencyRecord = async(id) => { |
|
|
|
const res = await emergencyRecord({ |
|
|
|
emergencyMissionId: id |
|
|
|
}) |
|
|
|
if (res.code === 0) { |
|
|
|
drawWarning(res.data?.warningId) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 绘制预警图层 |
|
|
|
const drawWarning = async(id) => { |
|
|
|
const res = await getWarningInfo(id) |
|
|
|
if (res.code === 0) { |
|
|
|
data.warningLngLat = |
|
|
|
[parseFloat(res.data?.lng), |
|
|
|
parseFloat(res.data?.lat)] |
|
|
|
const feature = new Feature({ |
|
|
|
geometry: new Point(fromLonLat(data.warningLngLat)) |
|
|
|
}) |
|
|
|
// 要素设置样式 |
|
|
|
feature.setStyle( |
|
|
|
new Style({ |
|
|
|
// 图标 |
|
|
|
image: new Icon({ |
|
|
|
src: warningIcon, |
|
|
|
crossOrigin: 'anonymous' |
|
|
|
}) |
|
|
|
}) |
|
|
|
) |
|
|
|
// 要素设置属性 |
|
|
|
feature.setProperties({ |
|
|
|
// 类型设为 预警 |
|
|
|
type: 'warning', |
|
|
|
// 属性 |
|
|
|
props: res.data |
|
|
|
}) |
|
|
|
|
|
|
|
// 添加图层 |
|
|
|
const warningLayer = new VectorLayer({ |
|
|
|
source: new VectorSource({ |
|
|
|
features: [feature] |
|
|
|
}), |
|
|
|
visible: true |
|
|
|
}) |
|
|
|
// 添加图层 |
|
|
|
data.mapData.addLayer(warningLayer) |
|
|
|
data.vectorLayers.push(warningLayer) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
watch(() => props.visible, (visible) => { |
|
|
|
if (visible) { |
|
|
|
nextTick(() => { |
|
|
|
if (!data.mapData) { |
|
|
|
initMap() |
|
|
|
// 飞行完成 轨迹回放 |
|
|
|
data.drawerTitle = '轨迹回放' |
|
|
|
if (data.videoInfo.status === 'init') { |
|
|
|
initOriginPlayer() |
|
|
|
} |
|
|
|
getTrackData().then(res => { |
|
|
|
if (res.trackList.length > 0) { |
|
|
|
initTrack(formatTradeList(res.trackList), 'route') |
|
|
|
} |
|
|
|
}) |
|
|
|
// 绘制机场 |
|
|
|
if (props.data.airportId) { |
|
|
|
drawAirport(props.data.airportId) |
|
|
|
} |
|
|
|
// 绘制预警点 |
|
|
|
if (props.data.id) { |
|
|
|
getEmergencyRecord(props.data.id) |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
} else { |
|
|
|
originRef.value?.disposeVideo() |
|
|
|
data.videoInfo = { |
|
|
|
url: null, |
|
|
|
currentTime: 0, |
|
|
|
duration: 100, |
|
|
|
isLive: false, |
|
|
|
status: 'init' |
|
|
|
} |
|
|
|
data.videoShow = 'back' |
|
|
|
data.mapData = null |
|
|
|
} |
|
|
|
}, { deep: true }) |
|
|
|
|
|
|
|
/* 初始化地图 */ |
|
|
|
const initMap = function() { |
|
|
|
const layers = [ |
|
|
|
new Tile({ |
|
|
|
visible: true, |
|
|
|
source: new XYZ({ |
|
|
|
projection: gcj02Mecator, |
|
|
|
url: 'https://wprd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=6&x={x}&y={y}&z={z}' |
|
|
|
}) |
|
|
|
}) |
|
|
|
] |
|
|
|
|
|
|
|
data.mapView = new View({ |
|
|
|
maxZoom: 18, |
|
|
|
zoom: 5, |
|
|
|
center: transform([118.837886, 32.057175], 'EPSG:4326', 'EPSG:3857') |
|
|
|
}) |
|
|
|
data.mapData = new Map({ |
|
|
|
layers: layers, |
|
|
|
target: 'history-map', |
|
|
|
view: data.mapView, |
|
|
|
controls: control.defaults({ |
|
|
|
attribution: false, |
|
|
|
rotate: false, |
|
|
|
zoom: false |
|
|
|
}) |
|
|
|
}) |
|
|
|
data.mapData?.render() |
|
|
|
} |
|
|
|
|
|
|
|
/* 处理轨迹列表 */ |
|
|
|
const formatTradeList = function(trackList) { |
|
|
|
return trackList.map((item) => |
|
|
|
fromLonLat([parseFloat(item.lng), parseFloat(item.lat)]) |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/* 初始化轨迹 */ |
|
|
|
const initTrack = function(coordinate, routeType) { |
|
|
|
let vectorLayer = null |
|
|
|
const route = new LineString(coordinate) |
|
|
|
const routeFeature = new Feature({ |
|
|
|
type: routeType, |
|
|
|
geometry: route |
|
|
|
}) |
|
|
|
const startMarker = new Feature({ |
|
|
|
type: 'icon', |
|
|
|
geometry: new Point(route.getFirstCoordinate()) |
|
|
|
}) |
|
|
|
const position = startMarker.getGeometry().clone() |
|
|
|
const geoMarker = new Feature({ |
|
|
|
type: 'geoMarker', |
|
|
|
geometry: position |
|
|
|
}) |
|
|
|
geoMarker.setProperties({ type: 'geoMarker' }, false) |
|
|
|
if (data.trackLayer) { |
|
|
|
data.trackLayer.setSource( |
|
|
|
new VectorSource({ |
|
|
|
features: [geoMarker, routeFeature] |
|
|
|
}) |
|
|
|
) |
|
|
|
vectorLayer = data.trackLayer |
|
|
|
} else { |
|
|
|
vectorLayer = new VectorLayer({ |
|
|
|
source: new VectorSource({ |
|
|
|
features: [geoMarker, routeFeature] |
|
|
|
}), |
|
|
|
style: function(feature) { |
|
|
|
return styleList[feature.get('type')] |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
data.mapData.addLayer(vectorLayer) |
|
|
|
data.mapView.fit(route, { padding: [50, 50, 50, 50] }) |
|
|
|
vectorLayer.on('postrender', moveFeature.bind()) |
|
|
|
function moveFeature(event) { |
|
|
|
/* 计算飞行的百分比 */ |
|
|
|
const percent = data.videoInfo.currentTime / data.videoInfo.duration |
|
|
|
const currentCoordinate = route.getCoordinateAt(percent) |
|
|
|
position.setCoordinates(currentCoordinate) |
|
|
|
geoMarker.setGeometry(null) |
|
|
|
const vectorContext = getVectorContext(event) |
|
|
|
vectorContext.setStyle(styleList['geoMarker']) |
|
|
|
vectorContext.drawGeometry(position) |
|
|
|
data.mapData?.render() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function handleVideoStatus(status) { |
|
|
|
const { currentTime, duration } = originRef.value?.getTime() |
|
|
|
data.videoInfo = { |
|
|
|
...data.videoInfo, |
|
|
|
currentTime, |
|
|
|
duration, |
|
|
|
status: status |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function handleDrawerColse() { |
|
|
|
emit('update:visible', false) |
|
|
|
data.vectorLayers.forEach((layer) => { |
|
|
|
if (layer) { |
|
|
|
data.mapData.removeLayer(layer) |
|
|
|
} |
|
|
|
}) |
|
|
|
data.vectorLayers.length = 0 |
|
|
|
} |
|
|
|
|
|
|
|
/* 视频窗口展开收起 */ |
|
|
|
const showVideo = function(value) { |
|
|
|
data.videoShow = value |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
...toRefs(data), |
|
|
|
originRef, |
|
|
|
getDrawerOptions, |
|
|
|
handleDrawerColse, |
|
|
|
handleVideoStatus, |
|
|
|
showVideo |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped lang='scss'> |
|
|
|
.n-button+.n-button{ |
|
|
|
margin-left: 30px; |
|
|
|
} |
|
|
|
.main_container { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
position: relative; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
#history-map { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
} |
|
|
|
/* 视频 */ |
|
|
|
.videobox { |
|
|
|
width: 600px; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
justify-content: flex-start; |
|
|
|
align-items: flex-end; |
|
|
|
position: absolute; |
|
|
|
top: 10px; |
|
|
|
right: 10px; |
|
|
|
} |
|
|
|
.flagbox { |
|
|
|
width: 600px; |
|
|
|
height: 27px; |
|
|
|
background: #fff; |
|
|
|
padding: 0 10px; |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
} |
|
|
|
.flagbox_open { |
|
|
|
width: 600px; |
|
|
|
animation: openbox 1s; |
|
|
|
} |
|
|
|
.flaxbox_back { |
|
|
|
width: 116px; |
|
|
|
animation: backbox 1s; |
|
|
|
} |
|
|
|
@keyframes backbox { |
|
|
|
from { |
|
|
|
width: 600px; |
|
|
|
} |
|
|
|
to { |
|
|
|
width: 116px; |
|
|
|
} |
|
|
|
} |
|
|
|
@keyframes openbox { |
|
|
|
from { |
|
|
|
width: 116px; |
|
|
|
} |
|
|
|
to { |
|
|
|
width: 600px; |
|
|
|
} |
|
|
|
} |
|
|
|
.video_content { |
|
|
|
width: 600px; |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
} |
|
|
|
.video_show { |
|
|
|
opacity: 1; |
|
|
|
animation: showIt 1s; |
|
|
|
} |
|
|
|
@keyframes showIt { |
|
|
|
from { |
|
|
|
opacity: 0; |
|
|
|
} |
|
|
|
to { |
|
|
|
opacity: 1; |
|
|
|
} |
|
|
|
} |
|
|
|
.video_hidden { |
|
|
|
opacity: 0; |
|
|
|
animation: hiddenIt 1s; |
|
|
|
} |
|
|
|
@keyframes hiddenIt { |
|
|
|
from { |
|
|
|
opacity: 1; |
|
|
|
} |
|
|
|
to { |
|
|
|
opacity: 0; |
|
|
|
} |
|
|
|
} |
|
|
|
.imageicon { |
|
|
|
width: 16px; |
|
|
|
height: 16px; |
|
|
|
margin-right: 10px; |
|
|
|
} |
|
|
|
</style> |