fix: EXIF GPS extraction now works with native camera capture
- Replaced getUserMedia() + canvas.toBlob() with <input type=file capture=environment> → native camera app preserves full EXIF data including GPS - Defined missing extractGpsFromPhoto() using exifr.gps() library - Manual mode now uploads the original file (with EXIF) instead of a canvas-encoded blob - Server-side exif_gps fallback if client-side extraction fails - Renamed manualPhotoBlob → manualPhotoFile for clarity
This commit is contained in:
+46
-94
@@ -983,14 +983,14 @@
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">📸 Photo (optional)</div>
|
||||
<div id="manPhotoArea" class="camera-area" style="aspect-ratio:4/3;margin-bottom:8px;">
|
||||
<div id="manPhotoPlaceholder" class="camera-placeholder" onclick="startManualPhoto()">
|
||||
<div id="manPhotoPlaceholder" class="camera-placeholder" onclick="document.getElementById('manPhotoInput').click()">
|
||||
<span class="cam-icon">📸</span>
|
||||
<div class="cam-hint" style="margin-top:4px;">Tap to take photo</div>
|
||||
</div>
|
||||
<video id="manPhotoVideo" autoplay playsinline muted style="display:none;"></video>
|
||||
<input type="file" id="manPhotoInput" accept="image/*" capture="environment" style="display:none;" onchange="handleManualPhotoFile(event)">
|
||||
</div>
|
||||
<div id="manPhotoCaptureRow" style="display:none;justify-content:center;margin-bottom:8px;">
|
||||
<button class="ocr-capture-btn" onclick="captureManualPhoto()" title="Capture" style="position:static;width:48px;height:48px;font-size:18px;">📸</button>
|
||||
<button class="ocr-capture-btn" onclick="document.getElementById('manPhotoInput').click()" title="Capture" style="position:static;width:48px;height:48px;font-size:18px;">📸</button>
|
||||
</div>
|
||||
<img id="manPhotoPreview" style="display:none;width:100%;border-radius:var(--radius-sm);">
|
||||
<button id="manPhotoRetake" class="btn btn-outline btn-sm" style="display:none;margin-top:6px;width:100%;" onclick="retakeManualPhoto()">🔄 Retake</button>
|
||||
@@ -2466,62 +2466,47 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// MANUAL MODE — Photo
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
let manualPhotoStream = null;
|
||||
let manualPhotoFile = null;
|
||||
let manualPhotoBlob = null;
|
||||
let manualPhotoGps = null;
|
||||
|
||||
async function startManualPhoto() {
|
||||
if (manualPhotoStream) return;
|
||||
try {
|
||||
manualPhotoStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } },
|
||||
audio: false,
|
||||
});
|
||||
const video = document.getElementById('manPhotoVideo');
|
||||
if (!video) return;
|
||||
video.srcObject = manualPhotoStream;
|
||||
await video.play();
|
||||
document.getElementById('manPhotoPlaceholder').style.display = 'none';
|
||||
video.style.display = 'block';
|
||||
document.getElementById('manPhotoCaptureRow').style.display = 'flex';
|
||||
} catch (e) {
|
||||
showToast('Camera failed: ' + e.message, true);
|
||||
}
|
||||
}
|
||||
async function handleManualPhotoFile(event) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
manualPhotoFile = file;
|
||||
manualPhotoBlob = file; // Use original file (preserves EXIF)
|
||||
|
||||
async function captureManualPhoto() {
|
||||
const video = document.getElementById('manPhotoVideo');
|
||||
if (!video || !manualPhotoStream) return;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||
manualPhotoBlob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.85));
|
||||
|
||||
const url = URL.createObjectURL(manualPhotoBlob);
|
||||
// Show preview
|
||||
const url = URL.createObjectURL(file);
|
||||
document.getElementById('manPhotoPreview').src = url;
|
||||
document.getElementById('manPhotoPreview').style.display = 'block';
|
||||
document.getElementById('manPhotoRetake').style.display = 'block';
|
||||
stopManualPhoto();
|
||||
document.getElementById('manPhotoPlaceholder').style.display = 'none';
|
||||
|
||||
// Try EXIF GPS from the original file
|
||||
manualPhotoGps = await extractGpsFromPhoto(file);
|
||||
if (manualPhotoGps) {
|
||||
const coords = manualPhotoGps.lat.toFixed(6) + ', ' + manualPhotoGps.lng.toFixed(6);
|
||||
const parkingEl = document.getElementById('manParkingLocation');
|
||||
if (parkingEl && !parkingEl.value.trim()) {
|
||||
parkingEl.value = coords;
|
||||
}
|
||||
const mapLinkEl = document.getElementById('manMapLink');
|
||||
if (mapLinkEl && !mapLinkEl.value.trim()) {
|
||||
mapLinkEl.value = `https://www.openstreetmap.org/?mlat=${manualPhotoGps.lat}&mlon=${manualPhotoGps.lng}&zoom=17`;
|
||||
}
|
||||
showToast('📍 GPS extracted from photo!');
|
||||
}
|
||||
}
|
||||
|
||||
function retakeManualPhoto() {
|
||||
manualPhotoFile = null;
|
||||
manualPhotoBlob = null;
|
||||
manualPhotoGps = null;
|
||||
document.getElementById('manPhotoPreview').style.display = 'none';
|
||||
document.getElementById('manPhotoRetake').style.display = 'none';
|
||||
startManualPhoto();
|
||||
}
|
||||
|
||||
function stopManualPhoto() {
|
||||
if (manualPhotoStream) {
|
||||
manualPhotoStream.getTracks().forEach(t => t.stop());
|
||||
manualPhotoStream = null;
|
||||
}
|
||||
const video = document.getElementById('manPhotoVideo');
|
||||
if (video) video.style.display = 'none';
|
||||
const ph = document.getElementById('manPhotoPlaceholder');
|
||||
if (ph) ph.style.display = '';
|
||||
const row = document.getElementById('manPhotoCaptureRow');
|
||||
if (row) row.style.display = 'none';
|
||||
document.getElementById('manPhotoPlaceholder').style.display = '';
|
||||
document.getElementById('manPhotoInput').value = '';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -2536,12 +2521,16 @@
|
||||
}
|
||||
|
||||
let photoPath = null;
|
||||
if (manualPhotoBlob) {
|
||||
if (manualPhotoFile) {
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', manualPhotoBlob, 'photo.jpg');
|
||||
fd.append('file', manualPhotoFile, manualPhotoFile.name || 'photo.jpg');
|
||||
const upData = await api('/api/upload/photo', { method: 'POST', body: fd });
|
||||
photoPath = upData.path;
|
||||
// If server returned EXIF GPS and we don't have GPS from the file yet, use it
|
||||
if (upData.exif_gps && !manualPhotoGps) {
|
||||
manualPhotoGps = upData.exif_gps;
|
||||
}
|
||||
} catch (e) { /* photo upload failed, continue without */ }
|
||||
}
|
||||
|
||||
@@ -3780,55 +3769,18 @@
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// HOOK: EXIF GPS extraction after camera capture in manual mode
|
||||
// Extract GPS from a photo file using exifr library
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Patch captureManualPhoto to also try EXIF GPS extraction
|
||||
const _origCaptureManualPhoto = captureManualPhoto;
|
||||
captureManualPhoto = async function() {
|
||||
await _origCaptureManualPhoto();
|
||||
// After capture, try EXIF GPS from the blob
|
||||
if (manualPhotoBlob) {
|
||||
async function extractGpsFromPhoto(file) {
|
||||
if (!file) return null;
|
||||
try {
|
||||
const gps = await extractGpsFromPhoto(manualPhotoBlob);
|
||||
if (gps) {
|
||||
const coords = gps.lat.toFixed(6) + ', ' + gps.lng.toFixed(6);
|
||||
const parkingEl = document.getElementById('manParkingLocation');
|
||||
if (parkingEl && !parkingEl.value.trim()) {
|
||||
parkingEl.value = coords;
|
||||
}
|
||||
const mapLinkEl = document.getElementById('manMapLink');
|
||||
if (mapLinkEl && !mapLinkEl.value.trim()) {
|
||||
mapLinkEl.value = `https://www.openstreetmap.org/?mlat=${gps.lat}&mlon=${gps.lng}&zoom=17`;
|
||||
}
|
||||
// Show GPS badge near photo
|
||||
const photoSection = document.querySelector('#addManualMode .form-section');
|
||||
let gpsTag = document.getElementById('manPhotoGpsTag');
|
||||
if (!gpsTag) {
|
||||
gpsTag = document.createElement('span');
|
||||
gpsTag.id = 'manPhotoGpsTag';
|
||||
gpsTag.className = 'gps-badge ok';
|
||||
gpsTag.style.marginLeft = '8px';
|
||||
const title = document.querySelector('#addManualMode .form-section-title');
|
||||
if (title) title.appendChild(gpsTag);
|
||||
}
|
||||
gpsTag.textContent = '📍 GPS from photo';
|
||||
gpsTag.style.display = 'inline-flex';
|
||||
showToast('📍 GPS extracted from photo!');
|
||||
}
|
||||
const gps = await exifr.gps(file);
|
||||
return gps || null;
|
||||
} catch (e) {
|
||||
// GPS extraction failure is non-critical
|
||||
console.warn('EXIF GPS from capture failed:', e.message);
|
||||
console.warn('EXIF GPS extraction failed:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Patch retakeManualPhoto to clear GPS tag
|
||||
const _origRetakeManualPhoto = retakeManualPhoto;
|
||||
retakeManualPhoto = function() {
|
||||
_origRetakeManualPhoto();
|
||||
const gpsTag = document.getElementById('manPhotoGpsTag');
|
||||
if (gpsTag) gpsTag.style.display = 'none';
|
||||
};
|
||||
|
||||
// Boot auth at page load
|
||||
document.addEventListener('DOMContentLoaded', () => { initAuth(); });
|
||||
|
||||
Reference in New Issue
Block a user