Files
avante-movil/src/ui/FeatureMap.tsx
T

138 lines
4.0 KiB
TypeScript
Raw Normal View History

/**
* Mapa de features usando OpenStreetMap via Leaflet y WebView.
* Elimina la dependencia de la API Key de Google Maps.
*/
import React, { useMemo, useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import { WebView } from 'react-native-webview';
import { Feature } from '../api/types';
import { COLORS } from './components';
export function FeatureMap({
features,
selectedId,
onSelect,
}: {
features: Feature[];
selectedId: number | null;
onSelect: (id: number) => void;
}) {
const webViewRef = useRef<WebView>(null);
// 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 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);
}
};
return (
<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, backgroundColor: '#f0f0f0' },
map: { flex: 1 },
});