Position Tracking (Blue Force Tracking)

Real-time GPS position sharing for team awareness.

7 min read

How It Works

Position tracking uses the browser's native Geolocation API to read GPS coordinates from the device. No native app or hardware driver is required. Accuracy depends on the hardware available — devices with dedicated GPS receivers will report sub-meter accuracy, while devices relying on Wi-Fi positioning will have coarser estimates.

The data flow for every position update is:

  1. The client's Geolocation watchPosition callback fires when the device GPS reports a new fix. Position updates are also emitted on a configurable interval (default: every 5 seconds) even when the raw position has not changed, to confirm the user is still connected.
  2. The client emits a position:update Socket.IO event to the server, including the full position payload.
  3. The server validates the payload, writes the position to PostgreSQL using a PostGIS GEOGRAPHY(Point, 4326) column, and immediately broadcasts a position:broadcast event to all connected clients.
  4. All clients receive the broadcast, update or create the corresponding marker on the map, and refresh the user roster panel with the new position data.

Position records are indexed using a PostGIS spatial index (GIST) on the geography column, enabling efficient bounding-box and distance queries for track retrieval and area-based filtering.

The update interval is configurable via the POSITION_INTERVAL_MS environment variable. Reducing it below 2000 ms is not recommended on low-power hardware as it increases Socket.IO message throughput and database write frequency proportionally.

Position Payload

Each position:update event carries the following JSON payload. All fields except latitude and longitude are optional; missing fields are stored as NULL in the database.

// Socket.IO event: position:update
{
  "latitude":    34.052235,   // required — WGS84 decimal degrees
  "longitude":   -118.243683, // required — WGS84 decimal degrees
  "altitude_m":  71.4,        // optional — meters above WGS84 ellipsoid
  "heading":     247.5,       // optional — degrees clockwise from true north
  "speed_mps":   1.3,         // optional — speed in meters per second
  "accuracy_m":  4.2,         // optional — horizontal accuracy radius in meters
  "recorded_at": "2026-02-27T14:32:00.000Z" // ISO 8601 UTC timestamp
}

The server attaches the authenticated user's user_id and callsign before writing to the database and before broadcasting position:broadcast, so clients do not need to include identity information in the payload.

Map Display

Each connected user with a known position is rendered on the map as a labeled marker. The marker displays the user's callsign directly on the map canvas so operators can identify team members at a glance without interacting with the roster panel.

Marker behavior:

  • Heading indicator — when heading data is available, the marker rotates to show the direction of travel as a directional arrow or wedge.
  • Accuracy ring — an optional translucent circle represents the reported GPS accuracy radius around the marker.
  • Stale indicator — when the server has not received a position update from a user within the configured stale threshold, a position:stale event is broadcast. The marker changes to a visually distinct state (muted color, dashed outline) to indicate the position may not reflect the user's current location.
  • Own marker — the current user's marker is visually differentiated (brighter or with a distinct style) so operators can locate themselves on the map quickly.

Clicking a user's marker opens a popup with their callsign, last known speed, heading, and the timestamp of the most recent position fix.

Track Trails

GroundWave stores the complete position history for each user in PostgreSQL. The track trail feature renders this history as a LineString on the map, showing the path a user has taken over a configurable time window.

Track data is retrieved via the REST API:

GET /api/positions/:userId/track
Returns the position history for a user as a GeoJSON LineString.
Query: hours=N — number of hours of history to return (default: 1, max: 24)

The track line is rendered as a semi-transparent polyline on the map. Older segments fade out toward the trail's origin, providing a directional sense of movement without cluttering the map.

Data Retention

Position data accumulates continuously during operations. To prevent unbounded database growth, GroundWave includes a scheduled job runner that automatically prunes position records older than the configured retention window.

The retention period is configured via the POSITION_RETENTION_HOURS environment variable. The default is 24 hours. Positions older than the retention window are deleted by a background job that runs on a configurable cron schedule.

Admins can also perform an immediate bulk deletion of all position history through the admin dashboard:

DELETE /api/admin/data/positions
Deletes all stored position records. Requires admin role. This action is immediate and irreversible.

The bulk delete endpoint removes all position records for all users. Export position data as GeoJSON or GPX from the admin Data Export tab before issuing this command if a historical record is required.

Battery Monitoring

For deployments running on battery-powered Linux hardware (Raspberry Pi, Intel NUC with UPS hat), GroundWave reads battery level and charging state from the Linux sysfs power supply interface (/sys/class/power_supply/).

Battery information is surfaced in the admin dashboard System Status tab as a dedicated card showing current charge percentage and charging status. The card is conditionally rendered — it appears only when a compatible power supply interface is detected on the host system. On desktop machines or containers where the interface is absent, the card is hidden.

This allows field operators monitoring the admin dashboard to track server battery level alongside system health metrics such as CPU load, RAM usage, and connected client count — all from a browser on any connected device.