feat: implement 4-level priority coordinates resolution on photo upload
This commit is contained in:
Vendored
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
|
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
|
||||||
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
|
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
|
||||||
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
|
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
|
||||||
<script type="module" crossorigin src="/assets/index-D6nIKZs7.js"></script>
|
<script type="module" crossorigin src="/assets/index-BuWSIZwN.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Cyuzqnbw.js">
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Cyuzqnbw.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-others-VIU5qAPG.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-others-VIU5qAPG.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-DOXkNaRS.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-DOXkNaRS.js">
|
||||||
|
|||||||
@@ -27,6 +27,130 @@ interface LandingPageProps {
|
|||||||
onOpenNavigation?: (payload: any) => void;
|
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<LandingPageProps> = ({
|
export const LandingPage: React.FC<LandingPageProps> = ({
|
||||||
onGoToSignup,
|
onGoToSignup,
|
||||||
onGoToMap,
|
onGoToMap,
|
||||||
@@ -49,7 +173,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
const cameraInputRef = useRef<HTMLInputElement>(null);
|
const cameraInputRef = useRef<HTMLInputElement>(null);
|
||||||
const galleryInputRef = useRef<HTMLInputElement>(null);
|
const galleryInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [pendingPhotoFile, setPendingPhotoFile] = useState<File | null>(null);
|
const [pendingPhotoFile, setPendingPhotoFile] = useState<File | null>(null);
|
||||||
const [pendingPhotoLocation, setPendingPhotoLocation] = useState<GeolocationPosition | null>(null);
|
const [pendingPhotoLocation, setPendingPhotoLocation] = useState<{ latitude: number; longitude: number } | null>(null);
|
||||||
const [photoPreviewUrl, setPhotoPreviewUrl] = useState<string>('');
|
const [photoPreviewUrl, setPhotoPreviewUrl] = useState<string>('');
|
||||||
const notify = useNotification();
|
const notify = useNotification();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -204,14 +328,32 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
}
|
}
|
||||||
const processedFile = moderationResult.file;
|
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)
|
// Determine image upload coordinates based on priority checklist:
|
||||||
const location = await Promise.race([
|
let finalLat: number | null = null;
|
||||||
|
let finalLng: number | null = null;
|
||||||
|
|
||||||
|
// 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<GeolocationPosition | null>((resolve) => {
|
new Promise<GeolocationPosition | null>((resolve) => {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
} else {
|
} else {
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(pos) => resolve(pos),
|
(p) => resolve(p),
|
||||||
() => resolve(null),
|
() => resolve(null),
|
||||||
{ timeout: 4000, enableHighAccuracy: true }
|
{ timeout: 4000, enableHighAccuracy: true }
|
||||||
);
|
);
|
||||||
@@ -219,10 +361,43 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
}),
|
}),
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 4500))
|
new Promise<null>((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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lưu file và location vào state pending, hiển thị modal tags
|
// 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);
|
setPendingPhotoFile(processedFile);
|
||||||
setPendingPhotoLocation(location);
|
setPendingPhotoLocation({ latitude: finalLat, longitude: finalLng });
|
||||||
|
|
||||||
// Tạo preview URL cho ảnh
|
// Tạo preview URL cho ảnh
|
||||||
const previewUrl = URL.createObjectURL(processedFile);
|
const previewUrl = URL.createObjectURL(processedFile);
|
||||||
@@ -272,8 +447,8 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('images', pendingPhotoFile);
|
formData.append('images', pendingPhotoFile);
|
||||||
if (pendingPhotoLocation) {
|
if (pendingPhotoLocation) {
|
||||||
formData.append('latitude', pendingPhotoLocation.coords.latitude.toString());
|
formData.append('latitude', pendingPhotoLocation.latitude.toString());
|
||||||
formData.append('longitude', pendingPhotoLocation.coords.longitude.toString());
|
formData.append('longitude', pendingPhotoLocation.longitude.toString());
|
||||||
}
|
}
|
||||||
// Thêm tags vào formData
|
// Thêm tags vào formData
|
||||||
if (selectedTags.length > 0) {
|
if (selectedTags.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user