ZYRNTOPO
Documentation

ZYRNTOPO Documentation

How the ZYRN apps work. ZYRNTOPO is the offline topographic map for Android and the browser, and ZYRNMAPS is its native Android sibling. ZYRN Workstation is the desktop app for Windows, macOS and Linux, and the ZYRN TAK server links all of them with ATAK, WinTAK and iTAK on hardware you control. The apps share one account, one Pro license, your map items and the same plugins.

The core apps are free. ZYRNTOPO and ZYRN Workstation ask you to sign in with a free account, and ZYRNMAPS does not. Your map, your tracks and your markers stay on your device apart from the items you sync to your own account, which you can turn off.

Free and Pro. Free: offline maps with unlimited downloads, GPS and track recording, drawing and measuring, coordinates and the MGRS grid, the 3D globe, viewshed, photo waypoints, live weather radar, your own WMS, WMTS and WFS map services, GPX, KML, GeoJSON and CSV in and out, and your markers and tracks synced between your own signed-in devices. ZYRNTOPO Pro adds 3D terrain, true-scale PDF map sheets, Team Sync with Team Chat, nearby TAK devices, TAK servers, the Meshtastic radio, MAVLink drones and the power lines overlay. Pro is $79.99 once, permanent, or $2.99 a month until you cancel; either covers Android, the web app and ZYRN Workstation on one account. Anything that needs Pro carries a PRO tag before you tap it. See Account & Pro.

Getting Started

Install, first launch, and finding your way around.

Map Interface

Base layers, overlays, 3D terrain and controls.

Offline Maps

Download an area before you lose signal.

Team Sync

Internet, TAK, Meshtastic and drones.

Search & Rescue

Periods, team members, clues and gear.

Import & Export

GPX, KML, GeoJSON, CSV and PDF.

ZYRNMAPS

The native Android client.

ZYRN Workstation

The desktop app for Windows, macOS and Linux.

TAK Server

Run your own for the whole team.

Plugins

Add tools to every app, write your own and share them.


Installation

ZYRNTOPO runs on Android and in the browser. Use whichever fits the device in your hand; your account and Pro carry across both.

Android

  1. Install from Google Play. Updates arrive automatically.
  2. On a Fire tablet, or any device without Google Play, install from the Amazon Appstore instead. It handles installation and updates for you.
  3. Open the app. A short setup asks how you will use it, your units and your starting map; then it is ready, with no sign-in.

The points-of-interest dataset (peaks, water, trailheads, shelters and more, about 1.4 GB) is not part of the download from the store. The app fetches it once in the background the first time it runs, so open the app for the first time on Wi-Fi; after that, POI search needs no connection. When a newer dataset is published, the saved copy updates itself over Wi-Fi.

Web / PWA

Open app.zyrntopo.com in any modern browser. For offline use, install it as an app: in Chrome or Edge, click Install app in the address bar; in Safari, use Share → Add to Home Screen. See Web / PWA for what the browser version can and cannot do.

Desktop and server

On Windows, macOS and Linux, the desktop app is ZYRN Workstation, installed with one command: see ZYRN Workstation. To run your own TAK server for your team, see ZYRN TAK server. Both install commands check what they download before running it.

First Launch

The first time you open ZYRNTOPO, a short setup asks three things: how you will use it (recreation, search and rescue, and so on), your theme and units, and which map to start on. Without Pro, a last step shows what Pro adds; you can skip it. Everything you choose can be changed later in Settings.

The app then explains why it wants your location and asks with the system dialog. Location is used for your position on the map, tracks, route following and nearby field information. It is foreground-only except while you record a track: the first time you tap Record Track, the app asks separately for background location so the recording continues with the screen off. The microphone is requested the first time you record a voice note, the camera the first time you scan a QR code, and Bluetooth the first time you connect a radio or receiver.

You can decline any permission. The app still works; only the feature that needed it is unavailable until you grant it in Settings.

The default map is VersaTiles Eclipse, a detailed street and terrain map. It is hosted, so it needs a connection until you download an area. Your base layer, map position and zoom are remembered and restored every time you open the app.

Finding Your Way Around

  • The map fills the screen. Drag to pan, pinch or scroll to zoom, twist with two fingers to rotate.
  • The bottom bar holds Tools (drop a marker, draw a line, measure, see point info), Layers (base map, overlays, Map Styling) and Items (everything you have created, in folders).
  • The menu is grouped by task:
    • Map: Save Map, Clear Map, Map Items, Saved Maps and Map Data (import a map, download a region, open one from a link).
    • Tools: Measure, Point Info, Download Map Area, Print to PDF, Gear / Pack List and Plugins.
    • Map setup: Map Layers, Settings, Import Map, Export Map and Clear Tile Cache.
    • Team: Team Members, Team Sync, Meshtastic radio and Team Chat.
    • Links to this documentation, your Account, Help & FAQ and Send feedback.

Long-press anywhere on the map to hide the on-screen controls for an uninterrupted view; long-press again, or right-click on a computer, to bring them back. They also come back the next time the app opens.

Help & FAQ in the menu answers the common questions and works offline, because it is built into the app.

Account & Pro

ZYRNTOPO and ZYRN Workstation ask you to sign in with a free account when they open. If you are offline or the account server cannot be reached, they let you skip it and sign in later. ZYRNMAPS does not ask. Your account gives you two things:

  • Your map items on all your devices. Markers, lines, areas and tracks sync between the devices you are signed in on, in real time. Turn it off with Sync map items on the Account page.
  • Pro, if you buy it. The license is checked on your device and keeps working offline; it refreshes in the background when you are online.

Sign in with an email address and password, or with Google, Microsoft or Yahoo. The same account works in ZYRNTOPO, ZYRNMAPS, ZYRN Workstation, the forum and the plugin submissions on this site. Manage it from Menu → Account or on zyrntopo.com/account, where you can:

  • set your display name (your callsign) and picture;
  • see every app and browser signed in to the account, sign one out, or sign out all the others at once;
  • add a password next to a Google, Microsoft or Yahoo sign-in, change your email address, and read the last 25 sign-ins and account changes;
  • download everything the account holds as JSON: profile, sign-in methods, license and payments, devices, synced map items, friends, plugin submissions and forum posts;
  • close the account, which erases your license, sessions, picture, profile, synced map items and friends, and withdraws any plugins you published. Forum posts stay and show as "Former member".

Pro

Pro is $79.99 once, or $2.99 a month until you cancel. It belongs to the account, not to a device: sign in anywhere and Pro unlocks there, and the license keeps working offline. A monthly plan keeps Pro to the end of the period you paid for after you cancel. Pro can also come from a trial, which ends on its date and leaves the app working as free, or from a seat in an organization's plan. Transfer Pro on the account page moves a license to another person's existing account.

Bought Pro but the app says Free? Open Settings → Account and use Restore purchase. Use the same after reinstalling or moving to a new device. If it still shows Free, send feedback with the email address you bought it under.

Base Layers

Tap Layers in the bottom bar. The current base map is the top row; tap it to open the picker, grouped by category with a preview of each layer. Twenty base maps are built in and free to use, 54 more work with your own key from a map provider (see Maps with your own key), and a catalog adds more than 1,300 regional and specialist imagery sources.

GroupLayersGood for
Topo & terrainUSGS Topo, USGS Imagery + Topo, OpenTopoMap, NatGeo World, Esri World Topo, Esri USA Topo (Historical)Contours, trails and terrain; historical USGS quadrangles
ImageryEsri Satellite, VersaTiles Satellite, USGS Imagery, Esri Firefly Imagery, Sentinel-2 CloudlessAerial and satellite views; Firefly is muted so your markers stand out
AeronauticalFAA VFR SectionalAirspace and aviation planning in the US
WaterEsri Ocean Basemap, EMODnet Bathymetry, NOAA Nautical Charts (ENC)Depths, coastlines and water search
Street & generalOpenStreetMap, Esri World Street, OSM Humanitarian, CyclOSMRoads, towns and cycling
VectorVersaTiles Eclipse (default)A detailed dark map that stays sharp at every zoom
Zoom in all the way. Every base layer keeps zooming to z22. Past a source's deepest detail, its last tiles are scaled up rather than the map going blank.

Maps with your own key

Some map providers only serve their maps to their own customers. If you have an account with one, add your key and its maps join the picker: Google (roadmap, satellite, hybrid and terrain), Bing, Azure Maps, Cesium ion (Bing and Google imagery), Mapbox, MapTiler, HERE, TomTom, Stadia Maps (including the Stamen styles), Thunderforest and Ordnance Survey for Great Britain. Add keys under Map Data → Map provider keys. Each provider's row links to the page where you get a key and says what to switch on there, and the app checks a key when you save it.

Keys stay on your device. Maps from a key stream online only and are never saved in an offline download, so use one of the built-in maps for an area you need offline.

Google needs the Map Tiles API. Switch it on in the key's Google Cloud project, which needs a billing account; Google bills only past its free monthly allowance. Google's demo keys do not include the Map Tiles API, so they can't load map tiles, and the app tells you when that is the reason.

Map Styling

Map Styling tweaks in the Layers panel restyle any base map, including satellite and your downloaded maps: all roads, highways with route shields and names, train tracks, borders with names, national and state parks, power lines, contour lines with a choice of interval, water and rivers, trails, offroad and gravel, campgrounds and viewpoints, live OpenStreetMap peaks, fishing spots and boat launches, and business labels. They stay on when you switch base maps and work offline once the area is saved. Map Legend, beside the Map Styling title, explains every symbol.

Stacking maps and quick setups

Add Base Layer stacks a second base map or downloaded map over the first, each with its own opacity, for example USGS Topo contours over satellite. The Catalog (Customize) lets you choose which layers the menu offers, with Reset To Defaults one tap away.

One-tap presets set up a sensible base map and overlays for an activity: Hike, Snow / Ski (hillshade, slope angle and avalanche danger), SAR and Off-road. The same panel has a brightness slider and the night mode switch.

Imagery Catalog

The catalog holds over 1,300 worldwide sources from the OpenStreetMap Editor Layer Index: regional aerial photos, government topo maps, historical photos and maps, and elevation layers. Open Layers → Custom WMS / WMTS → Browse Imagery Catalog and filter by name, type, category or country. Add turns an entry into one of your own map sources, which then appears in the picker with the built-in layers.

Catalog sources belong to the organizations that publish them, and many were licensed with OpenStreetMap editing in mind. Check a source's terms before relying on it for your own work.

To add any other map service, tap Add WMS/WMTS Source at the bottom of the Layers panel and enter its address. Your own WMS, WMTS and WFS services are free to use.

Overlays

Overlays draw on top of any base map. Add one from Layers → Add an overlay; each active overlay gets its own opacity slider, and you can run as many at once as you like. Overlays marked online-only need a connection and hide themselves while you are offline.

Terrain

  • Hillshade (worldwide), Shaded relief (USGS, US only) and Elevation tint (USGS 3DEP).
  • Slope Angle: terrain colored by steepness, computed on your device. The avalanche-oriented scale keeps gentle ground clear and heats up through the critical band: 27–29° yellow, 30–34° orange, 35–45° red, where most slab avalanches release, 46–50° purple, 51–59° blue, 60° and over gray.
  • Slope Aspect: the compass direction each slope faces, in eight colors, for sun exposure, snow loading and avalanche aspect.
  • Sun & shade: which slopes are in sun or shade for a time you choose.
  • Custom terrain coloring: color the ground by elevation or slope bands you set.

Slope, aspect and the UTM / MGRS grid are calculated on your device from elevation data, so they work offline once the terrain for the area is saved.

