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 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 ;