1. 增加webrtc 功能示例;2. 增加自定义飞行区功能示例
Esse commit está contido em:
+1
-1
@@ -1 +1 @@
|
|||||||
registry=https://registry.npm.taobao.org/
|
registry=https://registry.npmmirror.com/
|
||||||
+1
-1
@@ -14,7 +14,7 @@ For more documentation, please visit the [DJI Developer Documentation](https://d
|
|||||||
|
|
||||||
## Latest Release
|
## Latest Release
|
||||||
|
|
||||||
Cloud API 1.8.0 was released on 11 Dec 2023. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
|
Cloud API 1.9.0 was released on 22 Feb 2024. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
gerado
+536
-536
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -0,0 +1,81 @@
|
|||||||
|
import request from '../http/request'
|
||||||
|
import { IWorkspaceResponse } from '../http/type'
|
||||||
|
import { EFlightAreaType, ESyncStatus, FlightAreaContent } from './../../types/flight-area'
|
||||||
|
import { ELocalStorageKey } from '/@/types/enums'
|
||||||
|
import { GeojsonCoordinate } from '/@/utils/genjson'
|
||||||
|
|
||||||
|
export interface GetFlightArea {
|
||||||
|
area_id: string,
|
||||||
|
name: string,
|
||||||
|
type: EFlightAreaType,
|
||||||
|
content: FlightAreaContent,
|
||||||
|
status: boolean,
|
||||||
|
username: string,
|
||||||
|
create_time: number,
|
||||||
|
update_time: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostFlightAreaBody {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
type: EFlightAreaType,
|
||||||
|
content: {
|
||||||
|
properties: {
|
||||||
|
color: string,
|
||||||
|
clampToGround: boolean,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: string,
|
||||||
|
coordinates: GeojsonCoordinate | GeojsonCoordinate[][],
|
||||||
|
radius?: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlightAreaStatus {
|
||||||
|
sync_code: number,
|
||||||
|
sync_status: ESyncStatus,
|
||||||
|
sync_msg: string,
|
||||||
|
|
||||||
|
}
|
||||||
|
export interface GetDeviceStatus {
|
||||||
|
device_sn: string,
|
||||||
|
nickname?: string,
|
||||||
|
device_name?: string,
|
||||||
|
online?: boolean,
|
||||||
|
flight_area_status: FlightAreaStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAP_API_PREFIX = '/map/api/v1'
|
||||||
|
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
|
||||||
|
export async function getFlightAreaList (): Promise<IWorkspaceResponse<GetFlightArea[]>> {
|
||||||
|
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-areas`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeFlightAreaStatus (area_id: string, status: boolean): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.put(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`, { status })
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveFlightArea (body: PostFlightAreaBody): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFlightArea (area_id: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.delete(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFlightArea (device_sn: string[]): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/sync`, { device_sn })
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDeviceStatus (): Promise<IWorkspaceResponse<GetDeviceStatus[]>> {
|
||||||
|
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/device-status`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
+35
-15
@@ -13,9 +13,10 @@
|
|||||||
<div :class="state.currentType === 'polyline' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polyline', true)">
|
<div :class="state.currentType === 'polyline' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polyline', true)">
|
||||||
<a><LineOutlined :rotate="135" class="fz20"/></a>
|
<a><LineOutlined :rotate="135" class="fz20"/></a>
|
||||||
</div>
|
</div>
|
||||||
<div :class="state.currentType === 'polygon' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polygon', true)">
|
<div :class="state.currentType === 'polygon' && !state.isFlightArea ? 'g-action-item selection' : 'g-action-item'" @click="draw('polygon', true)">
|
||||||
<a><BorderOutlined class="fz18" /></a>
|
<a><BorderOutlined class="fz18" /></a>
|
||||||
</div>
|
</div>
|
||||||
|
<FlightAreaActionIcon class="g-action-item mt10" :class="{'selection': mouseMode && state.isFlightArea}" @select-action="selectFlightAreaAction" @click="selectFlightAreaAction"/>
|
||||||
<div v-if="mouseMode" class="g-action-item" @click="draw('off', false)">
|
<div v-if="mouseMode" class="g-action-item" @click="draw('off', false)">
|
||||||
<a style="color: red;"><CloseOutlined /></a>
|
<a style="color: red;"><CloseOutlined /></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -464,6 +465,10 @@ import DroneControlPanel from './g-map/DroneControlPanel.vue'
|
|||||||
import { useConnectMqtt } from './g-map/use-connect-mqtt'
|
import { useConnectMqtt } from './g-map/use-connect-mqtt'
|
||||||
import LivestreamOthers from './livestream-others.vue'
|
import LivestreamOthers from './livestream-others.vue'
|
||||||
import LivestreamAgora from './livestream-agora.vue'
|
import LivestreamAgora from './livestream-agora.vue'
|
||||||
|
import FlightAreaActionIcon from './flight-area/FlightAreaActionIcon.vue'
|
||||||
|
import { EFlightAreaType } from '../types/flight-area'
|
||||||
|
import { useFlightArea } from './flight-area/use-flight-area'
|
||||||
|
import { useFlightAreaDroneLocationEvent } from './flight-area/use-flight-area-drone-location-event'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -489,7 +494,8 @@ export default defineComponent({
|
|||||||
CarryOutOutlined,
|
CarryOutOutlined,
|
||||||
RocketOutlined,
|
RocketOutlined,
|
||||||
LivestreamOthers,
|
LivestreamOthers,
|
||||||
LivestreamAgora
|
LivestreamAgora,
|
||||||
|
FlightAreaActionIcon,
|
||||||
},
|
},
|
||||||
name: 'GMap',
|
name: 'GMap',
|
||||||
props: {},
|
props: {},
|
||||||
@@ -503,7 +509,8 @@ export default defineComponent({
|
|||||||
const store = useMyStore()
|
const store = useMyStore()
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
currentType: '',
|
currentType: '',
|
||||||
coverIndex: 0
|
coverIndex: 0,
|
||||||
|
isFlightArea: false,
|
||||||
})
|
})
|
||||||
const str: string = '--'
|
const str: string = '--'
|
||||||
const deviceInfo = reactive({
|
const deviceInfo = reactive({
|
||||||
@@ -589,19 +596,22 @@ export default defineComponent({
|
|||||||
|
|
||||||
watch(() => store.state.deviceState, data => {
|
watch(() => store.state.deviceState, data => {
|
||||||
if (data.currentType === EDeviceTypeName.Gateway && data.gatewayInfo[data.currentSn]) {
|
if (data.currentType === EDeviceTypeName.Gateway && data.gatewayInfo[data.currentSn]) {
|
||||||
deviceTsaUpdateHook.moveTo(data.currentSn, data.gatewayInfo[data.currentSn].longitude, data.gatewayInfo[data.currentSn].latitude)
|
const coordinate = wgs84togcj02(data.gatewayInfo[data.currentSn].longitude, data.gatewayInfo[data.currentSn].latitude)
|
||||||
|
deviceTsaUpdateHook.moveTo(data.currentSn, coordinate[0], coordinate[1])
|
||||||
if (osdVisible.value.visible && osdVisible.value.gateway_sn !== '') {
|
if (osdVisible.value.visible && osdVisible.value.gateway_sn !== '') {
|
||||||
deviceInfo.gateway = data.gatewayInfo[osdVisible.value.gateway_sn]
|
deviceInfo.gateway = data.gatewayInfo[osdVisible.value.gateway_sn]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.currentType === EDeviceTypeName.Aircraft && data.deviceInfo[data.currentSn]) {
|
if (data.currentType === EDeviceTypeName.Aircraft && data.deviceInfo[data.currentSn]) {
|
||||||
deviceTsaUpdateHook.moveTo(data.currentSn, data.deviceInfo[data.currentSn].longitude, data.deviceInfo[data.currentSn].latitude)
|
const coordinate = wgs84togcj02(data.deviceInfo[data.currentSn].longitude, data.deviceInfo[data.currentSn].latitude)
|
||||||
|
deviceTsaUpdateHook.moveTo(data.currentSn, coordinate[0], coordinate[1])
|
||||||
if (osdVisible.value.visible && osdVisible.value.sn !== '') {
|
if (osdVisible.value.visible && osdVisible.value.sn !== '') {
|
||||||
deviceInfo.device = data.deviceInfo[osdVisible.value.sn]
|
deviceInfo.device = data.deviceInfo[osdVisible.value.sn]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.currentType === EDeviceTypeName.Dock && data.dockInfo[data.currentSn]) {
|
if (data.currentType === EDeviceTypeName.Dock && data.dockInfo[data.currentSn]) {
|
||||||
deviceTsaUpdateHook.initMarker(EDeviceTypeName.Dock, EDeviceTypeName[EDeviceTypeName.Dock], data.currentSn, data.dockInfo[data.currentSn].basic_osd?.longitude, data.dockInfo[data.currentSn].basic_osd?.latitude)
|
const coordinate = wgs84togcj02(data.dockInfo[data.currentSn].basic_osd?.longitude, data.dockInfo[data.currentSn].basic_osd?.latitude)
|
||||||
|
deviceTsaUpdateHook.initMarker(EDeviceTypeName.Dock, EDeviceTypeName[EDeviceTypeName.Dock], data.currentSn, coordinate[0], coordinate[1])
|
||||||
if (osdVisible.value.visible && osdVisible.value.is_dock && osdVisible.value.gateway_sn !== '') {
|
if (osdVisible.value.visible && osdVisible.value.is_dock && osdVisible.value.gateway_sn !== '') {
|
||||||
deviceInfo.dock = data.dockInfo[osdVisible.value.gateway_sn]
|
deviceInfo.dock = data.dockInfo[osdVisible.value.gateway_sn]
|
||||||
deviceInfo.device = data.deviceInfo[deviceInfo.dock.basic_osd.sub_device?.device_sn ?? osdVisible.value.sn]
|
deviceInfo.device = data.deviceInfo[deviceInfo.dock.basic_osd.sub_device?.device_sn ?? osdVisible.value.sn]
|
||||||
@@ -677,10 +687,11 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function draw (type: MapDoodleType, bool: boolean) {
|
function draw (type: MapDoodleType, bool: boolean, flightAreaType?: EFlightAreaType) {
|
||||||
state.currentType = type
|
state.currentType = type
|
||||||
useMouseToolHook.mouseTool(type, getDrawCallback)
|
|
||||||
mouseMode.value = bool
|
mouseMode.value = bool
|
||||||
|
state.isFlightArea = !!flightAreaType
|
||||||
|
useMouseToolHook.mouseTool(type, getDrawCallback, flightAreaType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dock 控制面板
|
// dock 控制面板
|
||||||
@@ -698,7 +709,18 @@ export default defineComponent({
|
|||||||
useGMapManageHook.globalPropertiesConfig(app)
|
useGMapManageHook.globalPropertiesConfig(app)
|
||||||
})
|
})
|
||||||
|
|
||||||
function getDrawCallback ({ obj }) {
|
const { getDrawFlightAreaCallback, onFlightAreaDroneLocationWs } = useFlightArea()
|
||||||
|
useFlightAreaDroneLocationEvent(onFlightAreaDroneLocationWs)
|
||||||
|
|
||||||
|
function selectFlightAreaAction ({ type, isCircle }: { type: EFlightAreaType, isCircle: boolean }) {
|
||||||
|
draw(isCircle ? MapDoodleEnum.CIRCLE : MapDoodleEnum.POLYGON, true, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrawCallback ({ obj }: { obj : any }) {
|
||||||
|
if (state.isFlightArea) {
|
||||||
|
getDrawFlightAreaCallback(obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
switch (state.currentType) {
|
switch (state.currentType) {
|
||||||
case MapDoodleEnum.PIN:
|
case MapDoodleEnum.PIN:
|
||||||
postPinPositionResource(obj)
|
postPinPositionResource(obj)
|
||||||
@@ -721,8 +743,7 @@ export default defineComponent({
|
|||||||
(req.resource.content.geometry.coordinates as GeojsonCoordinate).push((coordinates as GeojsonCoordinate)[2])
|
(req.resource.content.geometry.coordinates as GeojsonCoordinate).push((coordinates as GeojsonCoordinate)[2])
|
||||||
const result = await postElementsReq(shareId.value, req)
|
const result = await postElementsReq(shareId.value, req)
|
||||||
obj.setExtData({ id: req.id, name: req.name })
|
obj.setExtData({ id: req.id, name: req.name })
|
||||||
store.state.coverList.push(obj)
|
store.state.coverMap[req.id] = [obj]
|
||||||
// console.log(store.state.coverList)
|
|
||||||
}
|
}
|
||||||
async function postPolylineResource (obj) {
|
async function postPolylineResource (obj) {
|
||||||
const req = getPolylineResource(obj)
|
const req = getPolylineResource(obj)
|
||||||
@@ -730,8 +751,7 @@ export default defineComponent({
|
|||||||
updateCoordinates('gcj02-wgs84', req)
|
updateCoordinates('gcj02-wgs84', req)
|
||||||
const result = await postElementsReq(shareId.value, req)
|
const result = await postElementsReq(shareId.value, req)
|
||||||
obj.setExtData({ id: req.id, name: req.name })
|
obj.setExtData({ id: req.id, name: req.name })
|
||||||
store.state.coverList.push(obj)
|
store.state.coverMap[req.id] = [obj]
|
||||||
// console.log(store.state.coverList)
|
|
||||||
}
|
}
|
||||||
async function postPolygonResource (obj) {
|
async function postPolygonResource (obj) {
|
||||||
const req = getPoygonResource(obj)
|
const req = getPoygonResource(obj)
|
||||||
@@ -739,8 +759,7 @@ export default defineComponent({
|
|||||||
updateCoordinates('gcj02-wgs84', req)
|
updateCoordinates('gcj02-wgs84', req)
|
||||||
const result = await postElementsReq(shareId.value, req)
|
const result = await postElementsReq(shareId.value, req)
|
||||||
obj.setExtData({ id: req.id, name: req.name })
|
obj.setExtData({ id: req.id, name: req.name })
|
||||||
store.state.coverList.push(obj)
|
store.state.coverMap[req.id] = [obj]
|
||||||
// console.log(store.state.coverList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPinPositionResource (obj) {
|
function getPinPositionResource (obj) {
|
||||||
@@ -882,6 +901,7 @@ export default defineComponent({
|
|||||||
closeLivestreamOthers,
|
closeLivestreamOthers,
|
||||||
closeLivestreamAgora,
|
closeLivestreamAgora,
|
||||||
qualityStyle,
|
qualityStyle,
|
||||||
|
selectFlightAreaAction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export default defineComponent({
|
|||||||
{ key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
|
{ key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
|
||||||
{ key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' },
|
{ key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' },
|
||||||
{ key: 4, label: 'Flight Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
|
{ key: 4, label: 'Flight Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
|
||||||
{ key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' }
|
{ key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' },
|
||||||
|
{ key: 6, label: 'Flight Area', path: '/' + ERouterName.FLIGHT_AREA, icon: 'GroupOutlined' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function selectedRoute (item: IOptions) {
|
function selectedRoute (item: IOptions) {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div @click="selectCurrent">
|
||||||
|
<a-dropdown class="height-100 width-100 icon-panel">
|
||||||
|
<FlightAreaIcon :type="actionMap[selectedKey].type" :is-circle="actionMap[selectedKey].isCircle" :hide-title="true"/>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="selectAction" mode="vertical-right" :selectedKeys="[selectedKey]">
|
||||||
|
<a-menu-item v-for="(v, k) in actionMap" :key="k">
|
||||||
|
<FlightAreaIcon :type="v.type" :is-circle="v.isCircle"/>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, defineEmits } from 'vue'
|
||||||
|
import { EFlightAreaType } from '../../types/flight-area'
|
||||||
|
import FlightAreaIcon from './FlightAreaIcon.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-action', 'click'])
|
||||||
|
|
||||||
|
const actionMap: Record<string, { type: EFlightAreaType, isCircle: boolean}> = {
|
||||||
|
1: {
|
||||||
|
type: EFlightAreaType.DFENCE,
|
||||||
|
isCircle: true,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
type: EFlightAreaType.DFENCE,
|
||||||
|
isCircle: false,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
type: EFlightAreaType.NFZ,
|
||||||
|
isCircle: true,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
type: EFlightAreaType.NFZ,
|
||||||
|
isCircle: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedKey = ref<string>('1')
|
||||||
|
const selectAction = (item: any) => {
|
||||||
|
selectedKey.value = item.key
|
||||||
|
emit('select-action', actionMap[item.key])
|
||||||
|
}
|
||||||
|
const selectCurrent = () => {
|
||||||
|
emit('click', actionMap[selectedKey.value])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon-panel {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flight-area-device-panel">
|
||||||
|
<Title title="Choose Synchronous Devices">
|
||||||
|
<div style="position: absolute; right: 10px;">
|
||||||
|
<a style="color: white;" @click="closePanel"><CloseOutlined /></a>
|
||||||
|
</div>
|
||||||
|
</Title>
|
||||||
|
<div class="scrollbar">
|
||||||
|
<div id="data" v-if="data.length !== 0">
|
||||||
|
<div v-for="dock in data" :key="dock.device_sn">
|
||||||
|
<div class="pt5 panel flex-row" @click="selectDock(dock)" :style="{opacity: selectedDocksMap[dock.device_sn] ? 1 : 0.5 }">
|
||||||
|
<div style="width: 88%">
|
||||||
|
<div class="title">
|
||||||
|
<RobotFilled class="fz20"/>
|
||||||
|
<a-tooltip :title="dock.nickname">
|
||||||
|
<div class="pr10 ml5" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="ml10 mr10 pr5 pl5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
|
||||||
|
<div>
|
||||||
|
Custom Flight Area
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-if="!dock.status">
|
||||||
|
<a-tooltip title="Dock offline">
|
||||||
|
<ApiOutlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZED">
|
||||||
|
<a-tooltip title="Data synced">
|
||||||
|
<CheckCircleTwoTone twoToneColor="#28d445"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING
|
||||||
|
|| deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC">
|
||||||
|
<a-tooltip title="To be synced">
|
||||||
|
<SyncOutlined spin />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a-tooltip :title="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_msg || 'No synchronization'">
|
||||||
|
<ExclamationCircleTwoTone twoToneColor="#e70102" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box" v-if="selectedDocksMap[dock.device_sn]">
|
||||||
|
<CheckOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DividerLine style="position: absolute; bottom: 68px;" />
|
||||||
|
<div class="flex-row flex-justify-between footer">
|
||||||
|
<a-button class="mr10" @click="closePanel">Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :disabled="confirmDisabled" @click="syncDeviceFlightArea">Sync
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CloseOutlined, RobotFilled, CheckOutlined, ApiOutlined, CheckCircleTwoTone, SyncOutlined, ExclamationCircleTwoTone } from '@ant-design/icons-vue'
|
||||||
|
import Title from '/@/components/workspace/Title.vue'
|
||||||
|
import { defineEmits, onMounted, ref, defineProps, computed } from 'vue'
|
||||||
|
import { getBindingDevices } from '/@/api/manage'
|
||||||
|
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
|
||||||
|
import { IPage } from '/@/api/http/type'
|
||||||
|
import { Device } from '/@/types/device'
|
||||||
|
import DividerLine from '../workspace/DividerLine.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { GetDeviceStatus, syncFlightArea } from '/@/api/flight-area'
|
||||||
|
import { ESyncStatus } from '/@/types/flight-area'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: GetDeviceStatus[]
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits(['closePanel'])
|
||||||
|
const closePanel = () => {
|
||||||
|
emit('closePanel', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDisabled = ref(false)
|
||||||
|
|
||||||
|
const deviceStatusMap = computed(() => props.data.reduce((obj: Record<string, GetDeviceStatus>, val: GetDeviceStatus) => {
|
||||||
|
obj[val.device_sn] = val
|
||||||
|
return obj
|
||||||
|
}, {} as Record<string, GetDeviceStatus>))
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
const body: IPage = {
|
||||||
|
page: 1,
|
||||||
|
total: 0,
|
||||||
|
page_size: 10,
|
||||||
|
}
|
||||||
|
const data = ref<Device[]>([])
|
||||||
|
const selectedDocksMap = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
const getDocks = async () => {
|
||||||
|
await getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => {
|
||||||
|
if (res.code !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.value.push(...res.data.list)
|
||||||
|
body.page = res.data.pagination.page
|
||||||
|
body.page_size = res.data.pagination.page_size
|
||||||
|
body.total = res.data.pagination.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDock = (dock: Device) => {
|
||||||
|
if (!dock.status) {
|
||||||
|
message.info(`Dock(${dock.nickname}) is offline.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING ||
|
||||||
|
deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC) {
|
||||||
|
message.info('The dock is synchronizing.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedDocksMap.value[dock.device_sn] = !selectedDocksMap.value[dock.device_sn]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDocks()
|
||||||
|
const key = setInterval(() => {
|
||||||
|
if (body.total === 0 || Math.ceil(body.total / body.page_size) <= body.page) {
|
||||||
|
clearInterval(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body.page++
|
||||||
|
getDocks()
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
const syncDeviceFlightArea = () => {
|
||||||
|
const keys = Object.keys(selectedDocksMap.value)
|
||||||
|
if (keys.length === 0) {
|
||||||
|
message.warn('Please select the docks that need to be synchronized.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmDisabled.value = true
|
||||||
|
Object.keys(selectedDocksMap.value).forEach(k => {
|
||||||
|
const device = deviceStatusMap.value[k]
|
||||||
|
if (device) {
|
||||||
|
device.flight_area_status = { sync_code: 0, sync_status: ESyncStatus.WAIT_SYNC, sync_msg: '' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
syncFlightArea(keys).then(res => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
message.success('The devices are synchronizing...')
|
||||||
|
selectedDocksMap.value = {}
|
||||||
|
}
|
||||||
|
}).finally(() => setTimeout(() => {
|
||||||
|
confirmDisabled.value = false
|
||||||
|
}, 3000))
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flight-area-device-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: 285px;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh;
|
||||||
|
float: right;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
color: white;
|
||||||
|
background: #282828;
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
button {
|
||||||
|
width: 45%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollbar {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-row flex-align-center">
|
||||||
|
<div class="shape" :class="type" :style="isCircle ? 'border-radius: 50%;' : ''"></div>
|
||||||
|
<div class="ml5" v-if="!hideTitle">{{ FlightAreaTypeTitleMap[type][isCircle ? EGeometryType.CIRCLE : EGeometryType.POLYGON] }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import { EFlightAreaType, EGeometryType, FlightAreaTypeTitleMap } from '../../types/flight-area'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: EFlightAreaType,
|
||||||
|
isCircle: boolean,
|
||||||
|
hideTitle?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.nfz {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
.dfence {
|
||||||
|
border-color: $tag-green;
|
||||||
|
}
|
||||||
|
.shape {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-width: 3px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel" style="padding-top: 5px;" :class="{disable: !flightArea.status}">
|
||||||
|
<div class="title">
|
||||||
|
<a-tooltip :title="flightArea.name">
|
||||||
|
<div class="pr10" style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ flightArea.name }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
|
||||||
|
<span class="mr10">Update at {{ formatDateTime(flightArea.update_time).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row flex-justify-between flex-align-center ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
|
||||||
|
<FlightAreaIcon :type="flightArea.type" :isCircle="EGeometryType.CIRCLE === flightArea.content.geometry.type"/>
|
||||||
|
<div class="mr10 operate">
|
||||||
|
<a-popconfirm v-if="flightArea.status" title="Is it determined to disable the current area?" okText="Disable" @confirm="changeAreaStatus(false)">
|
||||||
|
<stop-outlined />
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-popconfirm v-else @confirm="changeAreaStatus(true)" title="Is it determined to enable the current area?" okText="Enable" >
|
||||||
|
<check-circle-outlined />
|
||||||
|
</a-popconfirm>
|
||||||
|
<EnvironmentFilled class="ml10" @click="clickLocation"/>
|
||||||
|
<a-popconfirm title="Is it determined to delete the current area?" okText="Delete" okType="danger" @confirm="deleteArea">
|
||||||
|
<delete-outlined class="ml10" />
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, reactive, defineEmits, computed } from 'vue'
|
||||||
|
import { GetFlightArea, changeFlightAreaStatus } from '../../api/flight-area'
|
||||||
|
import FlightAreaIcon from './FlightAreaIcon.vue'
|
||||||
|
import { formatDateTime } from '../../utils/time'
|
||||||
|
import { EGeometryType } from '../../types/flight-area'
|
||||||
|
import { StopOutlined, CheckCircleOutlined, DeleteOutlined, EnvironmentFilled } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: GetFlightArea
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits(['delete', 'update', 'location'])
|
||||||
|
|
||||||
|
const flightArea = computed(() => props.data)
|
||||||
|
const changeAreaStatus = (status: boolean) => {
|
||||||
|
changeFlightAreaStatus(props.data.area_id, status).then(res => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
flightArea.value.status = status
|
||||||
|
emit('update', flightArea)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const deleteArea = () => {
|
||||||
|
emit('delete', flightArea.value.area_id)
|
||||||
|
}
|
||||||
|
const clickLocation = () => {
|
||||||
|
emit('location', flightArea.value.area_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.panel {
|
||||||
|
background: #3c3c3c;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 90px;
|
||||||
|
width: 95%;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0px 10px 0 10px;
|
||||||
|
}
|
||||||
|
.operate > *{
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disable {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flight-area-panel">
|
||||||
|
<div v-if="data.length === 0">
|
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
|
||||||
|
</div>
|
||||||
|
<div v-else v-for="area in flightAreaList" :key="area.area_id">
|
||||||
|
<FlightAreaItem :data="area" @delete="deleteArea" @update="updateArea" @location="clickLocation(area)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, defineEmits, ref, computed } from 'vue'
|
||||||
|
import FlightAreaItem from './FlightAreaItem.vue'
|
||||||
|
import { GetFlightArea } from '/@/api/flight-area'
|
||||||
|
|
||||||
|
const emit = defineEmits(['deleteArea', 'updateArea', 'locationArea'])
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: GetFlightArea[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const flightAreaList = computed(() => props.data)
|
||||||
|
|
||||||
|
const deleteArea = (areaId: string) => {
|
||||||
|
emit('deleteArea', areaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateArea = (area: GetFlightArea) => {
|
||||||
|
emit('updateArea', area)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickLocation = (area: GetFlightArea) => {
|
||||||
|
emit('locationArea', area)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flight-area-panel {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flight-area-sync-panel p10 flex-row flex-align-center" >
|
||||||
|
<RobotFilled class="fz30" twoToneColor="red" fill="#00ff00"/>
|
||||||
|
<div class="ml20 mr10 flex-column" @click="switchPanel">
|
||||||
|
<div class="fz18">Sync Across Devices</div>
|
||||||
|
<div v-if="syncDevicesCount > 0"><a-spin /> Syncing to {{ syncDevicesCount }} devices</div>
|
||||||
|
</div>
|
||||||
|
<RightOutlined class="fz18" @click="switchPanel"/>
|
||||||
|
<FlightAreaDevicePanel v-if="visible" @close-panel="closePanel" :data="syncDevices"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { RobotFilled, RightOutlined } from '@ant-design/icons-vue'
|
||||||
|
import FlightAreaDevicePanel from '/@/components/flight-area/FlightAreaDevicePanel.vue'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { GetDeviceStatus, getDeviceStatus } from '/@/api/flight-area'
|
||||||
|
import { ESyncStatus, FlightAreaSyncProgress } from '/@/types/flight-area'
|
||||||
|
import { useFlightAreaSyncProgressEvent } from './use-flight-area-sync-progress-event'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const syncDevices = ref<GetDeviceStatus[]>([])
|
||||||
|
const syncDevicesCount = computed(() => syncDevices.value.filter(device =>
|
||||||
|
device.flight_area_status.sync_status === ESyncStatus.SYNCHRONIZING || device.flight_area_status.sync_status === ESyncStatus.WAIT_SYNC).length)
|
||||||
|
const getAllDeviceStatus = () => {
|
||||||
|
getDeviceStatus().then(res => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
syncDevices.value = res.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getAllDeviceStatus()
|
||||||
|
})
|
||||||
|
const switchPanel = () => {
|
||||||
|
visible.value = !visible.value
|
||||||
|
}
|
||||||
|
const closePanel = (val: boolean) => {
|
||||||
|
visible.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSyncProgress = (data: FlightAreaSyncProgress) => {
|
||||||
|
let has = false
|
||||||
|
const status = { sync_code: data.result, sync_status: data.status, sync_msg: data.message }
|
||||||
|
syncDevices.value.forEach(device => {
|
||||||
|
if (data.sn === device.device_sn) {
|
||||||
|
device.flight_area_status = status
|
||||||
|
has = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!has) {
|
||||||
|
syncDevices.value.push({ device_sn: data.sn, flight_area_status: status })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useFlightAreaSyncProgressEvent(handleSyncProgress)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flight-area-sync-panel {
|
||||||
|
height: 70px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FlightAreasDroneLocation } from '/@/types/flight-area'
|
||||||
|
import { CommonHostWs } from '/@/websocket'
|
||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
export function useFlightAreaDroneLocationEvent (onFlightAreaDroneLocationWs: (data: CommonHostWs<FlightAreasDroneLocation>) => void): void {
|
||||||
|
function handleDroneLocationEvent (data: any) {
|
||||||
|
onFlightAreaDroneLocationWs(data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightAreasDroneLocationWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightAreasDroneLocationWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { FlightAreaSyncProgress } from '/@/types/flight-area'
|
||||||
|
|
||||||
|
export function useFlightAreaSyncProgressEvent (onFlightAreaSyncProgressWs: (data: FlightAreaSyncProgress) => void): void {
|
||||||
|
function handleSyncProgressEvent (data: FlightAreaSyncProgress) {
|
||||||
|
onFlightAreaSyncProgressWs(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightAreasSyncProgressWs', handleSyncProgressEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightAreasSyncProgressWs', handleSyncProgressEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { EFlightAreaUpdate, FlightAreaUpdate, FlightAreasDroneLocation } from '/@/types/flight-area'
|
||||||
|
import { CommonHostWs } from '/@/websocket'
|
||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
function doNothing (data: FlightAreaUpdate) {
|
||||||
|
}
|
||||||
|
export function useFlightAreaUpdateEvent (addFunc = doNothing, deleteFunc = doNothing, updateFunc = doNothing): void {
|
||||||
|
function handleDroneLocationEvent (data: FlightAreaUpdate) {
|
||||||
|
switch (data.operation) {
|
||||||
|
case EFlightAreaUpdate.ADD:
|
||||||
|
addFunc(data)
|
||||||
|
break
|
||||||
|
case EFlightAreaUpdate.UPDATE:
|
||||||
|
updateFunc(data)
|
||||||
|
break
|
||||||
|
case EFlightAreaUpdate.DELETE:
|
||||||
|
deleteFunc(data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightAreasUpdateWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightAreasUpdateWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { message, notification } from 'ant-design-vue'
|
||||||
|
import { MapDoodleEnum } from '/@/types/map-enum'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
import { PostFlightAreaBody, saveFlightArea } from '/@/api/flight-area'
|
||||||
|
import { generateCircleContent, generatePolyContent } from '/@/utils/map-layer-utils'
|
||||||
|
import { GeojsonCoordinate } from '/@/utils/genjson'
|
||||||
|
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform.js'
|
||||||
|
import { uuidv4 } from '/@/utils/uuid'
|
||||||
|
import { CommonHostWs } from '/@/websocket'
|
||||||
|
import { FlightAreasDroneLocation } from '/@/types/flight-area'
|
||||||
|
import rootStore from '/@/store'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { useGMapCover } from '/@/hooks/use-g-map-cover'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { DATE_FORMAT } from '/@/utils/constants'
|
||||||
|
|
||||||
|
export function useFlightArea () {
|
||||||
|
const root = getRoot()
|
||||||
|
const store = rootStore
|
||||||
|
const coverMap = store.state.coverMap
|
||||||
|
|
||||||
|
let useGMapCoverHook = useGMapCover()
|
||||||
|
|
||||||
|
const MIN_RADIUS = 10
|
||||||
|
function checkCircle (obj: any): boolean {
|
||||||
|
if (obj.getRadius() < MIN_RADIUS) {
|
||||||
|
message.error(`The radius must be greater than ${MIN_RADIUS}m.`)
|
||||||
|
root.$map.remove(obj)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPolygon (obj: any): boolean {
|
||||||
|
const path: any[][] = obj.getPath()
|
||||||
|
if (path.length < 3) {
|
||||||
|
message.error('The path of the polygon cannot be crossed.')
|
||||||
|
root.$map.remove(obj)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// root.$aMap.GeometryUtil.doesLineLineIntersect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setExtData (obj: any) {
|
||||||
|
let ext = obj.getExtData()
|
||||||
|
const id = uuidv4()
|
||||||
|
const name = `${ext.type}-${moment().format(DATE_FORMAT)}`
|
||||||
|
ext = Object.assign({}, ext, { id, name })
|
||||||
|
obj.setExtData(ext)
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
function createFlightArea (obj: any) {
|
||||||
|
const ext = obj.getExtData()
|
||||||
|
const data = {
|
||||||
|
id: ext.id,
|
||||||
|
type: ext.type,
|
||||||
|
name: ext.name,
|
||||||
|
}
|
||||||
|
let coordinates: GeojsonCoordinate | GeojsonCoordinate[][]
|
||||||
|
let content
|
||||||
|
switch (ext.mapType) {
|
||||||
|
case 'circle':
|
||||||
|
content = generateCircleContent(obj.getCenter(), obj.getRadius())
|
||||||
|
coordinates = getWgs84(content.geometry.coordinates as GeojsonCoordinate)
|
||||||
|
break
|
||||||
|
case 'polygon':
|
||||||
|
content = generatePolyContent(obj.getPath()).content
|
||||||
|
coordinates = [getWgs84(content.geometry.coordinates[0] as GeojsonCoordinate[])]
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message.error(`Invalid type: ${obj.mapType}`)
|
||||||
|
root.$map.remove(obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.geometry.coordinates = coordinates
|
||||||
|
|
||||||
|
saveFlightArea(Object.assign({}, data, { content }) as PostFlightAreaBody).then(res => {
|
||||||
|
if (res.code !== 0) {
|
||||||
|
useGMapCoverHook.removeCoverFromMap(ext.id)
|
||||||
|
}
|
||||||
|
}).finally(() => root.$map.remove(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrawFlightAreaCallback (obj: any) {
|
||||||
|
useGMapCoverHook = useGMapCover()
|
||||||
|
const ext = setExtData(obj)
|
||||||
|
switch (ext.mapType) {
|
||||||
|
case MapDoodleEnum.CIRCLE:
|
||||||
|
if (!checkCircle(obj)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case MapDoodleEnum.POLYGON:
|
||||||
|
if (!checkPolygon(obj)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
createFlightArea(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWgs84 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => {
|
||||||
|
if (coordinate[0] instanceof Array) {
|
||||||
|
return (coordinate as GeojsonCoordinate[]).map(c => gcj02towgs84(c[0], c[1])) as T
|
||||||
|
}
|
||||||
|
return gcj02towgs84(coordinate[0], coordinate[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGcj02 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => {
|
||||||
|
if (coordinate[0] instanceof Array) {
|
||||||
|
return (coordinate as GeojsonCoordinate[]).map(c => wgs84togcj02(c[0], c[1])) as T
|
||||||
|
}
|
||||||
|
return wgs84togcj02(coordinate[0], coordinate[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFlightAreaDroneLocationWs = (data: CommonHostWs<FlightAreasDroneLocation>) => {
|
||||||
|
const nearArea = data.host.drone_locations.filter(val => !val.is_in_area)
|
||||||
|
const inArea = data.host.drone_locations.filter(val => val.is_in_area)
|
||||||
|
notification.warning({
|
||||||
|
key: `flight-area-${data.sn}`,
|
||||||
|
message: `Drone(${data.sn}) flight area information`,
|
||||||
|
description: h('div',
|
||||||
|
[
|
||||||
|
h('div', [
|
||||||
|
h('span', { class: 'fz18' }, 'In the flight area: '),
|
||||||
|
h('ul', [
|
||||||
|
...inArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
h('div', [
|
||||||
|
h('span', { class: 'fz18' }, 'Near the flight area: '),
|
||||||
|
h('ul', [
|
||||||
|
...nearArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
duration: null,
|
||||||
|
style: {
|
||||||
|
width: '420px',
|
||||||
|
marginTop: '-8px',
|
||||||
|
marginLeft: '-28px',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getDrawFlightAreaCallback,
|
||||||
|
getGcj02,
|
||||||
|
getWgs84,
|
||||||
|
onFlightAreaDroneLocationWs,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
id="video-webrtc"
|
id="video-webrtc"
|
||||||
ref="videowebrtc"
|
ref="videowebrtc"
|
||||||
controls
|
controls
|
||||||
|
autoplay
|
||||||
class="mt20"
|
class="mt20"
|
||||||
></video>
|
></video>
|
||||||
<p class="fz24">Live streaming source selection</p>
|
<p class="fz24">Live streaming source selection</p>
|
||||||
@@ -120,6 +121,8 @@ import { CURRENT_CONFIG as config } from '/@/api/http/config'
|
|||||||
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
|
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
|
||||||
import { getRoot } from '/@/root'
|
import { getRoot } from '/@/root'
|
||||||
import jswebrtc from '/@/vendors/jswebrtc.min.js'
|
import jswebrtc from '/@/vendors/jswebrtc.min.js'
|
||||||
|
import srs from '/@/vendors/srs.sdk.js'
|
||||||
|
|
||||||
const root = getRoot()
|
const root = getRoot()
|
||||||
|
|
||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
@@ -140,6 +143,10 @@ const liveTypeList: SelectOption[] = [
|
|||||||
{
|
{
|
||||||
value: 3,
|
value: 3,
|
||||||
label: 'GB28181'
|
label: 'GB28181'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: 'WEBRTC'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const clarityList: SelectOption[] = [
|
const clarityList: SelectOption[] = [
|
||||||
@@ -182,6 +189,7 @@ const lensList = ref<string[]>([])
|
|||||||
const lensSelected = ref<String>()
|
const lensSelected = ref<String>()
|
||||||
const isDockLive = ref(false)
|
const isDockLive = ref(false)
|
||||||
const nonSwitchable = 'normal'
|
const nonSwitchable = 'normal'
|
||||||
|
let webrtc: any = null
|
||||||
|
|
||||||
const onRefresh = async () => {
|
const onRefresh = async () => {
|
||||||
droneList.value = []
|
droneList.value = []
|
||||||
@@ -258,6 +266,9 @@ const onStart = async () => {
|
|||||||
liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}`
|
liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 4: {
|
||||||
|
break
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
console.warn('warning: live type is not correct!!!')
|
console.warn('warning: live type is not correct!!!')
|
||||||
break
|
break
|
||||||
@@ -291,13 +302,7 @@ const onStart = async () => {
|
|||||||
})
|
})
|
||||||
} else if (livetypeSelected.value === 2) {
|
} else if (livetypeSelected.value === 2) {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
rtspData.value =
|
rtspData.value = 'url:' + res.data.url
|
||||||
'url:' +
|
|
||||||
res.data.url +
|
|
||||||
'&username:' +
|
|
||||||
res.data.username +
|
|
||||||
'&password:' +
|
|
||||||
res.data.password
|
|
||||||
} else if (livetypeSelected.value === 1) {
|
} else if (livetypeSelected.value === 1) {
|
||||||
const url = res.data.url
|
const url = res.data.url
|
||||||
const videoElement = videowebrtc.value
|
const videoElement = videowebrtc.value
|
||||||
@@ -310,6 +315,10 @@ const onStart = async () => {
|
|||||||
console.log('start play livestream')
|
console.log('start play livestream')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else if (livetypeSelected.value === 4) {
|
||||||
|
const videoElement = videowebrtc.value as unknown as HTMLMediaElement
|
||||||
|
videoElement.muted = true
|
||||||
|
playWebrtc(videoElement, res.data.url)
|
||||||
}
|
}
|
||||||
liveState.value = true
|
liveState.value = true
|
||||||
})
|
})
|
||||||
@@ -412,6 +421,19 @@ const onSwitch = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const playWebrtc = (videoElement: HTMLMediaElement, url: string) => {
|
||||||
|
if (webrtc) {
|
||||||
|
webrtc.close()
|
||||||
|
}
|
||||||
|
webrtc = new srs.SrsRtcWhipWhepAsync()
|
||||||
|
videoElement.srcObject = webrtc.stream
|
||||||
|
webrtc.play(url).then(function (session: any) {
|
||||||
|
console.info(session)
|
||||||
|
}).catch(function (reason: any) {
|
||||||
|
webrtc.close()
|
||||||
|
console.error(reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<Divider class="divider" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Divider } from 'ant-design-vue'
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.divider {
|
||||||
|
margin: 10px 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #4f4f4f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div style="height: 40px; line-height: 50px; font-weight: 450;">
|
||||||
|
<a-row>
|
||||||
|
<a-col :span="1"></a-col>
|
||||||
|
<a-col :span="(23 - (extSpan || 0))">{{ title }}</a-col>
|
||||||
|
<a-col :span="extSpan"><slot /></a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
<DividerLine />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import DividerLine from '/@/components/workspace/DividerLine.vue'
|
||||||
|
|
||||||
|
const props = defineProps < {
|
||||||
|
extSpan?: number,
|
||||||
|
title: string,
|
||||||
|
} >()
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -20,4 +20,4 @@ export enum MapElementEnum {
|
|||||||
LINE = 1,
|
LINE = 1,
|
||||||
POLY = 2
|
POLY = 2
|
||||||
}
|
}
|
||||||
export type MapDoodleType = 'pin' | 'polyline' | 'polygon' | 'off'
|
export type MapDoodleType = 'pin' | 'polyline' | 'polygon' | 'off' | 'circle'
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ type Events = {
|
|||||||
flightTaskWs: any // 机场任务消息
|
flightTaskWs: any // 机场任务消息
|
||||||
droneControlWs: any // 飞行指令信息
|
droneControlWs: any // 飞行指令信息
|
||||||
droneControlMqttInfo: any // drc 链路通知
|
droneControlMqttInfo: any // drc 链路通知
|
||||||
|
flightAreasDroneLocationWs: any
|
||||||
|
flightAreasSyncProgressWs: any
|
||||||
|
flightAreasUpdateWs: any
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitter: Emitter<Events> = mitt<Events>()
|
const emitter: Emitter<Events> = mitt<Events>()
|
||||||
|
|||||||
+143
-39
@@ -1,3 +1,4 @@
|
|||||||
|
import { EFlightAreaType } from '../types/flight-area'
|
||||||
import pin19be6b from '/@/assets/icons/pin-19be6b.svg'
|
import pin19be6b from '/@/assets/icons/pin-19be6b.svg'
|
||||||
import pin212121 from '/@/assets/icons/pin-212121.svg'
|
import pin212121 from '/@/assets/icons/pin-212121.svg'
|
||||||
import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
|
import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
|
||||||
@@ -14,12 +15,16 @@ export function useGMapCover () {
|
|||||||
|
|
||||||
const normalColor = '#2D8CF0'
|
const normalColor = '#2D8CF0'
|
||||||
const store = rootStore
|
const store = rootStore
|
||||||
const coverList = store.state.coverList
|
const coverMap = store.state.coverMap
|
||||||
|
const flightAreaColorMap = {
|
||||||
|
[EFlightAreaType.DFENCE]: '#19be6b',
|
||||||
|
[EFlightAreaType.NFZ]: '#ff0000',
|
||||||
|
}
|
||||||
|
const disableColor = '#b3b3b3'
|
||||||
|
|
||||||
function AddCoverToMap (cover :any) {
|
function AddCoverToMap (cover :any) {
|
||||||
root.$map.add(cover)
|
root.$map.add(cover)
|
||||||
coverList.push(cover)
|
coverMap[cover.getExtData().id] = [cover]
|
||||||
// console.log('coverList:', store.state.coverList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPinIcon (color?:string) {
|
function getPinIcon (color?:string) {
|
||||||
@@ -29,10 +34,10 @@ export function useGMapCover () {
|
|||||||
} = {
|
} = {
|
||||||
'2d8cf0': pin2d8cf0,
|
'2d8cf0': pin2d8cf0,
|
||||||
'19be6b': pin19be6b,
|
'19be6b': pin19be6b,
|
||||||
'212121': pin212121,
|
212121: pin212121,
|
||||||
'b620e0': pinb620e0,
|
b620e0: pinb620e0,
|
||||||
'e23c39': pine23c39,
|
e23c39: pine23c39,
|
||||||
'ffbb00': pineffbb00,
|
ffbb00: pineffbb00,
|
||||||
}
|
}
|
||||||
const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
|
const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
|
||||||
return new AMap.Icon({
|
return new AMap.Icon({
|
||||||
@@ -44,7 +49,6 @@ export function useGMapCover () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) {
|
function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) {
|
||||||
console.log(name, coordinates[0], coordinates[1], color, data)
|
|
||||||
const pin = new AMap.Marker({
|
const pin = new AMap.Marker({
|
||||||
position: new AMap.LngLat(coordinates[0], coordinates[1]),
|
position: new AMap.LngLat(coordinates[0], coordinates[1]),
|
||||||
title: name,
|
title: name,
|
||||||
@@ -59,7 +63,8 @@ export function useGMapCover () {
|
|||||||
|
|
||||||
function AddOverlayGroup (overlayGroup) {
|
function AddOverlayGroup (overlayGroup) {
|
||||||
root.$map.add(overlayGroup)
|
root.$map.add(overlayGroup)
|
||||||
coverList.push(overlayGroup)
|
const id = overlayGroup.getExtData().id
|
||||||
|
coverMap[id] = [...(coverMap[id] || []), overlayGroup]
|
||||||
}
|
}
|
||||||
function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
|
function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
|
||||||
const path = [] as GeojsonCoordinate[]
|
const path = [] as GeojsonCoordinate[]
|
||||||
@@ -98,36 +103,18 @@ export function useGMapCover () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeCoverFromMap (id:string) {
|
function removeCoverFromMap (id:string) {
|
||||||
for (let i = 0; i < coverList.length; i++) {
|
coverMap[id].forEach(cover => root.$map.remove(cover))
|
||||||
const ele = coverList[i]
|
coverMap[id] = []
|
||||||
// console.log(ele)
|
|
||||||
const extdata = ele?.getExtData()
|
|
||||||
if (extdata?.id === id) {
|
|
||||||
console.log(extdata)
|
|
||||||
root.$map.remove(ele)
|
|
||||||
coverList.slice(i, 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getElementFromMap (id:string) {
|
function getElementFromMap (id:string): any[] {
|
||||||
// console.log('start', new Date().getTime())
|
return coverMap[id]
|
||||||
const ele = coverList.find(ele => ele?.getExtData().id === id)
|
|
||||||
// console.log('end', new Date().getTime())
|
|
||||||
return ele
|
|
||||||
// coverList.forEach((ele:any) => {
|
|
||||||
// const extdata = ele?.getExtData()
|
|
||||||
// // console.log(extdata)
|
|
||||||
// if (extdata?.id === id) {
|
|
||||||
// return ele
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) {
|
function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) {
|
||||||
const element = getElementFromMap(id) as any
|
const elements = getElementFromMap(id)
|
||||||
if (element) {
|
if (elements && elements.length > 0) {
|
||||||
|
const element = elements[0]
|
||||||
const icon = getPinIcon(color)
|
const icon = getPinIcon(color)
|
||||||
element.setPosition(new AMap.LngLat(coordinates[0], coordinates[1]))
|
element.setPosition(new AMap.LngLat(coordinates[0], coordinates[1]))
|
||||||
element.setIcon(icon)
|
element.setIcon(icon)
|
||||||
@@ -142,8 +129,9 @@ export function useGMapCover () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePolylineElement (id:string, name: string, coordinates:GeojsonCoordinate[], color?:string) {
|
function updatePolylineElement (id:string, name: string, coordinates:GeojsonCoordinate[], color?:string) {
|
||||||
const element = getElementFromMap(id) as any
|
const elements = getElementFromMap(id)
|
||||||
if (element) {
|
if (elements && elements.length > 0) {
|
||||||
|
const element = elements[0]
|
||||||
const options = element.getOptions()
|
const options = element.getOptions()
|
||||||
options.strokeColor = color || normalColor
|
options.strokeColor = color || normalColor
|
||||||
element.setOptions(options)
|
element.setOptions(options)
|
||||||
@@ -156,8 +144,9 @@ export function useGMapCover () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePolygonElement (id:string, name: string, coordinates:GeojsonCoordinate[][], color?:string) {
|
function updatePolygonElement (id:string, name: string, coordinates:GeojsonCoordinate[][], color?:string) {
|
||||||
const element = getElementFromMap(id) as any
|
const elements = getElementFromMap(id)
|
||||||
if (element) {
|
if (elements && elements.length > 0) {
|
||||||
|
const element = elements[0]
|
||||||
const options = element.getOptions()
|
const options = element.getOptions()
|
||||||
options.fillColor = color || normalColor
|
options.fillColor = color || normalColor
|
||||||
options.strokeColor = color || normalColor
|
options.strokeColor = color || normalColor
|
||||||
@@ -170,6 +159,116 @@ export function useGMapCover () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initTextInfo (content: string, coordinates: GeojsonCoordinate, id: string) {
|
||||||
|
const info = new AMap.Text({
|
||||||
|
text: content,
|
||||||
|
position: new AMap.LngLat(coordinates[0], coordinates[1]),
|
||||||
|
extData: { id: id, type: 'text' },
|
||||||
|
anchor: 'top-center',
|
||||||
|
style: {
|
||||||
|
background: 'none',
|
||||||
|
borderStyle: 'none',
|
||||||
|
fontSize: '16px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
AddOverlayGroup(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFlightAreaCircle (name: string, radius: number, position: GeojsonCoordinate, data: { id: string, type: EFlightAreaType, enable: boolean }) {
|
||||||
|
const circle = new AMap.Circle({
|
||||||
|
strokeColor: data.enable ? flightAreaColorMap[data.type] : disableColor,
|
||||||
|
strokeOpacity: 1,
|
||||||
|
strokeWeight: 6,
|
||||||
|
extData: data,
|
||||||
|
strokeStyle: 'dashed',
|
||||||
|
strokeDasharray: EFlightAreaType.NFZ === data.type ? [10, 2] : [10, 1, 2],
|
||||||
|
fillColor: flightAreaColorMap[data.type],
|
||||||
|
fillOpacity: EFlightAreaType.NFZ === data.type && data.enable ? 0.3 : 0,
|
||||||
|
radius: radius,
|
||||||
|
center: new AMap.LngLat(position[0], position[1]),
|
||||||
|
})
|
||||||
|
AddOverlayGroup(circle)
|
||||||
|
initTextInfo(name, position, data.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFlightAreaCircle (id: string, name: string, radius: number, position: GeojsonCoordinate, enable: boolean, type: EFlightAreaType) {
|
||||||
|
const elements = getElementFromMap(id)
|
||||||
|
if (elements && elements.length > 0) {
|
||||||
|
let textIndex = elements.findIndex(ele => ele.getExtData()?.type === 'text')
|
||||||
|
if (textIndex === -1) {
|
||||||
|
textIndex = 1
|
||||||
|
initTextInfo(name, position, id)
|
||||||
|
} else {
|
||||||
|
const text = elements[textIndex]
|
||||||
|
text.setText(name)
|
||||||
|
text.setPosition(position)
|
||||||
|
}
|
||||||
|
const element = elements[textIndex ^ 1]
|
||||||
|
const options = element.getOptions()
|
||||||
|
|
||||||
|
options.fillOpacity = EFlightAreaType.NFZ === type && enable ? 0.3 : 0
|
||||||
|
options.strokeColor = enable ? flightAreaColorMap[type] : disableColor
|
||||||
|
options.radius = radius
|
||||||
|
options.center = new AMap.LngLat(position[0], position[1])
|
||||||
|
element.setOptions(options)
|
||||||
|
} else {
|
||||||
|
initFlightAreaCircle(name, radius, position, { id, type, enable })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcPolygonPosition (coordinate: GeojsonCoordinate[]): GeojsonCoordinate {
|
||||||
|
const index = coordinate.length - 1
|
||||||
|
return [(coordinate[0][0] + coordinate[index][0]) / 2.0, (coordinate[0][1] + coordinate[index][1]) / 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFlightAreaPolygon (name: string, coordinates: GeojsonCoordinate[], data: { id: string, type: EFlightAreaType, enable: boolean }) {
|
||||||
|
const path = [] as GeojsonCoordinate[]
|
||||||
|
coordinates.forEach(coordinate => {
|
||||||
|
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
|
||||||
|
})
|
||||||
|
const polygon = new AMap.Polygon({
|
||||||
|
path: path,
|
||||||
|
strokeColor: data.enable ? flightAreaColorMap[data.type] : disableColor,
|
||||||
|
strokeOpacity: 1,
|
||||||
|
strokeWeight: 4,
|
||||||
|
draggable: true,
|
||||||
|
extData: data,
|
||||||
|
strokeStyle: 'dashed',
|
||||||
|
strokeDasharray: EFlightAreaType.NFZ === data.type ? [10, 2] : [10, 1, 2],
|
||||||
|
fillColor: flightAreaColorMap[data.type],
|
||||||
|
fillOpacity: EFlightAreaType.NFZ === data.type && data.enable ? 0.3 : 0,
|
||||||
|
})
|
||||||
|
AddOverlayGroup(polygon)
|
||||||
|
initTextInfo(name, calcPolygonPosition(coordinates), data.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFlightAreaPolygon (id: string, name: string, coordinates: GeojsonCoordinate[], enable: boolean, type: EFlightAreaType) {
|
||||||
|
const elements = getElementFromMap(id)
|
||||||
|
if (elements && elements.length > 0) {
|
||||||
|
let textIndex = elements.findIndex(ele => ele.getExtData()?.type === 'text')
|
||||||
|
if (textIndex === -1) {
|
||||||
|
textIndex = 1
|
||||||
|
initTextInfo(name, calcPolygonPosition(coordinates), id)
|
||||||
|
} else {
|
||||||
|
const text = elements[textIndex]
|
||||||
|
text.setText(name)
|
||||||
|
text.setPosition(calcPolygonPosition(coordinates))
|
||||||
|
}
|
||||||
|
const element = elements[textIndex ^ 1]
|
||||||
|
const options = element.getOptions()
|
||||||
|
const path = [] as GeojsonCoordinate[]
|
||||||
|
coordinates.forEach(coordinate => {
|
||||||
|
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
|
||||||
|
})
|
||||||
|
options.path = path
|
||||||
|
options.fillOpacity = EFlightAreaType.NFZ === type && enable ? 0.3 : 0
|
||||||
|
options.strokeColor = enable ? flightAreaColorMap[type] : disableColor
|
||||||
|
element.setOptions(options)
|
||||||
|
} else {
|
||||||
|
initFlightAreaPolygon(name, coordinates, { id, type, enable })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init2DPin,
|
init2DPin,
|
||||||
initPolyline,
|
initPolyline,
|
||||||
@@ -178,6 +277,11 @@ export function useGMapCover () {
|
|||||||
getElementFromMap,
|
getElementFromMap,
|
||||||
updatePinElement,
|
updatePinElement,
|
||||||
updatePolylineElement,
|
updatePolylineElement,
|
||||||
updatePolygonElement
|
updatePolygonElement,
|
||||||
|
initFlightAreaCircle,
|
||||||
|
initFlightAreaPolygon,
|
||||||
|
updateFlightAreaPolygon,
|
||||||
|
updateFlightAreaCircle,
|
||||||
|
calcPolygonPosition,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function useGMapManage () {
|
|||||||
state.aMap = AMap
|
state.aMap = AMap
|
||||||
state.map = new AMap.Map(container, {
|
state.map = new AMap.Map(container, {
|
||||||
center: [113.943225499, 22.577673716],
|
center: [113.943225499, 22.577673716],
|
||||||
zoom: 15
|
zoom: 20
|
||||||
})
|
})
|
||||||
state.mouseTool = new AMap.MouseTool(state.map)
|
state.mouseTool = new AMap.MouseTool(state.map)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { GeojsonCoordinate } from '../utils/genjson'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
|
||||||
|
export function useMapTool () {
|
||||||
|
const root = getRoot()
|
||||||
|
const map = root.$map
|
||||||
|
const AMap = root.$aMap
|
||||||
|
|
||||||
|
function panTo (coordinate: GeojsonCoordinate) {
|
||||||
|
map.panTo(coordinate, 100)
|
||||||
|
map.setZoom(18, false, 100)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
panTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
|
|||||||
import { MapDoodleType } from '/@/constants/map'
|
import { MapDoodleType } from '/@/constants/map'
|
||||||
import { getRoot } from '/@/root'
|
import { getRoot } from '/@/root'
|
||||||
import { MapDoodleEnum } from '/@/types/map-enum'
|
import { MapDoodleEnum } from '/@/types/map-enum'
|
||||||
|
import { EFlightAreaType } from '../types/flight-area'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
export function useMouseTool () {
|
export function useMouseTool () {
|
||||||
const root = getRoot()
|
const root = getRoot()
|
||||||
@@ -13,7 +15,10 @@ export function useMouseTool () {
|
|||||||
PolygonNum: 0,
|
PolygonNum: 0,
|
||||||
currentType: '',
|
currentType: '',
|
||||||
})
|
})
|
||||||
|
const flightAreaColorMap = {
|
||||||
|
[EFlightAreaType.DFENCE]: '#19be6b',
|
||||||
|
[EFlightAreaType.NFZ]: '#ff0000',
|
||||||
|
}
|
||||||
function drawPin (type:MapDoodleType, getDrawCallback:Function) {
|
function drawPin (type:MapDoodleType, getDrawCallback:Function) {
|
||||||
root?.$mouseTool.marker({
|
root?.$mouseTool.marker({
|
||||||
title: type + state.pinNum,
|
title: type + state.pinNum,
|
||||||
@@ -29,7 +34,6 @@ export function useMouseTool () {
|
|||||||
strokeOpacity: 1,
|
strokeOpacity: 1,
|
||||||
strokeWeight: 2,
|
strokeWeight: 2,
|
||||||
strokeStyle: 'solid',
|
strokeStyle: 'solid',
|
||||||
draggable: true,
|
|
||||||
title: type + state.polylineNum++
|
title: type + state.polylineNum++
|
||||||
})
|
})
|
||||||
root?.$mouseTool.on('draw', getDrawCallback)
|
root?.$mouseTool.on('draw', getDrawCallback)
|
||||||
@@ -42,7 +46,6 @@ export function useMouseTool () {
|
|||||||
strokeWeight: 2,
|
strokeWeight: 2,
|
||||||
fillColor: '#1791fc',
|
fillColor: '#1791fc',
|
||||||
fillOpacity: 0.4,
|
fillOpacity: 0.4,
|
||||||
draggable: true,
|
|
||||||
title: type + state.PolygonNum++
|
title: type + state.PolygonNum++
|
||||||
})
|
})
|
||||||
root?.$mouseTool.on('draw', getDrawCallback)
|
root?.$mouseTool.on('draw', getDrawCallback)
|
||||||
@@ -53,8 +56,55 @@ export function useMouseTool () {
|
|||||||
root?.$mouseTool.off('draw')
|
root?.$mouseTool.off('draw')
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseTool (type: MapDoodleType, getDrawCallback: Function) {
|
function drawFlightAreaPolygon (type: EFlightAreaType, getDrawFlightAreaCallback: Function) {
|
||||||
|
root?.$mouseTool.polygon({
|
||||||
|
strokeColor: flightAreaColorMap[type],
|
||||||
|
strokeOpacity: 1,
|
||||||
|
strokeWeight: 4,
|
||||||
|
extData: {
|
||||||
|
type: type,
|
||||||
|
mapType: 'polygon',
|
||||||
|
},
|
||||||
|
strokeStyle: 'dashed',
|
||||||
|
strokeDasharray: EFlightAreaType.NFZ === type ? [10, 2] : [10, 1, 2],
|
||||||
|
fillColor: flightAreaColorMap[type],
|
||||||
|
fillOpacity: EFlightAreaType.NFZ === type ? 0.3 : 0,
|
||||||
|
})
|
||||||
|
root?.$mouseTool.on('draw', getDrawFlightAreaCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFlightAreaCircle (type: EFlightAreaType, getDrawFlightAreaCallback: Function) {
|
||||||
|
root?.$mouseTool.circle({
|
||||||
|
strokeColor: flightAreaColorMap[type],
|
||||||
|
strokeOpacity: 1,
|
||||||
|
strokeWeight: 6,
|
||||||
|
extData: {
|
||||||
|
type: type,
|
||||||
|
mapType: 'circle',
|
||||||
|
},
|
||||||
|
strokeStyle: 'dashed',
|
||||||
|
strokeDasharray: EFlightAreaType.NFZ === type ? [10, 2] : [10, 1, 2],
|
||||||
|
fillColor: flightAreaColorMap[type],
|
||||||
|
fillOpacity: EFlightAreaType.NFZ === type ? 0.3 : 0,
|
||||||
|
})
|
||||||
|
root?.$mouseTool.on('draw', getDrawFlightAreaCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseTool (type: MapDoodleType, getDrawCallback: Function, flightAreaType?: EFlightAreaType) {
|
||||||
state.currentType = type
|
state.currentType = type
|
||||||
|
if (flightAreaType) {
|
||||||
|
switch (type) {
|
||||||
|
case MapDoodleEnum.POLYGON:
|
||||||
|
drawFlightAreaPolygon(flightAreaType, getDrawCallback)
|
||||||
|
return
|
||||||
|
case MapDoodleEnum.CIRCLE:
|
||||||
|
drawFlightAreaCircle(flightAreaType, getDrawCallback)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
message.error(`Invalid type: ${flightAreaType}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MapDoodleEnum.PIN:
|
case MapDoodleEnum.PIN:
|
||||||
drawPin(type, getDrawCallback)
|
drawPin(type, getDrawCallback)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div class="project-flight-area-wrapper height-100">
|
||||||
|
<a-spin :spinning="loading" :delay="300" tip="loading" size="large" class="height-100">
|
||||||
|
<Title title="Custom Flight Area" />
|
||||||
|
<FlightAreaPanel :data="flightAreaList" @location-area="clickArea" @delete-area="deleteAreaById"/>
|
||||||
|
<DividerLine />
|
||||||
|
<FlightAreaSyncPanel />
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import Title from '/@/components/workspace/Title.vue'
|
||||||
|
import DividerLine from '/@/components/workspace/DividerLine.vue'
|
||||||
|
import FlightAreaPanel from '/@/components/flight-area/FlightAreaPanel.vue'
|
||||||
|
import FlightAreaSyncPanel from '/@/components/flight-area/FlightAreaSyncPanel.vue'
|
||||||
|
import { GetFlightArea, deleteFlightArea, getFlightAreaList } from '/@/api/flight-area'
|
||||||
|
import { useGMapCover } from '/@/hooks/use-g-map-cover'
|
||||||
|
import { useMapTool } from '/@/hooks/use-map-tool'
|
||||||
|
import { EFlightAreaType, EGeometryType, FlightAreaUpdate } from '/@/types/flight-area'
|
||||||
|
import { useFlightArea } from '/@/components/flight-area/use-flight-area'
|
||||||
|
import { useFlightAreaUpdateEvent } from '/@/components/flight-area/use-flight-area-update'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const flightAreaList = ref<GetFlightArea[]>([])
|
||||||
|
let useGMapCoverHook = useGMapCover()
|
||||||
|
let useMapToolHook = useMapTool()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDataList()
|
||||||
|
})
|
||||||
|
const { getGcj02 } = useFlightArea()
|
||||||
|
|
||||||
|
const initMapFlightArea = () => {
|
||||||
|
useMapToolHook = useMapTool()
|
||||||
|
useGMapCoverHook = useGMapCover()
|
||||||
|
flightAreaList.value.forEach(area => {
|
||||||
|
updateMapFlightArea(area)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMapFlightArea = (area: GetFlightArea) => {
|
||||||
|
switch (area.content.geometry.type) {
|
||||||
|
case EGeometryType.CIRCLE:
|
||||||
|
useGMapCoverHook.updateFlightAreaCircle(area.area_id, area.name, area.content.geometry.radius, getGcj02(area.content.geometry.coordinates), area.status, area.type)
|
||||||
|
break
|
||||||
|
case 'Polygon':
|
||||||
|
useGMapCoverHook.updateFlightAreaPolygon(area.area_id, area.name, getGcj02(area.content.geometry.coordinates[0]), area.status, area.type)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDataList = () => {
|
||||||
|
loading.value = true
|
||||||
|
getFlightAreaList().then(res => {
|
||||||
|
flightAreaList.value = res.data
|
||||||
|
setTimeout(initMapFlightArea, 2000)
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAreaById = (areaId: string) => {
|
||||||
|
deleteFlightArea(areaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteArea = (area: FlightAreaUpdate) => {
|
||||||
|
flightAreaList.value = flightAreaList.value.filter(data => data.area_id !== area.area_id)
|
||||||
|
useGMapCoverHook.removeCoverFromMap(area.area_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateArea = (area: FlightAreaUpdate) => {
|
||||||
|
flightAreaList.value = flightAreaList.value.map(data => data.area_id === area.area_id ? area : data)
|
||||||
|
updateMapFlightArea(area as GetFlightArea)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addArea = (area: FlightAreaUpdate) => {
|
||||||
|
flightAreaList.value.push(area as GetFlightArea)
|
||||||
|
updateMapFlightArea(area as GetFlightArea)
|
||||||
|
}
|
||||||
|
|
||||||
|
useFlightAreaUpdateEvent(addArea, deleteArea, updateArea)
|
||||||
|
|
||||||
|
const clickArea = (area: GetFlightArea) => {
|
||||||
|
console.info(area)
|
||||||
|
let coordinate
|
||||||
|
switch (area.content.geometry.type) {
|
||||||
|
case EGeometryType.CIRCLE:
|
||||||
|
coordinate = getGcj02(area.content.geometry.coordinates)
|
||||||
|
break
|
||||||
|
case 'Polygon':
|
||||||
|
coordinate = useGMapCoverHook.calcPolygonPosition(getGcj02(area.content.geometry.coordinates[0]))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
useMapToolHook.panTo(coordinate)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -109,6 +109,18 @@ const messageHandler = async (payload: any) => {
|
|||||||
EventBus.emit('droneControlWs', payload)
|
EventBus.emit('droneControlWs', payload)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case EBizCode.FlightAreasSyncProgress: {
|
||||||
|
EventBus.emit('flightAreasSyncProgressWs', payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.FlightAreasDroneLocation: {
|
||||||
|
EventBus.emit('flightAreasDroneLocationWs', payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.FlightAreasUpdate: {
|
||||||
|
EventBus.emit('flightAreasUpdateWs', payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,12 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/' + ERouterName.FLIGHT_AREA,
|
||||||
|
name: ERouterName.FLIGHT_AREA,
|
||||||
|
component: () => import('/@/pages/page-web/projects/flight-area.vue')
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// pilot
|
// pilot
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getLayers } from '/@/api/layer'
|
|||||||
import { LayerType } from '/@/types/mapLayer'
|
import { LayerType } from '/@/types/mapLayer'
|
||||||
import { WaylineFile } from '/@/types/wayline'
|
import { WaylineFile } from '/@/types/wayline'
|
||||||
import { DevicesCmdExecuteInfo } from '/@/types/device-cmd'
|
import { DevicesCmdExecuteInfo } from '/@/types/device-cmd'
|
||||||
|
import { FlightAreaStatus } from '../api/flight-area'
|
||||||
|
|
||||||
const initStateFunc = () => ({
|
const initStateFunc = () => ({
|
||||||
Layers: [
|
Layers: [
|
||||||
@@ -34,9 +35,9 @@ const initStateFunc = () => ({
|
|||||||
drawVisible: false,
|
drawVisible: false,
|
||||||
livestreamOthersVisible: false,
|
livestreamOthersVisible: false,
|
||||||
livestreamAgoraVisible: false,
|
livestreamAgoraVisible: false,
|
||||||
coverList: [
|
coverMap: {} as {
|
||||||
|
[key: string]: any[]
|
||||||
] as any,
|
},
|
||||||
wsEvent: {
|
wsEvent: {
|
||||||
mapElementCreat: {},
|
mapElementCreat: {},
|
||||||
mapElementUpdate: {},
|
mapElementUpdate: {},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export enum DOMAIN {
|
|||||||
export enum DRONE_TYPE {
|
export enum DRONE_TYPE {
|
||||||
M30 = 67,
|
M30 = 67,
|
||||||
M300 = 60,
|
M300 = 60,
|
||||||
Mavic3EnterpriseAdvanced = 77,
|
Mavic3EnterpriseAdvanced= 77,
|
||||||
M350 = 89,
|
M350 = 89,
|
||||||
M3D = 91,
|
M3D = 91,
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ export interface OnlineDevice {
|
|||||||
// 固件升级类型
|
// 固件升级类型
|
||||||
export enum DeviceFirmwareTypeEnum {
|
export enum DeviceFirmwareTypeEnum {
|
||||||
ToUpgraded = 3, // 普通升级
|
ToUpgraded = 3, // 普通升级
|
||||||
ConsistencyUpgrade = 2, // 一致性升级
|
ConsistencyUpgrade =2, // 一致性升级
|
||||||
}
|
}
|
||||||
|
|
||||||
// 固件升级状态
|
// 固件升级状态
|
||||||
@@ -245,7 +245,7 @@ export interface OSDVisible {
|
|||||||
is_dock: boolean,
|
is_dock: boolean,
|
||||||
gateway_sn: string,
|
gateway_sn: string,
|
||||||
gateway_callsign: string,
|
gateway_callsign: string,
|
||||||
payloads: null | PayloadInfo[],
|
payloads: null | PayloadInfo [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GatewayOsd {
|
export interface GatewayOsd {
|
||||||
@@ -402,7 +402,7 @@ export interface DockLinkOsd {
|
|||||||
down_quality: string,
|
down_quality: string,
|
||||||
frequency_band: number,
|
frequency_band: number,
|
||||||
},
|
},
|
||||||
wireless_link?: { // 图传链路<会包括4G和sdr信息
|
wireless_link?:{ // 图传链路<会包括4G和sdr信息
|
||||||
dongle_number: number, // dongle 数量
|
dongle_number: number, // dongle 数量
|
||||||
['4g_link_state']: FourGLinkStateEnum, // 4g_link_state
|
['4g_link_state']: FourGLinkStateEnum, // 4g_link_state
|
||||||
sdr_link_state: SdrLinkStateEnum, // sdr链路连接状态
|
sdr_link_state: SdrLinkStateEnum, // sdr链路连接状态
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export enum ERouterName {
|
|||||||
CREATE_PLAN = 'create-plan',
|
CREATE_PLAN = 'create-plan',
|
||||||
SELECT_PLAN = 'select-plan',
|
SELECT_PLAN = 'select-plan',
|
||||||
FIRMWARES = 'firmwares',
|
FIRMWARES = 'firmwares',
|
||||||
|
FLIGHT_AREA = 'flight-area',
|
||||||
|
|
||||||
PILOT = 'pilot-login',
|
PILOT = 'pilot-login',
|
||||||
PILOT_HOME = 'pilot-home',
|
PILOT_HOME = 'pilot-home',
|
||||||
@@ -123,6 +124,11 @@ export enum EBizCode {
|
|||||||
TakeoffToPointProgress = 'takeoff_to_point_progress', // 一键起飞
|
TakeoffToPointProgress = 'takeoff_to_point_progress', // 一键起飞
|
||||||
JoystickInvalidNotify = 'joystick_invalid_notify', // 设备端退出drc模式
|
JoystickInvalidNotify = 'joystick_invalid_notify', // 设备端退出drc模式
|
||||||
DrcStatusNotify = 'drc_status_notify', // 飞行控制模式状态
|
DrcStatusNotify = 'drc_status_notify', // 飞行控制模式状态
|
||||||
|
|
||||||
|
// custom flight area
|
||||||
|
FlightAreasSyncProgress = 'flight_areas_sync_progress',
|
||||||
|
FlightAreasDroneLocation = 'flight_areas_drone_location',
|
||||||
|
FlightAreasUpdate = 'flight_areas_update',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EDeviceTypeName {
|
export enum EDeviceTypeName {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { GeojsonCoordinate, GeojsonPolygon } from '../utils/genjson'
|
||||||
|
|
||||||
|
export enum EFlightAreaType {
|
||||||
|
NFZ = 'nfz',
|
||||||
|
DFENCE = 'dfence',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EGeometryType {
|
||||||
|
CIRCLE = 'Circle',
|
||||||
|
POLYGON = 'Polygon',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EFlightAreaUpdate {
|
||||||
|
ADD = 'add',
|
||||||
|
UPDATE = 'update',
|
||||||
|
DELETE = 'delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ESyncStatus {
|
||||||
|
WAIT_SYNC = 'wait_sync',
|
||||||
|
SWITCH_FAIL = 'switch_fail',
|
||||||
|
SYNCHRONIZING = 'synchronizing',
|
||||||
|
SYNCHRONIZED = 'synchronized',
|
||||||
|
FAIL = 'fail',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeojsonCircle {
|
||||||
|
type: 'Feature'
|
||||||
|
properties: {
|
||||||
|
color: string
|
||||||
|
clampToGround?: boolean
|
||||||
|
}
|
||||||
|
geometry: {
|
||||||
|
type: EGeometryType.CIRCLE
|
||||||
|
coordinates: GeojsonCoordinate
|
||||||
|
radius: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DroneLocation {
|
||||||
|
area_distance: number,
|
||||||
|
area_id: string,
|
||||||
|
is_in_area: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlightAreasDroneLocation {
|
||||||
|
drone_locations: DroneLocation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FlightAreaContent = GeojsonCircle | GeojsonPolygon
|
||||||
|
|
||||||
|
export interface FlightAreaUpdate {
|
||||||
|
operation: EFlightAreaUpdate,
|
||||||
|
area_id: string,
|
||||||
|
name: string,
|
||||||
|
type: EFlightAreaType,
|
||||||
|
content: FlightAreaContent,
|
||||||
|
status: boolean,
|
||||||
|
username: string,
|
||||||
|
create_time: number,
|
||||||
|
update_time: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlightAreaSyncProgress {
|
||||||
|
sn: string,
|
||||||
|
result: number,
|
||||||
|
status: ESyncStatus,
|
||||||
|
message: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlightAreaTypeTitleMap = {
|
||||||
|
[EFlightAreaType.NFZ]: {
|
||||||
|
[EGeometryType.CIRCLE]: 'Circular GEO Zone',
|
||||||
|
[EGeometryType.POLYGON]: 'Polygonal GEO Zone',
|
||||||
|
},
|
||||||
|
[EFlightAreaType.DFENCE]: {
|
||||||
|
[EGeometryType.CIRCLE]: 'Circular Task Area',
|
||||||
|
[EGeometryType.POLYGON]: 'Polygonal Task Area',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -2,5 +2,6 @@ export enum MapDoodleEnum {
|
|||||||
PIN = 'pin',
|
PIN = 'pin',
|
||||||
POLYLINE = 'polyline',
|
POLYLINE = 'polyline',
|
||||||
POLYGON = 'polygon',
|
POLYGON = 'polygon',
|
||||||
Close = 'off'
|
Close = 'off',
|
||||||
|
CIRCLE = 'circle',
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-1
@@ -39,7 +39,20 @@ export interface GeojsonPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GeojsonFeature = GeojsonLine | GeojsonPolygon | GeojsonPoint
|
export interface GeojsonCircle {
|
||||||
|
type: 'Feature'
|
||||||
|
properties: {
|
||||||
|
color: string
|
||||||
|
clampToGround?: boolean
|
||||||
|
}
|
||||||
|
geometry: {
|
||||||
|
type: 'Circle'
|
||||||
|
coordinates: GeojsonCoordinate
|
||||||
|
radius: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GeojsonFeature = GeojsonLine | GeojsonPolygon | GeojsonPoint | GeojsonCircle
|
||||||
|
|
||||||
export function geographic2Coordinate (position: MapGeographicPosition): GeojsonCoordinate {
|
export function geographic2Coordinate (position: MapGeographicPosition): GeojsonCoordinate {
|
||||||
const coordinates: GeojsonCoordinate = [position.longitude, position.latitude]
|
const coordinates: GeojsonCoordinate = [position.longitude, position.latitude]
|
||||||
@@ -79,3 +92,15 @@ export function generatePoint (position: MapGeographicPosition, properties: Geoj
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateCircle (position: MapGeographicPosition, properties: GeojsonCircle['properties'], radius: number): GeojsonFeature {
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
properties,
|
||||||
|
geometry: {
|
||||||
|
type: 'Circle',
|
||||||
|
coordinates: geographic2Coordinate(position),
|
||||||
|
radius: radius,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { pinAMapPosition, MapGeographicPosition, Layer, LayerType, LayerElevationLoadStatus } from '/@/types/map'
|
import { pinAMapPosition, MapGeographicPosition, Layer, LayerType, LayerElevationLoadStatus } from '/@/types/map'
|
||||||
import { generatePoint, generateLine, generatePolygon } from '/@/utils/genjson'
|
import { generatePoint, generateLine, generatePolygon, generateCircle } from '/@/utils/genjson'
|
||||||
import { MapDoodleColor, MapElementEnum } from '/@/constants/map'
|
import { MapDoodleColor, MapElementEnum } from '/@/constants/map'
|
||||||
function getPinPosition (pinAMapPosition: pinAMapPosition):MapGeographicPosition {
|
function getPinPosition (pinAMapPosition: pinAMapPosition):MapGeographicPosition {
|
||||||
return { height: 0, latitude: pinAMapPosition.lat, longitude: pinAMapPosition.lng }
|
return { height: 0, latitude: pinAMapPosition.lat, longitude: pinAMapPosition.lng }
|
||||||
@@ -42,3 +42,8 @@ export function generatePolyContent (mapPosition: pinAMapPosition[]) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateCircleContent (pinAMapPosition: pinAMapPosition, radius: number) {
|
||||||
|
const position = getPinPosition(pinAMapPosition)
|
||||||
|
return generateCircle(position, { color: MapDoodleColor.PolygonColor }, radius)
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import moment, { Moment } from 'moment'
|
|||||||
|
|
||||||
// 时间字符串 或者 Unix 时间戳(毫秒数)
|
// 时间字符串 或者 Unix 时间戳(毫秒数)
|
||||||
export function formatDateTime (time: string | number, format = DATE_FORMAT) {
|
export function formatDateTime (time: string | number, format = DATE_FORMAT) {
|
||||||
return time ? moment(time, format) : DEFAULT_PLACEHOLDER
|
return time ? moment(time).format(format) : DEFAULT_PLACEHOLDER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unix 时间戳 (秒)
|
// Unix 时间戳 (秒)
|
||||||
|
|||||||
externo
+1
-1
Diff do arquivo suprimido porque uma ou mais linhas são muito longas
externo
+687
@@ -0,0 +1,687 @@
|
|||||||
|
|
||||||
|
//
|
||||||
|
// Copyright (c) 2013-2021 Winlin
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function SrsError(name, message) {
|
||||||
|
this.name = name;
|
||||||
|
this.message = message;
|
||||||
|
this.stack = (new Error()).stack;
|
||||||
|
}
|
||||||
|
SrsError.prototype = Object.create(Error.prototype);
|
||||||
|
SrsError.prototype.constructor = SrsError;
|
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-awat-prmise based SRS RTC Publisher.
|
||||||
|
function SrsRtcPublisherAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
|
self.constraints = {
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
width: { ideal: 320, max: 576 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// or autostart the publish:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(eip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.publish = async function (url) {
|
||||||
|
var conf = self.__internal.prepareUrl(url);
|
||||||
|
self.pc.addTransceiver("audio", { direction: "sendonly" });
|
||||||
|
self.pc.addTransceiver("video", { direction: "sendonly" });
|
||||||
|
//self.pc.addTransceiver("video", {direction: "sendonly"});
|
||||||
|
//self.pc.addTransceiver("audio", {direction: "sendonly"});
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
|
||||||
|
throw new SrsError('HttpsRequiredError', `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`);
|
||||||
|
}
|
||||||
|
var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||||
|
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
stream.getTracks().forEach(function (track) {
|
||||||
|
self.pc.addTrack(track);
|
||||||
|
|
||||||
|
// Notify about local track when stream is ok.
|
||||||
|
self.ontrack && self.ontrack({ track: track });
|
||||||
|
});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
var session = await new Promise(function (resolve, reject) {
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = {
|
||||||
|
api: conf.apiUrl, tid: conf.tid, streamurl: conf.streamUrl,
|
||||||
|
clientip: null, sdp: offer.sdp
|
||||||
|
};
|
||||||
|
console.log("Generated offer: ", data);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', conf.apiUrl, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json');
|
||||||
|
xhr.send(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
|
||||||
|
);
|
||||||
|
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/';
|
||||||
|
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function () {
|
||||||
|
self.pc && self.pc.close();
|
||||||
|
self.pc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got local stream.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
self.ontrack = function (event) {
|
||||||
|
// Add track to stream of SDK.
|
||||||
|
self.stream.addTrack(event.track);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
defaultPath: '/rtc/v1/publish/',
|
||||||
|
prepareUrl: function (webrtcUrl) {
|
||||||
|
var urlObject = self.__internal.parse(webrtcUrl);
|
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema;
|
||||||
|
schema = schema ? schema + ':' : window.location.protocol;
|
||||||
|
|
||||||
|
var port = urlObject.port || 1985;
|
||||||
|
if (schema === 'https:') {
|
||||||
|
port = urlObject.port || 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath;
|
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) {
|
||||||
|
api += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiUrl = schema + '//' + urlObject.server + ':' + port + api;
|
||||||
|
for (var key in urlObject.user_query) {
|
||||||
|
if (key !== 'api' && key !== 'play') {
|
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
apiUrl = apiUrl.replace(api + '&', api + '?');
|
||||||
|
|
||||||
|
var streamUrl = urlObject.url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port,
|
||||||
|
tid: Number(parseInt(new Date().getTime() * Math.random() * 100)).toString(16).slice(0, 7)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parse: function (url) {
|
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = url.replace("rtmp://", "http://")
|
||||||
|
.replace("webrtc://", "http://")
|
||||||
|
.replace("rtc://", "http://");
|
||||||
|
|
||||||
|
var vhost = a.hostname;
|
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost=");
|
||||||
|
if (app.indexOf("?") >= 0) {
|
||||||
|
var params = app.slice(app.indexOf("?"));
|
||||||
|
app = app.slice(0, app.indexOf("?"));
|
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) {
|
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
|
||||||
|
if (vhost.indexOf("&") > 0) {
|
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) {
|
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
if (re.test(a.hostname)) {
|
||||||
|
vhost = "__defaultVhost__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp";
|
||||||
|
if (url.indexOf("://") > 0) {
|
||||||
|
schema = url.slice(0, url.indexOf("://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = a.port;
|
||||||
|
if (!port) {
|
||||||
|
// Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
|
||||||
|
if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
|
||||||
|
port = (url.indexOf(`webrtc://${a.host}:80`) === 0) ? 80 : 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess by schema.
|
||||||
|
if (schema === 'http') {
|
||||||
|
port = 80;
|
||||||
|
} else if (schema === 'https') {
|
||||||
|
port = 443;
|
||||||
|
} else if (schema === 'rtmp') {
|
||||||
|
port = 1935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {
|
||||||
|
url: url,
|
||||||
|
schema: schema,
|
||||||
|
server: a.hostname, port: port,
|
||||||
|
vhost: vhost, app: app, stream: stream
|
||||||
|
};
|
||||||
|
self.__internal.fill_query(a.search, ret);
|
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) {
|
||||||
|
if (schema === 'webrtc' || schema === 'rtc') {
|
||||||
|
if (ret.user_query.schema === 'https') {
|
||||||
|
ret.port = 443;
|
||||||
|
} else if (window.location.href.indexOf('https://') === 0) {
|
||||||
|
ret.port = 443;
|
||||||
|
} else {
|
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
fill_query: function (query_string, obj) {
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
if (query_string.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) {
|
||||||
|
query_string = query_string.split("?")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = query_string.split("&");
|
||||||
|
for (var i = 0; i < queries.length; i++) {
|
||||||
|
var elem = queries[i];
|
||||||
|
|
||||||
|
var query = elem.split("=");
|
||||||
|
obj[query[0]] = query[1];
|
||||||
|
obj.user_query[query[0]] = query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) {
|
||||||
|
obj.vhost = obj.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
|
||||||
|
// To keep api consistent between player and publisher.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
// @see https://webrtc.org/getting-started/media-devices
|
||||||
|
self.stream = new MediaStream();
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-await-promise based SRS RTC Player.
|
||||||
|
function SrsRtcPlayerAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream
|
||||||
|
// or specifies the API port:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/livestream
|
||||||
|
// webrtc://r.ossrs.net:80/live/livestream
|
||||||
|
// or autostart the play:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?autostart=true
|
||||||
|
// or change the app from live to myapp:
|
||||||
|
// webrtc://r.ossrs.net:11985/myapp/livestream
|
||||||
|
// or change the stream from livestream to mystream:
|
||||||
|
// webrtc://r.ossrs.net:11985/live/mystream
|
||||||
|
// or set the api server to myapi.domain.com:
|
||||||
|
// webrtc://myapi.domain.com/live/livestream
|
||||||
|
// or set the candidate(eip) of answer:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?candidate=39.107.238.185
|
||||||
|
// or force to access https API:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?schema=https
|
||||||
|
// or use plaintext, without SRTP:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?encrypt=false
|
||||||
|
// or any other information, will pass-by in the query:
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?vhost=xxx
|
||||||
|
// webrtc://r.ossrs.net/live/livestream?token=xxx
|
||||||
|
self.play = async function (url) {
|
||||||
|
var conf = self.__internal.prepareUrl(url);
|
||||||
|
self.pc.addTransceiver("audio", { direction: "recvonly" });
|
||||||
|
self.pc.addTransceiver("video", { direction: "recvonly" });
|
||||||
|
//self.pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
//self.pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
var session = await new Promise(function (resolve, reject) {
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var data = {
|
||||||
|
api: conf.apiUrl, tid: conf.tid, streamurl: conf.streamUrl,
|
||||||
|
clientip: null, sdp: offer.sdp
|
||||||
|
};
|
||||||
|
console.log("Generated offer: ", data);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', conf.apiUrl, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json');
|
||||||
|
xhr.send(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
|
||||||
|
);
|
||||||
|
session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/';
|
||||||
|
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the player.
|
||||||
|
self.close = function () {
|
||||||
|
self.pc && self.pc.close();
|
||||||
|
self.pc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got remote track.
|
||||||
|
// Note that the onaddstream is deprecated, @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onaddstream
|
||||||
|
self.ontrack = function (event) {
|
||||||
|
// https://webrtc.org/getting-started/remote-streams
|
||||||
|
self.stream.addTrack(event.track);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
defaultPath: '/rtc/v1/play/',
|
||||||
|
prepareUrl: function (webrtcUrl) {
|
||||||
|
var urlObject = self.__internal.parse(webrtcUrl);
|
||||||
|
|
||||||
|
// If user specifies the schema, use it as API schema.
|
||||||
|
var schema = urlObject.user_query.schema;
|
||||||
|
schema = schema ? schema + ':' : window.location.protocol;
|
||||||
|
|
||||||
|
var port = urlObject.port || 1985;
|
||||||
|
if (schema === 'https:') {
|
||||||
|
port = urlObject.port || 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/rtcdn/rtcdn-draft
|
||||||
|
var api = urlObject.user_query.play || self.__internal.defaultPath;
|
||||||
|
if (api.lastIndexOf('/') !== api.length - 1) {
|
||||||
|
api += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiUrl = schema + '//' + urlObject.server + ':' + port + api;
|
||||||
|
for (var key in urlObject.user_query) {
|
||||||
|
if (key !== 'api' && key !== 'play') {
|
||||||
|
apiUrl += '&' + key + '=' + urlObject.user_query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
|
||||||
|
apiUrl = apiUrl.replace(api + '&', api + '?');
|
||||||
|
|
||||||
|
var streamUrl = urlObject.url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiUrl: apiUrl, streamUrl: streamUrl, schema: schema, urlObject: urlObject, port: port,
|
||||||
|
tid: Number(parseInt(new Date().getTime() * Math.random() * 100)).toString(16).slice(0, 7)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parse: function (url) {
|
||||||
|
// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = url.replace("rtmp://", "http://")
|
||||||
|
.replace("webrtc://", "http://")
|
||||||
|
.replace("rtc://", "http://");
|
||||||
|
|
||||||
|
var vhost = a.hostname;
|
||||||
|
var app = a.pathname.substring(1, a.pathname.lastIndexOf("/"));
|
||||||
|
var stream = a.pathname.slice(a.pathname.lastIndexOf("/") + 1);
|
||||||
|
|
||||||
|
// parse the vhost in the params of app, that srs supports.
|
||||||
|
app = app.replace("...vhost...", "?vhost=");
|
||||||
|
if (app.indexOf("?") >= 0) {
|
||||||
|
var params = app.slice(app.indexOf("?"));
|
||||||
|
app = app.slice(0, app.indexOf("?"));
|
||||||
|
|
||||||
|
if (params.indexOf("vhost=") > 0) {
|
||||||
|
vhost = params.slice(params.indexOf("vhost=") + "vhost=".length);
|
||||||
|
if (vhost.indexOf("&") > 0) {
|
||||||
|
vhost = vhost.slice(0, vhost.indexOf("&"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when vhost equals to server, and server is ip,
|
||||||
|
// the vhost is __defaultVhost__
|
||||||
|
if (a.hostname === vhost) {
|
||||||
|
var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
if (re.test(a.hostname)) {
|
||||||
|
vhost = "__defaultVhost__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the schema
|
||||||
|
var schema = "rtmp";
|
||||||
|
if (url.indexOf("://") > 0) {
|
||||||
|
schema = url.slice(0, url.indexOf("://"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = a.port;
|
||||||
|
if (!port) {
|
||||||
|
// Finger out by webrtc url, if contains http or https port, to overwrite default 1985.
|
||||||
|
if (schema === 'webrtc' && url.indexOf(`webrtc://${a.host}:`) === 0) {
|
||||||
|
port = (url.indexOf(`webrtc://${a.host}:80`) === 0) ? 80 : 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess by schema.
|
||||||
|
if (schema === 'http') {
|
||||||
|
port = 80;
|
||||||
|
} else if (schema === 'https') {
|
||||||
|
port = 443;
|
||||||
|
} else if (schema === 'rtmp') {
|
||||||
|
port = 1935;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {
|
||||||
|
url: url,
|
||||||
|
schema: schema,
|
||||||
|
server: a.hostname, port: port,
|
||||||
|
vhost: vhost, app: app, stream: stream
|
||||||
|
};
|
||||||
|
self.__internal.fill_query(a.search, ret);
|
||||||
|
|
||||||
|
// For webrtc API, we use 443 if page is https, or schema specified it.
|
||||||
|
if (!ret.port) {
|
||||||
|
if (schema === 'webrtc' || schema === 'rtc') {
|
||||||
|
if (ret.user_query.schema === 'https') {
|
||||||
|
ret.port = 443;
|
||||||
|
} else if (window.location.href.indexOf('https://') === 0) {
|
||||||
|
ret.port = 443;
|
||||||
|
} else {
|
||||||
|
// For WebRTC, SRS use 1985 as default API port.
|
||||||
|
ret.port = 1985;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
fill_query: function (query_string, obj) {
|
||||||
|
// pure user query object.
|
||||||
|
obj.user_query = {};
|
||||||
|
|
||||||
|
if (query_string.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split again for angularjs.
|
||||||
|
if (query_string.indexOf("?") >= 0) {
|
||||||
|
query_string = query_string.split("?")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var queries = query_string.split("&");
|
||||||
|
for (var i = 0; i < queries.length; i++) {
|
||||||
|
var elem = queries[i];
|
||||||
|
|
||||||
|
var query = elem.split("=");
|
||||||
|
obj[query[0]] = query[1];
|
||||||
|
obj.user_query[query[0]] = query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias domain for vhost.
|
||||||
|
if (obj.domain) {
|
||||||
|
obj.vhost = obj.domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
|
||||||
|
// Create a stream to add track to the stream, @see https://webrtc.org/getting-started/remote-streams
|
||||||
|
self.stream = new MediaStream();
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||||
|
self.pc.ontrack = function (event) {
|
||||||
|
if (self.ontrack) {
|
||||||
|
self.ontrack(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||||
|
// Async-awat-prmise based SRS RTC Publisher by WHIP.
|
||||||
|
function SrsRtcWhipWhepAsync() {
|
||||||
|
var self = {};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||||
|
self.constraints = {
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
width: { ideal: 320, max: 576 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
|
||||||
|
// @url The WebRTC url to publish with, for example:
|
||||||
|
// http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream
|
||||||
|
self.publish = async function (url) {
|
||||||
|
if (url.indexOf('/whip/') === -1) throw new Error(`invalid WHIP url ${url}`);
|
||||||
|
|
||||||
|
self.pc.addTransceiver("audio", { direction: "sendonly" });
|
||||||
|
self.pc.addTransceiver("video", { direction: "sendonly" });
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
|
||||||
|
throw new SrsError('HttpsRequiredError', `Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576`);
|
||||||
|
}
|
||||||
|
var stream = await navigator.mediaDevices.getUserMedia(self.constraints);
|
||||||
|
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
stream.getTracks().forEach(function (track) {
|
||||||
|
self.pc.addTrack(track);
|
||||||
|
|
||||||
|
// Notify about local track when stream is ok.
|
||||||
|
self.ontrack && self.ontrack({ track: track });
|
||||||
|
});
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
const answer = await new Promise(function (resolve, reject) {
|
||||||
|
console.log("Generated offer: ", offer);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = xhr.responseText;
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/sdp');
|
||||||
|
xhr.send(offer.sdp);
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: answer })
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.__internal.parseId(url, offer.sdp, answer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// See https://datatracker.ietf.org/doc/draft-ietf-wish-whip/
|
||||||
|
// @url The WebRTC url to play with, for example:
|
||||||
|
// http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream
|
||||||
|
self.play = async function (url) {
|
||||||
|
if (url.indexOf('/whip-play/') === -1 && url.indexOf('/whep/') === -1) throw new Error(`invalid WHEP url ${url}`);
|
||||||
|
|
||||||
|
self.pc.addTransceiver("video", { direction: "recvonly" });
|
||||||
|
|
||||||
|
var offer = await self.pc.createOffer();
|
||||||
|
await self.pc.setLocalDescription(offer);
|
||||||
|
const answer = await new Promise(function (resolve, reject) {
|
||||||
|
console.log("Generated offer: ", offer);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.readyState !== xhr.DONE) return;
|
||||||
|
if (xhr.status !== 200 && xhr.status !== 201) return reject(xhr);
|
||||||
|
const data = xhr.responseText;
|
||||||
|
console.log("Got answer: ", data);
|
||||||
|
return data.code ? reject(xhr) : resolve(data);
|
||||||
|
}
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/sdp');
|
||||||
|
xhr.send(offer.sdp);
|
||||||
|
});
|
||||||
|
await self.pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({ type: 'answer', sdp: answer })
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.__internal.parseId(url, offer.sdp, answer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the publisher.
|
||||||
|
self.close = function () {
|
||||||
|
self.pc && self.pc.close();
|
||||||
|
self.pc = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The callback when got local stream.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
self.ontrack = function (event) {
|
||||||
|
// Add track to stream of SDK.
|
||||||
|
self.stream.addTrack(event.track);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pc = new RTCPeerConnection(null);
|
||||||
|
|
||||||
|
// To keep api consistent between player and publisher.
|
||||||
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||||
|
// @see https://webrtc.org/getting-started/media-devices
|
||||||
|
self.stream = new MediaStream();
|
||||||
|
|
||||||
|
// Internal APIs.
|
||||||
|
self.__internal = {
|
||||||
|
parseId: (url, offer, answer) => {
|
||||||
|
let sessionid = offer.substr(offer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length);
|
||||||
|
sessionid = sessionid.substr(0, sessionid.indexOf('\n') - 1) + ':';
|
||||||
|
sessionid += answer.substr(answer.indexOf('a=ice-ufrag:') + 'a=ice-ufrag:'.length);
|
||||||
|
sessionid = sessionid.substr(0, sessionid.indexOf('\n'));
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
return {
|
||||||
|
sessionid: sessionid, // Should be ice-ufrag of answer:offer.
|
||||||
|
simulator: a.protocol + '//' + a.host + '/rtc/v1/nack/',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||||
|
self.pc.ontrack = function (event) {
|
||||||
|
if (self.ontrack) {
|
||||||
|
self.ontrack(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the codec of RTCRtpSender, kind(audio/video) is optional filter.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs#getting_the_supported_codecs
|
||||||
|
function SrsRtcFormatSenders(senders, kind) {
|
||||||
|
var codecs = [];
|
||||||
|
senders.forEach(function (sender) {
|
||||||
|
var params = sender.getParameters();
|
||||||
|
params && params.codecs && params.codecs.forEach(function (c) {
|
||||||
|
if (kind && sender.track.kind !== kind) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.mimeType.indexOf('/red') > 0 || c.mimeType.indexOf('/rtx') > 0 || c.mimeType.indexOf('/fec') > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = '';
|
||||||
|
|
||||||
|
s += c.mimeType.replace('audio/', '').replace('video/', '');
|
||||||
|
s += ', ' + c.clockRate + 'HZ';
|
||||||
|
if (sender.track.kind === "audio") {
|
||||||
|
s += ', channels: ' + c.channels;
|
||||||
|
}
|
||||||
|
s += ', pt: ' + c.payloadType;
|
||||||
|
|
||||||
|
codecs.push(s);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return codecs.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
SrsError,
|
||||||
|
SrsRtcPublisherAsync,
|
||||||
|
SrsRtcPlayerAsync,
|
||||||
|
SrsRtcWhipWhepAsync,
|
||||||
|
SrsRtcFormatSenders,
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||||
|
import { EBizCode } from '../types'
|
||||||
|
|
||||||
interface WebSocketOptions {
|
interface WebSocketOptions {
|
||||||
data: any
|
data: any
|
||||||
@@ -11,6 +12,11 @@ export interface MessageHandler {
|
|||||||
(data : {[key: string]: any}): void
|
(data : {[key: string]: any}): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommonHostWs<T> {
|
||||||
|
sn: string
|
||||||
|
host: T
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConnectWebSocket 类
|
* ConnectWebSocket 类
|
||||||
* TODO: 优化messageHandler: EventEmitter。暂时传入回调函数
|
* TODO: 优化messageHandler: EventEmitter。暂时传入回调函数
|
||||||
|
|||||||
+226
-226
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
Referência em uma Nova Issue
Bloquear um usuário