Balloon Navigator

Balloon Navigator

Loading...

Extensions API

Extensions API is based on JSON RPC 2.0. Communication between extensions and app happens through postMessage method.

You use postMessage to send JSON RPC requests calls and in return you receive JSON RPC responses.


Sending and receiving data

To send data, call proper methods with arguments on objects available in API, for example to change map zoom you should call setZoom:

// all requests are valid
postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.setZoom(5)"
})

postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.setZoom()",
  params: 5 // argument value for last method
})

postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.setZoom()",
  params: [null, 5] // null argument for "glmap", 5 for setZoom
})

// since requests have no ID present, they are treated as JSON RPC notifications and not responded to.

Send complex arguments to methods either by stringified JSON (objects and arrays are supported inline) or by params key:

let arguments = {
  center: [9.7, 52.3], // [longitude, latitude]
  zoom: 10,
  bearing: 90,
  duration: 5000
}

// all requests are valid
port.postMessage({
  jsonrpc: "2.0",
  method: `map.glmap.flyTo(${JSON.stringify(arguments)})`
})

port.postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.flyTo()"
  params: [null, arguments] // if params is array, each element is used as argument for subsequent methods.
  // In this case, first element is argument for "glmap" method (null), second (arguments) for "flyTo" method.
})

port.postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.setCenter([9.7, 52.3])" // inline JSON arrays work too
})

Communication accepts only simple objects supported by structured clone algorithm.

For example, you cannot get the full Map object as it contains methods and functions which are not supported by postMessage:

// request
postMessage({
  jsonrpc: "2.0",
  method: "map.glmap"
  id: 1
})

// results in error:
// DataCloneError: Failed to execute 'postMessage' on 'MessagePort': (...) could not be cloned.

For the same reason, send method calls which return the Map object itself (like flyTo, setZoom or setCenter) as notifications (without id).

You can query methods which return simple objects:

// request
postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.getZoom()"
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: 5
  id: 1
}

API reference

Available API objects

ObjectProvides
dbLocal records in the browser’s Dexie database
mapCurrent map, camera, and rendered feature collections
gpsCurrent GPS state
settingsCurrent user settings (read only)
windreaderCurrent aggregated wind readings (read only)

db

db provides access to Balloon Navigator’s local IndexedDB database (DB) through Dexie. You can inspect it in browser developer tools under Application → Storage → IndexedDB → DB. Dexie documentation lists the available table methods.

TableWhat it stores
settingsThe app’s settings object, stored in row 0
featuresSaved GeoJSON waypoints and drawn shapes.
tracksRecorded or imported GeoJSON flight tracks.
mapsBasemap metadata and downloaded or uploaded map files.
windreadingsLocal, imported, and received wind observations.
flightpathsStatic copies of predicted flight paths placed on a map.
logsApp diagnostic events.
extensionsInstalled extension manifests and files.
dataRecords shared between pilot and crew.

Query examples

