<template>
|
<div :id="mapId" class="common-cesium-map" @contextmenu.prevent>
|
<template v-if="props.list && props.list.length">
|
<div class="card">
|
<div class="card-content">
|
<div class="card-item" v-for="(item, index) in props.list" @click="clickWeekTime(item, index)">
|
<div class="time-point">
|
<img
|
class="active"
|
v-if="index === selectedImgIndex"
|
src="@/assets/images/common/point-active.png"
|
alt=""
|
/>
|
<img v-else src="@/assets/images/common/point.png" alt="" />
|
</div>
|
<div class="time-bottom">{{ item.createTime.slice(11) }}</div>
|
</div>
|
|
</div>
|
</div>
|
<div class="time-line"></div>
|
</template>
|
</div>
|
</template>
|
|
<script setup>
|
import * as Cesium from 'cesium'
|
|
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
import { PublicCesium } from '@/utils/cesium/publicCesium'
|
import { loadJaAdminBoundary, removeJaAdminBoundary } from '@/utils/cesium/adminBoundary'
|
import positionIcon from '@/assets/images/dataCenter/positionicon.png'
|
|
const props = defineProps({
|
active: {
|
type: Boolean,
|
default: true,
|
},
|
domId: {
|
type: String,
|
default: '',
|
},
|
flatMode: {
|
type: Boolean,
|
default: false,
|
},
|
terrain: {
|
type: Boolean,
|
default: true,
|
},
|
layerMode: {
|
type: Number,
|
default: 4,
|
},
|
boundary: {
|
type: Boolean,
|
default: false,
|
},
|
contour: {
|
type: Boolean,
|
default: false,
|
},
|
showAdminBoundary: {
|
type: Boolean,
|
default: true,
|
},
|
zoomToBoundary: {
|
type: Boolean,
|
default: true,
|
},
|
enableStageEmit: {
|
type: Boolean,
|
default: false,
|
},
|
clusterHeight: {
|
type: Number,
|
default: 100000,
|
},
|
detailHeight: {
|
type: Number,
|
default: 10000,
|
},
|
list: {
|
type: Array,
|
default: () => [],
|
},
|
})
|
|
const emit = defineEmits(['ready', 'height-change', 'stage-change', 'point-change'])
|
const mapId = props.domId || `common-cesium-map-${Math.random().toString(36).slice(2, 10)}`
|
let viewInstance = null
|
let viewer = null
|
const ADMIN_BOUNDARY_NAMES = {
|
boundary: 'jaBoundarySource',
|
line: 'jaBoundaryLineSource',
|
label: 'jaBoundaryLabelSource',
|
}
|
let adminBoundarySources = null
|
let initialized = false
|
let removeCameraListener = null
|
|
const initMap = async () => {
|
if (initialized) return
|
initialized = true
|
viewInstance = new PublicCesium({
|
dom: mapId,
|
flatMode: props.flatMode,
|
terrain: props.terrain,
|
layerMode: props.layerMode,
|
boundary: props.boundary,
|
contour: props.contour,
|
})
|
viewer = viewInstance.getViewer()
|
if (props.showAdminBoundary) {
|
adminBoundarySources = await loadJaAdminBoundary(viewer, {
|
zoomTo: props.zoomToBoundary,
|
})
|
}
|
if (props.enableStageEmit) {
|
let lastStage = ''
|
const onCameraChanged = () => {
|
const height = viewer?.camera?.positionCartographic?.height
|
if (height == null) return
|
emit('height-change', height)
|
let stage = 'mid'
|
if (height >= props.clusterHeight) stage = 'cluster'
|
if (height <= props.detailHeight) stage = 'detail'
|
if (stage !== lastStage) {
|
lastStage = stage
|
emit('stage-change', stage)
|
}
|
}
|
viewer?.camera?.changed?.addEventListener(onCameraChanged)
|
removeCameraListener = () => viewer?.camera?.changed?.removeEventListener(onCameraChanged)
|
}
|
emit('ready', { viewer, publicCesium: viewInstance })
|
// 地图就绪时,若列表已加载则默认标记选中项(首个)
|
showSelectedMarker()
|
}
|
|
const selectedImgIndex = ref(0)
|
// 航线高度(与 TrajectoryDiaLog 里 MOCK_FLIGHT_HEIGHT 保持一致),让标记悬浮在轨迹线上
|
const FLIGHT_HEIGHT = 120
|
// 当前经纬度位置标记
|
let positionEntity = null
|
// 点击周期
|
const clickWeekTime = (item, index) => {
|
// clickImgSrc.value = item.url
|
selectedImgIndex.value = index
|
showPositionMarker(item)
|
// 通知父组件当前选中的轨迹点,用于右侧表格同步高亮
|
emit('point-change', { item, index })
|
}
|
|
// 在地图上通过经纬度显示位置图标
|
const showPositionMarker = item => {
|
if (!viewer) return
|
const longitude = Number(item?.longitude)
|
const latitude = Number(item?.latitude)
|
if (Number.isNaN(longitude) || Number.isNaN(latitude)) return
|
|
// 与航线相同的高度,让标记悬浮在轨迹线上,而不是贴地
|
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, FLIGHT_HEIGHT)
|
if (positionEntity) {
|
// 已存在则移动到新的位置
|
positionEntity.position = position
|
} else {
|
positionEntity = viewer.entities.add({
|
position,
|
billboard: {
|
image: positionIcon,
|
width: 36,
|
height: 36,
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
},
|
})
|
}
|
}
|
|
// 显示当前选中项的标记(默认首个),地图就绪和列表加载后都会调用
|
const showSelectedMarker = () => {
|
const item = props.list?.[selectedImgIndex.value]
|
if (item) showPositionMarker(item)
|
}
|
|
// 列表数据到位后,默认选中并标记首个点
|
watch(
|
() => props.list,
|
list => {
|
if (list && list.length) {
|
selectedImgIndex.value = 0
|
showSelectedMarker()
|
// 默认选中首个,同步通知父组件高亮表格首行
|
emit('point-change', { item: list[0], index: 0 })
|
}
|
}
|
)
|
|
watch(
|
() => props.active,
|
active => {
|
if (active) initMap()
|
}
|
)
|
|
onMounted(() => {
|
if (props.active) initMap()
|
})
|
|
onBeforeUnmount(() => {
|
if (removeCameraListener) removeCameraListener()
|
removeJaAdminBoundary(viewer, adminBoundarySources)
|
adminBoundarySources = null
|
positionEntity = null
|
viewInstance?.viewerDestroy?.()
|
})
|
|
const setAdminBoundaryVisible = async visible => {
|
if (!viewer) return
|
const removeByNames = () => {
|
Object.values(ADMIN_BOUNDARY_NAMES).forEach(name => {
|
const sources = viewer.dataSources.getByName(name) || []
|
sources.forEach(source => viewer.dataSources.remove(source))
|
})
|
}
|
const resolveSources = () => {
|
if (adminBoundarySources) return adminBoundarySources
|
const boundarySource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.boundary)?.[0] ?? null
|
const lineSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.line)?.[0] ?? null
|
const labelSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.label)?.[0] ?? null
|
if (boundarySource || lineSource || labelSource) {
|
adminBoundarySources = { boundarySource, lineSource, labelSource }
|
}
|
return adminBoundarySources
|
}
|
const setVisible = (source, next) => {
|
if (!source) return
|
source.show = next
|
source.entities?.values?.forEach(entity => {
|
entity.show = next
|
})
|
}
|
const sources = resolveSources()
|
if (!visible) {
|
if (sources) {
|
removeJaAdminBoundary(viewer, sources)
|
adminBoundarySources = null
|
}
|
removeByNames()
|
return
|
}
|
if (sources) {
|
const { boundarySource, lineSource, labelSource } = sources
|
setVisible(boundarySource, true)
|
setVisible(lineSource, true)
|
setVisible(labelSource, true)
|
return
|
}
|
adminBoundarySources = await loadJaAdminBoundary(viewer, {
|
zoomTo: false,
|
})
|
}
|
|
const zoomToAdminBoundary = async () => {
|
if (!viewer) return
|
const resolveSources = () => {
|
if (adminBoundarySources) return adminBoundarySources
|
const boundarySource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.boundary)?.[0] ?? null
|
const lineSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.line)?.[0] ?? null
|
const labelSource = viewer.dataSources.getByName(ADMIN_BOUNDARY_NAMES.label)?.[0] ?? null
|
if (boundarySource || lineSource || labelSource) {
|
adminBoundarySources = { boundarySource, lineSource, labelSource }
|
}
|
return adminBoundarySources
|
}
|
let sources = resolveSources()
|
if (!sources?.boundarySource) {
|
sources = await loadJaAdminBoundary(viewer, { zoomTo: false })
|
adminBoundarySources = sources
|
}
|
const target = sources?.boundarySource
|
if (!target) return
|
await viewer.flyTo(target, {
|
duration: 0,
|
offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-75), 0),
|
})
|
}
|
|
const getMap = () => ({ viewer, publicCesium: viewInstance })
|
const getViewer = () => viewer
|
const getPublicCesium = () => viewInstance
|
|
const flyToPoint = (options) => {
|
const { longitude, latitude, height } = options.position
|
|
viewer.camera.setView({
|
destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
|
orientation: {
|
heading: Cesium.Math.toRadians(0), // 方向
|
pitch: Cesium.Math.toRadians(-90), // 仰角
|
roll: Cesium.Math.toRadians(0) // 翻滚角
|
}
|
})
|
}
|
|
defineExpose({ getMap, getViewer, getPublicCesium, flyToPoint, setAdminBoundaryVisible, zoomToAdminBoundary })
|
</script>
|
|
<style scoped lang="scss">
|
.common-cesium-map {
|
width: 100%;
|
height: 100%;
|
position: relative;
|
.card {
|
position: absolute;
|
bottom: 0px;
|
width: 100%;
|
height: 67px;
|
background: rgba(0, 0, 0, 0.42);
|
border-radius: 0px 0px 20px 20px;
|
padding-left: 6px;
|
// padding-right: 6px;
|
// border: 1px solid red;
|
|
color: #d5d5d5;
|
font-size: 14px;
|
|
.card-content {
|
display: flex;
|
justify-content: center;
|
position: absolute;
|
|
width: calc(100% - 6px - 6px);
|
height: 100%;
|
overflow-x: scroll;
|
overflow-y: hidden;
|
white-space: nowrap;
|
.card-item {
|
cursor: pointer;
|
}
|
|
& > div {
|
position: relative;
|
}
|
|
.time-top {
|
margin-top: 10px;
|
line-height: 1;
|
text-align: center;
|
cursor: pointer;
|
}
|
|
.active {
|
font-weight: 400;
|
color: #ffffff;
|
}
|
|
.time-point {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
position: absolute;
|
top: 22px;
|
left: 50%;
|
width: 18px;
|
height: 18px;
|
z-index: 1;
|
transform: translate(-50%, 0);
|
|
img {
|
width: 11.2px;
|
height: 11.2px;
|
}
|
|
img.active {
|
width: 16.89px;
|
height: 16.89px;
|
}
|
}
|
|
.time-bottom {
|
// border: 1px solid red;
|
margin-top: 44px;
|
margin-left: 8px;
|
&:first-child {
|
margin-left: 0;
|
}
|
}
|
|
// .time-line {
|
// // position: absolute;
|
// // top: 35px;
|
// // width: 100%;
|
// position: fixed;
|
// bottom: 96px;
|
// width: 94%;
|
// border: 1px solid rgba(237, 237, 237, 0.6);
|
// z-index: 0;
|
// }
|
}
|
|
}
|
.time-line {
|
position: absolute;
|
// top: 35px;
|
width: 100%;
|
// position: fixed;
|
bottom: 36px;
|
width: 100%;
|
border: 1px solid rgba(237, 237, 237, 0.6);
|
z-index: 0;
|
}
|
}
|
</style>
|