add live page

This commit is contained in:
Tykayn 2025-09-26 11:57:54 +02:00 committed by tykayn
parent 114bcca24e
commit eb8c42d0c0
19 changed files with 2759 additions and 199 deletions

View file

@ -50,6 +50,8 @@ class DemoMainResource:
<link rel="stylesheet" href="/static/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<script src="/static/demo_auth.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="/static/social.js"></script>
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
.logo{
@ -243,72 +245,43 @@ class DemoMainResource:
<h2>
<img src="/static/oedb.png" class="logo" />
OpenEventDatabase Demo</h2>
<p>This map shows current events from the OpenEventDatabase.</p>
<!-- Event addition buttons - always visible -->
<p><a href="/demo/traffic" class="add-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;">+ Traffic event</a></p>
<p><a href="/demo/add" class="add-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;">+ Any Event</a></p>
<p><a href="/demo/live" class="live-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;"> Live</a></p>
<!-- Collapsible information section -->
<h3 id="info_panel_header" class="collapsible-header">Information Panel <span class="toggle-icon"></span></h3>
<div id="info_panel_content" class="collapsible-content">
<!-- User Information Panel -->
<div id="user-info-panel" class="user-info-panel" style="display: none; background-color: #f5f5f5; border-radius: 4px; padding: 10px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h3 style="margin-top: 0; margin-bottom: 10px; color: #333;">User Information</h3>
<p>Username: <strong id="username-display">Anonymous</strong></p>
<p>Points: <span id="points-display" style="font-weight: bold; color: #0078ff;">0</span></p>
<br/>
<br/>
<!-- Filtres pour les événements -->
<div class="event-filters" id="filters_panel" style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: 4px; display:none;">
<h3 id="filters_header" style="margin-top: 0; color: #0078ff; cursor:pointer;">Filtres</h3>
<div style="margin-top: 10px;">
<label style="display: block; margin-bottom: 5px;">Type d'événement:</label>
<select id="event-type-filter" style="width: 100%; padding: 5px; border-radius: 4px; border: 1px solid #ddd;">
<option value="">Tous</option>
<option value="traffic">Traffic</option>
<option value="weather">Météo</option>
<option value="gathering">Rassemblement</option>
<option value="incident">Incident</option>
</select>
</div>
<!-- Authentication section -->
<!--
# <div id="auth-section" class="auth-section">
# <h3>OpenStreetMap Authentication</h3>
#
<a href="https://www.openstreetmap.org/oauth2/authorize?client_id={client_id}&redirect_uri={client_redirect}&response_type=code&scope=read_prefs" class="osm-login-btn">
<span class="osm-logo"></span>
Login with OpenStreetMap
</a>
<script>
# // Replace server-side auth section with JavaScript-rendered version if available
document.addEventListener('DOMContentLoaded', function() {
fetchEvents();
if (window.osmAuth) {
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);
}
}
});
</script>
</div> -->
<h3 id="endpoints_list_header">API Endpoints:</h3>
<ul id="endpoints_list">
<li><a href="/" >/ - API Information</a></li>
<li><a href="/event" >/event - Get Events</a></li>
<li><a href="/stats" >/stats - Database Statistics</a></li>
</ul>
<h3 id="demo_pages_list_header">Demo Pages:</h3>
<ul id="demo_pages_list">
<li><a href="/demo/search" >/demo/search - Advanced Search</a></li>
<li><a href="/demo/by-what" >/demo/by-what - Events by Type</a></li>
<li><a href="/demo/map-by-what" >/demo/map-by-what - Map by Event Type</a></li>
<li><a href="/demo/traffic" >/demo/traffic - Report Traffic Jam</a></li>
<li><a href="/demo/view-events" >/demo/view-events - View Saved Events</a></li>
<li><a href="/event?what=music" >Search Music Events</a></li>
<li><a href="/event?what=sport" >Search Sport Events</a></li>
</ul>
<p class="sources" style="text-align: center; margin-top: 10px;">
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" title="View Source Code on Cipherbliss" style="font-size: 24px;">
<i class="fas fa-code-branch"></i> sources
</a>
</p>
<div style="margin-top:12px; display:flex; align-items:center; gap:8px;">
<input type="checkbox" id="autoRefreshToggle" checked>
<label for="autoRefreshToggle" style="margin:0;">Rafraîchissement automatique (30s)</label>
</div>
</div>
<div class="event-filters" style="margin-top: 10px; padding: 10px; background-color: #fff; border: 1px solid #e5e7eb; border-radius: 4px;">
<h3 style="margin-top: 0; color: #0078ff;">Histogramme des évènements</h3>
<canvas id="eventsHistogram" style="width:100%; height:220px;"></canvas>
</div>
</div>
<script>
@ -320,6 +293,8 @@ class DemoMainResource:
const demoPagesList = document.getElementById('demo_pages_list');
const infoPanelHeader = document.getElementById('info_panel_header');
const infoPanelContent = document.getElementById('info_panel_content');
const filtersPanel = document.getElementById('filters_panel');
const filtersHeader = document.getElementById('filters_header');
// Fonction pour basculer l'affichage d'une liste ou section
function toggleList(header, list) {
@ -343,8 +318,98 @@ class DemoMainResource:
toggleList(endpointsHeader, endpointsList);
toggleList(demoPagesHeader, demoPagesList);
toggleList(infoPanelHeader, infoPanelContent);
// Toggle pour le panneau de filtres via le titre "Filtres"
if (filtersHeader && filtersPanel) {
filtersHeader.addEventListener('click', function() {
if (filtersPanel.style.display === 'none' || filtersPanel.style.display === '') {
filtersPanel.style.display = 'block';
} else {
filtersPanel.style.display = 'none';
}
});
}
});
// Variable globale pour stocker les marqueurs d'événements
window.eventMarkers = [];
function addEventsToMap(geojsonData) {
if (!geojsonData || !geojsonData.features) return;
geojsonData.features.forEach(feature => {
// Créer un élément HTML pour le marqueur
const el = document.createElement('div');
el.className = 'event-marker';
el.style.width = '20px';
el.style.height = '20px';
el.style.borderRadius = '50%';
// Déterminer la couleur selon le type d'événement
let color = '#0078ff';
const eventType = feature.properties.what;
if (eventType) {
if (eventType.includes('traffic')) color = '#F44336';
else if (eventType.includes('weather')) color = '#4CAF50';
else if (eventType.includes('gathering')) color = '#FF9800';
else if (eventType.includes('incident')) color = '#9C27B0';
}
el.style.backgroundColor = color;
el.style.border = '2px solid white';
el.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)';
el.style.cursor = 'pointer';
// Créer le contenu de la popup
const popupContent = createEventPopupContent(feature);
// Créer la popup
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: true
}).setHTML(popupContent);
// Créer et ajouter le marqueur
const marker = new maplibregl.Marker(el)
.setLngLat(feature.geometry.coordinates)
.setPopup(popup)
.addTo(map);
// Ajouter à la liste des marqueurs
window.eventMarkers.push(marker);
});
}
function createEventPopupContent(feature) {
const properties = feature.properties;
// Extraire les informations principales
const title = properties.title || 'Événement sans titre';
const what = properties.what || 'Non spécifié';
const when = properties.when ? formatDate(properties.when) : 'Date inconnue';
const description = properties.description || 'Aucune description disponible';
// Créer le HTML de la popup
return `
<div class="event-popup">
<h3 style="margin-top: 0; color: #0078ff;">${title}</h3>
<p><strong>Type:</strong> ${what}</p>
<p><strong>Date:</strong> ${when}</p>
<p><strong>Description:</strong> ${description}</p>
<p><a href="/demo/view/${properties.id}" style="color: #0078ff; font-weight: bold;">Voir détails</a></p>
</div>
`;
}
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleString();
} catch (e) {
return dateString;
}
}
// Map style URLs
const mapStyles = {
default: 'https://tiles.openfreemap.org/styles/liberty',
@ -401,10 +466,50 @@ class DemoMainResource:
// Style switcher functionality
let currentStyle = 'default';
let eventsData = null;
let histogramChart = null;
let refreshIntervalId = null;
// Array to store markers so they can be removed on refresh
// Store markers with their family/type for filtering
let currentMarkers = [];
function getFamily(what) {
if (!what) return 'unknown';
const s = String(what);
const dot = s.indexOf('.');
return dot === -1 ? s : s.slice(0, dot);
}
function applyTypeFilter() {
const sel = document.getElementById('event-type-filter');
const val = sel ? sel.value : '';
currentMarkers.forEach(rec => {
const el = rec.marker.getElement();
if (!val) {
el.style.display = '';
} else {
el.style.display = (rec.family === val) ? '' : 'none';
}
});
// Also filter vector circle layer if present
try {
if (!val) {
map.setFilter('events-circle', null);
} else {
const len = val.length;
// Show features where what starts with selected family
const filter = [
"any",
["!", ["has", "what"]],
["==", ["slice", ["get", "what"], 0, len], val]
];
map.setFilter('events-circle', filter);
}
} catch (e) {
// Layer may not be ready yet; ignore
}
}
// Fetch events when the map is loaded and every 30 seconds thereafter
@ -413,10 +518,18 @@ class DemoMainResource:
fetchEvents();
// Set up interval to fetch events every 30 seconds
setInterval(fetchEvents, 30000);
setupAutoRefresh();
console.log('Event refresh interval set: events will update every 30 seconds');
});
function setupAutoRefresh() {
const cb = document.getElementById('autoRefreshToggle');
const start = () => { if (!refreshIntervalId) { refreshIntervalId = setInterval(fetchEvents, 30000); } };
const stop = () => { if (refreshIntervalId) { clearInterval(refreshIntervalId); refreshIntervalId = null; } };
if (cb && cb.checked) start(); else stop();
if (cb) cb.addEventListener('change', () => { if (cb.checked) start(); else stop(); });
}
// Function to fetch events from the API
function fetchEvents() {
@ -427,6 +540,8 @@ class DemoMainResource:
if (data.features && data.features.length > 0) {
// Add events to the map
addEventsToMap(data);
// Render histogram for retrieved events
try { renderEventsHistogram(data.features); } catch(e) { console.warn('Histogram error', e); }
// Fit map to events bounds
fitMapToBounds(data);
@ -440,12 +555,49 @@ class DemoMainResource:
showErrorToast(`Erreur de chargement des événements: ${error.message}`);
});
}
function bucket10(dateStr) {
const d = new Date(dateStr);
if (isNaN(d.getTime())) return null;
d.setSeconds(0,0);
const m = d.getMinutes();
d.setMinutes(m - (m % 10));
return d.toISOString();
}
function renderEventsHistogram(features) {
const counts = new Map();
features.forEach(f => {
const p = f.properties || {};
const t = p.createdate || p.start || p.lastupdate;
const b = bucket10(t);
if (!b) return;
counts.set(b, (counts.get(b) || 0) + 1);
});
const labels = Array.from(counts.keys()).sort();
const data = labels.map(k => counts.get(k));
const ctx = document.getElementById('eventsHistogram');
if (!ctx) return;
if (histogramChart) histogramChart.destroy();
histogramChart = new Chart(ctx, {
type: 'bar',
data: { labels, datasets: [{ label:'Évènements / 10 min', data, backgroundColor:'#3b82f6' }] },
options: {
// maintainAspectRatio: false,
scales: {
x: { ticks: { callback: (v,i) => new Date(labels[i]).toLocaleString() } },
y: { beginAtZero: true }
}
}
});
}
// Function to add events to the map
function addEventsToMap(geojson) {
// Remove all existing markers
if (currentMarkers.length > 0) {
currentMarkers.forEach(marker => marker.remove());
currentMarkers.forEach(rec => rec.marker.remove());
currentMarkers = [];
console.log('Removed existing markers');
}
@ -569,7 +721,12 @@ class DemoMainResource:
let iconColor = '#0078ff'; // Default color
// Map event types to icons
if (eventType.startsWith('weather')) {
// Travaux detection (label or what)
const labelLower = String(properties.label || '').toLowerCase();
if (labelLower.includes('travaux') || eventType.includes('roadwork')) {
iconClass = 'hard-hat';
iconColor = '#ff9800';
} else if (eventType.startsWith('weather')) {
iconClass = 'cloud';
iconColor = '#00d1b2'; // Teal
} else if (eventType.startsWith('traffic')) {
@ -607,10 +764,13 @@ class DemoMainResource:
.setLngLat(coordinates)
.setPopup(popup)
.addTo(map);
// Store marker reference for later removal
currentMarkers.push(marker);
// Store marker with its family for filtering
currentMarkers.push({ marker, family: getFamily(eventType) });
});
// Re-apply current filter on fresh markers
applyTypeFilter();
}
// Function to calculate relative time (e.g., "2 hours 30 minutes ago")
@ -862,6 +1022,12 @@ class DemoMainResource:
// Initialisation des gestionnaires d'événements pour le toast d'erreur
initErrorToast();
// Hook filters
const typeSel = document.getElementById('event-type-filter');
const applyBtn = document.getElementById('apply-filters');
if (typeSel) typeSel.addEventListener('change', applyTypeFilter);
if (applyBtn) applyBtn.addEventListener('click', applyTypeFilter);
});
// Fonction pour initialiser le toast d'erreur
@ -892,7 +1058,21 @@ class DemoMainResource:
}, 6000);
}
// Initialiser automatiquement le mode social quand la carte est chargée
map.on('load', function() {
// Vérifier si l'objet social existe
if (window.oedbSocial) {
console.log('Initialisation automatique du mode social...');
setTimeout(() => {
// Trouver le bouton d'activation du mode social et simuler un clic
const socialButton = document.querySelector('.toggle-social-btn');
if (socialButton) {
socialButton.click();
console.log('Mode social activé automatiquement');
}
}, 2000); // Attendre 2 secondes pour que tout soit bien chargé
}
});
</script>
</body>
</html>

