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) {