Map Implementation
Map Provider
- Uses
react-native-mapswith 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 animationpointerEvents="none"on all child views inside Marker (container,MarkerIconviews, 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
MarkerIconpins 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
- Sort all places by priority score: verified (+100) > promoted (+50) > self-reported (+10) > rating
- Iterate sorted list; for each place, compute approximate pixel distance to all already-placed primary pins
- If within 50px radius (
OVERLAP_PX_PRIMARY), skip the lower-priority pin - Result: clean set of non-overlapping SVG pin markers
Secondary pass
- Take remaining places not in the primary set
- For each, check pixel distance against all placed markers (both primary and secondary)
- If within 10px radius (
OVERLAP_PX_SECONDARY), skip - Cap at 200 dots (
MAX_SECONDARY_DOTS) - 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-sheetwith three snap points: 12% (peek), 45% (half), 85% (full)enableOverDrag={false}to prevent over-scrolling- List mode: Shows a scrollable
BottomSheetFlatListofPlaceCardcomponents. Tapping a card callsselectPlace()(inline drawer, no navigation). - Detail mode: When
selectedPlaceis 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-locationfor GPS permission and current position- "My location" floating button animates the map to the user's coordinates (0.06 degree deltas)