Compare commits

...

8 Commits
v1.5.0 ... main

Author SHA1 Message Date
zhangxu 9d8e1c6eae xxx 1 week ago
jessie.huo@dji.com b26ef22ab6 feat(v1.10.0): v1.10.0 版本增加 10 months ago
jessie.huo@dji.com ae62a2c5ef 1. 增加webrtc 功能示例;2. 增加自定义飞行区功能示例 11 months ago
sean.zhou 42854e4db7 Merge branch 'v1.8.0' 1 year ago
sean.zhou 3ef74070df What's new? 1 year ago
sean.zhou 91f0f86730 Merge branch 'v1.7.0' 1 year ago
sean.zhou 07ff9ab4cc What's new? 1 year ago
sean.zhou 763e39bd71 Merge branch 'v1.5.0' 2 years ago
  1. 2
      .npmrc
  2. 3
      README.md
  3. 1072
      package-lock.json
  4. 5185
      pnpm-lock.yaml
  5. 18
      src/api/drone-control/drone.ts
  6. 81
      src/api/flight-area/index.ts
  7. 17
      src/api/http/config.ts
  8. 6
      src/api/wayline.ts
  9. 179
      src/components/GMap.vue
  10. 3
      src/components/common/sidebar.vue
  11. 4
      src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue
  12. 59
      src/components/flight-area/FlightAreaActionIcon.vue
  13. 197
      src/components/flight-area/FlightAreaDevicePanel.vue
  14. 33
      src/components/flight-area/FlightAreaIcon.vue
  15. 89
      src/components/flight-area/FlightAreaItem.vue
  16. 43
      src/components/flight-area/FlightAreaPanel.vue
  17. 66
      src/components/flight-area/FlightAreaSyncPanel.vue
  18. 18
      src/components/flight-area/use-flight-area-drone-location-event.ts
  19. 17
      src/components/flight-area/use-flight-area-sync-progress-event.ts
  20. 30
      src/components/flight-area/use-flight-area-update.ts
  21. 155
      src/components/flight-area/use-flight-area.ts
  22. 7
      src/components/g-map/DockControlPanel.vue
  23. 91
      src/components/g-map/DroneControlPanel.vue
  24. 6
      src/components/g-map/use-dock-control.ts
  25. 2
      src/components/livestream-agora.vue
  26. 38
      src/components/livestream-others.vue
  27. 163
      src/components/task/CreatePlan.vue
  28. 15
      src/components/workspace/DividerLine.vue
  29. 21
      src/components/workspace/Title.vue
  30. 6
      src/constants/map.ts
  31. 38
      src/directives/drag-window.ts
  32. 6
      src/directives/index.ts
  33. 3
      src/event-bus/index.ts
  34. 209
      src/hooks/use-g-map-cover.ts
  35. 2
      src/hooks/use-g-map.ts
  36. 16
      src/hooks/use-map-tool.ts
  37. 58
      src/hooks/use-mouse-tool.ts
  38. 4
      src/main.ts
  39. 10
      src/pages/page-pilot/pilot-home.vue
  40. 28
      src/pages/page-pilot/pilot-liveshare.vue
  41. 2
      src/pages/page-web/projects/dock.vue
  42. 98
      src/pages/page-web/projects/flight-area.vue
  43. 41
      src/pages/page-web/projects/layer.vue
  44. 14
      src/pages/page-web/projects/livestream.vue
  45. 11
      src/pages/page-web/projects/tsa.vue
  46. 6
      src/pages/page-web/projects/wayline.vue
  47. 12
      src/pages/page-web/projects/workspace.vue
  48. 15
      src/router/index.ts
  49. 17
      src/store/index.ts
  50. 9
      src/types/device-cmd.ts
  51. 5
      src/types/device-firmware.ts
  52. 51
      src/types/device.ts
  53. 17
      src/types/drone-control.ts
  54. 6
      src/types/enums.ts
  55. 80
      src/types/flight-area.ts
  56. 3
      src/types/map-enum.ts
  57. 3
      src/types/task.ts
  58. 27
      src/utils/genjson.ts
  59. 7
      src/utils/map-layer-utils.ts
  60. 2
      src/utils/time.ts
  61. 2
      src/vendors/jswebrtc.min.js
  62. 687
      src/vendors/srs.sdk.js
  63. 6
      src/websocket/index.ts
  64. 5
      tsconfig.json
  65. 452
      yarn.lock

2
.npmrc

@ -1 +1 @@
registry=https://registry.npm.taobao.org/ registry=https://registry.npmmirror.com/

3
README.md

@ -14,9 +14,8 @@ For more documentation, please visit the [DJI Developer Documentation](https://d
## Latest Release ## Latest Release
Cloud API 1.1.0 was released on 22 July 2022. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/). Cloud API 1.10.0 was released on 7 April 2024. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
## License ## License
Cloud API is MIT-licensed. Please refer to the LICENSE file for more information. Cloud API is MIT-licensed. Please refer to the LICENSE file for more information.

1072
package-lock.json generated

File diff suppressed because it is too large Load Diff

5185
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

18
src/api/drone-control/drone.ts

@ -18,6 +18,18 @@ export enum LostControlActionInCommandFLight {
Land = 1, // 着陆 Land = 1, // 着陆
RETURN_HOME = 2, // 返航 RETURN_HOME = 2, // 返航
} }
export enum ERthMode {
SMART = 0,
SETTING = 1
}
export enum ECommanderModeLostAction {
CONTINUE = 0,
EXEC_LOST_ACTION = 1
}
export enum ECommanderFlightMode {
SMART = 0,
SETTING = 1
}
export interface PointBody { export interface PointBody {
latitude: number; latitude: number;
longitude: number; longitude: number;
@ -48,7 +60,11 @@ export interface PostTakeoffToPointBody{
max_speed: number; // flyto过程中能达到的最大速度, 单位m/s 跟飞机档位有关 max_speed: number; // flyto过程中能达到的最大速度, 单位m/s 跟飞机档位有关
rc_lost_action: LostControlActionInCommandFLight; // 失控行为 rc_lost_action: LostControlActionInCommandFLight; // 失控行为
rth_altitude: number; // 返航高度 rth_altitude: number; // 返航高度
exit_wayline_when_rc_lost: WaylineLostControlActionInCommandFlight exit_wayline_when_rc_lost: WaylineLostControlActionInCommandFlight;
rth_mode: ERthMode;
commander_mode_lost_action: ECommanderModeLostAction;
commander_flight_mode: ECommanderFlightMode;
commander_flight_height: number;
} }
// 一键起飞 // 一键起飞

81
src/api/flight-area/index.ts

@ -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
}

17
src/api/http/config.ts

@ -1,17 +1,17 @@
export const CURRENT_CONFIG = { export const CURRENT_CONFIG = {
// license // license
appId: 'Please enter the app id.', // You need to go to the development website to apply. appId: '156484', // You need to go to the development website to apply.
appKey: 'Please enter the app key.', // You need to go to the development website to apply. appKey: 'fdfca95ec7e04aa41c743b837f5b6c5', // You need to go to the development website to apply.
appLicense: 'Please enter the app license.', // You need to go to the development website to apply. appLicense:
'rpyFPcmu47YOtazIxOYXZDJEmmboLVt3urGN2HbP7yocd2oNxvI2Ty3luE+KL0iM5rCPb0R+o8xR3IebOhSzDdK0DU21iujkuRCt70FdMTZdm8uRXC6NMJXfmej6BUFZtAhBhT+87jEnPGjhmfd51rMgOmovSsw/IndV6as3dbQ=', // You need to go to the development website to apply.
// http // http
baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/' baseURL: 'http://47.113.196.86:30001', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
websocketURL: 'Please enter the WebSocket access address.', // Example: 'ws://192.168.1.1:6789/api/v1/ws' websocketURL: 'http://47.113.196.86:30001/api/v1/ws', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
// livestreaming // livestreaming
// RTMP Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream. // RTMP Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/' rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
// GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters. // GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
gbServerIp: 'Please enter the server ip.', gbServerIp: 'Please enter the server ip.',
gbServerPort: 'Please enter the server port.', gbServerPort: 'Please enter the server port.',
@ -31,6 +31,5 @@ export const CURRENT_CONFIG = {
// map // map
// You can apply on the AMap website. // You can apply on the AMap website.
amapKey: 'Please enter the amap key.', amapKey: '8b0131c34d408f6663aae7a779e62cd1',
} }

6
src/api/wayline.ts

@ -42,10 +42,12 @@ export interface CreatePlan {
dock_sn: string, dock_sn: string,
task_type: TaskType, // 任务类型 task_type: TaskType, // 任务类型
wayline_type: WaylineType, // 航线类型 wayline_type: WaylineType, // 航线类型
task_days?: number[] // 执行任务的日期(秒) task_days: number[] // 执行任务的日期(秒)
task_periods?: number[][] // 执行任务的时间点(秒) task_periods: number[][] // 执行任务的时间点(秒)
rth_altitude: number // 相对机场返航高度 20 - 500 rth_altitude: number // 相对机场返航高度 20 - 500
out_of_control_action: OutOfControlAction // 失控动作 out_of_control_action: OutOfControlAction // 失控动作
min_battery_capacity?: number, // The minimum battery capacity of aircraft.
min_storage_capacity?: number, // The minimum storage capacity of dock and aircraft.
} }
// Create Wayline Job // Create Wayline Job

179
src/components/GMap.vue