Topographic & reference

  • UTM / MGRS grid: a labeled reference grid, see Coordinate Display.
  • Quad index (USGS 7.5′ quadrangles) and Reference labels for imagery.
  • Country and state borders are always drawn and work offline.

Route discovery, water & marine

  • Public Lands (BLM) and Ski pistes.
  • OpenSeaMap seamarks, Hydrography (USGS NHD), Maritime limits / EEZ (NOAA) and Watersheds (USGS WBD).

Weather & hazards online only

  • Wind barbs: standard meteorological barbs across the view, refreshed every 15 minutes. A half barb is 5 kt, a full barb 10, a pennant 50; the barbs point along the true wind and do not turn with the map.
  • Weather radar (NEXRAD), refreshed about every 5 minutes.
  • Smoke forecast near the surface (NOAA).
  • Active wildfires (NIFC perimeters) and Satellite fire detections from the last 48 hours (VIIRS). Tap a perimeter for its name, size and start date.
  • Avalanche danger: US forecast zones colored by the current danger rating, with a legend.
  • Snow depth (NOAA NOHRSC).

United States field data

  • Trails & recreation: trails on national forests (USFS), trails nationwide (USGS), the USFS Motor Vehicle Use Map and USFS recreation sites.
  • Access & transport: USFS roads, and roads including 4WD (USGS).
  • Land status: wilderness areas, tribal lands and reservations, hunt units on wildlife refuges.
  • Hazards: FEMA flood zones and earthquakes from the last 24 hours (USGS).
  • Ground cover: land cover (NLCD) and cropland (USDA).
  • Survey & names: the PLSS survey grid (BLM), USGS place names, and wetlands (NWI).
Any map as an overlay. Beyond the overlays above, any base map, imported map or downloaded map can be stacked with its own opacity: tap Add Base Layer in the Layers panel.

3D Terrain View

Tap 3D in the Layers panel to tilt the map into real terrain. Any base layer and your map items drape over it. 3D terrain is part of ZYRNTOPO Pro; the 3D globe beside it is free.

  • Drag with two fingers to tilt, twist to rotate; the tilt slider does the same.
  • Raise the terrain exaggeration in Settings to make gentle ground easier to read.
  • The viewshed tool and the slope overlays use the same elevation data.

3D without a connection

  1. Frame the area you need.
  2. Open Layers → Save Terrain and confirm the size estimate.
  3. From then on, 3D uses the saved terrain for that area, with no connection.
For a fully offline 3D map, download the area and use Save Terrain in the same view. The two are saved separately and combine whenever you switch to 3D.

Vector Maps

Besides picture-tile maps, ZYRNTOPO shows vector maps, which stay sharp at every zoom and take up less space. The default VersaTiles Eclipse map is one, and it is ready to use with no import.

  • Import any .pmtiles file, picture or vector, with Settings → Offline Maps → Import. Archives from Protomaps, OpenFreeMap and BBBike work, as does your own build.
  • An imported vector map gets a style made for it automatically from the layers it contains, and drapes over 3D terrain like any other map.
  • Choose it in the base-layer picker under My Maps (offline).

Controls & Compass

  • Zoom buttons (+/−) sit in the bottom-right corner.
  • Download map area is in the top bar: tap it, frame the box and download. See Downloading an Area.
  • Compass: tap to turn the map back to north-up; long-press for heading-up, where the map turns with you.
  • Scale bar: metric and imperial together, so either reading is there at a glance.
  • Long-press the map to hide the on-screen controls; long-press again to bring them back.
  • Tools in the bottom bar holds every drawing, measuring and field tool, grouped by task.

Night Mode

The night button in the Layers panel cycles the map through Off → Dim → Red light, to protect your night vision without touching the controls. In Settings → Display you can also choose Auto, which follows your device's dark mode. Your choice is remembered.

Coordinate Display

Your position, or the map center when GPS is off, is shown at the bottom of the screen together with bearing and elevation. Choose the format in Settings → Coordinates → Coordinate Format:

FormatExample
Decimal Degrees (DD)39.0835° N, 108.5601° W
Degrees Minutes Seconds (DMS)39°05'00" N, 108°33'36" W
Degrees Decimal Minutes (DDM)39° 05.010' N, 108° 33.606' W
UTM12S 741234 4328765
MGRS12SVJ4123428765

Tap the coordinates to copy them. Use the format the people you work with use: mismatched formats are a classic way to send someone to the wrong place.

UTM / MGRS grid overlay

Add UTM / MGRS grid from Layers → Add an overlay → Topographic / Reference for a labeled reference grid over any map. The spacing follows the zoom (100 km, 10 km, 1 km), it has its own opacity slider, and it works offline.


Markers & Waypoints

Tap Tools → Marker, then tap the map where it goes. To mark exactly where you stand, use the drop-marker button beside the GPS button. The marker's details open straight away.

Marker properties

  • Name and notes.
  • Icon: dozens of typed symbols, the same on every device, including camp, water, summit, hazard, medical and wildlife. A marker with no icon of its own shows as a red dot.
  • Color, folder, and photo: a photo waypoint keeps where it was taken and which way the camera faced.
  • SAR properties: mark it as a clue or a find, and assign it to an operational period.

Tap a marker to open its details. In Items, every item has Go To, and point items also have Point Info, Directions, Navigate To and Transform. Tools → Photo waypoint takes a photo and pins it where you stand.

Averaged waypoint

For a more accurate point, use Tools → Averaged Survey Waypoint. Hold the device still; the dialog shows the number of fixes, the running average and an estimated accuracy that tightens as fixes accumulate. Save waypoint drops a marker at the averaged position with its accuracy noted.

Lines & Routes

Tap Tools → Line, then tap the map to place points. The running distance shows as you go; tap Finish when done.

  • Each segment shows its length in your units.
  • Edit a line to drag, insert or delete points, and to set its name, color and folder.
  • Free Draw (Tools → Free Draw): drag on the map to sketch. Each stroke saves as a line you can rename, recolor or move later.
  • Bearing Line (Tools → Bearing Line): a line from a point along a bearing you enter.

Elevation profile

Open any line, track or route and tap Elevation for its profile: total distance, gain, loss, lowest and highest point in your units. It needs a connection to fetch the elevation.

Polygons & Areas

Tools → Polygon: tap to place the corners and Finish to close it. Its area shows in acres, hectares, square kilometers or square miles.

  • Highlight (Tools → Highlight): shade an area with a see-through fill to mark a search segment, a hazard or an area of interest without hiding the map.
  • Circle (Tools → Circle): press at the center and drag out; the radius shows as you size it.
  • Buffer (Tools → Buffer): an area a set distance around a point, line or area.
  • Sector (Tools → Sector): a pie-wedge search area from a bearing and radius.
  • Assignments (Tools → Assignment — Line or Assignment — Area): search assignments that join an operational period.

Range Rings

Tools → Range Ring draws concentric rings around a point. Set the number of rings (1 to 10), the spacing, or your own radius for each ring. The set is one item you can edit or delete from Items.

Folders & Organization

Everything you create is listed under Items in the bottom bar, in folders such as "Day 1 Search", "Water Sources" or "Hazards".

  • Create a folder with a name and color, and move any item into it.
  • Hide or show a whole folder on the map at once.
  • Export one folder on its own.
  • Select several items for bulk actions.

Clearing caches never deletes your items: items, downloaded maps and the tile cache are stored separately.

Operational Periods

An operational period is a planning cycle, usually one 12- or 24-hour shift. Create one with Tools → Search Period.

  • Give it a name, a start time and a status.
  • Assign items to it from their details.
  • Hide the items of periods that are not active.
  • Export one period on its own.
For an incident run under ICS, create one period per official operational period and assign that period's assignments and finds to it before exporting for the Situation Unit.

Saved Maps & Sharing

Menu → Save Map saves what you have drawn (items, folders and search periods) as a named map. Menu → Saved Maps lists them; open one to load it, and Clear Map starts a fresh one.

Share a map by QR code, with no signal

  1. In Saved Maps, tap Share on the map you want to send.
  2. The other person taps Scan in their own Saved Maps and points their camera at your screen.
  3. A small map is one code; a big one cycles through several. Hold steady until it says every part is captured; a missed part is picked up on the next pass.

Everything travels inside the codes, so it needs no internet, no account and no cable. Items, areas and folders come across; downloaded map tiles do not, as they are far too big for a code.

Files and map packages

A saved map can also be exported as a .zyrn file and opened on another device with Saved Maps → Import .zyrn.

To send a whole map in one file, use Settings → Data → Send map package. It writes one .zip in ATAK's data package format: the map itself, a KML copy that ATAK imports, and, if you choose, the downloaded maps that cover the area in view. ZYRNTOPO opens it as a saved map, and ATAK imports it like any other data package.


GPS Modes & Receivers

The GPS button cycles through three states:

  1. Off: no GPS; the button is gray.
  2. Follow: the map keeps you centered as you move.
  3. Heading-up: the map also turns to match the direction you are facing.

A circle around your position shows its accuracy. Under heavy canopy or in a canyon it grows large; check your position against landmarks before trusting it.

Accuracy depends on your device's GPS and how much sky it can see. On critical operations, carry a dedicated GPS as a backup.

External GNSS receivers

A separate receiver, over USB or Bluetooth, can replace the phone's own GPS. Once connected it is the position source for everything: your position, track recording and what you share with your team. Disconnect it and the device GPS takes over again. Connect one from Settings → GPS Receiver.

Receivers are read over NMEA 0183, which nearly every GNSS unit speaks. Sentences that fail their checksum are discarded rather than guessed at.

ConnectionWhere it works
USB serialAndroid app; Chrome and Edge on a computer
Bluetooth LEAndroid app; Chrome and Edge on a computer
The baud rate is found for you. Auto-detect tries the common rates in turn and tells you which of three things happened: it found the rate, something is connected but not sending NMEA, or nothing is arriving at all.
Classic Bluetooth (SPP) receivers are not supported. Many units sold as "Bluetooth GPS" use classic Bluetooth; connect those over USB instead. On Android, a USB receiver and a USB drone link cannot both be connected at once. NMEA carries no accuracy figure, so no accuracy circle is drawn for an external receiver; satellite count, fix quality and HDOP are shown instead.

Track Recording

Tools → Record Track starts recording; your track draws on the map as you move, with live distance, elevation gain, elapsed time and average speed.

On Android the recording continues with the screen off, behind a notification, once you allow background location; on the web, keep the screen on. Tap Stop for a summary (distance, gain, duration and top speed). The track is saved with your items, ready to export as GPX or GeoJSON, with its elevation profile one tap away.

Measurement Tools

Tools → Measure, then tap points on the map:

  • Distance between points, with a running total, and the bearing of the last leg.
  • Bearing between two points, from true, magnetic and grid north at once, in your bearing units.
  • Area: close the shape to get its area, in your area units.
  • Elevation profile along the path.
  • Navigate: tap the point you want to go to and guidance starts. See Navigate & Route.
  • Circular scale: three range rings around the crosshair at round distances for the current zoom, with a compass rose on the inner ring that turns with the map. Tap it again to hide it.
  • Viewshed / line of sight: tap an observer point and the ground visible from it is shaded green, computed on your device. Set the observer and target height, a maximum radius, and an optional radio mode that models how radio bends over the horizon, for siting repeaters, lookouts or signal mirrors.

Results stay on the map in a card while you keep working. Tap its × to clear it.

