oedb-backend/oedb/resources/demo/static/traffic.js
2025-09-23 11:26:44 +02:00

676 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<h3>${event.properties.label || 'Traffic Event'}</h3>\n<p>Type: ${event.properties.what || 'Unknown'}</p>\n<p>Start: ${event.properties.start || 'Unknown'}</p>\n<p>End: ${event.properties.stop || 'Unknown'}</p>`;
if (needsRealityCheck) {
popupContent += `\n<div class="reality-check">\n<p>Is this traffic event still present?</p>\n<div class="reality-check-buttons">\n<button class="confirm-btn" onclick="confirmEvent('${event.properties.id}', true)">Yes, still there</button>\n<button class="deny-btn" onclick="confirmEvent('${event.properties.id}', false)">No, it's gone</button>\n</div>\n</div>`;
} else if (event.properties['reality_check']) {
popupContent += `\n<div class="reality-check-info">\n<p>Reality check: ${event.properties['reality_check']}</p>\n</div>`;
}
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<p>\n<a href="https://api.openeventdatabase.org/event/${data.id}" >View Report on Server</a> |\n<a href="/demo/view-events" >View Saved Reports</a> |\n<a href="/demo">Back to Map</a>\n</p>`;
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<h3>User Information</h3>\n<p>Username: <strong>${username}</strong></p>\n<p>Points: <span class="user-points">${points}</span></p>`;
}
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 laperç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;