Balloon Navigator

Balloon Navigator

Loading...

Extensions

Extensions (currently an experimental feature) are small programs you can install in Balloon Navigator to add more features or modify how it works to suit you better.

Installing extensions

If your extension is inside .zip file, unpack it first into a folder.

Open Settings ALT + S, go to Extensions tab and click on Upload extension button. Go to the extension folder, select all files and upload them.

After successful installation, enable extension with checkbox and reload the app to apply changes.

Install extensions only from trusted sources. They have access to your map and GPS data and are able to modify, delete or send it outside your app.

Development

Extension is a HTML file included in the app as a sandboxed iframe with allow-scripts. It has access to app data through Channel Messaging Web API.

Being based on iframes, extensions give you great freedom to do everything a web browser allows you to:

  • connect to external services
  • build dynamic pages
  • use Javascript libraries, including frameworks like React, Vue or Svelte
  • spawn web workers for heavy asynchronous computation

Whatever you build, just make sure to bundle everything (HTML, JS and CSS) into a single HTML file.


Extensions require manifest.json file with following structure:

{
  "name": "Example Extension", // required
  "description": "An example extension", // optional
  "version": "1.0", // optional
  "panelShortcutKey": "H" // optional
}

Each HTML file is registered to predefined place in the app based on their file names. Currently allowed registrations are:

  • map-overlay (map-overlay.html) - it covers the whole map window and it cannot be interacted with (all clicks are passed through to map). Always active.
  • panel (panel.html) - dynamic panel which can be moved around and collapsed (similar to Windreader or Selected). When collapsed, iframe is destroyed - use map-overlay if you want something to be always working in background.

This is how extension structure looks like:

/
├── manifest.json
├── map-overlay.html
└── panel.html

The best way to get started developing new extensions is to install and modify example extension. It provides necessary code to enable communication with the app and several example API calls.

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
}

Available API objects

db

db provides access to underlying Dexie database which stores maps, waypoints, tracks etc.

You can explore how database looks by opening browser developer tools -> Application -> Storage -> IndexedDB -> DB

Available methods are listed in Dexie documentation

Example calls to db:

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

// response
{
  jsonrpc: "2.0",
  result: Feature[]
  id: 1
}

let arguments = { key: "planet" }

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

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.

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.

Derived geometry (waypoint circle polygons, task rings, UTM grid lines etc.) is computed internally and pushed straight to the map renderer. It is not exposed through the collections, but it is possible to get access to it through map.glmap object.

{
  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, // raw user features (Point, LineString, Polygon)
    tracks: FeatureCollection,
    liveTracking: FeatureCollection,
    liveTrackingTracks: FeatureCollection,
    windreader: FeatureCollection, // windlines
    position: FeatureCollection, // current GPS position (arrow, track line, target line)
    flightAnalysis: FeatureCollection // i.e. best result markers
  },
  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
}

Example calls to map:

// read current map center
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
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
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
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 user persistent settings (read only)

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

// response
// note: settings schema can be changed by future app updates
{
  jsonrpc: "2.0",
  result: {
    "gps": {
      "enabled": Boolean,
      "source": String,
      "serialport": {
        "baudRate": Int,
        "bufferSize": Int,
        "dataBits": Int,
        "flowControl": String,
        "parity": String,
        "stopBits": Int
      }
    },
    "map": {
      "follow_position": Boolean,
      "follow_rotation": Boolean,
      "style": String,
      "layers": {
        "powerLines": Boolean,
        "tracks": Boolean,
        "liveTracking": Boolean,
        "liveTrackingTracks": Boolean,
        "graticule": Boolean,
        "hillshading": Boolean
      }
    },
    "interface": {
      "panels": {
        "windreader": {
          "active": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "target": {
          "active": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "selected": {
          "active": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        "gps": {
          "active": Boolean,
          "position": {
            "x": Int,
            "y": Int
          },
          "dimensions": {
            "width": String,
            "height": String
          }
        },
        (...) // can be more if extensions are enabled
      },
      "defaultCoordinatesSwitch": String,
      "altitudeUnit": String,
      "speedUnit": String,
      "distanceUnit": String,
      "snapToShortUTM": Boolean
    },
    "windreader": {
      "precisionMeters": Float,
      "syncEnabled": Boolean,
      "syncRadius": Float
    },
    "synced": Boolean,
    "serverSyncedAt": UnixTimestamp
  },
  id: 1
}

windreader

windreader returns current windreader readings (read only)

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

// response
{
  jsonrpc: "2.0",
  result: {
    String: { // altitude as String, ex. "500"
      "headings": Float[] // last 20 recorded headings
      "speeds": Float[], // last 20 recorded speeds
      "sources": String[], // used by live tracking sync
      "avgHeading": Float, // calculated from headings array
      "avgSpeed": Float, // calculated from speeds array
      "updatedAt": UnixTimestamp // time of last update at this altitude
    },
    (...)
  },
  id: 1
}