// Logique JavaScript spécifique à /demo/traffic // Variables globales faibles (map, marker) let map; let marker; let existingMarkers = []; const PANORAMAX_TOKEN_STORAGE_KEY = 'oedb_panoramax_token'; let mediaStream = null; function setDefaultDates() { const now = new Date(); const nowISO = now.toISOString().slice(0, 16); document.getElementById('start').value = nowISO; const sixHoursLater = new Date(now.getTime() + 6 * 60 * 60 * 1000); document.getElementById('stop').value = sixHoursLater.toISOString().slice(0, 16); } function initTabs() { const tabItems = document.querySelectorAll('.tab-item'); tabItems.forEach(tab => { tab.addEventListener('click', function() { tabItems.forEach(item => item.classList.remove('active')); this.classList.add('active'); const tabName = this.getAttribute('data-tab'); document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active')); document.getElementById(tabName + '-tab').classList.add('active'); localStorage.setItem('activeTab', tabName); }); }); const activeTab = localStorage.getItem('activeTab'); if (activeTab) { const tabItem = document.querySelector(`.tab-item[data-tab="${activeTab}"]`); if (tabItem) tabItem.click(); } } function initMap() { map = new maplibregl.Map({ container: 'map', style: 'https://tiles.openfreemap.org/styles/liberty', center: [2.2137, 46.2276], zoom: 5 }); map.addControl(new maplibregl.NavigationControl()); marker = new maplibregl.Marker({ draggable: true, color: '#ff3860' }); map.on('load', fetchExistingTrafficEvents); map.on('click', function(e) { marker.setLngLat(e.lngLat).addTo(map); setTimeout(validateForm, 100); }); } function setGpsStatus(text, ok = null) { const el = document.getElementById('gpsStatus'); if (!el) return; el.textContent = `GPS: ${text}`; if (ok === true) { el.style.color = '#2e7d32'; } else if (ok === false) { el.style.color = '#c62828'; } else { el.style.color = '#555'; } } function fetchExistingTrafficEvents() { existingMarkers.forEach(m => m.remove()); existingMarkers = []; fetch('https://api.openeventdatabase.org/event?what=traffic') .then(r => { if (!r.ok) throw new Error('Failed to fetch existing traffic events'); return r.json(); }) .then(data => { if (!data || !Array.isArray(data.features)) return; data.features.forEach(event => { if (event.geometry && event.geometry.type === 'Point') { const coords = event.geometry.coordinates; const needsRealityCheck = checkIfNeedsRealityCheck(event); const markerColor = needsRealityCheck ? '#ff9800' : '#888888'; const em = new maplibregl.Marker({ color: markerColor }).setLngLat(coords).addTo(map); let popupContent = `\n

${event.properties.label || 'Traffic Event'}

\n

Type: ${event.properties.what || 'Unknown'}

\n

Start: ${event.properties.start || 'Unknown'}

\n

End: ${event.properties.stop || 'Unknown'}

`; if (needsRealityCheck) { popupContent += `\n
\n

Is this traffic event still present?

\n
\n\n\n
\n
`; } else if (event.properties['reality_check']) { popupContent += `\n
\n

Reality check: ${event.properties['reality_check']}

