feat: add water quality markers to map
- Add 38 Irish beach water quality markers to map display - Create water quality marker icons with color-coded status (excellent=green, good=blue, sufficient=amber, poor=red) - Add water quality toggle in map filters - Display beach name, status, and last sampled date in popups - Create irishBeaches.ts data file with all beach locations - Water quality markers shown by default, can be toggled off Beaches covered: Dublin, Wicklow, Wexford, Waterford, Cork, Kerry, Clare, Galway, Mayo, Sligo, Donegal, Antrim, Down
This commit is contained in:
@@ -19,16 +19,26 @@ L.Icon.Default.mergeOptions({
|
||||
shadowUrl: markerShadow,
|
||||
});
|
||||
|
||||
interface WaterQualityMarker {
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
status: 'excellent' | 'good' | 'sufficient' | 'poor' | 'unknown';
|
||||
lastSampled?: string;
|
||||
}
|
||||
|
||||
interface MapViewProps {
|
||||
reports: Report[];
|
||||
events?: CleanupEvent[];
|
||||
hotspots?: HotspotWithSeverity[];
|
||||
waterQuality?: WaterQualityMarker[];
|
||||
filters?: FilterState;
|
||||
onMarkerClick?: (reportId: string) => void;
|
||||
onMapClick?: (lat: number, lng: number) => void;
|
||||
showHeatmap?: boolean;
|
||||
showClusters?: boolean;
|
||||
showHotspots?: boolean;
|
||||
showWaterQuality?: boolean;
|
||||
center?: [number, number];
|
||||
zoom?: number;
|
||||
}
|
||||
@@ -37,12 +47,14 @@ export function MapView({
|
||||
reports,
|
||||
events = [],
|
||||
hotspots = [],
|
||||
waterQuality = [],
|
||||
filters,
|
||||
onMarkerClick,
|
||||
onMapClick,
|
||||
showClusters = true,
|
||||
showHeatmap = false,
|
||||
showHotspots = false,
|
||||
showWaterQuality = true,
|
||||
center = [53.4, -9.0], // Centered on Ireland, minimizing UK visibility
|
||||
zoom = 7,
|
||||
}: MapViewProps) {
|
||||
@@ -52,6 +64,7 @@ export function MapView({
|
||||
const clusterGroupRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||
const heatLayerRef = useRef<L.HeatLayer | null>(null);
|
||||
const hotspotsLayerRef = useRef<L.LayerGroup | null>(null);
|
||||
const waterQualityLayerRef = useRef<L.LayerGroup | null>(null);
|
||||
|
||||
// Initialize map
|
||||
useEffect(() => {
|
||||
@@ -79,6 +92,9 @@ export function MapView({
|
||||
// Create hotspots layer
|
||||
hotspotsLayerRef.current = L.layerGroup();
|
||||
|
||||
// Create water quality layer
|
||||
waterQualityLayerRef.current = L.layerGroup();
|
||||
|
||||
// Create cluster group with custom configuration
|
||||
clusterGroupRef.current = L.markerClusterGroup({
|
||||
maxClusterRadius: 50, // Cluster markers within 50 pixels
|
||||
@@ -112,6 +128,7 @@ export function MapView({
|
||||
clusterGroupRef.current = null;
|
||||
heatLayerRef.current = null;
|
||||
hotspotsLayerRef.current = null;
|
||||
waterQualityLayerRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -439,6 +456,72 @@ export function MapView({
|
||||
}
|
||||
}, [hotspots, showHotspots]);
|
||||
|
||||
// Update water quality markers when waterQuality data or showWaterQuality changes
|
||||
useEffect(() => {
|
||||
if (!mapInstanceRef.current || !waterQualityLayerRef.current) return;
|
||||
|
||||
const map = mapInstanceRef.current;
|
||||
const waterQualityLayer = waterQualityLayerRef.current;
|
||||
|
||||
// Remove existing water quality layer if it exists
|
||||
if (map.hasLayer(waterQualityLayer)) {
|
||||
map.removeLayer(waterQualityLayer);
|
||||
}
|
||||
|
||||
// Clear existing water quality markers
|
||||
waterQualityLayer.clearLayers();
|
||||
|
||||
// Only create water quality layer if showWaterQuality is true
|
||||
if (showWaterQuality && waterQuality.length > 0) {
|
||||
waterQuality.forEach((beach) => {
|
||||
// Create marker for beach
|
||||
const icon = createWaterQualityMarkerIcon(beach.status);
|
||||
const marker = L.marker([beach.latitude, beach.longitude], { icon });
|
||||
|
||||
// Create popup with water quality details
|
||||
const lastSampledDate = beach.lastSampled
|
||||
? new Date(beach.lastSampled).toLocaleDateString('en-IE', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
: 'Unknown';
|
||||
|
||||
marker.bindPopup(`
|
||||
<div class="p-3 min-w-[220px]">
|
||||
<h3 class="font-bold text-base mb-2 flex items-center gap-2">
|
||||
<span class="text-2xl">💧</span>
|
||||
<span>${beach.name}</span>
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Water Quality:</span>
|
||||
<span class="inline-block px-2 py-1 text-xs font-semibold rounded ${getWaterQualityBadgeClass(beach.status)}">
|
||||
${beach.status.charAt(0).toUpperCase() + beach.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
${beach.lastSampled ? `
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">Last Sampled:</span>
|
||||
<span class="font-medium">${lastSampledDate}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="mt-2 pt-2 border-t text-xs text-gray-500">
|
||||
<p>Source: EU Bathing Water Quality (2024)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Add to layer
|
||||
waterQualityLayer.addLayer(marker);
|
||||
});
|
||||
|
||||
// Add water quality layer to map
|
||||
waterQualityLayer.addTo(map);
|
||||
}
|
||||
}, [waterQuality, showWaterQuality]);
|
||||
|
||||
// Handle geolocation
|
||||
const handleGeolocation = () => {
|
||||
if (!navigator.geolocation) {
|
||||
@@ -730,3 +813,52 @@ function getSeverityLabel(severity: string): string {
|
||||
};
|
||||
return labels[severity] || 'Unknown';
|
||||
}
|
||||
|
||||
// Helper function to create water quality marker icons
|
||||
function createWaterQualityMarkerIcon(status: string): L.DivIcon {
|
||||
const colors = {
|
||||
excellent: '#10B981', // green-500
|
||||
good: '#3B82F6', // blue-500
|
||||
sufficient: '#F59E0B', // amber-500
|
||||
poor: '#EF4444', // red-500
|
||||
unknown: '#6B7280', // gray-500
|
||||
};
|
||||
|
||||
const color = colors[status as keyof typeof colors] || colors.unknown;
|
||||
|
||||
return L.divIcon({
|
||||
className: 'water-quality-marker',
|
||||
html: `
|
||||
<div style="
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: ${color};
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
">
|
||||
💧
|
||||
</div>
|
||||
`,
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14],
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get water quality badge class
|
||||
function getWaterQualityBadgeClass(status: string): string {
|
||||
const classes = {
|
||||
excellent: 'bg-green-100 text-green-800',
|
||||
good: 'bg-blue-100 text-blue-800',
|
||||
sufficient: 'bg-amber-100 text-amber-800',
|
||||
poor: 'bg-red-100 text-red-800',
|
||||
unknown: 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
return classes[status as keyof typeof classes] || classes.unknown;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user