| | |
| | | <div class="detail-title">绘制区域列表</div> |
| | | <div class="command-table-container"> |
| | | <div class="command-table-content"> |
| | | <el-table class="command-table" :data="shapeList" row-key="id"> |
| | | <el-table class="command-table" :data="shapeList" row-key="id" height="240"> |
| | | <el-table-column prop="areaType" label="区域类型"> |
| | | <template v-slot="{ row }"> |
| | | {{ resolveShapeAreaTypeLabel(row) }} |
| | |
| | | <div class="command-table-container"> |
| | | <div class="command-table-content"> |
| | | <el-table class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id" |
| | | @row-click="handleDeviceRowClick"> |
| | | @row-click="handleDeviceRowClick" height="240"> |
| | | <el-table-column prop="deviceName" label="设备名称" /> |
| | | <el-table-column prop="deviceType" label="类型"> |
| | | <template v-slot="{ row }"> |
| | |
| | | </div> |
| | | |
| | | <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly" |
| | | label-width="96px"> |
| | | label-width="116px"> |
| | | <el-form-item label="区域名称" prop="areaName"> |
| | | <el-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入" |
| | | clearable /> |
| | |
| | | |
| | | <div class="shape-table-container"> |
| | | <div class="shape-table-title">绘制区域列表</div> |
| | | <el-table class="command-table" :data="shapeList" row-key="id" height="auto"> |
| | | <el-table class="command-table" :data="shapeList" row-key="id" height="240"> |
| | | <el-table-column prop="areaType" label="区域类型"> |
| | | <template v-slot="{ row }"> |
| | | {{ resolveShapeAreaTypeLabel(row) }} |
| | |
| | | {{ drawTypeLabelMap[row.drawType] || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" class-name="operation-btns" width="120"> |
| | | <el-table-column label="操作" class-name="operation-btns" fixed="right" width="120"> |
| | | <template v-slot="{ row }"> |
| | | <el-link @click="handleShapeEdit(row)">编辑</el-link> |
| | | <el-link @click="handleShapeDelete(row)">删除</el-link> |
| | |
| | | <el-form-item prop="deviceIds" label-width="0"> |
| | | <el-table class="command-table" ref="deviceTableRef" |
| | | :data="deviceOptions.filter(item => item.deviceName.includes(searchName))" row-key="id" |
| | | @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick"> |
| | | @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick" height="240"> |
| | | <el-table-column type="selection" width="55" :reserve-selection="true" /> |
| | | <el-table-column prop="deviceName" label="设备名称" /> |
| | | <el-table-column prop="deviceType" label="类型"> |
| | |
| | | import ellipseIcon from '@/assets/images/areaMap/ellipse.png' |
| | | import bufferIcon from '@/assets/images/areaMap/buffer.png' |
| | | import trashIcon from '@/assets/images/areaMap/trash.png' |
| | | import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png' |
| | | |
| | | const initForm = () => ({ |
| | | areaName: '', // 区域名称 |
| | |
| | | let viewEntity |
| | | let shapeDisplaySource |
| | | const deviceRangePrimitiveMap = new Map() |
| | | const deviceIconEntityMap = new Map() |
| | | const activeToolMode = ref('') |
| | | const suppressDeviceFly = ref(false) |
| | | const isDialogInitializing = ref(false) |
| | |
| | | let deviceRangeRenderToken = 0 |
| | | let deviceListTimer = null |
| | | let deviceListRequestToken = 0 |
| | | let deviceMapRequestToken = 0 |
| | | |
| | | const rules = { |
| | | areaName: fieldRules(true, 50), |
| | |
| | | hideTypePanel() |
| | | clearShapeDisplay() |
| | | clearDeviceRangePrimitives() |
| | | clearDeviceIconEntities() |
| | | } |
| | | } |
| | | ) |
| | |
| | | deviceRangePrimitiveMap.clear() |
| | | } |
| | | |
| | | function clearDeviceIconEntities () { |
| | | if (!viewer || !deviceIconEntityMap.size) return |
| | | for (const entity of deviceIconEntityMap.values()) { |
| | | viewer.entities.remove(entity) |
| | | } |
| | | deviceIconEntityMap.clear() |
| | | } |
| | | |
| | | function clearDeviceListState () { |
| | | deviceOptions.value = [] |
| | | selectedDeviceRows.value = [] |
| | |
| | | function isSameWktPoint (left, right) { |
| | | if (!left || !right) return false |
| | | return Math.abs(left.lat - right.lat) < 0.000001 && Math.abs(left.lng - right.lng) < 0.000001 |
| | | } |
| | | |
| | | function normalizePolygonPoints (points) { |
| | | if (!Array.isArray(points)) return [] |
| | | const ring = points.filter(point => point && Number.isFinite(point.lng) && Number.isFinite(point.lat)) |
| | | if (ring.length < 2) return ring |
| | | const first = ring[0] |
| | | const last = ring[ring.length - 1] |
| | | if (isSameWktPoint(first, last)) { |
| | | return ring.slice(0, -1) |
| | | } |
| | | return ring |
| | | } |
| | | |
| | | function isSelfIntersectingPolygon (points) { |
| | | const ring = normalizePolygonPoints(points) |
| | | if (ring.length < 4) return false |
| | | const epsilon = 1e-12 |
| | | const orientation = (a, b, c) => { |
| | | const value = (b.lat - a.lat) * (c.lng - b.lng) - (b.lng - a.lng) * (c.lat - b.lat) |
| | | if (Math.abs(value) < epsilon) return 0 |
| | | return value > 0 ? 1 : 2 |
| | | } |
| | | const onSegment = (a, b, c) => |
| | | b.lng <= Math.max(a.lng, c.lng) + epsilon && |
| | | b.lng + epsilon >= Math.min(a.lng, c.lng) && |
| | | b.lat <= Math.max(a.lat, c.lat) + epsilon && |
| | | b.lat + epsilon >= Math.min(a.lat, c.lat) |
| | | const segmentsIntersect = (p1, q1, p2, q2) => { |
| | | const o1 = orientation(p1, q1, p2) |
| | | const o2 = orientation(p1, q1, q2) |
| | | const o3 = orientation(p2, q2, p1) |
| | | const o4 = orientation(p2, q2, q1) |
| | | if (o1 !== o2 && o3 !== o4) return true |
| | | if (o1 === 0 && onSegment(p1, p2, q1)) return true |
| | | if (o2 === 0 && onSegment(p1, q2, q1)) return true |
| | | if (o3 === 0 && onSegment(p2, p1, q2)) return true |
| | | if (o4 === 0 && onSegment(p2, q1, q2)) return true |
| | | return false |
| | | } |
| | | |
| | | const count = ring.length |
| | | for (let i = 0; i < count; i += 1) { |
| | | const p1 = ring[i] |
| | | const q1 = ring[(i + 1) % count] |
| | | for (let j = i + 1; j < count; j += 1) { |
| | | const p2 = ring[j] |
| | | const q2 = ring[(j + 1) % count] |
| | | const isAdjacent = j === i || j === i + 1 || (i === 0 && j === count - 1) |
| | | if (isAdjacent) continue |
| | | if (segmentsIntersect(p1, q1, p2, q2)) return true |
| | | } |
| | | } |
| | | return false |
| | | } |
| | | |
| | | function buildPolygonWktFromPoints (points) { |
| | |
| | | return polygons |
| | | } |
| | | |
| | | async function renderDeviceRangeSingle (device) { |
| | | if (!viewer) return |
| | | clearDeviceRangePrimitives() |
| | | const position = getDeviceLngLat(device) |
| | | if (!position) return |
| | | const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm) |
| | | const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters) |
| | | if (!primitive) return |
| | | deviceRangePrimitiveMap.set(String(device?.id ?? Date.now()), primitive) |
| | | viewer.scene.primitives.add(primitive) |
| | | } |
| | | |
| | | async function renderDeviceRanges (rows) { |
| | | if (!viewer) return |
| | | deviceRangeRenderToken += 1 |
| | |
| | | deviceRangePrimitiveMap.delete(id) |
| | | } |
| | | } |
| | | for (const [id, entity] of deviceIconEntityMap.entries()) { |
| | | if (!nextIds.has(id)) { |
| | | viewer.entities.remove(entity) |
| | | deviceIconEntityMap.delete(id) |
| | | } |
| | | } |
| | | const pendingRows = selectedRows.filter(row => { |
| | | const id = String(row?.id) |
| | | return id && !deviceRangePrimitiveMap.has(id) |
| | |
| | | const primitives = await Promise.all( |
| | | pendingRows.map(async row => { |
| | | const position = getDeviceLngLat(row) |
| | | if (!position) return { row, primitive: null } |
| | | if (!position) return { row, primitive: null, position: null } |
| | | const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm) |
| | | const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters) |
| | | return { row, primitive } |
| | | return { row, primitive, position } |
| | | }) |
| | | ) |
| | | if (renderToken !== deviceRangeRenderToken) return |
| | | primitives.forEach(({ row, primitive }) => { |
| | | primitives.forEach(({ row, primitive, position }) => { |
| | | if (position) { |
| | | addDeviceIconEntity(row, position) |
| | | } |
| | | if (!primitive) return |
| | | const id = String(row?.id) |
| | | if (!nextIds.has(id)) return |
| | |
| | | function clearShapeDisplay () { |
| | | if (!shapeDisplaySource) return |
| | | shapeDisplaySource.entities.removeAll() |
| | | } |
| | | |
| | | function addDeviceIconEntity (device, position) { |
| | | if (!viewer || !position) return |
| | | const id = String(device?.id ?? '') |
| | | if (!id || deviceIconEntityMap.has(id)) return |
| | | const entity = viewer.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(position.lng, position.lat, position.height || 0), |
| | | billboard: { |
| | | image: equipmentIcon, |
| | | width: 28, |
| | | height: 38, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | }) |
| | | deviceIconEntityMap.set(id, entity) |
| | | } |
| | | |
| | | function normalizeShapePoint (point) { |
| | |
| | | const val = cartesian3Convert(item, viewer) |
| | | return { ...val, lng: val.longitude, lat: val.latitude } |
| | | }) |
| | | if (currentShapeType.value === 'polygon' && isSelfIntersectingPolygon(pointList)) { |
| | | ElMessage.warning('多边形存在自相交,请调整后再保存') |
| | | if (activeToolMode.value === 'draw') { |
| | | pointList = [] |
| | | activeShapeId.value = null |
| | | clearActiveTool() |
| | | startDraw('polygon') |
| | | } |
| | | return |
| | | } |
| | | const controlPoints = resolveControlPoints(meta, currentShapeType.value) |
| | | if (!activeAreaType.value) { |
| | | const detectAreaType = getDetectAreaTypeKey() |
| | |
| | | suppressDeviceFly.value = true |
| | | syncDeviceSelection() |
| | | suppressDeviceFly.value = prevSuppressDeviceFly |
| | | if (!readonly.value) { |
| | | renderDeviceRanges(selectedDeviceRows.value) |
| | | if (readonly.value) { |
| | | await renderDeviceRanges(deviceOptions.value) |
| | | } |
| | | } |
| | | |
| | | // 设备上图(新增/编辑用) |
| | | async function getDeviceListForMapDisplay () { |
| | | if (!visible.value || !viewer) return |
| | | deviceMapRequestToken += 1 |
| | | const requestToken = deviceMapRequestToken |
| | | const res = await fwDeviceListByPolygonsApi({ |
| | | areaId: dialogMode.value === 'add' ? '' : formData.value.id, |
| | | isAreaSelect: 1, |
| | | isTrack: 1, |
| | | }) |
| | | if (requestToken !== deviceMapRequestToken) return |
| | | const list = res?.data?.data ?? [] |
| | | if (!list.length) { |
| | | clearDeviceRangePrimitives() |
| | | clearDeviceIconEntities() |
| | | return |
| | | } |
| | | await renderDeviceRanges(list) |
| | | } |
| | | |
| | | // 关联设备变更 |
| | |
| | | selectedDeviceRows.value = rows |
| | | const ids = rows.map(item => item.id) |
| | | formData.value.deviceIds = ids.join(',') |
| | | renderDeviceRanges(rows) |
| | | if (suppressDeviceFly.value) { |
| | | lastSelectedDeviceIds = [...ids] |
| | | return |
| | |
| | | |
| | | function handleDeviceRowClick (row) { |
| | | if (readonly.value) { |
| | | renderDeviceRangeSingle(row) |
| | | void flyToDevice(row) |
| | | return |
| | | } |
| | | renderDeviceRanges(selectedDeviceRows.value) |
| | | void flyToDevice(row) |
| | | const isSelected = selectedDeviceRows.value.some(item => String(item?.id) === String(row?.id)) |
| | | if (isSelected) { |
| | | void flyToDevice(row) |
| | | } |
| | | } |
| | | |
| | | // 同步选择状态 |
| | |
| | | }) |
| | | selectedDeviceRows.value = rows |
| | | formData.value.deviceIds = rows.map(item => item.id).join(',') |
| | | if (readonly.value) { |
| | | renderDeviceRanges(selectedDeviceRows.value) |
| | | return |
| | | } |
| | | if (!deviceTableRef.value) return |
| | | deviceTableRef.value.clearSelection() |
| | | rows.forEach(row => { |
| | |
| | | initMap() |
| | | clearShapeDisplay() |
| | | clearDeviceRangePrimitives() |
| | | clearDeviceIconEntities() |
| | | viewEntity && viewer?.entities?.remove(viewEntity) |
| | | viewEntity = null |
| | | clearActiveTool() |
| | |
| | | lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id) |
| | | await nextTick() |
| | | suppressDeviceFly.value = false |
| | | if (!readonly.value) { |
| | | renderDeviceRanges(selectedDeviceRows.value) |
| | | } |
| | | isDialogInitializing.value = false |
| | | queueDeviceListRefresh() |
| | | if (dialogMode.value === 'add' || dialogMode.value === 'edit') { |
| | | void getDeviceListForMapDisplay() |
| | | } |
| | | void getPoliceStationList() |
| | | } |
| | | |