From f39750af72093c29f36d3de5949b06202d43ece4 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sat, 27 Jun 2026 21:08:42 +0700 Subject: [PATCH] feat: implement 4-level priority coordinates resolution on photo upload --- frontend/dist/index.html | 2 +- frontend/src/pages/LandingPage.tsx | 215 ++++++++++++++++++++++++++--- 2 files changed, 196 insertions(+), 21 deletions(-) diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 9928a8c..4c018c4 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -21,7 +21,7 @@ - + diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index e26bd75..89832bc 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -27,6 +27,130 @@ interface LandingPageProps { onOpenNavigation?: (payload: any) => void; } +// Helper to extract GPS location from EXIF tags in JPEG/JPG files +function getExifGps(file: File): Promise<{ latitude: number; longitude: number } | null> { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const buffer = e.target?.result as ArrayBuffer; + const view = new DataView(buffer); + if (view.byteLength < 4) { + resolve(null); + return; + } + if (view.getUint16(0, false) !== 0xFFD8) { + resolve(null); + return; + } + let offset = 2; + const length = view.byteLength; + while (offset < length - 2) { + const marker = view.getUint16(offset, false); + if (marker === 0xFFE1) { + const app1Length = view.getUint16(offset + 2, false); + if (offset + 4 + app1Length > length) { + resolve(null); + return; + } + const exifHeader = view.getUint32(offset + 4, false); + if (exifHeader === 0x45786966) { + const tiffOffset = offset + 10; + const bigEndian = view.getUint16(tiffOffset, false) === 0x4D4D; + if (view.getUint16(tiffOffset + 2, bigEndian) !== 0x002A) { + resolve(null); + return; + } + const firstIFD = view.getUint32(tiffOffset + 4, bigEndian); + let ifdOffset = tiffOffset + firstIFD; + if (ifdOffset + 2 > length) { + resolve(null); + return; + } + const numEntries = view.getUint16(ifdOffset, bigEndian); + let gpsInfoOffset = 0; + for (let i = 0; i < numEntries; i++) { + const entryOffset = ifdOffset + 2 + i * 12; + if (entryOffset + 12 > length) break; + const tag = view.getUint16(entryOffset, bigEndian); + if (tag === 0x8825) { + gpsInfoOffset = view.getUint32(entryOffset + 8, bigEndian); + break; + } + } + if (gpsInfoOffset) { + const gpsIFDOffset = tiffOffset + gpsInfoOffset; + if (gpsIFDOffset + 2 > length) { + resolve(null); + return; + } + const numGpsEntries = view.getUint16(gpsIFDOffset, bigEndian); + let latRef = 'N'; + let lonRef = 'E'; + let latVal: number[] = []; + let lonVal: number[] = []; + for (let i = 0; i < numGpsEntries; i++) { + const entryOffset = gpsIFDOffset + 2 + i * 12; + if (entryOffset + 12 > length) break; + const tag = view.getUint16(entryOffset, bigEndian); + if (tag === 1) { + latRef = String.fromCharCode(view.getUint8(entryOffset + 8)); + } else if (tag === 2) { + const offsetVal = view.getUint32(entryOffset + 8, bigEndian); + latVal = readRationalArray(view, tiffOffset + offsetVal, bigEndian, length); + } else if (tag === 3) { + lonRef = String.fromCharCode(view.getUint8(entryOffset + 8)); + } else if (tag === 4) { + const offsetVal = view.getUint32(entryOffset + 8, bigEndian); + lonVal = readRationalArray(view, tiffOffset + offsetVal, bigEndian, length); + } + } + if (latVal.length === 3 && lonVal.length === 3) { + const latitude = convertDMSToDD(latVal[0], latVal[1], latVal[2], latRef); + const longitude = convertDMSToDD(lonVal[0], lonVal[1], lonVal[2], lonRef); + resolve({ latitude, longitude }); + return; + } + } + } + break; + } else if (marker >= 0xFFD0 && marker <= 0xFFD9) { + offset += 2; + } else { + const blockLength = view.getUint16(offset + 2, false); + offset += 2 + blockLength; + } + } + resolve(null); + } catch (err) { + console.error('Error parsing EXIF:', err); + resolve(null); + } + }; + reader.onerror = () => resolve(null); + reader.readAsArrayBuffer(file.slice(0, 128 * 1024)); + }); +} + +function readRationalArray(view: DataView, offset: number, bigEndian: boolean, maxLen: number): number[] { + const values = []; + if (offset + 24 > maxLen) return []; + for (let i = 0; i < 3; i++) { + const num = view.getUint32(offset + i * 8, bigEndian); + const den = view.getUint32(offset + i * 8 + 4, bigEndian); + values.push(den === 0 ? 0 : num / den); + } + return values; +} + +function convertDMSToDD(degrees: number, minutes: number, seconds: number, ref: string): number { + let dd = degrees + minutes / 60 + seconds / 3600; + if (ref === 'S' || ref === 'W') { + dd = -dd; + } + return dd; +} + export const LandingPage: React.FC = ({ onGoToSignup, onGoToMap, @@ -49,7 +173,7 @@ export const LandingPage: React.FC = ({ const cameraInputRef = useRef(null); const galleryInputRef = useRef(null); const [pendingPhotoFile, setPendingPhotoFile] = useState(null); - const [pendingPhotoLocation, setPendingPhotoLocation] = useState(null); + const [pendingPhotoLocation, setPendingPhotoLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [photoPreviewUrl, setPhotoPreviewUrl] = useState(''); const notify = useNotification(); const { t } = useTranslation(); @@ -204,25 +328,76 @@ export const LandingPage: React.FC = ({ } const processedFile = moderationResult.file; - // Lấy tọa độ hiện tại của người dùng với cơ chế chống treo (Promise.race) - const location = await Promise.race([ - new Promise((resolve) => { - if (!navigator.geolocation) { - resolve(null); - } else { - navigator.geolocation.getCurrentPosition( - (pos) => resolve(pos), - () => resolve(null), - { timeout: 4000, enableHighAccuracy: true } - ); - } - }), - new Promise((resolve) => setTimeout(() => resolve(null), 4500)) - ]); + // Determine image upload coordinates based on priority checklist: + let finalLat: number | null = null; + let finalLng: number | null = null; - // Lưu file và location vào state pending, hiển thị modal tags + // 1. Read EXIF GPS metadata location from the uploaded image itself + try { + const exifLoc = await getExifGps(file); + if (exifLoc) { + finalLat = exifLoc.latitude; + finalLng = exifLoc.longitude; + console.log('[Upload Location] Priority 1: EXIF data coordinates found:', finalLat, finalLng); + } + } catch (e) { + console.error('[Upload Location] Error reading EXIF from file:', e); + } + + // 2. Get current mobile/device GPS position of the user + if (finalLat === null || finalLng === null) { + try { + const pos = await Promise.race([ + new Promise((resolve) => { + if (!navigator.geolocation) { + resolve(null); + } else { + navigator.geolocation.getCurrentPosition( + (p) => resolve(p), + () => resolve(null), + { timeout: 4000, enableHighAccuracy: true } + ); + } + }), + new Promise((resolve) => setTimeout(() => resolve(null), 4500)) + ]); + if (pos && pos.coords) { + finalLat = pos.coords.latitude; + finalLng = pos.coords.longitude; + console.log('[Upload Location] Priority 2: GPS coordinates found:', finalLat, finalLng); + } + } catch (e) { + console.error('[Upload Location] Error acquiring current GPS:', e); + } + } + + // 3. Get last viewed map viewport center coordinates + if (finalLat === null || finalLng === null) { + const lastViewStateStr = localStorage.getItem('map_view_state'); + if (lastViewStateStr) { + try { + const lastViewState = JSON.parse(lastViewStateStr); + if (lastViewState && Array.isArray(lastViewState.center) && lastViewState.center.length === 2) { + finalLat = Number(lastViewState.center[0]); + finalLng = Number(lastViewState.center[1]); + console.log('[Upload Location] Priority 3: Last viewed map viewport center used:', finalLat, finalLng); + } + } catch (e) { + console.error('[Upload Location] Error parsing map_view_state:', e); + } + } + } + + // 4. Default fallback location coordinates + if (finalLat === null || finalLng === null) { + finalLat = 10.7769; + finalLng = 106.7009; + console.log('[Upload Location] Priority 4: Using default fallback coordinates:', finalLat, finalLng); + } + + // Save file and resolved coordinates into state setPendingPhotoFile(processedFile); - setPendingPhotoLocation(location); + setPendingPhotoLocation({ latitude: finalLat, longitude: finalLng }); // Tạo preview URL cho ảnh const previewUrl = URL.createObjectURL(processedFile); @@ -272,8 +447,8 @@ export const LandingPage: React.FC = ({ const formData = new FormData(); formData.append('images', pendingPhotoFile); if (pendingPhotoLocation) { - formData.append('latitude', pendingPhotoLocation.coords.latitude.toString()); - formData.append('longitude', pendingPhotoLocation.coords.longitude.toString()); + formData.append('latitude', pendingPhotoLocation.latitude.toString()); + formData.append('longitude', pendingPhotoLocation.longitude.toString()); } // Thêm tags vào formData if (selectedTags.length > 0) {