@ -13,16 +13,17 @@
<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>
</div> </div>
<!-- 飞机OSD --> <!-- 飞机OSD -->
<div v-if="osdVisible.visible && !osdVisible.is_dock" class="osd-panel fz12"> <div v-if="osdVisible.visible && !osdVisible.is_dock" v-drag-window class="osd-panel fz12">
<div class="pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 18%;"> <div class="drag-title pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 18%;">
<span>{{ osdVisible.callsign }}</span> <span>{{ osdVisible.callsign }}</span>
<span><a class="fz16" style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span> <span><a class="fz16" style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
</div> </div>
@ -49,7 +50,7 @@
<a-col span="6"> <a-col span="6">
<a-tooltip title="RC Battery Level"> <a-tooltip title="RC Battery Level">
<span><ThunderboltOutlined class="fz14"/></span> <span><ThunderboltOutlined class="fz14"/></span>
<span class="ml10">{{ deviceInfo.gateway && deviceInfo.gateway.capacity_percent !== str ? deviceInfo.gateway.capacity_percent + ' %' : deviceInfo.gateway.capacity_percent }}</span> <span class="ml10">{{ deviceInfo.gateway && deviceInfo.gateway.capacity_percent !== str ? deviceInfo.gateway?.capacity_percent + ' %' : deviceInfo.gateway?.capacity_percent }}</span>
</a-tooltip> </a-tooltip>
</a-col> </a-col>
@ -141,11 +142,11 @@
</div> </div>
</div> </div>
<!-- 机场OSD --> <!-- 机场OSD -->
<div v-if="osdVisible.visible && osdVisible.is_dock" class="osd-panel fz12"> <div v-if="osdVisible.visible && osdVisible.is_dock" v-drag-window class="osd-panel fz12">
<div class="fz16 pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 10%;"> <div class="drag-title fz16 pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 10%;">
<span>{{ osdVisible.gateway_callsign }}</span> <span>{{ osdVisible.gateway_callsign }}</span>
<span><a style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
</div> </div>
<span><a style="color: white; position: absolute; top: 5px; right: 5px;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
<!-- 机场 --> <!-- 机场 -->
<div class ="flex-display" style="border-bottom: 1px solid #515151;"> <div class ="flex-display" style="border-bottom: 1px solid #515151;">
<div class="flex-column flex-align-stretch flex-justify-center" style="width: 60px; background: #2d2d2d;"> <div class="flex-column flex-align-stretch flex-justify-center" style="width: 60px; background: #2d2d2d;">
@ -185,8 +186,7 @@
<a-row> <a-row>
<a-col span="6"> <a-col span="6">
<a-tooltip title="Network State"> <a-tooltip title="Network State">
<span :style="deviceInfo.dock.basic_osd?.network_state?.type === NetworkStateTypeEnum.ETHERNET || deviceInfo.dock.basic_osd?.network_state?.quality === NetworkStateQualityEnum.GOOD ? <span :style="qualityStyle">
'color: #00ee8b' : deviceInfo.dock.basic_osd?.network_state?.quality === NetworkStateQualityEnum.MEDIUM ? 'color: yellow' : 'color: red'">
<span v-if="deviceInfo.dock.basic_osd?.network_state?.type === NetworkStateTypeEnum.FOUR_G"><SignalFilled /></span> <span v-if="deviceInfo.dock.basic_osd?.network_state?.type === NetworkStateTypeEnum.FOUR_G"><SignalFilled /></span>
<span v-else><GlobalOutlined /></span> <span v-else><GlobalOutlined /></span>
</span> </span>
@ -267,7 +267,7 @@
<a-col span="6"> <a-col span="6">
<a-tooltip title="Drone in dock"> <a-tooltip title="Drone in dock">
<span><RocketOutlined /></span> <span><RocketOutlined /></span>
<span class="ml10">{{ DroneInDockEnum[deviceInfo.dock.basic_osd?.drone_in_dock] }}</span> <span class="ml10">{{ deviceInfo.dock.basic_osd?.drone_in_dock }}</span>
</a-tooltip> </a-tooltip>
</a-col> </a-col>
</a-row> </a-row>
@ -413,6 +413,17 @@
<!-- 飞行指令 --> <!-- 飞行指令 -->
<DroneControlPanel :sn="osdVisible.gateway_sn" :deviceInfo="deviceInfo" :payloads="osdVisible.payloads"></DroneControlPanel> <DroneControlPanel :sn="osdVisible.gateway_sn" :deviceInfo="deviceInfo" :payloads="osdVisible.payloads"></DroneControlPanel>
</div> </div>
<!-- liveview -->
<div class="liveview" v-if="livestreamOthersVisible" v-drag-window >
<div style="height: 40px; width: 100%" class="drag-title"></div>
<a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="closeLivestreamOthers"><CloseOutlined /></a>
<LivestreamOthers />
</div>
<div class="liveview" v-if="livestreamAgoraVisible" v-drag-window >
<div style="height: 40px; width: 100%" class="drag-title"></div>
<a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="closeLivestreamAgora"><CloseOutlined /></a>
<LivestreamAgora />
</div>
</div> </div>
</template> </template>
@ -452,6 +463,12 @@ import DockControlPanel from './g-map/DockControlPanel.vue'
import { useDockControl } from './g-map/use-dock-control' import { useDockControl } from './g-map/use-dock-control'
import DroneControlPanel from './g-map/DroneControlPanel.vue' 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 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: {
@ -475,7 +492,10 @@ export default defineComponent({
DockControlPanel, DockControlPanel,
DroneControlPanel, DroneControlPanel,
CarryOutOutlined, CarryOutOutlined,
RocketOutlined RocketOutlined,
LivestreamOthers,
LivestreamAgora,
FlightAreaActionIcon,
}, },
name: 'GMap', name: 'GMap',
props: {}, props: {},
@ -489,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({
@ -534,10 +555,25 @@ export default defineComponent({
const drawVisible = computed(() => { const drawVisible = computed(() => {
return store.state.drawVisible return store.state.drawVisible
}) })
const livestreamOthersVisible = computed(() => {
return store.state.livestreamOthersVisible
})
const livestreamAgoraVisible = computed(() => {
return store.state.livestreamAgoraVisible
})
const osdVisible = computed(() => { const osdVisible = computed(() => {
return store.state.osdVisible return store.state.osdVisible
}) })
const qualityStyle = computed(() => {
if (deviceInfo.dock.basic_osd?.network_state?.type === NetworkStateTypeEnum.ETHERNET ||
(deviceInfo.dock.basic_osd?.network_state?.quality || 0) > NetworkStateQualityEnum.FAIR) {
return 'color: #00ee8b'
}
if ((deviceInfo.dock.basic_osd?.network_state?.quality || 0) === NetworkStateQualityEnum.FAIR) {
return 'color: yellow'
}
return 'color: red'
})
watch(() => store.state.deviceStatusEvent, watch(() => store.state.deviceStatusEvent,
data => { data => {
if (Object.keys(data.deviceOnline).length !== 0) { if (Object.keys(data.deviceOnline).length !== 0) {
@ -560,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.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]
@ -607,15 +646,27 @@ export default defineComponent({
}) })
updateCoordinates('wgs84-gcj02', ele) updateCoordinates('wgs84-gcj02', ele)
useGMapCoverHook.init2DPin( const data = { id: ele.id, name: ele.name }
ele.name, if (MapElementEnum.PIN === ele.resource?.type) {
ele.resource.content.geometry.coordinates, useGMapCoverHook.init2DPin(
ele.resource.content.properties.color, ele.name,
{ ele.resource.content.geometry.coordinates,
id: ele.id, ele.resource.content.properties.color,
name: ele.name data
} )
) } else if (MapElementEnum.LINE === ele.resource?.type) {
useGMapCoverHook.initPolyline(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
data)
} else if (MapElementEnum.POLY === ele.resource?.type) {
useGMapCoverHook.initPolygon(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
data)
}
} }
store.state.wsEvent.mapElementCreat = {} store.state.wsEvent.mapElementCreat = {}
@ -636,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
@ -657,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)
@ -680,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)
@ -689,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)
@ -698,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) {
@ -749,6 +809,12 @@ export default defineComponent({
console.log('layers', layers) console.log('layers', layers)
store.commit('SET_LAYER_INFO', layers) store.commit('SET_LAYER_INFO', layers)
} }
function closeLivestreamOthers () {
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', false)
}
function closeLivestreamAgora () {
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', false)
}
function updateCoordinates (transformType: string, element: any) { function updateCoordinates (transformType: string, element: any) {
const geoType = element.resource?.content.geometry.type const geoType = element.resource?.content.geometry.type
const type = element.resource?.type as number const type = element.resource?.type as number
@ -769,39 +835,38 @@ export default defineComponent({
) as GeojsonCoordinate ) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult element.resource.content.geometry.coordinates = transResult
} }
} else if (MapElementEnum.LINE === type && geoType === 'LineString') { } else if (MapElementEnum.LINE === type) {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[] .coordinates as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') { if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = wgs84togcj02( arr[i] = wgs84togcj02(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
}) })
} else if (transformType === 'gcj02-wgs84') { } else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = gcj02towgs84( arr[i] = gcj02towgs84(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
}) })
} }
element.resource.content.geometry.coordinates = coordinates element.resource.content.geometry.coordinates = coordinates
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') { } else if (MapElementEnum.POLY === type) {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[] .coordinates[0] as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') { if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = wgs84togcj02( arr[i] = wgs84togcj02(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
}) })
} else if (transformType === 'gcj02-wgs84') { } else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = gcj02towgs84( arr[i] = gcj02towgs84(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
@ -815,6 +880,8 @@ export default defineComponent({
draw, draw,
mouseMode, mouseMode,
drawVisible, drawVisible,
livestreamOthersVisible,
livestreamAgoraVisible,
osdVisible, osdVisible,
pin, pin,
state, state,
@ -830,7 +897,11 @@ export default defineComponent({
NetworkStateTypeEnum, NetworkStateTypeEnum,
NetworkStateQualityEnum, NetworkStateQualityEnum,
RainfallEnum, RainfallEnum,
DroneInDockEnum DroneInDockEnum,
closeLivestreamOthers,
closeLivestreamAgora,
qualityStyle,
selectFlightAreaAction,
} }
} }
}) })
@ -875,7 +946,8 @@ export default defineComponent({
} }
.osd-panel { .osd-panel {
position: absolute; position: absolute;
left: 10px; margin-left: 10px;
left: 0;
top: 10px; top: 10px;
width: 480px; width: 480px;
background: #000; background: #000;
@ -926,4 +998,17 @@ export default defineComponent({
min-height: 2px; min-height: 2px;
border-radius: 2px; border-radius: 2px;
} }
.liveview {
position: absolute;
color: #fff;
z-index: 1;
left: 0;
margin-left: 10px;
top: 10px;
text-align: center;
width: 800px;
height: 720px;
background: #232323;
}
</style> </style>

3
src/components/common/sidebar.vue

@ -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) {

4
src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue

@ -21,8 +21,8 @@
<!-- 设备类型 --> <!-- 设备类型 -->
<template #device_type="{ record }"> <template #device_type="{ record }">
<div> <div>
<div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).parents[0].device_model.key]}}</div> <div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).parents[0].device_model.device_model_key]}}</div>
<div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).hosts[0].device_model.key]}}</div> <div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).hosts[0].device_model.device_model_key]}}</div>
</div> </div>
</template> </template>
<!-- 设备sn --> <!-- 设备sn -->

