Maps
@fieldia/map puts maps in Fieldia pages, on the provider you choose: Azure Maps, Google, Mapbox, your own map images, or OpenStreetMap's for a demo. The page says what each map shows, by field name; the app gives the maps once.
Giving the viewer its maps
Three parts, each loaded only where a page uses it: the fields (mapWidgets), a list's records on a map (mapListViews), and a map on a form (mapParts). They share one set of settings: the provider above all.
import { azureMaps, mapListViews, mapParts, mapWidgets } from '@fieldia/map';
const maps = { provider: azureMaps({ token: () => fetch('/maps/token').then((r) => r.text()) }), center: { lat: 30.04, lng: 31.24 } };
mountViewer(host, {
page,
dataSource,
widgets: mapWidgets(maps), // "widget": "place" and "area"
listViews: mapListViews(maps), // a list with "map"
maps: mapParts(maps), // "type": "map" on a form
});
In React they are props of the same names, in Vue props, in Angular inputs. Leaflet, the map engine, loads the first time a map is shown, and a provider's routes the first time one is asked for: a page without a map carries none of it.
A list's map in a page of your own
A page that is not Fieldia's — your own list, filters and address — can show a list's map beside them with mountListMap. Give it the page that describes the records (a list with map), where they come from (a data source, or the records themselves), the conditions they are found by, and what to do when the map asks for something; it hands back the map, to reload and destroy.
import { mountListMap, azureMaps } from '@fieldia/map';
const [lat, lng, zoom] = (new URLSearchParams(location.search).get('at') ?? '').split(',').map(Number);
const map = mountListMap(document.getElementById('map'), {
page, // "map": { …, "height": "fill", "gestures": "all", "fitZoom": 14 }
provider: azureMaps({ token }),
dataSource, // or records: [{ id, values }, …]
filter: () => yourSearch(), // the conditions the records are found by
start: zoom ? { center: { lat, lng }, zoom } : undefined, // opens there, no fit at first
countArea: true, // "Search this area (24)": the source counts the rest
onSearch: ({ kind, filter }) => { keep(kind, filter); map.reload(); }, // view, drawn or region
onViewChange: ({ center, zoom, bounds, byPerson }) => writeTheAddress(center, zoom),
onHighlight: (id) => lightYourRow(id),
onOpenRecord: (id) => location.assign(`/homes/${id}`),
});
row.addEventListener('mouseenter', () => map.highlight(id));
The map brings its own base, on its own element alone: a part hidden stays hidden, words for a screen reader alone, its buttons. Its colours are Fieldia's tokens, light or dark by your page's color-scheme; set your own on .fd-map-embed, as .fd-map-embed { --fd-accent: #375dfb; }, and your rule wins whatever its weight. onSearch hands over one area at a time of each kind — the map's view (view), an area drawn (drawn), a shaded area pressed (region) — for your search to keep, a new one in the last one's place; onViewChange comes once the map stops, never while it moves. Try homes in a page of their own.
A place and an area
A place is a field set by a press on the map, a dragged pin, an address found as it is typed, typed coordinates or “My location”; an area is a polygon, a box or a circle. An address with suggestions (address-search on a char field) is the same search without a map: a suggestion picked fills a place field (placeField) and an address's parts (partsField), within the countries the page names (countries, ISO codes: Azure Maps is asked once for each, Google for fifteen at most), nearest a point first (near, in the page's own country) or where the app's maps open, the addresses before shops and malls. See maps among the fields.
A list's records on a map
A list page with map shows its records on a map beside the table or instead of it, its pins drawn as the page says, its search and choice shared with the table. See lists on a map.
A map on a form
A part of type map shows the record's places as layers. A layer is the record's own place, a place for each of its lines, a route between them, or the reader's own place:
{ "type": "map", "id": "order-map", "label": "Where your order is", "layers": [
{ "id": "kitchen", "label": "Kitchen", "latitude": "kitchen_lat", "longitude": "kitchen_lng",
"pin": { "look": "icon", "icon": "food" } },
{ "id": "door", "label": "Your door", "point": "door", "pin": { "look": "icon", "icon": "home" } },
{ "id": "courier", "label": "Courier", "latitude": "courier_lat", "longitude": "courier_lng",
"pin": { "look": "photo", "image": "courier_photo", "label": "eta_text" },
"live": { "staleAfter": 60, "follow": true } },
{ "id": "way", "route": { "from": "courier", "to": "door", "progress": true,
"write": { "seconds": "eta_seconds", "metres": "distance_m", "line": "route_line" } } },
{ "id": "me", "label": "You", "myLocation": true }
] }
| A layer's key | What it does |
|---|---|
point · latitude longitude | Where the place is kept: a json field holding a GeoJSON Point, or two number fields. |
lines | A field of lines: a place for each line, read from the line's own fields (a round's stops, numbered by a field). |
label | Words for the layer: its pins are named by them for a screen reader, “Courier, Mona Adel, 4 min”. |
pin · card | As a list's map has them: a pin, words, an icon, a round photo or a dot, coloured by condition; a card with its title and fields. |
live | A place that moves: see below. |
route | A line along the roads: see below. |
myLocation | The reader's own place, from the browser once they press its button and allow it. |
invisible | A condition, as any part's: no route while an order is still being made. |
Pins that crowd are gathered as a list's map gathers them, again at each zoom: a bubble with their count, named for a screen reader by whose they are (“2 here: Delta Foods, Amira Clinics. Zoom in to see them.”), pressed to zoom in until they part, or to list them when they stand on one spot. A place that moves is never hidden in a bubble: the pins that crowd it are gathered under it, and it wears their count. Routes, reachable areas and fences read where the places are, never where a bubble stands.
Try the order on its way · try a delivery round.
Places that move
When a layer's fields change, its pin glides to the new place: along its route when it is on it, straight otherwise (and at once for a reader who asks for less motion). The app sends each new place as a courier's app sends it, with setValues:
handle.form.setValues({ courier_lat: 30.0512, courier_lng: 31.2337 });
With staleAfter, a place without news for that many seconds greys and says when it was last seen, in the page's language. With follow, the map keeps it in view, until the person moves the map; then a Follow button brings it back.
Routes
A route runs from a place (or the reader's own) to another, or through a layer of lines in their order, by car, bike or foot. With progress, the part travelled fades as from moves along it, and the time and distance left are worked out as it goes. write names the record's fields its seconds, metres and line go into: values for words (“4 min”) and for keeping. A route is asked for again when its ends change, or when its traveller leaves it, at most every ten seconds.
| Provider | Routes |
|---|---|
azureMaps | By car and on foot (Route Directions of 2025), with the time in traffic when it gives one. |
googleMaps | By car (with traffic, which Google bills as Compute Routes Pro) and on foot; by bike where Google has bicycle routes, none in Egypt, Saudi Arabia, the UAE or Jordan. Through Google's Routes API: enable it for the key. Up to 25 stops; more than 10 are billed as Pro. |
mapbox | By car (with traffic), bike and on foot. |
openStreetMap | OSRM's public demo server, by car alone, for trying things out; or a copy you run (osrmUrl). |
tiles, or any | The app's own: route: ({ points, mode }) => ({ line, metres, seconds }). |
Values a route writes are changes to the record, as the page asks: they are saved as any change is. A page that only follows an order, and keeps nothing, is shown read-only.
Areas by colour
Districts, zones or municipalities, shaded in up to five steps of the page's accent, with a legend. Their borders are the app's, never Fieldia's: GeoJSON polygons, from your own data or an open source such as OpenStreetMap or geoBoundaries, given to the maps once by name, and loaded the first time a map shades them.
const maps = {
provider,
regions: { districts: () => fetch('/geo/districts.json').then((r) => r.json()) },
};
On a list's map, each area is shaded by its records: those whose field holds the area's key, counted by the data source's groups under the search. A press searches for an area's records (“In Zamalek”), and the other areas keep their shades. On a form's map, an area is shaded by a number among its own properties, and a press chooses it into a field.
"map": { "point": "place", "regions": { "from": "districts", "key": "code", "name": "name", "field": "district" } }
{ "id": "districts", "label": "Delivery fee, EGP",
"regions": { "from": "districts", "key": "code", "name": "name", "shade": "fee", "choose": "district" } }
Pointing at an area lights it and says its name and number. On the keyboard the areas are one stop: the arrow keys go from one to the next in reading order, Enter presses one. Try the orders by district · try the kitchen's map.
A heat map, and nearest first
With heat, a list's map shows its records as a heat map while it is far out, each weighing one or the number its weight names, their pins coming back close in, at the streets. With the tool nearest, a button under the zoom puts the list in order from where the reader is (the middle of the map, if they do not say): the table and the map both, the data source asked with sort: [{ field, near }]. Try the heat map.
The area within some minutes, and fences
A layer with reachable draws the area within some minutes of a place (or of the reader's own), by car unless said, asked again once the place has moved:
{ "id": "near", "label": "Within 15 minutes' drive", "reachable": { "from": "kitchen", "minutes": 15 } }
| Provider | The area within some minutes |
|---|---|
azureMaps | By car (Route Range of 2025). |
mapbox | By car, bike and on foot (Isochrone). |
googleMaps, openStreetMap | None: the app's own, reach: ({ at, minutes, mode }) => ({ rings }). Google's Isochrones API is in preview; Fieldia does not use it yet. |
A fence runs the page's own steps, as a button's, when a place that moves goes into an area or comes out of it: an area field, or a reachable area of the map. Never at first sight, when the place was there already:
"fences": [{ "layer": "courier", "area": "drop_zone", "when": "enter",
"steps": [{ "do": "set", "field": "arriving", "value": "true" }, { "do": "call", "action": "tell_customer" }] }]
A fence runs in the browser that shows the map: for something that must happen once, whoever is looking, your server watches the place instead.
Tools under the zoom
Any map, a field's, a list's or a form's, takes tools: satellite (no button where the provider has no satellite images), traffic (the provider's live traffic over the map, under its pins; no button where the provider has none), fullscreen (filling the window where the browser refuses the screen), measure (points clicked, or put at the cross with Space; its length said as it grows) and, on a list's map, nearest. Their code comes with the first map that has them.
| Provider | Live traffic |
|---|---|
azureMaps | How each road flows against its free flow, from zoom 6, in darker colours on the dark map; drawn again every four minutes, as Azure's images go out of date. |
googleMaps | Its traffic layer, a Map Tiles session of its own. |
mapbox | From trafficStyle: a style of yours that draws Mapbox Traffic's lines and nothing else. Mapbox has no such style of its own to draw as images. |
tiles | The app's own, from trafficUrl. |
openStreetMap | None. |
In the designer
The screen editor offers a Map among its blocks when it is given maps: its panel sets its name, its height, its tools and its layers, each with the fields it reads, adds a route between two of them, areas by colour from the app's borders (regionSets), the area within some minutes, and fences, each with the steps it runs, set as a button's are. A list's panel sets its map's tools, a heat map and its areas by colour. Try it draws the map, its places made up around the middle the app gives (sampleCentre).