Skip to content

Commit a78e2ea

Browse files
committed
Add pilot feature animation to map
1 parent 6084ffd commit a78e2ea

File tree

5 files changed

+135
-37
lines changed

5 files changed

+135
-37
lines changed

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@
2727
"helmet": "^8.1.0",
2828
"xml2js": "^0.6.2"
2929
}
30-
}
30+
}

apps/web/components/Map/Map.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "./Map.css";
55
import { useRouter } from "next/navigation";
66
import { onClick, onMoveEnd, onPointerMove, setNavigator } from "./utils/events";
77
import { initMap } from "./utils/init";
8+
import { animatePilotFeatures } from "./utils/pilotFeatures";
89

910
export default function OMap() {
1011
const router = useRouter();
@@ -17,11 +18,19 @@ export default function OMap() {
1718
map.on("pointermove", onPointerMove);
1819
map.on("click", onClick);
1920

21+
let animationFrameId = 0;
22+
const animate = () => {
23+
animatePilotFeatures(map);
24+
animationFrameId = window.requestAnimationFrame(animate);
25+
};
26+
animationFrameId = window.requestAnimationFrame(animate);
27+
2028
return () => {
2129
map.un(["moveend"], onMoveEnd);
2230
map.un("pointermove", onPointerMove);
2331
map.un("click", onClick);
2432
map.setTarget(undefined);
33+
window.cancelAnimationFrame(animationFrameId);
2534
};
2635
}, [router]);
2736

apps/web/components/Map/utils/events.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,23 @@ async function updateOverlay(feature: Feature<Point>, overlay: Overlay): Promise
287287
}
288288
}
289289