Field Shortcuts

  • Bearing From Heading (Tools): a line from where you stand along the way you are facing, at a length you choose (500 m, 1 km, 0.5 mi, 1000 ft and so on).
  • Bearing sight (Tools, compass icon): a full-screen compass showing the azimuth you are pointing at, magnetic or true, in degrees and mils, with the inclination angle. Shoot azimuth → map drops a bearing line from your position. It uses the device's compass and works offline.
  • Tap to measure from GPS (Settings → Map Behavior): with it on, tapping open map draws a line from your position to that spot with the bearing and distance on it. The line follows you as you move; its label has Navigate and Clear.
  • Share Field Location (Tools): share your position, or the map center if GPS is off, as a card with DD, DMS, UTM and MGRS coordinates and a map link.

How Offline Maps Work

A downloaded map is a single .pmtiles file on your device. One file per area is sturdier than thousands of cached pictures: the system does not clear it to make room, it survives restarts, and you can copy it to a teammate like any other file.

Nothing downloads behind your back: you choose the area and the zoom range. The default map is hosted and needs a connection until you save the area you care about. Do it before you leave service, not after.

What you can save

  • Every base map except Google Satellite and Google Hybrid, which Google does not allow to be saved.
  • The overlays you have on: they are saved with the base map for the same area and zooms.
  • Your own map sources, if their servers allow it.
  • 3D terrain, with Layers → Save Terrain.
  • Slope, aspect and the UTM / MGRS grid, which need nothing extra: they are worked out from the saved terrain.
  • Any .pmtiles or .mbtiles file you import; .mbtiles are converted on import.

The points-of-interest dataset is saved on your device after the first run, so POI search works offline from then on.

Using a downloaded map

A downloaded map does not switch itself on. Open the base-layer picker and choose it under My Maps (offline). To prove it works, turn off Wi-Fi and mobile data: the map should still draw, straight from the file.

Importing a file

  1. Open Settings → Offline Maps → Import, or Menu → Map Data → Import.
  2. Pick a .pmtiles or .mbtiles file. Files of many gigabytes are fine.
  3. It appears in your list with its area and zoom range, ready under My Maps (offline).

Archives from Protomaps, OpenFreeMap and BBBike work, as does your own build. Map Data → From link downloads one from an address.

Downloading an Area

  1. Open Settings → Offline Maps → Select Area on Map, or tap the download button in the top bar. A green box appears.
  2. Drag the box over the area and pull its corners to size it.
  3. Choose the lowest and highest zoom. A live estimate shows the size before you commit.
  4. Tap Download Area. The download runs in the background and carries on after a restart if it is interrupted.
  5. The map lands in My Maps (offline).

Download a whole region

Menu → Map Data → Download region saves the current base map for a country, continent or mountain range in one go, with the same zoom choice and size estimate. The same screen lists ready-made map packages.

How big will it be?

AreaZoom rangeApproximate size
One county (about 50 × 50 km)6–14about 120 MB
One county6–16about 600 MB
One state6–14about 1 GB
One state6–16about 5 GB

Each extra zoom level roughly quadruples the size. Areas of sea, forest canopy or empty ground are stored once, so real downloads are often smaller. Download on Wi-Fi.

Every zoom level for the whole world would be hundreds of terabytes, so no app ships it. Save the area you will actually be in, at the zooms you need.

Viewed Tile Cache

While you are online, map tiles you look at are also kept in a temporary cache, so recently viewed places keep drawing when the signal drops and panning stays smooth. The system may clear this cache when storage runs low, which is why a downloaded map is the dependable way to work offline. Manage it in Settings → Viewed Tile Cache.

Storage

  • Settings → Offline Maps lists every downloaded map with its size, area and zoom range. Each has Use and Remove; downloaded maps are usually by far the biggest thing the app stores.
  • Clear Tile Cache (in the menu, or Settings → Viewed Tile Cache) drops only the temporary cache and leaves your downloaded maps alone.
Your items, your downloaded maps and the tile cache are stored separately. Clearing one never touches the others.

Team Sync: Overview

Team Sync puts everyone on the same live map: positions, markers, lines, areas and tracks, plus chat. Open Menu → Team Sync. Team Sync and its links are Pro; the Team Members roster is free.

Pick your name, role and team color. The colors are the TAK team colors (Cyan, White, Yellow, Orange, Magenta, Red, Maroon, Purple, Dark Blue, Blue, Teal and Green), so you show up the same way to ATAK users. Teammates draw as contact balls with their callsign, and a teammate who reports a heading gets a direction cone. GPS switches on when you join, so your position goes out straight away.

LinkNeeds internetReaches
Session code✓ZYRNTOPO users anywhere
Nearby TAK✗ATAK, WinTAK and iTAK on the same Wi-Fi
TAK serverDepends on the serverEveryone on that server, in any TAK app
Meshtastic radio✗Radios on the same LoRa channel, for miles
MeshCore radio✗Radios on the same MeshCore channel, across many repeaters
MAVLink drone✗A drone or vehicle you connect

The links run side by side: you can be in a session, on a TAK server and on a radio at once, and the app passes what it hears between them.

When a teammate or a TAK contact stops reporting, they stay on the map where they were last seen, faded, with their name and the time, for example Team 3 · last seen 14:05. They come back to normal as soon as they report again. Nothing disappears on its own; Clear in the Team Sync panel removes the ones that stopped reporting.

If a teammate cannot see you, check that everyone is on a current version of the app and typed the same code. On Android, Team Sync keeps running with the screen off, and a notification tells you about new messages and items.

Team Sync: Over the Internet

  1. In Team Sync, set your name, role and team color.
  2. Create a session code, or type the one you were given. Codes are 10 characters of letters, digits and symbols.
  3. Share it with your team: show the QR code, copy it, or invite friends from your account.
  4. Tap Join Session. Everyone with the code sees the same map.
The code is the key. Anyone who has it can join and see your team, so share it only with people who should. Start a new session with a new code for each operation.

Team Sync: Nearby TAK Devices

On the same Wi-Fi as ATAK, WinTAK or iTAK users, the ZYRNTOPO Android app joins the local TAK network by itself. They appear on your map as TAK contacts with their callsigns, and you appear on theirs.

  • Markers, lines, areas, circles and range rings go both ways. Markers made in ATAK, iTAK and WinTAK show up in ZYRNTOPO, and deleting one reaches TAK.
  • Chat goes both ways too, including direct messages to one TAK user.
  • No server and no internet are involved; it is the network the TAK apps already use among themselves.
Browsers cannot join a local TAK network, so the web app does not see nearby TAK devices. Use a TAK server to link the web app with TAK users.

Team Sync: TAK Servers

A TAK server links everyone connected to it, whatever app they use: ZYRNTOPO, ATAK, WinTAK or iTAK all see each other's positions, markers and chat.

Connecting

Team Sync → TAK server. It is set to the ZYRNTOPO server, but nothing is sent until you press Connect: joining a server shares your position, markers and chat with everyone on it. It never connects on its own, and does not reconnect by itself the next time you open the app.

To use another server, choose Custom TAK Server and enter its address.

Servers are independent. Connecting to one does not disconnect another, and positions are not forwarded between them. If you want your team's data to stay entirely on your own hardware, connect only to your own server.

Running your own

The ZYRN TAK server is free and keeps everything on hardware you control. It is one program for Linux, macOS, Windows and Raspberry Pi, with no Docker. One command installs it, creates the certificates and an administrator account, starts it as a service, and tests itself before saying it worked:

curl -fsSL https://zyrntopo.com/tak-server.sh | sh

On Windows, in PowerShell:

irm https://zyrntopo.com/tak-server.ps1 | iex

It prints the dashboard address and the administrator password. In ZYRNTOPO, choose Custom TAK Server and enter ws://<server>:8090. Installing, connecting ATAK, WinTAK and iTAK, the dashboard and day-to-day running are covered under ZYRN TAK server.

Before you open it to the internet, put it on a VPN such as Tailscale (zyrn-tak setup --tailscale) or behind a reverse proxy with TLS. The ZYRNTOPO web app needs a wss:// address, which either gives you. If a device cannot connect, run zyrn-tak status on the server.

Team Sync: Meshtastic Radio

A Meshtastic radio carries positions and chat over LoRa, for miles and with no phone signal. Open Menu → Meshtastic radio and connect your radio over Bluetooth, USB or Wi-Fi (by its address, such as meshtastic.local).

  • ZYRNTOPO users and ATAK users with the Meshtastic plugin on the same channel appear on your map.
  • Anything the radio hears is passed on to your TAK server and to nearby TAK devices, so one radio can link the people out of signal with everyone else.

Team Sync: MeshCore Radio

MeshCore radios relay messages across many repeaters and keep messages for radios that are out of range for a while, which suits a search spread over a large area. ZYRNTOPO connects to a MeshCore companion radio through the MeshCore plugin: install it from Plugins → Get Plugins, open it, and connect over Bluetooth, USB or the network.

  • Everyone on the team types the same channel name and passphrase. The plugin writes the channel into the radio; the passphrase stays on the radio and the phone.
  • Positions, team chat, clues and markers travel on that channel. Teammates appear on the map, and every radio heard is listed with its signal strength and when it was last heard.
  • Chat also goes out as plain channel text, so teammates using the MeshCore phone app can read it.
  • Assignments travel too. When command sets an assignment to Assigned, the team's radio gets it, and the team's Acknowledge comes back. A radio delivery receipt is shown as delivered and never counts as acknowledged.
  • The same plugin runs in ZYRNTOPO, ZYRNMAPS and ZYRN Workstation.

Team Sync: Without Internet

When there is no internet at all, three links still work:

  • Nearby TAK devices on the same Wi-Fi network or hotspot, see above.
  • Your own TAK server on a laptop or Raspberry Pi on the local network: everyone connects to its local address, and nothing leaves the network.
  • Meshtastic radios where there is no network at all.
For an incident command post: a laptop running the ZYRN TAK server and sharing a Wi-Fi hotspot links ZYRNTOPO, ATAK, WinTAK and iTAK users with no internet service.

Team Sync: Sensor Detections

Search systems that report detections as Cursor on Target, such as a thermal drone, can send them to ZYRNTOPO through a TAK server or the local TAK network. Each detection appears as a colored marker with its scores, and is removed when its time runs out.

The color comes from the confidence score, against the thresholds the sender includes (by default 85% and 60%):

  • Red: at or above the alert threshold. Check it now.
  • Orange: between the two thresholds. Needs a look.
  • Slate: below the review threshold. Logged, low confidence.
  • Friendly: a friendly unit's position rather than a detection.

Color is taken from confidence, not from the CoT affiliation, so a missing person is never drawn as a hostile track.

Reviewing a detection

Tap a detection (right-click it in the TAK Server contact list in ZYRN Workstation) to see its picture, confidence, sensor and time, and choose:

  • Accept: keep it. It gets a green ring and accepted by your name on every ZYRN device.
  • Dismiss: hide it on this device only.
  • Deny: remove it for the whole team.

A dismissed or denied detection stays gone if the sender reports it again. A drone operator can accept the likely ones, and command decides what to send a team to.

For detection systems

Send each detection as a CoT event with a non-friendly type such as a-u-G, a stable uid, and a <sord> block in the detail:

<sord schema="1">
  <score type="composite" value="0.91"/>
  <thresholds autoalert="0.85" review="0.6"/>
  <source sensor="thermal" mode="survey"/>
  <media type="image/jpeg" url="https://kit.local/crops/7.jpg"/>
</sord>

The composite score is the confidence (0 to 1). Without a <sord> block, a remarks line such as Conf: 91% is read instead. The picture must be an https address or a small inline data:image/jpeg;base64,... image; plain http addresses are not shown. Review results come back as events of type t-x-zyrn-review placed at the detection, with remarks such as zyrn-review v1 uid=DET-7 verdict=denied by=IC, which a detection system can read to stop sending a denied track.

Team Chat