\n
`; } em.setPopup(new maplibregl.Popup({ offset: 25 }).setHTML(popupContent)); existingMarkers.push(em); } }); }); } function checkIfNeedsRealityCheck(event) { if (event.properties['reality_check']) return false; if (!event.properties.what || !event.properties.what.startsWith('traffic')) return false; const createDate = event.properties.createdate; if (!createDate) return false; const createTime = new Date(createDate).getTime(); const currentTime = new Date().getTime(); return (currentTime - createTime) > (60 * 60 * 1000); } function fillForm(issueType) { const labelInput = document.getElementById('label'); const issueTypeInput = document.getElementById('issueType'); const severitySelect = document.getElementById('severity'); let currentLngLat = marker.getLngLat ? marker.getLngLat() : null; marker.remove(); let markerColor = '#ff3860'; switch(issueType) { case 'bike_obstacle': labelInput.value = 'Obstacle vélo'; issueTypeInput.value = 'mobility.cycling.obstacle'; severitySelect.value = 'medium'; markerColor = '#388e3c'; break; case 'illegal_dumping': labelInput.value = 'Décharge sauvage'; issueTypeInput.value = 'environment.dumping.illegal'; severitySelect.value = 'medium'; markerColor = '#795548'; break; case 'pothole': labelInput.value = 'Nid de poule'; issueTypeInput.value = 'traffic.hazard.pothole'; severitySelect.value = 'medium'; markerColor = '#ff9800'; break; case 'obstacle': labelInput.value = 'Obstacle'; issueTypeInput.value = 'traffic.hazard.obstacle'; severitySelect.value = 'high'; markerColor = '#f44336'; break; case 'vehicle': labelInput.value = 'Véhicule sur le bas côté de la route'; issueTypeInput.value = 'traffic.hazard.vehicle'; severitySelect.value = 'low'; markerColor = '#2196f3'; break; case 'danger': labelInput.value = 'Danger non classé'; issueTypeInput.value = 'traffic.hazard.danger'; severitySelect.value = 'high'; markerColor = '#9c27b0'; break; case 'emergency_alert': labelInput.value = "Alerte d'urgence (SAIP)"; issueTypeInput.value = 'alert.emergency'; severitySelect.value = 'high'; markerColor = '#e91e63'; break; case 'daylight_saving': labelInput.value = "Période d'heure d'été"; issueTypeInput.value = 'time.daylight.summer'; severitySelect.value = 'low'; markerColor = '#ffc107'; break; case 'accident': labelInput.value = 'Accident de la route'; issueTypeInput.value = 'traffic.accident'; severitySelect.value = 'high'; markerColor = '#d32f2f'; break; case 'flooded_road': labelInput.value = 'Route inondée'; issueTypeInput.value = 'traffic.closed.flood'; severitySelect.value = 'high'; markerColor = '#1976d2'; break; case 'black_traffic': labelInput.value = 'Période noire bison futé vers la province'; issueTypeInput.value = 'traffic.forecast.black.out'; severitySelect.value = 'high'; markerColor = '#212121'; break; case 'roadwork': labelInput.value = 'Travaux'; issueTypeInput.value = 'traffic.roadwork'; severitySelect.value = 'medium'; markerColor = '#ff5722'; break; case 'flood_danger': labelInput.value = 'Vigilance rouge inondation'; issueTypeInput.value = 'weather.danger.flood'; severitySelect.value = 'high'; markerColor = '#b71c1c'; break; case 'thunderstorm_alert': labelInput.value = 'Vigilance orange orages'; issueTypeInput.value = 'weather.alert.thunderstorm'; severitySelect.value = 'medium'; markerColor = '#ff9800'; break; case 'fog_warning': labelInput.value = 'Vigilance jaune brouillard'; issueTypeInput.value = 'weather.warning.fog'; severitySelect.value = 'low'; markerColor = '#ffeb3b'; break; case 'unattended_luggage': labelInput.value = 'Bagage abandonné'; issueTypeInput.value = 'public_transport.incident.unattended_luggage'; severitySelect.value = 'medium'; markerColor = '#673ab7'; break; case 'transport_delay': labelInput.value = 'Retard'; issueTypeInput.value = 'public_transport.delay'; severitySelect.value = 'low'; markerColor = '#ffc107'; break; case 'major_transport_delay': labelInput.value = 'Retard important'; issueTypeInput.value = 'public_transport.delay.major'; severitySelect.value = 'medium'; markerColor = '#ff5722'; break; default: labelInput.value = 'Bouchon'; issueTypeInput.value = 'traffic.jam'; severitySelect.value = 'medium'; markerColor = '#ff3860'; } marker = new maplibregl.Marker({ draggable: true, color: markerColor }); if (currentLngLat) marker.setLngLat(currentLngLat).addTo(map); validateForm(); } document.getElementById('geolocateBtn').addEventListener('click', function() { document.getElementById('geolocateSpinner').style.display = 'inline-block'; this.disabled = true; if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function(position) { const lat = position.coords.latitude; const lng = position.coords.longitude; marker.setLngLat([lng, lat]).addTo(map); map.flyTo({ center: [lng, lat], zoom: 14 }); document.getElementById('geolocateSpinner').style.display = 'none'; document.getElementById('geolocateBtn').disabled = false; showResult('Current location detected successfully', 'success'); setGpsStatus('actif', true); validateForm(); fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`) .then(r => r.json()) .then(data => { if (data && data.address) { let location = ''; if (data.address.road) { location = data.address.road; if (data.address.city) location += `, ${data.address.city}`; } else if (data.address.suburb) { location = data.address.suburb; if (data.address.city) location += `, ${data.address.city}`; } if (location) document.getElementById('where').value = location; } }); }, function(error) { document.getElementById('geolocateSpinner').style.display = 'none'; document.getElementById('geolocateBtn').disabled = false; let msg = 'Unable to get your location. '; switch(error.code) { case error.PERMISSION_DENIED: msg += 'You denied the request for geolocation.'; break; case error.POSITION_UNAVAILABLE: msg += 'Location information is unavailable.'; break; case error.TIMEOUT: msg += 'The request to get your location timed out.'; break; default: msg += 'An unknown error occurred.'; } showResult(msg, 'error'); setGpsStatus('inactif', false); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }); } else { document.getElementById('geolocateSpinner').style.display = 'none'; document.getElementById('geolocateBtn').disabled = false; showResult('Geolocation is not supported by your browser', 'error'); setGpsStatus('non supporté', false); } }); function validateForm() { const requiredFields = document.querySelectorAll('#trafficForm [required]'); const submitButton = document.getElementById('report_issue_button'); let isValid = true; requiredFields.forEach(field => { if (!field.value.trim()) isValid = false; }); if (!marker || !marker.getLngLat()) isValid = false; submitButton.disabled = !isValid; if (isValid) submitButton.classList.remove('disabled'); else submitButton.classList.add('disabled'); return isValid; } document.addEventListener('DOMContentLoaded', function() { const formFields = document.querySelectorAll('#trafficForm input, #trafficForm select'); formFields.forEach(f => { f.addEventListener('input', validateForm); f.addEventListener('change', validateForm); }); validateForm(); // Charger token panoramax depuis localStorage const stored = localStorage.getItem(PANORAMAX_TOKEN_STORAGE_KEY); if (stored) { const input = document.getElementById('panoramaxTokenInput'); if (input) { input.value = stored; input.style.display = 'none'; const label = document.querySelector("label[for='panoramaxTokenInput']"); if (label) label.style.display = 'none'; } const saveBtn = document.getElementById('savePanoramaxTokenBtn'); const showBtn = document.getElementById('showPanoramaxTokenBtn'); if (saveBtn) saveBtn.style.display = 'none'; if (showBtn) showBtn.style.display = ''; } const saveBtn = document.getElementById('savePanoramaxTokenBtn'); if (saveBtn) { saveBtn.addEventListener('click', function() { const val = document.getElementById('panoramaxTokenInput')?.value || ''; if (val) { localStorage.setItem(PANORAMAX_TOKEN_STORAGE_KEY, val); showResult('Token Panoramax enregistré localement', 'success'); // Masquer champ + bouton save, afficher bouton show const input = document.getElementById('panoramaxTokenInput'); if (input) input.style.display = 'none'; const label = document.querySelector("label[for='panoramaxTokenInput']"); if (label) label.style.display = 'none'; saveBtn.style.display = 'none'; const showBtn = document.getElementById('showPanoramaxTokenBtn'); if (showBtn) showBtn.style.display = ''; } else { localStorage.removeItem(PANORAMAX_TOKEN_STORAGE_KEY); showResult('Token Panoramax supprimé du stockage local', 'success'); } }); } const showBtn = document.getElementById('showPanoramaxTokenBtn'); if (showBtn) { showBtn.addEventListener('click', function() { const input = document.getElementById('panoramaxTokenInput'); const label = document.querySelector("label[for='panoramaxTokenInput']"); if (input) input.style.display = ''; if (label) label.style.display = ''; const saveBtn = document.getElementById('savePanoramaxTokenBtn'); if (saveBtn) saveBtn.style.display = ''; showBtn.style.display = 'none'; }); } // État GPS initial setGpsStatus('inconnu'); }); // Aperçu photo const photoInput = document.getElementById('photo'); if (photoInput) { photoInput.addEventListener('change', function() { const file = this.files && this.files[0]; const ctn = document.getElementById('photoPreviewContainer'); if (!file) { ctn.style.display = 'none'; return; } const url = URL.createObjectURL(file); const img = document.getElementById('photoPreview'); img.src = url; ctn.style.display = 'block'; }); } async function readExifGps(file) { // Lecture minimale EXIF pour récupérer GPSLatitude/GPSLongitude si présents try { const buffer = await file.arrayBuffer(); const view = new DataView(buffer); // Vérifier JPEG if (view.getUint16(0, false) !== 0xFFD8) return null; let offset = 2; const length = view.byteLength; while (offset < length) { if (view.getUint16(offset, false) === 0xFFE1) { // APP1 const app1Len = view.getUint16(offset + 2, false); // "Exif\0\0" if (view.getUint32(offset + 4, false) === 0x45786966 && view.getUint16(offset + 8, false) === 0x0000) { let tiffOffset = offset + 10; const little = view.getUint16(tiffOffset, false) === 0x4949; // 'II' const getU16 = (pos) => view.getUint16(pos, little); const getU32 = (pos) => view.getUint32(pos, little); const firstIFDOffset = getU32(tiffOffset + 4) + tiffOffset; // Parcourir 0th IFD pour trouver GPS IFD pointer (tag 0x8825) const entries = getU16(firstIFDOffset); let gpsIFDPointer = 0; for (let i = 0; i < entries; i++) { const entryOffset = firstIFDOffset + 2 + i * 12; const tag = getU16(entryOffset); if (tag === 0x8825) { // GPSInfoIFDPointer gpsIFDPointer = getU32(entryOffset + 8) + tiffOffset; break; } } if (!gpsIFDPointer) return null; const gpsCount = getU16(gpsIFDPointer); let latRef = 'N', lonRef = 'E'; let latVals = null, lonVals = null; const readRational = (pos) => { const num = getU32(pos); const den = getU32(pos + 4); return den ? (num / den) : 0; }; for (let i = 0; i < gpsCount; i++) { const eOff = gpsIFDPointer + 2 + i * 12; const tag = getU16(eOff); const type = getU16(eOff + 2); const count = getU32(eOff + 4); let valueOffset = eOff + 8; let valuePtr = getU32(valueOffset) + tiffOffset; if (tag === 0x0001) { // GPSLatitudeRef const c = view.getUint8(valueOffset); latRef = String.fromCharCode(c); } else if (tag === 0x0002 && type === 5 && count === 3) { // GPSLatitude latVals = [readRational(valuePtr), readRational(valuePtr + 8), readRational(valuePtr + 16)]; } else if (tag === 0x0003) { // GPSLongitudeRef const c = view.getUint8(valueOffset); lonRef = String.fromCharCode(c); } else if (tag === 0x0004 && type === 5 && count === 3) { // GPSLongitude lonVals = [readRational(valuePtr), readRational(valuePtr + 8), readRational(valuePtr + 16)]; } } if (!latVals || !lonVals) return null; const toDecimal = (dms) => dms[0] + dms[1] / 60 + dms[2] / 3600; let lat = toDecimal(latVals); let lng = toDecimal(lonVals); if (latRef === 'S') lat = -lat; if (lonRef === 'W') lng = -lng; return { lat, lng }; } offset += 2 + app1Len; } else if ((view.getUint16(offset, false) & 0xFFF0) === 0xFFE0) { const segLen = view.getUint16(offset + 2, false); offset += 2 + segLen; } else { break; } } return null; } catch (e) { return null; } } async function uploadPhotoIfConfigured(file, lng, lat, isoDatetime) { try { const uploadUrl = document.getElementById('panoramaxUploadUrl')?.value || ''; // Priorité au token utilisateur (input/localStorage), sinon fallback hidden server const token = (document.getElementById('panoramaxTokenInput')?.value || localStorage.getItem(PANORAMAX_TOKEN_STORAGE_KEY) || document.getElementById('panoramaxToken')?.value || ''); if (!uploadUrl || !file) return null; // Exiger EXIF GPS const exifLoc = await readExifGps(file); if (!exifLoc) { showResult("La photo n'a pas de géolocalisation EXIF, envoi Panoramax interdit.", 'error'); return null; } const form = new FormData(); form.append('file', file, file.name || 'photo.jpg'); // Utiliser la géolocalisation EXIF uniquement form.append('lon', String(exifLoc.lng)); form.append('lat', String(exifLoc.lat)); if (isoDatetime) form.append('datetime', isoDatetime); const headers = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(uploadUrl, { method: 'POST', headers, body: form }); if (!res.ok) { throw new Error(await res.text() || `Upload failed (${res.status})`); } const data = await res.json().catch(() => ({})); return { id: data.id || data.uuid || data.photo_id || null, url: data.url || data.permalink || data.link || null, raw: data }; } catch (err) { console.error('Panoramax upload error:', err); showResult(`Erreur upload photo: ${err.message}`, 'error'); return null; } } document.getElementById('trafficForm').addEventListener('submit', async function(e) { e.preventDefault(); if (!validateForm()) { showResult('Please fill in all required fields and set a location on the map', 'error'); return; } const label = document.getElementById('label').value; const issueType = document.getElementById('issueType').value; const severity = document.getElementById('severity').value; const cause = document.getElementById('cause').value; const start = document.getElementById('start').value; const stop = document.getElementById('stop').value; const where = document.getElementById('where').value; const lngLat = marker.getLngLat(); const event = { type: 'Feature', geometry: { type: 'Point', coordinates: [lngLat.lng, lngLat.lat] }, properties: { label, type: 'unscheduled', what: issueType, 'issue:severity': severity, start, stop } }; if (cause) event.properties['issue:details'] = cause; if (where) event.properties.where = where; let osmUsernameValue = ''; const osmUsername = document.getElementById('osmUsername'); if (osmUsername && osmUsername.value) osmUsernameValue = osmUsername.value; if (window.osmAuth && osmAuth.isUserAuthenticated()) osmUsernameValue = osmAuth.getUsername(); if (osmUsernameValue) event.properties['reporter:osm'] = osmUsernameValue; let photoInfo = null; const photoFile = (photoInput && photoInput.files && photoInput.files[0]) ? photoInput.files[0] : null; if (photoFile) { photoInfo = await uploadPhotoIfConfigured(photoFile, lngLat.lng, lngLat.lat, start); if (photoInfo) { event.properties['photo:service'] = 'panoramax'; if (photoInfo.id) { event.properties['photo:id'] = String(photoInfo.id); // Tag panoramax (uuid) event.properties['panoramax'] = String(photoInfo.id); } if (photoInfo.url) event.properties['photo:url'] = photoInfo.url; } } saveEventToLocalStorage(event); fetch('https://api.openeventdatabase.org/event', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }) .then(response => { if (response.ok) return response.json(); return response.text().then(text => { throw new Error(text || response.statusText); }); }) .then(data => { if (data.id) updateEventInLocalStorage(event, data.id); showResult(`Issue reported successfully with ID: ${data.id}`, 'success'); const resultElement = document.getElementById('result'); resultElement.innerHTML += `\n

\nView Report on Server |\nView Saved Reports |\nBack to Map\n

`; document.getElementById('trafficForm').reset(); setDefaultDates(); marker.remove(); fetchExistingTrafficEvents(); }) .catch(error => { showResult(`Error reporting issue: ${error.message}`, 'error'); }); }); function saveEventToLocalStorage(event) { let savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]'); event.timestamp = new Date().toISOString(); savedEvents.push(event); localStorage.setItem('oedb_events', JSON.stringify(savedEvents)); } function updateEventInLocalStorage(event, serverId) { let savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]'); const eventIndex = savedEvents.findIndex(e => e.timestamp === event.timestamp && e.geometry.coordinates[0] === event.geometry.coordinates[0] && e.geometry.coordinates[1] === event.geometry.coordinates[1]); if (eventIndex !== -1) { savedEvents[eventIndex].properties.id = serverId; localStorage.setItem('oedb_events', JSON.stringify(savedEvents)); } } function showResult(message, type) { const resultElement = document.getElementById('result'); resultElement.textContent = message; resultElement.className = type; resultElement.style.display = 'block'; resultElement.scrollIntoView({ behavior: 'smooth' }); } function confirmEvent(eventId, isConfirmed) { let username = localStorage.getItem('oedb_username'); if (!username) { username = promptForUsername(); if (!username) return; } const now = new Date(); const dateTimeString = now.toISOString(); const realityCheckStatus = isConfirmed ? 'confirmed' : 'not confirmed'; const realityCheckValue = `${dateTimeString} | ${username} | ${realityCheckStatus}`; fetch(`https://api.openeventdatabase.org/event/${eventId}`) .then(r => { if (!r.ok) throw new Error(`Failed to fetch event ${eventId}`); return r.json(); }) .then(event => { event.properties['reality_check'] = realityCheckValue; return fetch(`https://api.openeventdatabase.org/event/${eventId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }); }) .then(r => { if (!r.ok) throw new Error('Failed to update event'); saveContribution(eventId, isConfirmed); awardPoints(3); showResult(`Thank you for your contribution! You've earned 3 points.`, 'success'); updateUserInfoDisplay(); fetchExistingTrafficEvents(); }) .catch(err => { console.error('Error updating event:', err); showResult(`Error: ${err.message}`, 'error'); }); } function promptForUsername() { const username = prompt('Please enter your username:'); if (username) { localStorage.setItem('oedb_username', username); return username; } return null; } function saveContribution(eventId, isConfirmed) { let contributions = JSON.parse(localStorage.getItem('oedb_contributions') || '[]'); contributions.push({ eventId, timestamp: new Date().toISOString(), isConfirmed }); localStorage.setItem('oedb_contributions', JSON.stringify(contributions)); } function awardPoints(points) { let currentPoints = parseInt(localStorage.getItem('oedb_points') || '0'); currentPoints += points; localStorage.setItem('oedb_points', currentPoints.toString()); } function updateUserInfoDisplay() { const username = localStorage.getItem('oedb_username') || 'Anonymous'; const points = localStorage.getItem('oedb_points') || '0'; let userInfoPanel = document.getElementById('user-info-panel'); if (!userInfoPanel) { userInfoPanel = document.createElement('div'); userInfoPanel.id = 'user-info-panel'; userInfoPanel.className = 'user-info-panel'; const navLinks = document.querySelector('.nav-links'); navLinks.parentNode.insertBefore(userInfoPanel, navLinks.nextSibling); } userInfoPanel.innerHTML = `\n

User Information

\n

Username: ${username}

\n

Points: ${points}

`; } document.addEventListener('DOMContentLoaded', function() { setDefaultDates(); initTabs(); initMap(); updateUserInfoDisplay(); }); // Contrôles Caméra const startCameraBtn = document.getElementById('startCameraBtn'); const capturePhotoBtn = document.getElementById('capturePhotoBtn'); const stopCameraBtn = document.getElementById('stopCameraBtn'); const cameraVideo = document.getElementById('cameraVideo'); const cameraCanvas = document.getElementById('cameraCanvas'); async function startCamera() { try { mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); cameraVideo.srcObject = mediaStream; capturePhotoBtn.disabled = false; stopCameraBtn.disabled = false; startCameraBtn.disabled = true; } catch (e) { showResult(`Impossible d'accéder à la caméra: ${e.message}`, 'error'); } } function stopCamera() { if (mediaStream) { mediaStream.getTracks().forEach(t => t.stop()); mediaStream = null; } cameraVideo.srcObject = null; capturePhotoBtn.disabled = true; stopCameraBtn.disabled = true; startCameraBtn.disabled = false; } function dataURLToFile(dataUrl, filename) { const arr = dataUrl.split(','); const mime = arr[0].match(/:(.*?);/)[1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mime }); } function capturePhoto() { try { const width = cameraVideo.videoWidth; const height = cameraVideo.videoHeight; if (!width || !height) { showResult('Vidéo non prête', 'error'); return; } cameraCanvas.width = width; cameraCanvas.height = height; const ctx = cameraCanvas.getContext('2d'); ctx.drawImage(cameraVideo, 0, 0, width, height); const dataUrl = cameraCanvas.toDataURL('image/jpeg', 0.92); const file = dataURLToFile(dataUrl, 'camera_capture.jpg'); // Remplit le file input pour réutiliser le flux existant const dt = new DataTransfer(); dt.items.add(file); const input = document.getElementById('photo'); input.files = dt.files; // Déclenche l’aperçu const url = URL.createObjectURL(file); const img = document.getElementById('photoPreview'); img.src = url; document.getElementById('photoPreviewContainer').style.display = 'block'; showResult('Photo capturée depuis la caméra', 'success'); } catch (e) { showResult(`Échec capture photo: ${e.message}`, 'error'); } } if (startCameraBtn && capturePhotoBtn && stopCameraBtn) { startCameraBtn.addEventListener('click', startCamera); capturePhotoBtn.addEventListener('click', capturePhoto); stopCameraBtn.addEventListener('click', stopCamera); } // Expose functions used in inline HTML popups window.fillForm = fillForm; window.confirmEvent = confirmEvent;