138 lines
4.0 KiB
TypeScript
138 lines
4.0 KiB
TypeScript
/**
|
|
* 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: '© 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 },
|
|
});
|