110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { computeCameraFollowStep } from '../lib/telemetry-camera-follow';
|
|
|
|
describe('telemetry-camera-follow engine', () => {
|
|
const createMockMap = (center = { lat: 35.0, lng: -115.0 }, size = { x: 600, y: 400 }) => {
|
|
return {
|
|
getCenter: vi.fn(() => ({ ...center })),
|
|
getSize: vi.fn(() => ({ ...size })),
|
|
latLngToContainerPoint: vi.fn(([lat, lng]: [number, number]) => {
|
|
// Linear scale for testing: 1 deg = 1000 px, centered at (300, 200)
|
|
const x = (size.x / 2) + (lng - center.lng) * 1000;
|
|
const y = (size.y / 2) - (lat - center.lat) * 1000;
|
|
return { x, y };
|
|
}),
|
|
containerPointToLatLng: vi.fn((pt: { x: number; y: number }) => {
|
|
const lng = center.lng + (pt.x - (size.x / 2)) / 1000;
|
|
const lat = center.lat - (pt.y - (size.y / 2)) / 1000;
|
|
return { lat, lng };
|
|
}),
|
|
};
|
|
};
|
|
|
|
it('keeps camera stationary when motorcycle marker is within dead zone radius', () => {
|
|
const map = createMockMap({ lat: 35.0, lng: -115.0 });
|
|
// Marker offset by only 10 pixels (dead zone is 32px)
|
|
// 10 px / 1000 = 0.01 deg
|
|
const markerLatLng = { lat: 35.005, lng: -115.005 }; // hypot is ~7 px
|
|
|
|
const result = computeCameraFollowStep(map, markerLatLng, 16.67, { deadZoneRadiusPx: 32 });
|
|
|
|
expect(result.shouldPan).toBe(false);
|
|
expect(result.excessPx).toBe(0);
|
|
expect(result.targetLatLng.lat).toBe(35.0);
|
|
expect(result.targetLatLng.lng).toBe(-115.0);
|
|
});
|
|
|
|
it('smoothly follows towards marker when outside dead zone', () => {
|
|
const map = createMockMap({ lat: 35.0, lng: -115.0 });
|
|
// Marker offset to the right by 100 pixels (dx = 100, dy = 0)
|
|
// lng = -115.0 + 100 / 1000 = -114.90
|
|
const markerLatLng = { lat: 35.0, lng: -114.90 };
|
|
|
|
const result = computeCameraFollowStep(map, markerLatLng, 16.67, {
|
|
deadZoneRadiusPx: 30,
|
|
maxStepPxPerFrame: 20,
|
|
dampingLambda: 4.5,
|
|
});
|
|
|
|
expect(result.shouldPan).toBe(true);
|
|
expect(result.distanceFromCenterPx).toBeCloseTo(100, 1);
|
|
expect(result.excessPx).toBeCloseTo(70, 1);
|
|
expect(result.panDeltaPx.x).toBeGreaterThan(0);
|
|
expect(result.panDeltaPx.y).toBeCloseTo(0, 4);
|
|
|
|
// Target longitude should move east (towards -114.90)
|
|
expect(result.targetLatLng.lng).toBeGreaterThan(-115.0);
|
|
expect(result.targetLatLng.lng).toBeLessThan(-114.90);
|
|
expect(result.targetLatLng.lat).toBeCloseTo(35.0, 5);
|
|
});
|
|
|
|
it('strictly clamps maximum pan step per frame to prevent outrunning tile loading', () => {
|
|
const map = createMockMap({ lat: 35.0, lng: -115.0 });
|
|
// Marker very far away (500 pixels away)
|
|
const markerLatLng = { lat: 35.0, lng: -114.50 };
|
|
|
|
const maxStep = 10;
|
|
const result = computeCameraFollowStep(map, markerLatLng, 100, {
|
|
deadZoneRadiusPx: 30,
|
|
maxStepPxPerFrame: maxStep,
|
|
dampingLambda: 10, // high damping
|
|
});
|
|
|
|
expect(result.shouldPan).toBe(true);
|
|
const stepLength = Math.hypot(result.panDeltaPx.x, result.panDeltaPx.y);
|
|
expect(stepLength).toBeCloseTo(maxStep, 2);
|
|
});
|
|
|
|
it('adjusts smoothly to higher playback speeds and larger frame deltas', () => {
|
|
const map = createMockMap({ lat: 35.0, lng: -115.0 });
|
|
const markerLatLng = { lat: 35.0, lng: -114.90 }; // 100px away
|
|
|
|
const stepShort = computeCameraFollowStep(map, markerLatLng, 16.67, { maxStepPxPerFrame: 50 });
|
|
const stepLong = computeCameraFollowStep(map, markerLatLng, 50, { maxStepPxPerFrame: 50 });
|
|
|
|
expect(stepLong.panDeltaPx.x).toBeGreaterThan(stepShort.panDeltaPx.x);
|
|
});
|
|
|
|
it('falls back safely to LatLng interpolation when container projection methods are missing', () => {
|
|
const mapWithoutProjection = {
|
|
getCenter: () => ({ lat: 35.0, lng: -115.0 }),
|
|
};
|
|
|
|
const markerLatLng = { lat: 35.01, lng: -115.01 }; // outside ~0.0003 deg dead zone
|
|
const result = computeCameraFollowStep(mapWithoutProjection, markerLatLng, 16.67);
|
|
|
|
expect(result.shouldPan).toBe(true);
|
|
expect(result.targetLatLng.lat).toBeGreaterThan(35.0);
|
|
expect(result.targetLatLng.lng).toBeLessThan(-115.0);
|
|
});
|
|
|
|
it('handles null, undefined, and non-finite coordinates gracefully without throwing', () => {
|
|
const map = createMockMap();
|
|
const resultNull = computeCameraFollowStep(null, null as any);
|
|
expect(resultNull.shouldPan).toBe(false);
|
|
|
|
const resultNaN = computeCameraFollowStep(map, { lat: NaN, lng: 10 });
|
|
expect(resultNaN.shouldPan).toBe(false);
|
|
});
|
|
});
|