πΊοΈ Map & Geospatial β
The map tab uses @rnmapbox/maps v10 with Turf.js for geospatial calculations.
Token Setup β
Two Mapbox tokens are required:
| Token | Env Var | Type | Usage |
|---|---|---|---|
Public (pk.) | EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN | Runtime | Map rendering |
Secret (sk.) | RNMAPBOX_MAPS_DOWNLOAD_TOKEN | Build-time only | Downloads Mapbox SDK during pod install / Gradle |
CAUTION
Never commit the secret sk. token. It is only needed in .env locally and in CI secrets β not in source code.
Layer Components (components/map/) β
| Component | Purpose |
|---|---|
map-view | Root MapboxGL.MapView wrapper with camera, gesture config |
fields-layer | Renders company fields as filled polygons |
notes-layer | Renders note pins (location-tagged drafts) |
parcels-layer | Renders cadastral parcels from the API |
reference-records-layer | Renders reference records (e.g. machinery, assets) as pins |
map-controls | Zoom, compass, and locate-me buttons |
map-home-drawer | Map entry drawer (workspace, "Talk to Tellia", quick nav) |
Tapping Features β
Tapping a field, note, or parcel opens the corresponding action sheet via SheetManager:
// Simplified from fields-layer
onPress={(feature) => {
SheetManager.show('field-action-sheet', {
payload: { fieldId: feature.id },
});
}}NOTE
Field drawing is currently disabled. The draw-a-polygon UI (DrawFieldButton, map-drawing-toolbar, field-drawing-layer) is hidden from the map ({/* Field drawing temporarily hidden */} in the map tab). The hooks/use-drawing.ts state machine and layers still exist for easy revival but are not reachable by users β don't rely on them.
Offline Map Downloads β
Each workspace (company) gets its own offline map pack so field workers keep map tiles without a connection. The pack is named workspace-<companyId>, uses the SatelliteStreet style, and covers zoom 10β16.
What gets downloaded β
The download region is a bounding box derived from the workspace's field polygons (computeWorkspaceBbox). If the workspace has no field geometry yet, it falls back to ~10 km around the last cached camera position. With neither fields nor a cached camera, canDownload is false β there's nothing meaningful to cache (we never download a pack at [0,0]).
Auto-download vs manual β
- Auto: the first time a workspace is opened on WiFi, the pack downloads automatically (once per workspace). Cellular never auto-triggers.
- Manual: users manage it from Menu β Offline map (
app/(app)/(tabs)/menu/offline-map.tsx) β download, see progress / size, or remove the pack.
Engine & hook β
State lives in a single-instance engine (hooks/use-offline-map-pack-engine.ts) mounted once via OfflineMapPackProvider. Read it through useOfflineMapPack():
const {
status, // 'idle' | 'downloading' | 'complete' | 'error'
percentage, // download progress 0β100
sizeBytes, // bytes downloaded so far
isWifi, // gates auto-download
isInitialized, // false until the existing-pack lookup resolves
canDownload, // workspace has a usable bbox
download, // start / restart the pack
remove, // delete the pack
} = useOfflineMapPack();IMPORTANT
Never mount the engine twice. rnmapbox keeps one listener slot per pack name and its JS pack cache is add-only, so two instances race on delete/create and steal each other's progress events. Always go through OfflineMapPackProvider / useOfflineMapPack().
CAUTION
deletePack does not cancel an in-flight download (rnmapbox keeps the native task running; re-creating the same pack then fails with "Group load with same identifier started"). pause() is the real cancel β cancelPackDownload in lib/map/offline-pack.ts handles it. A region exceeding Mapbox's 750-tile cap is recoverable: it's reported to Sentry as a warning, not an error.
Parcel Loading β
Parcels are fetched by map bounding box to avoid loading the entire country dataset. The useParcels(bbox) hook debounces bounds changes by 500ms to avoid hammering the API while the user pans.
const [bbox, setBbox] = useState<BoundingBox | null>(null);
const { data: parcels } = useParcels(bbox);
// Update bbox in onMapIdle or onRegionDidChange (debounced externally)The bbox parameter is [minLng, minLat, maxLng, maxLat].
Turf.js Utilities (lib/map/) β
Common geospatial helpers used in the app:
import * as turf from '@turf/turf';
// Check if a point is inside a polygon (e.g. note inside field)
turf.booleanPointInPolygon(point, polygon);
// Calculate field area in hectares
const area = turf.area(polygon) / 10_000; // mΒ² β ha
// Get bounding box of a polygon
const [minLng, minLat, maxLng, maxLat] = turf.bbox(polygon);