add panoramax token

This commit is contained in:
Tykayn 2025-09-22 11:44:25 +02:00 committed by tykayn
parent f66e5e3f7b
commit 1a3df2ed75
12 changed files with 1132 additions and 61 deletions

View file

@ -8,16 +8,13 @@
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
<link rel="stylesheet" href="/static/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<link rel="stylesheet" href="/static/traffic.css">
<script src="/static/demo_auth.js"></script>
<script src="/static/traffic.js" defer></script>
</head>
<body>
<div class="container">
<div class="nav-links">
<a href="/demo">&larr; Back to Map</a>
<a href="/">API Information</a>
<a href="/event">View Events</a>
<a href="/demo/view-events">View Saved Events</a>
</div>
{% include 'partials/demo_nav.html' %}
<h1>Report Road Issue</h1>
@ -25,6 +22,9 @@
<input type="hidden" id="osmClientId" value="{{ client_id }}">
<input type="hidden" id="osmClientSecret" value="{{ client_secret }}">
<input type="hidden" id="osmRedirectUri" value="{{ client_redirect }}">
<!-- Hidden Panoramax configuration (upload endpoint and token) -->
<input type="hidden" id="panoramaxUploadUrl" value="{{ panoramax_upload_url }}">
<input type="hidden" id="panoramaxToken" value="{{ panoramax_token }}">
<!-- Authentication section will be rendered by JavaScript or server-side -->
<div id="auth-section">
@ -46,17 +46,15 @@
{% endif %}
<script>
// Replace server-side auth section with JavaScript-rendered version if available
// Préserve l'affichage serveur si présent, sinon laisse traffic.js/dema_auth gérer
document.addEventListener('DOMContentLoaded', function() {
if (window.osmAuth) {
const hasServerAuth = document.getElementById('osmUsername') && document.getElementById('osmUsername').value;
if (hasServerAuth) return;
if (window.osmAuth && osmAuth.renderAuthSection) {
const clientId = document.getElementById('osmClientId').value;
const redirectUri = document.getElementById('osmRedirectUri').value;
const authSection = document.getElementById('auth-section');
// Only replace if osmAuth is loaded and has renderAuthSection method
if (osmAuth.renderAuthSection) {
authSection.innerHTML = osmAuth.renderAuthSection(clientId, redirectUri);
}
authSection.innerHTML = osmAuth.renderAuthSection(clientId, redirectUri);
}
});
</script>
@ -67,16 +65,19 @@
<!-- Tab Navigation -->
<div class="tabs">
<div class="tab-item active" data-tab="road">
<i class="fas fa-road"></i> Road
<i class="fas fa-road"></i> Route
</div>
<div class="tab-item" data-tab="rail">
<i class="fas fa-train"></i> Rail
</div>
<div class="tab-item" data-tab="weather">
<i class="fas fa-cloud-sun-rain"></i> Weather
<i class="fas fa-cloud-sun-rain"></i> Météo
</div>
<div class="tab-item" data-tab="emergency">
<i class="fas fa-exclamation-circle"></i> Emergency
<i class="fas fa-exclamation-circle"></i> Urgences
</div>
<div class="tab-item" data-tab="civic">
<i class="fas fa-bicycle"></i> Cycles
</div>
</div>
@ -95,7 +96,7 @@
</div>
<div class="issue-button road vehicle" onclick="fillForm('vehicle')">
<i class="fas fa-car"></i>
Vehicle on Side
Véhicule sur le bas côté de la route
</div>
<div class="issue-button road danger" onclick="fillForm('danger')">
<i class="fas fa-skull-crossbones"></i>
@ -107,15 +108,15 @@
</div>
<div class="issue-button road flooded-road" onclick="fillForm('flooded_road')">
<i class="fas fa-water"></i>
Flooded Road
Route inondée
</div>
<div class="issue-button road roadwork" onclick="fillForm('roadwork')">
<i class="fas fa-hard-hat"></i>
Roadwork
Travaux
</div>
<div class="issue-button road black-traffic" onclick="fillForm('black_traffic')">
<i class="fas fa-traffic-light"></i>
Black Traffic
journée noire bison futé
</div>
</div>
</div>
@ -125,15 +126,15 @@
<div class="issue-buttons">
<div class="issue-button rail unattended-luggage" onclick="fillForm('unattended_luggage')">
<i class="fas fa-suitcase"></i>
Unattended Luggage
Bagage abandonné
</div>
<div class="issue-button rail transport-delay" onclick="fillForm('transport_delay')">
<i class="fas fa-hourglass-half"></i>
Delay
Retard
</div>
<div class="issue-button rail major-delay" onclick="fillForm('major_transport_delay')">
<i class="fas fa-hourglass-end"></i>
Major Delay
Retard important
</div>
</div>
</div>
@ -143,15 +144,15 @@
<div class="issue-buttons">
<div class="issue-button weather flood-danger" onclick="fillForm('flood_danger')">
<i class="fas fa-water"></i>
Flood Alert
Vigilance rouge inondation
</div>
<div class="issue-button weather thunderstorm" onclick="fillForm('thunderstorm_alert')">
<i class="fas fa-bolt"></i>
Thunderstorm
Vigilance orange orages
</div>
<div class="issue-button weather fog" onclick="fillForm('fog_warning')">
<i class="fas fa-smog"></i>
Fog Warning
Vigilance jaune brouillard
</div>
</div>
</div>
@ -161,11 +162,25 @@
<div class="issue-buttons">
<div class="issue-button emergency emergency-alert" onclick="fillForm('emergency_alert')">
<i class="fas fa-exclamation-circle"></i>
Emergency Alert
Alerte d'urgence (SAIP)
</div>
<div class="issue-button emergency daylight-saving" onclick="fillForm('daylight_saving')">
<i class="fas fa-clock"></i>
Daylight Saving
Période d'heure d'été
</div>
</div>
</div>
<!-- Civic Tab -->
<div class="tab-pane" id="civic-tab">
<div class="issue-buttons">
<div class="issue-button civic bike-obstacle" onclick="fillForm('bike_obstacle')">
<i class="fas fa-bicycle"></i>
Obstacle vélo
</div>
<div class="issue-button civic illegal-dumping" onclick="fillForm('illegal_dumping')">
<i class="fas fa-trash"></i>
Décharge sauvage
</div>
</div>
</div>
@ -173,12 +188,45 @@
<button id="geolocateBtn" class="geolocation-btn">
<span id="geolocateSpinner" class="loading" style="display: none;"></span>
Get My Current Location
Obtenir ma position actuelle
</button>
<span id="gpsStatus" class="gps-status" title="État GPS">GPS: inconnu</span>
<form id="trafficForm">
<div class="form-group">
<label for="label" class="required">Issue Description</label>
<label for="photo">Photo (optionnelle)</label>
<input type="file" id="photo" name="photo" accept="image/*" capture="environment">
<div class="note">Prenez une photo géolocalisée de la situation (mobile recommandé)</div>
<div id="photoPreviewContainer" style="margin-top:8px; display:none;">
<img id="photoPreview" alt="Aperçu photo" style="max-width:100%; border-radius:4px;"/>
</div>
<div class="form-row" style="margin-top:8px;">
<div class="form-group">
<label for="panoramaxTokenInput">Token Panoramax</label>
<input type="password" id="panoramaxTokenInput" placeholder="Jeton d'API Panoramax">
<div class="note">Stocké en local sur cet appareil. Utilisé pour envoyer la photo.</div>
</div>
<div class="form-group" style="align-self:flex-end;">
<button type="button" id="savePanoramaxTokenBtn">Enregistrer le token</button>
<button type="button" id="showPanoramaxTokenBtn" style="display:none;">Modifier le token</button>
</div>
</div>
<div class="camera-block">
<label>Prendre une photo avec la caméra</label>
<div class="camera-controls">
<button type="button" id="startCameraBtn">Démarrer la caméra</button>
<button type="button" id="capturePhotoBtn" disabled>Prendre la photo</button>
<button type="button" id="stopCameraBtn" disabled>Arrêter</button>
</div>
<div class="camera-preview">
<video id="cameraVideo" autoplay playsinline muted></video>
<canvas id="cameraCanvas" style="display:none;"></canvas>
</div>
<div class="note">La photo capturée sera ajoutée au champ ci-dessus.</div>
</div>
</div>
<div class="form-group">
<label for="label" class="required">Description du problème</label>
<input type="text" id="label" name="label" placeholder="e.g., Large pothole on Highway A1" required>
</div>
@ -186,50 +234,50 @@
<div class="form-row">
<div class="form-group">
<label for="severity" class="required">Severity</label>
<label for="severity" class="required">Gravité</label>
<select id="severity" name="severity" required>
<option value="low">Low (Minor issue)</option>
<option value="medium" selected>Medium (Moderate issue)</option>
<option value="high">High (Severe issue)</option>
<option value="low">Faible (Problème mineur)</option>
<option value="medium" selected>Moyen (Problème modéré)</option>
<option value="high">Élevé (Problème grave)</option>
</select>
</div>
<div class="form-group">
<label for="cause">Additional Details</label>
<label for="cause">Détails supplémentaires</label>
<input type="text" id="cause" name="cause" placeholder="e.g., Size, specific location details">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start" class="required">Report Time</label>
<label for="start" class="required">Heure de début</label>
<input type="datetime-local" id="start" name="start" required value="">
</div>
<div class="form-group">
<label for="stop" class="required">Estimated Clear Time</label>
<label for="stop" class="required">Heure estimée de fin</label>
<input type="datetime-local" id="stop" name="stop" required value="">
</div>
</div>
<div class="form-group">
<label for="where">Road/Location Name</label>
<label for="where">Route/Nom du lieu</label>
<input type="text" id="where" name="where" placeholder="e.g., Highway A1, Main Street">
</div>
<div class="form-group">
<label class="required">Location</label>
<div id="map"></div>
<div class="note">Click on the map to set the issue location or use the "Get My Current Location" button</div>
<div class="note">Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"</div>
</div>
<button id="report_issue_button" type="submit" disabled>Report Issue</button>
<button id="report_issue_button" type="submit" disabled>Signaler le problème</button>
</form>
<div id="result"></div>
<a href="/demo/view-events" class="view-saved-events">
<i class="fas fa-map-marked-alt"></i> View All Saved Events on Map
<i class="fas fa-map-marked-alt"></i> Voir tous les événements enregistrés sur la carte
</a>
</div>
@ -242,9 +290,9 @@
// Set start time to current time
document.getElementById('start').value = nowISO;
// Set end time to current time + 1 hour
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
document.getElementById('stop').value = oneHourLater.toISOString().slice(0, 16);
// Set end time to current time + 6 hours (durée par défaut des signalements)
const sixHoursLater = new Date(now.getTime() + 6 * 60 * 60 * 1000);
document.getElementById('stop').value = sixHoursLater.toISOString().slice(0, 16);
}
// Call function to set default dates
@ -442,6 +490,18 @@
let markerColor = '#ff3860'; // Default red color
switch(issueType) {
case 'bike_obstacle':
labelInput.value = 'Obstacle vélo';
issueTypeInput.value = 'mobility.cycling.obstacle';
severitySelect.value = 'medium';
markerColor = '#388e3c'; // Green
break;
case 'illegal_dumping':
labelInput.value = 'Décharge sauvage';
issueTypeInput.value = 'environment.dumping.illegal';
severitySelect.value = 'medium';
markerColor = '#795548'; // Brown
break;
case 'pothole':
labelInput.value = 'Nid de poule';
issueTypeInput.value = 'traffic.hazard.pothole';
@ -721,8 +781,64 @@
setTimeout(validateForm, 100);
});
// Photo preview
const photoInput = document.getElementById('photo');
if (photoInput) {
photoInput.addEventListener('change', function() {
const file = this.files && this.files[0];
if (!file) {
document.getElementById('photoPreviewContainer').style.display = 'none';
return;
}
const url = URL.createObjectURL(file);
const img = document.getElementById('photoPreview');
img.src = url;
document.getElementById('photoPreviewContainer').style.display = 'block';
});
}
async function uploadPhotoIfConfigured(file, lng, lat, isoDatetime) {
try {
const uploadUrl = document.getElementById('panoramaxUploadUrl')?.value || '';
const token = document.getElementById('panoramaxToken')?.value || '';
if (!uploadUrl || !file) {
return null; // pas configuré ou pas de fichier
}
const form = new FormData();
form.append('file', file, file.name || 'photo.jpg');
// Métadonnées géo/temps standard
if (typeof lng === 'number' && typeof lat === 'number') {
form.append('lon', String(lng));
form.append('lat', String(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) {
const t = await res.text();
throw new Error(t || `Upload failed (${res.status})`);
}
const data = await res.json().catch(() => ({}));
// On essaie de normaliser quelques champs courants
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;
}
}
// Handle form submission
document.getElementById('trafficForm').addEventListener('submit', function(e) {
document.getElementById('trafficForm').addEventListener('submit', async function(e) {
e.preventDefault();
// Validate form before submission
@ -795,6 +911,18 @@
console.log(`Including OSM username in report: ${osmUsernameValue}`);
}
// Upload photo to Panoramax si configuré
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);
if (photoInfo.url) event.properties['photo:url'] = photoInfo.url;
}
}
// Save event to localStorage
saveEventToLocalStorage(event);