图片上叠加轨迹

This commit is contained in:
wangxk 2025-07-29 15:21:32 +08:00
parent bc86834992
commit 3f6ed98bbb
3 changed files with 373 additions and 16 deletions

View File

@ -245,4 +245,12 @@ export function confirmDeleteNodes(params:any){
method:'post', method:'post',
params:params, params:params,
}) })
}
//获取文件相关的属性
export function listTsFilesById(params:any){
return request ({
url:'/experimentalData/ts-files/listTsFilesById',
method:'post',
params:params,
})
} }

View File

@ -0,0 +1,233 @@
<template>
<div class="map-container">
<svg ref="svgRef" class="trajectory-svg" width="800" height="600">
<!-- <image> 放入 zoom-container 使其与轨迹同步缩放 -->
<g class="zoom-container">
<image
:xlink:href="imageUrl"
width="800"
height="600"
/>
</g>
</svg>
</div>
</template>
<script setup>
import { ref, onMounted, watch, defineProps, defineEmits, onBeforeUnmount } from 'vue'
import * as d3 from 'd3'
// === Props ===
const props = defineProps({
imageUrl: { type: String, required: true },
bounds: {
type: Array,
required: true,
default: () => [0, 0, 1, 1] // [minLng, minLat, maxLng, maxLat]
},
trajectory: {
type: Array,
required: true,
default: () => []
},
qvehuan: {
type: Boolean,
default: false
}
})
// === Emits ===
const emit = defineEmits(['trajectoryComplete'])
// === DOM ===
const svgRef = ref(null)
const xScale = ref(null)
const yScale = ref(null)
const zoomTransform = ref(d3.zoomIdentity)
// === ===
const initMap = () => {
const [minLng, minLat, maxLng, maxLat] = props.bounds
xScale.value = d3.scaleLinear()
.domain([minLng, maxLng])
.range([0, 800])
yScale.value = d3.scaleLinear()
.domain([minLat, maxLat])
.range([600, 0])
initZoom()
updateTrajectory()
}
// === ===
const initZoom = () => {
const mapZoom = d3.zoom()
.scaleExtent([0.5, 3]) //
.translateExtent([[-800, -600], [1600, 1200]]) //
.on("zoom", (event) => {
d3.select(svgRef.value).select('.zoom-container')
.attr("transform", event.transform)
zoomTransform.value = event.transform
})
d3.select(svgRef.value).call(mapZoom)
}
// === ===
const updateTrajectory = () => {
if (!props.trajectory?.length || !xScale.value || !yScale.value) return
if (props.qvehuan) {
clearOldElements()
}
if (shouldRedraw()) {
renderTrajectoryPath()
renderTrajectoryMarkers()
}
}
//
const clearOldElements = () => {
d3.select(svgRef.value).select('.zoom-container')
.selectAll('.trajectory-path, .marker-circle')
.remove()
}
//
const renderTrajectoryPath = () => {
const points = props.trajectory.map(([lng, lat]) => [
xScale.value(lng),
yScale.value(lat)
])
const path = d3.line()
.x(d => d[0])
.y(d => d[1])
d3.select(svgRef.value).select('.zoom-container')
.append('path')
.attr('class', 'trajectory-path')
.attr('d', path(points))
.attr('stroke', '#1890FF')
.attr('stroke-width', 2.5)
.attr('fill', 'none')
.attr('stroke-dasharray', function() {
return this.getTotalLength()
})
.attr('stroke-dashoffset', function() {
return this.getTotalLength()
})
.transition()
.duration(1500)
.attr('stroke-dashoffset', 0)
.on('end', () => {
if (props.qvehuan) {
emit('trajectoryComplete')
}
})
}
//
const renderTrajectoryMarkers = () => {
props.trajectory.forEach(([lng, lat], index) => {
const x = xScale.value(lng)
const y = yScale.value(lat)
d3.select(svgRef.value).select('.zoom-container')
.append('circle')
.attr('class', `marker-circle marker-${index}`)
.attr('cx', x)
.attr('cy', y)
.attr('r', 2.5)
.attr('fill', '#FF4D4F')
.attr('stroke', '#fff')
.attr('stroke-width', 0.8)
.on('mouseover', function() {
d3.select(this)
.attr('r', 4)
.attr('stroke-width', 1.2)
})
.on('mouseout', function() {
d3.select(this)
.attr('r', 2.5)
.attr('stroke-width', 0.8)
})
})
}
//
const shouldRedraw = () => {
if (props.qvehuan && props.trajectory.length > 0) {
return true
}
return props.trajectory.length > 0
}
// === ===
watch(() => props.trajectory, (newVal) => {
if (newVal.length) {
updateTrajectory()
}
}, { deep: true })
watch(() => props.qvehuan, (newVal) => {
if (newVal) {
clearOldElements()
}
})
onMounted(() => {
const imageElement = d3.select(svgRef.value).select('image')
if (imageElement) {
imageElement.on('load', initMap)
}
})
onBeforeUnmount(() => {
d3.select(svgRef.value).selectAll('*').remove()
})
</script>
<style scoped>
.map-container {
position: relative;
width: 800px;
height: 600px;
overflow: hidden;
margin-top: 10px;
}
.trajectory-svg {
position: absolute;
top: 0;
left: 0;
z-index: 1;
cursor: grab;
}
.trajectory-svg:active {
cursor: grabbing;
}
/* 轨迹路径样式 */
.trajectory-path {
stroke-linecap: round;
stroke-linejoin: round;
transition: all 0.3s ease;
stroke-dashoffset: 0;
}
/* 标记点样式 */
.marker-circle {
transition: all 0.2s ease;
cursor: pointer;
}
.marker-circle:hover {
transform: scale(1.5);
stroke-width: 1.2px;
}
</style>