290+
export function animateOverlays(): void {
291+
if (clickedOverlay) {
292+
if (clickedFeature) {
293+
const geom = clickedFeature.getGeometry();
294+
const coords = geom?.getCoordinates();
295+
clickedOverlay.setPosition(coords);
296+
}
297+
}
298+
if (hoveredOverlay) {
299+
if (hoveredFeature) {
300+
const geom = hoveredFeature.getGeometry();
301+
const coords = geom?.getCoordinates();
302+
hoveredOverlay.setPosition(coords);
303+
}
304+
}
305+
}
306+
290307
function toggleControllerSectorHover(feature: Feature<Point> | undefined | null, hovered: boolean, event: "hovered" | "clicked"): void {
291308
if (feature?.get("type") === "tracon") {
292309
const id = feature.getId()?.toString();

apps/web/components/Map/utils/pilotFeatures.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { PilotDelta, WsAll } from "@sr24/types/vatsim";
2-
import { Feature } from "ol";
2+
import { Feature, type Map as OlMap } from "ol";
33
import type { Extent } from "ol/extent";
44
import { Point } from "ol/geom";
55
import { fromLonLat, transformExtent } from "ol/proj";
66
import RBush from "rbush";
77
import type { PilotProperties } from "@/types/ol";
88
import { pilotMainSource } from "./dataLayers";
9-
import { resetMap } from "./events";
9+
import { animateOverlays, resetMap } from "./events";
1010
import { getMapView } from "./init";
11-
import { initTrackFeatures } from "./trackFeatures";
11+
import { animateTrackFeatures, initTrackFeatures } from "./trackFeatures";
1212

1313
interface RBushPilotFeature {
1414
minX: number;
@@ -169,11 +169,59 @@ export function moveToPilotFeature(id: string): Feature<Point> | null {
169169
view?.animate({
170170
center: coords,
171171
duration: 200,
172-
zoom: 8,
172+
zoom: 10,
173173
});
174174

175175
initTrackFeatures(`pilot_${id}`);
176176
addHighlightedPilot(id);
177177

178178
return feature;
179179
}
180+
181+
let timestamp = Date.now();
182+
let animating = false;
183+
184+
export function animatePilotFeatures(map: OlMap) {
185+
if (animating) return;
186+
animating = true;
187+
188+
const resolution = map.getView().getResolution() || 0;
189+
let interval = 1000;
190+
if (resolution > 1) {
191+
interval = Math.max(resolution * 10, 50);
192+
}
193+
194+
const now = Date.now();
195+
const elapsed = now - timestamp;
196+
197+
if (elapsed > interval) {
198+
const features = pilotMainSource.getFeatures() as Feature<Point>[];
199+
200+
features.forEach((feature) => {
201+
const groundspeed = (feature.get("groundspeed") as number) || 0;
202+
const heading = (feature.get("heading") as number) || 0;
203+
const latitude = (feature.get("latitude") as number) || 0;
204+
const longitude = (feature.get("longitude") as number) || 0;
205+
206+
const distKm = (groundspeed * 0.514444 * elapsed) / 1000 / 1000;
207+
const headingRad = (heading * Math.PI) / 180;
208+
const dx = distKm * Math.sin(headingRad);
209+
const dy = distKm * Math.cos(headingRad);
210+
211+
const newLat = latitude + (dy / 6378) * (180 / Math.PI);
212+
const newLon = longitude + ((dx / 6378) * (180 / Math.PI)) / Math.cos((latitude * Math.PI) / 180);
213+
214+
feature.getGeometry()?.setCoordinates(fromLonLat([newLon, newLat]));
215+
216+
feature.set("latitude", newLat, true);
217+
feature.set("longitude", newLon, true);
218+
});
219+
map.render();
220+
animateOverlays();
221+
animateTrackFeatures();
222+
223+
timestamp = now;
224+
}
225+
226+
animating = false;
227+
}

apps/web/components/Map/utils/trackFeatures.ts

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,31 @@
11
import type { PilotDelta } from "@sr24/types/vatsim";
22
import { Feature } from "ol";
3-
import { LineString } from "ol/geom";
3+
import type { Coordinate } from "ol/coordinate";
4+
import { LineString, type Point } from "ol/geom";
45
import { fromLonLat } from "ol/proj";
56
import Stroke from "ol/style/Stroke";
67
import Style from "ol/style/Style";
78
import { fetchTrackPoints } from "@/storage/cache";
8-
import { trackSource } from "./dataLayers";
9-
10-
interface Cached {
11-
id: string | null;
12-
coords: [number, number];
13-
index: number;
14-
stroke?: Stroke;
15-
timestamp?: number;
16-
}
9+
import { pilotMainSource, trackSource } from "./dataLayers";
1710

1811
const STALE_MS = 60 * 1000;
19-
const cached: Cached = {
20-
id: null,
21-
coords: [0, 0],
22-
index: -1,
23-
};
12+
13+
let pilotId: string | null = null;
14+
let lastIndex = 0;
15+
let lastCoords: Coordinate = [0, 0];
16+
let lastStroke: Stroke | undefined;
17+
let lastTimestamp = 0;
18+
let animatedTrackFeature: Feature<LineString> | null = null;
19+
let pilotFeature: Feature<Point> | null = null;
2420

2521
export async function initTrackFeatures(id: string | null): Promise<void> {
2622
if (!id) return;
2723
const trackPoints = await fetchTrackPoints(id.replace("pilot_", ""));
2824
const trackFeatures: Feature<LineString>[] = [];
2925

30-
for (cached.index = 0; cached.index < trackPoints.length - 1; cached.index++) {
31-
const start = trackPoints[cached.index];
32-
const end = trackPoints[cached.index + 1];
26+
for (lastIndex = 0; lastIndex < trackPoints.length - 1; lastIndex++) {
27+
const start = trackPoints[lastIndex];
28+
const end = trackPoints[lastIndex + 1];
3329

3430
const trackFeature = new Feature({
3531
geometry: new LineString(
@@ -44,48 +40,63 @@ export async function initTrackFeatures(id: string | null): Promise<void> {
4440
const style = new Style({ stroke: stroke });
4541

4642
trackFeature.setStyle(style);
47-
trackFeature.setId(`track_${id}_${cached.index}`);
43+
trackFeature.setId(`track_${id}_${lastIndex}`);
4844
trackFeatures.push(trackFeature);
4945

50-
if (cached.index === trackPoints.length - 2) {
51-
cached.coords = [end.longitude, end.latitude];
52-
cached.stroke = stroke;
46+
if (lastIndex === trackPoints.length - 2) {
47+
lastCoords = fromLonLat([end.longitude, end.latitude]);
48+
lastStroke = stroke;
49+
animatedTrackFeature = trackFeature;
5350
}
5451
}
5552

5653
trackSource.clear();
5754
trackSource.addFeatures(trackFeatures);
58-
cached.id = id;
59-
cached.timestamp = Date.now();
55+
56+
pilotId = id;
57+
pilotFeature = pilotMainSource.getFeatureById(id) as Feature<Point>;
58+
lastTimestamp = Date.now();
6059
}
6160

6261
export async function updateTrackFeatures(delta: PilotDelta): Promise<void> {
6362
if (trackSource.getFeatures().length === 0) return;
6463

65-
const pilot = delta.updated.find((p) => `pilot_${p.id}` === cached.id);
66-
if (!cached.id || !pilot) return;
64+
const pilot = delta.updated.find((p) => `pilot_${p.id}` === pilotId);
65+
if (!pilotId || !pilot) return;
6766
if (pilot.latitude === undefined || pilot.longitude === undefined) return;
6867

69-
if (Date.now() - (cached.timestamp || 0) > STALE_MS) {
70-
await initTrackFeatures(cached.id);
68+
if (Date.now() - (lastTimestamp || 0) > STALE_MS) {
69+
await initTrackFeatures(pilotId);
7170
return;
7271
}
7372

73+
if (animatedTrackFeature) {
74+
const geom = animatedTrackFeature.getGeometry() as LineString;
75+
const coords = geom.getCoordinates();
76+
coords[1] = lastCoords;
77+
geom.setCoordinates(coords);
78+
animatedTrackFeature.setGeometry(geom);
79+
}
80+
7481
const trackFeature = new Feature({
75-
geometry: new LineString([cached.coords, [pilot.longitude, pilot.latitude]].map((coord) => fromLonLat(coord))),
82+
geometry: new LineString([lastCoords, fromLonLat([pilot.longitude, pilot.latitude])]),
7683
type: "track",
7784
});
7885

7986
const stroke =
80-
pilot.altitude_agl !== undefined && pilot.altitude_ms !== undefined ? getTrackSegmentColor(pilot.altitude_agl, pilot.altitude_ms) : cached.stroke;
87+
pilot.altitude_agl !== undefined && pilot.altitude_ms !== undefined ? getTrackSegmentColor(pilot.altitude_agl, pilot.altitude_ms) : lastStroke;
8188
const style = new Style({ stroke: stroke });
8289

8390
trackFeature.setStyle(style);
84-
trackFeature.setId(`track_${pilot.id}_${++cached.index}`);
91+
trackFeature.setId(`track_${pilot.id}_${++lastIndex}`);
8592

8693
trackSource.addFeature(trackFeature);
8794

88-
cached.coords = [pilot.longitude, pilot.latitude];
95+
lastCoords = fromLonLat([pilot.longitude, pilot.latitude]);
96+
lastStroke = stroke;
97+
pilotFeature = pilotMainSource.getFeatureById(`pilot_${pilot.id}`) as Feature<Point>;
98+
lastTimestamp = Date.now();
99+
animatedTrackFeature = trackFeature;
89100
}
90101

91102
function getTrackSegmentColor(altitude_agl: number, altitude_ms: number): Stroke {
@@ -129,3 +140,16 @@ function getTrackSegmentColor(altitude_agl: number, altitude_ms: number): Stroke
129140
width: 3,
130141
});
131142
}
143+
144+
export function animateTrackFeatures(): void {
145+
if (!animatedTrackFeature || !pilotFeature || trackSource.getFeatures().length === 0) return;
146+
147+
const pilotCoords = pilotFeature.getGeometry()?.getCoordinates();
148+
if (!pilotCoords) return;
149+
150+
const geom = animatedTrackFeature.getGeometry() as LineString;
151+
const coords = geom.getCoordinates();
152+
coords[1] = pilotCoords;
153+
geom.setCoordinates(coords);
154+
animatedTrackFeature.setGeometry(geom);
155+
}

0 commit comments

Comments
 (0)