UNPKG

@silicon.js/map

Version:

A React Native map management package built on top of `react-native-maps` for Expo with a powerful provider-based architecture for managing regions, markers, and selections.

678 lines (519 loc) β€’ 15.5 kB
# Map Package A React Native map management package built on top of `react-native-maps` for Expo with a powerful provider-based architecture for managing regions, markers, and selections. ## Features - πŸ—ΊοΈ Easy map region management - πŸ“ Full marker CRUD operations (Create, Read, Update, Delete) - 🎯 Marker selection with visual feedback - πŸ“± User location tracking - 🎨 Custom marker icons support - πŸ”„ Automatic state synchronization - πŸ’Ύ Centralized map state management - 🎭 TypeScript support - ⚑ Optimized with memoized callbacks - πŸ“¦ Expo-compatible ## Installation ```bash npx expo install react-native-maps ``` ## Setup ### 1. Configure app.json/app.config.js Add the maps plugin to your Expo config: ```json { "expo": { "plugins": [ [ "expo-location", { "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location." } ] ], "ios": { "bundleIdentifier": "com.yourcompany.yourapp", "config": { "googleMapsApiKey": "YOUR_IOS_API_KEY" } }, "android": { "package": "com.yourcompany.yourapp", "config": { "googleMaps": { "apiKey": "YOUR_ANDROID_API_KEY" } } } } } ``` ### 2. Install location permissions (if using user location) ```bash npx expo install expo-location ``` ### 3. Wrap your app with the provider ```tsx import { MapProvider } from './map'; function App() { return <MapProvider>{/* Your app components */}</MapProvider>; } ``` ### 4. Use the Map component or hook ```tsx import { Map, useMapContext } from './map'; function MapScreen() { const { addMarker } = useMapContext(); const handleAddMarker = () => { addMarker({ id: '1', latitude: 37.78825, longitude: -122.4324, title: 'San Francisco', description: 'City by the Bay', }); }; return ( <View style={{ flex: 1 }}> <Map initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421, }} showsUserLocation style={{ flex: 1 }} /> <Button title="Add Marker" onPress={handleAddMarker} /> </View> ); } ``` ## API Reference ### MapProvider Wrap your app to provide map context to all child components. ```tsx <MapProvider>{children}</MapProvider> ``` ### Map Component Props | Prop | Type | Required | Description | | --------------------- | ---------------------------- | -------- | ----------------------------------------------- | | `initialRegion` | `MapRegion` | No | Initial map region (lat, lng, deltas) | | `showsUserLocation` | `boolean` | No | Show user's current location (default: `false`) | | `followsUserLocation` | `boolean` | No | Center map on user location (default: `false`) | | `style` | `ViewStyle` | No | Custom styles for the map container | | `onMarkerPress` | `(markerId: string) => void` | No | Callback when marker is pressed | ### useMapContext Hook Access map state and methods throughout your app. #### State ```typescript interface MapState { region: MapRegion | null; markers: MapMarker[]; selectedMarkerId: string | null; } ``` #### Methods **Region Management** - `setRegion(region: MapRegion): void` - Update map viewport **Marker Management** - `setMarkers(markers: MapMarker[]): void` - Replace all markers - `addMarker(marker: MapMarker): void` - Add a single marker - `removeMarker(markerId: string): void` - Remove marker by ID - `updateMarker(markerId: string, updates: Partial<MapMarker>): void` - Update marker properties - `clearMarkers(): void` - Remove all markers **Selection Management** - `selectMarker(markerId: string | null): void` - Select/deselect marker ## Types ### MapRegion ```typescript interface MapRegion { latitude: number; longitude: number; latitudeDelta?: number; longitudeDelta?: number; } ``` ### MapMarker ```typescript interface MapMarker { id: string; latitude: number; longitude: number; title?: string; description?: string; draggable?: boolean; icon?: ReactNode; } ``` ## Usage Examples ### Basic Map Display ```tsx import { Map } from './map'; function SimpleMap() { return ( <Map initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421, }} showsUserLocation style={{ flex: 1 }} /> ); } ``` ### Adding Markers Dynamically ```tsx import { useMapContext } from './map'; function LocationPicker() { const { addMarker, mapState } = useMapContext(); const handleMapPress = (coordinate) => { const newMarker = { id: Date.now().toString(), latitude: coordinate.latitude, longitude: coordinate.longitude, title: 'New Location', }; addMarker(newMarker); }; return ( <View> <Text>Total Markers: {mapState.markers.length}</Text> {/* Map component */} </View> ); } ``` ### Managing Multiple Markers ```tsx import { useMapContext } from './map'; function LocationManager() { const { setMarkers, clearMarkers, mapState } = useMapContext(); const loadLocations = async () => { const locations = await fetchLocations(); const markers = locations.map((loc) => ({ id: loc.id, latitude: loc.lat, longitude: loc.lng, title: loc.name, description: loc.address, })); setMarkers(markers); }; return ( <View> <Button title="Load Locations" onPress={loadLocations} /> <Button title="Clear All" onPress={clearMarkers} /> <Text>{mapState.markers.length} locations</Text> </View> ); } ``` ### Marker Selection with Details ```tsx import { useMapContext, Map } from './map'; function InteractiveMap() { const { mapState, selectMarker } = useMapContext(); const selectedMarker = mapState.markers.find((m) => m.id === mapState.selectedMarkerId); return ( <View style={{ flex: 1 }}> <Map showsUserLocation style={{ flex: 1 }} onMarkerPress={(markerId) => { selectMarker(markerId); }} /> {selectedMarker && ( <View style={styles.detailsPanel}> <Text style={styles.title}>{selectedMarker.title}</Text> <Text>{selectedMarker.description}</Text> <Button title="Close" onPress={() => selectMarker(null)} /> </View> )} </View> ); } ``` ### Updating Marker Properties ```tsx import { useMapContext } from './map'; function EditableMarkers() { const { updateMarker, mapState } = useMapContext(); const handleEditMarker = (markerId: string) => { updateMarker(markerId, { title: 'Updated Title', description: 'New description', draggable: true, }); }; return ( <View> {mapState.markers.map((marker) => ( <TouchableOpacity key={marker.id} onPress={() => handleEditMarker(marker.id)}> <Text>{marker.title}</Text> </TouchableOpacity> ))} </View> ); } ``` ### Custom Marker Icons ```tsx import { useMapContext } from './map'; import { Image } from 'react-native'; function CustomMarkerMap() { const { addMarker } = useMapContext(); const addCustomMarker = () => { addMarker({ id: 'custom-1', latitude: 37.78825, longitude: -122.4324, title: 'Custom Marker', icon: <Image source={require('./pin.png')} style={{ width: 30, height: 30 }} />, }); }; return <Button title="Add Custom Marker" onPress={addCustomMarker} />; } ``` ### Centering Map on Region ```tsx import { useMapContext } from './map'; function RegionControls() { const { setRegion } = useMapContext(); const centerOnSF = () => { setRegion({ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421, }); }; const centerOnNY = () => { setRegion({ latitude: 40.7128, longitude: -74.006, latitudeDelta: 0.0922, longitudeDelta: 0.0421, }); }; return ( <View> <Button title="San Francisco" onPress={centerOnSF} /> <Button title="New York" onPress={centerOnNY} /> </View> ); } ``` ### Using with Expo Location ```tsx import { useMapContext } from './map'; import * as Location from 'expo-location'; function UserLocationMap() { const { setRegion, addMarker } = useMapContext(); useEffect(() => { (async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { alert('Permission to access location was denied'); return; } const location = await Location.getCurrentPositionAsync({}); const { latitude, longitude } = location.coords; // Center map on user setRegion({ latitude, longitude, latitudeDelta: 0.01, longitudeDelta: 0.01, }); // Add user marker addMarker({ id: 'user', latitude, longitude, title: 'You are here', }); })(); }, []); return <Map showsUserLocation style={{ flex: 1 }} />; } ``` ### Filtering Markers ```tsx import { useMapContext } from './map'; function FilteredMap() { const { mapState, setMarkers } = useMapContext(); const [category, setCategory] = useState('all'); const filterByCategory = (cat: string) => { setCategory(cat); const filtered = allMarkers.filter((m) => cat === 'all' || m.category === cat); setMarkers(filtered); }; return ( <View> <Button title="All" onPress={() => filterByCategory('all')} /> <Button title="Restaurants" onPress={() => filterByCategory('restaurant')} /> <Button title="Hotels" onPress={() => filterByCategory('hotel')} /> </View> ); } ``` ## Advanced Usage ### Route Planning with Multiple Markers ```tsx import { useMapContext } from './map'; function RouteMap() { const { setMarkers, mapState } = useMapContext(); const planRoute = (waypoints: Coordinate[]) => { const markers = waypoints.map((point, index) => ({ id: `waypoint-${index}`, latitude: point.latitude, longitude: point.longitude, title: `Stop ${index + 1}`, draggable: true, })); setMarkers(markers); }; return <Map style={{ flex: 1 }} />; } ``` ### Real-time Location Tracking with Expo ```tsx import { useMapContext } from './map'; import * as Location from 'expo-location'; function LocationTracker() { const { updateMarker, setRegion } = useMapContext(); useEffect(() => { let subscription; (async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') return; subscription = await Location.watchPositionAsync( { accuracy: Location.Accuracy.High, timeInterval: 1000, distanceInterval: 10, }, (location) => { const { latitude, longitude } = location.coords; updateMarker('user-location', { latitude, longitude }); setRegion({ latitude, longitude, latitudeDelta: 0.01, longitudeDelta: 0.01, }); }, ); })(); return () => subscription?.remove(); }, []); return <Map followsUserLocation style={{ flex: 1 }} />; } ``` ## Common Use Cases ### 1. **Store Locator** Display multiple store locations with search and filtering ### 2. **Delivery Tracking** Show real-time delivery driver location and route ### 3. **Property Listings** Display real estate properties on a map with details ### 4. **Event Finder** Show nearby events with custom markers ### 5. **Route Planning** Create multi-stop routes with draggable waypoints ### 6. **Geo-tagging** Allow users to select locations by tapping the map ## Visual Features ### Marker Selection Feedback Selected markers are highlighted (opacity: 1.0) while unselected markers are dimmed (opacity: 0.5), providing clear visual feedback. ### Draggable Markers ```tsx addMarker({ id: 'draggable-1', latitude: 37.78825, longitude: -122.4324, draggable: true, // Enable dragging }); ``` ## Performance Optimization - **Memoized callbacks** - All context methods use `useCallback` to prevent unnecessary re-renders - **Efficient state updates** - State updates are batched and optimized - **Selective rendering** - Only affected markers re-render on updates ## Expo-Specific Features ### Using with Expo Location ```bash npx expo install expo-location ``` ```tsx import * as Location from 'expo-location'; const { status } = await Location.requestForegroundPermissionsAsync(); const location = await Location.getCurrentPositionAsync({}); ``` ### Development Builds For optimal performance and full feature support, use EAS Build or development builds: ```bash npx expo run:ios npx expo run:android ``` ### Expo Go Limitations When using Expo Go: - Limited map styling options - Some native features may not work - Consider using development builds for production apps ## Best Practices 1. **Always provide unique IDs** - Use UUIDs or timestamps for marker IDs 2. **Handle permissions** - Request location permissions before enabling user location 3. **Optimize marker count** - Consider clustering for 100+ markers 4. **Clean up on unmount** - Clear markers when navigating away 5. **Test on real devices** - Maps behave differently on simulators 6. **Handle loading states** - Show indicators while fetching location data 7. **Provide fallbacks** - Handle cases where location services are disabled 8. **Use development builds** - Expo Go has limitations; use dev builds for full features ## Platform Differences ### iOS - Requires location permissions in `app.json` - Native Apple Maps integration - Better performance with many markers ### Android - Requires Google Maps API key in `app.json` - Google Maps integration - May require Google Play Services ## Troubleshooting ### Map not showing - Check API key configuration in `app.json` - Verify permissions are granted - Ensure region has valid coordinates - Try using a development build instead of Expo Go ### Markers not appearing - Verify marker coordinates are within visible region - Check that markers array is not empty - Ensure marker IDs are unique ### Performance issues - Reduce number of visible markers - Implement marker clustering - Optimize custom marker icons - Use development builds for better performance ### Location permissions not working ```tsx import * as Location from 'expo-location'; const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission denied', 'Location permission is required'); } ``` ## Getting API Keys ### Google Maps API Key 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select existing 3. Enable Maps SDK for Android and iOS 4. Create credentials (API Key) 5. Add to `app.json` ## TypeScript Support Fully typed with TypeScript for excellent IDE support and type safety: ```typescript const { mapState, addMarker }: MapContextType = useMapContext(); ```