59
src/components/flight-area/FlightAreaActionIcon.vue

@ -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>

197
src/components/flight-area/FlightAreaDevicePanel.vue

@ -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>

33
src/components/flight-area/FlightAreaIcon.vue

@ -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>

89
src/components/flight-area/FlightAreaItem.vue

@ -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>

43
src/components/flight-area/FlightAreaPanel.vue

@ -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>

66
src/components/flight-area/FlightAreaSyncPanel.vue

@ -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>

18
src/components/flight-area/use-flight-area-drone-location-event.ts

@ -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)
})
}

17
src/components/flight-area/use-flight-area-sync-progress-event.ts

@ -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)
})
}

30
src/components/flight-area/use-flight-area-update.ts

@ -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)
})
}

155
src/components/flight-area/use-flight-area.ts

@ -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,
}
}

7
src/components/g-map/DockControlPanel.vue

@ -73,15 +73,14 @@ watch(() => props.deviceInfo, (value) => {
deep: true deep: true
}) })
// dock
const debugStatus = ref(props.deviceInfo.dock?.basic_osd?.mode_code === EDockModeCode.Remote_Debugging)
const emit = defineEmits(['close-control-panel']) const emit = defineEmits(['close-control-panel'])
function closeControlPanel () { function closeControlPanel () {
emit('close-control-panel', props.sn) emit('close-control-panel', props.sn, debugStatus.value)
} }
// dock
const debugStatus = ref(props.deviceInfo.dock?.basic_osd.mode_code === EDockModeCode.Remote_Debugging)
async function onDeviceStatusChange (status: boolean) { async function onDeviceStatusChange (status: boolean) {
let result = false let result = false
if (status) { if (status) {

91
src/components/g-map/DroneControlPanel.vue

@ -112,15 +112,53 @@
:options="WaylineLostControlActionInCommandFlightOptions" :options="WaylineLostControlActionInCommandFlightOptions"
></a-select> ></a-select>
</div> </div>
<div>
<span class="form-label">Return-to-Home Mode:</span>
<a-select
v-model:value="takeoffToPointPopoverData.rthMode"
style="width: 120px"
:options="RthModeInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Commander Mode Lost Action:</span>
<a-select
v-model:value="takeoffToPointPopoverData.commanderModeLostAction"
style="width: 120px"
:options="CommanderModeLostActionInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Commander Flight Mode:</span>
<a-select
v-model:value="takeoffToPointPopoverData.commanderFlightMode"
style="width: 120px"
:options="CommanderFlightModeInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Commander Flight Height(m):</span>
<a-input-number v-model:value="takeoffToPointPopoverData.commanderFlightHeight"/>
</div>
</div> </div>
</template> </template>
<Button size="small" ghost @click="onShowTakeoffToPointPopover" > <Button size="small" ghost @click="onShowTakeoffToPointPopover" >
<span>Take off</span> <span>Take off</span>
</Button> </Button>
<div v-for="(cmdItem) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
<Button :loading="cmdItem.loading" size="small" ghost @click="sendControlCmd(cmdItem, 0)">
{{ cmdItem.operateText }}
</Button>
</div>
<div>
<Button size="small" ghost @click="openLivestreamAgora" >
<span>Agora Live</span>
</Button>
<Button size="small" ghost @click="openLivestreamOthers" >
<span>RTMP/GB28181 Live</span>
</Button>
</div>
</DroneControlPopover> </DroneControlPopover>
<Button :loading="cmdItem.loading" size="small" ghost @click="sendControlCmd(cmdItem, 0)">
{{ cmdItem.operateText }}
</Button>
</div> </div>
</div> </div>
<div class="box"> <div class="box">
@ -250,9 +288,15 @@ import { usePayloadControl } from './use-payload-control'
import { CameraMode, CameraType, CameraTypeOptions, ZoomCameraTypeOptions, CameraListItem } from '/@/types/live-stream' import { CameraMode, CameraType, CameraTypeOptions, ZoomCameraTypeOptions, CameraListItem } from '/@/types/live-stream'
import { useDroneControlWsEvent } from './use-drone-control-ws-event' import { useDroneControlWsEvent } from './use-drone-control-ws-event'
import { useDroneControlMqttEvent } from './use-drone-control-mqtt-event' import { useDroneControlMqttEvent } from './use-drone-control-mqtt-event'
import { postFlightAuth, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone' import {
postFlightAuth, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight, ERthMode,
ECommanderModeLostAction, ECommanderFlightMode
} from '/@/api/drone-control/drone'
import { useDroneControl } from './use-drone-control' import { useDroneControl } from './use-drone-control'
import { GimbalResetMode, GimbalResetModeOptions, LostControlActionInCommandFLightOptions, WaylineLostControlActionInCommandFlightOptions } from '/@/types/drone-control' import {
GimbalResetMode, GimbalResetModeOptions, LostControlActionInCommandFLightOptions, WaylineLostControlActionInCommandFlightOptions,
RthModeInCommandFlightOptions, CommanderModeLostActionInCommandFlightOptions, CommanderFlightModeInCommandFlightOptions
} from '/@/types/drone-control'
import DroneControlPopover from './DroneControlPopover.vue' import DroneControlPopover from './DroneControlPopover.vue'
import DroneControlInfoPanel from './DroneControlInfoPanel.vue' import DroneControlInfoPanel from './DroneControlInfoPanel.vue'
import { noDebugCmdList as baseCmdList, DeviceCmdItem, DeviceCmd } from '/@/types/device-cmd' import { noDebugCmdList as baseCmdList, DeviceCmdItem, DeviceCmd } from '/@/types/device-cmd'
@ -269,8 +313,8 @@ const clientId = computed(() => {
return store.state.clientId return store.state.clientId
}) })
const initCmdList = baseCmdList.find(item => item.cmdKey === DeviceCmd.ReturnHome) as DeviceCmdItem const initCmdList = baseCmdList.map(cmdItem => Object.assign({}, cmdItem))
const cmdItem = ref(initCmdList) const cmdList = ref(initCmdList)
const { const {
sendDockControlCmd sendDockControlCmd
@ -283,9 +327,11 @@ async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
cmd: cmdItem.cmdKey, cmd: cmdItem.cmdKey,
action: cmdItem.action action: cmdItem.action
}, false) }, false)
if (result && flightController.value) { if (result) {
message.success('Return home successful') message.success('Return home successful')
exitFlightCOntrol() if (flightController.value) {
exitFlightCOntrol()
}
} else { } else {
message.error('Failed to return home') message.error('Failed to return home')
} }
@ -349,7 +395,11 @@ const takeoffToPointPopoverData = reactive({
maxSpeed: MAX_SPEED, maxSpeed: MAX_SPEED,
rthAltitude: null as null | number, rthAltitude: null as null | number,
rcLostAction: LostControlActionInCommandFLight.RETURN_HOME, rcLostAction: LostControlActionInCommandFLight.RETURN_HOME,
exitWaylineWhenRcLost: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION exitWaylineWhenRcLost: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION,
rthMode: ERthMode.SETTING,
commanderModeLostAction: ECommanderModeLostAction.CONTINUE,
commanderFlightMode: ECommanderFlightMode.SETTING,
commanderFlightHeight: null as null | number,
}) })
function onShowTakeoffToPointPopover () { function onShowTakeoffToPointPopover () {
@ -361,6 +411,10 @@ function onShowTakeoffToPointPopover () {
takeoffToPointPopoverData.rthAltitude = null takeoffToPointPopoverData.rthAltitude = null
takeoffToPointPopoverData.rcLostAction = LostControlActionInCommandFLight.RETURN_HOME takeoffToPointPopoverData.rcLostAction = LostControlActionInCommandFLight.RETURN_HOME
takeoffToPointPopoverData.exitWaylineWhenRcLost = WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION takeoffToPointPopoverData.exitWaylineWhenRcLost = WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION
takeoffToPointPopoverData.rthMode = ERthMode.SETTING
takeoffToPointPopoverData.commanderModeLostAction = ECommanderModeLostAction.CONTINUE
takeoffToPointPopoverData.commanderFlightMode = ECommanderFlightMode.SETTING
takeoffToPointPopoverData.commanderFlightHeight = null
} }
async function onTakeoffToPointConfirm (confirm: boolean) { async function onTakeoffToPointConfirm (confirm: boolean) {
@ -369,7 +423,8 @@ async function onTakeoffToPointConfirm (confirm: boolean) {
!takeoffToPointPopoverData.latitude || !takeoffToPointPopoverData.latitude ||
!takeoffToPointPopoverData.longitude || !takeoffToPointPopoverData.longitude ||
!takeoffToPointPopoverData.securityTakeoffHeight || !takeoffToPointPopoverData.securityTakeoffHeight ||
!takeoffToPointPopoverData.rthAltitude) { !takeoffToPointPopoverData.rthAltitude ||
!takeoffToPointPopoverData.commanderFlightHeight) {
message.error('Input error') message.error('Input error')
return return
} }
@ -382,7 +437,11 @@ async function onTakeoffToPointConfirm (confirm: boolean) {
rth_altitude: takeoffToPointPopoverData.rthAltitude, rth_altitude: takeoffToPointPopoverData.rthAltitude,
max_speed: takeoffToPointPopoverData.maxSpeed, max_speed: takeoffToPointPopoverData.maxSpeed,
rc_lost_action: takeoffToPointPopoverData.rcLostAction, rc_lost_action: takeoffToPointPopoverData.rcLostAction,
exit_wayline_when_rc_lost: takeoffToPointPopoverData.exitWaylineWhenRcLost exit_wayline_when_rc_lost: takeoffToPointPopoverData.exitWaylineWhenRcLost,
rth_mode: takeoffToPointPopoverData.rthMode,
commander_mode_lost_action: takeoffToPointPopoverData.commanderModeLostAction,
commander_flight_mode: takeoffToPointPopoverData.commanderFlightMode,
commander_flight_height: takeoffToPointPopoverData.commanderFlightHeight,
}) })
} catch (error) { } catch (error) {
} }
@ -672,6 +731,14 @@ function onShowCameraAimPopover () {
cameraAimPopoverData.y = null cameraAimPopoverData.y = null
} }
function openLivestreamOthers () {
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', true)
}
function openLivestreamAgora () {
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', true)
}
async function onCameraAimConfirm (confirm: boolean) { async function onCameraAimConfirm (confirm: boolean) {
if (confirm) { if (confirm) {
if (cameraAimPopoverData.cameraType === null || cameraAimPopoverData.x === null || cameraAimPopoverData.y === null) { if (cameraAimPopoverData.cameraType === null || cameraAimPopoverData.x === null || cameraAimPopoverData.y === null) {

6
src/components/g-map/use-dock-control.ts

@ -45,8 +45,10 @@ export function useDockControl () {
} }
// 控制面板关闭 // 控制面板关闭
async function onCloseControlPanel (sn: string) { async function onCloseControlPanel (sn: string, debugging: boolean) {
await dockDebugOnOff(sn, false) if (debugging) {
await dockDebugOnOff(sn, false)
}
setDockControlPanelVisible(false) setDockControlPanelVisible(false)
} }

2
src/components/livestream-agora.vue

@ -1,5 +1,5 @@
<template> <template>
<div class="mt20 flex-column flex-justify-start flex-align-center"> <div class="flex-column flex-justify-start flex-align-center">
<div id="player" style="width: 720px; height: 420px; border: 1px solid"></div> <div id="player" style="width: 720px; height: 420px; border: 1px solid"></div>
<p class="fz24">Live streaming source selection</p> <p class="fz24">Live streaming source selection</p>
<div class="flex-row flex-justify-center flex-align-center mt10"> <div class="flex-row flex-justify-center flex-align-center mt10">

38
src/components/livestream-others.vue

@ -1,10 +1,11 @@
<template> <template>
<div class="flex-column flex-justify-start flex-align-center mt20"> <div class="flex-column flex-justify-start flex-align-center">
<video <video
:style="{ width: '720px', height: '480px' }" :style="{ width: '720px', height: '480px' }"
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>

163
src/components/task/CreatePlan.vue

@ -30,10 +30,10 @@
</div> </div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> <div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span> <span><RocketOutlined /></span>
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span> <span class="ml5">{{ DEVICE_NAME[wayline.drone_model_key] }}</span>
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> <span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span>
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> <span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }} {{ DEVICE_NAME[payload] }}
</span> </span>
</div> </div>
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> <div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
@ -49,7 +49,7 @@
>Select Device</router-link> >Select Device</router-link>
</a-form-item> </a-form-item>
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;"> <a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;">
<div class="panel" style="padding-top: 5px;" @click="selectDock(dock)"> <div class="panel" style="padding-top: 5px;">
<div class="title"> <div class="title">
<a-tooltip :title="dock.nickname"> <a-tooltip :title="dock.nickname">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div> <div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
@ -62,22 +62,64 @@
</div> </div>
</a-form-item> </a-form-item>
<!-- 任务类型 --> <!-- 任务类型 -->
<a-form-item label="Plan Timer" class="plan-timer-form-item" :labelCol="{span: 23}"> <a-form-item label="Plan Timer" class="plan-timer-form-item">
<div style="white-space: nowrap;"> <div style="white-space: nowrap;">
<a-radio-group v-model:value="planBody.task_type" button-style="solid"> <a-radio-group v-model:value="planBody.task_type" button-style="solid">
<a-radio-button v-for="type in TaskTypeOptions" :value="type.value" :key="type.value">{{ type.label }}</a-radio-button> <a-radio-button v-for="type in TaskTypeOptions" :value="type.value" :key="type.value">{{ type.label }}</a-radio-button>
</a-radio-group> </a-radio-group>
</div> </div>
</a-form-item> </a-form-item>
<!-- 执行时间 --> <!-- execute date -->
<a-form-item label="Start Time" v-if="planBody.task_type === TaskType.Timed" name="select_execute_time" :labelCol="{span: 23}"> <a-form-item label="Date" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition" name="select_execute_date" :labelCol="{span: 23}">
<a-date-picker <a-range-picker
v-model:value="planBody.select_execute_time" v-model:value="planBody.select_execute_date"
format="YYYY-MM-DD HH:mm:ss" :disabledDate="(current: Moment) => current < moment().subtract(1, 'days')"
format="YYYY-MM-DD"
:placeholder="['Start Time', 'End Time']"
style="width: 100%;"
/>
</a-form-item>
<!-- execute time -->
<a-form-item label="Time" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition"
name="select_execute_time" ref="select_execute_time" :labelCol="{span: 23}" :autoLink="false">
<div class="mb10 flex-row flex-align-center flex-justify-around" v-for="n in planBody.select_time_number" :key="n">
<a-time-picker
v-model:value="planBody.select_time[n - 1][0]"
format="HH:mm:ss"
show-time show-time
placeholder="Select Time" placeholder="Start Time"
:style="planBody.task_type === TaskType.Condition ? 'width: 40%' : 'width: 82%'"
@change="() => $refs.select_execute_time.onFieldChange()"
/> />
<template v-if="planBody.task_type === TaskType.Condition">
<div><span style="color: white;">-</span></div>
<a-time-picker
v-model:value="planBody.select_time[n - 1][1]"
format="HH:mm:ss"
show-time
placeholder="End Time"
style="width: 40%;"
/>
</template>
<div class="ml5" style="font-size:18px">
<PlusCircleOutlined class="mr5" style="color: #1890ff" @click="addTime"/>
<MinusCircleOutlined :style="planBody.select_time_number === 1 ? 'color: gray' : 'color: red;'" @click="removeTime"/>
</div>
</div>
</a-form-item> </a-form-item>
<template v-if="planBody.task_type === TaskType.Condition">
<!-- battery capacity -->
<a-form-item label="Start task when battery level reaches" :labelCol="{span: 23}" name="min_battery_capacity">
<a-input-number class="width-100" v-model:value="planBody.min_battery_capacity" :min="50" :max="100"
:formatter="(value: number) => `${value}%`" :parser="(value: string) => value.replace('%', '')">
</a-input-number>
</a-form-item>
<!-- storage capacity -->
<a-form-item label="Start task when storage level reaches (MB)" :labelCol="{span: 23}" name="storage_capacity">
<a-input-number v-model:value="planBody.min_storage_capacity" class="width-100">
</a-input-number>
</a-form-item>
</template>
<!-- RTH Altitude Relative to Dock --> <!-- RTH Altitude Relative to Dock -->
<a-form-item label="RTH Altitude Relative to Dock (m)" :labelCol="{span: 23}" name="rth_altitude"> <a-form-item label="RTH Altitude Relative to Dock (m)" :labelCol="{span: 23}" name="rth_altitude">
<a-input-number v-model:value="planBody.rth_altitude" :min="20" :max="1500" class="width-100" required> <a-input-number v-model:value="planBody.rth_altitude" :min="20" :max="1500" class="width-100" required>
@ -120,7 +162,7 @@ import { CloseOutlined, RocketOutlined, CameraFilled, UserOutlined, PlusCircleOu
import { ELocalStorageKey, ERouterName } from '/@/types' import { ELocalStorageKey, ERouterName } from '/@/types'
import { useMyStore } from '/@/store' import { useMyStore } from '/@/store'
import { WaylineType, WaylineFile } from '/@/types/wayline' import { WaylineType, WaylineFile } from '/@/types/wayline'
import { Device, EDeviceType } from '/@/types/device' import { Device, DEVICE_NAME } from '/@/types/device'
import { createPlan, CreatePlan } from '/@/api/wayline' import { createPlan, CreatePlan } from '/@/api/wayline'
import { getRoot } from '/@/root' import { getRoot } from '/@/root'
import { TaskType, OutOfControlActionOptions, OutOfControlAction, TaskTypeOptions } from '/@/types/task' import { TaskType, OutOfControlActionOptions, OutOfControlAction, TaskTypeOptions } from '/@/types/task'
@ -145,12 +187,16 @@ const disabled = ref(false)
const routeName = ref('') const routeName = ref('')
const planBody = reactive({ const planBody = reactive({
name: '', name: '',
file_id: computed(() => store.state.waylineInfo.id), file_id: computed(() => store.state?.waylineInfo.id),
dock_sn: computed(() => store.state.dockInfo.device_sn), dock_sn: computed(() => store.state?.dockInfo.device_sn),
task_type: TaskType.Immediate, task_type: TaskType.Immediate,
select_execute_time: undefined as Moment| undefined, select_execute_date: [moment(), moment()] as Moment[],
select_time_number: 1,
select_time: [[]] as Moment[][],
rth_altitude: '', rth_altitude: '',
out_of_control_action: OutOfControlAction.ReturnToHome, out_of_control_action: OutOfControlAction.ReturnToHome,
min_battery_capacity: 90 as number,
min_storage_capacity: undefined as number | undefined,
}) })
const drawerVisible = ref(false) const drawerVisible = ref(false)
@ -158,16 +204,35 @@ const valueRef = ref()
const rules = { const rules = {
name: [ name: [
{ required: true, message: 'Please enter plan name.' }, { required: true, message: 'Please enter plan name.' },
{ max: 20, message: 'Length should be 1 to 20', trigger: 'blur' } { max: 20, message: 'Length should be 1 to 20' }
], ],
file_id: [{ required: true, message: 'Select Route' }], file_id: [{ required: true, message: 'Select Route' }],
dock_sn: [{ required: true, message: 'Select Device' }], dock_sn: [{ required: true, message: 'Select Device' }],
select_execute_time: [{ required: true, message: 'Select start time' }], select_execute_time: [{
validator: async (rule: RuleObject, value: Moment[]) => {
validEndTime()
validStartTime()
if (planBody.select_time.length < planBody.select_time_number) {
throw new Error('Select time')
}
validOverlapped()
}
}],
select_execute_date: [{ required: true, message: 'Select date' }],
rth_altitude: [ rth_altitude: [
{ {
validator: async (rule: RuleObject, value: string) => { validator: async (rule: RuleObject, value: string) => {
if (!/^[0-9]{1,}$/.test(value)) { if (!/^[0-9]{1,}$/.test(value)) {
throw new Error('RTH Altitude Relative Require number') throw new Error('RTH Altitude Require number')
}
},
}
],
min_battery_capacity: [
{
validator: async (rule: RuleObject, value: any) => {
if (TaskType.Condition === planBody.task_type && !value) {
throw new Error('Please enter battery capacity')
} }
}, },
} }
@ -175,19 +240,61 @@ const rules = {
out_of_control_action: [{ required: true, message: 'Select Lost Action' }], out_of_control_action: [{ required: true, message: 'Select Lost Action' }],
} }
function validStartTime (): Error | void {
for (let i = 0; i < planBody.select_time.length; i++) {
if (!planBody.select_time[i][0]) {
throw new Error('Select start time')
}
}
}
function validEndTime (): Error | void {
if (TaskType.Condition !== planBody.task_type) return
for (let i = 0; i < planBody.select_time.length; i++) {
if (!planBody.select_time[i][1]) {
throw new Error('Select end time')
}
if (planBody.select_time[i][0] && planBody.select_time[i][1].isSameOrBefore(planBody.select_time[i][0])) {
throw new Error('End time should be later than start time')
}
}
}
function validOverlapped (): Error | void {
if (TaskType.Condition !== planBody.task_type) return
const arr = planBody.select_time.slice()
arr.sort((a, b) => a[0].unix() - b[0].unix())
arr.forEach((v, i, arr) => {
if (i > 0 && v[0] < arr[i - 1][1]) {
throw new Error('Overlapping time periods.')
}
})
}
function onSubmit () { function onSubmit () {
console.info(dock, '12131231')
valueRef.value.validate().then(() => { valueRef.value.validate().then(() => {
disabled.value = true disabled.value = true
const createPlanBody = { ...planBody } as unknown as CreatePlan const createPlanBody = { ...planBody } as unknown as CreatePlan
if (planBody.select_execute_time) { if (planBody.select_execute_date.length === 2) {
createPlanBody.task_days = [moment(planBody.select_execute_time).unix()] createPlanBody.task_days = []
createPlanBody.task_periods = [createPlanBody.task_days] for (let i = planBody.select_execute_date[0]; i.isSameOrBefore(planBody.select_execute_date[1]); i.add(1, 'days')) {
createPlanBody.task_days.push(i.unix())
}
}
createPlanBody.task_periods = []
if (TaskType.Immediate !== planBody.task_type) {
for (let i = 0; i < planBody.select_time.length; i++) {
const result = []
result.push(planBody.select_time[i][0].unix())
if (TaskType.Condition === planBody.task_type) {
result.push(planBody.select_time[i][1].unix())
}
createPlanBody.task_periods.push(result)
}
} }
createPlanBody.rth_altitude = Number(createPlanBody.rth_altitude) createPlanBody.rth_altitude = Number(createPlanBody.rth_altitude)
if (wayline.value && wayline.value.template_types && wayline.value.template_types.length > 0) { if (wayline.value && wayline.value.template_types && wayline.value.template_types.length > 0) {
createPlanBody.wayline_type = wayline.value.template_types[0] createPlanBody.wayline_type = wayline.value.template_types[0]
} }
// console.log('planBody', createPlanBody)
createPlan(workspaceId, createPlanBody) createPlan(workspaceId, createPlanBody)
.then(res => { .then(res => {
disabled.value = false disabled.value = false
@ -217,6 +324,18 @@ function selectDevice () {
drawerVisible.value = true drawerVisible.value = true
routeName.value = 'DockPanel' routeName.value = 'DockPanel'
} }
function addTime () {
valueRef.value.validateFields(['select_execute_time']).then(() => {
planBody.select_time_number++
planBody.select_time.push([])
})
}
function removeTime () {
if (planBody.select_time_number === 1) return
planBody.select_time_number--
planBody.select_time.splice(planBody.select_time_number)
}
</script> </script>
<style lang="scss"> <style lang="scss">
@ -266,7 +385,7 @@ function selectDevice () {
.ant-radio-button-wrapper{ .ant-radio-button-wrapper{
background-color: #232323; background-color: #232323;
color: #fff; color: #fff;
width: 80%; width: 33%;
text-align: center; text-align: center;
&.ant-radio-button-wrapper-checked{ &.ant-radio-button-wrapper-checked{
background-color: #1890ff; background-color: #1890ff;

15
src/components/workspace/DividerLine.vue

@ -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>

21
src/components/workspace/Title.vue

@ -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>

6
src/constants/map.ts

@ -11,8 +11,8 @@ export const MapElementDefaultColor = MapElementColor.Default
export enum MapDoodleColor { export enum MapDoodleColor {
PinColor = '#2D8CF0', PinColor = '#2D8CF0',
PolylineColor = '#3366FF', PolylineColor = '#2D8CF0',
PolygonColor = '#FF33FF' PolygonColor = '#2D8CF0'
} }
export enum MapElementEnum { export enum MapElementEnum {
@ -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'

38
src/directives/drag-window.ts

@ -0,0 +1,38 @@
import { nextTick, App } from 'vue'
export default function useDragWindowDirective (app: App): void {
app.directive('drag-window', async (el) => {
await nextTick()
const modal = el
const header = el.getElementsByClassName('drag-title')[0]
let left = 0
let top = 0
header.style.cursor = 'move'
top = top || modal.offsetTop
header.onpointerdown = (e: { clientX: number; clientY: number; pointerId: number }) => {
const startX = e.clientX
const startY = e.clientY
header.left = header.offsetLeft
header.top = header.offsetTop
header.setPointerCapture(e.pointerId)
el.onpointermove = (event: { clientX: number; clientY: number }) => {
const endX = event.clientX
const endY = event.clientY
modal.left = header.left + (endX - startX) + left
modal.top = header.top + (endY - startY) + top
modal.style.left = modal.left + 'px'
modal.style.top = modal.top + 'px'
}
el.onpointerup = () => {
left = modal.left || 0
top = modal.top || 0
el.onpointermove = null
el.onpointerup = null
header.releasePointerCapture(e.pointerId)
}
}
})
}

6
src/directives/index.ts

@ -0,0 +1,6 @@
import { App } from 'vue'
import useDragWindowDirective from './drag-window'
export function useDirectives (app: App): void {
useDragWindowDirective(app)
}

3
src/event-bus/index.ts

@ -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>()

209
src/hooks/use-g-map-cover.ts

@ -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[]
@ -78,9 +83,9 @@ export function useGMapCover () {
AddOverlayGroup(polyline) AddOverlayGroup(polyline)
} }
function initPolygon (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) { function initPolygon (name: string, coordinates:GeojsonCoordinate[][], color?:string, data?:{}) {
const path = [] as GeojsonCoordinate[] const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => { coordinates[0].forEach(coordinate => {
path.push(new AMap.LngLat(coordinate[0], coordinate[1])) path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
}) })
// console.log('Polygon', path) // console.log('Polygon', path)
@ -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)
@ -141,12 +128,160 @@ export function useGMapCover () {
} }
} }
function updatePolylineElement (id:string, name: string, coordinates:GeojsonCoordinate[], color?:string) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
const element = elements[0]
const options = element.getOptions()
options.strokeColor = color || normalColor
element.setOptions(options)
} else {
initPolyline(name, coordinates, color, {
id: id,
name: name
})
}
}
function updatePolygonElement (id:string, name: string, coordinates:GeojsonCoordinate[][], color?:string) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
const element = elements[0]
const options = element.getOptions()
options.fillColor = color || normalColor
options.strokeColor = color || normalColor
element.setOptions(options)
} else {
initPolygon(name, coordinates, color, {
id: id,
name: name
})
}
}
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,
initPolygon, initPolygon,
removeCoverFromMap, removeCoverFromMap,
getElementFromMap, getElementFromMap,
updatePinElement updatePinElement,
updatePolylineElement,
updatePolygonElement,
initFlightAreaCircle,
initFlightAreaPolygon,
updateFlightAreaPolygon,
updateFlightAreaCircle,
calcPolygonPosition,
} }
} }

2
src/hooks/use-g-map.ts

@ -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)

16
src/hooks/use-map-tool.ts

@ -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,
}
}

58
src/hooks/use-mouse-tool.ts

@ -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)

4
src/main.ts

@ -5,10 +5,14 @@ import { CommonComponents } from './use-common-components'
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register'
import store, { storeKey } from './store' import store, { storeKey } from './store'
import { createInstance } from '/@/root' import { createInstance } from '/@/root'
import { useDirectives } from './directives'
import '/@/styles/index.scss' import '/@/styles/index.scss'
const app = createInstance(App) const app = createInstance(App)
app.use(store, storeKey) app.use(store, storeKey)
app.use(router) app.use(router)
app.use(CommonComponents) app.use(CommonComponents)
app.use(antComponents) app.use(antComponents)
app.use(useDirectives)
app.mount('#demo-app') app.mount('#demo-app')

10
src/pages/page-pilot/pilot-home.vue

@ -13,9 +13,9 @@
<RightOutlined style="float: right; margin-top: 5px; color: #8894a0" /> <RightOutlined style="float: right; margin-top: 5px; color: #8894a0" />
</div> </div>
<div style="height: 50%;"> <div style="height: 50%;">
<CloudSyncOutlined v-if="state === EStatusValue.CONNECTED" style="color: #75c5f6" /> <CloudSyncOutlined v-if="thingState === EStatusValue.CONNECTED" style="color: #75c5f6" />
<SyncOutlined spin v-else/> <SyncOutlined spin v-else/>
<span style="color: #737373; margin-left: 3px;">{{ state }}</span> <span style="color: #737373; margin-left: 3px;">{{ thingState }}</span>
</div> </div>
<a-drawer placement="right" v-model:visible="drawerVisible" width="340px"> <a-drawer placement="right" v-model:visible="drawerVisible" width="340px">
<div class="mb10 flex-row flex-justify-center flex-align-center"> <div class="mb10 flex-row flex-justify-center flex-align-center">
@ -135,7 +135,6 @@ import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
const root = getRoot() const root = getRoot()
const gatewayState = ref<boolean>(localStorage.getItem(ELocalStorageKey.GatewayOnline) === 'true') const gatewayState = ref<boolean>(localStorage.getItem(ELocalStorageKey.GatewayOnline) === 'true')
const state = ref(EStatusValue.DISCONNECT)
const thingState = ref(EStatusValue.DISCONNECT) const thingState = ref(EStatusValue.DISCONNECT)
const apiState = ref(EStatusValue.DISCONNECT) const apiState = ref(EStatusValue.DISCONNECT)
const liveState = ref(EStatusValue.DISCONNECT) const liveState = ref(EStatusValue.DISCONNECT)
@ -157,7 +156,7 @@ interface DeviceInfoData {
} }
const device = reactive<DeviceInfoData>({ const device = reactive<DeviceInfoData>({
data: { data: {
sn: EStatusValue.DISCONNECT, sn: '',
online_status: false, online_status: false,
device_callsign: '', device_callsign: '',
user_id: '', user_id: '',
@ -218,9 +217,7 @@ const messageHandler = async (payload: any) => {
case EBizCode.DeviceOnline: { case EBizCode.DeviceOnline: {
console.info('online: ', payload) console.info('online: ', payload)
if (payload.data.sn === device.data.gateway_sn) { if (payload.data.sn === device.data.gateway_sn) {
gatewayState.value = true
localStorage.setItem(ELocalStorageKey.GatewayOnline, gatewayState.value.toString()) localStorage.setItem(ELocalStorageKey.GatewayOnline, gatewayState.value.toString())
state.value = gatewayState.value && thingState.value === EStatusValue.CONNECTED ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
break break
} }
if (payload.data.gateway_sn === device.data.gateway_sn) { if (payload.data.gateway_sn === device.data.gateway_sn) {
@ -441,7 +438,6 @@ function refreshStatus () {
tsaState.value = apiPilot.isComponentLoaded(EComponentName.Tsa) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT tsaState.value = apiPilot.isComponentLoaded(EComponentName.Tsa) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
mediaState.value = apiPilot.isComponentLoaded(EComponentName.Media) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT mediaState.value = apiPilot.isComponentLoaded(EComponentName.Media) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
waylineState.value = apiPilot.isComponentLoaded(EComponentName.Mission) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT waylineState.value = apiPilot.isComponentLoaded(EComponentName.Mission) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
state.value = thingState.value === EStatusValue.CONNECTED && gatewayState.value ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
} }
function moduleInstall (m: any) { function moduleInstall (m: any) {

28
src/pages/page-pilot/pilot-liveshare.vue

@ -54,7 +54,26 @@
<div class="width-100" style="margin-top: -10px;"> <div class="width-100" style="margin-top: -10px;">
<div class="ml10" style="width: 97%;"> <div class="ml10" style="width: 97%;">
<span class="fz16">Param: </span> <span class="fz16">Param: </span>
<span v-if="liveStreamStatus.type === ELiveTypeValue.Agora" style="word-break: break-all; color: #75c5f6;">{{ agoraParam }}</span> <span v-if="liveStreamStatus.type === ELiveTypeValue.Agora" style="word-break: break-all; color: #75c5f6;">
<div class="flex-col flex-justify-center flex-align-center">
<div>
<span class="ml10">Token:</span>
<a-input
class="ml10"
v-model:value="agoraParam.token"
placeholder="Token"
></a-input>
</div>
<div>
<span class="ml10">Channel:</span>
<a-input
class="ml10"
v-model:value="agoraParam.channelId"
placeholder="Channel"
></a-input>
</div>
</div>
</span>
<span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTMP" style="word-break: break-all; color: #75c5f6;">{{ rtmpParam }}</span> <span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTMP" style="word-break: break-all; color: #75c5f6;">{{ rtmpParam }}</span>
<span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTSP" style="word-break: break-all; color: #75c5f6;">{{ rtspParam }}</span> <span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTSP" style="word-break: break-all; color: #75c5f6;">{{ rtspParam }}</span>
<span v-else-if="liveStreamStatus.type === ELiveTypeValue.GB28181" style="word-break: break-all; color: #75c5f6;">{{ gb28181Param }}</span> <span v-else-if="liveStreamStatus.type === ELiveTypeValue.GB28181" style="word-break: break-all; color: #75c5f6;">{{ gb28181Param }}</span>
@ -154,11 +173,11 @@ const liveTypeList = [
label: ELiveTypeName.GB28181 label: ELiveTypeName.GB28181
} }
] ]
const agoraParam = { const agoraParam = reactive({
uid: '2892130292', uid: '2892130292',
token: config.agoraToken, token: config.agoraToken,
channelId: config.agoraChannel channelId: config.agoraChannel
} })
const rtmpParam = { const rtmpParam = {
url: config.rtmpURL + new Date().getTime() url: config.rtmpURL + new Date().getTime()
} }
@ -174,7 +193,7 @@ const gb28181Param: GB28181Param = {
agentId: CURRENT_CONFIG.gbAgentId, agentId: CURRENT_CONFIG.gbAgentId,
password: CURRENT_CONFIG.gbPassword, password: CURRENT_CONFIG.gbPassword,
agentPort: CURRENT_CONFIG.gbAgentPort, agentPort: CURRENT_CONFIG.gbAgentPort,
agentChannel: CURRENT_CONFIG.gbAgentChannel agentChannel: CURRENT_CONFIG.gbAgentChannel,
} }
const playVisiable = ref(false) const playVisiable = ref(false)
@ -253,6 +272,7 @@ const onPublishModeSelect = (val: string) => {
apiPilot.setVideoPublishType(publishModeSelected.value) apiPilot.setVideoPublishType(publishModeSelected.value)
} }
const onPlay = () => { const onPlay = () => {
console.info(JSON.stringify(agoraParam))
if (!publishModeSelected.value) { if (!publishModeSelected.value) {
message.warn('Please select publish mode!') message.warn('Please select publish mode!')
return return

2
src/pages/page-web/projects/dock.vue

@ -37,7 +37,7 @@ import { onMounted, ref } from 'vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline' import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline'
import { EDeviceTypeName, ELocalStorageKey } from '/@/types' import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue' import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { Device, EDeviceType } from '/@/types/device' import { Device } from '/@/types/device'
import { useMyStore } from '/@/store' import { useMyStore } from '/@/store'
import { getBindingDevices } from '/@/api/manage' import { getBindingDevices } from '/@/api/manage'
import { IPage } from '/@/api/http/type' import { IPage } from '/@/api/http/type'

98
src/pages/page-web/projects/flight-area.vue

@ -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>

41
src/pages/page-web/projects/layer.vue

@ -171,7 +171,6 @@ function updateMapElement (
const geoType = element.resource?.content.geometry.type const geoType = element.resource?.content.geometry.type
const id = element.id const id = element.id
const type = element.resource?.type as number const type = element.resource?.type as number
if (MapElementEnum.PIN === type) { if (MapElementEnum.PIN === type) {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate .coordinates as GeojsonCoordinate
@ -179,17 +178,11 @@ function updateMapElement (
} else if (MapElementEnum.LINE === type && geoType === 'LineString') { } else if (MapElementEnum.LINE === type && geoType === 'LineString') {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[] .coordinates as GeojsonCoordinate[]
useGMapCoverHook.initPolyline(name, coordinates, color, { useGMapCoverHook.updatePolylineElement(id, name, coordinates, color)
id: id,
name: name
})
} else if (MapElementEnum.POLY === type && geoType === 'Polygon') { } else if (MapElementEnum.POLY === type && geoType === 'Polygon') {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[] .coordinates as GeojsonCoordinate[][]
useGMapCoverHook.initPolygon(name, coordinates, color, { useGMapCoverHook.updatePolygonElement(id, name, coordinates, color)
id: id,
name: name
})
} }
} }
function checkLayer (keys: string[]) { function checkLayer (keys: string[]) {
@ -235,10 +228,12 @@ function setBaseInfo () {
layerState.layerName = layer.name layerState.layerName = layer.name
layerState.layerId = layer.id layerState.layerId = layer.id
layerState.color = layer.resource?.content.properties.color layerState.color = layer.resource?.content.properties.color
let coordinate: GeojsonCoordinate
switch (geoType) { switch (geoType) {
case GeoType.Point: case GeoType.Point:
layerState.longitude = layer.resource?.content.geometry.coordinates[0] coordinate = gcj02towgs84(layer.resource?.content.geometry.coordinates[0], layer.resource?.content.geometry.coordinates[1]) as GeojsonCoordinate
layerState.latitude = layer.resource?.content.geometry.coordinates[1] layerState.longitude = coordinate[0]
layerState.latitude = coordinate[1]
break break
case GeoType.LineString: case GeoType.LineString:
break break
@ -250,7 +245,7 @@ function setBaseInfo () {
onMounted(() => { onMounted(() => {
const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
const parent = element?.parentNode as HTMLDivElement const parent = element?.parentNode as HTMLDivElement
scorllHeight.value = parent.clientHeight - parent.firstElementChild!.clientHeight scorllHeight.value = parent?.clientHeight - parent.firstElementChild!.clientHeight
getAllElement() getAllElement()
}) })
function closeDrawer () { function closeDrawer () {
@ -356,39 +351,39 @@ function updateCoordinates (transformType: string, element: LayerResource) {
) as GeojsonCoordinate ) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult element.resource.content.geometry.coordinates = transResult
} }
} else if (MapElementEnum.LINE === type && geoType === 'LineString') { } else if (MapElementEnum.LINE === type) {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[] .coordinates as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') { if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = wgs84togcj02( arr[i] = wgs84togcj02(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
}) })
} else if (transformType === 'gcj02-wgs84') { } else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = gcj02towgs84( arr[i] = gcj02towgs84(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
}) })
} }
element.resource.content.geometry.coordinates = coordinates element.resource.content.geometry.coordinates = coordinates
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') { } else if (MapElementEnum.POLY === type) {
const coordinates = element.resource?.content.geometry const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[] .coordinates[0] as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') { if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = wgs84togcj02( arr[i] = wgs84togcj02(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate
}) })
} else if (transformType === 'gcj02-wgs84') { } else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => { coordinates.forEach((coordinate, i, arr) => {
coordinate = gcj02towgs84( arr[i] = gcj02towgs84(
coordinate[0], coordinate[0],
coordinate[1] coordinate[1]
) as GeojsonCoordinate ) as GeojsonCoordinate

14
src/pages/page-web/projects/livestream.vue

@ -18,7 +18,8 @@
> >
</router-link> </router-link>
</div> </div>
<div class="live" v-if="showLive"> <div class="live" v-if="showLive" v-drag-window>
<div style="height: 40px; width: 100%" class="drag-title"></div>
<a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="() => root.$router.push('/' + ERouterName.LIVESTREAM)"><CloseOutlined /></a> <a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="() => root.$router.push('/' + ERouterName.LIVESTREAM)"><CloseOutlined /></a>
<router-view :name="routeName" /> <router-view :name="routeName" />
</div> </div>
@ -74,14 +75,13 @@ onMounted(() => {
.live { .live {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
right: 50%; left: 0;
left: 50%; top: 10px;
top: 50%; margin-left: 345px;
margin: auto;
transform: translate(-50%, -50%);
text-align: center; text-align: center;
width: 800px; width: 800px;
height: 700px; height: 720px;
background: #232323; background: #232323;
} }
</style> </style>

11
src/pages/page-web/projects/tsa.vue

@ -203,7 +203,7 @@ import { EHmsLevel } from '/@/types/enums'
const store = useMyStore() const store = useMyStore()
const username = ref(localStorage.getItem(ELocalStorageKey.Username)) const username = ref(localStorage.getItem(ELocalStorageKey.Username))
const workspaceId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!) const workspaceId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!)
const osdVisible = ref({} as OSDVisible) const osdVisible = computed(() => store.state.osdVisible)
const hmsVisible = new Map<string, boolean>() const hmsVisible = new Map<string, boolean>()
const scorllHeight = ref() const scorllHeight = ref()
@ -344,6 +344,15 @@ function readHms (visiable: boolean, sn: string) {
}) })
} }
} }
function openLivestreamOthers () {
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', true)
}
function openLivestreamAgora () {
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', true)
}
</script> </script>
<style lang="scss"> <style lang="scss">

6
src/pages/page-web/projects/wayline.vue

@ -52,10 +52,10 @@
</div> </div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> <div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span> <span><RocketOutlined /></span>
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span> <span class="ml5">{{ DEVICE_NAME[wayline.drone_model_key] }}</span>
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> <span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span>
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> <span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }} {{ DEVICE_NAME[payload] }}
</span> </span>
</div> </div>
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> <div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
@ -87,7 +87,7 @@ import { onMounted, onUpdated, ref } from 'vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles, importKmzFile } from '/@/api/wayline' import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles, importKmzFile } from '/@/api/wayline'
import { ELocalStorageKey, ERouterName } from '/@/types' import { ELocalStorageKey, ERouterName } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined, SelectOutlined } from '@ant-design/icons-vue' import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined, SelectOutlined } from '@ant-design/icons-vue'
import { EDeviceType } from '/@/types/device' import { DEVICE_NAME } from '/@/types/device'
import { useMyStore } from '/@/store' import { useMyStore } from '/@/store'
import { WaylineFile } from '/@/types/wayline' import { WaylineFile } from '/@/types/wayline'
import { downloadFile } from '/@/utils/common' import { downloadFile } from '/@/utils/common'