// request
port.postMessage({
  jsonrpc: "2.0",
  method: "db.features.toArray()",
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: [/* GeoJSON features */],
  id: 1
}
let arguments = { key: "planet" }

// both requests are valid
port.postMessage({
  jsonrpc: "2.0",
  method: `db.maps.where(${JSON.stringify(arguments)}).toArray()`,
  id: 1
})

port.postMessage({
  jsonrpc: "2.0",
  method: `db.maps.where().toArray()`,
  params: [null, arguments, null],
  id: 1
})


// response
{
  jsonrpc: "2.0",
  result: [{
    key: "planet",
    name: "Planet",
    description: "Map of Earth",
    url: "---",
    fileSize: 72019488577,
    source: "online-only",
    official: true,
    center: [-25, 40],
    zoom: 2
  }],
  id: 1
}

map

map provides access to the heart of Balloon Navigator - the MapLibre GL JS mapping library.

It describes the current map display; saved waypoints and tracks are in db.features and db.tracks.

MapLibre’s Map object is available as map.glmap. It handles both map interaction and the camera - use it for zooming, panning, rotation etc.

Map data is available as GeoJSON FeatureCollections in map.collections, keyed by source id, ex. map.collections.waypoints holds all waypoint features displayed on the map.

{
  glmap: Map, // maplibregl.Map instance. Available methods are listed in MapLibre GL JS documentation: https://maplibre.org/maplibre-gl-js/docs/API/
  ready: Boolean, // true once the style has loaded and overlay sources/layers are registered
  terradraw: Object, // MaplibreTerradrawControl instance (only during active draw/edit sessions)
  basemapUrl: String, // URL of the active basemap .pmtiles
  collections: { // plain GeoJSON FeatureCollections, keyed by source id
    waypoints: FeatureCollection,
    tracks: FeatureCollection,
    liveTracking: FeatureCollection,
    liveTrackingTracks: FeatureCollection,
    windreader: FeatureCollection, // windlines
    position: FeatureCollection, // current GPS position (arrow, track line, target line)
    flightPath: FeatureCollection // simulated balloon flight path
  },
  selectedId: String, // id of the selected feature (or null)
  targetId: String, // id of the target feature (or null)
  panelsWidth: Number // width of opened side panels in px
}

For performance reasons, derived geometry (waypoint circle polygons, task rings, UTM grid lines, marker-drop overlays, measurements, etc.) is computed internally and pushed straight to map renderer. These are not exposed through the map.collections, but it is possible to read them with map.glmap.getSource(sourceId); application code owns their contents and may replace them whenever GPS, wind, selection, or pointer state changes.

Source idOwnerGeometryNotes
markerDropBestResultMarker drop calculatorPointRed X showing the geometric Best result.
markerDropPathMarker drop simulatorLineStringPurple descent path; the only marker source with MapLibre line metrics enabled.
markerDropImpactMarker drop simulatorPointBest drop diamond and its distance-to-waypoint property.
measurementsMeasure toolLineString, Polygon, PointFinished and in-progress measurement geometry; updated independently during pointer movement.

Query examples

// read current map center
port.postMessage({
  jsonrpc: "2.0",
  method: "map.glmap.getCenter()",
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: { lng: 9.7, lat: 52.3 },
  id: 1
}
// get all waypoints displayed on the map
port.postMessage({
  jsonrpc: "2.0",
  method: "map.collections.waypoints",
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: { type: "FeatureCollection", features: Feature[] },
  id: 1
}
// get id of the currently selected feature, then look it up in the waypoints collection
port.postMessage({
  jsonrpc: "2.0",
  method: "map.selectedId",
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: "waypoint-1751558400000",
  id: 1
}

gps

gps returns current GPS position data (read only)

// request
port.postMessage({
  jsonrpc: "2.0",
  method: `gps`,
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: {
    enabled: Boolean,
    status: String,
    longitude: Float,
    latitude: Float,
    altitude: Float,
    accuracy: Float,
    altitudeAccuracy: Float,
    speed: Float,
    heading: Float,
    time: DateTime,
    satsActive: [],
    satsVisible: [],
    fix: String,
    timestamp: Integer,
    nmea: String
  },
  id: 1
}

settings

settings returns the current preferences (read only). The app persists this object in db.settings.

// request
port.postMessage({
  jsonrpc: "2.0",
  method: "settings",
  id: 1
})

// response
// note: following settings schema is an example and will be changing over time as the app is updated
{
  jsonrpc: "2.0",
  result: {
    "gps": {
      "enabled": Boolean,
      "source": String, // ex. "serialport", "simulator"
      "serialport": {
        "baudRate": Int,
        "bufferSize": Int,
        "dataBits": Int,
        "flowControl": String,
        "parity": String,
        "stopBits": Int
      },
      "simulator": {
        "initialAltitude": Float,
        "heading": Float,
        "speed": Float,
        "vario": Float,
        "headingChange": Float
      }
    },
    "map": {
      "follow_position": Boolean,
      "follow_rotation": Boolean,
      "style": String,
      "projection": String, // ex. "mercator", "globe"
      "camera": {
        "center": [Float, Float], // [longitude, latitude]
        "zoom": Float,
        "bearing": Float,
        "pitch": Float
      },
      "layers": {
        "powerLines": Boolean,
        "tracks": Boolean,
        "liveTracking": Boolean,
        "liveTrackingTracks": Boolean,
        "graticule": Boolean,
        "utmGrid": Boolean,
        "hillshading": Boolean
      }
    },
    "interface": {
      "panels": {
        "windreader": {
          "active": Boolean,
          "visible": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "target": {
          "active": Boolean,
          "visible": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "selected": {
          "active": Boolean,
          "visible": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "gps": {
          "active": Boolean,
          "visible": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "temperature": { ... },
        "flightpath": { ... },
        "task3d": { ... },
        (...) // can be more if extensions are enabled
      },
      "locale": String, // null if not set
      "defaultCoordinatesSwitch": String,
      "altitudeUnit": String,
      "speedUnit": String,
      "distanceUnit": String,
      "areaUnit": String,
      "varioUnit": String,
      "snapToShortUTM": Boolean,
      "snapToPoints": Boolean,
      "showCursorBearingDistance": Boolean,
      "labelScale": Float
    },
    "windreader": {
      "precisionMeters": Float,
      "syncEnabled": Boolean,
      "syncRadius": Float,
      "sourceFilter": Array[String],
      "importSync": Boolean,
      "receiveImportSync": Boolean,
      "constantLineLength": Boolean
    },
    "flight": {
      "pathEnabled": Boolean,
      "envelopeVolume": Float,
      "envelopeVolumeUnit": String,
      "payloadKg": Float,
      "finalDescentRate": Float,
      "profile": Array
    },
    "temperature": {
      "configured": Boolean,
      "deviceName": String // null if not set
    },
    "synced": Boolean,
    "serverSyncedAt": UnixTimestamp
  },
  id: 1
}

windreader

windreader returns current aggregated readings (read only). Individual observations are stored in db.windreadings.

// request
port.postMessage({
  jsonrpc: "2.0",
  method: "windreader",
  id: 1
})

// response
{
  jsonrpc: "2.0",
  result: {
    String: { // altitude in meters as String, ex. "500"
      "avgHeading": Float,
      "avgSpeed": Float,
      "updatedAt": UnixTimestamp, // time of last update at this altitude
      "sourceNames": Array[String] // list of names of external sources feeding data for this altitude.
    },
    (...)
  },
  id: 1
}