View file

@ -108,6 +108,9 @@ button:hover {
.nav-links {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.nav-links a {
@ -120,6 +123,99 @@ button:hover {
text-decoration: underline;
}
/* Navigation container */
.nav-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
}
/* Hamburger menu for mobile */
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #0078ff;
padding: 5px;
}
/* Responsive styles */
@media (max-width: 768px) {
.nav-container {
flex-direction: column;
align-items: flex-start;
}
.menu-toggle {
display: block;
align-self: flex-end;
margin-bottom: 10px;
}
.nav-links {
display: none;
flex-direction: column;
width: 100%;
}
.nav-links.active {
display: flex;
}
.nav-links a {
padding: 10px 0;
border-bottom: 1px solid #eee;
margin-right: 0;
}
}
/* Collapsible panel styles */
.collapsible-panel {
margin-bottom: 15px;
}
.collapsible-header {
background-color: #f8f9fa;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #e9ecef;
}
.collapsible-header h3 {
margin: 0;
font-size: 16px;
}
.collapsible-header .toggle-icon {
transition: transform 0.3s ease;
}
.collapsible-header.active .toggle-icon {
transform: rotate(180deg);
}
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
border-left: 1px solid #e9ecef;
border-right: 1px solid #e9ecef;
border-bottom: 1px solid #e9ecef;
border-radius: 0 0 5px 5px;
}
.collapsible-content.active {
max-height: 1000px;
}
/* Authentication section styles */
.auth-section {
background-color: #f8f9fa;
@ -523,4 +619,10 @@ select:invalid {
.add-event-btn{
float: left;
width: 130px;
}
button{
padding: 1rem 0.5rem;
border-radius: 5px;
background-color: #79a2d1;
}

View file

@ -0,0 +1 @@
# Ce fichier est un MP3 binaire qui contiendra le son 'pouet pouet'.

View file

@ -0,0 +1,750 @@
// Fonctionnalités sociales pour OEDB
class OEDBSocial {
constructor() {
this.socket = null;
this.position = null;
this.username = '';
this.friends = [];
this.markers = {};
this.map = null;
this.lastPouetTime = 0;
this.showOnlyFriends = false;
// Charger les amis depuis le localStorage
this.loadFriends();
// Boutons pour l'interface sociale
this.createSocialUI();
}
// Initialiser la connexion WebSocket
init(map, username) {
this.map = map;
this.username = username || localStorage.getItem('oedb_social_username') || '';
if (!this.username) {
this.promptForUsername();
} else {
localStorage.setItem('oedb_social_username', this.username);
}
// Créer la connexion WebSocket
// Utiliser l'URL relative au serveur actuel
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const wsUrl = `${wsProtocol}${window.location.host}/ws`;
this.socket = new WebSocket(wsUrl);
this.socket.onopen = () => {
console.log('Connexion WebSocket établie');
this.startSendingPosition();
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleSocketMessage(data);
};
this.socket.onclose = () => {
console.log('Connexion WebSocket fermée');
// Tentative de reconnexion après 5 secondes
setTimeout(() => this.init(this.map, this.username), 5000);
};
this.socket.onerror = (error) => {
console.error('Erreur WebSocket:', error);
this.showToast(`Erreur de connexion au serveur WebSocket. Vérifiez que le serveur est en cours d'exécution sur le port 8765.`, 'error');
};
}
// Demander le pseudo à l'utilisateur
promptForUsername() {
// Créer une boîte de dialogue modale pour demander le pseudo
const modalOverlay = document.createElement('div');
modalOverlay.className = 'modal-overlay';
modalOverlay.style.position = 'fixed';
modalOverlay.style.top = '0';
modalOverlay.style.left = '0';
modalOverlay.style.width = '100%';
modalOverlay.style.height = '100%';
modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
modalOverlay.style.zIndex = '1000';
modalOverlay.style.display = 'flex';
modalOverlay.style.justifyContent = 'center';
modalOverlay.style.alignItems = 'center';
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
modalContent.style.backgroundColor = '#fff';
modalContent.style.padding = '20px';
modalContent.style.borderRadius = '5px';
modalContent.style.maxWidth = '400px';
modalContent.style.width = '80%';
const title = document.createElement('h3');
title.textContent = 'Choisissez un pseudo';
title.style.marginBottom = '15px';
const form = document.createElement('form');
form.onsubmit = (e) => {
e.preventDefault();
const input = document.getElementById('username-input');
const username = input.value.trim();
if (username) {
this.username = username;
localStorage.setItem('oedb_social_username', username);
document.body.removeChild(modalOverlay);
this.startSendingPosition();
}
};
const input = document.createElement('input');
input.type = 'text';
input.id = 'username-input';
input.placeholder = 'Votre pseudo';
input.style.width = '100%';
input.style.padding = '8px';
input.style.marginBottom = '15px';
input.style.borderRadius = '4px';
input.style.border = '1px solid #ddd';
const button = document.createElement('button');
button.type = 'submit';
button.textContent = 'Valider';
button.style.padding = '8px 15px';
button.style.backgroundColor = '#0078ff';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
form.appendChild(input);
form.appendChild(button);
modalContent.appendChild(title);
modalContent.appendChild(form);
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
input.focus();
}
// Commencer à envoyer sa position
startSendingPosition() {
if (!this.username || !this.socket) return;
// Obtenir la position actuelle
this.getCurrentPosition();
// Mettre à jour la position toutes les 5 secondes
setInterval(() => {
this.getCurrentPosition();
}, 5000);
}
// Obtenir la position GPS actuelle
getCurrentPosition() {
if (navigator.geolocation && this.socket && this.socket.readyState === WebSocket.OPEN) {
navigator.geolocation.getCurrentPosition(
(position) => {
this.position = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
// Envoyer la position au serveur WebSocket
this.socket.send(JSON.stringify({
type: 'position',
username: this.username,
position: this.position,
timestamp: new Date().toISOString(),
showOnlyToFriends: this.showOnlyFriends
}));
},
(error) => {
console.error('Erreur lors de la récupération de la position:', error);
}
);
}
}
// Traiter les messages reçus par WebSocket
handleSocketMessage(data) {
switch (data.type) {
case 'position':
this.updateUserPosition(data);
break;
case 'pouet':
this.receivePouet(data);
break;
case 'friendRequest':
this.receiveFriendRequest(data);
break;
case 'users':
this.updateAllUsers(data.users);
break;
}
}
// Mettre à jour la position d'un utilisateur sur la carte
updateUserPosition(data) {
// Ignorer les mises à jour de notre propre position
if (data.username === this.username) return;
// Vérifier si l'utilisateur est visible uniquement pour ses amis
if (data.showOnlyToFriends && !this.friends.includes(data.username)) return;
// Supprimer l'ancien marqueur s'il existe
if (this.markers[data.username]) {
this.markers[data.username].remove();
}
// Créer un élément HTML personnalisé pour le marqueur
const el = document.createElement('div');
el.className = 'user-marker';
// Styles de base pour le marqueur
el.style.width = '40px';
el.style.height = '40px';
el.style.borderRadius = '50%';
el.style.backgroundColor = this.friends.includes(data.username) ? '#4CAF50' : '#0078ff';
el.style.border = '2px solid white';
el.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)';
el.style.display = 'flex';
el.style.justifyContent = 'center';
el.style.alignItems = 'center';
el.style.color = 'white';
el.style.fontWeight = 'bold';
el.style.fontSize = '12px';
el.style.cursor = 'pointer';
// Ajouter les initiales de l'utilisateur
const initials = data.username.substring(0, 2).toUpperCase();
el.textContent = initials;
// Ajouter un tooltip avec le nom complet
el.title = data.username;
// Créer le popup
const popupContent = `
<div class="user-popup" style="padding: 10px; max-width: 200px;">
<h3 style="margin-top: 0;">${data.username}</h3>
<p style="margin-bottom: 10px;">Position mise à jour: ${this.formatTimestamp(data.timestamp)}</p>
<div style="display: flex; justify-content: space-between;">
<button class="pouet-btn" style="padding: 5px 10px; background-color: #FFC107; border: none; border-radius: 3px; cursor: pointer;">Pouet Pouet!</button>
${!this.friends.includes(data.username) ? `
<button class="add-friend-btn" style="padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;">Ajouter ami</button>
` : ''}
</div>
</div>
`;
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: true
}).setHTML(popupContent);
// Ajouter des gestionnaires d'événements au popup
popup.on('open', () => {
// Gérer le clic sur le bouton Pouet Pouet
setTimeout(() => {
const pouetBtn = document.querySelector('.pouet-btn');
if (pouetBtn) {
pouetBtn.addEventListener('click', () => {
this.sendPouet(data.username);
});
}
// Gérer le clic sur le bouton Ajouter ami
const addFriendBtn = document.querySelector('.add-friend-btn');
if (addFriendBtn) {
addFriendBtn.addEventListener('click', () => {
this.sendFriendRequest(data.username);
});
}
}, 100);
});
// Créer le marqueur et l'ajouter à la carte
const marker = new maplibregl.Marker(el)
.setLngLat([data.position.lng, data.position.lat])
.setPopup(popup)
.addTo(this.map);
// Stocker le marqueur pour pouvoir le supprimer plus tard
this.markers[data.username] = marker;
}
// Mettre à jour tous les utilisateurs actifs
updateAllUsers(users) {
// Supprimer les marqueurs des utilisateurs qui ne sont plus actifs
Object.keys(this.markers).forEach(username => {
if (!users.find(user => user.username === username)) {
this.markers[username].remove();
delete this.markers[username];
}
});
}
// Envoyer un pouet à un utilisateur
sendPouet(username) {
const now = Date.now();
// Vérifier si on peut envoyer un pouet (limité à 1 toutes les 10 secondes)
if (now - this.lastPouetTime < 10000) {
const remainingTime = Math.ceil((10000 - (now - this.lastPouetTime)) / 1000);
this.showToast(`Merci d'attendre encore ${remainingTime} secondes avant d'envoyer un autre pouet!`, 'warning');
return;
}
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'pouet',
from: this.username,
to: username,
timestamp: new Date().toISOString()
}));
this.lastPouetTime = now;
this.showToast(`Pouet pouet envoyé à ${username}!`, 'success');
}
}
// Recevoir un pouet
receivePouet(data) {
this.showToast(`${data.from} vous a envoyé un pouet pouet!`, 'info');
// Jouer un son
const audio = new Audio('/static/pouet.mp3');
audio.play().catch(e => console.log('Erreur lors de la lecture du son:', e));
}
// Envoyer une demande d'ami
sendFriendRequest(username) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'friendRequest',
from: this.username,
to: username,
timestamp: new Date().toISOString()
}));
this.showToast(`Demande d'ami envoyée à ${username}!`, 'success');
}
}
// Recevoir une demande d'ami
receiveFriendRequest(data) {
// Créer une boîte de dialogue modale pour la demande d'ami
const modalOverlay = document.createElement('div');
modalOverlay.className = 'modal-overlay';
modalOverlay.style.position = 'fixed';
modalOverlay.style.top = '0';
modalOverlay.style.left = '0';
modalOverlay.style.width = '100%';
modalOverlay.style.height = '100%';
modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
modalOverlay.style.zIndex = '1000';
modalOverlay.style.display = 'flex';
modalOverlay.style.justifyContent = 'center';
modalOverlay.style.alignItems = 'center';
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
modalContent.style.backgroundColor = '#fff';
modalContent.style.padding = '20px';
modalContent.style.borderRadius = '5px';
modalContent.style.maxWidth = '400px';
modalContent.style.width = '80%';
const title = document.createElement('h3');
title.textContent = 'Demande d\'ami';
title.style.marginBottom = '15px';
const message = document.createElement('p');
message.textContent = `${data.from} souhaite vous ajouter à sa liste d'amis.`;
message.style.marginBottom = '20px';
const buttonsContainer = document.createElement('div');
buttonsContainer.style.display = 'flex';
buttonsContainer.style.justifyContent = 'space-between';
const acceptButton = document.createElement('button');
acceptButton.textContent = 'Accepter';
acceptButton.style.padding = '8px 15px';
acceptButton.style.backgroundColor = '#4CAF50';
acceptButton.style.color = 'white';
acceptButton.style.border = 'none';
acceptButton.style.borderRadius = '4px';
acceptButton.style.cursor = 'pointer';
acceptButton.onclick = () => {
this.addFriend(data.from);
document.body.removeChild(modalOverlay);
};
const rejectButton = document.createElement('button');
rejectButton.textContent = 'Refuser';
rejectButton.style.padding = '8px 15px';
rejectButton.style.backgroundColor = '#f44336';
rejectButton.style.color = 'white';
rejectButton.style.border = 'none';
rejectButton.style.borderRadius = '4px';
rejectButton.style.cursor = 'pointer';
rejectButton.onclick = () => {
document.body.removeChild(modalOverlay);
};
buttonsContainer.appendChild(acceptButton);
buttonsContainer.appendChild(rejectButton);
modalContent.appendChild(title);
modalContent.appendChild(message);
modalContent.appendChild(buttonsContainer);
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
}
// Ajouter un ami à la liste d'amis
addFriend(username) {
if (!this.friends.includes(username)) {
this.friends.push(username);
this.saveFriends();
this.showToast(`${username} a été ajouté à votre liste d'amis!`, 'success');
// Mettre à jour le marqueur de cet ami s'il est visible
if (this.markers[username]) {
const position = this.markers[username].getLngLat();
this.markers[username].remove();
delete this.markers[username];
// Simuler une mise à jour de position pour recréer le marqueur
this.updateUserPosition({
username: username,
position: {
lng: position.lng,
lat: position.lat
},
timestamp: new Date().toISOString()
});
}
}
}
// Sauvegarder la liste d'amis dans le localStorage
saveFriends() {
localStorage.setItem('oedb_social_friends', JSON.stringify(this.friends));
}
// Charger la liste d'amis depuis le localStorage
loadFriends() {
try {
const friendsJson = localStorage.getItem('oedb_social_friends');
if (friendsJson) {
this.friends = JSON.parse(friendsJson);
}
} catch (e) {
console.error('Erreur lors du chargement des amis:', e);
this.friends = [];
}
}
// Créer l'interface utilisateur pour les fonctionnalités sociales
createSocialUI() {
// Conteneur principal pour les contrôles sociaux
const socialContainer = document.createElement('div');
socialContainer.className = 'social-controls';
socialContainer.style.position = 'absolute';
socialContainer.style.top = '10px';
socialContainer.style.right = '10px';
socialContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
socialContainer.style.padding = '10px';
socialContainer.style.borderRadius = '5px';
socialContainer.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
socialContainer.style.zIndex = '10';
socialContainer.style.display = 'flex';
socialContainer.style.flexDirection = 'column';
socialContainer.style.gap = '10px';
// Titre
const title = document.createElement('h3');
title.textContent = 'Mode Social';
title.style.margin = '0 0 10px 0';
title.style.textAlign = 'center';
// Bouton pour activer/désactiver le mode social
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-social-btn';
toggleButton.textContent = 'Activer le mode social';
toggleButton.style.padding = '8px';
toggleButton.style.backgroundColor = '#0078ff';
toggleButton.style.color = 'white';
toggleButton.style.border = 'none';
toggleButton.style.borderRadius = '4px';
toggleButton.style.cursor = 'pointer';
toggleButton.style.fontWeight = 'bold';
let socialActive = false;
toggleButton.addEventListener('click', () => {
socialActive = !socialActive;
if (socialActive) {
toggleButton.textContent = 'Désactiver le mode social';
toggleButton.style.backgroundColor = '#f44336';
this.init(this.map);
} else {
toggleButton.textContent = 'Activer le mode social';
toggleButton.style.backgroundColor = '#0078ff';
// Fermer la connexion WebSocket
if (this.socket) {
this.socket.close();
this.socket = null;
}
// Supprimer tous les marqueurs
Object.values(this.markers).forEach(marker => marker.remove());
this.markers = {};
}
// Afficher/masquer les options supplémentaires
optionsContainer.style.display = socialActive ? 'block' : 'none';
});
// Conteneur pour les options supplémentaires
const optionsContainer = document.createElement('div');
optionsContainer.className = 'social-options';
optionsContainer.style.display = 'none';
// Bouton pour changer de pseudo
const changeUsernameBtn = document.createElement('button');
changeUsernameBtn.textContent = 'Changer de pseudo';
changeUsernameBtn.style.width = '100%';
changeUsernameBtn.style.padding = '8px';
changeUsernameBtn.style.backgroundColor = '#FFC107';
changeUsernameBtn.style.border = 'none';
changeUsernameBtn.style.borderRadius = '4px';
changeUsernameBtn.style.marginBottom = '10px';
changeUsernameBtn.style.cursor = 'pointer';
changeUsernameBtn.addEventListener('click', () => {
this.promptForUsername();
});
// Case à cocher pour la visibilité uniquement aux amis
const visibilityContainer = document.createElement('div');
visibilityContainer.style.display = 'flex';
visibilityContainer.style.alignItems = 'center';
visibilityContainer.style.marginBottom = '10px';
const visibilityCheckbox = document.createElement('input');
visibilityCheckbox.type = 'checkbox';
visibilityCheckbox.id = 'visibility-checkbox';
visibilityCheckbox.checked = this.showOnlyFriends;
const visibilityLabel = document.createElement('label');
visibilityLabel.htmlFor = 'visibility-checkbox';
visibilityLabel.textContent = 'Visible uniquement par mes amis';
visibilityLabel.style.marginLeft = '5px';
visibilityContainer.appendChild(visibilityCheckbox);
visibilityContainer.appendChild(visibilityLabel);
visibilityCheckbox.addEventListener('change', () => {
this.showOnlyFriends = visibilityCheckbox.checked;
this.getCurrentPosition(); // Mettre à jour immédiatement avec le nouveau paramètre
});
// Gestionnaire d'amis
const friendsManager = document.createElement('div');
friendsManager.style.marginTop = '10px';
const friendsTitle = document.createElement('h4');
friendsTitle.textContent = 'Mes amis';
friendsTitle.style.margin = '0 0 5px 0';
const friendsList = document.createElement('ul');
friendsList.style.listStyle = 'none';
friendsList.style.padding = '0';
friendsList.style.margin = '0';
friendsList.style.maxHeight = '150px';
friendsList.style.overflowY = 'auto';
friendsList.style.border = '1px solid #ddd';
friendsList.style.borderRadius = '4px';
friendsList.style.padding = '5px';
// Fonction pour mettre à jour la liste d'amis
const updateFriendsList = () => {
friendsList.innerHTML = '';
if (this.friends.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.textContent = 'Aucun ami pour l\'instant';
emptyItem.style.fontStyle = 'italic';
emptyItem.style.padding = '5px';
friendsList.appendChild(emptyItem);
} else {
this.friends.forEach(friend => {
const listItem = document.createElement('li');
listItem.style.display = 'flex';
listItem.style.justifyContent = 'space-between';
listItem.style.alignItems = 'center';
listItem.style.padding = '5px';
listItem.style.borderBottom = '1px solid #eee';
const friendName = document.createElement('span');
friendName.textContent = friend;
const removeBtn = document.createElement('button');
removeBtn.textContent = 'X';
removeBtn.style.backgroundColor = '#f44336';
removeBtn.style.color = 'white';
removeBtn.style.border = 'none';
removeBtn.style.borderRadius = '50%';
removeBtn.style.width = '20px';
removeBtn.style.height = '20px';
removeBtn.style.fontSize = '10px';
removeBtn.style.cursor = 'pointer';
removeBtn.style.display = 'flex';
removeBtn.style.justifyContent = 'center';
removeBtn.style.alignItems = 'center';
removeBtn.addEventListener('click', () => {
this.friends = this.friends.filter(f => f !== friend);
this.saveFriends();
updateFriendsList();
});
listItem.appendChild(friendName);
listItem.appendChild(removeBtn);
friendsList.appendChild(listItem);
});
}
};
// Initialiser la liste d'amis
updateFriendsList();
friendsManager.appendChild(friendsTitle);
friendsManager.appendChild(friendsList);
// Ajouter tous les éléments au conteneur d'options
optionsContainer.appendChild(changeUsernameBtn);
optionsContainer.appendChild(visibilityContainer);
optionsContainer.appendChild(friendsManager);
// Ajouter tous les éléments au conteneur principal
socialContainer.appendChild(title);
socialContainer.appendChild(toggleButton);
socialContainer.appendChild(optionsContainer);
// Ajouter le conteneur au document après le chargement du DOM
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(socialContainer);
});
}
// Afficher un toast (message flottant)
showToast(message, type = 'info') {
// Créer le conteneur de toast s'il n'existe pas
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.style.position = 'fixed';
toastContainer.style.top = '20px';
toastContainer.style.left = '50%';
toastContainer.style.transform = 'translateX(-50%)';
toastContainer.style.zIndex = '1000';
toastContainer.style.display = 'flex';
toastContainer.style.flexDirection = 'column';
toastContainer.style.alignItems = 'center';
toastContainer.style.gap = '10px';
document.body.appendChild(toastContainer);
}
// Créer le toast
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.style.padding = '10px 15px';
toast.style.borderRadius = '5px';
toast.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.2)';
toast.style.minWidth = '250px';
toast.style.textAlign = 'center';
toast.style.animation = 'fadeIn 0.3s, fadeOut 0.3s 2.7s';
toast.style.opacity = '0';
toast.style.maxWidth = '80vw';
// Définir la couleur en fonction du type
switch (type) {
case 'success':
toast.style.backgroundColor = '#4CAF50';
toast.style.color = 'white';
break;
case 'warning':
toast.style.backgroundColor = '#FFC107';
toast.style.color = 'black';
break;
case 'error':
toast.style.backgroundColor = '#f44336';
toast.style.color = 'white';
break;
default: // info
toast.style.backgroundColor = '#0078ff';
toast.style.color = 'white';
}
toast.textContent = message;
// Ajouter le style d'animation s'il n'existe pas
if (!document.getElementById('toast-style')) {
const style = document.createElement('style');
style.id = 'toast-style';
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-20px); }
}
`;
document.head.appendChild(style);
}
// Ajouter le toast au conteneur
toastContainer.appendChild(toast);
// Animer l'entrée
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
}, 10);
// Supprimer le toast après 3 secondes
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(-20px)';
setTimeout(() => {
toastContainer.removeChild(toast);
}, 300);
}, 3000);
}
// Formatter un timestamp en heure locale
formatTimestamp(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString();
}
}
// Initialiser l'objet social lorsque le DOM est chargé
document.addEventListener('DOMContentLoaded', () => {
// Créer l'instance sociale
window.oedbSocial = new OEDBSocial();
});

View file

@ -7,6 +7,23 @@ let existingMarkers = [];
const PANORAMAX_TOKEN_STORAGE_KEY = 'oedb_panoramax_token';
let mediaStream = null;
// 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;
}
function setDefaultDates() {
const now = new Date();
const nowISO = now.toISOString().slice(0, 16);
@ -70,8 +87,22 @@ function fetchExistingTrafficEvents() {
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 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);
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>`;
@ -590,11 +621,24 @@ function updateUserInfoDisplay() {
userInfoPanel.innerHTML = `\n<h3>User Information</h3>\n<p>Username: <strong>${username}</strong></p>\n<p>Points: <span class="user-points">${points}</span></p>`;
}
// 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');
});
});
}
document.addEventListener('DOMContentLoaded', function() {
setDefaultDates();
initTabs();
initMap();
updateUserInfoDisplay();
initCollapsiblePanels();
});
// Contrôles Caméra