12
src/pages/page-web/projects/workspace.vue

@ -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
} }

15
src/router/index.ts

@ -47,6 +47,10 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('/@/pages/page-web/projects/workspace.vue'), component: () => import('/@/pages/page-web/projects/workspace.vue'),
redirect: '/' + ERouterName.TSA, redirect: '/' + ERouterName.TSA,
children: [ children: [
{
path: '/' + ERouterName.TSA,
component: () => import('/@/pages/page-web/projects/tsa.vue')
},
{ {
path: '/' + ERouterName.LIVESTREAM, path: '/' + ERouterName.LIVESTREAM,
name: ERouterName.LIVESTREAM, name: ERouterName.LIVESTREAM,
@ -62,10 +66,6 @@ const routes: Array<RouteRecordRaw> = [
} }
] ]
}, },
{
path: '/' + ERouterName.TSA,
component: () => import('/@/pages/page-web/projects/tsa.vue')
},
{ {
path: '/' + ERouterName.LAYER, path: '/' + ERouterName.LAYER,
name: ERouterName.LAYER, name: ERouterName.LAYER,
@ -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

17
src/store/index.ts

@ -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: [
@ -32,9 +33,11 @@ const initStateFunc = () => ({
[key:string]:string [key:string]:string
}, },
drawVisible: false, drawVisible: false,
coverList: [ livestreamOthersVisible: false,
livestreamAgoraVisible: false,
] as any, coverMap: {} as {
[key: string]: any[]
},
wsEvent: { wsEvent: {
mapElementCreat: {}, mapElementCreat: {},
mapElementUpdate: {}, mapElementUpdate: {},
@ -124,7 +127,7 @@ const mutations: MutationTree<RootStateType> = {
dock.basic_osd = info.host dock.basic_osd = info.host
return return
} }
if (info.host.sdr) { if (info.host.wireless_link) {
dock.link_osd = info.host dock.link_osd = info.host
return return
} }
@ -135,6 +138,12 @@ const mutations: MutationTree<RootStateType> = {
SET_DRAW_VISIBLE_INFO (state, bool) { SET_DRAW_VISIBLE_INFO (state, bool) {
state.drawVisible = bool state.drawVisible = bool
}, },
SET_LIVESTREAM_OTHERS_VISIBLE (state, bool) {
state.livestreamOthersVisible = bool
},
SET_LIVESTREAM_AGORA_VISIBLE (state, bool) {
state.livestreamAgoraVisible = bool
},
SET_MAP_ELEMENT_CREATE (state, info) { SET_MAP_ELEMENT_CREATE (state, info) {
state.wsEvent.mapElementCreat = info state.wsEvent.mapElementCreat = info
}, },

9
src/types/device-cmd.ts

@ -7,6 +7,7 @@ export enum DeviceCmd {
SupplementLightOpen = 'supplement_light_open', // 打开补光灯 SupplementLightOpen = 'supplement_light_open', // 打开补光灯
SupplementLightClose = 'supplement_light_close', // 关闭补光灯 SupplementLightClose = 'supplement_light_close', // 关闭补光灯
ReturnHome = 'return_home', // 一键返航 ReturnHome = 'return_home', // 一键返航
ReturnHomeCancel = 'return_home_cancel', // 取消返航
// 复杂指令 // 复杂指令
DeviceReboot = 'device_reboot', // 机场重启 DeviceReboot = 'device_reboot', // 机场重启
DroneOpen = 'drone_open', // 飞行器开机 DroneOpen = 'drone_open', // 飞行器开机
@ -48,6 +49,14 @@ export const noDebugCmdList: DeviceCmdItem[] = [
func: 'returnHome', func: 'returnHome',
loading: false, loading: false,
}, },
{
label: 'Return Home Cancel',
status: '--',
operateText: 'Return Home Cancel',
cmdKey: DeviceCmd.ReturnHomeCancel,
func: 'returnHomeCancel',
loading: false,
}
] ]
// 机场指令 // 机场指令

5
src/types/device-firmware.ts

@ -30,6 +30,9 @@ export interface FirmwareUploadParam {
export enum DeviceNameEnum { export enum DeviceNameEnum {
DJI_DOCK = 'DJI Dock', DJI_DOCK = 'DJI Dock',
DJI_DOCK2 = 'DJI Dock2',
MATRICE_30 = 'Matrice 30', MATRICE_30 = 'Matrice 30',
MATRICE_30T = 'Matrice 30T' MATRICE_30T = 'Matrice 30T',
M3D = 'M3D',
M3TD = 'M3TD',
} }

51
src/types/device.ts

@ -24,6 +24,7 @@ export enum DRONE_TYPE {
M300 = 60, M300 = 60,
Mavic3EnterpriseAdvanced= 77, Mavic3EnterpriseAdvanced= 77,
M350 = 89, M350 = 89,
M3D = 91,
} }
// DJI负载类型枚举值 // DJI负载类型枚举值
@ -43,6 +44,8 @@ export enum PAYLOAD_TYPE {
M3E = 66, M3E = 66,
M3T = 67, M3T = 67,
M3D = 80,
M3TD = 81,
// UNKNOWN = 65535 // UNKNOWN = 65535
} }
@ -56,6 +59,7 @@ export enum RC_TYPE {
// DOCK type // DOCK type
export enum DOCK_TYPE { export enum DOCK_TYPE {
Dock = 1, Dock = 1,
Dock2 = 2,
} }
// 设备sub_type 从0升序 // 设备sub_type 从0升序
@ -77,6 +81,9 @@ export const DEVICE_MODEL_KEY = {
M300: `${DOMAIN.DRONE}-${DRONE_TYPE.M300}-${DEVICE_SUB_TYPE.ZERO}`, M300: `${DOMAIN.DRONE}-${DRONE_TYPE.M300}-${DEVICE_SUB_TYPE.ZERO}`,
M350: `${DOMAIN.DRONE}-${DRONE_TYPE.M350}-${DEVICE_SUB_TYPE.ZERO}`, M350: `${DOMAIN.DRONE}-${DRONE_TYPE.M350}-${DEVICE_SUB_TYPE.ZERO}`,
M3D: `${DOMAIN.DRONE}-${DRONE_TYPE.M3D}-${DEVICE_SUB_TYPE.ZERO}`,
M3TD: `${DOMAIN.DRONE}-${DRONE_TYPE.M3D}-${DEVICE_SUB_TYPE.ONE}`,
FPV: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.FPV}-${DEVICE_SUB_TYPE.ZERO}`, FPV: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.FPV}-${DEVICE_SUB_TYPE.ZERO}`,
H20: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20}-${DEVICE_SUB_TYPE.ZERO}`, H20: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20}-${DEVICE_SUB_TYPE.ZERO}`,
H20T: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20T}-${DEVICE_SUB_TYPE.ZERO}`, H20T: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20T}-${DEVICE_SUB_TYPE.ZERO}`,
@ -88,6 +95,8 @@ export const DEVICE_MODEL_KEY = {
M3ECamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3E}-${DEVICE_SUB_TYPE.ZERO}`, M3ECamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3E}-${DEVICE_SUB_TYPE.ZERO}`,
M3TCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3T}-${DEVICE_SUB_TYPE.ZERO}`, M3TCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3T}-${DEVICE_SUB_TYPE.ZERO}`,
M3DCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3D}-${DEVICE_SUB_TYPE.ZERO}`,
M3TDCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3TD}-${DEVICE_SUB_TYPE.ZERO}`,
// M3MCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3M}-${DEVICE_SUB_TYPE.ZERO}`, // M3MCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3M}-${DEVICE_SUB_TYPE.ZERO}`,
XT2: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.XT2}-${DEVICE_SUB_TYPE.ZERO}`, XT2: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.XT2}-${DEVICE_SUB_TYPE.ZERO}`,
@ -99,6 +108,7 @@ export const DEVICE_MODEL_KEY = {
RCPlus: `${DOMAIN.RC}-${RC_TYPE.RCPlus}-${DEVICE_SUB_TYPE.ZERO}`, RCPlus: `${DOMAIN.RC}-${RC_TYPE.RCPlus}-${DEVICE_SUB_TYPE.ZERO}`,
Dock: `${DOMAIN.DOCK}-${DOCK_TYPE.Dock}-${DEVICE_SUB_TYPE.ZERO}`, Dock: `${DOMAIN.DOCK}-${DOCK_TYPE.Dock}-${DEVICE_SUB_TYPE.ZERO}`,
Dock2: `${DOMAIN.DOCK}-${DOCK_TYPE.Dock2}-${DEVICE_SUB_TYPE.ZERO}`,
} }
export const DEVICE_NAME = { export const DEVICE_NAME = {
@ -110,6 +120,8 @@ export const DEVICE_NAME = {
// [DEVICE_MODEL_KEY.M3M]: 'Mavic 3M', // [DEVICE_MODEL_KEY.M3M]: 'Mavic 3M',
[DEVICE_MODEL_KEY.M300]: 'M300 RTK', [DEVICE_MODEL_KEY.M300]: 'M300 RTK',
[DEVICE_MODEL_KEY.M350]: 'M350 RTK', [DEVICE_MODEL_KEY.M350]: 'M350 RTK',
[DEVICE_MODEL_KEY.M3D]: 'M3D',
[DEVICE_MODEL_KEY.M3TD]: 'M3TD',
// payload // payload
[DEVICE_MODEL_KEY.FPV]: 'FPV', [DEVICE_MODEL_KEY.FPV]: 'FPV',
@ -127,6 +139,8 @@ export const DEVICE_NAME = {
[DEVICE_MODEL_KEY.XTS]: 'XTS', [DEVICE_MODEL_KEY.XTS]: 'XTS',
[DEVICE_MODEL_KEY.Z30]: 'Z30', [DEVICE_MODEL_KEY.Z30]: 'Z30',
[DEVICE_MODEL_KEY.DockTopCamera]: 'Dock Camera', [DEVICE_MODEL_KEY.DockTopCamera]: 'Dock Camera',
[DEVICE_MODEL_KEY.M3DCamera]: 'M3D Camera',
[DEVICE_MODEL_KEY.M3TDCamera]: 'M3TD Camera',
// rc // rc
[DEVICE_MODEL_KEY.RC]: 'RC', [DEVICE_MODEL_KEY.RC]: 'RC',
@ -134,6 +148,7 @@ export const DEVICE_NAME = {
// dock // dock
[DEVICE_MODEL_KEY.Dock]: 'Dock', [DEVICE_MODEL_KEY.Dock]: 'Dock',
[DEVICE_MODEL_KEY.Dock2]: 'Dock2',
} }
// 控制权 // 控制权
@ -293,9 +308,12 @@ export enum NetworkStateTypeEnum {
} }
export enum NetworkStateQualityEnum { export enum NetworkStateQualityEnum {
BAD = 0, NO_SIGNAL = 0,
MEDIUM = 1, BAD = 1,
GOOD = 2 POOR = 2,
FAIR = 3,
GOOD = 4,
EXCELLENT = 5,
} }
export enum RainfallEnum { export enum RainfallEnum {
@ -319,7 +337,7 @@ export interface DockBasicOsd {
state: number, state: number,
capacity_percent: number, capacity_percent: number,
}, },
drone_in_dock: DroneInDockEnum, drone_in_dock: boolean,
rainfall: RainfallEnum, rainfall: RainfallEnum,
wind_speed: number, wind_speed: number,
environment_temperature: number, environment_temperature: number,
@ -379,7 +397,7 @@ export interface DockLinkOsd {
media_file_detail?: { media_file_detail?: {
remain_upload: number remain_upload: number
}, },
sdr: { sdr?: {
up_quality: string, up_quality: string,
down_quality: string, down_quality: string,
frequency_band: number, frequency_band: number,
@ -463,29 +481,6 @@ export enum EGear {
T T
} }
export enum EDeviceType {
M30 = '0-67-0' as any,
M30T = '0-67-1' as any,
M300 = '0-60-0' as any,
M350 = DEVICE_MODEL_KEY.M350 as any,
Z30 = '1-20-0' as any,
XT2 = '1-26-0' as any,
FPV = '1-39-0' as any,
XTS = '1-41-0' as any,
H20 = '1-42-0' as any,
H20T = '1-43-0' as any,
P1 = '1-50-65535' as any,
M30_Camera = '1-52-0' as any,
M30T_Camera = '1-53-0' as any,
H20N = '1-61-0' as any,
DJI_Dock_Camera = '1-165-0' as any,
L1 = '1-90742-0' as any,
M3E = '0-77-0' as any,
M3D = '0-77-1' as any,
M3E_Camera = '1-66-0' as any,
M3T_Camera = '1-67-0' as any,
}
export enum EDockModeCode { export enum EDockModeCode {
Disconnected = -1, Disconnected = -1,
Idle, Idle,

17
src/types/drone-control.ts

@ -1,5 +1,5 @@
import { ControlSource } from './device' import { ControlSource } from './device'
import { LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone' import { ECommanderModeLostAction, ERthMode, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone'
export enum ControlSourceChangeType { export enum ControlSourceChangeType {
Flight = 1, Flight = 1,
@ -52,6 +52,21 @@ export const LostControlActionInCommandFLightOptions = [
{ label: 'Landing', value: LostControlActionInCommandFLight.Land } { label: 'Landing', value: LostControlActionInCommandFLight.Land }
] ]
export const RthModeInCommandFlightOptions = [
{ label: 'Smart Height', value: ERthMode.SMART },
{ label: 'Setting Height', value: ERthMode.SETTING }
]
export const CommanderModeLostActionInCommandFlightOptions = [
{ label: 'Continue', value: ECommanderModeLostAction.CONTINUE },
{ label: 'Execute Lost Action', value: ECommanderModeLostAction.EXEC_LOST_ACTION }
]
export const CommanderFlightModeInCommandFlightOptions = [
{ label: 'Smart Height', value: ERthMode.SMART },
{ label: 'Setting Height', value: ERthMode.SETTING }
]
// 云台重置模式 // 云台重置模式
export enum GimbalResetMode { export enum GimbalResetMode {
Recenter = 0, Recenter = 0,

6
src/types/enums.ts

@ -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 {

80
src/types/flight-area.ts

@ -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',
},
}

3
src/types/map-enum.ts

@ -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',
} }

3
src/types/task.ts

@ -4,16 +4,19 @@ import { commonColor } from '/@/utils/color'
export enum TaskType { export enum TaskType {
Immediate = 0, // 立即执行 Immediate = 0, // 立即执行
Timed = 1, // 单次定时任务 Timed = 1, // 单次定时任务
Condition = 2,
} }
export const TaskTypeMap = { export const TaskTypeMap = {
[TaskType.Immediate]: 'Immediate', [TaskType.Immediate]: 'Immediate',
[TaskType.Timed]: 'Timed', [TaskType.Timed]: 'Timed',
[TaskType.Condition]: 'Continuous',
} }
export const TaskTypeOptions = [ export const TaskTypeOptions = [
{ value: TaskType.Immediate, label: TaskTypeMap[TaskType.Immediate] }, { value: TaskType.Immediate, label: TaskTypeMap[TaskType.Immediate] },
{ value: TaskType.Timed, label: TaskTypeMap[TaskType.Timed] }, { value: TaskType.Timed, label: TaskTypeMap[TaskType.Timed] },
{ value: TaskType.Condition, label: TaskTypeMap[TaskType.Condition] },
] ]
// 失控动作 // 失控动作

27
src/utils/genjson.ts

@ -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,
},
}
}

7
src/utils/map-layer-utils.ts

@ -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)
}

2
src/utils/time.ts

@ -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 时间戳 (秒)

2
src/vendors/jswebrtc.min.js vendored

File diff suppressed because one or more lines are too long

687
src/vendors/srs.sdk.js vendored

@ -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,
}

6
src/websocket/index.ts

@ -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

5
tsconfig.json

@ -24,6 +24,7 @@
"src/**/*.ts", "src/**/*.ts",
"src/**/*.d.ts", "src/**/*.d.ts",
"src/**/*.tsx", "src/**/*.tsx",
"src/**/*.vue" "src/**/*.vue",
, "src/vendors/coordtransform.js" ] "src/vendors/coordtransform.js"
]
} }

452
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save