|
|
@@ -1,9 +1,10 @@
|
|
|
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
|
|
|
import { CompassHeadingController } from '../sensor/compassHeadingController'
|
|
|
+import { LocationController } from '../sensor/locationController'
|
|
|
import { WebGLMapRenderer } from '../renderer/webglMapRenderer'
|
|
|
import { type MapRendererStats } from '../renderer/mapRenderer'
|
|
|
-import { worldTileToLonLat, type LonLatPoint } from '../../utils/projection'
|
|
|
-import { type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
|
|
+import { gcj02ToWgs84, lonLatToWorldTile, worldTileToLonLat, type LonLatPoint } from '../../utils/projection'
|
|
|
+import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
|
|
|
|
|
const RENDER_MODE = 'Single WebGL Pipeline'
|
|
|
const PROJECTION_MODE = 'WGS84 -> WorldTile -> Camera -> Screen'
|
|
|
@@ -35,6 +36,8 @@ const AUTO_ROTATE_DEADZONE_DEG = 4
|
|
|
const AUTO_ROTATE_MAX_STEP_DEG = 0.75
|
|
|
const AUTO_ROTATE_HEADING_SMOOTHING = 0.32
|
|
|
const COMPASS_NEEDLE_SMOOTHING = 0.12
|
|
|
+const GPS_TRACK_MAX_POINTS = 200
|
|
|
+const GPS_TRACK_MIN_STEP_METERS = 3
|
|
|
|
|
|
const SAMPLE_TRACK_WGS84: LonLatPoint[] = [
|
|
|
worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X - 0.72, y: DEFAULT_CENTER_TILE_Y + 0.44 }, DEFAULT_ZOOM),
|
|
|
@@ -108,6 +111,9 @@ export interface MapEngineViewState {
|
|
|
stageLeft: number
|
|
|
stageTop: number
|
|
|
statusText: string
|
|
|
+ gpsTracking: boolean
|
|
|
+ gpsTrackingText: string
|
|
|
+ gpsCoordText: string
|
|
|
}
|
|
|
|
|
|
export interface MapEngineCallbacks {
|
|
|
@@ -149,6 +155,9 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|
|
'cacheHitRateText',
|
|
|
'tileSizePx',
|
|
|
'statusText',
|
|
|
+ 'gpsTracking',
|
|
|
+ 'gpsTrackingText',
|
|
|
+ 'gpsCoordText',
|
|
|
]
|
|
|
|
|
|
function buildCenterText(zoom: number, x: number, y: number): string {
|
|
|
@@ -349,10 +358,32 @@ function formatCacheHitRate(memoryHitCount: number, diskHitCount: number, networ
|
|
|
const hitRate = ((memoryHitCount + diskHitCount) / total) * 100
|
|
|
return `${Math.round(hitRate)}%`
|
|
|
}
|
|
|
+
|
|
|
+function formatGpsCoordText(point: LonLatPoint | null, accuracyMeters: number | null): string {
|
|
|
+ if (!point) {
|
|
|
+ return '--'
|
|
|
+ }
|
|
|
+
|
|
|
+ const base = `${point.lat.toFixed(6)}, ${point.lon.toFixed(6)}`
|
|
|
+ if (accuracyMeters === null || !Number.isFinite(accuracyMeters)) {
|
|
|
+ return base
|
|
|
+ }
|
|
|
+
|
|
|
+ return `${base} / ±${Math.round(accuracyMeters)}m`
|
|
|
+}
|
|
|
+
|
|
|
+function getApproxDistanceMeters(a: LonLatPoint, b: LonLatPoint): number {
|
|
|
+ const avgLatRad = ((a.lat + b.lat) / 2) * Math.PI / 180
|
|
|
+ const dx = (b.lon - a.lon) * 111320 * Math.cos(avgLatRad)
|
|
|
+ const dy = (b.lat - a.lat) * 110540
|
|
|
+ return Math.sqrt(dx * dx + dy * dy)
|
|
|
+}
|
|
|
+
|
|
|
export class MapEngine {
|
|
|
buildVersion: string
|
|
|
renderer: WebGLMapRenderer
|
|
|
compassController: CompassHeadingController
|
|
|
+ locationController: LocationController
|
|
|
onData: (patch: Partial<MapEngineViewState>) => void
|
|
|
state: MapEngineViewState
|
|
|
previewScale: number
|
|
|
@@ -392,6 +423,10 @@ export class MapEngine {
|
|
|
defaultCenterTileX: number
|
|
|
defaultCenterTileY: number
|
|
|
tileBoundsByZoom: Record<number, TileZoomBounds> | null
|
|
|
+ currentGpsPoint: LonLatPoint
|
|
|
+ currentGpsTrack: LonLatPoint[]
|
|
|
+ currentGpsAccuracyMeters: number | null
|
|
|
+ hasGpsCenteredOnce: boolean
|
|
|
|
|
|
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
|
|
this.buildVersion = buildVersion
|
|
|
@@ -414,12 +449,34 @@ export class MapEngine {
|
|
|
this.handleCompassError(message)
|
|
|
},
|
|
|
})
|
|
|
+ this.locationController = new LocationController({
|
|
|
+ onLocation: (update) => {
|
|
|
+ this.handleLocationUpdate(update.longitude, update.latitude, typeof update.accuracy === 'number' ? update.accuracy : null)
|
|
|
+ },
|
|
|
+ onStatus: (message) => {
|
|
|
+ this.setState({
|
|
|
+ gpsTracking: this.locationController.listening,
|
|
|
+ gpsTrackingText: message,
|
|
|
+ }, true)
|
|
|
+ },
|
|
|
+ onError: (message) => {
|
|
|
+ this.setState({
|
|
|
+ gpsTracking: false,
|
|
|
+ gpsTrackingText: message,
|
|
|
+ statusText: `${message} (${this.buildVersion})`,
|
|
|
+ }, true)
|
|
|
+ },
|
|
|
+ })
|
|
|
this.minZoom = MIN_ZOOM
|
|
|
this.maxZoom = MAX_ZOOM
|
|
|
this.defaultZoom = DEFAULT_ZOOM
|
|
|
this.defaultCenterTileX = DEFAULT_CENTER_TILE_X
|
|
|
this.defaultCenterTileY = DEFAULT_CENTER_TILE_Y
|
|
|
this.tileBoundsByZoom = null
|
|
|
+ this.currentGpsPoint = SAMPLE_GPS_WGS84
|
|
|
+ this.currentGpsTrack = []
|
|
|
+ this.currentGpsAccuracyMeters = null
|
|
|
+ this.hasGpsCenteredOnce = false
|
|
|
this.state = {
|
|
|
buildVersion: this.buildVersion,
|
|
|
renderMode: RENDER_MODE,
|
|
|
@@ -464,6 +521,9 @@ export class MapEngine {
|
|
|
stageLeft: 0,
|
|
|
stageTop: 0,
|
|
|
statusText: `单 WebGL 管线已准备接入方向传感器 (${this.buildVersion})`,
|
|
|
+ gpsTracking: false,
|
|
|
+ gpsTrackingText: '持续定位待启动',
|
|
|
+ gpsCoordText: '--',
|
|
|
}
|
|
|
this.previewScale = 1
|
|
|
this.previewOriginX = 0
|
|
|
@@ -508,10 +568,58 @@ export class MapEngine {
|
|
|
this.clearViewSyncTimer()
|
|
|
this.clearAutoRotateTimer()
|
|
|
this.compassController.destroy()
|
|
|
+ this.locationController.destroy()
|
|
|
this.renderer.destroy()
|
|
|
this.mounted = false
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ handleLocationUpdate(longitude: number, latitude: number, accuracyMeters: number | null): void {
|
|
|
+ const nextPoint: LonLatPoint = gcj02ToWgs84({ lon: longitude, lat: latitude })
|
|
|
+ const lastTrackPoint = this.currentGpsTrack.length ? this.currentGpsTrack[this.currentGpsTrack.length - 1] : null
|
|
|
+ if (!lastTrackPoint || getApproxDistanceMeters(lastTrackPoint, nextPoint) >= GPS_TRACK_MIN_STEP_METERS) {
|
|
|
+ this.currentGpsTrack = [...this.currentGpsTrack, nextPoint].slice(-GPS_TRACK_MAX_POINTS)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.currentGpsPoint = nextPoint
|
|
|
+ this.currentGpsAccuracyMeters = accuracyMeters
|
|
|
+
|
|
|
+ const gpsWorldPoint = lonLatToWorldTile(nextPoint, this.state.zoom)
|
|
|
+ const gpsTileX = Math.floor(gpsWorldPoint.x)
|
|
|
+ const gpsTileY = Math.floor(gpsWorldPoint.y)
|
|
|
+ const gpsInsideMap = isTileWithinBounds(this.tileBoundsByZoom, this.state.zoom, gpsTileX, gpsTileY)
|
|
|
+
|
|
|
+ if (gpsInsideMap && !this.hasGpsCenteredOnce) {
|
|
|
+ this.hasGpsCenteredOnce = true
|
|
|
+ this.commitViewport({
|
|
|
+ centerTileX: gpsWorldPoint.x,
|
|
|
+ centerTileY: gpsWorldPoint.y,
|
|
|
+ tileTranslateX: 0,
|
|
|
+ tileTranslateY: 0,
|
|
|
+ gpsTracking: true,
|
|
|
+ gpsTrackingText: '持续定位进行中',
|
|
|
+ gpsCoordText: formatGpsCoordText(nextPoint, accuracyMeters),
|
|
|
+ }, `GPS定位成功,已定位到当前位置 (${this.buildVersion})`, true)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ gpsTracking: true,
|
|
|
+ gpsTrackingText: gpsInsideMap ? '持续定位进行中' : 'GPS不在当前地图范围内',
|
|
|
+ gpsCoordText: formatGpsCoordText(nextPoint, accuracyMeters),
|
|
|
+ statusText: gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`,
|
|
|
+ }, true)
|
|
|
+ this.syncRenderer()
|
|
|
+ }
|
|
|
+
|
|
|
+ handleToggleGpsTracking(): void {
|
|
|
+ if (this.locationController.listening) {
|
|
|
+ this.locationController.stop()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.locationController.start()
|
|
|
+ }
|
|
|
setStage(rect: MapEngineStageRect): void {
|
|
|
this.previewScale = 1
|
|
|
this.previewOriginX = rect.width / 2
|
|
|
@@ -1272,8 +1380,8 @@ export class MapEngine {
|
|
|
previewScale: this.previewScale || 1,
|
|
|
previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
|
|
|
previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
|
|
|
- track: SAMPLE_TRACK_WGS84,
|
|
|
- gpsPoint: SAMPLE_GPS_WGS84,
|
|
|
+ track: this.currentGpsTrack.length ? this.currentGpsTrack : SAMPLE_TRACK_WGS84,
|
|
|
+ gpsPoint: this.currentGpsPoint,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1643,6 +1751,18 @@ export class MapEngine {
|
|
|
|
|
|
|
|
|
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
|
|
|
|