Map
A MapKit view showing markers and polyline paths. Feed it the native payload or a
GeoJSON FeatureCollection; the camera auto-fits everything it's given. Optionally
interactive.
Type
"map"
Relevant Fields
Inherits all Control Definition. Key fields:
| Field | Type | Default | Description |
|---|---|---|---|
mapStyle |
string | "standard" |
"standard", "satellite", "hybrid", "globe" |
mapInteractive |
bool | true |
Allow user pan/zoom |
mapZoom |
number | 0.01 |
Default coordinate span in degrees (ignored by "globe", which always frames the whole Earth) |
controlHeight |
number | 120 (min) |
In a 2-D grid, give the map more rowSpan for a taller map (height = rowSpan × rowHeight). In a flow grid it is ~120pt by default — set this (e.g. 300) for a full-screen-feel map. See Grid Dimensions. |
label |
string | — | Header label above the map |
tint |
string | "#667eea" |
Default marker color (a marker's own color wins) |
mapConfig |
object | — | Property→style mapping for GeoJSON feeds — see GeoJSON payloads |
Sync Payload Structure
The map receives its data as a JSON string via Sync — or, since it is a JSON-document control, as a plain JSON object/array, which the app encodes for it. That means an HTTP source can point straight at a GeoJSON endpoint with no server in between.
Native payload
{
"center": [42.3601, -71.0589],
"markers": [
{ "coordinate": [42.3601, -71.0589], "color": "#FF9F0A", "size": 16, "label": "Depot" },
{ "coordinate": [42.3875, -71.0995], "color": "#FF453A", "size": 20, "label": "Stalled",
"ripple": { "color": "#FF453A", "period": 1.4, "radius": 56 } }
],
"paths": [
{
"points": [[42.36, -71.05], [42.37, -71.06], [42.38, -71.04]],
"color": "#FF0000"
}
]
}
| Field | Type | Description |
|---|---|---|
center |
[lat, lng] | Optional. Drawn as a single pin only when there are no markers and no paths |
markers |
array | Optional array of pins |
markers[].coordinate |
[lat, lng] | Required. lat + lng (or lon/longitude) keys are also accepted |
markers[].color |
string | Hex fill; falls back to the control's tint |
markers[].size |
number | Dot diameter in points (default 12, clamped 4–60) |
markers[].label |
string | Text under the dot (title is used when absent) |
markers[].ripple |
bool | object | An expanding ring animated at this marker |
markers[].ripple.color |
string | Ring color (default: the marker's color) |
markers[].ripple.period |
number | Seconds per expansion (default 1.6) |
markers[].ripple.radius |
number | Widest ring radius in points (default 44) |
paths |
array | Optional array of polyline paths |
paths[].points |
[[lat, lng], ...] | Array of coordinate pairs |
paths[].color |
string | Hex color for this path line |
ripple is data, not a wire event: it rides in the payload, so an HTTP poll can
raise it. That's different from Pulse, which flashes the whole control when a
MeshSocket frame arrives.
GeoJSON payloads
A payload whose type is FeatureCollection, Feature, or a bare geometry is read
as GeoJSON:
Point/MultiPoint→ markersLineString/MultiLineString/Polygonrings /MultiPolygon→ pathsGeometryCollection→ walked recursively
GeoJSON positions are
[longitude, latitude]— the reverse of the native payload's[lat, lng]. The app flips them for you; don't pre-flip your feed. A third element (elevation/depth) is ignored.
Feature properties style each marker. Recognized without configuration:
color or marker-color, size or marker-size, label or name, title,
ripple: true; paths read color or stroke.
For a feed you don't control, mapConfig points at whatever properties it does
have:
mapConfig field |
Type | Default | Description |
|---|---|---|---|
markerSizeProperty |
string | — | Numeric property that drives marker diameter |
markerSizeDomain |
[number, number] | [0, 10] |
Input range of that property |
markerSizeRange |
[number, number] | [8, 30] |
Output diameter in points (clamped at the ends) |
markerColorProperty |
string | — | Property holding a hex color |
markerLabelProperty |
string | — | Property used as the marker label |
rippleProperty |
string | — | Numeric property that decides which markers ripple |
rippleThreshold |
number | 0 |
rippleProperty >= threshold ⇒ that marker ripples |
ripple |
object | — | Ring styling (color/period/radius) for those markers |
maxMarkers |
number | 400 |
Cap on rendered markers — public feeds can be unbounded |
Object-array feeds (not GeoJSON)
Many public feeds are plain-JSON arrays of point objects, not GeoJSON — a flight feed
{ "ac": [ { "lat": …, "lon": …, "flight": … } ] }, a single-point tracker (the ISS:
{ "latitude": …, "longitude": … }), or a raw [lng, lat, weight] grid. Point the map
at those by naming the coordinate fields; all the styling keys above still apply (their
"property" is a key on each object). Declaring markersPath or markerLatProperty
switches the map into this mode — ordinary payloads are unaffected.
mapConfig field |
Type | Default | Description |
|---|---|---|---|
markersPath |
string | — | Dot-path to the array of objects (e.g. "ac"). Omit if the payload is the array, or a single point object. |
markerLatProperty / markerLngProperty |
string | — | Latitude/longitude keys on each object. lat/lon/latitude/longitude are auto-detected when omitted. |
markerIdProperty |
string | — | Stable identity per marker across polls — required for smooth motion with more than one marker (e.g. "hex"). |
coordinateOrder |
string | "lnglat" |
For raw [a, b, weight] number triples (e.g. NOAA aurora): "lnglat" or "latlng". The third element feeds size (markerSizeDomain) and ripple. |
minWeight |
number | 0 |
Number-triple mode: drop points weaker than this before the maxMarkers cap keeps the strongest. |
Marker style & SF Symbols
mapConfig field |
Type | Default | Description |
|---|---|---|---|
markerStyle |
enum | "dot" |
dot, symbol (a bare SF Symbol), or puck (an SF Symbol inside a filled circle). |
markerSymbol |
string | — | Default SF Symbol name for symbol/puck (e.g. "airplane"). |
markerSymbolProperty |
string | — | Per-object property giving an SF Symbol name, overriding markerSymbol. A feed property named symbol/sf-symbol is honored without config. |
Motion & camera
mapConfig field |
Type | Default | Description |
|---|---|---|---|
motion |
bool | false |
Smoothly interpolate marker positions between polls so they glide instead of teleporting. Needs markerIdProperty for >1 marker. |
motionDuration |
number | 1.2 |
Seconds a glide takes — set near your poll interval. |
recenter |
enum | "auto" |
How the camera reacts to new data: auto (frame + follow, but stop the moment the user pans/zooms), always (refit every update), initial (frame once), off (never move). |
center |
[lat, lng] | — | Pin the camera to a fixed point (and the control's mapZoom) instead of fitting the data's bounds — e.g. frame a high-latitude feed. |
Example — live flight radar, no server
{
"type": "map", "id": "flights", "position": [0, 0], "span": [8, 4],
"mapStyle": "hybrid", "mapZoom": 3.2, "tint": "#30D158",
"mapConfig": {
"markersPath": "ac", "markerLatProperty": "lat", "markerLngProperty": "lon",
"markerIdProperty": "hex", "markerLabelProperty": "flight",
"markerStyle": "puck", "markerSymbol": "airplane",
"motion": true, "motionDuration": 8, "recenter": "initial", "maxMarkers": 200
},
"sync": [{ "method": "http", "url": "https://api.adsb.lol/v2/lat/40.7/lon/-74/dist/100", "interval": 8 }]
}
Examples
Vehicle tracker
{
"type": "map",
"id": "vehicle-location",
"position": [0, 0],
"span": [3, 4],
"mapStyle": "standard",
"mapInteractive": true,
"mapZoom": 0.005,
"label": "Vehicle",
"tint": "#FF3B30",
"sync": [{ "method": "meshsocket", "type": "listen", "event": "broadcast", "filter": { "msg_type": "gps_update" }, "valuePath": "map_data" }]
}
Static satellite view (non-interactive)
{
"type": "map",
"id": "property-view",
"position": [0, 0],
"span": [2, 4],
"mapStyle": "satellite",
"mapInteractive": false,
"mapZoom": 0.002,
"label": "Property"
}
Fleet map with multiple markers and a ripple
{
"type": "map",
"id": "fleet-map",
"position": [0, 0],
"span": [6, 4],
"label": "Fleet",
"mapZoom": 0.05,
"tint": "#FF9F0A",
"sync": [{ "method": "meshsocket", "type": "listen", "event": "broadcast", "filter": { "msg_type": "fleet_positions" }, "valuePath": "map" }]
}
The server broadcasts {"msg_type":"fleet_positions","map":{"markers":[…],"paths":[…]}} —
see Native payload for the marker shape.
Live USGS earthquake feed (GeoJSON over HTTP, no server)
{
"type": "map",
"id": "quake-map",
"position": [0, 0],
"span": [6, 4],
"label": "USGS — last hour",
"mapStyle": "hybrid",
"mapZoom": 20,
"mapConfig": {
"markerSizeProperty": "mag",
"markerSizeDomain": [0, 7],
"markerSizeRange": [6, 30],
"markerLabelProperty": "place",
"rippleProperty": "mag",
"rippleThreshold": 4.0,
"ripple": { "color": "#FF453A", "period": 1.8, "radius": 60 }
},
"sync": [{ "method": "http", "url": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson", "interval": 300 }]
}
The endpoint's raw response is consumed as-is: each quake's [lng, lat, depth]
position becomes a marker, properties.mag scales it, properties.place labels it,
and M4.0+ quakes ripple. No valuePath is needed — the whole document is the payload.
Worldwide feed on the globe
{
"type": "map",
"id": "quake-globe",
"position": [0, 0],
"span": [6, 4],
"label": "Worldwide seismicity",
"mapStyle": "globe",
"mapConfig": {
"markerSizeProperty": "mag",
"markerSizeDomain": [0, 8],
"markerSizeRange": [5, 26],
"markerLabelProperty": "place"
},
"sync": [{ "method": "http", "url": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson", "interval": 300 }]
}
"globe" is the style to reach for whenever the data is global. A fitted region
can't express "the whole planet" — MapKit's Mercator projection crops a 180°
latitude span to roughly 120° of longitude in a portrait cell, so a worldwide feed
opened on a partial world you had to pinch out of. The globe style instead points
the camera at the data's centroid from ~42,000 km, which keeps every marker and
both poles in frame at any spread.
Route history with multiple paths
{
"type": "map",
"id": "route-map",
"position": [0, 0],
"span": [4, 4],
"mapStyle": "standard",
"mapZoom": 0.02,
"label": "Today's Routes",
"sync": [{ "method": "meshsocket", "type": "listen", "event": "broadcast", "filter": { "msg_type": "route_history" }, "valuePath": "map_data" }]
}
Behavior
- Every marker renders as a colored dot with a white ring;
label/titleshows beneath it - A
centerwith no markers and no paths draws one pin in the control'stintcolor - Paths render as polylines with their individual colors
- The camera auto-fits the bounding box of all markers and path points (padded);
with a single point it centers there at
mapZoom mapStyle: "globe"overrides that fit: satellite imagery with realistic elevation, viewed from ~42,000 km at the data's centroid, so the whole Earth is in frame. Use it for any worldwide feed;mapZoomhas no effect on it- Rippling markers animate an expanding ring at their coordinate; ripples are suppressed under Reduce Motion
- When no data is synced, shows a placeholder with a map icon
- Map interactions (pan/zoom) don't fire any actions — display only
- A re-broadcast that resolves to the same center is ignored, so a live feed doesn't fight the user's panning
Notes
- The sync value may be a JSON string or a JSON object/array — both work
- Multiple paths with different colors can show different routes, zones, etc.
mapZoomis a coordinate span — smaller values = more zoomed in. It only applies when there's a single point to show; multi-marker data is auto-fitted- Legacy
{ "center": …, "paths": … }payloads are unchanged —markersis additive
Related
- Control Definition — Base fields
- Sync — How map data is received