View File

@ -13,7 +13,7 @@ import { ElMessageBox, ElMessage, ElMain } from "element-plus";
import Page from '@/components/Pagination/page.vue'; import Page from '@/components/Pagination/page.vue';
import AudioPlayer from '@/components/file/preview/AudioPlayer.vue'; import AudioPlayer from '@/components/file/preview/AudioPlayer.vue';
import { batchDeleteReq } from "@/api/file-operator"; import { batchDeleteReq } from "@/api/file-operator";
import { tstaskList, obtaintestData, decompressionFolderData, getTsNodesTree, confirmDeleteNodes, addTsNodes, selectTsNodesByTskeId, updateTsNodes, deleteTsNodesById, tsFilesPage, addTsFiles, testDataScanById, updateTsFiles, deleteTsFilesById, listTsFiles, deleteTsFilesByIds, compress, Decompression, compare, downloadToLocal, uploadToBackup, addTsFile, list, moveFileFolder, copyFileFolder, startSimpleNavi, stopSimpleNavi } from "@/api/datamanagement"; import { tstaskList, obtaintestData, decompressionFolderData, getTsNodesTree, confirmDeleteNodes, addTsNodes, selectTsNodesByTskeId, updateTsNodes, deleteTsNodesById, tsFilesPage, addTsFiles, testDataScanById, updateTsFiles, deleteTsFilesById, listTsFiles, deleteTsFilesByIds, compress, Decompression, compare, downloadToLocal, uploadToBackup, addTsFile, list, moveFileFolder, copyFileFolder, startSimpleNavi, stopSimpleNavi, listTsFilesById } from "@/api/datamanagement";
import ZUpload from '@/components/file/ZUpload.vue' import ZUpload from '@/components/file/ZUpload.vue'
import useFileUpload from "@/components/file/file/useFileUpload"; import useFileUpload from "@/components/file/file/useFileUpload";
import useHeaderStorageList from "@/components/header/useHeaderStorageList"; import useHeaderStorageList from "@/components/header/useHeaderStorageList";
@ -24,6 +24,7 @@ import txtexl from '@/components/textEditing/txtexl.vue'
// //
import MapChart from '@/components/trajectory/index.vue'; import MapChart from '@/components/trajectory/index.vue';
import Echart from '@/components/trajectory/echarts.vue'; import Echart from '@/components/trajectory/echarts.vue';
import Imggui from '@/components/trajectory/imggui.vue';
// //
import PDFImg from '@/assets/fileimg/PDF.png' import PDFImg from '@/assets/fileimg/PDF.png'
import WordImg from '@/assets/fileimg/word.png' import WordImg from '@/assets/fileimg/word.png'
@ -203,16 +204,16 @@ function readyto2(msg: any) {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
} }
).then(() => { ).then(() => {
gai.value = true gai.value = true
getdata() getdata()
}) })
setTimeout(() => { setTimeout(() => {
ElMessageBox.close() ElMessageBox.close()
if(!gai.value){ if (!gai.value) {
getdata() getdata()
} }
}, 5000) }, 5000)
} }
@ -1401,7 +1402,7 @@ async function submitzip(formEl: any) {
Decompression({ id: jiezip.value.id, parentId: zipParentid.value, decompressionPath: zipObj.value.compressedPath, path: filepath.value, taskId: projectId.value }).then((res: any) => { Decompression({ id: jiezip.value.id, parentId: zipParentid.value, decompressionPath: zipObj.value.compressedPath, path: filepath.value, taskId: projectId.value }).then((res: any) => {
if (res.code == 0) { if (res.code == 0) {
// ElMessage.success('') // ElMessage.success('')
zipfiles.value = false zipfiles.value = false
tableIdarr.value.length = 0 tableIdarr.value.length = 0
filetableRef.value!.clearSelection() filetableRef.value!.clearSelection()
@ -1587,24 +1588,100 @@ const fileIcon = (row: any) => {
return FILE_ICONS[ext as keyof typeof FILE_ICONS] || (row.type == 'ZIP' ? ZipImg : TextImg); return FILE_ICONS[ext as keyof typeof FILE_ICONS] || (row.type == 'ZIP' ? ZipImg : TextImg);
}; };
const mapTrajectory = ref(false) const mapTrajectory = ref(false)
const fredid = ref('') const fredid = ref('')
const pngform: any = ref({})
const imgor = ref(true)
function openMap(row: any) { function openMap(row: any) {
pngobj.vlaue = {
pngurl: '',
textcontent: []
}
pngform.value = JSON.parse(JSON.stringify(row))
fredid.value = row.id fredid.value = row.id
getSSELink() getSSELink()
// setInterval(() => { getpngdata()
// lineData.value = lineData.value.map(d => ({ tabbas.value = '1'
// x: d.x,
// y: Math.random() * 10
// }));
// }, 2000);
mapTrajectory.value = true mapTrajectory.value = true
maptime.value = 300 maptime.value = 300
if (row.custom1) {
ElMessageBox.confirm(
'当前轨迹文件已有关联图片,是否直接加载轨迹?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
pngobj.value = JSON.parse(row.custom1)
imgor.value = false
})
}
}
const pngArr = ref([])
const txtArr = ref([])
const pngloading = ref(false)
function getpngdata() {
pngloading.value = true
listTsFilesById({ id: fredid.value, taskId: projectId.value, nodeId: pathid.value }).then((res: any) => {
pngloading.value = false
pngArr.value = res.data.tsFilesListPng
txtArr.value = res.data.tsFilesListTxt
})
}
const pngobj: any = ref({
pngurl: '',
textcontent: []
})
function pngsure() {
if (!(pngradio.value && txtradio.value)) {
ElMessage.warning('请选择底图或地理信息文件')
return
}
//
const newPngobj = {
pngurl: '',
textcontent: []
}
// URL
pngArr.value.forEach((item: any) => {
if (pngradio.value == item.id) {
newPngobj.pngurl = item.url
}
})
//
txtArr.value.forEach((item: any) => {
if (txtradio.value == item.id) {
newPngobj.textcontent = JSON.parse(JSON.stringify(item.fileContent)).split(",")
}
})
// 使
pngobj.value = {
...pngobj.value,
...newPngobj
}
//
pngform.value.custom1 = JSON.stringify(pngobj.value)
//
updateTsFiles(pngform.value).then((res: any) => { })
imgor.value = false
} }
function mapClose() { function mapClose() {
txtradio.value = null
pngradio.value = null
imgor.value = true
mapTrajectory.value = false mapTrajectory.value = false
lineData.value.length = 0 lineData.value.length = 0
dynamicCoordinates.value.length = 0 dynamicCoordinates.value.length = 0
imgarrdata.value.length = 0
closeSSE() closeSSE()
// index = 0 // index = 0
} }
@ -1634,6 +1711,7 @@ function frequency(row: any) {
qvehuan1.value = true qvehuan1.value = true
lineData.value.length = 0 lineData.value.length = 0
dynamicCoordinates.value.length = 0 dynamicCoordinates.value.length = 0
imgarrdata.value.length = 0
} }
}) })
} }
@ -1641,9 +1719,11 @@ function frequency(row: any) {
const eventSource = ref(null) const eventSource = ref(null)
let index = 0 let index = 0
const dynamicCoordinates = ref([]) const dynamicCoordinates = ref([])
const imgarrdata = ref([])
// let SSEclose // let SSEclose
function closeSSE() { function closeSSE() {
dynamicCoordinates.value.length = 0 dynamicCoordinates.value.length = 0
imgarrdata.value.length = 0
stopSimpleNavi({ token: userStore.userId }).then((res: any) => { stopSimpleNavi({ token: userStore.userId }).then((res: any) => {
}) })
eventSource.value?.close() eventSource.value?.close()
@ -1667,6 +1747,7 @@ function getSSELink() {
dynamicCoordinates.value.shift() dynamicCoordinates.value.shift()
} }
lineData.value.push({ x: data.UtcTime, y: data.alt }) lineData.value.push({ x: data.UtcTime, y: data.alt })
imgarrdata.value.push([data.lon, data.lat])
} }
} catch (err) { } catch (err) {
console.error('消息解析失败:', err) console.error('消息解析失败:', err)
@ -1720,7 +1801,7 @@ function repstring(row: any) {
* @returns 格式化后的文件大小字符串 * @returns 格式化后的文件大小字符串
*/ */
const formatFileSize = (size: number): string => { const formatFileSize = (size: number): string => {
if(!size) return '--' if (!size) return '--'
if (size < 1024) return `${size.toFixed(1)} KB`; if (size < 1024) return `${size.toFixed(1)} KB`;
const mb = size / 1024; const mb = size / 1024;
@ -1729,6 +1810,9 @@ const formatFileSize = (size: number): string => {
const gb = mb / 1024; const gb = mb / 1024;
return `${gb.toFixed(1)} GB`; return `${gb.toFixed(1)} GB`;
}; };
const txtradio: any = ref(null)
const pngradio: any = ref(null)
</script> </script>
<template> <template>
@ -1847,8 +1931,9 @@ const formatFileSize = (size: number): string => {
style="cursor: pointer;"> --> style="cursor: pointer;"> -->
<img src="@/assets/project/chong.png" alt="" title="重命名" @click="editfile(scope.row, false)" <img src="@/assets/project/chong.png" alt="" title="重命名" @click="editfile(scope.row, false)"
style="cursor: pointer;"> style="cursor: pointer;">
<img v-if="scope.row.fileName.includes('ins_img')" src="@/assets/MenuIcon/guiji.png" alt="" <img v-if="scope.row.fileName.includes('ins_img') || scope.row.fileName == 'VINS.csv'"
@click="openMap(scope.row)" title="轨迹预览图" style="cursor: pointer;"> src="@/assets/MenuIcon/guiji.png" alt="" @click="openMap(scope.row)" title="轨迹预览图"
style="cursor: pointer;">
<img src="@/assets/MenuIcon/lbcz_xg.png" alt="" @click="editfile(scope.row, true)" <img src="@/assets/MenuIcon/lbcz_xg.png" alt="" @click="editfile(scope.row, true)"
title="修改" style="cursor: pointer;"> title="修改" style="cursor: pointer;">
<img v-if="scope.row.type == 'ZIP'" src="@/assets/images/jieyasuo.png" alt="" <img v-if="scope.row.type == 'ZIP'" src="@/assets/images/jieyasuo.png" alt=""
@ -2129,13 +2214,38 @@ const formatFileSize = (size: number): string => {
<div class="mapbox"> <div class="mapbox">
<div @click="tabbas = '1'" :class="tabbas == '1' ? 'mapbox_border' : 'mapbox_border1'">载体运动轨迹</div> <div @click="tabbas = '1'" :class="tabbas == '1' ? 'mapbox_border' : 'mapbox_border1'">载体运动轨迹</div>
<div @click="tabbas = '2'" :class="tabbas == '2' ? 'mapbox_border' : 'mapbox_border1'">高程变化动态图</div> <div @click="tabbas = '2'" :class="tabbas == '2' ? 'mapbox_border' : 'mapbox_border1'">高程变化动态图</div>
<div @click="tabbas = '3'" :class="tabbas == '3' ? 'mapbox_border' : 'mapbox_border1'">卫星叠加轨迹</div>
</div> </div>
<div v-show="tabbas == '1'" style="width:800px;height:600px;overflow: hidden;margin-top: 20px;"> <div v-show="tabbas == '1'" style="width:800px;height:600px;overflow: hidden;">
<MapChart :coordinates="dynamicCoordinates" :qvehuan="qvehuan" @qvehuan1="handleCustomEvent" /> <MapChart :coordinates="dynamicCoordinates" :qvehuan="qvehuan" @qvehuan1="handleCustomEvent" />
</div> </div>
<div v-show="tabbas == '2'" style="width: 800px;height:600px;overflow: hidden;"> <div v-show="tabbas == '2'" style="width: 800px;height:600px;overflow: hidden;">
<Echart :chart-data="lineData" /> <Echart :chart-data="lineData" />
</div> </div>
<div v-show="tabbas == '3'" v-loading="pngloading">
<div v-if="imgor">
<div class="map_select">
<div style="width: 210px;margin-top: 5px;">请选择底图(支持png/jpg格式)</div>
<el-radio-group v-model="pngradio">
<el-radio v-for="(item, index) in pngArr" :value="item.id">{{ item.fileName }}</el-radio>
</el-radio-group>
</div>
<div class="map_select">
<div style="width: 220px;margin-top: 5px;">请选择地理信息文件(maps*.txt)</div>
<el-radio-group v-model="txtradio">
<el-radio v-for="(item, index) in txtArr" :value="item.id">{{ item.fileName }}</el-radio>
</el-radio-group>
</div>
<div style="width: 100%;display: flex;">
<el-button type="primary" @click="pngsure">确定</el-button>
<el-button @click="">取消</el-button>
</div>
</div>
<div style="width: 800px;height:600px;overflow: hidden;margin: auto;" v-else>
<Imggui :imageUrl="pngobj.pngurl" :bounds="pngobj.textcontent" :trajectory="imgarrdata"
:qvehuan="qvehuan" @qvehuan1="handleCustomEvent" />
</div>
</div>
</el-dialog> </el-dialog>
<!-- 组件预览 --> <!-- 组件预览 -->
@ -2442,4 +2552,10 @@ const formatFileSize = (size: number): string => {
cursor: pointer; cursor: pointer;
} }
} }
.map_select {
width: 100%;
display: flex;
margin-top: 10px;
}
</style> </style>