Chat works across every Team Sync link. Open Menu → Team Chat, or tap the chat bubble.

  • Text to everyone in the session.
  • Voice messages: hold the microphone button to record, release to send.
  • TAK chat: on a TAK server or with nearby TAK devices, your messages land in ATAK's chat and theirs in yours.
  • An unread badge shows on the chat button, and on Android a notification arrives while the app is in the background.

SAR: Operational Periods

SAR mode. The search and rescue tools (search periods, assignments and clue markers) appear in Tools when SAR mode is on. Choose search and rescue in the first-run setup, or switch between Recreation and SAR in Settings → Display.

Create a period with Tools → Search Period. Each period is a planning cycle in a search, usually 12 or 24 hours; assign items to it to keep the map organized.

  • Name, start time and status.
  • Show or hide a period's items with one switch.
  • Export a period's items on their own for the incident record.
  • Print a period from its print button in Map Items: either a packet with one page and map per assignment, or an ICS 204 assignment list, one table with each assignment's team, resource, radio, status and instructions. In ZYRN Workstation it is Search & Rescue → ICS 204. For an ICS 214 activity log, use the Unit Log plugin's export.

SAR: Assignments

An assignment moves through Draft, Prepared, Assigned, Acknowledged, In Progress and Completed. Command sets Assigned to (a team's callsign or team name) and the status in the assignment's editor.

  • The assigned team sees Acknowledge at the top of the assignment's menu (in ZYRN Workstation, at the top of its properties), then Start and Mark complete. Pressing Acknowledge records who acknowledged and when, and every device shows it, for example Acknowledged by Team 3 at 14:05.
  • Only a person pressing Acknowledge counts. A message delivered to a radio, or an assignment that simply arrived on a phone, stays Assigned, not acknowledged yet.
  • Sending an assignment back to Assigned or earlier clears the old acknowledgement.
  • Statuses travel with the assignment over Team Sync, TAK and the MeshCore plugin, between ZYRNTOPO, ZYRNMAPS and ZYRN Workstation.

SAR: Team Members & Roles

Menu → Team Members is a free roster, with no account needed. Each member has:

  • Name, role and phone.
  • Medical and emergency information: blood type, allergies and an emergency contact.
  • Specializations and notes.

Rosters import and export as JSON or CSV for briefings and ICS forms. The roster stays on your device; it is separate from the people in a Team Sync session.

SAR: Gear / Pack List

Menu → Gear / Pack List keeps an inventory of field equipment:

  • Item, category, quantity and weight, grouped by category (shelter, navigation, medical, comms and so on).
  • Total pack weight.
  • Export and import as JSON or CSV, to reuse a list from a previous operation.

SAR: Clue & Find Logging

Tools → Clue Marker drops a clue: a yellow marker that stands apart from ordinary markers. Like any marker it takes a name, notes, a photo and its operational period, so each find records what was found, where, and in which shift.

Clues keep all their details when exported as GeoJSON, and print on PDF map sheets with the rest of the map.


Import & Export: GeoJSON

GeoJSON keeps the most detail and is the best format for moving data in and out of ZYRNTOPO.

Export

Menu → Export Map, then pick the format. Export everything, one folder or one operational period. On Android the file goes to the place you choose or the share sheet; in the browser it downloads.

Import

Menu → Import Map and pick a GeoJSON, GPX, KML, KMZ or .zyrn file. Imported items merge into your map: items you already have are updated and new ones are added. In the web app you can also drag a file onto the window.

Import & Export: KML / KMZ

KML suits Google Earth, CalTopo and ATAK. Exported KML keeps points, lines and areas with their names and colors; KMZ files import too.

Import & Export: GPX

GPX is what handheld GPS units such as Garmin and Suunto expect. Markers export as waypoints and recorded tracks as tracks; importing reads waypoints, tracks and routes.

Import & Export: CSV

CSV writes markers with their coordinates and details as a spreadsheet, for ArcGIS, QGIS or agencies that work in spreadsheets. Team rosters and gear lists also import and export as CSV.

POI Data

Points of interest are the peaks, springs and water, trailheads, passes, caves, fishing spots, boat launches, parking, power lines, parks, helipads and airstrips, and SAR shelters, rescue points and lookouts the app shows and searches.

  • Gather POI Data pulls the current OpenStreetMap points for the whole view and keeps them offline. Zoom out until the region you want fills the screen: one gather over a region beats a dozen small ones.
  • Settings → Data → Export POI Data writes everything you hold to one file, and Import POI Data merges a file back in. Importing only adds; it never removes what you already have, and a damaged file is refused.


Settings: Coordinate Formats

Settings → Coordinates → Coordinate Format: DD, DMS, DDM, UTM or MGRS. The choice applies everywhere: the coordinate readout, Point Info, marker details and CSV export.

Settings: Units & Display

Settings opens on a list of pages: Display, Map Display, Coordinates, Map Behavior, Appearance, GPS Receiver, Track Recording, Map Sources, Offline Maps, Viewed Tile Cache, Data, Account and Version.

  • Units (Coordinates): distance in imperial or metric, elevation in feet or meters.
  • Area units (Coordinates): metric with ares or hectares, imperial with square yards and acres, or nautical. Match distance units keeps acres or km².
  • Bearing units (Coordinates): degrees (0–360, ±180 or quadrants such as S 72° W), gradians, or mils (NATO 6400, exact 6283, Swedish 6300, Warsaw Pact 6000).
  • Reference north (Coordinates): true, grid (UTM) or magnetic. Every bearing the app shows is measured from it and marked T, G or M.
  • Mode (Display): Recreation or SAR, which shows the search and rescue tools.
  • Glove mode (Display): bigger buttons for gloves, cold hands or a moving vehicle.
  • Color-blind palette (Display): switches the slope and aspect colors to a color-blind-safe scale, with matching legends.
  • Map Display: show or hide the coordinate readout, the weather widget, the scale bar, the on-map buttons and the Tools button.
  • Map title: the name printed on PDF sheets, also editable at the top of the menu.
Offline-aware. Features that need a connection (trail routing, the weather widget, and the weather and hazard overlays) hide themselves while you are offline and come back when you reconnect, so what you see is what works right now.

Your base layer, map position and zoom, and settings are saved on the device and restored every time you open the app. Settings → Version has a Location diagnostics check for when GPS will not lock.

Settings: Permissions

The app asks for each sensitive permission the first time a feature needs it, and you can say no. To change one later:

  • Android: Settings → Apps → ZYRNTOPO → Permissions.
  • Chrome or Edge: the site icon in the address bar → Permissions.
PermissionUsed for
Location (background only while recording a track)Your position, tracks and route following, and sharing your position when you choose to
CameraScanning QR codes of shared maps
MicrophoneVoice notes and voice messages in Team Chat
BluetoothMeshtastic radios and Bluetooth GNSS receivers
NotificationsThe notice while a track records or Team Sync runs, and new chat and items
Nearby Wi-Fi trafficSeeing ATAK, WinTAK and iTAK devices on the same network
Photos for waypoints come through the system photo picker. Your position leaves the device only when you share it: in a Team Sync session, on a TAK server, over a radio, or in a location you send.

Platforms: Android

Android 7.0 or newer with Android System WebView 80 or newer, on phones and tablets of every kind, from Google Play or the Amazon Appstore.

  • No hardware is required: a Wi-Fi-only Fire tablet with no GPS receiver still installs it, and the features that need missing hardware simply stay off.
  • Track recording, Team Sync and chat keep working with the screen off, behind a notification.
  • USB serial devices (GNSS receivers, drone telemetry radios) plug in through USB-C or an OTG adapter.
  • The ATAK-style notification and nearby TAK devices work only in the Android app.

Platforms: Web / PWA

The web version runs in any modern browser at app.zyrntopo.com. Install it as an app for offline use:

  • Chrome / Edge: the install icon in the address bar.
  • Safari on iPhone and iPad: Share → Add to Home Screen.
  • Firefox: Menu → Install as App, where offered.

Once installed, the app itself works offline, along with your downloaded maps.

What the browser cannot do. Browsers pause location when the screen locks or the tab is in the background, so keep the screen on while recording a track. They cannot join a local TAK network, so use a TAK server to link with TAK users. USB and Bluetooth devices work in Chrome and Edge on a computer.

ZYRNMAPS: the native Android client

ZYRNMAPS is ZYRNTOPO built as a native Android app, drawing the map with the GPU rather than inside a web view. It is a separate app from ZYRNTOPO, with its own store listing and package name (com.zyrntopo.maps), and both can be installed side by side. Your account and Pro work in both.

Which one should I use? ZYRNTOPO is the same app on a phone and in the browser. ZYRNMAPS is for Android specifically: it draws faster, keeps a steady frame rate on low-end hardware, starts quicker, and takes native Android plugins.

Requirements

ItemRequirement
Android versionAndroid 7.0 or newer
GraphicsVulkan 1.0.3 or newer
Package namecom.zyrntopo.maps
Vulkan is required. Some older and lower-end devices that run ZYRNTOPO perfectly well lack it. If ZYRNMAPS will not install, or shows a black map on a device that otherwise works, use ZYRNTOPO on that device.

ZYRNMAPS: feature reference

ZYRNMAPS covers the same ground as ZYRNTOPO. Where it differs, it says so here.

The map

  • The built-in base layers, the imagery catalog, and any .pmtiles map you import, with Map Styling on any of them and a choice of contour interval.
  • Maps with your own key: add keys under Map provider keys in the basemap picker.
  • 3D terrain, viewshed and line of sight, worked out on the device.
  • The overlays: terrain, reference grids, weather and hazards, and the US field data.
  • Map labels follow the device language, and night mode follows the device's dark mode.

Location and GPS

  • GPS modes (off, follow and heading-up) and external GNSS receivers, as in ZYRNTOPO.
  • Track recording continues with the screen off, behind a notification.
  • Averaged waypoints and the bearing sight.

Items, drawing and measuring

  • Markers, lines, areas and tracks in folders, with the full SAR properties.
  • Distance, area, elevation profile, range rings, buffers, heading lines and bearings.
  • Point Info, and identify: tap a map feature to see what it is.

Search and rescue

  • Scent cone, routes, clue logging, search periods, photo waypoints, viewshed, gear list and team roster.
  • True-scale PDF map sheets.

Offline

  • Downloads with a size estimate before you commit, that resume if interrupted, and a list of what you already hold.
  • Offline 3D terrain from imported elevation data.
  • A downloaded area draws on a cold start in airplane mode.

Sharing and interchange

  • Share a saved map by QR code: open the map's card, choose Show code, and the other person scans it from their own Saved Maps. No signal, account or cable needed.
  • GeoJSON, GPX, KML/KMZ and CSV in and out, and data packages for moving a whole working set at once.

Team Sync and TAK

  • Team Sync sessions, TAK servers and nearby TAK devices in both directions, and MAVLink drones.
  • A Meshtastic radio over Bluetooth or Wi-Fi from Menu → Meshtastic radio, and over USB with the Meshtastic plugin.
  • Nothing is shared until you press Connect.

Plugins and place data

  • The same .zyrnplugin plugins as every ZYRN app, plus native plugins built as Android apps. See ZYRNMAPS plugins.
  • The points-of-interest dataset downloads by itself in the background after you install, so open the app on Wi-Fi the first time; place search then works offline.

ZYRNMAPS plugins

ZYRNMAPS runs two kinds of plugin. The first is the same .zyrnplugin file every ZYRN app runs: open Menu → Plugins and use Get plugins, Install file or From a link, or open a .zyrnplugin from Files, Downloads or a chat app. Those work exactly as described under Plugins, including community plugins and Report.

The second is a native plugin: an ordinary Android app that extends ZYRNMAPS. It ships as its own APK, installs the way any app does, and can add layers, panels, map objects, search results and context-menu actions. Building one is covered in Native ZYRNMAPS plugins below; the rest of this section is about running them.

Installing a native plugin

  1. Install the plugin's APK from a store, with adb install, or by opening the file on the device.
  2. Open ZYRNMAPS ▸ Menu ▸ Plugins. The plugin appears in the list.
  3. Switch it on. Nothing runs until you do: a discovered plugin is off until a person enables it, with no allow-list that pre-approves anything.

What the Plugins screen tells you

The list shows every plugin found on the device, including ones ZYRNMAPS has decided not to run, each with its reason. That is deliberate: a plugin that is switched off and a plugin that is missing are different problems, and a screen that showed nothing for both would send you looking in the wrong place.

What it saysWhat it meansWhat to do
Not enabledFound, allowed, and waiting for youSwitch it on
Needs a newer ZYRNMAPSBuilt against a host API this version does not haveUpdate ZYRNMAPS
Built for an older ZYRNMAPSBuilt against a host API no longer supportedUpdate the plugin
No entry pointIts manifest names no plugin class, so there is nothing to loadReport it to whoever built it
Could not be loadedThe named class is missing, or is not a ZYRNTOPO pluginReport it, quoting the reason shown
Requires ProThe plugin declares itself Pro and this account has no entitlementUnlock Pro, or remove the plugin

Who signed it

Every row shows where the plugin came from, and ZYRNMAPS records the SHA-256 of its signing certificate either way.

LevelMeaning
First-partySigned with the same certificate as ZYRNMAPS itself: shipped by us, or built by somebody holding our key
Third-partySigned by somebody else. The fingerprint is shown, so you can check it against what the author published
UnknownThe signature could not be read at all. Rare, and it means the package manager refused, not that the APK is suspect
The signature is provenance, not a gate. ZYRNMAPS will load a third-party plugin. It reads the signature, records it and shows it, because who built something is worth knowing, but that is not what decides whether it runs. You are. A plugin runs code inside ZYRNMAPS with access to the map, your position and its own storage, so enable ones you have a reason to trust, and check the fingerprint when the author publishes one.

Turning one off

Switch it off in the same screen. Everything the plugin added (layers, sources, map objects, panels, its search provider, tap handlers and subscriptions) is removed by the host, whether or not the plugin cleaned up after itself, and whether or not its own teardown threw. Objects it drew are namespaced to it, so they go and nothing you drew goes with them.

Uninstalling the APK does the same, and also sweeps any objects it left behind.


ZYRN Workstation

ZYRN Workstation is the desktop app for Windows, macOS and Linux. It has its own 3D globe with ZYRNTOPO's maps, terrain analysis and GIS tools, and the same Team Sync and TAK links as the phone app. It signs in to the same account, uses the same Pro license and syncs the same map items, so a plan made at the desk is on the phones in the field.

Installing

Windows, in PowerShell:

irm https://zyrntopo.com/zyrngis.ps1 | iex

macOS or Linux, in a terminal:

curl -fsSL https://zyrntopo.com/zyrngis.sh | sh

The command picks the right package for the computer, checks it against its published SHA-256 and stops on any mismatch. On Windows it installs for the current user; to install for every user on the machine with the .msi, for Group Policy or Intune, run $env:ZYRNGIS_METHOD='msi' first. On Linux it uses a .deb, .rpm or pacman package where it can and an AppImage in your home folder everywhere else. On a Mac the app goes to Applications, and Apple silicon Macs get Rosetta 2 if they don't have it. On Linux, ZYRN Workstation is also in the Snap Store; that copy runs sandboxed, so it can't use USB GPS receivers, radios or the system's location service.

It needs Windows 10 or 11 (64-bit), macOS 13.3 or newer, or 64-bit Linux with glibc 2.35 or newer (Ubuntu 22.04, Debian 12, Fedora 36, Linux Mint 21, Arch and later), and a graphics driver with OpenGL 4.1.

Updates

The Windows installer copy and the Linux AppImage update themselves: when a new version is out, ZYRN Workstation downloads it, checks it against the published checksums and installs it when you close the app. Other installs tell you a new version is available. Update those by running the install command again, or with zyrngis update on macOS and Linux. zyrngis version shows the installed and published versions, and zyrngis remove uninstalls it and keeps your projects and settings. The Snap Store updates its copy by itself.

The Workstation window

The map fills the middle. Contents on the right lists the basemap, the layers you added and Map Styling; task panes such as Point Info, Viewshed, Plugins and the TAK server open beside it; and the status bar at the bottom always shows the cursor's position in latitude and longitude, UTM and MGRS. The ribbon across the top groups the tools by job:

TabWhat is on it
MapZoom, north up, the 3D view, bookmarks, basemaps, adding data, the imagery catalog, select, identify, Point Info and coordinates
SearchGo to a coordinate in any format, and place and address search
GPS & TeamA GPS receiver over USB or serial, replaying a recorded log, Team Sync, the TAK server and team chat
ServerSigning in to a ZYRN TAK server and running it from here, see Team, TAK and the Server tab
UASFlying a MAVLink drone: telemetry, flight commands, routes and what its camera sees
DrawMarkers, routes, lines, areas and shapes with their style, editing, bearing lines, and field tools: set your location, record a track, photo and averaged waypoints
Range & BearingMeasuring distance, area and bearing, and the elevation profile of a line
Search & RescueAssignment lines and areas, clues, the operational period, a scent cone and the wind plot
AnalysisViewshed and line of sight, terrain tools such as slope and hillshade, geoprocessing and density
DataAdding files, links and map services (XYZ, WMS, WFS), the attribute table and symbology, selection, export and printing, offline areas and archives, 3D Tiles
PluginsYour plugins' buttons, Get Plugins, Install and New Plugin, see Plugins
ViewWhich panes are open, the sky, the lat/lon and UTM grids, a colour-blind palette, night mode, the map legend and resetting the layout

Projects (.zyrnproj) save the map with its layers and styling, and print layouts make map sheets as PDF; both are under Data → Export and in the ≡ menu at the left end of the ribbon. Your account and Pro status sit at the top right.

Workstation offline maps

Data → Download Area saves a basemap for an area so it works with no connection. Draw the area, press Use Current View, or pick one of the ready-made regions, then choose the basemap and zoom range. Two extra boxes add the elevation the 3D view, slope and profiles need, and address points for offline address search. Save Offline Maps In sets the folder. A canceled download keeps nothing.

The same group opens map and terrain archives you already have (.pmtiles), and adds OGC 3D Tiles: a tileset.json by address or from disk, or a Cesium ion asset.

Workstation GIS data and tools

Add Data opens GeoJSON, KML and KMZ, GPX, Shapefiles, GeoPackages, CSV and the other common vector formats, and reprojects them to the map. Each layer gets a legend in Contents, an attribute table, and symbology: a single symbol, unique values, or graduated colours in classes by natural breaks, quantile, equal interval or standard deviation. You can select features by attributes or by location, calculate fields, and export a layer to another format.

Geoprocessing holds 41 tools in five groups:

  • Overlay: buffer, clip, erase, intersect, union, dissolve, merge, spatial join, count points in areas and near.
  • Geometry: convex and concave hulls, centroids, bounding boxes, vertices to points, polygons to lines and back, multipart to singlepart, simplify, densify, repair, and geometry attributes.
  • Create: grids, range rings, Delaunay triangles and Thiessen polygons.
  • Terrain: contours, hillshade, slope, aspect, ruggedness, topographic position, curvature, elevation tint, elevation statistics and cut and fill.
  • Density and selection: kernel density, IDW interpolation and summary statistics.

Tools run on the layers you have open and add their result as a new layer, so nothing you had is changed.

Workstation Team, TAK and the Server tab

GPS & Team connects a GPS receiver over USB or a serial port, or replays a recorded NMEA log. Team Sync works as it does on the phone, with the same session codes, and the TAK Server pane connects to a TAK server so ATAK, WinTAK and iTAK users share the map with you. Team Sync and TAK need Pro, as they do everywhere.

The Server tab signs you in to a ZYRN TAK server, the ZYRNTOPO one or your own, and then does what the server's web dashboard does without a browser: every dashboard page opens in the Server pane, and the dashboard's map is the Workstation globe itself. Two-step sign-in works. Signing in also puts your TAK link under your account, so your position, markers, drawings and chat reach the server as you, in your groups. The Workstation keeps a personal access token instead of your password; it lasts 30 days, renews itself while you use it, and signing out revokes it on the server.

The UAS tab flies an ArduPilot or PX4 aircraft over UDP, TCP or a telemetry radio: arm and take off, go to or circle a point you click, return to launch and land, send it a route, and see what its camera covers on the map, shared with TAK.


ZYRN TAK server

The ZYRN TAK server is a free TAK server in a single program for Linux, macOS, Windows and Raspberry Pi, with nothing else to install: no Docker, no database, no Java. ZYRNTOPO, ZYRNMAPS, ZYRN Workstation, ATAK, WinTAK, iTAK and TAK Aware all connect to it, and positions, chat, markers, data packages, missions, video and voice stay on hardware you control. Its download page is TAK server.

Installing

Linux, Raspberry Pi or macOS:

curl -fsSL https://zyrntopo.com/tak-server.sh | sh

Windows, in PowerShell:

irm https://zyrntopo.com/tak-server.ps1 | iex

The installer downloads the program for your system, checks its SHA-256 and runs zyrn-tak setup. Setup creates a certificate authority, a server certificate and an administrator account, installs a service that starts at boot and restarts after a failure, opens the ports in the firewall, and sends a test position through every port before it says it is ready. It prints the dashboard address and the administrator password, which also stays in admin-password.txt in the data folder until you change it. Running the command again is safe: that is how you update or repair it, and users, certificates and settings are kept.

Setup takes options after sh -s --, or in ZYRN_TAK_ARGS on Windows. The useful ones: --host sets the address devices use, --name the server name clients show, --tailscale publishes it on your tailnet as a wss:// address (which also works from the ZYRNTOPO web app), --zerotier NETWORK_ID joins a ZeroTier network, and --no-anonymous requires a sign-in on every port.

It needs a computer that stays on: a Raspberry Pi 2 or newer, a spare laptop, a desktop or a small VPS, running Linux, macOS 11 or later, or Windows 10 or 11, with administrator rights for the install. On a VPS, also open TCP 8089, 8090, 8443 and 8446 in the provider's firewall.

Connecting devices to your TAK server

AppHow
ZYRNTOPO and ZYRNMAPSTeam Sync → TAK server → Custom TAK Server, address ws://<server>:8090
ZYRN WorkstationThe TAK Server pane, or sign in on the Server tab
ATAK and WinTAKDashboard → Connect a device → the enrollment QR code, or a connection package
iTAKDashboard → Connect a device → the iTAK QR code, then sign in
Any TAK clientOpen https://<server>:8446/package on the device, sign in and open the downloaded zip

On the server itself, zyrn-tak qr NAME prints the same QR codes in the terminal and zyrn-tak user package NAME saves a connection package to copy to a device.

Port 8090 lets ZYRNTOPO in without a sign-in. That suits one trusted network. Off your own network, keep the server on a VPN such as Tailscale or behind a TLS reverse proxy, or turn the anonymous WebSocket off under Settings → Access. The ZYRNTOPO web app needs a wss:// address, which either gives you.

The TAK server dashboard

Open https://<server>:8446 and sign in as the administrator. The dashboard works on phones and desktops, follows the system's light or dark theme, comes in English, Spanish, French, German, Portuguese and Ukrainian, and Ctrl+K (or /) jumps to any page or setting.

  • Overview shows connected devices, the addresses to connect to, the server's state and emergencies. Map shows live positions with their symbols and headings, routes, drawings, CasEvac reports and the last hour of tracks.
  • Chat reaches every TAK client and ZYRNTOPO, and Calls makes voice and video calls between dashboard users.
  • Connect a device has the QR codes, packages, single-use download links and manual settings. Users and Groups manage accounts, roles, channels, certificates and two-step sign-in.
  • Files and Missions hold data packages and Data Sync. Video and Voice run the built-in video server and the Mumble voice server. Data feeds brings in ADS-B, AIS, Traccar, Meshtastic and CoT.
  • Server links, Federation and Cluster join this server to others. Settings has every setting, and Updates and backup updates the server, backs it up and renews certificates.

Running your TAK server

The zyrn-tak command manages it. On Linux and macOS put sudo in front of commands that change something; on Windows run them in an Administrator PowerShell.

CommandWhat it does
zyrn-tak statusService state, addresses, connected clients and a self-test
zyrn-tak user add NAMEAdds a person (--admin, --groups, --callsign as needed)
zyrn-tak user remove NAMEDeletes the user and revokes their certificates
zyrn-tak logs -fFollows the log
zyrn-tak backupWrites a backup of the data folder
zyrn-tak updateUpdates to the current release
zyrn-tak uninstallRemoves the service and keeps the data (--purge deletes it)

zyrn-tak help lists every command. zyrn-tak import-ca moves an existing TAK server's certificate authority over, so devices enrolled against the old server keep working.

The data folder is /var/lib/zyrn-tak on Linux, /Library/Application Support/ZYRN TAK on macOS and C:\ProgramData\ZYRN TAK on Windows. Back it up: certs/ca.key in it signs every device certificate.

PortUsed by
8089ATAK, WinTAK and iTAK, over TLS
8090ZYRNTOPO and ZYRNMAPS (CoT over WebSocket)
8443Data Sync, files, missions and the update server for TAK apps
8446The dashboard, enrollment and package downloads

New installs also open the video, voice and call-relay ports; everything else, such as federation and unencrypted CoT, stays off until you turn it on under Settings → Ports.


Plugins

Plugins add tools to the ZYRN apps: buttons and panes, map layers, calculators and field forms, and links to radios, drones and other systems. One plugin file runs unchanged in ZYRNTOPO on the web and Android, in ZYRNMAPS and in ZYRN Workstation, because every app runs plugins in the same small JavaScript engine with the same API.

AppWhere plugins liveGetting one
ZYRNTOPOPlugins on the toolbar, or in Tools on a phoneGet plugins in the Plugins panel, or Import and From a link
ZYRNMAPSMenu → PluginsGet plugins, Install file, From a link, or open a .zyrnplugin from Files, Downloads or a chat app
ZYRN WorkstationThe Plugins ribbon tab and paneGet Plugins…, Install…, or double-click or drop a .zyrnplugin

Get plugins lists the official plugins and the community plugins that passed the automatic check, with one-tap Install. A plugin installed from the list keeps itself up to date: the app checks the list shortly after it starts and every few hours after that. The Plugins page shows the same list.

A plugin can always add its own buttons, panes and map layers. The first time it asks for anything more (the internet, TAK messages, your position, files, your map items, or radios and other devices), the app asks you once whether to allow it, and says which servers it wants to reach. Turn a plugin off or remove it in the same place you installed it; everything it added goes with it.

Community plugins are marked as such, with their author, their licence and a Report button. Report one that is unsafe, misleading or broken; one that several people report is taken down and turns itself off on every device that has it. See Community plugins.

Community plugins

Anyone can publish a plugin to every ZYRN app. Sign in to your ZYRN account and submit the .zyrnplugin file under Submit a plugin on the Plugins page. It can be open source or closed source: you choose the licence when you submit, and nobody can download your code from the website. People get it from the Plugins section of their app.

A scanner on the ZYRN server checks each upload, usually within a minute. It reads the package and the code, then starts the plugin in the same engine the apps use, with nothing real behind it, and watches what it tries to do. The report appears under Your plugins on the same page and arrives by email. A plugin that passes is published at once, with nobody at ZYRNTOPO having to approve it. One that fails gets a report that says what to change: fix it, raise version in plugin.json and submit again.

The scan refuses:

  • code that builds more code from text while it runs, such as eval or new Function, and the tricks that reach them;
  • native programs, and hidden or scrambled code;
  • a name or id that copies an official plugin or someone else's;
  • servers that exist to collect data or pass messages on, such as webhook services and chat bots.

WebAssembly and source files may be in the package, and the report notes them. Every new version goes through the same scan, and apps update to it on their own.

When three people with verified accounts report a plugin as unsafe, misleading or inappropriate, it is taken down: it leaves the list and every installed copy turns off. The author can bring it back with a fixed, higher version, unless it has been taken down three times, which needs a review. An author can also withdraw a plugin from Your plugins; it leaves the list, and copies people already have keep working without updates. The rules are in the Terms of Use.

A scan cannot prove a plugin is safe. That is why the apps still ask before a community plugin uses the internet, TAK messages, your position, files or your map items. Allow only what a plugin plainly needs.

Writing a plugin

A plugin is a folder with a plugin.json and an ES module. ZYRN Workstation makes a working one for you: Plugins → New Plugin… creates it, starts it and opens its folder, and the folder includes zyrn.d.ts and jsconfig.json so zyrn. autocompletes in VS Code. The plugin SDK has the same files and a template to copy.

weather/
  plugin.json
  main.js
  lib/units.js        imported from main.js as './lib/units.js'
  icon.svg            optional
{
  "id": "com.example.weather",
  "name": "Weather at a Point",
  "version": "1.0.0",
  "author": "Example",
  "description": "Pick a point and read the weather there.",
  "main": "main.js",
  "icon": "icon.svg",
  "apiLevel": 4,
  "permissions": ["network"],
  "hosts": ["api.open-meteo.com"]
}
FieldWhat it does
idRequired and unique. Lower case a-z 0-9 . - _, usually your reverse domain, such as com.example.weather.
nameRequired. What the Plugins screens show.
versionYours to set. A submitted update must be higher than the last one.
mainThe module to load. main.js if left out.
iconAn .svg or .png in the folder, or a Material Symbols name such as air.
apiLevelThe API level you need. This page describes level 4, and a plugin written for an earlier level keeps working in later apps.
permissionsAny of network, cot, location, files, items, devices.
hostsThe servers network may reach (api.example.com, *.example.com). Listing hosts implies network, and users see the list when they allow the plugin.

The app calls activate(zyrn) when the plugin starts and deactivate() when it stops. Modern JavaScript works, including async/await, classes and modules.

export function activate(zyrn) {
    const pane = zyrn.ui.pane({ title: 'Weather', icon: 'air' });
    pane.label('wind', 'Wind', '-');
    pane.button('pick', { label: 'Pick a point', primary: true, onClick: async () => {
        const at = await zyrn.map.pick('Choose where you want the weather');
        const r = await zyrn.net.fetch(`https://api.open-meteo.com/v1/forecast?latitude=${at.lat}&longitude=${at.lon}&current=wind_speed_10m`);
        pane.set('wind', `${r.json().current.wind_speed_10m} km/h`);
    } });
    zyrn.ui.button({ label: 'Weather', icon: 'air', onClick: () => pane.show() });
}

export function deactivate() {}

When a plugin stops, the app removes everything it added: buttons, panes, menu entries, map layers, timers, listeners, sockets and requests. Imports are relative and must stay inside the plugin's folder; there is no npm, so bundle dependencies into the folder or ship a bundler's output as main.js. A call that breaks a rule, such as a host that is not in hosts, throws an Error that says what was wrong.

Trying it while you work

In ZYRN Workstation, edit the plugin in its folder (Open Folder in the Plugins pane) and it reloads within half a second of each save; Log shows console.log output and every error with its file and line. In ZYRNTOPO, Folder in the Plugins panel (Chrome and Edge) loads a folder from your computer and reloads it on save. For ZYRNMAPS, push the folder to the phone and each push reloads it:

adb push my-plugin /sdcard/Android/data/com.zyrntopo.maps/files/plugins/

To share a plugin, zip its folder and rename the zip to name.zyrnplugin. The file installs in any of the apps, and installing a newer copy with the same id replaces the old one. To list it for everyone, submit it.

Limits

One call into a plugin, such as a click handler, a timer or activate(), may run for 5 seconds before the app stops the plugin and says why, so split long work with await or setTimeout. Each plugin may use 256 MB of memory. Plugins can't read or write files except through zyrn.files, can't start programs, and can't reach the network without network. A plugin that throws is not stopped; the error goes to its log.

ZYRN Workstation and ZYRNMAPS make network requests themselves, so a plugin can reach any server in its hosts, including plain http:// and ws:// addresses of a radio, a drone or another device on the local network. ZYRNTOPO runs plugins inside its web page, so net.fetch follows the browser's rules there: the server has to allow cross-origin requests (CORS), and a page on https can only reach https:// and wss:// addresses.

Plugin API reference

Everything is on the zyrn object handed to activate. zyrn.plugin.app says which app the plugin is in (ZYRNTOPO, ZYRNMAPS or ZYRNGIS for ZYRN Workstation) and zyrn.plugin.platform where (windows, macos, linux, web or android). Level 3 added pane.image, binary net.fetch, ui.openUrl and ui.playVideo; level 4 added zyrn.device, cot.inject, cot.outgoing, cot.link and zyrn.self().

zyrn.ui

CallWhat it does
button({ label, icon?, tooltip?, onClick })A button for the plugin: on the Plugins tab in ZYRN Workstation, at the top of the Plugins panel in ZYRNTOPO, under Plugin tools in ZYRNMAPS. Returns { setEnabled, setLabel, setTooltip, setChecked, remove }.
pane({ title, icon? })A pane built from rows (below): a task pane in ZYRN Workstation, a page in the Plugins panel in ZYRNTOPO, the whole Plugins screen in ZYRNMAPS.
menu({ label, icon?, onSelect })An entry in the map's right-click or long-press menu. onSelect({ lat, lon }) gets the point. Returns { off() }.
notify(text)A short message.
alert(title, text)A notification.
confirm(title, text)A yes/no question. Promise<boolean>.
prompt(title, text, value?)Asks for a line of text. Promise<string | null>.
openUrl(url)Opens an http or https address in the browser.
playVideo(url, { title? })Plays an http, https or rtsp stream. Needs network and the host in hosts.

A pane is built with chained calls, and every row with an id can be read and changed later:

const pane = zyrn.ui.pane({ title: 'Search' });
pane.heading('Area')
    .hint('Choose a spacing and press Run.')
    .number('spacing', { label: 'Spacing', value: 50, min: 5, suffix: ' m', onChange: (v) => {} })
    .select('mode', { label: 'Mode', options: ['grid', { value: 'line', label: 'Line search' }], value: 'grid' })
    .text('note', { label: 'Note', placeholder: 'optional', lines: 3, onSubmit: (t) => {} })
    .check('wide', { label: 'Wide', value: false, onChange: (on) => {} })
    .label('result', 'Result', '-')
    .progress('done', { label: 'Done', value: 0 })
    .image('photo', { label: 'Camera', height: 160, placeholder: 'loading' })
    .list('hits', { items: [{ label: 'One', detail: 'first', value: 1 }], onSelect: (value) => {} })
    .separator()
    .button('run', { label: 'Run', primary: true, onClick: () => {} });
pane.set('result', 'done').set('done', 0.5);
pane.get('spacing');
pane.enable('run', false);
pane.visible('note', false);
pane.show();

Icons are Material Symbols names (extension, air, groups, my_location, route, flag, search and so on) or a file in the plugin's folder.

zyrn.map

CallWhat it does
points(layer, [{ lat, lon, name?, description?, colour?, icon?, course? }])Draws a live point layer named after the plugin and layer. Calling again replaces it.
lines(layer, [{ name?, points: [[lat, lon], …] }], { colour?, width? })Draws a live line layer.
geojson(layer, data, { colour?, width?, fillOpacity?, radius? })Draws GeoJSON points, lines and polygons. A feature's name, description and colour properties are used. Returns how many shapes were drawn.
tiles(layer, { url, maxZoom?, opacity? })Adds an XYZ tile layer from an https://…/{z}/{x}/{y}.png template. Needs network.
clear(layer)Removes the layer's points, lines, shapes and tiles.
goTo(lat, lon, zoom?)Moves the map. zoom is a web-map zoom level: 0 is the whole world, 15 a street.
view(){ lat, lon, zoom, bearing, pitch, bounds: { north, south, east, west } }
on('click' | 'longpress', ({ lat, lon }) => …)A click or tap, or a long press or right-click. Returns { off() }.
on('moveend', (view) => …)The map stopped moving.
pick(prompt?)Lets the user choose a point. Promise<{ lat, lon }>. On a phone the panel moves aside while they choose, so write prompts like "Choose where the marker goes".
elevation([[lat, lon], …])Ground height in metres, null where there is none. Promise<number[]>.
addPoint(lat, lon, name)A permanent point in the user's map items.
addGeoJSON(name, geojson)Adds GeoJSON as an ordinary layer.
metric()Whether the user works in metric units.

zyrn.items items

The user's own markers, lines, areas and circles: the same items every ZYRN app syncs with their account, so what a plugin adds also appears on their other devices.

CallWhat it does
list()Every item: { id, kind, type, name, notes, colour, symbol?, lat?, lon?, radius?, points?, updatedAt }, where kind is point, line, area or circle.
get(id)One item, or null.
add({ kind, name?, notes?, colour?, symbol?, lat?, lon?, radius?, points? })Adds an item and returns its id. Circles need radius in metres, lines two or more points, areas three or more.
update(id, patch)Changes the fields given.
remove(id)Deletes an item.
on(change => …){ change: 'add' | 'update' | 'remove', id, item } whenever any item changes, whoever changed it.

zyrn.cot cot

Cursor on Target, the traffic ATAK, WinTAK and iTAK exchange, from TAK servers, nearby TAK devices and radios.

CallWhat it does
on(message => …)Every CoT message the app receives: { xml, via, kind, uid, type, callsign, lat, lon, hae, how, remarks, stale, course } for map items, { kind: 'chat', from, room, text } for chat, { kind: 'delete', uid } for deletions. The raw xml is always there, which is how a plugin reads what an ATAK plugin sends.
send(xml)Sends CoT on every open TAK connection. true if one was open.
marker({ uid?, callsign, lat, lon, type?, remarks?, staleSeconds? })Builds a marker's CoT XML, ready for send.
connected()Whether any TAK connection is open.
inject(xml, { via?, relay? })Hands CoT to the app as if it had arrived over via: the map, chat, alerts and other plugins see it, and it is passed on to the TAK network unless relay is false.
outgoing(traffic => …)The user's own position reports and team chat as the app sends them.
link({ name?, connected, detail? })Declares the plugin a TAK link, as a radio plugin does: while it is connected, chat and positions go out over it even with no server.

zyrn.self() returns { uid, callsign, team, role, lat?, lon? }, the user as the TAK network knows them.

zyrn.device devices

Radios and other hardware, as raw bytes in base64. kinds() says what the app can reach: serial, tcp and, where the platform allows, ble. serialPorts() lists serial and USB devices, bleScan({ service?, timeout? }) finds Bluetooth LE devices, and open({ kind, id?, baud?, host?, port?, service? }) opens one and returns a channel with write, close and on('open' | 'data' | 'close' | 'error', fn). Bluetooth channels also have read(characteristic) and subscribe(characteristic), and write takes the characteristic.

zyrn.net network

fetch(url, { method?, headers?, body?, timeout?, binary? }) returns Promise<{ status, ok, url, headers, text, base64?, json() }>. A body object is sent as JSON, and binary: true returns the body as base64, ready for an image row. socket(url, { protocols? }) opens a WebSocket and returns { send(text), close(), on('open' | 'message' | 'close' | 'error', fn) }. Only the hosts in hosts, when it is set.

zyrn.storage, zyrn.files and zyrn.location

zyrn.storage keeps small settings as JSON across restarts: get(key, fallback), set(key, value), remove(key), keys().

zyrn.files reads and writes the plugin's own data folder: read(name), write(name, text), list(). With the files permission, open({ title?, filter? }) lets the user choose a file to read and save({ title?, name?, filter?, text }) lets them choose where to save one.

zyrn.location (permission location): current() returns { lat, lon, altitude?, accuracy?, heading?, speed?, source } or null, and watch(position => …) calls back whenever it changes.

zyrn.bus: plugins working together

zyrn.bus.provide('weather.windAt', async (lat, lon) => ({ speed: 4.2, from: 270 }));
const wind = await zyrn.bus.call('weather.windAt', 43.7, -110.8);

zyrn.bus.on('search.segment-done', (data, meta) => console.log(meta.from, data));
zyrn.bus.emit('search.segment-done', { id: 'A3' });

provide offers a named service any plugin can call, and emit and on broadcast events. Data is copied as JSON. Name services and topics yourplugin.thing so they don't collide. The official plugins use the zyrn.log topic: emit { text, lat?, lon? } on it and Unit Log records it with a time and place.

Plugins also get console.log, setTimeout, setInterval, Promise, JSON, Math, Date and basic Intl, and zyrn.plugin holds { id, name, version, dir, dataDir, apiLevel, app, appVersion, platform, permissions }.

Plugins in other languages

The apps only run JavaScript, but you don't have to write it. The plugin SDK turns a plugin written in Python, Java, Go, C or C++ into the same .zyrnplugin package, so it installs, runs and goes through the scan like any other. Copy the template for your language, change id, name and description in its plugin.json, and build it with Node.js 22 or newer:

node build.mjs path/to/your-plugin

The package lands in dist/ inside the plugin's folder.

  • Python goes through Transcrypt (pip install transcrypt==3.9.5), which runs on Python 3.13 or older. Call zyrn the same way as in JavaScript.
  • Java goes through TeaVM. You need JDK 21 or newer; the template's Gradle wrapper fetches the rest. zyrn.Zyrn has the common calls and call for any other.
  • Go goes through TinyGo 0.43 and then wasm2js from Binaryen 133 or newer.
  • C and C++ go through the clang in wasi-sdk 34 or newer and then wasm2js. You write zyrn_main and call the app with zyrn_call(path, json); C++ gets a wrapper with std::string and std::function.

The SDK's README says where each tool goes and how each language calls zyrn. Compiled languages carry their runtime, so their packages are bigger (a Go plugin starts near 700 kB, C and C++ near 30 kB), and the apps' engine has no JIT, so heavy number crunching runs slower than in the language's own runtime. Plugins that mostly react to the user and call the app don't notice. The scan checks the JavaScript the build produced; source files you leave in the package are checked for passwords and keys and then left out.


Native ZYRNMAPS plugins

Most plugins should be .zyrnplugin plugins, which run in every ZYRN app. The native SDK is for ZYRNMAPS only: it lets you extend ZYRNMAPS with a separately built, separately signed Android APK. Your plugin can add map sources and layers, put objects on the map, contribute a panel, answer search queries, add context-menu actions, react to taps, post notifications and talk on the app's event bus.

This is not the ATAK plugin, and the two get confused constantly. The ATAK plugin is an APK that loads inside ATAK, lives in a separate build, and shares only the word "plugin". This page is about ZYRNTOPO's own extension surface, loading inside ZYRNMAPS. If you are looking to extend ATAK, you are on the wrong page.

The one design decision worth knowing first

Everything your plugin creates through the context it is handed is tracked by the host, and released when the plugin is deactivated, whether or not your teardown ran, and whether or not it threw.

This is deliberate, and it is aimed at a specific well-known failure. ATAK plugins are notorious for leaking listeners across reloads, because onDestroyImpl has to manually undo whatever onCreate did and a human has to remember every line. Here the context records every source, layer, subscription, object, panel and cleanup callback you create, and deactivate() releases them regardless. A plugin that forgets to clean up cannot leak, because it was never the plugin's job.

The practical consequence: your onDeactivate can be empty, and in most plugins it should be. Register things through the context and let the host own their lifetime.

What a plugin cannot do

Named up front, because discovering a wall later is worse than being told about it now.

  • It cannot draw its own UI. A panel is described as rows and the host draws them, in the host's own controls. A plugin that needs a chart or a camera preview cannot have one yet. That is the stated gap, and the reason is in Panels.
  • It cannot consume a map tap. Tap delivery is read-only; a plugin that could swallow the map's primary gesture could make the app look broken with no way to find out why.
  • It cannot set the camera. It requests a move, and the host's camera controller remains the only writer.
  • It cannot choose a notification channel, importance or full-screen intent. A notice is a title and a body. A map layer should not be able to hold the screen on and buzz through a silent profile.
  • It cannot touch MapLibre directly. It gets a narrowed facade, so an engine upgrade is not a plugin-breaking change.

Native plugins: quick start

Depend on the API, and only on the API

dependencies {
    compileOnly(project(":plugin:api"))   // or the published artifact
}
compileOnly is load-bearing. This is the mistake that costs an afternoon. A plugin that bundles the API ships a second copy of every class under the same names. Loaded through a DexClassLoader, those are not the same types the host holds, so an is ZyrnPlugin check fails and the error reads as a ClassCastException between a type and itself. Compile against the API; never package it. No Compose, no MapLibre, no host internals either.

Declare yourself in the manifest

<application>
    <meta-data android:name="zyrn-plugin-api"   android:value="2" />
    <meta-data android:name="zyrn-plugin-class" android:value="com.example.MyPlugin" />

    <!-- Never started. Exists so ZYRNMAPS can SEE this package. -->
    <activity android:name=".PluginMarker" android:exported="true" android:enabled="false">
        <intent-filter>
            <action android:name="com.zyrntopo.plugin.ACTION_PLUGIN" />
        </intent-filter>
    </activity>
</application>
Remove that activity and your plugin becomes undiscoverable, with no error anywhere. Android 11 (API 30) stopped getInstalledApplications returning other people's packages, so a host that only reads manifest meta-data finds nothing at all. The way through is <queries>, which filters by intent rather than by package, so a plugin declares an activity for this action purely to be visible, and the host declares a matching <queries><intent>. The activity is never started and needs no class behind it. ATAK's own template carries the same warning about its equivalent entry, for exactly the same reason.

Implement ZyrnPlugin

class MyPlugin : ZyrnPlugin {
    override val id = "com.example.myplugin"
    override val name = "My Plugin"
    override val minHostApi = 2
    override val capabilities = setOf(Capability.PANEL, Capability.MAP_OBJECTS)

    override suspend fun onActivate(ctx: PluginContext) {
        ctx.addObject(PluginObject("home", """
            {"type":"Feature",
             "geometry":{"type":"Point","coordinates":[-122.68,45.52]},
             "properties":{"name":"Base"}}
        """.trimIndent()))
    }
}

Build, install, enable

./gradlew :myplugin:assembleDebug
adb install -r myplugin/build/outputs/apk/debug/myplugin-debug.apk

Then ZYRNMAPS ▸ Menu ▸ Plugins ▸ enable it.


Native plugins: interface and lifecycle

What you declare

MemberTypeWhy the host wants it
idStringNamespaces your storage and your map objects
nameStringWhat the Plugins screen shows
minHostApiIntThe lowest host API you work against, so the host can refuse rather than crash
capabilitiesSet<Capability>What you need. Declared, never assumed
tierTierFREE (default) or PRO. The host refuses a Pro plugin without entitlement, and says so
Set minHostApi to the lowest API level your plugin really needs. If you set it lower, the plugin loads into older apps and crashes when it calls a method they don't have. With the right value, the Plugins screen tells the user the app needs an update instead.

Activation

suspend fun onActivate(ctx: PluginContext)

It suspends, and that removes a distinction you would otherwise have to make. A plugin that lazily loads a large payload cannot finish inside a synchronous call, and a naive host marks such a plugin active the instant activation returns, leaving a failed load reporting as active with nothing behind it. A suspend function has one shape and cannot have that bug.

Deactivation

fun onDeactivate() {}   // may be empty, and usually should be

Undo only what you created outside the context: your own timers, threads, or state the API does not model. Everything registered through the context is released by the host afterwards, whether or not this ran, and whether or not it threw.

Why a plugin might be refused

Each of these is a different thing to tell the user, which is why they are separate values rather than one error string.

ReasonMeaning
HostTooOldBuilt against a newer host than this one. The user needs a ZYRNMAPS update
PluginTooOldBuilt against a host API this version no longer speaks. The plugin needs updating
NoEntryPointThe manifest names no plugin class
NotLoadableThe class could not be loaded, or is not a ZyrnPlugin. Carries the reason
NotEnabledDiscovered and allowed; the user simply has not switched it on

Native plugins: capabilities

The list is fixed. A plugin can't ask for a capability outside it, because no version of the app would know whether to grant or refuse it.

CapabilityWhat it grants
MAP_LAYERSAdd sources and layers to the map
MAP_CAMERARead the camera and request moves. Never writes it directly
MAP_QUERYQuery rendered features under a point
MAP_OBJECTSPut objects on the map and take them off again
MAP_EVENTSBe told where the user tapped (read-only)
MAP_ACTIONContribute an entry to the map's context menu
PANELContribute a panel to the UI
SEARCHContribute results to the search sheet
NOTIFICATIONPost a notification
LOCATIONRead the user's current position
STORAGENamespaced persistent storage
BUSSubscribe and publish on the integration bus

The last four in the list above (map objects, map events, notifications and search) were picked from what ATAK plugins register. Capabilities ZYRNTOPO has no equivalent for (radios, contacts, the GL item factory, video layers) are left out, so a plugin can't declare something the app can't provide.


Native plugins: the context

PluginContext is what you are handed on activation. Every registering method on it is tracked, and released on deactivate.

Map sources and layers

ctx.addSource("my-src", SourceSpec(type = "geojson", json = """{"type":"geojson","data":{...}}"""))
ctx.addLayer(LayerSpec(id = "my-layer", json = """{"id":"my-layer","type":"line","source":"my-src"}"""))

Sources and layers are style JSON as text, not SDK objects, for the same reason the map is a facade: a plugin compiled against RasterSource breaks the day the SDK renames it. On teardown, layers are always removed before their sources.

The map facade

interface MapFacade {
    fun hasSource(sourceId: String): Boolean
    fun addSource(sourceId: String, spec: SourceSpec)
    fun removeSource(sourceId: String)
    fun hasLayer(layerId: String): Boolean
    fun addLayer(spec: LayerSpec)
    fun removeLayer(layerId: String)
    fun requestCamera(intent: CameraIntent)   // intent, not a command
    fun camera(): CameraIntent                // read-only
}

The API is small because every method in it has to keep working through engine upgrades. A method is only added when a plugin can't do its job without it. There is no camera setter: requestCamera asks for a camera move, and the app's own camera controller makes it.

Storage, bus and entitlements

ctx.storage.set("last-run", System.currentTimeMillis().toString())
val sub = ctx.subscribe("position") { payload -> /* … */ }
ctx.publish("my.topic", payload)
if (ctx.entitlements.isPro) { /* … */ }

Storage is namespaced by plugin id, so two plugins cannot collide on a key. Subscriptions are released for you; canceling one twice is not an error, because the host may already have released it.

Anything the API does not model

ctx.onCleanup { myTimer.cancel() }

Native plugins: panels

A plugin describes rows; the host draws them, in its own controls, honoring night mode, glove mode and the accent color.

Why not just hand you a View?

A View passed across the DexClassLoader boundary would tie every plugin to the app's UI toolkit. Plugins would compile against our Compose version and theme, and every upgrade of either would break the plugins already installed. Panels drawn by plugins in their own style would also look out of place, and would keep the wrong colors when the map switches to night mode.

The downside is that a plugin can't show a chart or a camera preview yet.

The rows

RowRenders as
Reading(label, value)A label and a value, in the shape the diagnostics screens use
Note(text)A paragraph: an explanation, or a refusal with its reason in it
Section(title)A heading between groups of readings
Action(id, label, enabled, onInvoke)A chip that does something. enabled = false draws it dimmed rather than hiding it
Toggle(id, label, on, onChange)A chip that is lit or not, reporting its new state when pressed

rows is a lambda, and you must call refreshPanels()

ctx.registerPanel(PluginPanel(
    id = "mine",
    title = "My Plugin",
    rows = { listOf(PanelRow.Reading("Queue", "$queued")) },   // read at DRAW time
))

rows is a lambda rather than a list so a panel showing a count, a status or a reading updates when the underlying thing moves. A snapshot taken at registration is a panel that is correct exactly once.

A live lambda is still not enough on its own. The lambda closes over your plugin's own fields: ordinary fields in an ordinary class, which the UI toolkit is not watching. So a panel showing a queue that grew because your backend sent an event keeps showing the old count until something unrelated redraws the screen. Call ctx.refreshPanels() whenever something a row displays has moved. It is cheap, idempotent, and safe to call from a background thread. Calling it when nothing changed costs one recomposition of one screen; not calling it is a panel that is correct once, the exact failure the lambda was chosen to avoid, arriving through the other door.

Native plugins: objects, taps, notices and search

Map objects

ctx.addObject(PluginObject(id = "beacon", featureJson = """
    {"type":"Feature",
     "geometry":{"type":"Point","coordinates":[-122.6765,45.5231]},
     "properties":{"name":"Beacon"}}
""".trimIndent()))
ctx.removeObject("beacon")

Geometry is a GeoJSON Feature as text, which the object store already speaks and what every export path understands. The host rejects anything that is not one. Your id is namespaced to plugin:<your-id>:<yours>, so two plugins choosing the same name cannot collide, a deactivate removes exactly what you added, and nothing the user drew is ever touched.

Map taps

ctx.onMapTap { tap -> /* tap.lat, tap.lon, tap.zoom */ }

Read-only, and you cannot consume the event.

Notifications

ctx.notify(PluginNotice(title = "My Plugin", body = "Something happened."))

A title and a body, and nothing else; see what a plugin cannot do for why.

Search providers

ctx.registerSearch { query ->
    if (!query.equals("beacon", ignoreCase = true)) emptyList()
    else listOf(PluginSearchResult("Beacon", "A place only this plugin knows", 45.5231, -122.6765))
}

The case this exists for is a team with its own gazetteer (grid squares, hut names, a numbered pole network) that no general geocoder has heard of, and which is exactly what somebody types into a search box first.

Search providers must be fast and offline. Yours runs beside the coordinate parser, which answers before anything is debounced. A provider that needs the network belongs behind a panel button, not here.

Native plugins: signing, trust and versioning

Signing

Sign with your own key. ZYRNMAPS reads the signature, records the SHA-256 fingerprint and shows it, and loads your plugin either way; see Who signed it for what users are told. Publish your fingerprint so people can check it.

Do not test with Android's shared debug keystore. Every debug build uses it, including ZYRNMAPS' own, so under it your APK reads as first-party, and the one path that was never in doubt becomes the only one you have tested. Generate a key of your own and confirm your plugin loads as third-party.
keytool -genkeypair -v -keystore my-plugin.keystore \
  -alias myplugin -keyalg RSA -keysize 2048 -validity 10000 \
  -dname "CN=My Plugin, O=My Org, C=US"

Host API versioning

ConstantValueMeaning
HostApi.VERSION2The current host API
PluginManifest.MIN_PLUGIN_API1The oldest plugin API still loaded

Version 2 is where the surface grew map objects, map taps, notifications, search providers and panel content. A plugin built against 1 declares minHostApi = 1 and still runs, because the comparison is minHostApi > VERSION; a plugin that needs the newer surface says so and is refused with a sentence rather than a NoSuchMethodError at some later moment.

The floor stays at 1 until something is actually removed from the contract. Refusing an old plugin costs a user a working tool; loading one that calls a deleted method costs them a crash, so the floor moves only when the second becomes true.

Manifest keys, in one place

KeyValue
zyrn-plugin-apiThe host API you built against. Doubles as the marker that this APK is a plugin
zyrn-plugin-classFully-qualified class implementing ZyrnPlugin
com.zyrntopo.plugin.ACTION_PLUGINThe intent action your marker activity declares, so the package is visible on Android 11+

The same key gives the API version and marks the package as a plugin, so the two can't disagree.


Native plugins: a complete example

This exercises every surface the host honors today: it drops a marker, adds a panel with live counts, answers one search word, listens for taps and posts a notice on the third one.

class SamplePlugin : ZyrnPlugin {
    override val id = "com.example.sample"
    override val name = "Sample Plugin"
    override val minHostApi = 2
    override val capabilities = setOf(
        Capability.PANEL, Capability.STORAGE, Capability.MAP_OBJECTS,
        Capability.MAP_EVENTS, Capability.NOTIFICATION, Capability.SEARCH,
    )

    private var taps = 0
    private var lastTap: MapTap? = null

    override suspend fun onActivate(ctx: PluginContext) {
        ctx.addObject(PluginObject("beacon", """
            {"type":"Feature",
             "geometry":{"type":"Point","coordinates":[-122.6765,45.5231]},
             "properties":{"name":"Sample plugin beacon"}}
        """.trimIndent()))

        ctx.onMapTap { tap ->
            taps++
            lastTap = tap
            ctx.refreshPanels()          // without this the panel is frozen
            if (taps == 3) ctx.notify(
                PluginNotice("Sample plugin", "You have tapped the map $taps times.")
            )
        }

        ctx.registerSearch { query ->
            if (!query.trim().equals("beacon", ignoreCase = true)) emptyList()
            else listOf(PluginSearchResult(
                "Sample plugin beacon", "A place only this plugin knows about",
                45.5231, -122.6765,
            ))
        }

        ctx.registerPanel(PluginPanel(
            id = "sample",
            title = "Sample plugin",
            rows = {
                listOf(
                    PanelRow.Reading("Taps seen", "$taps"),
                    PanelRow.Reading("Last tap", lastTap?.let { "%.5f, %.5f".format(it.lat, it.lon) } ?: "none yet"),
                    PanelRow.Note("Search \"beacon\" to find this plugin's marker."),
                    PanelRow.Action("reset", "Reset count") {
                        taps = 0; lastTap = null; ctx.refreshPanels()
                    },
                )
            },
        ))
    }

    // The object, tap handler, search provider and panel were registered
    // through the context, which releases them on deactivate.
}
The teardown is empty. Everything registered through the context is released when the plugin is deactivated, so there is nothing to clean up by hand.

Verifying your plugin actually loads

Unit tests cannot prove the part that really fails. Activating an object your test constructed says nothing about whether a separately compiled, separately signed APK loaded through a DexClassLoader sees ZyrnPlugin as the same interface the host holds. Install the real APK on a real device, enable it, and confirm the Plugins screen reports it as third-party and running.