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">
|
||||||
<div class="form-section-title">📸 Photo (optional)</div>
|
<div class="form-section-title">📸 Photo (optional)</div>
|
||||||
<div id="manPhotoArea" class="camera-area" style="aspect-ratio:4/3;margin-bottom:8px;">
|
<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>
|
<span class="cam-icon">📸</span>
|
||||||
<div class="cam-hint" style="margin-top:4px;">Tap to take photo</div>
|
<div class="cam-hint" style="margin-top:4px;">Tap to take photo</div>
|
||||||
</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>
|
||||||
<div id="manPhotoCaptureRow" style="display:none;justify-content:center;margin-bottom:8px;">
|
<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>
|
</div>
|
||||||
<img id="manPhotoPreview" style="display:none;width:100%;border-radius:var(--radius-sm);">
|
<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>
|
<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
|
// MANUAL MODE — Photo
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
let manualPhotoStream = null;
|
let manualPhotoFile = null;
|
||||||
|
let manualPhotoBlob = null;
|
||||||
|
let manualPhotoGps = null;
|
||||||
|
|
||||||
async function startManualPhoto() {
|
async function handleManualPhotoFile(event) {
|
||||||
if (manualPhotoStream) return;
|
const file = event.target.files?.[0];
|
||||||
try {
|
if (!file) return;
|
||||||
manualPhotoStream = await navigator.mediaDevices.getUserMedia({
|
manualPhotoFile = file;
|
||||||
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } },
|
manualPhotoBlob = file; // Use original file (preserves EXIF)
|
||||||
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 captureManualPhoto() {
|
// Show preview
|
||||||
const video = document.getElementById('manPhotoVideo');
|
const url = URL.createObjectURL(file);
|
||||||
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);
|
|
||||||
document.getElementById('manPhotoPreview').src = url;
|
document.getElementById('manPhotoPreview').src = url;
|
||||||
document.getElementById('manPhotoPreview').style.display = 'block';
|
document.getElementById('manPhotoPreview').style.display = 'block';
|
||||||
document.getElementById('manPhotoRetake').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() {
|
function retakeManualPhoto() {
|
||||||
|
manualPhotoFile = null;
|
||||||
manualPhotoBlob = null;
|
manualPhotoBlob = null;
|
||||||
|
manualPhotoGps = null;
|
||||||
document.getElementById('manPhotoPreview').style.display = 'none';
|
document.getElementById('manPhotoPreview').style.display = 'none';
|
||||||
document.getElementById('manPhotoRetake').style.display = 'none';
|
document.getElementById('manPhotoRetake').style.display = 'none';
|
||||||
startManualPhoto();
|
document.getElementById('manPhotoPlaceholder').style.display = '';
|
||||||
}
|
document.getElementById('manPhotoInput').value = '';
|
||||||
|
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -2536,12 +2521,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let photoPath = null;
|
let photoPath = null;
|
||||||
if (manualPhotoBlob) {
|
if (manualPhotoFile) {
|
||||||
try {
|
try {
|
||||||
const fd = new FormData();
|
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 });
|
const upData = await api('/api/upload/photo', { method: 'POST', body: fd });
|
||||||
photoPath = upData.path;
|
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 */ }
|
} 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
|
async function extractGpsFromPhoto(file) {
|
||||||
const _origCaptureManualPhoto = captureManualPhoto;
|
if (!file) return null;
|
||||||
captureManualPhoto = async function() {
|
|
||||||
await _origCaptureManualPhoto();
|
|
||||||
// After capture, try EXIF GPS from the blob
|
|
||||||
if (manualPhotoBlob) {
|
|
||||||
try {
|
try {
|
||||||
const gps = await extractGpsFromPhoto(manualPhotoBlob);
|
const gps = await exifr.gps(file);
|
||||||
if (gps) {
|
return gps || null;
|
||||||
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!');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// GPS extraction failure is non-critical
|
console.warn('EXIF GPS extraction failed:', e.message);
|
||||||
console.warn('EXIF GPS from capture 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
|
// Boot auth at page load
|
||||||
document.addEventListener('DOMContentLoaded', () => { initAuth(); });
|
document.addEventListener('DOMContentLoaded', () => { initAuth(); });
|
||||||
|
|||||||
Reference in New Issue
Block a user