add live page
This commit is contained in:
parent
114bcca24e
commit
eb8c42d0c0
19 changed files with 2759 additions and 199 deletions
62
LANCER_SERVEUR.md
Normal file
62
LANCER_SERVEUR.md
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Lancement du serveur OEDB avec WebSockets
|
||||||
|
|
||||||
|
Ce document explique comment lancer le serveur OEDB avec le support des WebSockets intégré via uWSGI.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
Assurez-vous d'avoir installé les dépendances nécessaires :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install uwsgi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lancement du serveur
|
||||||
|
|
||||||
|
Pour lancer le serveur avec le support WebSocket, vous avez plusieurs options :
|
||||||
|
|
||||||
|
### Utiliser la commande make
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make websocket
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou pour lancer en arrière-plan (mode démon) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make websocket-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lancer manuellement avec uWSGI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uwsgi --ini uwsgi.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces commandes démarreront le serveur sur le port 8080 avec le support WebSocket activé sur la route `/ws`.
|
||||||
|
|
||||||
|
## Configuration en production
|
||||||
|
|
||||||
|
En production, vous pouvez utiliser uWSGI avec Nginx. Voici un exemple de configuration Nginx :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name votre-domaine.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass 127.0.0.1:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette configuration permet d'utiliser Nginx comme proxy inverse pour les requêtes HTTP normales et les connexions WebSocket.
|
11
Makefile
11
Makefile
|
@ -7,4 +7,13 @@ LOGFILE ?= uwsgi.log
|
||||||
|
|
||||||
start:
|
start:
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
. venv/bin/activate && pip install -r requirements.txt && uwsgi --http :$(PORT) --wsgi-file backend.py --callable app
|
. venv/bin/activate && pip install -r requirements.txt && uwsgi --http :$(PORT) --wsgi-file backend.py --callable app
|
||||||
|
|
||||||
|
websocket:
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate && pip install -r requirements.txt && pip install websockets && uwsgi --ini uwsgi.ini
|
||||||
|
|
||||||
|
# Version en arrière-plan (démon)
|
||||||
|
websocket-daemon:
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate && pip install -r requirements.txt && pip install websockets && uwsgi --ini uwsgi.ini --daemonize $(LOGFILE)
|
|
@ -25,6 +25,8 @@ from oedb.resources.stats import StatsResource
|
||||||
from oedb.resources.search import EventSearch
|
from oedb.resources.search import EventSearch
|
||||||
from oedb.resources.root import root
|
from oedb.resources.root import root
|
||||||
from oedb.resources.demo import demo, demo_stats
|
from oedb.resources.demo import demo, demo_stats
|
||||||
|
from oedb.resources.live import live
|
||||||
|
from oedb.resources.rss import rss_latest, rss_by_family
|
||||||
from oedb.resources.event_form import event_form
|
from oedb.resources.event_form import event_form
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
|
@ -86,9 +88,13 @@ def create_app():
|
||||||
app.add_route('/demo/by-what', demo, suffix='by_what') # Handle events by type page
|
app.add_route('/demo/by-what', demo, suffix='by_what') # Handle events by type page
|
||||||
app.add_route('/demo/map-by-what', demo, suffix='map_by_what') # Handle map by event type page
|
app.add_route('/demo/map-by-what', demo, suffix='map_by_what') # Handle map by event type page
|
||||||
app.add_route('/demo/edit/{id}', demo, suffix='edit') # Handle event editing page
|
app.add_route('/demo/edit/{id}', demo, suffix='edit') # Handle event editing page
|
||||||
|
app.add_route('/demo/by_id/{id}', demo, suffix='by_id') # Handle view single event by id
|
||||||
app.add_route('/demo/traffic', demo, suffix='traffic') # Handle traffic jam reporting page
|
app.add_route('/demo/traffic', demo, suffix='traffic') # Handle traffic jam reporting page
|
||||||
app.add_route('/demo/view-events', demo, suffix='view_events') # Handle view saved events page
|
app.add_route('/demo/view-events', demo, suffix='view_events') # Handle view saved events page
|
||||||
app.add_route('/demo/stats', demo_stats) # Handle stats by what page
|
app.add_route('/demo/stats', demo_stats) # Handle stats by what page
|
||||||
|
app.add_route('/demo/live', live) # Live page
|
||||||
|
app.add_route('/rss', rss_latest) # RSS latest 200
|
||||||
|
app.add_route('/rss/by/{family}', rss_by_family) # RSS by family
|
||||||
|
|
||||||
logger.success("Application initialized successfully")
|
logger.success("Application initialized successfully")
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -3,18 +3,35 @@
|
||||||
OSM Calendar Extractor for the OpenEventDatabase.
|
OSM Calendar Extractor for the OpenEventDatabase.
|
||||||
|
|
||||||
This script fetches events from the OpenStreetMap Calendar RSS feed
|
This script fetches events from the OpenStreetMap Calendar RSS feed
|
||||||
and adds them to the OpenEventDatabase if they don't already exist.
|
and adds them to the OpenEventDatabase via the API.
|
||||||
|
|
||||||
|
For events that don't have geographic coordinates in the RSS feed but have a link
|
||||||
|
to an OSM Calendar event (https://osmcal.org/event/...), the script will fetch
|
||||||
|
the iCal version of the event and extract the coordinates and location from there.
|
||||||
|
|
||||||
RSS Feed URL: https://osmcal.org/events.rss
|
RSS Feed URL: https://osmcal.org/events.rss
|
||||||
|
API Endpoint: https://api.openeventdatabase.org/event
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python osm_cal.py [--max-events MAX_EVENTS] [--offset OFFSET]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
--max-events MAX_EVENTS Maximum number of events to insert (default: 1)
|
||||||
|
--offset OFFSET Number of events to skip from the beginning of the RSS feed (default: 0)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Insert the first event from the RSS feed
|
||||||
|
python osm_cal.py
|
||||||
|
|
||||||
|
# Insert up to 5 events from the RSS feed
|
||||||
|
python osm_cal.py --max-events 5
|
||||||
|
|
||||||
|
# Skip the first 3 events and insert the next 2
|
||||||
|
python osm_cal.py --offset 3 --max-events 2
|
||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
DB_NAME: The name of the database (default: "oedb")
|
These environment variables can be set in the system environment or in a .env file
|
||||||
DB_HOST: The hostname of the database server (default: "localhost")
|
in the project root directory.
|
||||||
DB_USER: The username to connect to the database (default: "")
|
|
||||||
POSTGRES_PASSWORD: The password to connect to the database (default: None)
|
|
||||||
|
|
||||||
These environment variables can be set in the system environment or in a .env file
|
|
||||||
in the project root directory.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -34,6 +51,8 @@ from oedb.utils.logging import logger
|
||||||
|
|
||||||
# RSS Feed URL for OSM Calendar
|
# RSS Feed URL for OSM Calendar
|
||||||
RSS_URL = "https://osmcal.org/events.rss"
|
RSS_URL = "https://osmcal.org/events.rss"
|
||||||
|
# Base URL for OSM Calendar events
|
||||||
|
OSMCAL_EVENT_BASE_URL = "https://osmcal.org/event/"
|
||||||
|
|
||||||
def fetch_osm_calendar_data():
|
def fetch_osm_calendar_data():
|
||||||
"""
|
"""
|
||||||
|
@ -179,6 +198,73 @@ def parse_event_dates(description):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
return (now.isoformat(), (now + timedelta(days=1)).isoformat())
|
return (now.isoformat(), (now + timedelta(days=1)).isoformat())
|
||||||
|
|
||||||
|
def fetch_ical_data(event_url):
|
||||||
|
"""
|
||||||
|
Fetch and parse iCal data for an OSM Calendar event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_url (str): The URL of the OSM Calendar event.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing (location_name, coordinates).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check if the URL is an OSM Calendar event URL
|
||||||
|
if not event_url.startswith(OSMCAL_EVENT_BASE_URL):
|
||||||
|
logger.warning(f"Not an OSM Calendar event URL: {event_url}")
|
||||||
|
return ("Unknown Location", [0, 0])
|
||||||
|
|
||||||
|
# Extract the event ID from the URL
|
||||||
|
event_id_match = re.search(r'event/(\d+)', event_url)
|
||||||
|
if not event_id_match:
|
||||||
|
logger.warning(f"Could not extract event ID from URL: {event_url}")
|
||||||
|
return ("Unknown Location", [0, 0])
|
||||||
|
|
||||||
|
event_id = event_id_match.group(1)
|
||||||
|
|
||||||
|
# Construct the iCal URL
|
||||||
|
ical_url = f"{OSMCAL_EVENT_BASE_URL}{event_id}.ics"
|
||||||
|
|
||||||
|
# Fetch the iCal content
|
||||||
|
logger.info(f"Fetching iCal data from: {ical_url}")
|
||||||
|
response = requests.get(ical_url)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
logger.warning(f"Failed to fetch iCal data: {response.status_code}")
|
||||||
|
return ("Unknown Location", [0, 0])
|
||||||
|
|
||||||
|
# Parse the iCal content
|
||||||
|
ical_content = response.text
|
||||||
|
|
||||||
|
# Extract GEO information
|
||||||
|
geo_match = re.search(r'GEO:([-+]?\d+\.\d+);([-+]?\d+\.\d+)', ical_content)
|
||||||
|
if geo_match:
|
||||||
|
# GEO format is latitude;longitude
|
||||||
|
latitude = float(geo_match.group(2))
|
||||||
|
longitude = float(geo_match.group(1))
|
||||||
|
coordinates = [longitude, latitude] # GeoJSON uses [longitude, latitude]
|
||||||
|
logger.info(f"Extracted coordinates from iCal: {coordinates}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"No GEO information found in iCal data for event: {event_id}")
|
||||||
|
coordinates = [0, 0]
|
||||||
|
|
||||||
|
# Extract LOCATION information
|
||||||
|
location_match = re.search(r'LOCATION:(.+?)(?:\r\n|\n|\r)', ical_content)
|
||||||
|
if location_match:
|
||||||
|
location_name = location_match.group(1).strip()
|
||||||
|
# Unescape backslash-escaped characters (e.g., \, becomes ,)
|
||||||
|
location_name = re.sub(r'\\(.)', r'\1', location_name)
|
||||||
|
logger.info(f"Extracted location from iCal: {location_name}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"No LOCATION information found in iCal data for event: {event_id}")
|
||||||
|
location_name = "Unknown Location"
|
||||||
|
|
||||||
|
return (location_name, coordinates)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching or parsing iCal data: {e}")
|
||||||
|
return ("Unknown Location", [0, 0])
|
||||||
|
|
||||||
def extract_location(description):
|
def extract_location(description):
|
||||||
"""
|
"""
|
||||||
Extract location information from the event description.
|
Extract location information from the event description.
|
||||||
|
@ -239,9 +325,25 @@ def create_event(item):
|
||||||
# Parse dates from the description
|
# Parse dates from the description
|
||||||
start_date, end_date = parse_event_dates(description)
|
start_date, end_date = parse_event_dates(description)
|
||||||
|
|
||||||
# Extract location information
|
# Extract location information from the description
|
||||||
location_name, coordinates = extract_location(description)
|
location_name, coordinates = extract_location(description)
|
||||||
|
|
||||||
|
# If we don't have coordinates and the link is to an OSM Calendar event,
|
||||||
|
# try to get coordinates and location from the iCal file
|
||||||
|
if coordinates == [0, 0] and link and link.startswith(OSMCAL_EVENT_BASE_URL):
|
||||||
|
logger.info(f"No coordinates found in description, trying to get from iCal: {link}")
|
||||||
|
ical_location_name, ical_coordinates = fetch_ical_data(link)
|
||||||
|
|
||||||
|
# Use iCal coordinates if available
|
||||||
|
if ical_coordinates != [0, 0]:
|
||||||
|
coordinates = ical_coordinates
|
||||||
|
logger.info(f"Using coordinates from iCal: {coordinates}")
|
||||||
|
|
||||||
|
# Use iCal location name if available and better than what we have
|
||||||
|
if ical_location_name != "Unknown Location":
|
||||||
|
location_name = ical_location_name
|
||||||
|
logger.info(f"Using location name from iCal: {location_name}")
|
||||||
|
|
||||||
# Create a descriptive label
|
# Create a descriptive label
|
||||||
label = title
|
label = title
|
||||||
|
|
||||||
|
@ -325,7 +427,7 @@ def event_exists(db, properties):
|
||||||
|
|
||||||
def submit_event(event):
|
def submit_event(event):
|
||||||
"""
|
"""
|
||||||
Submit an event to the OpenEventDatabase.
|
Submit an event to the OpenEventDatabase using the API.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event: A GeoJSON Feature representing the event.
|
event: A GeoJSON Feature representing the event.
|
||||||
|
@ -334,129 +436,53 @@ def submit_event(event):
|
||||||
bool: True if the event was successfully submitted, False otherwise.
|
bool: True if the event was successfully submitted, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Connect to the database
|
# Extract event properties for logging
|
||||||
db = db_connect()
|
|
||||||
|
|
||||||
# Extract event properties
|
|
||||||
properties = event['properties']
|
properties = event['properties']
|
||||||
|
|
||||||
# Check if the event already exists
|
# API endpoint for OpenEventDatabase
|
||||||
if event_exists(db, properties):
|
api_url = "https://api.openeventdatabase.org/event"
|
||||||
logger.info(f"Skipping event '{properties.get('label')}' as it already exists")
|
|
||||||
db.close()
|
# Make the API request
|
||||||
return False
|
logger.info(f"Submitting event '{properties.get('label')}' to API")
|
||||||
|
response = requests.post(
|
||||||
cur = db.cursor()
|
api_url,
|
||||||
geometry = json.dumps(event['geometry'])
|
headers={"Content-Type": "application/json"},
|
||||||
|
data=json.dumps(event)
|
||||||
print('event: ', event)
|
)
|
||||||
# Insert the geometry into the geo table
|
|
||||||
cur.execute("""
|
# Check if the request was successful
|
||||||
INSERT INTO geo
|
if response.status_code == 200 or response.status_code == 201:
|
||||||
SELECT geom, md5(st_astext(geom)) as hash, st_centroid(geom) as geom_center FROM
|
# Parse the response to get the event ID
|
||||||
(SELECT st_setsrid(st_geomfromgeojson(%s),4326) as geom) as g
|
response_data = response.json()
|
||||||
WHERE ST_IsValid(geom)
|
event_id = response_data.get('id')
|
||||||
ON CONFLICT DO NOTHING RETURNING hash;
|
|
||||||
""", (geometry,))
|
|
||||||
|
|
||||||
# Get the geometry hash
|
|
||||||
hash_result = cur.fetchone()
|
|
||||||
|
|
||||||
if hash_result is None:
|
|
||||||
# If the hash is None, check if the geometry already exists in the database
|
|
||||||
cur.execute("""
|
|
||||||
SELECT hash FROM geo
|
|
||||||
WHERE hash = md5(st_astext(st_setsrid(st_geomfromgeojson(%s),4326)));
|
|
||||||
""", (geometry,))
|
|
||||||
existing_hash = cur.fetchone()
|
|
||||||
|
|
||||||
if existing_hash:
|
if event_id:
|
||||||
# Geometry already exists in the database, use its hash
|
logger.success(f"Event created with ID: {event_id}")
|
||||||
geo_hash = existing_hash[0]
|
return True
|
||||||
logger.info(f"Using existing geometry with hash: {geo_hash}")
|
|
||||||
else:
|
else:
|
||||||
# Geometry doesn't exist, try to insert it directly
|
logger.warning(f"Event created but no ID returned in response")
|
||||||
cur.execute("""
|
return True
|
||||||
SELECT md5(st_astext(geom)) as hash,
|
|
||||||
ST_IsValid(geom),
|
|
||||||
ST_IsValidReason(geom) from (SELECT st_setsrid(st_geomfromgeojson(%s),4326) as geom) as g;
|
|
||||||
""", (geometry,))
|
|
||||||
hash_result = cur.fetchone()
|
|
||||||
|
|
||||||
if hash_result is None or not hash_result[1]:
|
|
||||||
logger.error(f"Invalid geometry for event: {properties.get('label')}")
|
|
||||||
if hash_result and len(hash_result) > 2:
|
|
||||||
logger.error(f"Reason: {hash_result[2]}")
|
|
||||||
db.close()
|
|
||||||
return False
|
|
||||||
|
|
||||||
geo_hash = hash_result[0]
|
|
||||||
|
|
||||||
# Now insert the geometry explicitly
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO geo (geom, hash, geom_center)
|
|
||||||
VALUES (
|
|
||||||
st_setsrid(st_geomfromgeojson(%s),4326),
|
|
||||||
%s,
|
|
||||||
st_centroid(st_setsrid(st_geomfromgeojson(%s),4326))
|
|
||||||
)
|
|
||||||
ON CONFLICT (hash) DO NOTHING;
|
|
||||||
""", (geometry, geo_hash, geometry))
|
|
||||||
|
|
||||||
# Verify the geometry was inserted
|
|
||||||
cur.execute("SELECT 1 FROM geo WHERE hash = %s", (geo_hash,))
|
|
||||||
if cur.fetchone() is None:
|
|
||||||
logger.error(f"Failed to insert geometry with hash: {geo_hash}")
|
|
||||||
db.close()
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(f"Inserted new geometry with hash: {geo_hash}")
|
|
||||||
else:
|
else:
|
||||||
geo_hash = hash_result[0]
|
logger.warning(f"Failed to create event: {properties.get('label')}. Status code: {response.status_code}")
|
||||||
|
logger.warning(f"Response: {response.text}")
|
||||||
# Determine the bounds for the time range
|
|
||||||
bounds = '[]' if properties['start'] == properties['stop'] else '[)'
|
|
||||||
|
|
||||||
# Insert the event into the database
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO events (events_type, events_what, events_when, events_tags, events_geo)
|
|
||||||
VALUES (%s, %s, tstzrange(%s, %s, %s), %s, %s)
|
|
||||||
ON CONFLICT DO NOTHING RETURNING events_id;
|
|
||||||
""", (
|
|
||||||
properties['type'],
|
|
||||||
properties['what'],
|
|
||||||
properties['start'],
|
|
||||||
properties['stop'],
|
|
||||||
bounds,
|
|
||||||
json.dumps(properties),
|
|
||||||
geo_hash
|
|
||||||
))
|
|
||||||
|
|
||||||
# Get the event ID
|
|
||||||
event_id = cur.fetchone()
|
|
||||||
|
|
||||||
if event_id:
|
|
||||||
logger.success(f"Event created with ID: {event_id[0]}")
|
|
||||||
db.commit()
|
|
||||||
db.close()
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.warning(f"Failed to create event: {properties.get('label')}")
|
|
||||||
db.close()
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error submitting event: {e}")
|
logger.error(f"Error submitting event: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def main():
|
def main(max_events=1, offset=0):
|
||||||
"""
|
"""
|
||||||
Main function to fetch OSM Calendar events and add them to the database.
|
Main function to fetch OSM Calendar events and add them to the OpenEventDatabase API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_events (int): Maximum number of events to insert (default: 1)
|
||||||
|
offset (int): Number of events to skip from the beginning of the RSS feed (default: 0)
|
||||||
|
|
||||||
The function will exit if the .env file doesn't exist, as it's required
|
The function will exit if the .env file doesn't exist, as it's required
|
||||||
for database connection parameters.
|
for environment variables.
|
||||||
"""
|
"""
|
||||||
logger.info("Starting OSM Calendar extractor")
|
logger.info(f"Starting OSM Calendar extractor (max_events={max_events}, offset={offset})")
|
||||||
|
|
||||||
# Load environment variables from .env file and check if it exists
|
# Load environment variables from .env file and check if it exists
|
||||||
if not load_env_from_file():
|
if not load_env_from_file():
|
||||||
|
@ -472,20 +498,42 @@ def main():
|
||||||
logger.warning("No events found, exiting")
|
logger.warning("No events found, exiting")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Apply offset and limit
|
||||||
|
if offset >= len(items):
|
||||||
|
logger.warning(f"Offset {offset} is greater than or equal to the number of events {len(items)}, no events to process")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Slice the items list according to offset and max_events
|
||||||
|
items_to_process = items[offset:offset + max_events]
|
||||||
|
logger.info(f"Processing {len(items_to_process)} events (offset={offset}, max_events={max_events})")
|
||||||
|
|
||||||
# Process each item
|
# Process each item
|
||||||
success_count = 0
|
success_count = 0
|
||||||
for item in items:
|
for item in items_to_process:
|
||||||
# Create an event from the item
|
# Create an event from the item
|
||||||
event = create_event(item)
|
event = create_event(item)
|
||||||
|
|
||||||
if not event:
|
if not event:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Submit the event to the database
|
# Submit the event to the API
|
||||||
if submit_event(event):
|
if submit_event(event):
|
||||||
success_count += 1
|
success_count += 1
|
||||||
|
|
||||||
logger.success(f"Successfully added {success_count} out of {len(items)} events to the database")
|
logger.success(f"Successfully added {success_count} out of {len(items_to_process)} events to the OpenEventDatabase")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
import argparse
|
||||||
|
|
||||||
|
# Set up command line argument parsing
|
||||||
|
parser = argparse.ArgumentParser(description='OSM Calendar Extractor for the OpenEventDatabase')
|
||||||
|
parser.add_argument('--max-events', type=int, default=1,
|
||||||
|
help='Maximum number of events to insert (default: 1)')
|
||||||
|
parser.add_argument('--offset', type=int, default=0,
|
||||||
|
help='Number of events to skip from the beginning of the RSS feed (default: 0)')
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Run the main function with the provided arguments
|
||||||
|
main(max_events=args.max_events, offset=args.offset)
|
|
@ -59,6 +59,8 @@ class DemoResource:
|
||||||
<title>Edit Event - OpenEventDatabase</title>
|
<title>Edit Event - OpenEventDatabase</title>
|
||||||
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
|
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
|
||||||
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
|
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
|
||||||
|
<script src="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
<style>
|
<style>
|
||||||
body {{
|
body {{
|
||||||
|
@ -231,7 +233,10 @@ class DemoResource:
|
||||||
<div class="note">Click on the map to set the event location</div>
|
<div class="note">Click on the map to set the event location</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit">Update Event</button>
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button type="submit">Update Event</button>
|
||||||
|
<button type="button" id="deleteButton" style="background-color: #dc3545;">Delete Event</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="result"></div>
|
<div id="result"></div>
|
||||||
|
@ -406,6 +411,50 @@ class DemoResource:
|
||||||
showResult(`Error: ${{error.message}}`, 'error');
|
showResult(`Error: ${{error.message}}`, 'error');
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
// Handle delete button click
|
||||||
|
document.getElementById('deleteButton').addEventListener('click', function() {{
|
||||||
|
// Get event ID
|
||||||
|
const eventId = document.getElementById('eventId').value;
|
||||||
|
|
||||||
|
// Show confirmation dialog
|
||||||
|
if (confirm('Are you sure you want to delete this event? This action cannot be undone.')) {{
|
||||||
|
// Submit delete request to API
|
||||||
|
fetch(`/event/${{eventId}}`, {{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {{
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}}
|
||||||
|
}})
|
||||||
|
.then(response => {{
|
||||||
|
if (response.ok) {{
|
||||||
|
showResult('Event deleted successfully', 'success');
|
||||||
|
|
||||||
|
// Add link to go back to map
|
||||||
|
const resultElement = document.getElementById('result');
|
||||||
|
resultElement.innerHTML += `<p><a href="/demo">Back to Map</a></p>`;
|
||||||
|
|
||||||
|
// Disable form controls
|
||||||
|
const formElements = document.querySelectorAll('#eventForm input, #eventForm select, #eventForm button');
|
||||||
|
formElements.forEach(element => {{
|
||||||
|
element.disabled = true;
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Redirect to demo page after 2 seconds
|
||||||
|
setTimeout(() => {{
|
||||||
|
window.location.href = '/demo';
|
||||||
|
}}, 2000);
|
||||||
|
}} else {{
|
||||||
|
return response.text().then(text => {{
|
||||||
|
throw new Error(text || response.statusText);
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
}})
|
||||||
|
.catch(error => {{
|
||||||
|
showResult(`Error deleting event: ${{error.message}}`, 'error');
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
}});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1940,5 +1989,76 @@ class DemoResource:
|
||||||
"""
|
"""
|
||||||
return demo_view_events.on_get(req, resp)
|
return demo_view_events.on_get(req, resp)
|
||||||
|
|
||||||
|
def on_get_by_id(self, req, resp, id):
|
||||||
|
"""
|
||||||
|
Handle GET requests to /demo/by_id/{id}.
|
||||||
|
Show a map with the event location and a table of its properties.
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
logger.info(f"Processing GET request to /demo/by_id/{id}")
|
||||||
|
try:
|
||||||
|
resp.content_type = 'text/html'
|
||||||
|
r = requests.get(f"https://api.openeventdatabase.org/event/{id}")
|
||||||
|
r.raise_for_status()
|
||||||
|
feature = r.json()
|
||||||
|
html = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\"fr\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\"UTF-8\">
|
||||||
|
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
|
||||||
|
<title>Event {id} - OpenEventDatabase</title>
|
||||||
|
<script src=\"https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js\"></script>
|
||||||
|
<link href=\"https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css\" rel=\"stylesheet\" />
|
||||||
|
<style>
|
||||||
|
body {{ margin:0; font-family: Arial, sans-serif; }}
|
||||||
|
.container {{ max-width: 1100px; margin: 0 auto; padding: 12px; }}
|
||||||
|
#map {{ width:100%; height: 360px; border:1px solid #ddd; border-radius:4px; }}
|
||||||
|
table {{ width:100%; border-collapse: collapse; margin-top:12px; }}
|
||||||
|
th, td {{ padding: 6px 8px; border-bottom: 1px solid #eee; text-align:left; }}
|
||||||
|
th {{ background:#f9fafb; }}
|
||||||
|
.nav a {{ margin-right: 10px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class=\"container\">
|
||||||
|
<div class=\"nav\">
|
||||||
|
<a href=\"/demo\">← Retour à la démo</a>
|
||||||
|
<a href=\"/demo/traffic\">Signaler trafic</a>
|
||||||
|
<a href=\"/demo/view-events\">Voir événements</a>
|
||||||
|
</div>
|
||||||
|
<h1>Évènement {id}</h1>
|
||||||
|
<div id=\"map\"></div>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Clé</th><th>Valeur</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{''.join([f'<tr><td>{k}</td><td>{(v if not isinstance(v, dict) else str(v))}</td></tr>' for k,v in sorted((feature.get('properties') or {{}}).items())])}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const f = {feature};
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: 'map',
|
||||||
|
style: 'https://tiles.openfreemap.org/styles/liberty',
|
||||||
|
center: f.geometry && f.geometry.coordinates ? f.geometry.coordinates : [2.3522,48.8566],
|
||||||
|
zoom: 12
|
||||||
|
});
|
||||||
|
map.addControl(new maplibregl.NavigationControl());
|
||||||
|
if (f.geometry && f.geometry.type === 'Point') {
|
||||||
|
new maplibregl.Marker().setLngLat(f.geometry.coordinates).addTo(map);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
resp.text = html
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
logger.success(f"Successfully processed GET request to /demo/by_id/{id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing GET request to /demo/by_id/{id}: {e}")
|
||||||
|
resp.status = falcon.HTTP_500
|
||||||
|
resp.text = f"Error: {str(e)}"
|
||||||
|
|
||||||
# Create a global instance of DemoResource
|
# Create a global instance of DemoResource
|
||||||
demo = DemoResource()
|
demo = DemoResource()
|
|
@ -50,6 +50,8 @@ class DemoMainResource:
|
||||||
<link rel="stylesheet" href="/static/demo_styles.css">
|
<link rel="stylesheet" href="/static/demo_styles.css">
|
||||||
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
|
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
|
||||||
<script src="/static/demo_auth.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>
|
<style>
|
||||||
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
||||||
.logo{
|
.logo{
|
||||||
|
@ -243,72 +245,43 @@ class DemoMainResource:
|
||||||
<h2>
|
<h2>
|
||||||
<img src="/static/oedb.png" class="logo" />
|
<img src="/static/oedb.png" class="logo" />
|
||||||
OpenEventDatabase Demo</h2>
|
OpenEventDatabase Demo</h2>
|
||||||
<p>This map shows current events from the OpenEventDatabase.</p>
|
|
||||||
|
|
||||||
<!-- Event addition buttons - always visible -->
|
<!-- 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/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/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 -->
|
<!-- Collapsible information section -->
|
||||||
<h3 id="info_panel_header" class="collapsible-header">Information Panel <span class="toggle-icon">▼</span></h3>
|
<br/>
|
||||||
<div id="info_panel_content" class="collapsible-content">
|
<br/>
|
||||||
<!-- User Information Panel -->
|
<!-- Filtres pour les événements -->
|
||||||
<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);">
|
<div class="event-filters" id="filters_panel" style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: 4px; display:none;">
|
||||||
<h3 style="margin-top: 0; margin-bottom: 10px; color: #333;">User Information</h3>
|
<h3 id="filters_header" style="margin-top: 0; color: #0078ff; cursor:pointer;">Filtres</h3>
|
||||||
<p>Username: <strong id="username-display">Anonymous</strong></p>
|
<div style="margin-top: 10px;">
|
||||||
<p>Points: <span id="points-display" style="font-weight: bold; color: #0078ff;">0</span></p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Authentication section -->
|
<div style="margin-top:12px; display:flex; align-items:center; gap:8px;">
|
||||||
<!--
|
<input type="checkbox" id="autoRefreshToggle" checked>
|
||||||
# <div id="auth-section" class="auth-section">
|
<label for="autoRefreshToggle" style="margin:0;">Rafraîchissement automatique (30s)</label>
|
||||||
# <h3>OpenStreetMap Authentication</h3>
|
</div>
|
||||||
#
|
|
||||||
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -320,6 +293,8 @@ class DemoMainResource:
|
||||||
const demoPagesList = document.getElementById('demo_pages_list');
|
const demoPagesList = document.getElementById('demo_pages_list');
|
||||||
const infoPanelHeader = document.getElementById('info_panel_header');
|
const infoPanelHeader = document.getElementById('info_panel_header');
|
||||||
const infoPanelContent = document.getElementById('info_panel_content');
|
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
|
// Fonction pour basculer l'affichage d'une liste ou section
|
||||||
function toggleList(header, list) {
|
function toggleList(header, list) {
|
||||||
|
@ -343,8 +318,98 @@ class DemoMainResource:
|
||||||
toggleList(endpointsHeader, endpointsList);
|
toggleList(endpointsHeader, endpointsList);
|
||||||
toggleList(demoPagesHeader, demoPagesList);
|
toggleList(demoPagesHeader, demoPagesList);
|
||||||
toggleList(infoPanelHeader, infoPanelContent);
|
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
|
// Map style URLs
|
||||||
const mapStyles = {
|
const mapStyles = {
|
||||||
default: 'https://tiles.openfreemap.org/styles/liberty',
|
default: 'https://tiles.openfreemap.org/styles/liberty',
|
||||||
|
@ -401,10 +466,50 @@ class DemoMainResource:
|
||||||
// Style switcher functionality
|
// Style switcher functionality
|
||||||
let currentStyle = 'default';
|
let currentStyle = 'default';
|
||||||
let eventsData = null;
|
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 = [];
|
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
|
// Fetch events when the map is loaded and every 30 seconds thereafter
|
||||||
|
@ -413,10 +518,18 @@ class DemoMainResource:
|
||||||
fetchEvents();
|
fetchEvents();
|
||||||
|
|
||||||
// Set up interval to fetch events every 30 seconds
|
// 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');
|
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 to fetch events from the API
|
||||||
function fetchEvents() {
|
function fetchEvents() {
|
||||||
|
@ -427,6 +540,8 @@ class DemoMainResource:
|
||||||
if (data.features && data.features.length > 0) {
|
if (data.features && data.features.length > 0) {
|
||||||
// Add events to the map
|
// Add events to the map
|
||||||
addEventsToMap(data);
|
addEventsToMap(data);
|
||||||
|
// Render histogram for retrieved events
|
||||||
|
try { renderEventsHistogram(data.features); } catch(e) { console.warn('Histogram error', e); }
|
||||||
|
|
||||||
// Fit map to events bounds
|
// Fit map to events bounds
|
||||||
fitMapToBounds(data);
|
fitMapToBounds(data);
|
||||||
|
@ -440,12 +555,49 @@ class DemoMainResource:
|
||||||
showErrorToast(`Erreur de chargement des événements: ${error.message}`);
|
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 to add events to the map
|
||||||
function addEventsToMap(geojson) {
|
function addEventsToMap(geojson) {
|
||||||
// Remove all existing markers
|
// Remove all existing markers
|
||||||
if (currentMarkers.length > 0) {
|
if (currentMarkers.length > 0) {
|
||||||
currentMarkers.forEach(marker => marker.remove());
|
currentMarkers.forEach(rec => rec.marker.remove());
|
||||||
currentMarkers = [];
|
currentMarkers = [];
|
||||||
console.log('Removed existing markers');
|
console.log('Removed existing markers');
|
||||||
}
|
}
|
||||||
|
@ -569,7 +721,12 @@ class DemoMainResource:
|
||||||
let iconColor = '#0078ff'; // Default color
|
let iconColor = '#0078ff'; // Default color
|
||||||
|
|
||||||
// Map event types to icons
|
// 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';
|
iconClass = 'cloud';
|
||||||
iconColor = '#00d1b2'; // Teal
|
iconColor = '#00d1b2'; // Teal
|
||||||
} else if (eventType.startsWith('traffic')) {
|
} else if (eventType.startsWith('traffic')) {
|
||||||
|
@ -607,10 +764,13 @@ class DemoMainResource:
|
||||||
.setLngLat(coordinates)
|
.setLngLat(coordinates)
|
||||||
.setPopup(popup)
|
.setPopup(popup)
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
|
|
||||||
// Store marker reference for later removal
|
// Store marker with its family for filtering
|
||||||
currentMarkers.push(marker);
|
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")
|
// 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
|
// Initialisation des gestionnaires d'événements pour le toast d'erreur
|
||||||
initErrorToast();
|
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
|
// Fonction pour initialiser le toast d'erreur
|
||||||
|
@ -892,7 +1058,21 @@ class DemoMainResource:
|
||||||
}, 6000);
|
}, 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -108,6 +108,9 @@ button:hover {
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
|
@ -120,6 +123,99 @@ button:hover {
|
||||||
text-decoration: underline;
|
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 */
|
/* Authentication section styles */
|
||||||
.auth-section {
|
.auth-section {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
|
@ -523,4 +619,10 @@ select:invalid {
|
||||||
.add-event-btn{
|
.add-event-btn{
|
||||||
float: left;
|
float: left;
|
||||||
width: 130px;
|
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';
|
const PANORAMAX_TOKEN_STORAGE_KEY = 'oedb_panoramax_token';
|
||||||
let mediaStream = null;
|
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() {
|
function setDefaultDates() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const nowISO = now.toISOString().slice(0, 16);
|
const nowISO = now.toISOString().slice(0, 16);
|
||||||
|
@ -70,8 +87,22 @@ function fetchExistingTrafficEvents() {
|
||||||
if (event.geometry && event.geometry.type === 'Point') {
|
if (event.geometry && event.geometry.type === 'Point') {
|
||||||
const coords = event.geometry.coordinates;
|
const coords = event.geometry.coordinates;
|
||||||
const needsRealityCheck = checkIfNeedsRealityCheck(event);
|
const needsRealityCheck = checkIfNeedsRealityCheck(event);
|
||||||
const markerColor = needsRealityCheck ? '#ff9800' : '#888888';
|
let markerColor = needsRealityCheck ? '#ff9800' : '#888888';
|
||||||
const em = new maplibregl.Marker({ color: markerColor }).setLngLat(coords).addTo(map);
|
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>`;
|
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) {
|
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>`;
|
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>`;
|
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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
setDefaultDates();
|
setDefaultDates();
|
||||||
initTabs();
|
initTabs();
|
||||||
initMap();
|
initMap();
|
||||||
updateUserInfoDisplay();
|
updateUserInfoDisplay();
|
||||||
|
initCollapsiblePanels();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contrôles Caméra
|
// Contrôles Caméra
|
||||||
|
|
|
@ -1,8 +1,26 @@
|
||||||
<div class="nav-links">
|
<div class="nav-container">
|
||||||
<a href="/demo">← Retour à la démo</a>
|
<button class="menu-toggle" aria-label="Toggle menu">
|
||||||
<a href="/demo/traffic">Signaler trafic</a>
|
<i class="fas fa-bars"></i>
|
||||||
<a href="/demo/view-events">Voir événements</a>
|
</button>
|
||||||
<a href="/demo/map-by-what">Carte par type</a>
|
<div class="nav-links">
|
||||||
<a href="/demo/stats">Stats</a>
|
<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>
|
</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)
|
|
@ -36,6 +36,8 @@ class EventFormResource:
|
||||||
<title>Add Event - OpenEventDatabase</title>
|
<title>Add Event - OpenEventDatabase</title>
|
||||||
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
|
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
|
||||||
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
|
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
|
||||||
|
<script src="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.css" type="text/css" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
406
oedb/resources/live.py
Normal file
406
oedb/resources/live.py
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
"""
|
||||||
|
Live page: shows last 7 days events from public OEDB API, refreshes every minute,
|
||||||
|
displays 10-minute bucket histogram and a table of events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
from oedb.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
class LiveResource:
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
logger.info("Processing GET request to /live")
|
||||||
|
resp.content_type = 'text/html'
|
||||||
|
html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OEDB Live - derniers événements</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
|
||||||
|
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 16px; font-family: Arial, sans-serif; background: #f6f7f9; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { margin: 0 0 12px; }
|
||||||
|
.controls { display: flex; align-items: center; gap: 8px; margin: 8px 0 16px; }
|
||||||
|
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px; margin-bottom: 16px; }
|
||||||
|
#chart { width: 100%; height: 320px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td { padding: 6px 8px; border-bottom: 1px solid #eee; text-align: left; font-size: 13px; }
|
||||||
|
th { background: #fafafa; }
|
||||||
|
.muted { color: #6b7280; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>
|
||||||
|
<a href="/demo">OEDB</a> Live
|
||||||
|
</h1>
|
||||||
|
<div class="controls">
|
||||||
|
<span>Période: 7 jours (rafraîchit chaque minute)</span>
|
||||||
|
<button id="refreshBtn">Rafraîchir</button>
|
||||||
|
<span id="lastUpdate" class="muted"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="display:flex; gap:12px; align-items:flex-start;">
|
||||||
|
<div style="flex:1 1 auto; min-width: 0;">
|
||||||
|
<canvas id="chart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div style="flex:0 0 240px; max-height: 360px; overflow:auto; border-left:1px solid #eee; padding-left:12px;">
|
||||||
|
<h3 style="margin:0 0 8px">Filtrer par type</h3>
|
||||||
|
<div style="margin-bottom:8px">
|
||||||
|
<button id="selectAllBtn">Tout cocher</button>
|
||||||
|
<button id="clearAllBtn">Tout décocher</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:8px">
|
||||||
|
<label style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<input type="checkbox" id="onlyRealityCheck">
|
||||||
|
<span>Seulement avec reality_check</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="filters"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div id="info_panel_content" class="">
|
||||||
|
<!-- 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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="margin:0 0 8px">Arbre des familles d'évènements</h2>
|
||||||
|
<div id="familiesGraph" style="width:100%; height:360px; border:1px solid #eee; border-radius:4px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="margin:0 0 8px">Derniers évènements</h2>
|
||||||
|
<div style="overflow:auto; max-height: 50vh;">
|
||||||
|
<table id="eventsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>What</th>
|
||||||
|
<th>Label</th>
|
||||||
|
<th>Start</th>
|
||||||
|
<th>Stop</th>
|
||||||
|
<th>Lon</th>
|
||||||
|
<th>Lat</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const API_URL = 'https://api.openeventdatabase.org/event?when=last7days&limit=2000';
|
||||||
|
let chart;
|
||||||
|
let allFeatures = [];
|
||||||
|
let familySet = new Set();
|
||||||
|
const familyToColor = new Map();
|
||||||
|
const pastel = ['#cce5ff','#c2f0f0','#d5f5e3','#fde2cf','#f6d5ff','#ffd6e7','#e3f2fd','#e8f5e9','#fff3e0','#f3e5f5','#f1f8e9','#e0f7fa','#fff8e1','#ede7f6','#fce4ec'];
|
||||||
|
|
||||||
|
function bucket10min(date) {
|
||||||
|
const d = new Date(date);
|
||||||
|
if (isNaN(d.getTime())) return null;
|
||||||
|
// round down to 10 minute
|
||||||
|
d.setSeconds(0, 0);
|
||||||
|
const m = d.getMinutes();
|
||||||
|
d.setMinutes(m - (m % 10));
|
||||||
|
return d.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFamily(p) {
|
||||||
|
const w = (p && p.what) ? String(p.what) : '';
|
||||||
|
return w.split('.')[0] || 'other';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureColors() {
|
||||||
|
let i = 0;
|
||||||
|
for (const fam of familySet) {
|
||||||
|
if (!familyToColor.has(fam)) {
|
||||||
|
familyToColor.set(fam, pastel[i % pastel.length]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStackedHistogram(features, enabledFamilies, onlyReality) {
|
||||||
|
const allBuckets = new Set();
|
||||||
|
const famBuckets = new Map();
|
||||||
|
for (const f of features) {
|
||||||
|
const fam = getFamily(f.properties);
|
||||||
|
if (enabledFamilies && !enabledFamilies.has(fam)) continue;
|
||||||
|
if (onlyReality && !(f.properties && f.properties['reality_check'])) continue;
|
||||||
|
const t = f.properties && (f.properties.createdate || f.properties.start || f.properties.lastupdate);
|
||||||
|
const bucket = bucket10min(t);
|
||||||
|
if (!bucket) continue;
|
||||||
|
allBuckets.add(bucket);
|
||||||
|
if (!famBuckets.has(fam)) famBuckets.set(fam, new Map());
|
||||||
|
const m = famBuckets.get(fam);
|
||||||
|
m.set(bucket, (m.get(bucket) || 0) + 1);
|
||||||
|
}
|
||||||
|
const labels = Array.from(allBuckets).sort();
|
||||||
|
const datasets = [];
|
||||||
|
for (const [fam, mapCounts] of famBuckets.entries()) {
|
||||||
|
const data = labels.map(k => mapCounts.get(k) || 0);
|
||||||
|
datasets.push({ label: fam, data, backgroundColor: familyToColor.get(fam) || '#ddd', stack: 'events' });
|
||||||
|
}
|
||||||
|
return { labels, datasets };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart(labels, datasets) {
|
||||||
|
const ctx = document.getElementById('chart');
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: { labels, datasets },
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: { stacked: true, ticks: { callback: (v, i) => new Date(labels[i]).toLocaleString() } },
|
||||||
|
y: { stacked: true, beginAtZero: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(features, enabledFamilies) {
|
||||||
|
const tbody = document.querySelector('#eventsTable tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
for (const f of features) {
|
||||||
|
const p = f.properties || {};
|
||||||
|
const fam = getFamily(p);
|
||||||
|
if (enabledFamilies && !enabledFamilies.has(fam)) continue;
|
||||||
|
const onlyReality = document.getElementById('onlyRealityCheck')?.checked;
|
||||||
|
if (onlyReality && !p['reality_check']) continue;
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.style.background = (familyToColor.get(fam) || '#fff');
|
||||||
|
let iconClass = 'info-circle';
|
||||||
|
let iconColor = '#0078ff';
|
||||||
|
const eventType = String(p.what || '');
|
||||||
|
const labelLower = String(p.label || '').toLowerCase();
|
||||||
|
if (labelLower.includes('travaux') || eventType.includes('roadwork')) { iconClass = 'hard-hat'; iconColor = '#ff9800'; }
|
||||||
|
else if (eventType.startsWith('weather')) { iconClass = 'cloud'; iconColor = '#00d1b2'; }
|
||||||
|
else if (eventType.startsWith('traffic')) { iconClass = 'car'; iconColor = '#ff3860'; }
|
||||||
|
else if (eventType.startsWith('sport')) { iconClass = 'futbol'; iconColor = '#3273dc'; }
|
||||||
|
else if (eventType.startsWith('culture')) { iconClass = 'theater-masks'; iconColor = '#ffdd57'; }
|
||||||
|
else if (eventType.startsWith('health')) { iconClass = 'heartbeat'; iconColor = '#ff3860'; }
|
||||||
|
else if (eventType.startsWith('education')) { iconClass = 'graduation-cap'; iconColor = '#3273dc'; }
|
||||||
|
else if (eventType.startsWith('politics')) { iconClass = 'landmark'; iconColor = '#209cee'; }
|
||||||
|
else if (eventType.startsWith('nature')) { iconClass = 'leaf'; iconColor = '#23d160'; }
|
||||||
|
const idHtml = p.id ? `<a href="/demo/by_id/${p.id}">${p.id}</a>` : '';
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td style="width:28px;text-align:center"><i class="fas fa-${iconClass}" style="color:${iconColor}"></i></td>
|
||||||
|
<td>${idHtml}</td>
|
||||||
|
<td>${p.what || ''}</td>
|
||||||
|
<td>${(p.label || '').toString().slice(0,120)}</td>
|
||||||
|
<td>${p.start || ''}</td>
|
||||||
|
<td>${p.stop || ''}</td>
|
||||||
|
<td>${f.geometry && f.geometry.coordinates ? f.geometry.coordinates[0] : ''}</td>
|
||||||
|
<td>${f.geometry && f.geometry.coordinates ? f.geometry.coordinates[1] : ''}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API_URL);
|
||||||
|
const data = await res.json();
|
||||||
|
allFeatures = (data && data.features) ? data.features : [];
|
||||||
|
familySet = new Set(allFeatures.map(f => getFamily(f.properties)));
|
||||||
|
ensureColors();
|
||||||
|
buildFilters();
|
||||||
|
applyFiltersAndRender();
|
||||||
|
try { renderFamiliesGraph(allFeatures); } catch(e) { console.warn('Graph error', e); }
|
||||||
|
document.getElementById('lastUpdate').textContent = 'Mise à jour: ' + new Date().toLocaleString();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilters() {
|
||||||
|
const cont = document.getElementById('filters');
|
||||||
|
cont.innerHTML = '';
|
||||||
|
const sorted = Array.from(familySet).sort();
|
||||||
|
for (const fam of sorted) {
|
||||||
|
const id = 'fam_' + fam.replace(/[^a-z0-9]/gi, '_');
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.style.marginBottom = '6px';
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<label style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<input type="checkbox" id="${id}" checked>
|
||||||
|
<span style="display:inline-block; width:12px; height:12px; background:${familyToColor.get(fam)}; border:1px solid #ddd"></span>
|
||||||
|
<span>${fam}</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
cont.appendChild(wrap);
|
||||||
|
document.getElementById(id).addEventListener('change', applyFiltersAndRender);
|
||||||
|
}
|
||||||
|
document.getElementById('selectAllBtn').onclick = () => { cont.querySelectorAll('input[type=checkbox]').forEach(c => { c.checked = true; }); applyFiltersAndRender(); };
|
||||||
|
document.getElementById('clearAllBtn').onclick = () => { cont.querySelectorAll('input[type=checkbox]').forEach(c => { c.checked = false; }); applyFiltersAndRender(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledFamilies() {
|
||||||
|
const cont = document.getElementById('filters');
|
||||||
|
const enabled = new Set();
|
||||||
|
cont.querySelectorAll('input[type=checkbox]').forEach(c => {
|
||||||
|
const lbl = c.closest('label');
|
||||||
|
const name = lbl && lbl.querySelector('span:last-child') ? lbl.querySelector('span:last-child').textContent : '';
|
||||||
|
if (c.checked && name) enabled.add(name);
|
||||||
|
});
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFiltersAndRender() {
|
||||||
|
const enabled = getEnabledFamilies();
|
||||||
|
const onlyReality = document.getElementById('onlyRealityCheck')?.checked;
|
||||||
|
const res = buildStackedHistogram(allFeatures, enabled, onlyReality);
|
||||||
|
renderChart(res.labels, res.datasets);
|
||||||
|
renderTable(allFeatures, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFamilyGraph(features) {
|
||||||
|
const seen = new Set();
|
||||||
|
const nodes = [];
|
||||||
|
const links = [];
|
||||||
|
function addNode(name) {
|
||||||
|
if (!seen.has(name)) {
|
||||||
|
seen.add(name);
|
||||||
|
const top = name.split('.')[0];
|
||||||
|
nodes.push({ id: name, group: top });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const whats = new Set();
|
||||||
|
features.forEach(f => { const w = f.properties && f.properties.what; if (w) whats.add(String(w)); });
|
||||||
|
whats.forEach(w => {
|
||||||
|
const parts = w.split('.');
|
||||||
|
let cur = '';
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
cur = i === 0 ? parts[0] : cur + '.' + parts[i];
|
||||||
|
addNode(cur);
|
||||||
|
if (i > 0) {
|
||||||
|
const parent = cur.slice(0, cur.lastIndexOf('.'));
|
||||||
|
addNode(parent);
|
||||||
|
links.push({ source: parent, target: cur });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { nodes, links };
|
||||||
|
}
|
||||||
|
|
||||||
|
let familiesSim = null;
|
||||||
|
function renderFamiliesGraph(features) {
|
||||||
|
const { nodes, links } = buildFamilyGraph(features);
|
||||||
|
const container = document.getElementById('familiesGraph');
|
||||||
|
const width = container.clientWidth || 800;
|
||||||
|
const height = container.clientHeight || 360;
|
||||||
|
container.innerHTML = '';
|
||||||
|
const svg = d3.select(container).append('svg').attr('width', width).attr('height', height);
|
||||||
|
const color = d => familyToColor.get(d.group) || '#bbb';
|
||||||
|
|
||||||
|
const link = svg.append('g').attr('stroke', '#aaa').attr('stroke-opacity', 0.7)
|
||||||
|
.selectAll('line').data(links).join('line').attr('stroke-width', 1.5);
|
||||||
|
|
||||||
|
const node = svg.append('g').attr('stroke', '#fff').attr('stroke-width', 1.5)
|
||||||
|
.selectAll('circle').data(nodes).join('circle')
|
||||||
|
.attr('r', d => d.id.indexOf('.') === -1 ? 8 : 5)
|
||||||
|
.attr('fill', color)
|
||||||
|
.call(d3.drag()
|
||||||
|
.on('start', (event, d) => { if (!event.active) familiesSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
||||||
|
.on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; })
|
||||||
|
.on('end', (event, d) => { if (!event.active) familiesSim.alphaTarget(0); d.fx = null; d.fy = null; }));
|
||||||
|
|
||||||
|
const labels = svg.append('g').selectAll('text').data(nodes).join('text')
|
||||||
|
.text(d => d.id)
|
||||||
|
.attr('font-size', '10px')
|
||||||
|
.attr('fill', '#333');
|
||||||
|
|
||||||
|
familiesSim = d3.forceSimulation(nodes)
|
||||||
|
.force('link', d3.forceLink(links).id(d => d.id).distance(d => d.target.id.indexOf('.') === -1 ? 40 : 25).strength(0.8))
|
||||||
|
.force('charge', d3.forceManyBody().strength(-120))
|
||||||
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||||
|
.on('tick', () => {
|
||||||
|
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
||||||
|
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
||||||
|
labels.attr('x', d => d.x + 10).attr('y', d => d.y + 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
const zoom = d3.zoom().scaleExtent([0.5, 5]).on('zoom', (event) => {
|
||||||
|
svg.selectAll('g').attr('transform', event.transform);
|
||||||
|
});
|
||||||
|
svg.call(zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', loadData);
|
||||||
|
loadData();
|
||||||
|
setInterval(loadData, 60 * 1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
resp.text = html
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
|
|
||||||
|
live = LiveResource()
|
||||||
|
|
||||||
|
|
86
oedb/resources/rss.py
Normal file
86
oedb/resources/rss.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
"""
|
||||||
|
RSS feeds for recent events and by family.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
import html
|
||||||
|
from datetime import datetime
|
||||||
|
from oedb.utils.logging import logger
|
||||||
|
from oedb.utils.db import db_connect
|
||||||
|
|
||||||
|
|
||||||
|
def _rss_header(title: str, link: str, desc: str) -> str:
|
||||||
|
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>{html.escape(title)}</title>
|
||||||
|
<link>{html.escape(link)}</link>
|
||||||
|
<description>{html.escape(desc)}</description>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _rss_footer() -> str:
|
||||||
|
return """
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_item(row) -> str:
|
||||||
|
events_id, events_tags, createdate = row
|
||||||
|
title = html.escape(str(events_tags.get('label') or events_tags.get('what') or f"event {events_id}"))
|
||||||
|
link = f"https://api.openeventdatabase.org/event/{events_id}"
|
||||||
|
guid = link
|
||||||
|
pubdate = datetime.fromisoformat(str(createdate)).strftime('%a, %d %b %Y %H:%M:%S %z') if createdate else ''
|
||||||
|
description = html.escape(str(events_tags))
|
||||||
|
return f"""
|
||||||
|
<item>
|
||||||
|
<title>{title}</title>
|
||||||
|
<link>{link}</link>
|
||||||
|
<guid>{guid}</guid>
|
||||||
|
<pubDate>{pubdate}</pubDate>
|
||||||
|
<description>{description}</description>
|
||||||
|
</item>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class RSSLatestResource:
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
logger.info("Processing GET /rss")
|
||||||
|
resp.content_type = 'application/rss+xml; charset=utf-8'
|
||||||
|
db = db_connect()
|
||||||
|
cur = db.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT events_id, events_tags, createdate
|
||||||
|
FROM events
|
||||||
|
ORDER BY createdate DESC
|
||||||
|
LIMIT 200
|
||||||
|
""")
|
||||||
|
items = ''.join(_row_to_item(r) for r in cur.fetchall())
|
||||||
|
xml = _rss_header('OEDB - Latest events', 'https://api.openeventdatabase.org/event', 'Latest 200 events') + items + _rss_footer()
|
||||||
|
resp.text = xml
|
||||||
|
|
||||||
|
|
||||||
|
class RSSByFamilyResource:
|
||||||
|
def on_get(self, req, resp, family: str):
|
||||||
|
logger.info(f"Processing GET /rss/by/{family}")
|
||||||
|
resp.content_type = 'application/rss+xml; charset=utf-8'
|
||||||
|
db = db_connect()
|
||||||
|
cur = db.cursor()
|
||||||
|
like = family + '%'
|
||||||
|
cur.execute("""
|
||||||
|
SELECT events_id, events_tags, createdate
|
||||||
|
FROM events
|
||||||
|
WHERE events_what LIKE %s
|
||||||
|
ORDER BY createdate DESC
|
||||||
|
LIMIT 200
|
||||||
|
""", (like,))
|
||||||
|
items = ''.join(_row_to_item(r) for r in cur.fetchall())
|
||||||
|
xml = _rss_header(f'OEDB - {family}', f'https://api.openeventdatabase.org/event?what={family}', f'Latest 200 events in {family}') + items + _rss_footer()
|
||||||
|
resp.text = xml
|
||||||
|
|
||||||
|
|
||||||
|
rss_latest = RSSLatestResource()
|
||||||
|
rss_by_family = RSSByFamilyResource()
|
||||||
|
|
||||||
|
|
48
test_osm_cal_api.py
Normal file
48
test_osm_cal_api.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for the OSM Calendar extractor with different parameter combinations.
|
||||||
|
This script tests the functionality of the osm_cal.py script with various
|
||||||
|
combinations of max_events and offset parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from extractors.osm_cal import main as osm_cal_main
|
||||||
|
|
||||||
|
def run_test(max_events, offset):
|
||||||
|
"""
|
||||||
|
Run the OSM Calendar extractor with the specified parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_events (int): Maximum number of events to insert
|
||||||
|
offset (int): Number of events to skip from the beginning of the RSS feed
|
||||||
|
"""
|
||||||
|
print(f"\n=== Testing with max_events={max_events}, offset={offset} ===")
|
||||||
|
osm_cal_main(max_events=max_events, offset=offset)
|
||||||
|
print("=== Test completed ===\n")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Run tests with different parameter combinations.
|
||||||
|
"""
|
||||||
|
print("Starting OSM Calendar API tests...")
|
||||||
|
|
||||||
|
# Test 1: Default parameters (max_events=1, offset=0)
|
||||||
|
run_test(1, 0)
|
||||||
|
|
||||||
|
# Test 2: Multiple events (max_events=3, offset=0)
|
||||||
|
run_test(3, 0)
|
||||||
|
|
||||||
|
# Test 3: With offset (max_events=2, offset=2)
|
||||||
|
run_test(2, 2)
|
||||||
|
|
||||||
|
# Test 4: Large offset (max_events=1, offset=10)
|
||||||
|
run_test(1, 10)
|
||||||
|
|
||||||
|
# Test 5: Large max_events (max_events=10, offset=0)
|
||||||
|
run_test(10, 0)
|
||||||
|
|
||||||
|
print("All tests completed.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
128
test_osm_cal_ical.py
Normal file
128
test_osm_cal_ical.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for the OSM Calendar extractor's iCal functionality.
|
||||||
|
|
||||||
|
This script tests the ability of the osm_cal.py script to extract
|
||||||
|
geographic coordinates and location information from iCal files
|
||||||
|
for events that don't have this information in the RSS feed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from extractors.osm_cal import fetch_ical_data, OSMCAL_EVENT_BASE_URL
|
||||||
|
from oedb.utils.logging import logger
|
||||||
|
|
||||||
|
def test_fetch_ical_data():
|
||||||
|
"""
|
||||||
|
Test the fetch_ical_data function with a real OSM Calendar event.
|
||||||
|
"""
|
||||||
|
print("\n=== Testing fetch_ical_data function ===")
|
||||||
|
|
||||||
|
# Test with a real OSM Calendar event URL
|
||||||
|
# Note: This event ID might not exist in the future, so the test might fail
|
||||||
|
event_url = f"{OSMCAL_EVENT_BASE_URL}3973"
|
||||||
|
|
||||||
|
print(f"Fetching iCal data for event: {event_url}")
|
||||||
|
location_name, coordinates = fetch_ical_data(event_url)
|
||||||
|
|
||||||
|
print(f"Location name: {location_name}")
|
||||||
|
print(f"Coordinates: {coordinates}")
|
||||||
|
|
||||||
|
if coordinates != [0, 0]:
|
||||||
|
print("✅ Successfully extracted coordinates from iCal")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to extract coordinates from iCal")
|
||||||
|
|
||||||
|
if location_name != "Unknown Location":
|
||||||
|
print("✅ Successfully extracted location name from iCal")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to extract location name from iCal")
|
||||||
|
|
||||||
|
print("=== Test completed ===\n")
|
||||||
|
|
||||||
|
return coordinates != [0, 0] and location_name != "Unknown Location"
|
||||||
|
|
||||||
|
def test_with_invalid_url():
|
||||||
|
"""
|
||||||
|
Test the fetch_ical_data function with an invalid URL.
|
||||||
|
"""
|
||||||
|
print("\n=== Testing fetch_ical_data with invalid URL ===")
|
||||||
|
|
||||||
|
# Test with an invalid event URL
|
||||||
|
event_url = f"{OSMCAL_EVENT_BASE_URL}999999999"
|
||||||
|
|
||||||
|
print(f"Fetching iCal data for invalid event: {event_url}")
|
||||||
|
location_name, coordinates = fetch_ical_data(event_url)
|
||||||
|
|
||||||
|
print(f"Location name: {location_name}")
|
||||||
|
print(f"Coordinates: {coordinates}")
|
||||||
|
|
||||||
|
if coordinates == [0, 0]:
|
||||||
|
print("✅ Correctly returned default coordinates for invalid URL")
|
||||||
|
else:
|
||||||
|
print("❌ Unexpectedly returned coordinates for invalid URL")
|
||||||
|
|
||||||
|
if location_name == "Unknown Location":
|
||||||
|
print("✅ Correctly returned default location name for invalid URL")
|
||||||
|
else:
|
||||||
|
print("❌ Unexpectedly returned location name for invalid URL")
|
||||||
|
|
||||||
|
print("=== Test completed ===\n")
|
||||||
|
|
||||||
|
return coordinates == [0, 0] and location_name == "Unknown Location"
|
||||||
|
|
||||||
|
def test_with_non_osmcal_url():
|
||||||
|
"""
|
||||||
|
Test the fetch_ical_data function with a non-OSM Calendar URL.
|
||||||
|
"""
|
||||||
|
print("\n=== Testing fetch_ical_data with non-OSM Calendar URL ===")
|
||||||
|
|
||||||
|
# Test with a non-OSM Calendar URL
|
||||||
|
event_url = "https://example.com/event/123"
|
||||||
|
|
||||||
|
print(f"Fetching iCal data for non-OSM Calendar URL: {event_url}")
|
||||||
|
location_name, coordinates = fetch_ical_data(event_url)
|
||||||
|
|
||||||
|
print(f"Location name: {location_name}")
|
||||||
|
print(f"Coordinates: {coordinates}")
|
||||||
|
|
||||||
|
if coordinates == [0, 0]:
|
||||||
|
print("✅ Correctly returned default coordinates for non-OSM Calendar URL")
|
||||||
|
else:
|
||||||
|
print("❌ Unexpectedly returned coordinates for non-OSM Calendar URL")
|
||||||
|
|
||||||
|
if location_name == "Unknown Location":
|
||||||
|
print("✅ Correctly returned default location name for non-OSM Calendar URL")
|
||||||
|
else:
|
||||||
|
print("❌ Unexpectedly returned location name for non-OSM Calendar URL")
|
||||||
|
|
||||||
|
print("=== Test completed ===\n")
|
||||||
|
|
||||||
|
return coordinates == [0, 0] and location_name == "Unknown Location"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Run all tests.
|
||||||
|
"""
|
||||||
|
print("Starting OSM Calendar iCal tests...")
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test1 = test_fetch_ical_data()
|
||||||
|
test2 = test_with_invalid_url()
|
||||||
|
test3 = test_with_non_osmcal_url()
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print("\n=== Test Summary ===")
|
||||||
|
print(f"Test 1 (fetch_ical_data): {'PASSED' if test1 else 'FAILED'}")
|
||||||
|
print(f"Test 2 (invalid URL): {'PASSED' if test2 else 'FAILED'}")
|
||||||
|
print(f"Test 3 (non-OSM Calendar URL): {'PASSED' if test3 else 'FAILED'}")
|
||||||
|
|
||||||
|
if test1 and test2 and test3:
|
||||||
|
print("\n✅ All tests PASSED")
|
||||||
|
else:
|
||||||
|
print("\n❌ Some tests FAILED")
|
||||||
|
|
||||||
|
print("All tests completed.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
19
uwsgi.ini
Normal file
19
uwsgi.ini
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[uwsgi]
|
||||||
|
http = :8080
|
||||||
|
http-websockets = true
|
||||||
|
wsgi-file = wsgi_websocket.py
|
||||||
|
chdir = .
|
||||||
|
virtualenv = venv
|
||||||
|
async = 100
|
||||||
|
ugreen = true
|
||||||
|
processes = 2
|
||||||
|
threads = 4
|
||||||
|
master = true
|
||||||
|
vacuum = true
|
||||||
|
die-on-term = true
|
||||||
|
pythonpath = .
|
||||||
|
buffer-size = 65535
|
||||||
|
harakiri = 30
|
||||||
|
max-requests = 1000
|
||||||
|
socket-timeout = 120
|
||||||
|
log-date = true
|
194
wsgi_websocket.py
Normal file
194
wsgi_websocket.py
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Configuration uWSGI pour l'intégration des WebSockets avec OEDB
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
from oedb.utils.logging import logger
|
||||||
|
|
||||||
|
# Import l'application Falcon principale
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from backend import app
|
||||||
|
|
||||||
|
# Import le gestionnaire WebSocket
|
||||||
|
from oedb.resources.demo.websocket import WebSocketManager
|
||||||
|
|
||||||
|
# Créer une instance du gestionnaire WebSocket
|
||||||
|
ws_manager = WebSocketManager()
|
||||||
|
|
||||||
|
# Gestionnaire de WebSocket pour uWSGI
|
||||||
|
def websocket_handler(environ, start_response):
|
||||||
|
"""Gestionnaire pour les connexions WebSocket."""
|
||||||
|
path = environ.get('PATH_INFO', '')
|
||||||
|
|
||||||
|
# Vérifier si c'est une requête WebSocket et qu'elle est sur le bon chemin
|
||||||
|
if path == '/ws' and environ.get('HTTP_UPGRADE', '').lower() == 'websocket':
|
||||||
|
try:
|
||||||
|
# Importer uwsgi ici car il n'est disponible qu'à l'exécution dans l'environnement uWSGI
|
||||||
|
import uwsgi
|
||||||
|
|
||||||
|
# Passer la requête au gestionnaire de WebSocket uWSGI
|
||||||
|
uwsgi.websocket_handshake()
|
||||||
|
logger.info(f"Nouvelle connexion WebSocket établie")
|
||||||
|
|
||||||
|
client_id = id(environ)
|
||||||
|
# Simuler la fonction handle_connection pour ce client
|
||||||
|
try:
|
||||||
|
# Ajouter le client à la liste
|
||||||
|
with ws_manager.lock:
|
||||||
|
ws_manager.clients[client_id] = {
|
||||||
|
'websocket': environ, # Utiliser environ comme proxy
|
||||||
|
'username': None,
|
||||||
|
'position': None,
|
||||||
|
'last_seen': time.time(),
|
||||||
|
'show_only_to_friends': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Envoyer la liste des utilisateurs connectés
|
||||||
|
users_list = []
|
||||||
|
with ws_manager.lock:
|
||||||
|
for client_info in ws_manager.clients.values():
|
||||||
|
if client_info.get('username'):
|
||||||
|
users_list.append({
|
||||||
|
'username': client_info['username'],
|
||||||
|
'timestamp': time.time()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Envoyer la liste des utilisateurs
|
||||||
|
uwsgi.websocket_send(json.dumps({
|
||||||
|
'type': 'users',
|
||||||
|
'users': users_list
|
||||||
|
}).encode('utf-8'))
|
||||||
|
|
||||||
|
# Boucle de traitement des messages
|
||||||
|
while True:
|
||||||
|
msg = uwsgi.websocket_recv()
|
||||||
|
data = json.loads(msg.decode('utf-8'))
|
||||||
|
message_type = data.get('type')
|
||||||
|
|
||||||
|
if message_type == 'position':
|
||||||
|
# Traiter la mise à jour de position
|
||||||
|
username = data.get('username')
|
||||||
|
position = data.get('position')
|
||||||
|
timestamp = data.get('timestamp')
|
||||||
|
show_only_to_friends = data.get('showOnlyToFriends', False)
|
||||||
|
|
||||||
|
# Mettre à jour les informations du client
|
||||||
|
with ws_manager.lock:
|
||||||
|
if client_id in ws_manager.clients:
|
||||||
|
ws_manager.clients[client_id]['username'] = username
|
||||||
|
ws_manager.clients[client_id]['position'] = position
|
||||||
|
ws_manager.clients[client_id]['last_seen'] = time.time()
|
||||||
|
ws_manager.clients[client_id]['show_only_to_friends'] = show_only_to_friends
|
||||||
|
|
||||||
|
# Mettre à jour dans le dictionnaire des positions
|
||||||
|
ws_manager.positions[username] = {
|
||||||
|
'position': position,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'show_only_to_friends': show_only_to_friends
|
||||||
|
}
|
||||||
|
|
||||||
|
# Diffuser à tous les autres clients
|
||||||
|
broadcast_position(username, position, timestamp, show_only_to_friends)
|
||||||
|
|
||||||
|
elif message_type in ['pouet', 'friendRequest']:
|
||||||
|
# Rediriger ces messages vers les destinataires
|
||||||
|
handle_direct_message(data, message_type)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur WebSocket: {e}")
|
||||||
|
finally:
|
||||||
|
# Supprimer le client de la liste
|
||||||
|
with ws_manager.lock:
|
||||||
|
if client_id in ws_manager.clients:
|
||||||
|
username = ws_manager.clients[client_id].get('username')
|
||||||
|
if username and username in ws_manager.positions:
|
||||||
|
del ws_manager.positions[username]
|
||||||
|
del ws_manager.clients[client_id]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur de connexion WebSocket: {e}")
|
||||||
|
return ['500 Internal Server Error', [], [b'WebSocket Error']]
|
||||||
|
|
||||||
|
# Si ce n'est pas une requête WebSocket, passer à l'application WSGI
|
||||||
|
return app(environ, start_response)
|
||||||
|
|
||||||
|
# Fonctions utilitaires pour les WebSockets uWSGI
|
||||||
|
def broadcast_position(username, position, timestamp, show_only_to_friends):
|
||||||
|
"""Diffuse la position d'un utilisateur à tous les autres utilisateurs."""
|
||||||
|
import uwsgi
|
||||||
|
|
||||||
|
message = json.dumps({
|
||||||
|
'type': 'position',
|
||||||
|
'username': username,
|
||||||
|
'position': position,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'showOnlyToFriends': show_only_to_friends
|
||||||
|
}).encode('utf-8')
|
||||||
|
|
||||||
|
with ws_manager.lock:
|
||||||
|
for client_id, client_info in ws_manager.clients.items():
|
||||||
|
# Ne pas envoyer à l'utilisateur lui-même
|
||||||
|
if client_info.get('username') == username:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Dans uWSGI, nous devons utiliser environ pour identifier le socket
|
||||||
|
if 'websocket' in client_info:
|
||||||
|
uwsgi.websocket_send(message)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur d'envoi de broadcast de position: {e}")
|
||||||
|
|
||||||
|
def handle_direct_message(data, message_type):
|
||||||
|
"""Traite un message direct (pouet ou demande d'ami)."""
|
||||||
|
import uwsgi
|
||||||
|
|
||||||
|
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 ws_manager.lock:
|
||||||
|
for client_id, client_info in ws_manager.clients.items():
|
||||||
|
if client_info.get('username') == to_user:
|
||||||
|
recipient_client_id = client_id
|
||||||
|
break
|
||||||
|
|
||||||
|
if recipient_client_id and recipient_client_id in ws_manager.clients:
|
||||||
|
# Préparer le message selon le type
|
||||||
|
if message_type == 'pouet':
|
||||||
|
msg = json.dumps({
|
||||||
|
'type': 'pouet',
|
||||||
|
'from': from_user,
|
||||||
|
'timestamp': data.get('timestamp')
|
||||||
|
}).encode('utf-8')
|
||||||
|
logger.info(f"Pouet pouet envoyé de {from_user} à {to_user}")
|
||||||
|
elif message_type == 'friendRequest':
|
||||||
|
msg = json.dumps({
|
||||||
|
'type': 'friendRequest',
|
||||||
|
'from': from_user,
|
||||||
|
'timestamp': data.get('timestamp')
|
||||||
|
}).encode('utf-8')
|
||||||
|
logger.info(f"Demande d'ami envoyée de {from_user} à {to_user}")
|
||||||
|
|
||||||
|
# Envoyer le message
|
||||||
|
try:
|
||||||
|
uwsgi.websocket_send(msg)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur d'envoi de message direct: {e}")
|
||||||
|
|
||||||
|
# Appliquer le patch au démarrage
|
||||||
|
patch_websocket_manager()
|
||||||
|
|
||||||
|
# Application WSGI pour uWSGI
|
||||||
|
application = websocket_handler
|
||||||
|
|
||||||
|
# Remarque: Pour utiliser ce fichier, démarrez uWSGI avec:
|
||||||
|
# uwsgi --http :8080 --http-websockets --wsgi-file wsgi_websocket.py --async 100 --ugreen
|
Loading…
Add table
Add a link
Reference in a new issue