feat RTK轨迹绘制

This commit is contained in:
Ironsp
2026-09-17 15:26:32 +08:00
parent 449cf1f6c7
commit 1ae0371c18
5 changed files with 467 additions and 29 deletions

View File

@@ -1,23 +1,271 @@
<script setup lang="ts">
/**
* 固定比例尺绘制:轨迹变长只加高画布/白卡,图案不缩小
*/
import type { MockRtkPoint } from '@/mock/rtkTrack'
import { getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useCurrentProject } from '@/composables/useCurrentProject'
import { createMockRtkStream } from '@/mock/rtkTrack'
const { hasCurrentProject } = useCurrentProject()
const instance = getCurrentInstance()?.proxy
const CANVAS_ID = 'rtkTrackCanvas'
const PADDING = 28
const ROLLER_WIDTH = 14
const MIN_H = 160
const canvasHeight = ref(MIN_H)
const trail: MockRtkPoint[] = []
let finished = false
let cssWidth = 0
let cssHeight = MIN_H
let ready = false
let initTries = 0
let stream = createMockRtkStream({ intervalMs: 80, loop: true, rows: 80, pointsPerRow: 40 })
/** 固定东西向范围与比例尺(米/像素),避免越画越小 */
let fixedMinLon = 0
let fixedMaxLon = 0
let fixedMaxLat = 0 // 北端固定(起点一侧)
let pxPerMeter = 1
function goToRtk() {
uni.navigateTo({
url: '/pages/rtk/index',
uni.navigateTo({ url: '/pages/rtk/index' })
}
function compactionColor(value: number) {
const t = Math.min(1, Math.max(0, (value - 75) / 25))
const r = Math.round(200 - t * 90)
const g = Math.round(230 - t * 40)
const b = Math.round(170 - t * 70)
return `rgba(${r}, ${g}, ${b}, 0.75)`
}
function metersPerDeg(lat: number) {
return {
lat: 111320,
lon: 111320 * Math.cos((lat * Math.PI) / 180),
}
}
/** 用完整假路径定死东西跨度与比例尺;高度只随已画南北跨度增加 */
function initFixedScale(allPoints: MockRtkPoint[]) {
if (!allPoints.length || cssWidth <= 0)
return
let minLat = Infinity
let maxLat = -Infinity
let minLon = Infinity
let maxLon = -Infinity
for (const p of allPoints) {
minLat = Math.min(minLat, p.latitude)
maxLat = Math.max(maxLat, p.latitude)
minLon = Math.min(minLon, p.longitude)
maxLon = Math.max(maxLon, p.longitude)
}
const padLon = (maxLon - minLon) * 0.08 || 0.00001
fixedMinLon = minLon - padLon
fixedMaxLon = maxLon + padLon
fixedMaxLat = maxLat + ((maxLat - minLat) * 0.08 || 0.00001)
const midLat = (minLat + maxLat) / 2
const { lon: mLon } = metersPerDeg(midLat)
const lonSpanM = Math.max((fixedMaxLon - fixedMinLon) * mLon, 1e-3)
pxPerMeter = (cssWidth - PADDING * 2) / lonSpanM
}
function trailMinLat() {
let minLat = fixedMaxLat
for (const p of trail)
minLat = Math.min(minLat, p.latitude)
const pad = 0.00001
return minLat - pad
}
/** 按已绘制南北跨度撑高(比例尺不变) */
function updateHeightFromTrail() {
if (cssWidth <= 0 || !trail.length || pxPerMeter <= 0)
return
const minLat = trailMinLat()
const { lat: mLat } = metersPerDeg((fixedMaxLat + minLat) / 2)
const latSpanM = Math.max((fixedMaxLat - minLat) * mLat, 0)
const next = Math.max(MIN_H, Math.round(PADDING * 2 + latSpanM * pxPerMeter))
if (next !== cssHeight) {
cssHeight = next
canvasHeight.value = next
}
}
function toXY(point: MockRtkPoint) {
const midLat = (fixedMaxLat + point.latitude) / 2
const { lat: mLat, lon: mLon } = metersPerDeg(midLat)
return {
x: PADDING + (point.longitude - fixedMinLon) * mLon * pxPerMeter,
y: PADDING + (fixedMaxLat - point.latitude) * mLat * pxPerMeter,
}
}
function draw() {
if (!ready || cssWidth <= 0)
return
const ctx = uni.createCanvasContext(CANVAS_ID, instance as any)
const path = trail
ctx.setFillStyle('#ffffff')
ctx.fillRect(0, 0, cssWidth, cssHeight)
if (!path.length) {
ctx.draw()
return
}
const coords = path.map(p => toXY(p))
ctx.setLineCap('round')
ctx.setLineJoin('round')
ctx.setLineWidth(ROLLER_WIDTH)
for (let i = 1; i < coords.length; i++) {
ctx.beginPath()
ctx.setStrokeStyle(compactionColor(path[i].compaction))
ctx.moveTo(coords[i - 1].x, coords[i - 1].y)
ctx.lineTo(coords[i].x, coords[i].y)
ctx.stroke()
}
ctx.beginPath()
ctx.setLineWidth(2.5)
ctx.setStrokeStyle('#2563eb')
coords.forEach((c, i) => {
if (i === 0)
ctx.moveTo(c.x, c.y)
else
ctx.lineTo(c.x, c.y)
})
ctx.stroke()
const start = coords[0]
ctx.beginPath()
ctx.setFillStyle('#22c55e')
ctx.arc(start.x, start.y, 10, 0, Math.PI * 2)
ctx.fill()
ctx.setFillStyle('#166534')
ctx.setFontSize(10)
ctx.setTextAlign('center')
ctx.fillText('起点', start.x, start.y - 14)
const last = coords[coords.length - 1]
const cur = path[path.length - 1]
if (finished) {
ctx.beginPath()
ctx.setFillStyle('#ef4444')
ctx.moveTo(last.x, last.y - 14)
ctx.lineTo(last.x + 10, last.y)
ctx.lineTo(last.x, last.y + 4)
ctx.lineTo(last.x - 10, last.y)
ctx.closePath()
ctx.fill()
ctx.setFillStyle('#991b1b')
ctx.fillText('结束', last.x, last.y + 18)
}
else {
ctx.beginPath()
ctx.setFillStyle('#f97316')
ctx.arc(last.x, last.y, 7, 0, Math.PI * 2)
ctx.fill()
const heading = (cur.heading * Math.PI) / 180
ctx.beginPath()
ctx.setStrokeStyle('#ea580c')
ctx.setLineWidth(2)
ctx.moveTo(last.x, last.y)
ctx.lineTo(last.x + Math.sin(heading) * 12, last.y - Math.cos(heading) * 12)
ctx.stroke()
}
ctx.draw()
}
function measureAndReady(onReady?: () => void) {
uni.createSelectorQuery()
.in(instance as any)
.select('.rtk-canvas')
.boundingClientRect((rect) => {
const box = Array.isArray(rect) ? rect[0] : rect
const w = Number(box?.width) || 0
if (w <= 0) {
initTries += 1
if (initTries < 20)
setTimeout(() => measureAndReady(onReady), 50)
return
}
cssWidth = w
cssHeight = MIN_H
canvasHeight.value = MIN_H
ready = true
onReady?.()
})
.exec()
}
function startStream() {
stream.stop()
stream = createMockRtkStream({ intervalMs: 80, loop: true, rows: 80, pointsPerRow: 40 })
trail.length = 0
finished = false
initFixedScale(stream.points)
cssHeight = MIN_H
canvasHeight.value = MIN_H
draw()
stream.start((point, index) => {
trail.push(point)
finished = index >= stream.points.length - 1
updateHeightFromTrail()
nextTick(() => draw())
})
}
function stopStream() {
stream.stop()
trail.length = 0
finished = false
cssHeight = MIN_H
canvasHeight.value = MIN_H
if (ready)
draw()
}
function boot() {
if (!hasCurrentProject.value) {
stopStream()
return
}
initTries = 0
measureAndReady(() => startStream())
}
watch(hasCurrentProject, () => nextTick(() => boot()))
onMounted(() => nextTick(() => boot()))
onUnmounted(() => stream.stop())
</script>
<template>
<view class="rtk-canvas" @click="goToRtk">
<view class="canvas-placeholder">
<view class="rtk-canvas" :style="{ height: `${canvasHeight}px` }" @click="goToRtk">
<canvas
v-if="hasCurrentProject"
:id="CANVAS_ID"
:canvas-id="CANVAS_ID"
class="track-canvas"
:style="{ width: '100%', height: `${canvasHeight}px` }"
/>
<view v-else class="canvas-placeholder">
<text class="placeholder-text">
{{ hasCurrentProject ? '轨迹动画区域' : '--' }}
</text>
<text v-if="hasCurrentProject" class="placeholder-hint">
(Canvas 绘制行驶轨迹 + 压实热力图)
--
</text>
</view>
</view>
@@ -25,30 +273,30 @@ function goToRtk() {
<style scoped>
.rtk-canvas {
position: relative;
width: 100%;
height: 300px;
background: #f0fdf4;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.track-canvas {
width: 100%;
display: block;
}
.canvas-placeholder {
width: 100%;
height: 100%;
min-height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background: #f0fdf4;
}
.placeholder-text {
font-size: 14px;
color: #666;
}
.placeholder-hint {
font-size: 12px;
color: #999;
}
</style>

View File

@@ -1,24 +1,34 @@
<script setup lang="ts">
// RTK轨迹实时监测 + 压实热力图 - 父组件(组合头部 + 画布 + 底部)
// RTK轨迹实时监测:整卡高度随画布内容撑开(不要 min-height:100% 锁死视口)
</script>
<template>
<view class="card">
<!-- 状态头部 -->
<work-rtkHeader />
<!-- 轨迹画布 -->
<work-rtkCanvas />
<!-- 底部信息 -->
<work-rtkFooter />
<work-rtkHeader class="card-header" />
<work-rtkCanvas class="card-canvas" />
<work-rtkFooter class="card-footer" />
</view>
</template>
<style scoped>
.card {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 12px;
padding: 16px;
/* 高度完全跟内容走,轨迹变长整卡变高 */
height: auto;
box-sizing: border-box;
}
.card-header,
.card-footer {
flex-shrink: 0;
}
.card-canvas {
flex: 0 0 auto;
width: 100%;
}
</style>