UNPKG

@naarni/design-system

Version:

Naarni React Native Design System for EV Fleet Apps

477 lines (476 loc) 18.5 kB
import React, { useRef, useEffect, useState, useCallback, useImperativeHandle } from 'react'; import { View, Text, StyleSheet, Platform, ActivityIndicator, } from 'react-native'; import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps'; const MapComponent = React.forwardRef(({ vehicles = [], initialRegion, style, mapStyle = 'default', showVehicleInfo = true, autoFitToVehicles = false, onVehiclePress, onMapPress, onRegionChange, zoomEnabled = true, scrollEnabled = true, rotateEnabled = true, pitchEnabled = true, showsUserLocation = false, showsMyLocationButton = false, showsCompass = true, showsScale = true, loadingEnabled = true, loadingIndicatorColor = '#007AFF', loadingBackgroundColor = '#ffffff', customMapStyle, testID, }, ref) => { const mapRef = useRef(null); const [isLoading, setIsLoading] = useState(true); const [currentRegion, setCurrentRegion] = useState(initialRegion); const [mapError, setMapError] = useState(null); // Debug logging (reduced for stability) console.log('🚗 MapComponent: Platform:', Platform.OS, 'Vehicles:', vehicles.length); // Default region if not provided const defaultRegion = { latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421, }; const finalRegion = initialRegion || defaultRegion; // Map style configurations const getMapStyle = useCallback(() => { if (customMapStyle) { return customMapStyle; } const styles = { default: [], dark: [ { elementType: 'geometry', stylers: [{ color: '#212121' }], }, { elementType: 'labels.text.fill', stylers: [{ color: '#757575' }], }, { elementType: 'labels.text.stroke', stylers: [{ color: '#212121' }], }, { featureType: 'administrative', elementType: 'geometry', stylers: [{ color: '#757575' }], }, { featureType: 'administrative.country', elementType: 'labels.text.fill', stylers: [{ color: '#9e9e9e' }], }, { featureType: 'administrative.land_parcel', stylers: [{ visibility: 'off' }], }, { featureType: 'administrative.locality', elementType: 'labels.text.fill', stylers: [{ color: '#bdbdbd' }], }, { featureType: 'poi', elementType: 'labels.text.fill', stylers: [{ color: '#757575' }], }, { featureType: 'poi.park', elementType: 'geometry', stylers: [{ color: '#181818' }], }, { featureType: 'poi.park', elementType: 'labels.text.fill', stylers: [{ color: '#616161' }], }, { featureType: 'poi.park', elementType: 'labels.text.stroke', stylers: [{ color: '#1b1b1b' }], }, { featureType: 'road', elementType: 'geometry.fill', stylers: [{ color: '#2c2c2c' }], }, { featureType: 'road', elementType: 'labels.text.fill', stylers: [{ color: '#8a8a8a' }], }, { featureType: 'road.arterial', elementType: 'geometry', stylers: [{ color: '#373737' }], }, { featureType: 'road.highway', elementType: 'geometry', stylers: [{ color: '#3c3c3c' }], }, { featureType: 'road.highway.controlled_access', elementType: 'geometry', stylers: [{ color: '#4e4e4e' }], }, { featureType: 'road.local', elementType: 'labels.text.fill', stylers: [{ color: '#616161' }], }, { featureType: 'transit', elementType: 'labels.text.fill', stylers: [{ color: '#757575' }], }, { featureType: 'water', elementType: 'geometry', stylers: [{ color: '#000000' }], }, { featureType: 'water', elementType: 'labels.text.fill', stylers: [{ color: '#3d3d3d' }], }, ], light: [ { elementType: 'geometry', stylers: [{ color: '#f5f5f5' }], }, { elementType: 'labels.text.fill', stylers: [{ color: '#616161' }], }, { elementType: 'labels.text.stroke', stylers: [{ color: '#f5f5f5' }], }, { featureType: 'administrative.land_parcel', elementType: 'labels.text.fill', stylers: [{ color: '#bdbdbd' }], }, { featureType: 'poi', elementType: 'geometry', stylers: [{ color: '#eeeeee' }], }, { featureType: 'poi', elementType: 'labels.text.fill', stylers: [{ color: '#757575' }], }, { featureType: 'poi.park', elementType: 'geometry', stylers: [{ color: '#e5e5e5' }], }, { featureType: 'poi.park', elementType: 'labels.text.fill', stylers: [{ color: '#9e9e9e' }], }, { featureType: 'road', elementType: 'geometry', stylers: [{ color: '#ffffff' }], }, { featureType: 'road.arterial', elementType: 'labels.text.fill', stylers: [{ color: '#757575' }], }, { featureType: 'road.highway', elementType: 'geometry', stylers: [{ color: '#dadada' }], }, { featureType: 'road.highway', elementType: 'labels.text.fill', stylers: [{ color: '#616161' }], }, { featureType: 'road.local', elementType: 'labels.text.fill', stylers: [{ color: '#9e9e9e' }], }, { featureType: 'transit.line', elementType: 'geometry', stylers: [{ color: '#e5e5e5' }], }, { featureType: 'transit.station', elementType: 'geometry', stylers: [{ color: '#eeeeee' }], }, { featureType: 'water', elementType: 'geometry', stylers: [{ color: '#c9c9c9' }], }, { featureType: 'water', elementType: 'labels.text.fill', stylers: [{ color: '#9e9e9e' }], }, ], blackAndWhite: [ { elementType: 'geometry', stylers: [{ color: '#ffffff' }], }, { elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { elementType: 'labels.text.stroke', stylers: [{ color: '#ffffff' }], }, { featureType: 'administrative', elementType: 'geometry', stylers: [{ color: '#f5f5f5' }], }, { featureType: 'administrative.country', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'administrative.land_parcel', stylers: [{ visibility: 'off' }], }, { featureType: 'administrative.locality', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'poi', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'poi.park', elementType: 'geometry', stylers: [{ color: '#f5f5f5' }], }, { featureType: 'poi.park', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'poi.park', elementType: 'labels.text.stroke', stylers: [{ color: '#ffffff' }], }, { featureType: 'road', elementType: 'geometry.fill', stylers: [{ color: '#ffffff' }], }, { featureType: 'road', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'road.arterial', elementType: 'geometry', stylers: [{ color: '#ffffff' }], }, { featureType: 'road.highway', elementType: 'geometry', stylers: [{ color: '#ffffff' }], }, { featureType: 'road.highway.controlled_access', elementType: 'geometry', stylers: [{ color: '#ffffff' }], }, { featureType: 'road.local', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'transit', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, { featureType: 'water', elementType: 'geometry', stylers: [{ color: '#ffffff' }], }, { featureType: 'water', elementType: 'labels.text.fill', stylers: [{ color: '#000000' }], }, ], }; return styles[mapStyle] || styles.default; }, [mapStyle, customMapStyle]); // Handle map loading const handleMapReady = useCallback(() => { console.log('✅ Map is ready!'); setIsLoading(false); setMapError(null); }, []); // Add timeout to detect if map is stuck loading useEffect(() => { const timeout = setTimeout(() => { if (isLoading) { console.warn('⚠️ Map loading timeout'); setMapError('Map loading timeout - check API key configuration'); setIsLoading(false); } }, 10000); // 10 second timeout return () => clearTimeout(timeout); }, [isLoading]); // Handle map error const handleMapError = useCallback((error) => { console.error('❌ Map Error:', error); setMapError(error?.message || 'Unknown map error'); setIsLoading(false); // Remove Alert to prevent crashes }, []); // Handle region change const handleRegionChange = useCallback((region) => { setCurrentRegion(region); onRegionChange?.(region); }, [onRegionChange]); // Fit map to show all vehicles const fitToVehicles = useCallback(() => { if (!mapRef.current || vehicles.length === 0) return; const coordinates = vehicles.map(vehicle => ({ latitude: vehicle.latitude, longitude: vehicle.longitude, })); mapRef.current.fitToCoordinates(coordinates, { edgePadding: { top: 50, right: 50, bottom: 50, left: 50 }, animated: true, }); }, [vehicles]); // Fit map to show specific vehicle const fitToVehicle = useCallback((vehicle) => { if (!mapRef.current) return; mapRef.current.animateToRegion({ latitude: vehicle.latitude, longitude: vehicle.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01, }, 1000); }, []); // Expose methods via ref useImperativeHandle(ref, () => ({ fitToVehicles, fitToVehicle, animateToRegion: (region, duration) => { mapRef.current?.animateToRegion(region, duration); }, fitToCoordinates: (coordinates, options) => { mapRef.current?.fitToCoordinates(coordinates, options); }, }), [fitToVehicles, fitToVehicle]); // Auto-fit to vehicles when vehicles change useEffect(() => { if (vehicles.length > 0 && autoFitToVehicles) { setTimeout(fitToVehicles, 500); } }, [vehicles, fitToVehicles]); return (<View style={styles.container}> {isLoading && (<View style={[styles.loadingContainer, { backgroundColor: loadingBackgroundColor }]}> <ActivityIndicator size="large" color={loadingIndicatorColor}/> <Text style={styles.loadingText}>Loading map...</Text> </View>)} {mapError && (<View style={styles.errorContainer}> <Text style={styles.errorText}>Map Error: {mapError}</Text> </View>)} {(() => { try { return (<MapView ref={mapRef} provider={PROVIDER_GOOGLE} style={[styles.map, style]} initialRegion={finalRegion} onMapReady={() => { try { handleMapReady(); } catch (error) { console.error('Map ready error:', error); setIsLoading(false); } }} onPress={(event) => { try { onMapPress?.(event); } catch (error) { console.error('Map press error:', error); } }} onRegionChangeComplete={(region) => { try { handleRegionChange(region); } catch (error) { console.error('Region change error:', error); } }} zoomEnabled={zoomEnabled} scrollEnabled={scrollEnabled} rotateEnabled={rotateEnabled} pitchEnabled={pitchEnabled} showsUserLocation={showsUserLocation} showsMyLocationButton={showsMyLocationButton} showsCompass={showsCompass} showsScale={showsScale} // loadingEnabled={loadingEnabled} loadingIndicatorColor={loadingIndicatorColor} loadingBackgroundColor={loadingBackgroundColor} customMapStyle={getMapStyle()} testID={testID}> {vehicles.map((vehicle, index) => (<Marker key={vehicle.id || index} coordinate={{ latitude: vehicle.latitude, longitude: vehicle.longitude, }} title={showVehicleInfo ? vehicle.name || `Vehicle ${index + 1}` : undefined} description={showVehicleInfo ? vehicle.description : undefined} onPress={() => { try { onVehiclePress?.(vehicle); } catch (error) { console.error('Vehicle press error:', error); } }} pinColor={vehicle.color || '#007AFF'} rotation={vehicle.heading || 0}/>))} </MapView>); } catch (error) { console.error('MapView render error:', error); return (<View style={styles.errorContainer}> <Text style={styles.errorText}>Failed to render map</Text> </View>); } })()} </View>); }); const styles = StyleSheet.create({ container: { flex: 1, }, map: { flex: 1, }, loadingContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 1, }, loadingText: { marginTop: 10, fontSize: 16, color: '#666', }, errorContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(255, 0, 0, 0.1)', zIndex: 2, }, errorText: { fontSize: 16, color: 'red', textAlign: 'center', padding: 20, }, }); export default MapComponent;