View file

@ -1,8 +1,26 @@
<div class="nav-links">
<a href="/demo">← Retour à la démo</a>
<a href="/demo/traffic">Signaler trafic</a>
<a href="/demo/view-events">Voir événements</a>
<a href="/demo/map-by-what">Carte par type</a>
<a href="/demo/stats">Stats</a>
<div class="nav-container">
<button class="menu-toggle" aria-label="Toggle menu">
<i class="fas fa-bars"></i>
</button>
<div class="nav-links">
<a href="/demo">← Retour à la démo</a>
<a href="/demo/traffic">Signaler trafic</a>
<a href="/demo/view-events">Voir événements</a>
<a href="/demo/map-by-what">Carte par type</a>
<a href="/demo/stats">Stats</a>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const menuToggle = document.querySelector('.menu-toggle');
const navLinks = document.querySelector('.nav-links');
if (menuToggle) {
menuToggle.addEventListener('click', function() {
navLinks.classList.toggle('active');
});
}
});
</script>

View file

@ -0,0 +1,337 @@
import json
import time
import threading
import asyncio
import websockets
from oedb.utils.logging import logger
class WebSocketManager:
"""
Gestionnaire de WebSockets pour les fonctionnalités sociales d'OEDB.
Gère les connexions WebSocket des utilisateurs et distribue les messages.
Peut être utilisé soit de façon autonome, soit intégré avec uWSGI.
"""
def __init__(self):
self.clients = {}
self.positions = {}
self.lock = threading.Lock()
self.server = None
async def handle_connection(self, websocket, path):
"""
Gère une connexion WebSocket entrante.
Args:
websocket: La connexion WebSocket.
path: Le chemin de la demande.
"""
client_id = id(websocket)
logger.debug(f"Tentative de connexion WebSocket reçue: {client_id} - {path}")
try:
logger.info(f"Nouvelle connexion WebSocket: {client_id} - {path}")
# Ajouter le client à la liste
with self.lock:
self.clients[client_id] = {
'websocket': websocket,
'username': None,
'position': None,
'last_seen': time.time(),
'show_only_to_friends': False,
}
# Envoyer la liste des utilisateurs connectés
await self.send_users_list(websocket)
async for message in websocket:
await self.handle_message(client_id, message)
except websockets.exceptions.ConnectionClosed:
logger.info(f"Connexion WebSocket fermée: {client_id}")
except Exception as e:
logger.error(f"Erreur WebSocket: {e}")
finally:
# Supprimer le client de la liste
with self.lock:
if client_id in self.clients:
username = self.clients[client_id].get('username')
if username and username in self.positions:
del self.positions[username]
del self.clients[client_id]
# Informer les autres clients de la déconnexion
await self.broadcast_users_list()
async def handle_message(self, client_id, message):
"""
Traite un message WebSocket reçu.
Args:
client_id: L'ID du client qui a envoyé le message.
message: Le message JSON reçu.
"""
try:
data = json.loads(message)
message_type = data.get('type')
if message_type == 'position':
await self.handle_position_update(client_id, data)
elif message_type == 'pouet':
await self.handle_pouet(data)
elif message_type == 'friendRequest':
await self.handle_friend_request(data)
except json.JSONDecodeError:
logger.error(f"Message JSON invalide: {message}")
except Exception as e:
logger.error(f"Erreur de traitement du message: {e}")
async def handle_position_update(self, client_id, data):
"""
Traite une mise à jour de position d'un utilisateur.
Args:
client_id: L'ID du client qui a envoyé la mise à jour.
data: Les données de position.
"""
username = data.get('username')
position = data.get('position')
show_only_to_friends = data.get('showOnlyToFriends', False)
if not username or not position:
return
# Mettre à jour les informations du client
with self.lock:
if client_id in self.clients:
self.clients[client_id]['username'] = username
self.clients[client_id]['position'] = position
self.clients[client_id]['last_seen'] = time.time()
self.clients[client_id]['show_only_to_friends'] = show_only_to_friends
# Mettre à jour la position dans le dictionnaire des positions
self.positions[username] = {
'position': position,
'timestamp': data.get('timestamp'),
'show_only_to_friends': show_only_to_friends
}
# Diffuser la position à tous les autres clients
await self.broadcast_position(username, position, data.get('timestamp'), show_only_to_friends)
# Envoyer la liste mise à jour des utilisateurs
await self.broadcast_users_list()
async def handle_pouet(self, data):
"""
Traite un 'pouet pouet' envoyé d'un utilisateur à un autre.
Args:
data: Les données du pouet pouet.
"""
from_user = data.get('from')
to_user = data.get('to')
if not from_user or not to_user:
return
# Trouver le client destinataire
recipient_client_id = None
with self.lock:
for client_id, client_info in self.clients.items():
if client_info.get('username') == to_user:
recipient_client_id = client_id
break
if recipient_client_id and recipient_client_id in self.clients:
# Envoyer le pouet au destinataire
try:
await self.clients[recipient_client_id]['websocket'].send(json.dumps({
'type': 'pouet',
'from': from_user,
'timestamp': data.get('timestamp')
}))
logger.info(f"Pouet pouet envoyé de {from_user} à {to_user}")
except Exception as e:
logger.error(f"Erreur d'envoi de pouet pouet: {e}")
async def handle_friend_request(self, data):
"""
Traite une demande d'ami d'un utilisateur à un autre.
Args:
data: Les données de la demande d'ami.
"""
from_user = data.get('from')
to_user = data.get('to')
if not from_user or not to_user:
return
# Trouver le client destinataire
recipient_client_id = None
with self.lock:
for client_id, client_info in self.clients.items():
if client_info.get('username') == to_user:
recipient_client_id = client_id
break
if recipient_client_id and recipient_client_id in self.clients:
# Envoyer la demande d'ami au destinataire
try:
await self.clients[recipient_client_id]['websocket'].send(json.dumps({
'type': 'friendRequest',
'from': from_user,
'timestamp': data.get('timestamp')
}))
logger.info(f"Demande d'ami envoyée de {from_user} à {to_user}")
except Exception as e:
logger.error(f"Erreur d'envoi de demande d'ami: {e}")
async def broadcast_position(self, username, position, timestamp, show_only_to_friends):
"""
Diffuse la position d'un utilisateur à tous les autres utilisateurs.
Args:
username: Le nom d'utilisateur.
position: La position de l'utilisateur.
timestamp: L'horodatage de la mise à jour.
show_only_to_friends: Indique si la position est visible uniquement par les amis.
"""
message = json.dumps({
'type': 'position',
'username': username,
'position': position,
'timestamp': timestamp,
'showOnlyToFriends': show_only_to_friends
})
with self.lock:
for client_id, client_info in self.clients.items():
# Ne pas envoyer à l'utilisateur lui-même
if client_info.get('username') == username:
continue
try:
await client_info['websocket'].send(message)
except Exception as e:
logger.error(f"Erreur d'envoi de broadcast de position: {e}")
async def send_users_list(self, websocket):
"""
Envoie la liste des utilisateurs connectés à un client spécifique.
Args:
websocket: La connexion WebSocket du client.
"""
users = []
with self.lock:
for client_info in self.clients.values():
if client_info.get('username'):
users.append({
'username': client_info['username'],
'timestamp': time.time()
})
try:
await websocket.send(json.dumps({
'type': 'users',
'users': users
}))
except Exception as e:
logger.error(f"Erreur d'envoi de liste d'utilisateurs: {e}")
async def broadcast_users_list(self):
"""
Diffuse la liste des utilisateurs connectés à tous les clients.
"""
users = []
with self.lock:
for client_info in self.clients.values():
if client_info.get('username'):
users.append({
'username': client_info['username'],
'timestamp': time.time()
})
message = json.dumps({
'type': 'users',
'users': users
})
with self.lock:
for client_info in self.clients.values():
try:
await client_info['websocket'].send(message)
except Exception as e:
logger.error(f"Erreur de broadcast de liste d'utilisateurs: {e}")
async def cleanup_inactive_clients(self):
"""
Nettoie les clients inactifs (pas de mise à jour depuis plus de 5 minutes).
"""
inactive_clients = []
with self.lock:
current_time = time.time()
for client_id, client_info in self.clients.items():
if current_time - client_info['last_seen'] > 300: # 5 minutes
inactive_clients.append(client_id)
for client_id in inactive_clients:
username = self.clients[client_id].get('username')
if username and username in self.positions:
del self.positions[username]
del self.clients[client_id]
if inactive_clients:
logger.info(f"Nettoyage de {len(inactive_clients)} clients inactifs")
await self.broadcast_users_list()
async def cleanup_task(self):
"""
Tâche périodique pour nettoyer les clients inactifs.
"""
while True:
await asyncio.sleep(60) # Exécuter toutes les minutes
await self.cleanup_inactive_clients()
async def start_server(self, host='0.0.0.0', port=8765):
"""
Démarre le serveur WebSocket.
Args:
host: L'hôte à écouter.
port: Le port à écouter.
"""
self.server = await websockets.serve(self.handle_connection, host, port)
logger.info(f"Serveur WebSocket démarré sur {host}:{port}")
# Démarrer la tâche de nettoyage
asyncio.create_task(self.cleanup_task())
# Garder le serveur en cours d'exécution
await asyncio.Future()
def start(self, host='0.0.0.0', port=8765):
"""
Démarre le serveur WebSocket dans un thread séparé.
Args:
host: L'hôte à écouter.
port: Le port à écouter.
"""
def run_server():
asyncio.run(self.start_server(host, port))
server_thread = threading.Thread(target=run_server, daemon=True)
server_thread.start()
logger.info(f"Serveur WebSocket démarré dans un thread séparé sur {host}:{port}")
# Créer une instance du gestionnaire WebSocket
ws_manager = WebSocketManager()
# Démarrer automatiquement le serveur WebSocket
ws_manager.start(host='127.0.0.1', port=8765)