图片上叠加轨迹
This commit is contained in:
parent
bc86834992
commit
3f6ed98bbb
@ -246,3 +246,11 @@ export function confirmDeleteNodes(params:any){
|
||||
params:params,
|
||||
})
|
||||
}
|
||||
//获取文件相关的属性
|
||||
export function listTsFilesById(params:any){
|
||||
return request ({
|
||||
url:'/experimentalData/ts-files/listTsFilesById',
|
||||
method:'post',
|
||||
params:params,
|
||||
})
|
||||
}
|
233
web/src/components/trajectory/imggui.vue
Normal file
233
web/src/components/trajectory/imggui.vue
Normal 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>
|
142
web/src/views/testdata/datamanagement/index.vue
vendored
142
web/src/views/testdata/datamanagement/index.vue
vendored
@ -13,7 +13,7 @@ import { ElMessageBox, ElMessage, ElMain } from "element-plus";
|
||||
import Page from '@/components/Pagination/page.vue';
|
||||
import AudioPlayer from '@/components/file/preview/AudioPlayer.vue';
|
||||
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 useFileUpload from "@/components/file/file/useFileUpload";
|
||||
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 Echart from '@/components/trajectory/echarts.vue';
|
||||
import Imggui from '@/components/trajectory/imggui.vue';
|
||||
// 在顶部添加导入
|
||||
import PDFImg from '@/assets/fileimg/PDF.png'
|
||||
import WordImg from '@/assets/fileimg/word.png'
|
||||
@ -209,7 +210,7 @@ function readyto2(msg: any) {
|
||||
})
|
||||
setTimeout(() => {
|
||||
ElMessageBox.close()
|
||||
if(!gai.value){
|
||||
if (!gai.value) {
|
||||
getdata()
|
||||
}
|
||||
|
||||
@ -1587,24 +1588,100 @@ const fileIcon = (row: any) => {
|
||||
return FILE_ICONS[ext as keyof typeof FILE_ICONS] || (row.type == 'ZIP' ? ZipImg : TextImg);
|
||||
};
|
||||
const mapTrajectory = ref(false)
|
||||
|
||||
const fredid = ref('')
|
||||
const pngform: any = ref({})
|
||||
const imgor = ref(true)
|
||||
function openMap(row: any) {
|
||||
pngobj.vlaue = {
|
||||
pngurl: '',
|
||||
textcontent: []
|
||||
}
|
||||
pngform.value = JSON.parse(JSON.stringify(row))
|
||||
fredid.value = row.id
|
||||
getSSELink()
|
||||
// setInterval(() => {
|
||||
// lineData.value = lineData.value.map(d => ({
|
||||
// x: d.x,
|
||||
// y: Math.random() * 10
|
||||
// }));
|
||||
// }, 2000);
|
||||
getpngdata()
|
||||
tabbas.value = '1'
|
||||
mapTrajectory.value = true
|
||||
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() {
|
||||
txtradio.value = null
|
||||
pngradio.value = null
|
||||
imgor.value = true
|
||||
mapTrajectory.value = false
|
||||
lineData.value.length = 0
|
||||
dynamicCoordinates.value.length = 0
|
||||
imgarrdata.value.length = 0
|
||||
closeSSE()
|
||||
// index = 0
|
||||
}
|
||||
@ -1634,6 +1711,7 @@ function frequency(row: any) {
|
||||
qvehuan1.value = true
|
||||
lineData.value.length = 0
|
||||
dynamicCoordinates.value.length = 0
|
||||
imgarrdata.value.length = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1641,9 +1719,11 @@ function frequency(row: any) {
|
||||
const eventSource = ref(null)
|
||||
let index = 0
|
||||
const dynamicCoordinates = ref([])
|
||||
const imgarrdata = ref([])
|
||||
// let SSEclose
|
||||
function closeSSE() {
|
||||
dynamicCoordinates.value.length = 0
|
||||
imgarrdata.value.length = 0
|
||||
stopSimpleNavi({ token: userStore.userId }).then((res: any) => {
|
||||
})
|
||||
eventSource.value?.close()
|
||||
@ -1667,6 +1747,7 @@ function getSSELink() {
|
||||
dynamicCoordinates.value.shift()
|
||||
}
|
||||
lineData.value.push({ x: data.UtcTime, y: data.alt })
|
||||
imgarrdata.value.push([data.lon, data.lat])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('消息解析失败:', err)
|
||||
@ -1720,7 +1801,7 @@ function repstring(row: any) {
|
||||
* @returns 格式化后的文件大小字符串
|
||||
*/
|
||||
const formatFileSize = (size: number): string => {
|
||||
if(!size) return '--'
|
||||
if (!size) return '--'
|
||||
if (size < 1024) return `${size.toFixed(1)} KB`;
|
||||
|
||||
const mb = size / 1024;
|
||||
@ -1729,6 +1810,9 @@ const formatFileSize = (size: number): string => {
|
||||
const gb = mb / 1024;
|
||||
return `${gb.toFixed(1)} GB`;
|
||||
};
|
||||
|
||||
const txtradio: any = ref(null)
|
||||
const pngradio: any = ref(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -1847,8 +1931,9 @@ const formatFileSize = (size: number): string => {
|
||||
style="cursor: pointer;"> -->
|
||||
<img src="@/assets/project/chong.png" alt="" title="重命名" @click="editfile(scope.row, false)"
|
||||
style="cursor: pointer;">
|
||||
<img v-if="scope.row.fileName.includes('ins_img')" src="@/assets/MenuIcon/guiji.png" alt=""
|
||||
@click="openMap(scope.row)" title="轨迹预览图" style="cursor: pointer;">
|
||||
<img v-if="scope.row.fileName.includes('ins_img') || scope.row.fileName == 'VINS.csv'"
|
||||
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)"
|
||||
title="修改" style="cursor: pointer;">
|
||||
<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 @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 = '3'" :class="tabbas == '3' ? 'mapbox_border' : 'mapbox_border1'">卫星叠加轨迹</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" />
|
||||
</div>
|
||||
<div v-show="tabbas == '2'" style="width: 800px;height:600px;overflow: hidden;">
|
||||
<Echart :chart-data="lineData" />
|
||||
</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>
|
||||
<!-- 组件预览 -->
|
||||
@ -2442,4 +2552,10 @@ const formatFileSize = (size: number): string => {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.map_select {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user