feat RTK轨迹绘制
This commit is contained in:
@@ -1,23 +1,271 @@
|
|||||||
<script setup lang="ts">
|
<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 { useCurrentProject } from '@/composables/useCurrentProject'
|
||||||
|
import { createMockRtkStream } from '@/mock/rtkTrack'
|
||||||
|
|
||||||
const { hasCurrentProject } = useCurrentProject()
|
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() {
|
function goToRtk() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({ url: '/pages/rtk/index' })
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="rtk-canvas" @click="goToRtk">
|
<view class="rtk-canvas" :style="{ height: `${canvasHeight}px` }" @click="goToRtk">
|
||||||
<view class="canvas-placeholder">
|
<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">
|
<text class="placeholder-text">
|
||||||
{{ hasCurrentProject ? '轨迹动画区域' : '--' }}
|
--
|
||||||
</text>
|
|
||||||
<text v-if="hasCurrentProject" class="placeholder-hint">
|
|
||||||
(Canvas 绘制行驶轨迹 + 压实热力图)
|
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -25,30 +273,30 @@ function goToRtk() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.rtk-canvas {
|
.rtk-canvas {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
background: #ffffff;
|
||||||
background: #f0fdf4;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-canvas {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-placeholder {
|
.canvas-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
min-height: 160px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
background: #f0fdf4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// RTK轨迹实时监测 + 压实热力图 - 父组件(组合头部 + 画布 + 底部)
|
// RTK轨迹实时监测:整卡高度随画布内容撑开(不要 min-height:100% 锁死视口)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="card">
|
<view class="card">
|
||||||
<!-- 状态头部 -->
|
<work-rtkHeader class="card-header" />
|
||||||
<work-rtkHeader />
|
<work-rtkCanvas class="card-canvas" />
|
||||||
|
<work-rtkFooter class="card-footer" />
|
||||||
<!-- 轨迹画布 -->
|
|
||||||
<work-rtkCanvas />
|
|
||||||
|
|
||||||
<!-- 底部信息 -->
|
|
||||||
<work-rtkFooter />
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.card {
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
/* 高度完全跟内容走,轨迹变长整卡变高 */
|
||||||
|
height: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header,
|
||||||
|
.card-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-canvas {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import formattedDate from './time'
|
import formattedDate from './time'
|
||||||
|
|
||||||
|
export { createMockRtkStream, generateSerpentineTrack } from './rtkTrack'
|
||||||
|
export type { MockRtkPoint, MockRtkStream, MockRtkStreamOptions } from './rtkTrack'
|
||||||
|
|
||||||
export const mockData = {
|
export const mockData = {
|
||||||
time: formattedDate,
|
time: formattedDate,
|
||||||
message: 'Hello, this is mock data!',
|
message: 'Hello, this is mock data!',
|
||||||
|
|||||||
171
src/mock/rtkTrack.ts
Normal file
171
src/mock/rtkTrack.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* RTK 轨迹假数据:蛇形碾压路径 + 持续推送
|
||||||
|
* 字段:经纬度、航向角(度,0=北)、压实度
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MockRtkPoint {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
heading: number
|
||||||
|
compaction: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockRtkStreamOptions {
|
||||||
|
/** 推送间隔 ms,默认 120 */
|
||||||
|
intervalMs?: number
|
||||||
|
/** 结束后是否循环,默认 true */
|
||||||
|
loop?: boolean
|
||||||
|
/** 横向行数,默认 80(约 10 倍于早期 8 行) */
|
||||||
|
rows?: number
|
||||||
|
/** 每行采样点数,默认 40 */
|
||||||
|
pointsPerRow?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockRtkStream {
|
||||||
|
/** 完整路径(用于画布定界) */
|
||||||
|
readonly points: MockRtkPoint[]
|
||||||
|
start: (onPoint: (point: MockRtkPoint, index: number) => void) => void
|
||||||
|
stop: () => void
|
||||||
|
reset: () => void
|
||||||
|
getIndex: () => number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 航向归一化到 [0, 360) */
|
||||||
|
function normalizeHeading(deg: number) {
|
||||||
|
return ((deg % 360) + 360) % 360
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCompaction(seed: number) {
|
||||||
|
const wave = Math.sin(seed / 4) * 4
|
||||||
|
return Number(Math.min(98, Math.max(78, 88 + wave + (Math.random() - 0.5) * 3)).toFixed(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行末三角掉头:行末 → 外侧顶点 → 下行起点
|
||||||
|
* 航向先转 60°(到顶点),再转 120°(合计 180° 衔接下行)
|
||||||
|
*/
|
||||||
|
function pushUTurnTriangle(
|
||||||
|
points: MockRtkPoint[],
|
||||||
|
startLat: number,
|
||||||
|
startLon: number,
|
||||||
|
fromHeading: number,
|
||||||
|
latStep: number,
|
||||||
|
eastward: boolean,
|
||||||
|
seedRef: { value: number },
|
||||||
|
) {
|
||||||
|
const nextLat = startLat + latStep
|
||||||
|
const midLat = startLat + latStep / 2
|
||||||
|
// 外侧突出量 ≈ 半个行距(换算到经度),形成尖角
|
||||||
|
const tipLatOffset = Math.abs(latStep) / 2
|
||||||
|
const metersPerDegLat = 111320
|
||||||
|
const metersPerDegLon = 111320 * Math.cos((startLat * Math.PI) / 180)
|
||||||
|
const tipLonOffset = (tipLatOffset * metersPerDegLat) / metersPerDegLon
|
||||||
|
const tipLon = startLon + (eastward ? tipLonOffset : -tipLonOffset)
|
||||||
|
|
||||||
|
const turnSign = eastward ? 1 : -1
|
||||||
|
|
||||||
|
// 1) 外侧顶点:航向 +60° / -60°
|
||||||
|
points.push({
|
||||||
|
latitude: Number(midLat.toFixed(7)),
|
||||||
|
longitude: Number(tipLon.toFixed(7)),
|
||||||
|
heading: normalizeHeading(fromHeading + 60 * turnSign),
|
||||||
|
compaction: makeCompaction(seedRef.value++),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2) 下行起点:航向转满 180°
|
||||||
|
points.push({
|
||||||
|
latitude: Number(nextLat.toFixed(7)),
|
||||||
|
longitude: Number(startLon.toFixed(7)),
|
||||||
|
heading: normalizeHeading(fromHeading + 180 * turnSign),
|
||||||
|
compaction: makeCompaction(seedRef.value++),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成类似示意图的左右蛇形碾压路径 */
|
||||||
|
export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): MockRtkPoint[] {
|
||||||
|
const rows = options.rows ?? 80
|
||||||
|
const pointsPerRow = options.pointsPerRow ?? 40
|
||||||
|
|
||||||
|
// 以雄安附近坐标为基准,量级约几十米行程
|
||||||
|
const originLat = 39.0425
|
||||||
|
const originLon = 115.8620
|
||||||
|
const lonStep = 0.00012 // ~10m 东向
|
||||||
|
const latStep = -0.000045 // ~5m 南向(行间距)
|
||||||
|
|
||||||
|
const points: MockRtkPoint[] = []
|
||||||
|
const seedRef = { value: 0 }
|
||||||
|
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
const eastward = row % 2 === 0
|
||||||
|
const heading = eastward ? 90 : 270
|
||||||
|
const lat = originLat + row * latStep
|
||||||
|
|
||||||
|
for (let col = 0; col < pointsPerRow; col++) {
|
||||||
|
const t = eastward ? col : (pointsPerRow - 1 - col)
|
||||||
|
const lon = originLon + t * lonStep
|
||||||
|
points.push({
|
||||||
|
latitude: Number(lat.toFixed(7)),
|
||||||
|
longitude: Number(lon.toFixed(7)),
|
||||||
|
heading,
|
||||||
|
compaction: makeCompaction(seedRef.value++),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行末三角掉头(外凸尖角)
|
||||||
|
if (row < rows - 1) {
|
||||||
|
const endT = eastward ? (pointsPerRow - 1) : 0
|
||||||
|
const endLon = originLon + endT * lonStep
|
||||||
|
pushUTurnTriangle(points, lat, endLon, heading, latStep, eastward, seedRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 持续推送假轨迹点 */
|
||||||
|
export function createMockRtkStream(options: MockRtkStreamOptions = {}): MockRtkStream {
|
||||||
|
const points = generateSerpentineTrack(options)
|
||||||
|
const intervalMs = options.intervalMs ?? 120
|
||||||
|
const loop = options.loop ?? true
|
||||||
|
|
||||||
|
let index = 0
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
if (timer != null) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
stop()
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(onPoint: (point: MockRtkPoint, index: number) => void) {
|
||||||
|
stop()
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (index >= points.length) {
|
||||||
|
if (loop) {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const point = points[index]
|
||||||
|
onPoint(point, index)
|
||||||
|
index += 1
|
||||||
|
}, intervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
points,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
reset,
|
||||||
|
getIndex: () => index,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,11 +40,12 @@ definePage({
|
|||||||
padding-top: 72px;
|
padding-top: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 双列布局 */
|
/* 双列:最小一屏,内容变高时整页跟着撑开可滚动 */
|
||||||
.two-column {
|
.two-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
height: calc(100vh - 84px);
|
min-height: calc(100vh - 84px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-left {
|
.column-left {
|
||||||
@@ -52,11 +53,16 @@ definePage({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
position: sticky;
|
||||||
|
top: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-right {
|
.column-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
/* 右侧白卡随轨迹变高,不锁成一屏高 */
|
||||||
|
height: auto;
|
||||||
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user