Skip to content

πŸ—ΊοΈ Map & Geospatial ​

The map tab uses @rnmapbox/maps v10 with Turf.js for geospatial calculations.

Token Setup ​

Two Mapbox tokens are required:

TokenEnv VarTypeUsage
Public (pk.)EXPO_PUBLIC_MAPBOX_ACCESS_TOKENRuntimeMap rendering
Secret (sk.)RNMAPBOX_MAPS_DOWNLOAD_TOKENBuild-time onlyDownloads 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/) ​

ComponentPurpose
map-viewRoot MapboxGL.MapView wrapper with camera, gesture config
fields-layerRenders company fields as filled polygons
notes-layerRenders note pins (location-tagged drafts)
parcels-layerRenders cadastral parcels from the API
reference-records-layerRenders reference records (e.g. machinery, assets) as pins
map-controlsZoom, compass, and locate-me buttons
map-home-drawerMap entry drawer (workspace, "Talk to Tellia", quick nav)

Tapping Features ​

Tapping a field, note, or parcel opens the corresponding action sheet via SheetManager:

tsx
// 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():

ts
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.

ts
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:

ts
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);