add live page
This commit is contained in:
parent
114bcca24e
commit
eb8c42d0c0
19 changed files with 2759 additions and 199 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
1
oedb/resources/demo/static/pouet.mp3
Normal file
1
oedb/resources/demo/static/pouet.mp3
Normal file
|
@ -0,0 +1 @@
|
|||
# Ce fichier est un MP3 binaire qui contiendra le son 'pouet pouet'.
|
750
oedb/resources/demo/static/social.js
Normal file
750
oedb/resources/demo/static/social.js
Normal 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();
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
337
oedb/resources/demo/websocket.py
Normal file
337
oedb/resources/demo/websocket.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue