src/app/home/home.component.ts (~3,500 lines). This page documents how the map and the 3D vehicle-tracking pipeline are built and animated. Where a vehicle’s position and yaw come from is covered in Real-time Telemetry Client; waypoint editing lives in Mission & Waypoint Planning; the camera panel and gimbal are in Video, Gimbal & Manual Flight Control.
The two-layer rendering stack
The map is a single MapboxMap with a custom Threebox layer stacked on top. Mapbox draws the basemap, terrain, buildings and geofence polygons; Threebox draws every 3D object (vehicle arrows, mission spheres, lines) into the same WebGL context so they share the camera and depth.
Map bootstrap — initMap()
initMap() (src/app/home/home.component.ts:489) constructs the Mapbox map into the #projected-route-map container (home.component.html:91):
| Setting | Value | Source |
|---|---|---|
container | projected-route-map | home.component.ts:494 |
style | stored mapStyle, defaults to the satellite style | home.component.ts:495 |
center | [environment.DEFAULT_LNG, environment.DEFAULT_LAT] — Plovdiv 24.7453, 42.1354 | home.component.ts:496, environment.ts:13 |
zoom | stored mapZoom, default 17 | home.component.ts:499 |
minZoom | 7 | home.component.ts:500 |
pitch | 45 | home.component.ts:502 |
hash | true (map position is written to the URL hash) | home.component.ts:501 |
src/app/home/home.component.ts:50
load event the component wires up the rest of the scene: addTerrain(), mapService.addSkyLayer(), mapService.addMissionLayer(), a Mapbox Streets vector road layer, browser geolocation (to remember the user’s location), loadAllGeofences(), and handleSphereObjectsVisibility() (home.component.ts:505-543).
addTerrain() (home.component.ts:769) adds a mapbox-terrain-dem-v1 raster-DEM source plus two building layers: 3d-buildings (fill-extrusion, zoom 12–19, buildings taller than 45 m are painted red, others white) and building-outlines (zoom 18+).
The Mapbox access token and
DEFAULT_LAT/DEFAULT_LNG come from src/environments/environment.ts, while the two style URLs are hardcoded in home.component.ts:50-51. See Environments, Build & CI for how per-environment configs are swapped at build time.Threebox bootstrap — initTreebox()
initTreebox() (home.component.ts:846) creates the global Threebox instance bound to the Mapbox WebGL context and mirrors it onto the component:
src/app/home/home.component.ts:846
mission_layer custom Mapbox layer added by MapService.addMissionLayer() — its render() callback simply calls threebox.update() on every Mapbox frame, keeping the Three.js scene in sync with the Mapbox camera:
src/app/services/map.service.ts
Rendering a vehicle — the compass arrow
Each vehicle marker is built byaddDroneMarkerToMap() (home.component.ts:2403). It is not a GLTF model — it is two extruded Three.js triangles that together form a Google-Maps-style navigation arrow:
- Left triangle — the base color darkened 50% (
darkenColor()), giving a shaded look. - Right triangle — the full base color (
#720000for live vehicles, set athome.component.ts:923). - Both use
THREE.ExtrudeGeometrywith a bevel,MeshBasicMaterialwithdepthTest:false/depthWrite:false, and are grouped into anarrowGroup.
src/app/home/home.component.ts:2486
Object3D({ obj: arrowGroup, units: 'meters' }) and placed with .setCoords([lng, lat, altitude]). A green name/altitude tooltip is attached and kept permanently visible (selected = true), a zoom listener rebuilds the geometry so the arrow keeps a constant on-screen size, and renderOrder = 999999 is stamped on the object and every child so arrows always draw above lines and spheres.
Constant on-screen size
Arrows, mission spheres and ground-line sphere radii all use the same zoom-compensating formula so they look the same size regardless of zoom:src/app/home/home.component.ts:2404
map.on('zoom', ...) handler on each object disposes and rebuilds its geometry at the new size (home.component.ts:2526 for arrows, :2274 for mission spheres). handleSphereObjectsVisibility() (home.component.ts:2831) additionally hides all 3D objects when zoom drops to 15 or below.
The telemetry-to-map animation pipeline
When a drone is selected,DroneStatusComponent.onChange wires the telemetry subscriptions and HomeComponent subscribes to GPS via getDroneGpsData() (home.component.ts:883). Each GPS frame from the dashboard stream drives the marker.
The horizontal position (
latitude/longitude) comes from the GPS payload, but the marker’s altitude does not — getDroneGpsData() reads this.currentDroneAltitude, which is fed by the relAltSubject (relative altitude / AGL) subscription at home.component.ts:341-350. So the arrow floats at the vehicle’s height-above-ground, not its GPS MSL altitude.Position animation — animateMarker()
animateMarker() (home.component.ts:1022) never snaps. It stores a targetPosition and, if no animation frame is already running, starts a requestAnimationFrame loop that eases toward the target by a fixed fraction each frame:
src/app/home/home.component.ts:1058
ngZone.runOutsideAngular() so the per-frame loop does not trigger Angular change detection. During the loop it also updates the ground line and, in GUIDED mode, the flight-path line’s start point.
Yaw animation — animateMarkerRotation()
animateMarkerRotation() (home.component.ts:1107) eases arrowGroup.rotation.z toward the target using the same 0.08 factor, but first normalizes the delta to -π..π so a heading crossing 360° takes the shortest rotational path instead of spinning the long way around. It too runs outside the Angular zone.
Helper geometry
Ground drop-line — updateDroneGroundLine()
Ground drop-line — updateDroneGroundLine()
updateDroneGroundLine() (home.component.ts:2619) draws a red (#ff0000, opacity 0.5) dotted vertical line from beneath the vehicle down to the ground, so the arrow’s true ground position is legible against terrain. It uses the marker’s already-smoothed coordinates, builds 8 dash segments, and reuses the existing segments in place (setLineCoords()) when the segment count is unchanged — a per-frame hot path. The line is removed entirely when altitude falls to the 0.1 m floor (vehicle on ground).Guided flight-path line — updateFlightPathLine()
Guided flight-path line — updateFlightPathLine()
In GUIDED mode, after a go-to target is set,
updateFlightPathLine() (home.component.ts:2870) draws a green (#00ff00) line from the vehicle to the target plus a vertical dotted drop-line at the target. The start point is smoothed every animation frame so the line tracks the moving vehicle; the endpoint altitude is deliberately pinned to flightPathLineStartPos[2] (not the marker’s live altitude) so the two writers don’t fight and flicker during climbs. Cleared by clearGuidedModeElements() (home.component.ts:2814).Orientation arrows — updateOrientationArrows() (disabled)
Orientation arrows — updateOrientationArrows() (disabled)
updateOrientationArrows() (home.component.ts:2704) can draw separate yaw (blue), pitch (green) and roll (red) vectors from the vehicle. It is fully implemented but currently commented out at its call site (home.component.ts:952), because the dashboard GPS payload doesn’t carry pitch/roll. Kept in place for future re-enable — don’t delete it assuming it’s dead.Home position marker — addHomePositionMarker()
Home position marker — addHomePositionMarker()
subscribeToHomePosition() (home.component.ts:986) listens on homePositionSubject and renders an 'H' label at the vehicle’s home/launch coordinate via addHomePositionMarker() (home.component.ts:2330) — an invisible 0.1 m sphere used purely to anchor the label, renderOrder = 999997.Render order — a fragile contract
Draw order is forced with explicitrenderOrder values (stamped on the object and recursively on every child) because the depth-test is disabled on these materials. Preserve this ordering:
| Object | renderOrder | Meaning |
|---|---|---|
| Vehicle compass arrows | 999999 | Always on top |
| Mission point spheres | 999998 | Above lines, below vehicles |
Home 'H' markers | 999997 | Above lines, below mission points |
| Ground / flight-path / ruler lines | (default) | Bottom of the Threebox stack |
ensureThreeboxLayerOnTop(), this keeps vehicles visible above Mapbox’s own line layers. Changing any value here, or removing the moveLayer calls, reintroduces the “Mapbox lines cover the drone” bug the offsets were added to fix.
Missions & geofences on the map
HomeComponent also renders mission and geofence geometry (editing them is documented in Mission & Waypoint Planning):
- Mission spheres —
renderMission()(home.component.ts:1312) reacts tocurrentMission$, drawing aMeshToonMaterialsphere per waypoint viaaddMissionPoint()(:2236), colored by point type, labeled with its sequence, with a hover tooltip and an edit pencil. Commands 20 (RTL) and 178 (Set Speed) carry no coordinates and are skipped. Connecting lines and per-waypoint ground drop-lines are added alongside. - Geofences — rendered as Mapbox
fill+linelayers (not Threebox), one source/layer set per geofence id.renderCircleGeofenceOnMap()(home.component.ts:605) generates a 64-point circle from a single center +param1radius; polygons are closed by repeating the first vertex (home.component.ts:748-766). Enabled geofences are loaded on maploadvialoadAllGeofences().
Map click dispatch — onMapClickAddMarker()
A single Mapbox click handler (home.component.ts:1871) routes to different behaviors based on the current mode, checked in this order:
Ruler mode
If
rullerMode, the click adds a point: a Mapbox marker + coordinate popup, a green Threebox poly-line through all points, per-segment distance popups and a running total. Distances use getDistanceInMeters() (haversine). toggleRuller() (home.component.ts:3065) clears everything on exit.Pick-location modes
pickingLocationMode forwards the coordinate to the mission edit-marker dialog; pickingGeofenceLocationMode forwards it to the geofence-point dialog.Mission marker click
If the cursor is over an existing mission sphere (
lastMissionPointId, tracked by Threebox ObjectMouseOver/Out events), the click opens that waypoint.Go-to (guided)
If
controlVehicleMode is set (via the GoTo button or the ‘A’ key in GUIDED), goTo() (home.component.ts:2211) opens the takeoff-altitude dialog with action 'goto', sets the target, draws the flight-path line, and stores the go-to position in AppStateService. The actual command is dispatched from the dialog — see Vehicle Commands & Gamepad.Camera controls — QuickButtonsComponent
The floating map controls (src/app/home/quick-buttons/quick-buttons.component.ts) drive the camera:
| Control | Behavior | Source |
|---|---|---|
| Follow vehicle | Locks the camera to the selected vehicle. When on, every currentDronePosition$ update calls map.easeTo({ center, duration: 1000 }). State persists to localStorage key follow_vehicle_mode. | quick-buttons.component.ts:130, 197 |
| View drone / Space | viewDrone() toggles map pitch between 45 (angled) and 0 (top-down), flying to the vehicle, else to mission center, else the current center. | quick-buttons.component.ts:146 |
| Geocoder / Ctrl+F | Toggles the Mapbox geocoder search box visibility. | quick-buttons.component.ts:235 |
| Ruler | Emits rullerToggle up to HomeComponent.toggleRuller(). | quick-buttons.component.ts:252 |
| Takeoff / Land | Opens the takeoff-altitude dialog / sends LAND. “Flying” state = armed and relative altitude > 0.5 m. | quick-buttons.component.ts:113-128 |
HomeComponent.getDroneGpsData() also performs a one-time map.flyTo() to the vehicle on its first valid GPS fix (home.component.ts:962), and re-flies when the selected drone changes (home.component.ts:322).
Performance & teardown gotchas
Related pages
Real-time Telemetry Client
Where GPS, yaw and home-position frames come from, and the per-type RxJS Subjects that feed the map.
Mission & Waypoint Planning
Creating/editing the waypoints and geofences this page renders as spheres and polygons.
Vehicle Commands & Gamepad
How go-to and other map-driven actions become commands to the vehicle.
App State & Video
AppStateService — selected drone, modes, follow/edit flags that gate the map’s behavior.

