2026-06-18 18:30:44 +02:00
|
|
|
/**
|
|
|
|
|
* Mapa de features. Dibuja la geometría GeoJSON del proyecto (puntos, líneas,
|
|
|
|
|
* polígonos) sobre Google Maps y permite seleccionar una feature tocándola.
|
|
|
|
|
*
|
|
|
|
|
* Nota: las tiles de Google Maps requieren conexión; la geometría sí se dibuja
|
|
|
|
|
* sin red. Requiere una API key de Google Maps (ver app.config.js / README).
|
|
|
|
|
*/
|
|
|
|
|
import * as Location from 'expo-location';
|
|
|
|
|
import React, { useMemo, useRef } from 'react';
|
2026-07-07 09:45:51 +02:00
|
|
|
import { Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
|
|
|
import MapView, { Marker, Polygon, Polyline, PROVIDER_DEFAULT, PROVIDER_GOOGLE } from 'react-native-maps';
|
|
|
|
|
|
|
|
|
|
// Android usa Google Maps (requiere GOOGLE_MAPS_API_KEY).
|
|
|
|
|
// iOS usa Apple Maps por defecto (sin key, PROVIDER_DEFAULT).
|
|
|
|
|
const MAP_PROVIDER = Platform.OS === 'android' ? PROVIDER_GOOGLE : PROVIDER_DEFAULT;
|
2026-06-18 18:30:44 +02:00
|
|
|
import { Feature } from '../api/types';
|
|
|
|
|
import { COLORS } from './components';
|
|
|
|
|
import { geometryToShapes, LatLng, regionFor } from './geojson';
|
|
|
|
|
|
|
|
|
|
const STATUS_COLOR: Record<string, string> = {
|
|
|
|
|
pending: '#9aa0a6',
|
|
|
|
|
in_progress: '#1f6f43',
|
|
|
|
|
completed: '#2e7d32',
|
|
|
|
|
blocked: '#b00020',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function colorFor(status?: string): string {
|
|
|
|
|
return (status && STATUS_COLOR[status]) || COLORS.primary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function FeatureMap({
|
|
|
|
|
features,
|
|
|
|
|
selectedId,
|
|
|
|
|
onSelect,
|
|
|
|
|
}: {
|
|
|
|
|
features: Feature[];
|
|
|
|
|
selectedId: number | null;
|
|
|
|
|
onSelect: (id: number) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const mapRef = useRef<MapView>(null);
|
|
|
|
|
|
|
|
|
|
const { shaped, region } = useMemo(() => {
|
|
|
|
|
const all: LatLng[] = [];
|
|
|
|
|
const shaped = features.map((f) => {
|
|
|
|
|
const shapes = geometryToShapes(f.geometry);
|
|
|
|
|
all.push(...shapes.points, ...shapes.lines.flat(), ...shapes.polygons.flat());
|
|
|
|
|
return { feature: f, shapes };
|
|
|
|
|
});
|
|
|
|
|
return { shaped, region: regionFor(all) };
|
|
|
|
|
}, [features]);
|
|
|
|
|
|
|
|
|
|
const recenter = async () => {
|
|
|
|
|
const perm = await Location.requestForegroundPermissionsAsync();
|
|
|
|
|
if (!perm.granted) return;
|
|
|
|
|
const pos = await Location.getCurrentPositionAsync({});
|
|
|
|
|
mapRef.current?.animateToRegion({
|
|
|
|
|
latitude: pos.coords.latitude,
|
|
|
|
|
longitude: pos.coords.longitude,
|
|
|
|
|
latitudeDelta: 0.01,
|
|
|
|
|
longitudeDelta: 0.01,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!region) {
|
|
|
|
|
return (
|
|
|
|
|
<View style={styles.empty}>
|
|
|
|
|
<Text style={{ color: COLORS.muted }}>Las features no tienen geometría.</Text>
|
|
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-28 19:42:11 +02:00
|
|
|
<View style={[styles.container, { backgroundColor: '#f0f0f0', justifyContent: 'center', alignItems: 'center' }]}>
|
|
|
|
|
<Text style={{ color: COLORS.muted, textAlign: 'center' }}>
|
|
|
|
|
Mapa desactivado temporalmente para pruebas.{"\n"}
|
|
|
|
|
Requiere API Key de Google Maps válida.
|
|
|
|
|
</Text>
|
2026-06-18 18:30:44 +02:00
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
container: { flex: 1 },
|
|
|
|
|
empty: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
|
|
|
|
|
locBtn: {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
right: 16,
|
|
|
|
|
bottom: 16,
|
|
|
|
|
width: 48,
|
|
|
|
|
height: 48,
|
|
|
|
|
borderRadius: 24,
|
|
|
|
|
backgroundColor: '#fff',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
elevation: 4,
|
|
|
|
|
shadowColor: '#000',
|
|
|
|
|
shadowOpacity: 0.2,
|
|
|
|
|
shadowRadius: 4,
|
|
|
|
|
shadowOffset: { width: 0, height: 2 },
|
|
|
|
|
},
|
|
|
|
|
locIcon: { fontSize: 22, color: COLORS.primary },
|
|
|
|
|
});
|