import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Constants from 'expo-constants'; import { MapView, Camera, ShapeSource, CircleLayer, FillLayer, LineLayer, UserLocation, } from '@maplibre/maplibre-react-native'; import { useFocusEffect, useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '@/theme/ThemeContext'; import { ps } from '@/api/client'; import { useLocation, type Coords } from '@/features/location/useLocation'; import { useAuth } from '@/auth/AuthContext'; import { getMirrorZones, syncZones } from '@/api/zoneMirror'; import { getAdjustedAreas, refreshAreas, type ParkingArea } from '@/api/parkingAreas'; import { distanceToGeometry, type LonLat } from '@/features/citymap/geo'; import { getParkedPin, setParkedPin, type ParkedSpot, } from '@/features/session/activeParkingStore'; import { pinParkedSpot } from '@/features/session/activeParking'; import type { RootStackParamList } from '@/navigation/RootNavigator'; import type { Zone } from 'parksmarter-client'; const MAP_STYLE_LIGHT = (Constants.expoConfig?.extra?.mapStyleUrl as string) ?? 'https://tiles.openfreemap.org/styles/liberty'; const MAP_STYLE_DARK = (Constants.expoConfig?.extra?.mapStyleUrlDark as string) ?? 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'; // Default view when we have no last-session lot: all of downtown Sandpoint, ID. const DEFAULT_CENTER: Coords = { latitude: 48.2766, longitude: -116.5533 }; const DEFAULT_ZOOM = 14; type Nav = NativeStackNavigationProp; /** * How far from a city-map area a parked pin can be and still be taken as "that's * where I am". A block is ~170 m, a street ~12 m wide; 40 m picks the right side * of the right street without silently matching a spot two blocks away. */ const AREA_SNAP_METERS = 40; /** Nearest city-map area to a point, or null if nothing is close enough. */ function areaAt(point: LonLat, areas: ParkingArea[]): ParkingArea | null { let best: ParkingArea | null = null; let bestDist = AREA_SNAP_METERS; for (const a of areas) { const d = distanceToGeometry(point, a.geometry); if (d < bestDist) { best = a; bestDist = d; } } return best; } /** Coerce the server's zone color (hex string, color name, or numeric) into a usable color. */ function normalizeColor(v: unknown): string | null { if (typeof v === 'number') return '#' + (v & 0xffffff).toString(16).padStart(6, '0'); if (typeof v === 'string') { const s = v.trim(); if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s)) return s; if (/^\d+$/.test(s)) return '#' + (Number(s) & 0xffffff).toString(16).padStart(6, '0'); if (/^[a-z]+$/i.test(s)) return s; // named color } return null; } export function MapScreen() { const navigation = useNavigation