Skip to main content

Map Implementation

Map Provider​

  • Uses react-native-maps with Google provider on Android, Apple Maps on iOS
  • iOS: mapType="mutedStandard" for a subdued look
  • Android: Custom JSON map style (grey/silver palette, POI labels hidden)

Custom SVG Markers (Primary Tier)​

Each high-priority place renders as a custom SVG pin marker (MarkerIcon component) matching the web version's design:

  • Same SVG path data (viewBox 0 0 29 31)
  • White circle background for contrast against the map
  • Pin color: COLORS.primary (teal), selected: red (#B91C1C)
  • Selected markers scale to 1.3x
  • Place name rendered below the pin as a <Text> with white text shadow highlight
  • Names truncated to 14 characters with ellipsis
  • Staggered opacity fade-in animation (30ms delay between each marker)
  • tracksViewChanges={true} to support the fade-in animation
  • pointerEvents="none" on all child views inside Marker (container, MarkerIcon views, and text labels) to prevent them from intercepting marker taps

Marker Tap Handling​

Marker taps are handled via the native onMarkerPress event on MapView rather than per-Marker onPress callbacks. When a marker is tapped, the handler uses a placesById lookup map (built from the places array) to resolve the tapped place by its marker identifier.

Selected Marker Behavior​

When a place is selected:

  • Primary markers get a red fill (#B91C1C), 1.3x scale, and zIndex 999
  • Secondary dots are promoted to full MarkerIcon pins with red fill when selected
  • Marker keys include selection state (${id}-sel / ${id}-def) to force native re-render on selection change

Secondary Dot Markers​

Lower-priority places that don't fit as primary markers are shown as small dots:

  • 14px teal circles with 2px white border
  • Drop shadow for depth
  • Staggered opacity fade-in (15ms delay, starting after primary markers finish)
  • Tapping a dot selects the place and shows its detail in the bottom sheet

Two-Tier Overlap Removal (replaces clustering)​

Instead of react-native-map-clustering, the app uses a priority-based two-tier deduplication algorithm (tieredMarkers() function):

Primary pass​

  1. Sort all places by priority score: verified (+100) > promoted (+50) > self-reported (+10) > rating
  2. Iterate sorted list; for each place, compute approximate pixel distance to all already-placed primary pins
  3. If within 50px radius (OVERLAP_PX_PRIMARY), skip the lower-priority pin
  4. Result: clean set of non-overlapping SVG pin markers

Secondary pass​

  1. Take remaining places not in the primary set
  2. For each, check pixel distance against all placed markers (both primary and secondary)
  3. If within 10px radius (OVERLAP_PX_SECONDARY), skip
  4. Cap at 200 dots (MAX_SECONDARY_DOTS)
  5. Result: dense field of small dots filling gaps between primary pins

Pixel distance is approximated from lat/lng deltas relative to the current map region's latitudeDelta/longitudeDelta, assuming a ~400px map width.

Bottom Sheet​

  • @gorhom/bottom-sheet with three snap points: 12% (peek), 45% (half), 85% (full)
  • enableOverDrag={false} to prevent over-scrolling
  • List mode: Shows a scrollable BottomSheetFlatList of PlaceCard components. Tapping a card calls selectPlace() (inline drawer, no navigation).
  • Detail mode: When selectedPlace is set, shows inline place detail with "← Back to results" header, place info, ACS attributes, and "Write a Review" CTA.
  • Sheet tracks its index via onChange; when fully expanded (index 2 / 85%), a floating "Back to map" pill button appears at center-bottom.

Floating Controls​

  • Search bar — Top of screen, shows the EM logo icon, a divider, search icon, and placeholder text. Tapping opens the search modal.
  • Zoom buttons — Right side, +/− buttons that halve or double the region deltas with animation.
  • Location button — Below zoom buttons, requests GPS and animates to user's position.
  • "Back to map" pill — Center-bottom, only visible when sheet is at 85%. Collapses sheet to 12%.

Location​

  • expo-location for GPS permission and current position
  • "My location" floating button animates the map to the user's coordinates (0.06 degree deltas)