import { PageContainer, Container, Button, Spinner, SegmentInput, TextInput, SelectInput, MultiSelectInput } from '@ifrc-go/ui'; import { useParams, useNavigate } from 'react-router-dom'; import { useState, useEffect, useMemo, useCallback } from 'react'; import { ChevronLeftLineIcon, ChevronRightLineIcon, DeleteBinLineIcon } from '@ifrc-go/icons'; import styles from './MapDetailPage.module.css'; import { useFilterContext } from '../../hooks/useFilterContext'; import { useAdmin } from '../../hooks/useAdmin'; import ExportModal from '../../components/ExportModal'; import { FullSizeImageModal } from '../../components/upload/ModalComponents'; import FilterBar from '../../components/FilterBar'; interface MapOut { image_id: string; file_key: string; sha256: string; source: string; event_type: string; epsg: string; image_type: string; image_url: string; detail_url?: string; // URL to medium quality version (800x600px) countries: Array<{ c_code: string; label: string; r_code: string; }>; title?: string; prompt?: string; model?: string; schema_id?: string; raw_json?: { extracted_metadata?: { description?: string; analysis?: string; recommended_actions?: string; metadata?: Record; }; fallback_info?: Record; [key: string]: unknown; }; generated?: string; edited?: string; accuracy?: number; context?: number; usability?: number; starred?: boolean; created_at?: string; updated_at?: string; // Multi-upload fields all_image_ids?: string[]; image_count?: number; } export default function MapDetailPage() { const { mapId } = useParams<{ mapId: string }>(); const navigate = useNavigate(); const { isAuthenticated } = useAdmin(); // Debug: Log the current URL and mapId for production debugging console.log('MapDetailsPage: Current URL:', window.location.href); console.log('MapDetailsPage: Hash:', window.location.hash); console.log('MapDetailsPage: mapId from useParams:', mapId); console.log('MapDetailsPage: mapId type:', typeof mapId); console.log('MapDetailsPage: mapId length:', mapId?.length); console.log('MapDetailsPage: mapId value:', JSON.stringify(mapId)); // Early validation - if mapId is invalid, show error immediately const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!mapId || mapId === 'undefined' || mapId === 'null' || mapId.trim() === '' || !uuidRegex.test(mapId)) { return (
⚠️
Invalid Map ID
The map ID provided is not valid.
Debug Info: mapId = "{mapId}" (type: {typeof mapId})
); } const [view, setView] = useState<'explore' | 'mapDetails'>('mapDetails'); const [map, setMap] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [sources, setSources] = useState<{s_code: string, label: string}[]>([]); const [types, setTypes] = useState<{t_code: string, label: string}[]>([]); const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]); const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]); const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]); const [hasPrevious, setHasPrevious] = useState(false); const [hasNext, setHasNext] = useState(false); const [isNavigating, setIsNavigating] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [isExporting, setIsExporting] = useState(false); const [exportSuccess, setExportSuccess] = useState(false); const [exportMode, setExportMode] = useState<'standard' | 'fine-tuning'>('standard'); const [trainSplit, setTrainSplit] = useState(80); const [testSplit, setTestSplit] = useState(10); const [valSplit, setValSplit] = useState(10); const [crisisMapsSelected, setCrisisMapsSelected] = useState(true); const [droneImagesSelected, setDroneImagesSelected] = useState(true); const [isDeleting, setIsDeleting] = useState(false); // Full-size image modal state const [showFullSizeModal, setShowFullSizeModal] = useState(false); const [selectedImageForModal, setSelectedImageForModal] = useState(null); const [isLoadingFullSizeImage, setIsLoadingFullSizeImage] = useState(false); // Carousel state for multi-upload const [allImages, setAllImages] = useState([]); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [isLoadingImages, setIsLoadingImages] = useState(false); const { search, setSearch, srcFilter, setSrcFilter, catFilter, setCatFilter, regionFilter, setRegionFilter, countryFilter, setCountryFilter, imageTypeFilter, setImageTypeFilter, uploadTypeFilter, setUploadTypeFilter, showReferenceExamples, setShowReferenceExamples, clearAllFilters } = useFilterContext(); const viewOptions = [ { key: 'explore' as const, label: 'List' }, { key: 'mapDetails' as const, label: 'Carousel' } ]; const fetchMapData = useCallback(async (id: string) => { console.log('fetchMapData called with id:', id); console.log('fetchMapData id type:', typeof id); // Validate the ID before making the request if (!id || id === 'undefined' || id === 'null' || id.trim() === '') { console.log('fetchMapData: Invalid ID detected:', id); setError('Invalid Map ID'); setLoading(false); return; } // Additional UUID format validation const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(id)) { console.log('fetchMapData: Invalid UUID format:', id); setError('Invalid Map ID format'); setLoading(false); return; } console.log('fetchMapData: Making API call for id:', id); setIsNavigating(true); setLoading(true); try { const response = await fetch(`/api/images/${id}`); if (!response.ok) { throw new Error('Map not found'); } const data = await response.json(); setMap(data); // If this is a multi-upload item, fetch all images if (data.all_image_ids && data.all_image_ids.length > 1) { await fetchAllImages(data.all_image_ids); } else if (data.image_count && data.image_count > 1) { // Multi-upload but no all_image_ids, try to fetch from grouped endpoint console.log('Multi-upload detected but no all_image_ids, trying grouped endpoint'); try { const groupedResponse = await fetch('/api/images/grouped'); if (groupedResponse.ok) { const groupedData = await groupedResponse.json(); const matchingItem = groupedData.find((item: any) => item.all_image_ids && item.all_image_ids.includes(data.image_id) ); if (matchingItem && matchingItem.all_image_ids) { await fetchAllImages(matchingItem.all_image_ids); } else { setAllImages([data]); setCurrentImageIndex(0); } } else { setAllImages([data]); setCurrentImageIndex(0); } } catch (err) { console.error('Failed to fetch from grouped endpoint:', err); setAllImages([data]); setCurrentImageIndex(0); } } else { setAllImages([data]); setCurrentImageIndex(0); } await checkNavigationAvailability(id); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Unknown error occurred'); } finally { setLoading(false); setIsNavigating(false); } }, []); const fetchAllImages = useCallback(async (imageIds: string[]) => { console.log('fetchAllImages called with imageIds:', imageIds); setIsLoadingImages(true); try { const imagePromises = imageIds.map(async (imageId) => { const response = await fetch(`/api/images/${imageId}`); if (!response.ok) { throw new Error(`Failed to fetch image ${imageId}`); } return response.json(); }); const images = await Promise.all(imagePromises); setAllImages(images); setCurrentImageIndex(0); console.log('fetchAllImages: Loaded', images.length, 'images'); } catch (err: unknown) { console.error('fetchAllImages error:', err); setError(err instanceof Error ? err.message : 'Failed to load all images'); } finally { setIsLoadingImages(false); } }, []); // Carousel navigation functions const goToPrevious = useCallback(() => { if (allImages.length > 1) { setCurrentImageIndex((prev) => (prev > 0 ? prev - 1 : allImages.length - 1)); } }, [allImages.length]); const goToNext = useCallback(() => { if (allImages.length > 1) { setCurrentImageIndex((prev) => (prev < allImages.length - 1 ? prev + 1 : 0)); } }, [allImages.length]); const goToImage = useCallback((index: number) => { if (index >= 0 && index < allImages.length) { setCurrentImageIndex(index); } }, [allImages.length]); // Full-size image modal functions const handleViewFullSize = useCallback(async (image?: MapOut) => { const imageToShow = image || (allImages.length > 0 ? allImages[currentImageIndex] : map); if (imageToShow) { setIsLoadingFullSizeImage(true); setSelectedImageForModal(imageToShow); setShowFullSizeModal(true); // Preload the full-size image try { const img = new Image(); img.onload = () => { setIsLoadingFullSizeImage(false); }; img.onerror = () => { setIsLoadingFullSizeImage(false); }; img.src = imageToShow.image_url; } catch (error) { console.error('Error preloading full-size image:', error); setIsLoadingFullSizeImage(false); } } }, [allImages, currentImageIndex, map]); const handleCloseFullSizeModal = useCallback(() => { setShowFullSizeModal(false); setSelectedImageForModal(null); setIsLoadingFullSizeImage(false); }, []); useEffect(() => { console.log('MapDetailsPage: mapId from useParams:', mapId); console.log('MapDetailsPage: mapId type:', typeof mapId); console.log('MapDetailsPage: mapId value:', mapId); if (!mapId || mapId === 'undefined' || mapId === 'null' || mapId.trim() === '' || mapId === undefined || mapId === null) { console.log('MapDetailsPage: Invalid mapId, setting error'); setError('Map ID is required'); setLoading(false); return; } const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(mapId)) { console.log('MapDetailsPage: Invalid UUID format:', mapId); setError('Invalid Map ID format'); setLoading(false); return; } console.log('MapDetailsPage: Fetching data for mapId:', mapId); fetchMapData(mapId); }, [mapId, fetchMapData]); // Auto-navigate to first matching item when filters change useEffect(() => { if (!map || loading || isDeleting) return; if (!mapId || mapId === 'undefined' || mapId === 'null' || mapId.trim() === '') { console.log('Auto-navigation skipped: Invalid mapId'); return; } // Validate current mapId format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(mapId)) { console.log('Auto-navigation skipped: Invalid mapId format'); return; } const currentMapMatches = () => { const matchesSearch = !search || map.title?.toLowerCase().includes(search.toLowerCase()) || map.generated?.toLowerCase().includes(search.toLowerCase()) || map.source?.toLowerCase().includes(search.toLowerCase()) || map.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || map.source === srcFilter; const matchesCategory = !catFilter || map.event_type === catFilter; const matchesRegion = !regionFilter || map.countries.some(country => country.r_code === regionFilter); const matchesCountry = !countryFilter || map.countries.some(country => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || map.image_type === imageTypeFilter; const matchesReferenceExamples = !showReferenceExamples || map.starred === true; const matches = matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesReferenceExamples; console.log('Auto-navigation check:', { mapId, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples, matchesSearch, matchesSource, matchesCategory, matchesRegion, matchesCountry, matchesImageType, matchesReferenceExamples, matches }); return matches; }; if (!currentMapMatches()) { console.log('Current map does not match filters, looking for first matching item'); // Find first matching item and navigate to it fetch('/api/images') .then(r => r.json()) .then(images => { console.log('Auto-navigation: Received images from API:', images.length); console.log('Auto-navigation: First few images:', images.slice(0, 3).map((img: any) => ({ image_id: img.image_id, title: img.title }))); const firstMatching = images.find((img: any) => { const matchesSearch = !search || img.title?.toLowerCase().includes(search.toLowerCase()) || img.generated?.toLowerCase().includes(search.toLowerCase()) || img.source?.toLowerCase().includes(search.toLowerCase()) || img.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || img.source === srcFilter; const matchesCategory = !catFilter || img.event_type === catFilter; const matchesRegion = !regionFilter || img.countries?.some((country: any) => country.r_code === regionFilter); const matchesCountry = !countryFilter || img.countries?.some((country: any) => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter; const matchesReferenceExamples = !showReferenceExamples || img.starred === true; return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesReferenceExamples; }); console.log('Auto-navigation: Found first matching image:', firstMatching ? { image_id: firstMatching.image_id, title: firstMatching.title, source: firstMatching.source } : 'No matching image found'); if (firstMatching && firstMatching.image_id && firstMatching.image_id !== 'undefined' && firstMatching.image_id !== 'null' && firstMatching.image_id.trim() !== '' && firstMatching.image_id !== mapId) { // Additional UUID validation const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(firstMatching.image_id)) { console.log('Auto-navigating to:', firstMatching.image_id); navigate(`/map/${firstMatching.image_id}`); } else { console.error('Auto-navigation blocked: Invalid image_id format:', firstMatching.image_id); } } }) .catch(console.error); } }, [map, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples, mapId, navigate, loading, isDeleting]); const checkNavigationAvailability = async (currentId: string) => { // Validate the ID before making the request if (!currentId || currentId === 'undefined' || currentId === 'null' || currentId.trim() === '') { return; } try { const response = await fetch('/api/images/grouped'); if (response.ok) { const images = await response.json(); const filteredImages = images.filter((img: any) => { const matchesSearch = !search || img.title?.toLowerCase().includes(search.toLowerCase()) || img.generated?.toLowerCase().includes(search.toLowerCase()) || img.source?.toLowerCase().includes(search.toLowerCase()) || img.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || img.source === srcFilter; const matchesCategory = !catFilter || img.event_type === catFilter; const matchesRegion = !regionFilter || img.countries?.some((country: any) => country.r_code === regionFilter); const matchesCountry = !countryFilter || img.countries?.some((country: any) => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter; const matchesUploadType = !uploadTypeFilter || (uploadTypeFilter === 'single' && (!img.image_count || img.image_count <= 1)) || (uploadTypeFilter === 'multiple' && img.image_count && img.image_count > 1); const matchesReferenceExamples = !showReferenceExamples || img.starred === true; return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesUploadType && matchesReferenceExamples; }); const currentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === currentId); setHasPrevious(filteredImages.length > 1 && currentIndex > 0); setHasNext(filteredImages.length > 1 && currentIndex < filteredImages.length - 1); } } catch (error) { console.error('Failed to check navigation availability:', error); } }; const navigateToItem = async (direction: 'previous' | 'next') => { if (isNavigating) return; setIsNavigating(true); try { const response = await fetch('/api/images/grouped'); if (response.ok) { const images = await response.json(); const filteredImages = images.filter((img: any) => { const matchesSearch = !search || img.title?.toLowerCase().includes(search.toLowerCase()) || img.generated?.toLowerCase().includes(search.toLowerCase()) || img.source?.toLowerCase().includes(search.toLowerCase()) || img.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || img.source === srcFilter; const matchesCategory = !catFilter || img.event_type === catFilter; const matchesRegion = !regionFilter || img.countries?.some((country: any) => country.r_code === regionFilter); const matchesCountry = !countryFilter || img.countries?.some((country: any) => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter; const matchesUploadType = !uploadTypeFilter || (uploadTypeFilter === 'single' && (!img.image_count || img.image_count <= 1)) || (uploadTypeFilter === 'multiple' && img.image_count && img.image_count > 1); const matchesReferenceExamples = !showReferenceExamples || img.starred === true; return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesUploadType && matchesReferenceExamples; }); const currentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === mapId); // If current image is not in filtered list, add it temporarily for navigation if (currentIndex === -1) { const currentImage = images.find((img: any) => img.image_id === mapId); if (currentImage) { filteredImages.push(currentImage); } } const adjustedCurrentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === mapId); if (adjustedCurrentIndex === -1) { console.error('Current image not found in filtered list'); return; } let targetIndex: number; if (direction === 'previous') { targetIndex = adjustedCurrentIndex > 0 ? adjustedCurrentIndex - 1 : filteredImages.length - 1; } else { targetIndex = adjustedCurrentIndex < filteredImages.length - 1 ? adjustedCurrentIndex + 1 : 0; } const targetImage = filteredImages[targetIndex]; if (targetImage && targetImage.image_id && targetImage.image_id !== 'undefined' && targetImage.image_id !== 'null' && targetImage.image_id.trim() !== '') { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(targetImage.image_id)) { console.log('Carousel navigating to:', targetImage.image_id); navigate(`/map/${targetImage.image_id}`); } else { console.error('Carousel navigation blocked: Invalid image_id format:', targetImage.image_id); } } } } catch (error) { console.error('Failed to navigate to item:', error); } finally { setIsNavigating(false); } }; // Check navigation availability when filters change useEffect(() => { if (map && mapId && !loading && !isDeleting) { checkNavigationAvailability(mapId); } }, [map, mapId, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, loading, isDeleting, checkNavigationAvailability]); useEffect(() => { Promise.all([ fetch('/api/sources').then(r => r.json()), fetch('/api/types').then(r => r.json()), fetch('/api/image-types').then(r => r.json()), fetch('/api/regions').then(r => r.json()), fetch('/api/countries').then(r => r.json()), ]).then(([sourcesData, typesData, imageTypesData, regionsData, countriesData]) => { setSources(sourcesData); setTypes(typesData); setImageTypes(imageTypesData); setRegions(regionsData); setCountries(countriesData); }).catch(console.error); }, []); // delete function const handleDelete = async () => { if (!map) return; setShowDeleteConfirm(true); }; const toggleStarred = async () => { if (!map) return; try { const response = await fetch(`/api/images/${map.image_id}`, { method: "PUT", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ starred: !map.starred }) }); if (response.ok) { setMap(prev => prev ? { ...prev, starred: !prev.starred } : null); } else { console.error('Failed to toggle starred status'); } } catch (error) { console.error('Error toggling starred status:', error); } }; const confirmDelete = async () => { if (!map) return; setIsDeleting(true); try { console.log('Deleting image with ID:', map.image_id); const response = await fetch(`/api/images/${map.image_id}`, { method: 'DELETE', }); if (response.ok) { setMap(prev => prev ? { ...prev, starred: !prev.starred } : null); setShowDeleteConfirm(false); try { const response = await fetch('/api/images/grouped'); if (response.ok) { const images = await response.json(); const filteredImages = images.filter((img: any) => { const matchesSearch = !search || img.title?.toLowerCase().includes(search.toLowerCase()) || img.generated?.toLowerCase().includes(search.toLowerCase()) || img.source?.toLowerCase().includes(search.toLowerCase()) || img.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || img.source === srcFilter; const matchesCategory = !catFilter || img.event_type === catFilter; const matchesRegion = !regionFilter || img.countries?.some((country: any) => country.r_code === regionFilter); const matchesCountry = !countryFilter || img.countries?.some((country: any) => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter; const matchesUploadType = !uploadTypeFilter || (uploadTypeFilter === 'single' && (!img.image_count || img.image_count <= 1)) || (uploadTypeFilter === 'multiple' && img.image_count && img.image_count > 1); const matchesReferenceExamples = !showReferenceExamples || img.starred === true; return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesUploadType && matchesReferenceExamples; }); const remainingImages = filteredImages.filter((img: any) => img.image_id !== map.image_id); if (remainingImages.length > 0) { const currentIndex = filteredImages.findIndex((img: any) => img.image_id === map.image_id); let targetIndex: number; if (currentIndex === filteredImages.length - 1) { targetIndex = currentIndex - 1; } else { targetIndex = currentIndex; } console.log('Navigation target:', { currentIndex, targetIndex, targetId: remainingImages[targetIndex]?.image_id }); if (targetIndex >= 0 && targetIndex < remainingImages.length) { const nextImage = remainingImages[targetIndex]; if (nextImage && nextImage.image_id && nextImage.image_id !== 'undefined' && nextImage.image_id !== 'null' && nextImage.image_id.trim() !== '') { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(nextImage.image_id)) { console.log('Navigating to:', nextImage.image_id); navigate(`/map/${nextImage.image_id}`); } else { console.error('Navigation blocked: Invalid image_id format:', nextImage.image_id); navigate('/explore'); } } else { console.error('Navigation blocked: Invalid image_id:', nextImage?.image_id); navigate('/explore'); } } else if (remainingImages[0] && remainingImages[0].image_id && remainingImages[0].image_id !== 'undefined' && remainingImages[0].image_id !== 'null' && remainingImages[0].image_id.trim() !== '') { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(remainingImages[0].image_id)) { console.log('Fallback navigation to first item:', remainingImages[0].image_id); navigate(`/map/${remainingImages[0].image_id}`); } else { console.error('Fallback navigation blocked: Invalid image_id format:', remainingImages[0].image_id); navigate('/explore'); } } else { console.log('No valid remaining items, going to explore page'); navigate('/explore'); } } else { console.log('No remaining items, going to explore page'); navigate('/explore'); } } else { navigate('/explore'); } } catch (error) { console.error('Failed to navigate to next item:', error); navigate('/explore'); } finally { setIsDeleting(false); } } else { console.error('Delete failed'); setIsDeleting(false); } } catch (error) { console.error('Delete failed:', error); setIsDeleting(false); } }; const filteredMap = useMemo(() => { if (!map) return null; if (!search && !srcFilter && !catFilter && !regionFilter && !countryFilter && !imageTypeFilter && !uploadTypeFilter && !showReferenceExamples) { return map; } const matchesSearch = !search || map.title?.toLowerCase().includes(search.toLowerCase()) || map.generated?.toLowerCase().includes(search.toLowerCase()) || map.source?.toLowerCase().includes(search.toLowerCase()) || map.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || map.source === srcFilter; const matchesCategory = !catFilter || map.event_type === catFilter; const matchesRegion = !regionFilter || map.countries.some(country => country.r_code === regionFilter); const matchesCountry = !countryFilter || map.countries.some(country => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || map.image_type === imageTypeFilter; const matchesUploadType = !uploadTypeFilter || (uploadTypeFilter === 'single' && (!map.image_count || map.image_count <= 1)) || (uploadTypeFilter === 'multiple' && map.image_count && map.image_count > 1); const matchesReferenceExamples = !showReferenceExamples || map.starred === true; const matches = matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesUploadType && matchesReferenceExamples; // If current map doesn't match filters, navigate to a matching image if (!matches && (search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || uploadTypeFilter || showReferenceExamples)) { // Navigate to a matching image after a short delay to avoid infinite loops setTimeout(() => { navigateToMatchingImage(); }, 100); // Return the current map while loading to show loading state instead of "no match found" return map; } return matches ? map : null; }, [map, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]); const navigateToMatchingImage = useCallback(async () => { setLoading(true); try { const response = await fetch('/api/images/grouped'); if (response.ok) { const images = await response.json(); const filteredImages = images.filter((img: any) => { const matchesSearch = !search || img.title?.toLowerCase().includes(search.toLowerCase()) || img.generated?.toLowerCase().includes(search.toLowerCase()) || img.source?.toLowerCase().includes(search.toLowerCase()) || img.event_type?.toLowerCase().includes(search.toLowerCase()); const matchesSource = !srcFilter || img.source === srcFilter; const matchesCategory = !catFilter || img.event_type === catFilter; const matchesRegion = !regionFilter || img.countries?.some((country: any) => country.r_code === regionFilter); const matchesCountry = !countryFilter || img.countries?.some((country: any) => country.c_code === countryFilter); const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter; const matchesUploadType = !uploadTypeFilter || (uploadTypeFilter === 'single' && (!img.image_count || img.image_count <= 1)) || (uploadTypeFilter === 'multiple' && img.image_count && img.image_count > 1); const matchesReferenceExamples = !showReferenceExamples || img.starred === true; return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesUploadType && matchesReferenceExamples; }); if (filteredImages.length > 0) { const firstMatchingImage = filteredImages[0]; if (firstMatchingImage && firstMatchingImage.image_id) { navigate(`/map/${firstMatchingImage.image_id}`); } } else { // No matching images, go back to explore navigate('/explore'); } } } catch (error) { console.error('Failed to navigate to matching image:', error); navigate('/explore'); } finally { setLoading(false); } }, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples, navigate]); const handleContribute = () => { if (!map) return; // For single image contribution if (!map.all_image_ids || map.all_image_ids.length <= 1) { const imageIds = [map.image_id]; const url = `/upload?step=1&contribute=true&imageIds=${imageIds.join(',')}`; navigate(url); return; } // For multi-upload contribution const imageIds = map.all_image_ids; const url = `/upload?step=1&contribute=true&imageIds=${imageIds.join(',')}`; navigate(url); }; const createImageData = (map: any, fileName: string) => ({ image: `images/${fileName}`, caption: map.edited || map.generated || '', metadata: { image_id: map.image_count && map.image_count > 1 ? map.all_image_ids || [map.image_id] : map.image_id, title: map.title, source: map.source, event_type: map.event_type, image_type: map.image_type, countries: map.countries, starred: map.starred, image_count: map.image_count || 1 } }); const exportDataset = async (mode: 'standard' | 'fine-tuning') => { if (!map) return; setIsExporting(true); setExportSuccess(false); try { const JSZip = (await import('jszip')).default; const zip = new JSZip(); if (map.image_type === 'crisis_map') { const crisisFolder = zip.folder('crisis_maps_dataset'); const crisisImagesFolder = crisisFolder?.folder('images'); if (crisisImagesFolder) { try { // Get all image IDs for this map const imageIds = map.image_count && map.image_count > 1 ? map.all_image_ids || [map.image_id] : [map.image_id]; // Fetch all images for this map const imagePromises = imageIds.map(async (imageId, imgIndex) => { try { const response = await fetch(`/api/images/${imageId}/file`); if (!response.ok) throw new Error(`Failed to fetch image ${imageId}`); const blob = await response.blob(); const fileExtension = map.file_key.split('.').pop() || 'jpg'; const fileName = `0001_${String(imgIndex + 1).padStart(2, '0')}.${fileExtension}`; crisisImagesFolder.file(fileName, blob); return { success: true, fileName, imageId }; } catch (error) { console.error(`Failed to process image ${imageId}:`, error); return { success: false, fileName: '', imageId }; } }); const imageResults = await Promise.all(imagePromises); const successfulImages = imageResults.filter(result => result.success); if (successfulImages.length === 0) { throw new Error('No images could be processed'); } if (mode === 'fine-tuning') { const trainData: any[] = []; const testData: any[] = []; const valData: any[] = []; const imageFiles = successfulImages.map(result => `images/${result.fileName}`); const random = Math.random(); const entry = { image: imageFiles.length === 1 ? imageFiles[0] : imageFiles, caption: map.edited || map.generated || '', metadata: { image_id: imageIds, title: map.title, source: map.source, event_type: map.event_type, image_type: map.image_type, countries: map.countries, starred: map.starred, image_count: map.image_count || 1 } }; if (random < trainSplit / 100) { trainData.push(entry); } else if (random < (trainSplit + testSplit) / 100) { testData.push(entry); } else { valData.push(entry); } if (crisisFolder) { crisisFolder.file('train.jsonl', JSON.stringify(trainData, null, 2)); crisisFolder.file('test.jsonl', JSON.stringify(testData, null, 2)); crisisFolder.file('val.jsonl', JSON.stringify(valData, null, 2)); } } else { const imageFiles = successfulImages.map(result => `images/${result.fileName}`); const jsonData = { image: imageFiles.length === 1 ? imageFiles[0] : imageFiles, caption: map.edited || map.generated || '', metadata: { image_id: imageIds, title: map.title, source: map.source, event_type: map.event_type, image_type: map.image_type, countries: map.countries, starred: map.starred, image_count: map.image_count || 1 } }; if (crisisFolder) { crisisFolder.file('0001.json', JSON.stringify(jsonData, null, 2)); } } } catch (error) { console.error(`Failed to process image ${map.image_id}:`, error); throw error; } } } else if (map.image_type === 'drone_image') { const droneFolder = zip.folder('drone_images_dataset'); const droneImagesFolder = droneFolder?.folder('images'); if (droneImagesFolder) { try { const response = await fetch(`/api/images/${map.image_id}/file`); if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`); const blob = await response.blob(); const fileExtension = map.file_key.split('.').pop() || 'jpg'; const fileName = `0001.${fileExtension}`; droneImagesFolder.file(fileName, blob); if (mode === 'fine-tuning') { const trainData: any[] = []; const testData: any[] = []; const valData: any[] = []; if (String(map?.image_type) === 'crisis_map') { const random = Math.random(); if (random < trainSplit / 100) { trainData.push(createImageData(map, '0001')); } else if (random < (trainSplit + testSplit) / 100) { testData.push(createImageData(map, '0001')); } else { valData.push(createImageData(map, '0001')); } } else if (String(map?.image_type) === 'drone_image') { const random = Math.random(); if (random < trainSplit / 100) { trainData.push(createImageData(map, '0001')); } else if (random < (trainSplit + testSplit) / 100) { testData.push(createImageData(map, '0001')); } else { valData.push(createImageData(map, '0001')); } } if (droneFolder) { droneFolder.file('train.jsonl', JSON.stringify(trainData, null, 2)); droneFolder.file('test.jsonl', JSON.stringify(testData, null, 2)); droneFolder.file('val.jsonl', JSON.stringify(valData, null, 2)); } } else { const jsonData = { image: `images/${fileName}`, caption: map.edited || map.generated || '', metadata: { image_id: map.image_count && map.image_count > 1 ? map.all_image_ids || [map.image_id] : map.image_id, title: map.title, source: map.source, event_type: map.event_type, image_type: map.image_type, countries: map.countries, starred: map.starred, image_count: map.image_count || 1 } }; if (droneFolder) { droneFolder.file('0001.json', JSON.stringify(jsonData, null, 2)); } } } catch (error) { console.error(`Failed to process image ${map.image_id}:`, error); throw error; } } } else { const genericFolder = zip.folder('generic_dataset'); const genericImagesFolder = genericFolder?.folder('images'); if (genericImagesFolder) { try { const response = await fetch(`/api/images/${map.image_id}/file`); if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`); const blob = await response.blob(); const fileExtension = map.file_key.split('.').pop() || 'jpg'; const fileName = `0001.${fileExtension}`; genericImagesFolder.file(fileName, blob); if (mode === 'fine-tuning') { const trainData: any[] = []; const testData: any[] = []; const valData: any[] = []; if (String(map?.image_type) === 'crisis_map') { const random = Math.random(); if (random < trainSplit / 100) { trainData.push(createImageData(map, '0001')); } else if (random < (trainSplit + testSplit) / 100) { testData.push(createImageData(map, '0001')); } else { valData.push(createImageData(map, '0001')); } } else if (String(map?.image_type) === 'drone_image') { const random = Math.random(); if (random < trainSplit / 100) { trainData.push(createImageData(map, '0001')); } else if (random < (trainSplit + testSplit) / 100) { testData.push(createImageData(map, '0001')); } else { valData.push(createImageData(map, '0001')); } } if (genericFolder) { genericFolder.file('train.jsonl', JSON.stringify(trainData, null, 2)); genericFolder.file('test.jsonl', JSON.stringify(testData, null, 2)); genericFolder.file('val.jsonl', JSON.stringify(valData, null, 2)); } } else { const jsonData = { image: `images/${fileName}`, caption: map.edited || map.generated || '', metadata: { image_id: map.image_count && map.image_count > 1 ? map.all_image_ids || [map.image_id] : map.image_id, title: map.title, source: map.source, event_type: map.event_type, image_type: map.image_type, countries: map.countries, starred: map.starred, image_count: map.image_count || 1 } }; if (genericFolder) { genericFolder.file('0001.json', JSON.stringify(jsonData, null, 2)); } } } catch (error) { console.error(`Failed to process image ${map.image_id}:`, error); throw error; } } } // Generate and download zip const zipBlob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(zipBlob); const link = document.createElement('a'); link.href = url; link.download = `dataset_${map.image_type}_${map.image_id}_${mode}_${new Date().toISOString().split('T')[0]}.zip`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); console.log(`Exported ${map.image_type} dataset with 1 image in ${mode} mode`); setExportSuccess(true); } catch (error) { console.error('Export failed:', error); alert('Failed to export dataset. Please try again.'); } finally { setIsExporting(false); } }; if (loading) { return (
Loading map details...
); } if (error || !map) { return (
⚠️
Unable to load map
{error || 'Map not found'}
); } return (
{ if (value === 'mapDetails' || value === 'explore') { setView(value); if (value === 'explore') { navigate('/explore'); } } }} options={viewOptions} keySelector={(o) => o.key} labelSelector={(o) => o.label} /> {/* Action Buttons - Right Aligned */}
{/* Reference Examples Filter - Available to all users */} {/* Export Dataset Button */}
{/* Filter Bar */} {view === 'mapDetails' ? (
{filteredMap ? ( <>
{/* Image Section */} {filteredMap.title || "Map Image"} {filteredMap.starred && ( )}
} headingLevel={2} withHeaderBorder withInternalPadding spacing="comfortable" >
{(map?.image_count && map.image_count > 1) || allImages.length > 1 ? ( // Multi-upload carousel
{isLoadingImages ? (
Loading images...
) : allImages[currentImageIndex]?.detail_url ? ( {allImages[currentImageIndex].file_key} { console.log('MapDetailsPage: Detail image failed to load, falling back to original:', allImages[currentImageIndex].detail_url); // Fallback to original image const target = e.target as HTMLImageElement; if (allImages[currentImageIndex].image_url) { target.src = allImages[currentImageIndex].image_url; } }} onLoad={() => console.log('MapDetailsPage: Detail image loaded successfully:', allImages[currentImageIndex].detail_url)} /> ) : allImages[currentImageIndex]?.image_url ? ( {allImages[currentImageIndex].file_key} console.log('MapDetailsPage: Original image loaded successfully:', allImages[currentImageIndex].image_url)} /> ) : (
No image available
)}
{/* Carousel Navigation */}
{allImages.map((_, index) => ( ))}
{/* View Image Button for Carousel */}
) : ( // Single image display
{/* Map Details Page: Prioritize detail versions for better quality */} {filteredMap.detail_url ? ( {filteredMap.file_key} { console.log('MapDetailsPage: Detail image failed to load, falling back to original:', filteredMap.detail_url); // Fallback to original image const target = e.target as HTMLImageElement; if (filteredMap.image_url) { target.src = filteredMap.image_url; } }} onLoad={() => console.log('MapDetailsPage: Detail image loaded successfully:', filteredMap.detail_url)} /> ) : filteredMap.image_url ? ( {filteredMap.file_key} console.log('MapDetailsPage: Original image loaded successfully:', filteredMap.image_url)} /> ) : (
No image available
)} {/* View Image Button for Single Image */}
)}
{/* Tags Section - Inside Image Container */}
{filteredMap.image_type !== 'drone_image' && ( {sources.find(s => s.s_code === filteredMap.source)?.label || filteredMap.source} )} {types.find(t => t.t_code === filteredMap.event_type)?.label || filteredMap.event_type} {imageTypes.find(it => it.image_type === filteredMap.image_type)?.label || filteredMap.image_type} {filteredMap.countries && filteredMap.countries.length > 0 && ( <> {regions.find(r => r.r_code === filteredMap.countries[0].r_code)?.label || 'Unknown Region'} {filteredMap.countries.map(country => country.label).join(', ')} )} {filteredMap.image_count && filteredMap.image_count > 1 && ( 📷 {filteredMap.image_count} )} {(!filteredMap.image_count || filteredMap.image_count <= 1) && ( Single )}
{/* Details Section */}
{/* Combined Analysis Structure */} {(filteredMap.edited && filteredMap.edited.includes('Description:')) || (filteredMap.generated && filteredMap.generated.includes('Description:')) ? (
{(filteredMap.edited || filteredMap.generated || '').split('\n').map((line, index) => (
{line.startsWith('Description:') || line.startsWith('Analysis:') || line.startsWith('Recommended Actions:') ? (

{line}

) : line.trim() === '' ? (
) : (

{line}

)}
))}
) : (
{filteredMap.generated ? (

{filteredMap.edited || filteredMap.generated}

) : (

— no caption yet —

)}
)}
{/* Contribute Section with Navigation Arrows */}
{hasPrevious && ( )} {/* Delete Button - Admin Only */} {isAuthenticated && ( )} {/* Star Toggle Button - Admin Only */} {isAuthenticated && ( )} {hasNext && ( )}
) : (
No matches found
)}
) : null} {/* Delete Confirmation Modal */} {showDeleteConfirm && (
setShowDeleteConfirm(false)}>
e.stopPropagation()}>

Delete Image?

This action cannot be undone. Are you sure you want to delete this saved image and all related data?

)} {/* Export Selection Modal */} {showExportModal && ( { setShowExportModal(false); setExportSuccess(false); setIsExporting(false); }} onExport={(mode, selectedTypes) => { if (selectedTypes.includes(map.image_type)) { exportDataset(mode); } }} filteredCount={1} totalCount={1} hasFilters={false} crisisMapsCount={map.image_type === 'crisis_map' ? 1 : 0} droneImagesCount={map.image_type === 'drone_image' ? 1 : 0} isLoading={isExporting} exportSuccess={exportSuccess} variant="single" onNavigateToList={() => { setShowExportModal(false); navigate('/explore'); }} onNavigateAndExport={() => { setShowExportModal(false); navigate('/explore?export=true'); }} /> )} {/* Full Size Image Modal */}
); }