Implement custom camera with real-time overlay and resolution settings

This commit is contained in:
2026-07-28 23:55:24 +02:00
parent c6240784ff
commit 6d11434061
9 changed files with 479 additions and 138 deletions
+111 -76
View File
@@ -1,32 +1,12 @@
/**
* 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).
* Mapa de features usando OpenStreetMap via Leaflet y WebView.
* Elimina la dependencia de la API Key de Google Maps.
*/
import * as Location from 'expo-location';
import React, { useMemo, useRef } from 'react';
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;
import { StyleSheet, View } from 'react-native';
import { WebView } from 'react-native-webview';
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,
@@ -37,66 +17,121 @@ export function FeatureMap({
selectedId: number | null;
onSelect: (id: number) => void;
}) {
const mapRef = useRef<MapView>(null);
const webViewRef = useRef<WebView>(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]);
// Convertimos las features a un objeto GeoJSON simple para Leaflet
const geoData = useMemo(() => {
return {
type: 'FeatureCollection',
features: features.map((f) => ({
type: 'Feature',
id: f.id,
geometry: typeof f.geometry === 'string' ? JSON.parse(f.geometry) : f.geometry,
properties: {
name: f.name,
status: f.status,
selected: f.id === selectedId,
},
})),
};
}, [features, selectedId]);
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,
});
const htmlContent = useMemo(() => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { height: 100vh; width: 100vw; background: #f0f0f0; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const map = L.map('map', { zoomControl: false });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap'
}).addTo(map);
const geoData = ${JSON.stringify(geoData)};
const STATUS_COLORS = {
pending: '#9aa0a6',
in_progress: '#1f6f43',
completed: '#2e7d32',
blocked: '#b00020'
};
const geoLayer = L.geoJSON(geoData, {
style: (feature) => ({
color: feature.properties.selected ? '#000' : (STATUS_COLORS[feature.properties.status] || '#1f6f43'),
weight: feature.properties.selected ? 4 : 2,
fillOpacity: 0.4,
fillColor: STATUS_COLORS[feature.properties.status] || '#1f6f43'
}),
pointToLayer: (feature, latlng) => {
return L.circleMarker(latlng, {
radius: 8,
fillColor: STATUS_COLORS[feature.properties.status] || '#1f6f43',
color: feature.properties.selected ? '#000' : '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
},
onEachFeature: (feature, layer) => {
layer.on('click', () => {
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'select', id: feature.id }));
});
}
}).addTo(map);
if (geoData.features.length > 0) {
try {
map.fitBounds(geoLayer.getBounds(), { padding: [20, 20] });
} catch(e) {
map.setView([40.4167, -3.7037], 13);
}
} else {
map.setView([40.4167, -3.7037], 13); // Madrid default
}
</script>
</body>
</html>
`, [geoData]);
const onMessage = (event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === 'select') {
onSelect(data.id);
}
} catch (e) {
console.warn('Error parsing WebView message:', e);
}
};
if (!region) {
return (
<View style={styles.empty}>
<Text style={{ color: COLORS.muted }}>Las features no tienen geometría.</Text>
</View>
);
}
return (
<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>
<View style={styles.container}>
<WebView
ref={webViewRef}
originWhitelist={['*']}
source={{ html: htmlContent }}
onMessage={onMessage}
style={styles.map}
javaScriptEnabled={true}
domStorageEnabled={true}
/>
</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 },
container: { flex: 1, backgroundColor: '#f0f0f0' },
map: { flex: 1 },
});