Skip to content

Maps

Tomoda renders maps through the MapLibre rendering stack. The same shared types and marker spec drive both surfaces, but each platform uses its native renderer:

Platform Renderer Package
Web MapLibre GL JS maplibre-gl
iOS / Android MapLibre Native (Metal / OpenGL ES) @maplibre/maplibre-react-native

No WebView. No JS bridge for tile rendering. Markers on native are real React Native views composited by the platform.

Where the code lives

Path Role
frontend/components/map/MapView.tsx Shared types: MapMarker, MapRegion, MapControls, MapViewProps.
frontend/components/map/MapView.web.tsx Web shim. Mounts <MapEngine> inline.
frontend/components/map/MapView.native.tsx Native shim. Wraps @maplibre/maplibre-react-native <Map> + <Marker> per marker.
frontend/components/map/engine/MapEngine.tsx Web-only MapLibre GL JS wrapper. Owns the JS map instance, marker DOM diff loop, style swap, event emission.
frontend/components/map/engine/styles.ts Shared style descriptors (CARTO raster source, light/dark).
frontend/components/map/engine/markers/ Web-only HTML marker components (divs + CSS for hover/glass-card expansion).
frontend/components/map/native/MarkerOverlay.tsx Native marker components (RN primitives: View, Image, Text, react-native-svg).
frontend/components/map/MapAttribution.tsx Shared compact ⓘ pill (hidden by default; tap/hover reveals OSM + CARTO credit).

Tiles

Both platforms consume CARTO raster basemaps (basemaps.cartocdn.com). Free, no API key. Light + dark variants selected via the mapTheme prop.

To move to vector tiles or self-host, edit engine/styles.ts. The descriptor is passed through to both renderers identically.

Marker API

Callsites pass a flat markers: MapMarker[] array. The union is discriminated by type:

Variant Visual
pin Generic single-point pin. Used by LocationPicker.
user Current user's avatar tile.
friend Friend avatar tile with optional online/active status dots.
cluster Stacked-avatar cluster bubble with count.
discovery Unified marker for event, location, moment, geo_cluster sub-kinds.

onMarkerPress(id) fires when any marker is tapped. Marker ids are caller-controlled.

Adding a new marker variant

  1. Extend the MapMarker union in MapView.tsx.
  2. Add a web case in engine/markers/index.tsx (HTML primitives).
  3. Add a native case in native/MarkerOverlay.tsx (RN primitives).
  4. Add styling for both. Reuse MARKER_COLORS + getEventIcon / getLocationIcon from components/ui/categoryIcons.ts for visual consistency.

Map controls

onMapReady receives MapControls once the map loads:

Method Web Native
zoomIn() / zoomOut() map.zoomIn() / map.zoomOut() cameraRef.zoomTo(zoom ± 1)
animateTo(lat, lng, zoom?) map.flyTo cameraRef.flyTo
panTo(lat, lng, padding?) map.easeTo({ padding }) cameraRef.easeTo({ padding })
canZoomIn() / canZoomOut() Compare with getMaxZoom / getMinZoom Always returns true

panTo is the right call when a UI panel covers part of the viewport — the padding rect tells the map which area to centre within.

Why this stack

  • No WebView on native — tile rendering, gestures, and marker compositing all run on the GPU via Metal / OpenGL ES. Pan and zoom stay smooth even with hundreds of markers in view.
  • Same MapMarker schema on both platforms — adding a marker kind is a coordinated change but the data model + visual spec live in one place.
  • No tile-server infra — CARTO raster basemaps are free; a future migration to self-hosted vector tiles only changes engine/styles.ts.