oedb-backend/oedb/resources/demo/static/traffic.js

721 lines
28 KiB
JavaScript
Raw Normal View History

2025-09-22 11:44:25 +02:00
// 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;
2025-09-26 11:57:54 +02:00
// Fonction pour créer un marqueur personnalisé avec emoji
function createCustomMarker(emoji, backgroundColor) {
const markerElement = document.createElement('div');
markerElement.className = 'custom-marker';
markerElement.style.width = '30px';
markerElement.style.height = '30px';
markerElement.style.borderRadius = '50%';
markerElement.style.backgroundColor = backgroundColor;
markerElement.style.display = 'flex';
markerElement.style.justifyContent = 'center';
markerElement.style.alignItems = 'center';
markerElement.style.fontSize = '16px';
markerElement.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
markerElement.innerHTML = emoji;
return markerElement;
}
2025-09-22 11:44:25 +02:00
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',
2025-09-23 11:26:44 +02:00
style: 'https://tiles.openfreemap.org/styles/liberty',
2025-09-22 11:44:25 +02:00
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);
2025-09-26 11:57:54 +02:00
let markerColor = needsRealityCheck ? '#ff9800' : '#888888';
let markerOptions = { color: markerColor };
// Check if event title contains "vélo" or "travaux"
const eventTitle = event.properties.label || '';
if (eventTitle.toLowerCase().includes('vélo')) {
markerOptions = {
element: createCustomMarker('🚲', markerColor)
};
} else if (eventTitle.toLowerCase().includes('travaux')) {
markerOptions = {
element: createCustomMarker('🚧', markerColor)
};
}
const em = new maplibregl.Marker(markerOptions).setLngLat(coords).addTo(map);
2025-09-22 11:44:25 +02:00
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>`;
}
2025-09-26 11:57:54 +02:00
// Initialize collapsible panels
function initCollapsiblePanels() {
const headers = document.querySelectorAll('.collapsible-header');
headers.forEach(header => {
header.addEventListener('click', function() {
this.classList.toggle('active');
const content = this.nextElementSibling;
content.classList.toggle('active');
});
});
}
2025-09-22 11:44:25 +02:00
document.addEventListener('DOMContentLoaded', function() {
setDefaultDates();
initTabs();
initMap();
updateUserInfoDisplay();
2025-09-26 11:57:54 +02:00
initCollapsiblePanels();
2025-09-22 11:44:25 +02:00
});
// 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;