The map is the centerpiece of the Dashboard: a full-screen Mapbox GL 3D map with a Threebox/Three.js overlay drawn on the same WebGL context. Every vehicle is rendered as a 3D compass arrow that animates its GPS position and heading in real time; missions appear as colored spheres, geofences as filled polygons, and a set of helper lines (ground drop-lines, guided flight-path, ruler) sits on top. Almost all of this lives in one very large component, 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. Live map/control view with telemetry HUD

The two-layer rendering stack

The map is a single Mapbox Map 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):
SettingValueSource
containerprojected-route-maphome.component.ts:494
stylestored mapStyle, defaults to the satellite stylehome.component.ts:495
center[environment.DEFAULT_LNG, environment.DEFAULT_LAT] — Plovdiv 24.7453, 42.1354home.component.ts:496, environment.ts:13
zoomstored mapZoom, default 17home.component.ts:499
minZoom7home.component.ts:500
pitch45home.component.ts:502
hashtrue (map position is written to the URL hash)home.component.ts:501
Two Mapbox styles are hardcoded and toggled at runtime:
src/app/home/home.component.ts:50
satelliteMapUrl = 'mapbox://styles/superactro/clmhisbau01jf01nz80dahrad';
darkMapUrl = 'mapbox://styles/superactro/clivhndql001501qqgv331jcx';
On the map 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
window.tb = new Threebox(this.map, this.map.getCanvas().getContext('webgl'), {
  defaultLights: true,
  enableSelectingObjects: true,
  enableDraggingObjects: false,
  enableRotatingObjects: false,
  enableTooltips: true,
  enableHelpTooltips: true,
});
this.tb = window.tb;
The render loop is the 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
map.addLayer({
  id: 'mission_layer',
  type: 'custom',
  renderingMode: '3d',
  onAdd: () => {},
  render: () => { threebox.update(); },
});
window.tb is a global Threebox instance, and MapService.tb = window.tb reads it at construction. Because Mapbox draws layers in insertion order, mission_layer can end up below later-added Mapbox line layers, which would hide the 3D vehicle arrows. ensureThreeboxLayerOnTop() (home.component.ts:820) finds the custom layer and map.moveLayer()s it to the very top; it is called after Threebox init, after every vehicle arrow is added, and after style changes. Do not remove these calls.

Rendering a vehicle — the compass arrow

Each vehicle marker is built by addDroneMarkerToMap() (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 (#720000 for live vehicles, set at home.component.ts:923).
  • Both use THREE.ExtrudeGeometry with a bevel, MeshBasicMaterial with depthTest:false / depthWrite:false, and are grouped into an arrowGroup.
The group is laid flat and oriented by heading:
src/app/home/home.component.ts:2486
// Lay the triangle flat on the ground plane (XY -> XZ)
arrowGroup.rotation.x = -Math.PI / 2;

// Apply yaw. Two class constants control the mapping:
const adjustedYaw = this.DRONE_TRIANGLE_INVERT_YAW ? -yaw : yaw;
const yawRadians = ((adjustedYaw + this.DRONE_TRIANGLE_ROTATION_OFFSET) * Math.PI) / 180;
arrowGroup.rotation.z = yawRadians;
The arrow’s heading depends on two class constants at the top of HomeComponent:
src/app/home/home.component.ts:37
private readonly DRONE_TRIANGLE_ROTATION_OFFSET = 180;   // degrees
private readonly DRONE_TRIANGLE_INVERT_YAW = true;
The same (adjustedYaw + OFFSET) * PI/180 formula is applied in three places — initial creation (:2491), the GPS-update path (:913), and the rotation animator’s target (:927). Change one and you must change all three, or vehicle arrows will point the wrong way. This is purely a visual convention layered on top of the server-computed compass yaw; the yaw itself is authoritative (see below).
The arrow is wrapped in a Threebox 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
const zoom = this.map.getZoom();
const baseSize = 15.6;              // arrow (mission sphere uses 4.5, ground-line 6)
const scale = 2 ** (17 - zoom);
return baseSize * scale;            // meters
A 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 notgetDroneGpsData() 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
const smoothingFactor = 0.08; // move 8% of the remaining distance each frame
The loop stops once it converges (< ~1 cm in each axis) and restarts on the next GPS update — deliberately lagging the raw GPS a little to mask position jitter. It runs inside 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.
The yaw itself is computed server-side in the Gateway’s dashboard stream — VFR_HUD heading first, else derived from the IMU quaternion — and delivered as data.yaw on the GPS payload. The client only applies the visual offset/inversion and animates. See Socket.IO Telemetry Streaming. The raw IMU_ORIENTATION/VFR_HUD telemetry types are intentionally not re-emitted to the client.

Helper geometry

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).
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).
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.
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 explicit renderOrder values (stamped on the object and recursively on every child) because the depth-test is disabled on these materials. Preserve this ordering:
ObjectrenderOrderMeaning
Vehicle compass arrows999999Always on top
Mission point spheres999998Above lines, below vehicles
Home 'H' markers999997Above lines, below mission points
Ground / flight-path / ruler lines(default)Bottom of the Threebox stack
Combined with 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 spheresrenderMission() (home.component.ts:1312) reacts to currentMission$, drawing a MeshToonMaterial sphere per waypoint via addMissionPoint() (: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 + line layers (not Threebox), one source/layer set per geofence id. renderCircleGeofenceOnMap() (home.component.ts:605) generates a 64-point circle from a single center + param1 radius; polygons are closed by repeating the first vertex (home.component.ts:748-766). Enabled geofences are loaded on map load via loadAllGeofences().

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:
1

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.
2

Pick-location modes

pickingLocationMode forwards the coordinate to the mission edit-marker dialog; pickingGeofenceLocationMode forwards it to the geofence-point dialog.
3

Mission marker click

If the cursor is over an existing mission sphere (lastMissionPointId, tracked by Threebox ObjectMouseOver/Out events), the click opens that waypoint.
4

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.
5

Geofence edit

Otherwise, if in geofence-edit mode, the click appends a vertex with the correct MAVLink FENCE_COMMANDS value (circle geofences accept only a single center point).

Camera controls — QuickButtonsComponent

The floating map controls (src/app/home/quick-buttons/quick-buttons.component.ts) drive the camera:
ControlBehaviorSource
Follow vehicleLocks 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 / SpaceviewDrone() 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+FToggles the Mapbox geocoder search box visibility.quick-buttons.component.ts:235
RulerEmits rullerToggle up to HomeComponent.toggleRuller().quick-buttons.component.ts:252
Takeoff / LandOpens 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

Everything hot runs outside the Angular zone. The position and rotation animators, and the Socket.IO handlers that feed them, run via NgZone.runOutsideAngular() and only re-enter for template-bound updates. Losing this pattern reintroduces heavy per-frame change-detection churn across a component that renders at 60 fps.
ngOnDestroy() must fully dispose the scene (home.component.ts:156). It cancels all requestAnimationFrame loops (removeAllDroneMarkers), disposes lines (Threebox tb.line objects are THREE.Line2 and have no dispose(), so their geometry/material are freed manually in disposeAndRemoveLine), removes home/ruler/mission objects, sets window.tb = null, and calls map.remove(). Because window.tb is a global, skipping this leaks the entire Three.js scene and the component.

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.