🎉 First commit !!!
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
!**/__pycache__/
|
0
backend/README.md
Normal file
19
backend/docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
database:
|
||||||
|
image: postgres:15.1-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
logging:
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
max-file: "3"
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:5438:5432'
|
||||||
|
volumes:
|
||||||
|
- ./docker/database/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
|
||||||
|
- ./docker/database/data:/var/lib/postgresql/data
|
11
backend/docker/database/docker-entrypoint-initdb.d/init-user-db.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
CREATE USER idfm_matrix_bot;
|
||||||
|
CREATE DATABASE bot;
|
||||||
|
CREATE DATABASE idfm;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE bot TO idfm_matrix_bot;
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE idfm TO idfm_matrix_bot;
|
||||||
|
EOSQL
|
||||||
|
|
4
backend/idfm_matrix_backend/db/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .db import Database
|
||||||
|
from .base_class import Base
|
||||||
|
|
||||||
|
db = Database()
|
34
backend/idfm_matrix_backend/db/base_class.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
from typing import Iterable, Self
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
Base.db = None
|
||||||
|
|
||||||
|
|
||||||
|
async def base_add(cls, stops: Self | Iterable[Self]) -> bool:
|
||||||
|
try:
|
||||||
|
method = (
|
||||||
|
cls.db.session.add_all
|
||||||
|
if isinstance(stops, Iterable)
|
||||||
|
else cls.db.session.add
|
||||||
|
)
|
||||||
|
method(stops)
|
||||||
|
await cls.db.session.commit()
|
||||||
|
except IntegrityError as err:
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
|
||||||
|
Base.add = classmethod(base_add)
|
||||||
|
|
||||||
|
|
||||||
|
async def base_get_by_id(cls, id_: int | str) -> None | Base:
|
||||||
|
res = await cls.db.session.execute(select(cls).where(cls.id == id_))
|
||||||
|
element = res.scalar_one_or_none()
|
||||||
|
return element
|
||||||
|
|
||||||
|
|
||||||
|
Base.get_by_id = classmethod(base_get_by_id)
|
80
backend/idfm_matrix_backend/db/db.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from asyncio import gather as asyncio_gather
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
from typing import Callable, Iterable, Optional
|
||||||
|
|
||||||
|
from rich import print
|
||||||
|
from sqlalchemy import event, select, tuple_
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import (
|
||||||
|
selectinload,
|
||||||
|
sessionmaker,
|
||||||
|
with_polymorphic,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm.attributes import set_committed_value
|
||||||
|
|
||||||
|
from .base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
# import logging
|
||||||
|
|
||||||
|
# logging.basicConfig()
|
||||||
|
# logger = logging.getLogger("bot.sqltime")
|
||||||
|
# logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
# @event.listens_for(Engine, "before_cursor_execute")
|
||||||
|
# def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||||
|
# conn.info.setdefault("query_start_time", []).append(time())
|
||||||
|
# logger.debug("Start Query: %s", statement)
|
||||||
|
|
||||||
|
|
||||||
|
# @event.listens_for(Engine, "after_cursor_execute")
|
||||||
|
# def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||||
|
# total = time() - conn.info["query_start_time"].pop(-1)
|
||||||
|
# logger.debug("Query Complete!")
|
||||||
|
# logger.debug("Total Time: %f", total)
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._engine = None
|
||||||
|
self._session_maker = None
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self) -> None:
|
||||||
|
if self._session is None:
|
||||||
|
self._session = self._session_maker()
|
||||||
|
return self._session
|
||||||
|
|
||||||
|
def use_session(func: Callable):
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(self, *args, **kwargs):
|
||||||
|
if self._check_session() is not None:
|
||||||
|
return await func(self, *args, **kwargs)
|
||||||
|
# TODO: Raise an exception ?
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
async def connect(self, db_path: str, clear_static_data: bool = False) -> None:
|
||||||
|
# TODO: Preserve UserLastStopSearchResults table from drop.
|
||||||
|
self._engine = create_async_engine(db_path)
|
||||||
|
self._session_maker = sessionmaker(
|
||||||
|
self._engine, expire_on_commit=False, class_=AsyncSession
|
||||||
|
)
|
||||||
|
await self.session.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;")
|
||||||
|
|
||||||
|
async with self._engine.begin() as conn:
|
||||||
|
if clear_static_data:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
if self._session is not None:
|
||||||
|
await self._session.close()
|
||||||
|
self._session = None
|
||||||
|
await self._engine.dispose()
|
2
backend/idfm_matrix_backend/idfm_interface/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .idfm_interface import IdfmInterface
|
||||||
|
from .idfm_types import *
|
447
backend/idfm_matrix_backend/idfm_interface/idfm_interface.py
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from re import compile as re_compile
|
||||||
|
from time import time
|
||||||
|
from typing import ByteString, Iterable, List, Optional
|
||||||
|
|
||||||
|
from aiofiles import open as async_open
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
|
from msgspec import ValidationError
|
||||||
|
from msgspec.json import Decoder
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from ..db import Database
|
||||||
|
from ..models import Line, LinePicto, Stop, StopArea
|
||||||
|
from .idfm_types import (
|
||||||
|
IdfmLineState,
|
||||||
|
IdfmResponse,
|
||||||
|
Line as IdfmLine,
|
||||||
|
MonitoredVehicleJourney,
|
||||||
|
LinePicto as IdfmPicto,
|
||||||
|
IdfmState,
|
||||||
|
Stop as IdfmStop,
|
||||||
|
StopArea as IdfmStopArea,
|
||||||
|
StopAreaStopAssociation,
|
||||||
|
StopLineAsso as IdfmStopLineAsso,
|
||||||
|
Stops,
|
||||||
|
)
|
||||||
|
from .ratp_types import Picto as RatpPicto
|
||||||
|
|
||||||
|
|
||||||
|
class IdfmInterface:
|
||||||
|
|
||||||
|
IDFM_ROOT_URL = "https://prim.iledefrance-mobilites.fr/marketplace"
|
||||||
|
IDFM_STOP_MON_URL = f"{IDFM_ROOT_URL}/stop-monitoring"
|
||||||
|
|
||||||
|
IDFM_ROOT_URL = "https://data.iledefrance-mobilites.fr/explore/dataset"
|
||||||
|
IDFM_STOPS_URL = (
|
||||||
|
f"{IDFM_ROOT_URL}/arrets/download/?format=json&timezone=Europe/Berlin"
|
||||||
|
)
|
||||||
|
IDFM_PICTO_URL = f"{IDFM_ROOT_URL}/referentiel-des-lignes/files"
|
||||||
|
|
||||||
|
RATP_ROOT_URL = "https://data.ratp.fr/explore/dataset"
|
||||||
|
RATP_PICTO_URL = f"{RATP_ROOT_URL}/pictogrammes-des-lignes-de-metro-rer-tramway-bus-et-noctilien/files"
|
||||||
|
|
||||||
|
OPERATOR_RE = re_compile(r"[^:]+:Operator::([^:]+):")
|
||||||
|
LINE_RE = re_compile(r"[^:]+:Line::([^:]+):")
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, database: Database) -> None:
|
||||||
|
self._api_key = api_key
|
||||||
|
self._database = database
|
||||||
|
|
||||||
|
self._http_headers = {"Accept": "application/json", "apikey": self._api_key}
|
||||||
|
|
||||||
|
self._json_stops_decoder = Decoder(type=List[IdfmStop])
|
||||||
|
self._json_stop_areas_decoder = Decoder(type=List[IdfmStopArea])
|
||||||
|
self._json_lines_decoder = Decoder(type=List[IdfmLine])
|
||||||
|
self._json_stops_lines_assos_decoder = Decoder(type=List[IdfmStopLineAsso])
|
||||||
|
self._json_ratp_pictos_decoder = Decoder(type=List[RatpPicto])
|
||||||
|
self._json_stop_area_stop_asso_decoder = Decoder(
|
||||||
|
type=List[StopAreaStopAssociation]
|
||||||
|
)
|
||||||
|
|
||||||
|
self._response_json_decoder = Decoder(type=IdfmResponse)
|
||||||
|
|
||||||
|
async def startup(self) -> None:
|
||||||
|
BATCH_SIZE = 10000
|
||||||
|
STEPS = (
|
||||||
|
(
|
||||||
|
StopArea,
|
||||||
|
self._request_idfm_stop_areas,
|
||||||
|
IdfmInterface._format_idfm_stop_areas,
|
||||||
|
),
|
||||||
|
(Stop, self._request_idfm_stops, IdfmInterface._format_idfm_stops),
|
||||||
|
)
|
||||||
|
|
||||||
|
for model, get_method, format_method in STEPS:
|
||||||
|
step_begin_ts = time()
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
async for element in get_method():
|
||||||
|
elements.append(element)
|
||||||
|
|
||||||
|
if len(elements) == BATCH_SIZE:
|
||||||
|
await model.add(format_method(*elements))
|
||||||
|
elements.clear()
|
||||||
|
|
||||||
|
if elements:
|
||||||
|
await model.add(format_method(*elements))
|
||||||
|
|
||||||
|
print(f"Add {model.__name__}s: {time() - step_begin_ts}s")
|
||||||
|
|
||||||
|
begin_ts = time()
|
||||||
|
await self._load_lines()
|
||||||
|
print(f"Add Lines and IDFM LinePictos: {time() - begin_ts}s")
|
||||||
|
|
||||||
|
begin_ts = time()
|
||||||
|
await self._load_ratp_pictos(30)
|
||||||
|
print(f"Add RATP LinePictos: {time() - begin_ts}s")
|
||||||
|
|
||||||
|
begin_ts = time()
|
||||||
|
await self._load_lines_stops_assos()
|
||||||
|
print(f"Link Stops to Lines: {time() - begin_ts}s")
|
||||||
|
|
||||||
|
begin_ts = time()
|
||||||
|
await self._load_stop_areas_stops_assos()
|
||||||
|
print(f"Link Stops to StopAreas: {time() - begin_ts}s")
|
||||||
|
|
||||||
|
async def _load_lines(self, batch_size: int = 5000) -> None:
|
||||||
|
lines, pictos = [], []
|
||||||
|
picto_ids = set()
|
||||||
|
async for line in self._request_idfm_lines():
|
||||||
|
if (picto := line.fields.picto) is not None and picto.id_ not in picto_ids:
|
||||||
|
picto_ids.add(picto.id_)
|
||||||
|
pictos.append(picto)
|
||||||
|
|
||||||
|
lines.append(line)
|
||||||
|
if len(lines) == batch_size:
|
||||||
|
await LinePicto.add(IdfmInterface._format_idfm_pictos(*pictos))
|
||||||
|
await Line.add(await self._format_idfm_lines(*lines))
|
||||||
|
lines.clear()
|
||||||
|
pictos.clear()
|
||||||
|
|
||||||
|
if pictos:
|
||||||
|
await LinePicto.add(IdfmInterface._format_idfm_pictos(*pictos))
|
||||||
|
if lines:
|
||||||
|
await Line.add(await self._format_idfm_lines(*lines))
|
||||||
|
|
||||||
|
async def _load_ratp_pictos(self, batch_size: int = 5) -> None:
|
||||||
|
pictos = []
|
||||||
|
|
||||||
|
async for picto in self._request_ratp_pictos():
|
||||||
|
pictos.append(picto)
|
||||||
|
if len(pictos) == batch_size:
|
||||||
|
formatted_pictos = IdfmInterface._format_ratp_pictos(*pictos)
|
||||||
|
await LinePicto.add(formatted_pictos.values())
|
||||||
|
await Line.add_pictos(formatted_pictos)
|
||||||
|
pictos.clear()
|
||||||
|
|
||||||
|
if pictos:
|
||||||
|
formatted_pictos = IdfmInterface._format_ratp_pictos(*pictos)
|
||||||
|
await LinePicto.add(formatted_pictos.values())
|
||||||
|
await Line.add_pictos(formatted_pictos)
|
||||||
|
|
||||||
|
async def _load_lines_stops_assos(self, batch_size: int = 5000) -> None:
|
||||||
|
total_assos_nb = total_found_nb = 0
|
||||||
|
assos = []
|
||||||
|
async for asso in self._request_idfm_stops_lines_associations():
|
||||||
|
fields = asso.fields
|
||||||
|
try:
|
||||||
|
stop_id = int(fields.stop_id.rsplit(":", 1)[-1])
|
||||||
|
except ValueError as err:
|
||||||
|
print(err)
|
||||||
|
print(f"{fields.stop_id = }")
|
||||||
|
continue
|
||||||
|
|
||||||
|
assos.append((fields.route_long_name, fields.operatorname, stop_id))
|
||||||
|
if len(assos) == batch_size:
|
||||||
|
total_assos_nb += batch_size
|
||||||
|
total_found_nb += await Line.add_stops(assos)
|
||||||
|
assos.clear()
|
||||||
|
|
||||||
|
if assos:
|
||||||
|
total_assos_nb += len(assos)
|
||||||
|
total_found_nb += await Line.add_stops(assos)
|
||||||
|
|
||||||
|
print(f"{total_found_nb} line <-> stop ({total_assos_nb = } found)")
|
||||||
|
|
||||||
|
async def _load_stop_areas_stops_assos(self, batch_size: int = 5000) -> None:
|
||||||
|
total_assos_nb = total_found_nb = 0
|
||||||
|
assos = []
|
||||||
|
async for asso in self._request_idfm_stop_area_stop_associations():
|
||||||
|
fields = asso.fields
|
||||||
|
|
||||||
|
assos.append((int(fields.zdaid), int(fields.arrid)))
|
||||||
|
if len(assos) == batch_size:
|
||||||
|
total_assos_nb += batch_size
|
||||||
|
total_found_nb += await StopArea.add_stops(assos)
|
||||||
|
assos.clear()
|
||||||
|
|
||||||
|
if assos:
|
||||||
|
total_assos_nb += len(assos)
|
||||||
|
total_found_nb += await StopArea.add_stops(assos)
|
||||||
|
|
||||||
|
print(f"{total_found_nb} stop area <-> stop ({total_assos_nb = } found)")
|
||||||
|
|
||||||
|
async def _request_idfm_stops(self):
|
||||||
|
# headers = {"Accept": "application/json", "apikey": self._api_key}
|
||||||
|
# async with ClientSession(headers=headers) as session:
|
||||||
|
# async with session.get(self.STOPS_URL) as response:
|
||||||
|
# # print("Status:", response.status)
|
||||||
|
# if response.status == 200:
|
||||||
|
# for point in self._json_stops_decoder.decode(await response.read()):
|
||||||
|
# yield point
|
||||||
|
# TODO: Use HTTP
|
||||||
|
async with async_open("./tests/datasets/stops_dataset.json", "rb") as raw:
|
||||||
|
for element in self._json_stops_decoder.decode(await raw.read()):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
async def _request_idfm_stop_areas(self):
|
||||||
|
# TODO: Use HTTP
|
||||||
|
async with async_open("./tests/datasets/zones-d-arrets.json", "rb") as raw:
|
||||||
|
for element in self._json_stop_areas_decoder.decode(await raw.read()):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
async def _request_idfm_lines(self):
|
||||||
|
# TODO: Use HTTP
|
||||||
|
async with async_open("./tests/datasets/lines_dataset.json", "rb") as raw:
|
||||||
|
for element in self._json_lines_decoder.decode(await raw.read()):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
async def _request_idfm_stops_lines_associations(self):
|
||||||
|
# TODO: Use HTTP
|
||||||
|
async with async_open("./tests/datasets/arrets-lignes.json", "rb") as raw:
|
||||||
|
for element in self._json_stops_lines_assos_decoder.decode(
|
||||||
|
await raw.read()
|
||||||
|
):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
async def _request_idfm_stop_area_stop_associations(self):
|
||||||
|
# TODO: Use HTTP
|
||||||
|
async with async_open("./tests/datasets/relations.json", "rb") as raw:
|
||||||
|
for element in self._json_stop_area_stop_asso_decoder.decode(
|
||||||
|
await raw.read()
|
||||||
|
):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
async def _request_ratp_pictos(self):
|
||||||
|
# TODO: Use HTTP
|
||||||
|
async with async_open(
|
||||||
|
"./tests/datasets/pictogrammes-des-lignes-de-metro-rer-tramway-bus-et-noctilien.json",
|
||||||
|
"rb",
|
||||||
|
) as fd:
|
||||||
|
for element in self._json_ratp_pictos_decoder.decode(await fd.read()):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_idfm_pictos(cls, *pictos: IdfmPicto) -> Iterable[LinePicto]:
|
||||||
|
ret = []
|
||||||
|
|
||||||
|
for picto in pictos:
|
||||||
|
ret.append(
|
||||||
|
LinePicto(
|
||||||
|
id=picto.id_,
|
||||||
|
mime_type=picto.mimetype,
|
||||||
|
height_px=picto.height,
|
||||||
|
width_px=picto.width,
|
||||||
|
filename=picto.filename,
|
||||||
|
url=f"{cls.IDFM_PICTO_URL}/{picto.id_}/download",
|
||||||
|
thumbnail=picto.thumbnail,
|
||||||
|
format=picto.format,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_ratp_pictos(cls, *pictos: RatpPicto) -> dict[str, None | LinePicto]:
|
||||||
|
ret = {}
|
||||||
|
|
||||||
|
for picto in pictos:
|
||||||
|
if (fields := picto.fields.noms_des_fichiers) is not None:
|
||||||
|
ret[picto.fields.indices_commerciaux] = LinePicto(
|
||||||
|
id=fields.id_,
|
||||||
|
mime_type=f"image/{fields.format.lower()}",
|
||||||
|
height_px=fields.height,
|
||||||
|
width_px=fields.width,
|
||||||
|
filename=fields.filename,
|
||||||
|
url=f"{cls.RATP_PICTO_URL}/{fields.id_}/download",
|
||||||
|
thumbnail=fields.thumbnail,
|
||||||
|
format=fields.format,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def _format_idfm_lines(self, *lines: IdfmLine) -> Iterable[Line]:
|
||||||
|
ret = []
|
||||||
|
|
||||||
|
optional_value = IdfmLine.optional_value
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
fields = line.fields
|
||||||
|
|
||||||
|
picto_id = fields.picto.id_ if fields.picto is not None else None
|
||||||
|
picto = await LinePicto.get_by_id(picto_id) if picto_id else None
|
||||||
|
|
||||||
|
ret.append(
|
||||||
|
Line(
|
||||||
|
id=fields.id_line,
|
||||||
|
short_name=fields.shortname_line,
|
||||||
|
name=fields.name_line,
|
||||||
|
status=IdfmLineState(fields.status.value),
|
||||||
|
transport_mode=fields.transportmode.value,
|
||||||
|
transport_submode=optional_value(fields.transportsubmode),
|
||||||
|
network_name=optional_value(fields.networkname),
|
||||||
|
group_of_lines_id=optional_value(fields.id_groupoflines),
|
||||||
|
group_of_lines_shortname=optional_value(
|
||||||
|
fields.shortname_groupoflines
|
||||||
|
),
|
||||||
|
colour_web_hexa=fields.colourweb_hexa,
|
||||||
|
text_colour_hexa=fields.textcolourprint_hexa,
|
||||||
|
operator_id=optional_value(fields.operatorref),
|
||||||
|
operator_name=optional_value(fields.operatorname),
|
||||||
|
accessibility=fields.accessibility.value,
|
||||||
|
visual_signs_available=fields.visualsigns_available.value,
|
||||||
|
audible_signs_available=fields.audiblesigns_available.value,
|
||||||
|
picto_id=fields.picto.id_ if fields.picto is not None else None,
|
||||||
|
picto=picto,
|
||||||
|
record_id=line.recordid,
|
||||||
|
record_ts=int(line.record_timestamp.timestamp()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_idfm_stops(*stops: IdfmStop) -> Iterable[Stop]:
|
||||||
|
for stop in stops:
|
||||||
|
fields = stop.fields
|
||||||
|
try:
|
||||||
|
created_ts = int(fields.arrcreated.timestamp())
|
||||||
|
except AttributeError:
|
||||||
|
created_ts = None
|
||||||
|
yield Stop(
|
||||||
|
id=int(fields.arrid),
|
||||||
|
name=fields.arrname,
|
||||||
|
latitude=fields.arrgeopoint.lat,
|
||||||
|
longitude=fields.arrgeopoint.lon,
|
||||||
|
town_name=fields.arrtown,
|
||||||
|
postal_region=fields.arrpostalregion,
|
||||||
|
xepsg2154=fields.arrxepsg2154,
|
||||||
|
yepsg2154=fields.arryepsg2154,
|
||||||
|
transport_mode=fields.arrtype.value,
|
||||||
|
version=fields.arrversion,
|
||||||
|
created_ts=created_ts,
|
||||||
|
changed_ts=int(fields.arrchanged.timestamp()),
|
||||||
|
accessibility=fields.arraccessibility.value,
|
||||||
|
visual_signs_available=fields.arrvisualsigns.value,
|
||||||
|
audible_signs_available=fields.arraudiblesignals.value,
|
||||||
|
record_id=stop.recordid,
|
||||||
|
record_ts=int(stop.record_timestamp.timestamp()),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_idfm_stop_areas(*stop_areas: IdfmStopArea) -> Iterable[StopArea]:
|
||||||
|
for stop_area in stop_areas:
|
||||||
|
fields = stop_area.fields
|
||||||
|
try:
|
||||||
|
created_ts = int(fields.arrcreated.timestamp())
|
||||||
|
except AttributeError:
|
||||||
|
created_ts = None
|
||||||
|
yield StopArea(
|
||||||
|
id=int(fields.zdaid),
|
||||||
|
name=fields.zdaname,
|
||||||
|
town_name=fields.zdatown,
|
||||||
|
postal_region=fields.zdapostalregion,
|
||||||
|
xepsg2154=fields.zdaxepsg2154,
|
||||||
|
yepsg2154=fields.zdayepsg2154,
|
||||||
|
type=fields.zdatype.value,
|
||||||
|
version=fields.zdaversion,
|
||||||
|
created_ts=created_ts,
|
||||||
|
changed_ts=int(fields.zdachanged.timestamp()),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def render_line_picto(self, line: Line) -> tuple[None | str, None | str]:
|
||||||
|
begin_ts = time()
|
||||||
|
line_picto_path = line_picto_format = None
|
||||||
|
target = f"/tmp/{line.id}_repr"
|
||||||
|
|
||||||
|
picto = line.picto
|
||||||
|
if picto is not None:
|
||||||
|
picto_data = await self._get_line_picto(line)
|
||||||
|
async with async_open(target, "wb") as fd:
|
||||||
|
await fd.write(picto_data)
|
||||||
|
line_picto_path = target
|
||||||
|
line_picto_format = picto.mime_type
|
||||||
|
|
||||||
|
print(f"render_line_picto: {time() - begin_ts}")
|
||||||
|
return (line_picto_path, line_picto_format)
|
||||||
|
|
||||||
|
async def _get_line_picto(self, line: Line) -> Optional[ByteString]:
|
||||||
|
print("---------------------------------------------------------------------")
|
||||||
|
begin_ts = time()
|
||||||
|
data = None
|
||||||
|
|
||||||
|
picto = line.picto
|
||||||
|
if picto is not None:
|
||||||
|
headers = (
|
||||||
|
self._http_headers if picto.url.startswith(self.IDFM_ROOT_URL) else None
|
||||||
|
)
|
||||||
|
session_begin_ts = time()
|
||||||
|
async with ClientSession(headers=headers) as session:
|
||||||
|
session_creation_ts = time()
|
||||||
|
print(f"Session creation {session_creation_ts - session_begin_ts}")
|
||||||
|
async with session.get(picto.url) as response:
|
||||||
|
get_end_ts = time()
|
||||||
|
print(f"GET {get_end_ts - session_creation_ts}")
|
||||||
|
data = await response.read()
|
||||||
|
print(f"read {time() - get_end_ts}")
|
||||||
|
|
||||||
|
print(f"render_line_picto: {time() - begin_ts}")
|
||||||
|
print("---------------------------------------------------------------------")
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def get_next_passages(self, stop_point_id: str) -> Optional[IdfmResponse]:
|
||||||
|
# print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
||||||
|
begin_ts = time()
|
||||||
|
ret = None
|
||||||
|
params = {"MonitoringRef": f"STIF:StopPoint:Q:{stop_point_id}:"}
|
||||||
|
session_begin_ts = time()
|
||||||
|
async with ClientSession(headers=self._http_headers) as session:
|
||||||
|
session_creation_ts = time()
|
||||||
|
# print(f"Session creation {session_creation_ts - session_begin_ts}")
|
||||||
|
async with session.get(self.IDFM_STOP_MON_URL, params=params) as response:
|
||||||
|
get_end_ts = time()
|
||||||
|
# print(f"GET {get_end_ts - session_creation_ts}")
|
||||||
|
if response.status == 200:
|
||||||
|
get_end_ts = time()
|
||||||
|
# print(f"GET {get_end_ts - session_creation_ts}")
|
||||||
|
data = await response.read()
|
||||||
|
# print(data)
|
||||||
|
try:
|
||||||
|
ret = self._response_json_decoder.decode(data)
|
||||||
|
except ValidationError as err:
|
||||||
|
print(err)
|
||||||
|
# print(f"read {time() - get_end_ts}")
|
||||||
|
|
||||||
|
# print(f"get_next_passages: {time() - begin_ts}")
|
||||||
|
# print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def get_destinations(self, stop_point_id: str) -> Iterable[str]:
|
||||||
|
# TODO: Store in database the destination for the given stop and line id.
|
||||||
|
begin_ts = time()
|
||||||
|
destinations: dict[str, str] = {}
|
||||||
|
if (res := await self.get_next_passages(stop_point_id)) is not None:
|
||||||
|
for delivery in res.Siri.ServiceDelivery.StopMonitoringDelivery:
|
||||||
|
if delivery.Status == IdfmState.true:
|
||||||
|
for stop_visit in delivery.MonitoredStopVisit:
|
||||||
|
journey = stop_visit.MonitoredVehicleJourney
|
||||||
|
if (destination_name := journey.DestinationName) and (
|
||||||
|
line_ref := journey.LineRef
|
||||||
|
):
|
||||||
|
line_id = line_ref.value.replace("STIF:Line::", "")[:-1]
|
||||||
|
print(f"{line_id = }")
|
||||||
|
destinations[line_id] = destination_name[0].value
|
||||||
|
print(f"get_next_passages: {time() - begin_ts}")
|
||||||
|
return destinations
|
277
backend/idfm_matrix_backend/idfm_interface/idfm_types.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum, StrEnum
|
||||||
|
from typing import Any, Literal, Optional, NamedTuple
|
||||||
|
|
||||||
|
from msgspec import Struct
|
||||||
|
|
||||||
|
|
||||||
|
class Coordinate(NamedTuple):
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
|
||||||
|
|
||||||
|
class IdfmState(Enum):
|
||||||
|
unknown = "unknown"
|
||||||
|
false = "false"
|
||||||
|
partial = "partial"
|
||||||
|
true = "true"
|
||||||
|
|
||||||
|
|
||||||
|
class TrainStatus(Enum):
|
||||||
|
unknown = ""
|
||||||
|
arrived = "arrived"
|
||||||
|
onTime = "onTime"
|
||||||
|
delayed = "delayed"
|
||||||
|
noReport = "noReport"
|
||||||
|
early = "early"
|
||||||
|
cancelled = "cancelled"
|
||||||
|
undefined = "undefined"
|
||||||
|
|
||||||
|
|
||||||
|
class TransportMode(StrEnum):
|
||||||
|
bus = "bus"
|
||||||
|
tram = "tram"
|
||||||
|
metro = "metro"
|
||||||
|
rail = "rail"
|
||||||
|
funicular = "funicular"
|
||||||
|
|
||||||
|
|
||||||
|
class TransportSubMode(Enum):
|
||||||
|
unknown = "unknown"
|
||||||
|
|
||||||
|
localBus = "localBus"
|
||||||
|
regionalBus = "regionalBus"
|
||||||
|
highFrequencyBus = "highFrequencyBus"
|
||||||
|
expressBus = "expressBus"
|
||||||
|
nightBus = "nightBus"
|
||||||
|
demandAndResponseBus = "demandAndResponseBus"
|
||||||
|
airportLinkBus = "airportLinkBus"
|
||||||
|
|
||||||
|
regionalRail = "regionalRail"
|
||||||
|
railShuttle = "railShuttle"
|
||||||
|
suburbanRailway = "suburbanRailway"
|
||||||
|
|
||||||
|
local = "local"
|
||||||
|
|
||||||
|
|
||||||
|
class StopFields(Struct, kw_only=True):
|
||||||
|
arrgeopoint: Coordinate
|
||||||
|
arrtown: str
|
||||||
|
arrcreated: None | datetime = None
|
||||||
|
arryepsg2154: int
|
||||||
|
arrpostalregion: str
|
||||||
|
arrid: str
|
||||||
|
arrxepsg2154: int
|
||||||
|
arraccessibility: IdfmState
|
||||||
|
arrvisualsigns: IdfmState
|
||||||
|
arrtype: TransportMode
|
||||||
|
arrname: str
|
||||||
|
arrversion: str
|
||||||
|
arrchanged: datetime
|
||||||
|
arraudiblesignals: IdfmState
|
||||||
|
|
||||||
|
|
||||||
|
class Point(Struct):
|
||||||
|
coordinates: Coordinate
|
||||||
|
|
||||||
|
|
||||||
|
class Stop(Struct):
|
||||||
|
datasetid: str
|
||||||
|
recordid: str
|
||||||
|
fields: StopFields
|
||||||
|
record_timestamp: datetime
|
||||||
|
# geometry: Union[Point]
|
||||||
|
|
||||||
|
|
||||||
|
Stops = dict[str, Stop]
|
||||||
|
|
||||||
|
|
||||||
|
class StopAreaType(Enum):
|
||||||
|
metroStation = "metroStation"
|
||||||
|
onstreetBus = "onstreetBus"
|
||||||
|
onstreetTram = "onstreetTram"
|
||||||
|
railStation = "railStation"
|
||||||
|
|
||||||
|
|
||||||
|
class StopAreaFields(Struct, kw_only=True):
|
||||||
|
zdaname: str
|
||||||
|
zdcid: str
|
||||||
|
zdatown: str
|
||||||
|
zdaversion: str
|
||||||
|
zdaid: str
|
||||||
|
zdacreated: Optional[datetime] = None
|
||||||
|
zdatype: StopAreaType
|
||||||
|
zdayepsg2154: int
|
||||||
|
zdapostalregion: str
|
||||||
|
zdachanged: datetime
|
||||||
|
zdaxepsg2154: int
|
||||||
|
|
||||||
|
|
||||||
|
class StopArea(Struct):
|
||||||
|
datasetid: str
|
||||||
|
recordid: str
|
||||||
|
fields: StopAreaFields
|
||||||
|
record_timestamp: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class StopAreaStopAssociationFields(Struct, kw_only=True):
|
||||||
|
arrid: str # TODO: use int ?
|
||||||
|
artid: Optional[str] = None
|
||||||
|
arrversion: str
|
||||||
|
zdcid: str
|
||||||
|
version: int
|
||||||
|
zdaid: str
|
||||||
|
zdaversion: str
|
||||||
|
artversion: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class StopAreaStopAssociation(Struct):
|
||||||
|
datasetid: str
|
||||||
|
recordid: str
|
||||||
|
fields: StopAreaStopAssociationFields
|
||||||
|
record_timestamp: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class IdfmLineState(Enum):
|
||||||
|
active = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class LinePicto(Struct, rename={"id_": "id"}):
|
||||||
|
id_: str
|
||||||
|
mimetype: str
|
||||||
|
height: int
|
||||||
|
width: int
|
||||||
|
filename: str
|
||||||
|
thumbnail: bool
|
||||||
|
format: str
|
||||||
|
# color_summary: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class LineFields(Struct, kw_only=True):
|
||||||
|
name_line: str
|
||||||
|
status: IdfmLineState
|
||||||
|
accessibility: IdfmState
|
||||||
|
shortname_groupoflines: Optional[str] = None
|
||||||
|
transportmode: TransportMode
|
||||||
|
colourweb_hexa: str
|
||||||
|
textcolourprint_hexa: str
|
||||||
|
transportsubmode: Optional[TransportSubMode] = TransportSubMode.unknown
|
||||||
|
operatorref: Optional[str] = None
|
||||||
|
visualsigns_available: IdfmState
|
||||||
|
networkname: Optional[str] = None
|
||||||
|
id_line: str
|
||||||
|
id_groupoflines: Optional[str] = None
|
||||||
|
operatorname: Optional[str] = None
|
||||||
|
audiblesigns_available: IdfmState
|
||||||
|
shortname_line: str
|
||||||
|
picto: Optional[LinePicto] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Line(Struct):
|
||||||
|
datasetid: str
|
||||||
|
recordid: str
|
||||||
|
fields: LineFields
|
||||||
|
record_timestamp: datetime
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def optional_value(value: Any) -> Any:
|
||||||
|
if value:
|
||||||
|
return value.value if isinstance(value, Enum) else value
|
||||||
|
return "NULL"
|
||||||
|
|
||||||
|
|
||||||
|
Lines = dict[str, Line]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Set structs frozen
|
||||||
|
class StopLineAssoFields(Struct):
|
||||||
|
pointgeo: Coordinate
|
||||||
|
stop_id: str
|
||||||
|
stop_name: str
|
||||||
|
operatorname: str
|
||||||
|
nom_commune: str
|
||||||
|
route_long_name: str
|
||||||
|
id: str
|
||||||
|
stop_lat: str
|
||||||
|
stop_lon: str
|
||||||
|
code_insee: str
|
||||||
|
|
||||||
|
|
||||||
|
class StopLineAsso(Struct):
|
||||||
|
datasetid: str
|
||||||
|
recordid: str
|
||||||
|
fields: StopLineAssoFields
|
||||||
|
# geometry: Union[Point]
|
||||||
|
|
||||||
|
|
||||||
|
class Value(Struct):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class FramedVehicleJourney(Struct):
|
||||||
|
DataFrameRef: Value
|
||||||
|
DatedVehicleJourneyRef: str
|
||||||
|
|
||||||
|
|
||||||
|
class TrainNumber(Struct):
|
||||||
|
TrainNumberRef: list[Value]
|
||||||
|
|
||||||
|
|
||||||
|
class MonitoredCall(Struct, kw_only=True):
|
||||||
|
Order: Optional[int] = None
|
||||||
|
StopPointName: list[Value]
|
||||||
|
VehicleAtStop: bool
|
||||||
|
DestinationDisplay: list[Value]
|
||||||
|
AimedArrivalTime: Optional[datetime] = None
|
||||||
|
ExpectedArrivalTime: Optional[datetime] = None
|
||||||
|
ArrivalPlatformName: Optional[Value] = None
|
||||||
|
AimedDepartureTime: Optional[datetime] = None
|
||||||
|
ExpectedDepartureTime: Optional[datetime] = None
|
||||||
|
ArrivalStatus: TrainStatus = None
|
||||||
|
DepartureStatus: TrainStatus = None
|
||||||
|
|
||||||
|
|
||||||
|
class MonitoredVehicleJourney(Struct, kw_only=True):
|
||||||
|
LineRef: Value
|
||||||
|
OperatorRef: Value
|
||||||
|
FramedVehicleJourneyRef: FramedVehicleJourney
|
||||||
|
DestinationRef: Value
|
||||||
|
DestinationName: list[Value] | None = None
|
||||||
|
JourneyNote: list[Value] | None = None
|
||||||
|
TrainNumbers: Optional[TrainNumber] = None
|
||||||
|
MonitoredCall: MonitoredCall
|
||||||
|
|
||||||
|
|
||||||
|
class StopDelivery(Struct):
|
||||||
|
RecordedAtTime: datetime
|
||||||
|
ItemIdentifier: str
|
||||||
|
MonitoringRef: Value
|
||||||
|
MonitoredVehicleJourney: MonitoredVehicleJourney
|
||||||
|
|
||||||
|
|
||||||
|
class StopMonitoringDelivery(Struct):
|
||||||
|
ResponseTimestamp: datetime
|
||||||
|
Version: str
|
||||||
|
Status: IdfmState
|
||||||
|
MonitoredStopVisit: list[StopDelivery]
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceDelivery(Struct):
|
||||||
|
ResponseTimestamp: datetime
|
||||||
|
ProducerRef: str
|
||||||
|
ResponseMessageIdentifier: str
|
||||||
|
StopMonitoringDelivery: list[StopMonitoringDelivery]
|
||||||
|
|
||||||
|
|
||||||
|
class Siri(Struct):
|
||||||
|
ServiceDelivery: ServiceDelivery
|
||||||
|
|
||||||
|
|
||||||
|
class IdfmOperator(Enum):
|
||||||
|
SNCF = "SNCF"
|
||||||
|
|
||||||
|
|
||||||
|
class IdfmResponse(Struct):
|
||||||
|
Siri: Siri
|
25
backend/idfm_matrix_backend/idfm_interface/ratp_types.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from msgspec import Struct
|
||||||
|
|
||||||
|
|
||||||
|
class PictoFieldsFile(Struct, rename={"id_": "id"}):
|
||||||
|
id_: str
|
||||||
|
height: int
|
||||||
|
width: int
|
||||||
|
filename: str
|
||||||
|
thumbnail: bool
|
||||||
|
format: str
|
||||||
|
|
||||||
|
|
||||||
|
class PictoFields(Struct):
|
||||||
|
indices_commerciaux: str
|
||||||
|
noms_des_fichiers: Optional[PictoFieldsFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Picto(Struct):
|
||||||
|
datasetid: str
|
||||||
|
recordid: str
|
||||||
|
fields: PictoFields
|
||||||
|
record_timestamp: datetime
|
3
backend/idfm_matrix_backend/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .line import Line, LinePicto
|
||||||
|
from .stop import Stop, StopArea
|
||||||
|
from .user import UserLastStopSearchResults
|
176
backend/idfm_matrix_backend/models/line.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
from asyncio import gather as asyncio_gather
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Iterable, Self
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
BigInteger,
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
Enum,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
select,
|
||||||
|
String,
|
||||||
|
Table,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, relationship, selectinload
|
||||||
|
from sqlalchemy.orm.attributes import set_committed_value
|
||||||
|
from sqlalchemy.sql.expression import tuple_
|
||||||
|
|
||||||
|
from ..db import Base, db
|
||||||
|
from ..idfm_interface.idfm_types import (
|
||||||
|
IdfmState,
|
||||||
|
IdfmLineState,
|
||||||
|
TransportMode,
|
||||||
|
TransportSubMode,
|
||||||
|
)
|
||||||
|
from .stop import _Stop
|
||||||
|
|
||||||
|
line_stop_association_table = Table(
|
||||||
|
"line_stop_association_table",
|
||||||
|
Base.metadata,
|
||||||
|
Column("line_id", ForeignKey("lines.id")),
|
||||||
|
Column("stop_id", ForeignKey("_stops.id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LinePicto(Base):
|
||||||
|
|
||||||
|
db = db
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
mime_type = Column(String, nullable=False)
|
||||||
|
height_px = Column(Integer, nullable=False)
|
||||||
|
width_px = Column(Integer, nullable=False)
|
||||||
|
filename = Column(String, nullable=False)
|
||||||
|
url = Column(String, nullable=False)
|
||||||
|
thumbnail = Column(Boolean, nullable=False)
|
||||||
|
format = Column(String, nullable=False)
|
||||||
|
|
||||||
|
__tablename__ = "line_pictos"
|
||||||
|
|
||||||
|
|
||||||
|
class Line(Base):
|
||||||
|
|
||||||
|
db = db
|
||||||
|
|
||||||
|
id = Column(String, primary_key=True)
|
||||||
|
|
||||||
|
short_name = Column(String)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
status = Column(Enum(IdfmLineState), nullable=False)
|
||||||
|
transport_mode = Column(Enum(TransportMode), nullable=False)
|
||||||
|
transport_submode = Column(Enum(TransportSubMode), nullable=False)
|
||||||
|
|
||||||
|
network_name = Column(String)
|
||||||
|
group_of_lines_id = Column(String)
|
||||||
|
group_of_lines_shortname = Column(String)
|
||||||
|
|
||||||
|
colour_web_hexa = Column(String, nullable=False)
|
||||||
|
text_colour_hexa = Column(String, nullable=False)
|
||||||
|
|
||||||
|
operator_id = Column(String)
|
||||||
|
operator_name = Column(String)
|
||||||
|
|
||||||
|
accessibility = Column(Enum(IdfmState), nullable=False)
|
||||||
|
visual_signs_available = Column(Enum(IdfmState), nullable=False)
|
||||||
|
audible_signs_available = Column(Enum(IdfmState), nullable=False)
|
||||||
|
|
||||||
|
picto_id = Column(String, ForeignKey("line_pictos.id"))
|
||||||
|
picto: Mapped[LinePicto] = relationship(LinePicto, lazy="selectin")
|
||||||
|
|
||||||
|
record_id = Column(String, nullable=False)
|
||||||
|
record_ts = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
stops: Mapped[list["_Stop"]] = relationship(
|
||||||
|
"_Stop",
|
||||||
|
secondary=line_stop_association_table,
|
||||||
|
back_populates="lines",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
__tablename__ = "lines"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_by_name(
|
||||||
|
cls, name: str, operator_name: None | str = None
|
||||||
|
) -> list[Self]:
|
||||||
|
filters = {"name": name}
|
||||||
|
if operator_name is not None:
|
||||||
|
filters["operator_name"] = operator_name
|
||||||
|
|
||||||
|
lines = None
|
||||||
|
stmt = (
|
||||||
|
select(Line)
|
||||||
|
.filter_by(**filters)
|
||||||
|
.options(selectinload(Line.stops), selectinload(Line.picto))
|
||||||
|
)
|
||||||
|
res = await cls.db.session.execute(stmt)
|
||||||
|
lines = res.scalars().all()
|
||||||
|
return lines
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _add_picto_to_line(cls, line: str | Self, picto: LinePicto) -> None:
|
||||||
|
if isinstance(line, str):
|
||||||
|
if (lines := await cls.get_by_name(line)) is not None:
|
||||||
|
if len(lines) == 1:
|
||||||
|
line = lines[0]
|
||||||
|
else:
|
||||||
|
for candidate_line in lines:
|
||||||
|
if candidate_line.operator_name == "RATP":
|
||||||
|
line = candidate_line
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(line, Line) and line.picto is None:
|
||||||
|
line.picto = picto
|
||||||
|
line.picto_id = picto.id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def add_pictos(cls, line_to_pictos: dict[str | Self, LinePicto]) -> None:
|
||||||
|
await asyncio_gather(
|
||||||
|
*[
|
||||||
|
cls._add_picto_to_line(line, picto)
|
||||||
|
for line, picto in line_to_pictos.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await cls.db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def add_stops(cls, line_to_stop_ids: Iterable[tuple[str, str, str]]) -> int:
|
||||||
|
line_names_ops, stop_ids = set(), set()
|
||||||
|
for line_name, operator_name, stop_id in line_to_stop_ids:
|
||||||
|
line_names_ops.add((line_name, operator_name))
|
||||||
|
stop_ids.add(stop_id)
|
||||||
|
|
||||||
|
res = await cls.db.session.execute(
|
||||||
|
select(Line).where(
|
||||||
|
tuple_(Line.name, Line.operator_name).in_(line_names_ops)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = defaultdict(list)
|
||||||
|
for line in res.scalars():
|
||||||
|
lines[(line.name, line.operator_name)].append(line)
|
||||||
|
|
||||||
|
res = await cls.db.session.execute(select(_Stop).where(_Stop.id.in_(stop_ids)))
|
||||||
|
stops = {stop.id: stop for stop in res.scalars()}
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for line_name, operator_name, stop_id in line_to_stop_ids:
|
||||||
|
if (stop := stops.get(stop_id)) is not None:
|
||||||
|
if (stop_lines := lines.get((line_name, operator_name))) is not None:
|
||||||
|
if len(stop_lines) > 1:
|
||||||
|
print(stop_lines)
|
||||||
|
for stop_line in stop_lines:
|
||||||
|
stop_line.stops.append(stop)
|
||||||
|
found += 1
|
||||||
|
else:
|
||||||
|
print(f"No line found for {line_name}/{operator_name}")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"No stop found for {stop_id} id (used by {line_name}/{operator_name})"
|
||||||
|
)
|
||||||
|
|
||||||
|
await cls.db.session.commit()
|
||||||
|
return found
|
144
backend/idfm_matrix_backend/models/stop.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
from typing import Iterable, Self
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
BigInteger,
|
||||||
|
Column,
|
||||||
|
Enum,
|
||||||
|
Float,
|
||||||
|
ForeignKey,
|
||||||
|
select,
|
||||||
|
String,
|
||||||
|
Table,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, relationship, selectinload, with_polymorphic
|
||||||
|
from sqlalchemy.schema import Index
|
||||||
|
|
||||||
|
from ..db import Base, db
|
||||||
|
from ..idfm_interface.idfm_types import TransportMode, IdfmState, StopAreaType
|
||||||
|
|
||||||
|
stop_area_stop_association_table = Table(
|
||||||
|
"stop_area_stop_association_table",
|
||||||
|
Base.metadata,
|
||||||
|
Column("stop_id", ForeignKey("_stops.id")),
|
||||||
|
Column("stop_area_id", ForeignKey("stop_areas.id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Stop(Base):
|
||||||
|
|
||||||
|
db = db
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True)
|
||||||
|
kind = Column(String)
|
||||||
|
|
||||||
|
name = Column(String, nullable=False, index=True)
|
||||||
|
town_name = Column(String, nullable=False)
|
||||||
|
postal_region = Column(String, nullable=False)
|
||||||
|
xepsg2154 = Column(BigInteger, nullable=False)
|
||||||
|
yepsg2154 = Column(BigInteger, nullable=False)
|
||||||
|
version = Column(String, nullable=False)
|
||||||
|
created_ts = Column(BigInteger)
|
||||||
|
changed_ts = Column(BigInteger, nullable=False)
|
||||||
|
lines: Mapped[list["Line"]] = relationship(
|
||||||
|
"Line",
|
||||||
|
secondary="line_stop_association_table",
|
||||||
|
back_populates="stops",
|
||||||
|
# lazy="joined",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
areas: Mapped[list["StopArea"]] = relationship(
|
||||||
|
"StopArea", secondary=stop_area_stop_association_table, back_populates="stops"
|
||||||
|
)
|
||||||
|
|
||||||
|
__tablename__ = "_stops"
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "_stops", "polymorphic_on": kind}
|
||||||
|
__table_args__ = (
|
||||||
|
# To optimize the ilike requests
|
||||||
|
Index(
|
||||||
|
"name_idx_gin",
|
||||||
|
name,
|
||||||
|
postgresql_ops={"name": "gin_trgm_ops"},
|
||||||
|
postgresql_using="gin",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Test https://www.cybertec-postgresql.com/en/postgresql-more-performance-for-like-and-ilike-statements/
|
||||||
|
# TODO: Should be able to remove with_polymorphic ?
|
||||||
|
@classmethod
|
||||||
|
async def get_by_name(cls, name: str) -> list[Self]:
|
||||||
|
stop_stop_area = with_polymorphic(_Stop, [Stop, StopArea])
|
||||||
|
stmt = (
|
||||||
|
select(stop_stop_area)
|
||||||
|
.where(stop_stop_area.name.ilike(f"%{name}%"))
|
||||||
|
.options(
|
||||||
|
selectinload(stop_stop_area.areas),
|
||||||
|
selectinload(stop_stop_area.lines),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = await cls.db.session.execute(stmt)
|
||||||
|
return res.scalars()
|
||||||
|
|
||||||
|
|
||||||
|
class Stop(_Stop):
|
||||||
|
|
||||||
|
id = Column(BigInteger, ForeignKey("_stops.id"), primary_key=True)
|
||||||
|
|
||||||
|
latitude = Column(Float, nullable=False)
|
||||||
|
longitude = Column(Float, nullable=False)
|
||||||
|
transport_mode = Column(Enum(TransportMode), nullable=False)
|
||||||
|
accessibility = Column(Enum(IdfmState), nullable=False)
|
||||||
|
visual_signs_available = Column(Enum(IdfmState), nullable=False)
|
||||||
|
audible_signs_available = Column(Enum(IdfmState), nullable=False)
|
||||||
|
record_id = Column(String, nullable=False)
|
||||||
|
record_ts = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
__tablename__ = "stops"
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "stops", "polymorphic_load": "inline"}
|
||||||
|
|
||||||
|
|
||||||
|
class StopArea(_Stop):
|
||||||
|
|
||||||
|
id = Column(BigInteger, ForeignKey("_stops.id"), primary_key=True)
|
||||||
|
|
||||||
|
type = Column(Enum(StopAreaType), nullable=False)
|
||||||
|
stops: Mapped[list[_Stop]] = relationship(
|
||||||
|
_Stop,
|
||||||
|
secondary=stop_area_stop_association_table,
|
||||||
|
back_populates="areas",
|
||||||
|
lazy="selectin",
|
||||||
|
# lazy="joined",
|
||||||
|
)
|
||||||
|
|
||||||
|
__tablename__ = "stop_areas"
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "stop_areas", "polymorphic_load": "inline"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def add_stops(cls, stop_area_to_stop_ids: Iterable[tuple[str, str]]) -> int:
|
||||||
|
stop_area_ids, stop_ids = set(), set()
|
||||||
|
for stop_area_id, stop_id in stop_area_to_stop_ids:
|
||||||
|
stop_area_ids.add(stop_area_id)
|
||||||
|
stop_ids.add(stop_id)
|
||||||
|
|
||||||
|
res = await cls.db.session.execute(
|
||||||
|
select(StopArea)
|
||||||
|
.where(StopArea.id.in_(stop_area_ids))
|
||||||
|
.options(selectinload(StopArea.stops))
|
||||||
|
)
|
||||||
|
stop_areas = {stop_area.id: stop_area for stop_area in res.scalars()}
|
||||||
|
|
||||||
|
res = await cls.db.session.execute(select(_Stop).where(_Stop.id.in_(stop_ids)))
|
||||||
|
stops = {stop.id: stop for stop in res.scalars()}
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for stop_area_id, stop_id in stop_area_to_stop_ids:
|
||||||
|
if (stop_area := stop_areas.get(stop_area_id)) is not None:
|
||||||
|
if (stop := stops.get(stop_id)) is not None:
|
||||||
|
stop_area.stops.append(stop)
|
||||||
|
found += 1
|
||||||
|
else:
|
||||||
|
print(f"No stop found for {stop_id} id")
|
||||||
|
else:
|
||||||
|
print(f"No stop area found for {stop_area_id}")
|
||||||
|
|
||||||
|
await cls.db.session.commit()
|
||||||
|
return found
|
25
backend/idfm_matrix_backend/models/user.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey, String, Table
|
||||||
|
from sqlalchemy.orm import Mapped, relationship
|
||||||
|
|
||||||
|
from ..db import Base, db
|
||||||
|
from .stop import _Stop
|
||||||
|
|
||||||
|
user_last_stop_search_stops_associations_table = Table(
|
||||||
|
"user_last_stop_search_stops_associations_table",
|
||||||
|
Base.metadata,
|
||||||
|
Column("user_mxid", ForeignKey("user_last_stop_search_results.user_mxid")),
|
||||||
|
Column("stop_id", ForeignKey("_stops.id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLastStopSearchResults(Base):
|
||||||
|
|
||||||
|
db = db
|
||||||
|
|
||||||
|
__tablename__ = "user_last_stop_search_results"
|
||||||
|
|
||||||
|
user_mxid = Column(String, primary_key=True)
|
||||||
|
request_content = Column(String, nullable=False)
|
||||||
|
stops: Mapped[list[_Stop]] = relationship(
|
||||||
|
_Stop, secondary=user_last_stop_search_stops_associations_table
|
||||||
|
)
|
3
backend/idfm_matrix_backend/schemas/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .line import Line, TransportMode
|
||||||
|
from .next_passage import NextPassage, NextPassages
|
||||||
|
from .stop import Stop, StopArea
|
60
backend/idfm_matrix_backend/schemas/line.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from enum import StrEnum
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ..idfm_interface import (
|
||||||
|
IdfmLineState,
|
||||||
|
IdfmState,
|
||||||
|
TransportMode as IdfmTransportMode,
|
||||||
|
TransportSubMode as IdfmTransportSubMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TransportMode(StrEnum):
|
||||||
|
"""Computed transport mode from
|
||||||
|
idfm_interface.TransportMode and idfm_interface.TransportSubMode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bus = "bus"
|
||||||
|
tram = "tram"
|
||||||
|
metro = "metro"
|
||||||
|
funicular = "funicular"
|
||||||
|
# idfm_types.TransportMode.rail + idfm_types.TransportSubMode.regionalRail
|
||||||
|
rail_ter = "ter"
|
||||||
|
# idfm_types.TransportMode.rail + idfm_types.TransportSubMode.local
|
||||||
|
rail_rer = "rer"
|
||||||
|
# idfm_types.TransportMode.rail + idfm_types.TransportSubMode.suburbanRailway
|
||||||
|
rail_transilien = "transilien"
|
||||||
|
# idfm_types.TransportMode.rail + idfm_types.TransportSubMode.railShuttle
|
||||||
|
val = "val"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_idfm_transport_mode(
|
||||||
|
cls, mode: IdfmTransportMode, sub_mode: IdfmTransportSubMode
|
||||||
|
) -> Self:
|
||||||
|
if mode == IdfmTransportMode.rail:
|
||||||
|
if sub_mode == IdfmTransportSubMode.regionalRail:
|
||||||
|
return cls.rail_ter
|
||||||
|
if sub_mode == IdfmTransportSubMode.local:
|
||||||
|
return cls.rail_rer
|
||||||
|
if sub_mode == IdfmTransportSubMode.suburbanRailway:
|
||||||
|
return cls.rail_transilien
|
||||||
|
if sub_mode == IdfmTransportSubMode.railShuttle:
|
||||||
|
return cls.val
|
||||||
|
return TransportMode(mode)
|
||||||
|
|
||||||
|
|
||||||
|
class Line(BaseModel):
|
||||||
|
id: str
|
||||||
|
shortName: str
|
||||||
|
name: str
|
||||||
|
status: IdfmLineState
|
||||||
|
transportMode: TransportMode
|
||||||
|
backColorHexa: str
|
||||||
|
foreColorHexa: str
|
||||||
|
operatorId: str
|
||||||
|
accessibility: IdfmState
|
||||||
|
visualSignsAvailable: IdfmState
|
||||||
|
audibleSignsAvailable: IdfmState
|
||||||
|
stopIds: list[str]
|
22
backend/idfm_matrix_backend/schemas/next_passage.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ..idfm_interface.idfm_types import TrainStatus
|
||||||
|
|
||||||
|
|
||||||
|
class NextPassage(BaseModel):
|
||||||
|
line: str
|
||||||
|
operator: str
|
||||||
|
destinations: list[str]
|
||||||
|
atStop: bool
|
||||||
|
aimedArrivalTs: None | int
|
||||||
|
expectedArrivalTs: None | int
|
||||||
|
arrivalPlatformName: None | str
|
||||||
|
aimedDepartTs: None | int
|
||||||
|
expectedDepartTs: None | int
|
||||||
|
arrivalStatus: TrainStatus
|
||||||
|
departStatus: TrainStatus
|
||||||
|
|
||||||
|
|
||||||
|
class NextPassages(BaseModel):
|
||||||
|
ts: int
|
||||||
|
passages: dict[str, dict[str, list[NextPassage]]]
|
25
backend/idfm_matrix_backend/schemas/stop.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ..idfm_interface import IdfmLineState, IdfmState, StopAreaType, TransportMode
|
||||||
|
|
||||||
|
|
||||||
|
class Stop(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
town: str
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
# xepsg2154: int
|
||||||
|
# yepsg2154: int
|
||||||
|
lines: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class StopArea(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
town: str
|
||||||
|
# xepsg2154: int
|
||||||
|
# yepsg2154: int
|
||||||
|
type: StopAreaType
|
||||||
|
lines: list[str] # SNCF lines are linked to stop areas and not stops.
|
||||||
|
stops: list[Stop]
|
208
backend/main.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from idfm_matrix_backend.db import db
|
||||||
|
from idfm_matrix_backend.idfm_interface import IdfmInterface
|
||||||
|
from idfm_matrix_backend.models import Line, Stop, StopArea
|
||||||
|
from idfm_matrix_backend.schemas import (
|
||||||
|
Line as LineSchema,
|
||||||
|
TransportMode,
|
||||||
|
NextPassage as NextPassageSchema,
|
||||||
|
NextPassages as NextPassagesSchema,
|
||||||
|
Stop as StopSchema,
|
||||||
|
StopArea as StopAreaSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
API_KEY = environ.get("API_KEY")
|
||||||
|
# TODO: Add error message if no key is given.
|
||||||
|
|
||||||
|
# TODO: Remove postgresql+asyncpg from environ variable
|
||||||
|
DB_PATH = "postgresql+asyncpg://postgres:postgres@127.0.0.1:5438/idfm"
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[
|
||||||
|
"https://localhost:4443",
|
||||||
|
"https://localhost:3000",
|
||||||
|
],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
idfm_interface = IdfmInterface(API_KEY, db)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup():
|
||||||
|
# await db.connect(DB_PATH, clear_static_data=True)
|
||||||
|
# await idfm_interface.startup()
|
||||||
|
await db.connect(DB_PATH, clear_static_data=False)
|
||||||
|
print("Connected")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
await db.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
# /addwidget https://localhost:4443/static/#?widgetId=$matrix_widget_id&userId=$matrix_user_id
|
||||||
|
# /addwidget https://localhost:3000/widget?widgetId=$matrix_widget_id&userId=$matrix_user_id
|
||||||
|
STATIC_ROOT = "../frontend/"
|
||||||
|
app.mount("/widget", StaticFiles(directory=STATIC_ROOT, html=True), name="widget")
|
||||||
|
|
||||||
|
|
||||||
|
def optional_datetime_to_ts(dt: datetime) -> int | None:
|
||||||
|
return dt.timestamp() if dt else None
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/line/{line_id}", response_model=LineSchema)
|
||||||
|
async def get_line(line_id: str) -> JSONResponse:
|
||||||
|
line: Line | None = await Line.get_by_id(line_id)
|
||||||
|
|
||||||
|
if line is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f'Line "{line_id}" not found')
|
||||||
|
|
||||||
|
return LineSchema(
|
||||||
|
id=line.id,
|
||||||
|
shortName=line.short_name,
|
||||||
|
name=line.name,
|
||||||
|
status=line.status,
|
||||||
|
transportMode=TransportMode.from_idfm_transport_mode(
|
||||||
|
line.transport_mode, line.transport_submode
|
||||||
|
),
|
||||||
|
backColorHexa=line.colour_web_hexa,
|
||||||
|
foreColorHexa=line.text_colour_hexa,
|
||||||
|
operatorId=line.operator_id,
|
||||||
|
accessibility=line.accessibility,
|
||||||
|
visualSignsAvailable=line.visual_signs_available,
|
||||||
|
audibleSignsAvailable=line.audible_signs_available,
|
||||||
|
stopIds=[stop.id for stop in line.stops],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_stop(stop: Stop) -> StopSchema:
|
||||||
|
print(stop.__dict__)
|
||||||
|
return StopSchema(
|
||||||
|
id=stop.id,
|
||||||
|
name=stop.name,
|
||||||
|
town=stop.town_name,
|
||||||
|
# xepsg2154=stop.xepsg2154,
|
||||||
|
# yepsg2154=stop.yepsg2154,
|
||||||
|
lat=stop.latitude,
|
||||||
|
lon=stop.longitude,
|
||||||
|
lines=[line.id for line in stop.lines],
|
||||||
|
)
|
||||||
|
|
||||||
|
# châtelet
|
||||||
|
|
||||||
|
@app.get("/stop/")
|
||||||
|
async def get_stop(
|
||||||
|
name: str = "", limit: int = 10
|
||||||
|
) -> list[StopAreaSchema | StopSchema]:
|
||||||
|
# TODO: Add limit support
|
||||||
|
|
||||||
|
formatted = []
|
||||||
|
matching_stops = await Stop.get_by_name(name)
|
||||||
|
# print(matching_stops, flush=True)
|
||||||
|
|
||||||
|
stop_areas: dict[int, StopArea] = {}
|
||||||
|
stops: dict[int, Stop] = {}
|
||||||
|
for stop in matching_stops:
|
||||||
|
# print(f"{stop.__dict__ = }", flush=True)
|
||||||
|
dst = stop_areas if isinstance(stop, StopArea) else stops
|
||||||
|
dst[stop.id] = stop
|
||||||
|
|
||||||
|
for stop_area in stop_areas.values():
|
||||||
|
|
||||||
|
formatted_stops = []
|
||||||
|
for stop in stop_area.stops:
|
||||||
|
formatted_stops.append(_format_stop(stop))
|
||||||
|
try:
|
||||||
|
del stops[stop.id]
|
||||||
|
except KeyError as err:
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
formatted.append(
|
||||||
|
StopAreaSchema(
|
||||||
|
id=stop_area.id,
|
||||||
|
name=stop_area.name,
|
||||||
|
town=stop_area.town_name,
|
||||||
|
# xepsg2154=stop_area.xepsg2154,
|
||||||
|
# yepsg2154=stop_area.yepsg2154,
|
||||||
|
type=stop_area.type,
|
||||||
|
lines=[line.id for line in stop_area.lines],
|
||||||
|
stops=formatted_stops,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# print(f"{stops = }", flush=True)
|
||||||
|
formatted.extend(_format_stop(stop) for stop in stops.values())
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Cache response for 30 secs ?
|
||||||
|
@app.get("/stop/nextPassages/{stop_id}")
|
||||||
|
async def get_next_passages(stop_id: str) -> JSONResponse:
|
||||||
|
res = await idfm_interface.get_next_passages(stop_id)
|
||||||
|
|
||||||
|
# print(res)
|
||||||
|
|
||||||
|
service_delivery = res.Siri.ServiceDelivery
|
||||||
|
stop_monitoring_deliveries = service_delivery.StopMonitoringDelivery
|
||||||
|
|
||||||
|
by_line_by_dst_passages = defaultdict(lambda: defaultdict(list))
|
||||||
|
|
||||||
|
for delivery in stop_monitoring_deliveries:
|
||||||
|
for stop_visit in delivery.MonitoredStopVisit:
|
||||||
|
|
||||||
|
journey = stop_visit.MonitoredVehicleJourney
|
||||||
|
|
||||||
|
# re.match will return None if the given journey.LineRef.value is not valid.
|
||||||
|
try:
|
||||||
|
line_id = IdfmInterface.LINE_RE.match(journey.LineRef.value).group(1)
|
||||||
|
except AttributeError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f'Line "{journey.LineRef.value}" not found'
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
call = journey.MonitoredCall
|
||||||
|
|
||||||
|
dst_names = call.DestinationDisplay
|
||||||
|
dsts = [dst.value for dst in dst_names] if dst_names else []
|
||||||
|
|
||||||
|
print(f"{call.ArrivalPlatformName = }")
|
||||||
|
|
||||||
|
next_passage = NextPassageSchema(
|
||||||
|
line=line_id,
|
||||||
|
operator=journey.OperatorRef.value,
|
||||||
|
destinations=dsts,
|
||||||
|
atStop=call.VehicleAtStop,
|
||||||
|
aimedArrivalTs=optional_datetime_to_ts(call.AimedArrivalTime),
|
||||||
|
expectedArrivalTs=optional_datetime_to_ts(call.ExpectedArrivalTime),
|
||||||
|
arrivalPlatformName=call.ArrivalPlatformName.value if call.ArrivalPlatformName else None,
|
||||||
|
aimedDepartTs=optional_datetime_to_ts(call.AimedDepartureTime),
|
||||||
|
expectedDepartTs=optional_datetime_to_ts(call.ExpectedDepartureTime),
|
||||||
|
arrivalStatus=call.ArrivalStatus.value,
|
||||||
|
departStatus=call.DepartureStatus.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
by_line_passages = by_line_by_dst_passages[line_id]
|
||||||
|
# TODO: by_line_passages[dst].extend(dsts) instead ?
|
||||||
|
for dst in dsts:
|
||||||
|
by_line_passages[dst].append(next_passage)
|
||||||
|
|
||||||
|
return NextPassagesSchema(
|
||||||
|
ts=service_delivery.ResponseTimestamp.timestamp(),
|
||||||
|
passages=by_line_by_dst_passages,
|
||||||
|
)
|
58
backend/pyproject.toml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "idfm-matrix-widget"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Adrien SUEUR <me@adrien.run>"]
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [{include = "idfm_matrix_backend"}]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.11"
|
||||||
|
aiohttp = "^3.8.3"
|
||||||
|
rich = "^12.6.0"
|
||||||
|
aiofiles = "^22.1.0"
|
||||||
|
sqlalchemy = {extras = ["asyncio"], version = "^1.4.46"}
|
||||||
|
fastapi = "^0.88.0"
|
||||||
|
uvicorn = "^0.20.0"
|
||||||
|
asyncpg = "^0.27.0"
|
||||||
|
msgspec = "^0.12.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
mypy = "^0.971"
|
||||||
|
pylsp-mypy = "^0.6.2"
|
||||||
|
autopep8 = "^1.7.0"
|
||||||
|
mccabe = "^0.7.0"
|
||||||
|
pycodestyle = "^2.9.1"
|
||||||
|
pydocstyle = "^6.1.1"
|
||||||
|
pyflakes = "^2.5.0"
|
||||||
|
pylint = "^2.14.5"
|
||||||
|
rope = "^1.3.0"
|
||||||
|
python-lsp-server = {extras = ["yapf"], version = "^1.5.0"}
|
||||||
|
python-lsp-black = "^1.2.1"
|
||||||
|
black = "^22.10.0"
|
||||||
|
whatthepatch = "^1.0.2"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
types-aiofiles = "^22.1.0.2"
|
||||||
|
sqlalchemy-stubs = "^0.4"
|
||||||
|
wrapt = "^1.14.1"
|
||||||
|
pydocstyle = "^6.2.2"
|
||||||
|
pylint = "^2.15.9"
|
||||||
|
dill = "^0.3.6"
|
||||||
|
|
||||||
|
[tool.pylsp-mypy]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
plugins = "sqlmypy"
|
||||||
|
|
||||||
|
[pycodestyle]
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
[pylint]
|
||||||
|
max-line-length = 100
|
||||||
|
|
23
frontend/.eslintrc.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:solid/typescript",
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"solid"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
3
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
package-lock.json
|
34
frontend/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||||
|
|
||||||
|
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm dev` or `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.<br>
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
16
frontend/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
||||||
|
<title>Métro-Boulot-Dodo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
frontend/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-template-solid",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"engine": "19.3.0",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"dev": "vite --debug",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||||
|
"eslint": "^8.32.0",
|
||||||
|
"eslint-plugin-solid": "^0.9.3",
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"typescript-eslint-language-service": "^5.0.0",
|
||||||
|
"vite": "^4.0.3",
|
||||||
|
"vite-plugin-solid": "^2.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hope-ui/solid": "^0.6.7",
|
||||||
|
"@motionone/solid": "^10.15.5",
|
||||||
|
"@solid-primitives/date": "^2.0.5",
|
||||||
|
"@stitches/core": "^1.2.8",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
|
"leaflet": "^1.9.3",
|
||||||
|
"matrix-widget-api": "^1.1.1",
|
||||||
|
"solid-js": "^1.6.6",
|
||||||
|
"solid-transition-group": "^0.0.10"
|
||||||
|
}
|
||||||
|
}
|
1
frontend/public/Trafic_fluide_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46469 283.46466"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#00643c;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M141.73236,0C63.58334,0,0,63.57928,0,141.73236c0,78.149,63.58334,141.7323,141.73236,141.7323,78.15305,0,141.73233-63.58331,141.73233-141.7323C283.46469,63.57928,219.88541,0,141.73236,0Zm.00018,265.09479A123.36231,123.36231,0,1,1,265.09436,141.73248,123.5019,123.5019,0,0,1,141.73254,265.09479Z"/><circle class="cls-2" cx="141.73528" cy="141.73235" r="123.36528"/></g></g></svg>
|
After Width: | Height: | Size: 618 B |
1
frontend/public/Trafic_perturbe_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46469 283.46466"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#ffbe00;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M141.73236,0C63.58334,0,0,63.57928,0,141.73236c0,78.149,63.58334,141.7323,141.73236,141.7323,78.15305,0,141.73233-63.58331,141.73233-141.7323C283.46469,63.57928,219.88541,0,141.73236,0Zm.00018,265.09479A123.36231,123.36231,0,1,1,265.09436,141.73248,123.5019,123.5019,0,0,1,141.73254,265.09479Z"/><circle class="cls-2" cx="141.73528" cy="141.73235" r="123.36528"/></g></g></svg>
|
After Width: | Height: | Size: 618 B |
1
frontend/public/Trafic_tres_perturbe_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46469 283.46466"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#ff5a00;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M141.73236,0C63.58334,0,0,63.57928,0,141.73236c0,78.149,63.58334,141.7323,141.73236,141.7323,78.15305,0,141.73233-63.58331,141.73233-141.7323C283.46469,63.57928,219.88541,0,141.73236,0Zm.00018,265.09479A123.36231,123.36231,0,1,1,265.09436,141.73248,123.5019,123.5019,0,0,1,141.73254,265.09479Z"/><circle class="cls-2" cx="141.73528" cy="141.73235" r="123.36528"/></g></g></svg>
|
After Width: | Height: | Size: 618 B |
BIN
frontend/public/fonts/IDFVoyageur-Bold.otf
Normal file
BIN
frontend/public/fonts/IDFVoyageur-Medium.otf
Normal file
BIN
frontend/public/fonts/IDFVoyageur-Regular.otf
Normal file
1
frontend/public/symbole_bus_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><g id="Layer_1-2"><g><path d="M244.14,161.22c0-14.53-8.15-23.03-18.42-28.7l-7.8-4.25c-4.6-2.48-8.5-6.38-8.5-11.34,0-5.67,4.61-9.56,11.34-9.56,4.61,0,8.86,1.77,12.05,3.19,4.96,2.48,8.15-.71,9.57-6.02,1.77-6.03-.71-8.15-4.96-10.27-4.25-2.13-10.27-3.55-16.29-3.55-16.66,0-30.48,10.98-30.48,28.7,0,13.82,7.08,21.62,18.43,27.99l8.15,4.61c4.96,2.84,8.15,6.74,8.15,12.05,0,6.73-6.38,9.92-12.76,9.92-4.96,0-10.98-2.13-14.18-3.55-5.31-2.48-8.15,.71-9.92,6.38-1.76,6.02,1.07,8.51,5.67,10.63,4.25,2.12,11.69,3.89,18.43,3.89,17.36,0,31.53-11.69,31.53-30.12m-62-3.55v-58.82c0-4.96-4.96-7.44-9.57-7.44-4.96,0-9.56,2.48-9.56,7.44v56.34c0,11.34-3.55,18.78-12.76,18.78s-13.11-7.09-13.11-18.78v-56.34c0-4.96-5.31-7.44-9.92-7.44-5.31,0-9.92,2.48-9.92,7.44v58.46c0,23.39,11.69,34.02,32.95,34.02,19.13,0,31.89-10.98,31.89-33.66m-74.06,4.26c0-16.31-11.33-22.33-17.36-23.39v-.35c7.09-3.55,15.59-10.28,15.59-22.33,0-16.29-14.18-24.09-27.99-24.09h-26.58c-4.96,0-7.79,3.89-7.79,8.15v82.91c0,4.96,4.96,7.45,9.57,7.45h25.15c15.24,0,29.41-8.15,29.41-28.35m-21.61-41.46c0,9.21-7.79,12.4-15.94,12.4h-7.44v-24.81h7.44c8.5,0,15.94,2.84,15.94,12.41m2.84,39.33c0,9.57-5.32,14.18-16.65,14.18h-8.51v-27.64h8.51c7.09,0,16.65,2.48,16.65,13.46M274.25,18.43H9.21C4.13,18.43,0,14.3,0,9.22S4.13,0,9.21,0H274.25c5.09,0,9.21,4.13,9.21,9.22s-4.12,9.21-9.21,9.21m0,265.04H9.21c-5.08,0-9.21-4.12-9.21-9.21s4.13-9.21,9.21-9.21H274.25c5.09,0,9.21,4.12,9.21,9.21s-4.12,9.21-9.21,9.21"/><path d="M244.14,161.22c0-14.53-8.15-23.03-18.42-28.7l-7.8-4.25c-4.6-2.48-8.5-6.38-8.5-11.34,0-5.67,4.61-9.56,11.34-9.56,4.61,0,8.86,1.77,12.05,3.19,4.96,2.48,8.15-.71,9.57-6.02,1.77-6.03-.71-8.15-4.96-10.27-4.25-2.13-10.27-3.55-16.29-3.55-16.66,0-30.48,10.98-30.48,28.7,0,13.82,7.08,21.62,18.43,27.99l8.15,4.61c4.96,2.84,8.15,6.74,8.15,12.05,0,6.73-6.38,9.92-12.76,9.92-4.96,0-10.98-2.13-14.18-3.55-5.31-2.48-8.15,.71-9.92,6.38-1.76,6.02,1.07,8.51,5.67,10.63,4.25,2.12,11.69,3.89,18.43,3.89,17.36,0,31.53-11.69,31.53-30.12m-62-3.55v-58.82c0-4.96-4.96-7.44-9.57-7.44-4.96,0-9.56,2.48-9.56,7.44v56.34c0,11.34-3.55,18.78-12.76,18.78s-13.11-7.09-13.11-18.78v-56.34c0-4.96-5.31-7.44-9.92-7.44-5.31,0-9.92,2.48-9.92,7.44v58.46c0,23.39,11.69,34.02,32.95,34.02,19.13,0,31.89-10.98,31.89-33.66m-74.06,4.26c0-16.31-11.33-22.33-17.36-23.39v-.35c7.09-3.55,15.59-10.28,15.59-22.33,0-16.29-14.18-24.09-27.99-24.09h-26.58c-4.96,0-7.79,3.89-7.79,8.15v82.91c0,4.96,4.96,7.45,9.57,7.45h25.15c15.24,0,29.41-8.15,29.41-28.35m-21.61-41.46c0,9.21-7.79,12.4-15.94,12.4h-7.44v-24.81h7.44c8.5,0,15.94,2.84,15.94,12.41m2.84,39.33c0,9.57-5.32,14.18-16.65,14.18h-8.51v-27.64h8.51c7.09,0,16.65,2.48,16.65,13.46M274.25,18.43H9.21C4.13,18.43,0,14.3,0,9.22S4.13,0,9.21,0H274.25c5.09,0,9.21,4.13,9.21,9.22s-4.12,9.21-9.21,9.21m0,265.04H9.21c-5.08,0-9.21-4.12-9.21-9.21s4.13-9.21,9.21-9.21H274.25c5.09,0,9.21,4.12,9.21,9.21s-4.12,9.21-9.21,9.21"/></g></g></svg>
|
After Width: | Height: | Size: 2.9 KiB |
23
frontend/public/symbole_bus_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M165.206,155.193V97.366c0-2.976,3.686-5.883,7.655-5.883c3.683,0,7.653,2.907,7.653,5.883v60.307
|
||||||
|
c0,22.675-10.34,33.447-30.278,33.447c-22.063,0-31.569-10.415-31.569-33.801V97.366c0-2.976,3.688-5.883,7.938-5.883
|
||||||
|
c3.683,0,7.936,2.907,7.936,5.883v57.827c0,18.598,9.528,22.039,15.695,22.039C157.457,177.232,165.206,172.823,165.206,155.193
|
||||||
|
M72.638,144.988c11.66,0,18.621,5.768,18.621,15.431c0,10.709-6.265,16.139-18.621,16.139H60.913v-31.57H72.638z M89.899,138.539
|
||||||
|
v-0.354c6.654-3.594,14.587-10.39,14.587-22.355c0-16.532-13.57-24.063-28.79-24.062H53.605c-6.58-0.001-8.849,3.899-8.849,8.151
|
||||||
|
v82.91c0,4.961,3.969,7.442,10.063,7.442h21.329c27.936,0,29.912-21.844,29.912-28.538c0-19.969-15.493-23.068-16.161-23.196
|
||||||
|
M60.064,134.216v-28.415h9.248c11.447,0,18.952,5.046,18.952,14.208c0,8.896-7.837,14.207-18.952,14.207H60.064z M220.59,90.846
|
||||||
|
c-15.719,0.274-28.971,11.133-28.683,27.623c0.226,12.935,7.566,21.01,18.376,26.82l8.348,4.523
|
||||||
|
c5.794,3.191,9.859,6.681,9.977,13.386c0.155,8.884-7.359,13.902-15.429,14.043c-5.236,0.091-11.336-1.793-14.717-3.3
|
||||||
|
c-4.39-1.915-6.486,0.76-7.854,5.34c-1.37,4.863,0.986,6.812,4.788,8.452c4.238,2.008,11.675,3.555,18.314,3.44
|
||||||
|
c16.308-0.285,29.582-11.151,29.28-28.437c-0.238-13.629-7.92-21.829-17.816-27.082l-8.072-4.27
|
||||||
|
c-5.609-2.985-10.214-7.567-10.325-13.876c-0.139-8.03,5.632-13.83,13.763-13.972c4.878-0.085,9.396,1.701,12.811,3.328
|
||||||
|
c4.002,1.875,6.425-0.707,7.487-4.892c1.335-4.747-0.677-6.379-4.107-7.986C232.492,91.979,226.552,90.742,220.59,90.846
|
||||||
|
M283.465,7.37L283.465,7.37c0-4.07-3.3-7.37-7.37-7.37H7.37C3.3,0,0,3.3,0,7.37c0,4.071,3.3,7.371,7.37,7.371h268.725
|
||||||
|
C280.165,14.741,283.465,11.441,283.465,7.37 M283.465,276.095L283.465,276.095c0-4.07-3.3-7.37-7.37-7.37H7.37
|
||||||
|
c-4.07,0-7.37,3.3-7.37,7.37c0,4.07,3.3,7.37,7.37,7.37h268.725C280.165,283.465,283.465,280.165,283.465,276.095"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
1
frontend/public/symbole_cable_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><g id="Layer_1-2"><g><path d="M274.25,18.43H9.21C4.12,18.43,0,14.31,0,9.21S4.12,0,9.21,0H274.25c5.09,0,9.21,4.12,9.21,9.21s-4.12,9.22-9.21,9.22m9.21,255.82h0c0-5.09-4.12-9.21-9.21-9.21H9.21c-5.09,0-9.21,4.12-9.21,9.21s4.12,9.21,9.21,9.21H274.25c5.09,0,9.21-4.12,9.21-9.21M230.98,62.08l-84.43,14.89v36.42h38.9c11.2,0,21.21,6.69,25.5,17.03,5.7,13.69,8.59,28.23,8.59,43.2s-2.89,29.51-8.6,43.2c-4.29,10.34-14.29,17.03-25.49,17.03H98.02c-11.2,0-21.21-6.69-25.51-17.03-5.7-13.69-8.59-28.23-8.59-43.2s2.89-29.51,8.59-43.21c4.29-10.34,14.31-17.03,25.51-17.03h38.89v-34.72l-81.07,14.29c-1.75,.31-3.42-.86-3.72-2.61l-.56-3.17c-.31-1.74,.85-3.41,2.6-3.71l66.63-11.75-.29-1.67c-.76-4.32,2.12-8.44,6.44-9.19l26.24-4.63c4.32-.76,8.44,2.12,9.19,6.44l.3,1.67,66.63-11.75c1.74-.31,3.4,.85,3.71,2.59l.56,3.18c.31,1.75-.86,3.41-2.6,3.71m-116.88,120.76v-54.72c0-1.09-.89-1.98-1.99-1.98h-14.09c-6.03,0-11.41,3.59-13.72,9.16-5.06,12.15-7.62,25.04-7.62,38.32,0,3.16,.15,6.31,.45,9.42,.09,1.02,.96,1.79,1.98,1.79h33.01c1.1,0,1.99-.89,1.99-1.99m57.26,1.99h33.01c1.02,0,1.88-.77,1.98-1.79,.29-3.11,.45-6.26,.45-9.42,0-13.28-2.56-26.17-7.62-38.32-2.31-5.57-7.69-9.16-13.72-9.16h-14.09c-1.09,0-1.98,.89-1.98,1.98v54.72c0,1.1,.89,1.99,1.98,1.99m-14.74,34.29v-90.99c0-1.09-.89-1.98-1.98-1.98h-25.8c-1.09,0-1.98,.89-1.98,1.98v90.99c0,1.09,.89,1.98,1.98,1.98h25.8c1.09,0,1.98-.89,1.98-1.98"/><path d="M274.25,18.43H9.21C4.12,18.43,0,14.31,0,9.21S4.12,0,9.21,0H274.25c5.09,0,9.21,4.12,9.21,9.21s-4.12,9.22-9.21,9.22m9.21,255.82h0c0-5.09-4.12-9.21-9.21-9.21H9.21c-5.09,0-9.21,4.12-9.21,9.21s4.12,9.21,9.21,9.21H274.25c5.09,0,9.21-4.12,9.21-9.21M230.98,62.08l-84.43,14.89v36.42h38.9c11.2,0,21.21,6.69,25.5,17.03,5.7,13.69,8.59,28.23,8.59,43.2s-2.89,29.51-8.6,43.2c-4.29,10.34-14.29,17.03-25.49,17.03H98.02c-11.2,0-21.21-6.69-25.51-17.03-5.7-13.69-8.59-28.23-8.59-43.2s2.89-29.51,8.59-43.21c4.29-10.34,14.31-17.03,25.51-17.03h38.89v-34.72l-81.07,14.29c-1.75,.31-3.42-.86-3.72-2.61l-.56-3.17c-.31-1.74,.85-3.41,2.6-3.71l66.63-11.75-.29-1.67c-.76-4.32,2.12-8.44,6.44-9.19l26.24-4.63c4.32-.76,8.44,2.12,9.19,6.44l.3,1.67,66.63-11.75c1.74-.31,3.4,.85,3.71,2.59l.56,3.18c.31,1.75-.86,3.41-2.6,3.71m-116.88,120.76v-54.72c0-1.09-.89-1.98-1.99-1.98h-14.09c-6.03,0-11.41,3.59-13.72,9.16-5.06,12.15-7.62,25.04-7.62,38.32,0,3.16,.15,6.31,.45,9.42,.09,1.02,.96,1.79,1.98,1.79h33.01c1.1,0,1.99-.89,1.99-1.99m57.26,1.99h33.01c1.02,0,1.88-.77,1.98-1.79,.29-3.11,.45-6.26,.45-9.42,0-13.28-2.56-26.17-7.62-38.32-2.31-5.57-7.69-9.16-13.72-9.16h-14.09c-1.09,0-1.98,.89-1.98,1.98v54.72c0,1.1,.89,1.99,1.98,1.99m-14.74,34.29v-90.99c0-1.09-.89-1.98-1.98-1.98h-25.8c-1.09,0-1.98,.89-1.98,1.98v90.99c0,1.09,.89,1.98,1.98,1.98h25.8c1.09,0,1.98-.89,1.98-1.98"/></g></g></svg>
|
After Width: | Height: | Size: 2.8 KiB |
23
frontend/public/symbole_cable_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M233.576,58.371l-0.56-3.182c-0.308-1.742-1.969-2.906-3.711-2.598L162.67,64.34l-0.296-1.675
|
||||||
|
c-0.762-4.317-4.878-7.199-9.195-6.438l-26.24,4.627c-4.317,0.761-7.2,4.878-6.438,9.195l0.295,1.674l-66.635,11.75
|
||||||
|
c-1.742,0.307-2.905,1.969-2.598,3.711l0.559,3.173c0.308,1.747,1.974,2.914,3.722,2.606l81.07-14.295v34.719H98.018
|
||||||
|
c-11.198,0-21.209,6.685-25.5,17.025c-5.704,13.705-8.596,28.243-8.596,43.21c0,14.969,2.892,29.507,8.594,43.203
|
||||||
|
c4.293,10.348,14.303,17.034,25.502,17.034h87.429c11.2,0,21.21-6.687,25.498-17.027c5.706-13.701,8.598-28.239,8.598-43.21
|
||||||
|
c0-14.969-2.892-29.507-8.594-43.202c-4.293-10.347-14.302-17.033-25.502-17.033h-38.895V76.969l84.427-14.887
|
||||||
|
C232.721,61.775,233.884,60.113,233.576,58.371 M112.111,184.829H79.098c-1.019,0-1.883-0.77-1.978-1.785
|
||||||
|
c-0.292-3.116-0.443-6.258-0.443-9.422c0-13.276,2.563-26.165,7.621-38.314c2.31-5.568,7.696-9.166,13.72-9.166h14.093
|
||||||
|
c1.096,0,1.984,0.889,1.984,1.984v54.719C114.095,183.941,113.207,184.829,112.111,184.829 M169.37,182.845v-54.719
|
||||||
|
c0-1.095,0.889-1.984,1.985-1.984h14.092c6.025,0,11.411,3.598,13.721,9.166c5.058,12.149,7.62,25.038,7.62,38.314
|
||||||
|
c0,3.164-0.15,6.306-0.442,9.422c-0.095,1.015-0.959,1.785-1.979,1.785h-33.012C170.259,184.829,169.37,183.941,169.37,182.845
|
||||||
|
M154.63,221.103h-25.795c-1.096,0-1.984-0.889-1.984-1.985v-90.992c0-1.095,0.888-1.984,1.984-1.984h25.795
|
||||||
|
c1.097,0,1.984,0.889,1.984,1.984v90.992C156.614,220.214,155.727,221.103,154.63,221.103 M283.465,7.37L283.465,7.37
|
||||||
|
c0-4.07-3.3-7.37-7.37-7.37H7.37C3.3,0,0,3.3,0,7.37c0,4.071,3.3,7.371,7.37,7.371h268.725
|
||||||
|
C280.165,14.741,283.465,11.441,283.465,7.37 M283.465,276.095L283.465,276.095c0-4.07-3.3-7.37-7.37-7.37H7.37
|
||||||
|
c-4.07,0-7.37,3.3-7.37,7.37c0,4.07,3.3,7.37,7.37,7.37h268.725C280.165,283.465,283.465,280.165,283.465,276.095"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
frontend/public/symbole_funicular_RVB.svg
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
symbole_metro_RVB.svg
|
1
frontend/public/symbole_funicular_support_fonce_RVB.svg
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
symbole_metro_support_fonce_RVB.svg
|
1
frontend/public/symbole_metro_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><g id="Layer_1-2"><g><path d="M141.73,0C63.45,0,0,63.46,0,141.73s63.45,141.73,141.73,141.73,141.73-63.46,141.73-141.73S220.01,0,141.73,0m0,265.04c-68.1,0-123.31-55.21-123.31-123.31S73.63,18.43,141.73,18.43s123.3,55.2,123.3,123.3-55.2,123.31-123.3,123.31m70.51-59.89c0,6.74-5.32,10.63-11.69,10.63-5.68,0-12.05-3.89-12.05-10.63V113.03h-.35l-35.08,71.57c-2.48,4.96-5.66,7.79-11.34,7.79s-8.86-2.84-11.34-7.79l-35.08-71.57h-.35v92.13c0,6.74-6.38,10.63-12.05,10.63-6.38,0-11.69-3.89-11.69-10.63V81.5c0-7.09,3.9-14.53,15.59-14.53,8.86,0,12.4,3.9,16.3,11.69l38.62,79.72h.35l38.27-79.72c3.89-7.79,7.44-11.69,16.29-11.69,11.69,0,15.59,7.44,15.59,14.53v123.66"/><path d="M141.73,0C63.45,0,0,63.46,0,141.73s63.45,141.73,141.73,141.73,141.73-63.46,141.73-141.73S220.01,0,141.73,0m0,265.04c-68.1,0-123.31-55.21-123.31-123.31S73.63,18.43,141.73,18.43s123.3,55.2,123.3,123.3-55.2,123.31-123.3,123.31m70.51-59.89c0,6.74-5.32,10.63-11.69,10.63-5.68,0-12.05-3.89-12.05-10.63V113.03h-.35l-35.08,71.57c-2.48,4.96-5.66,7.79-11.34,7.79s-8.86-2.84-11.34-7.79l-35.08-71.57h-.35v92.13c0,6.74-6.38,10.63-12.05,10.63-6.38,0-11.69-3.89-11.69-10.63V81.5c0-7.09,3.9-14.53,15.59-14.53,8.86,0,12.4,3.9,16.3,11.69l38.62,79.72h.35l38.27-79.72c3.89-7.79,7.44-11.69,16.29-11.69,11.69,0,15.59,7.44,15.59,14.53v123.66"/></g></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
14
frontend/public/symbole_metro_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M141.733,0.001C63.456,0.001,0,63.456,0,141.733s63.456,141.733,141.733,141.733
|
||||||
|
c78.276,0,141.732-63.456,141.732-141.733S220.009,0.001,141.733,0.001 M141.733,268.725c-70.136,0-126.992-56.856-126.992-126.992
|
||||||
|
S71.597,14.741,141.733,14.741c70.135,0,126.992,56.856,126.992,126.992S211.868,268.725,141.733,268.725 M141.721,192.325
|
||||||
|
c-4.736,0-7.4-2.32-9.472-6.38l-38.104-75.984h-0.354l0.001,96.712c0,5.721-5.42,9.035-10.24,9.035
|
||||||
|
c-5.421,0-9.939-3.315-9.939-9.036V78.7c0-5.669,3.261-11.622,13.045-11.622c7.413,0,10.57,3.118,13.639,9.355l41.259,87.057h0.354
|
||||||
|
l41.235-87.057c3.069-6.237,6.225-9.355,13.639-9.355c9.784,0,13.045,5.953,13.045,11.622v127.972c0,5.721-4.517,9.036-9.939,9.036
|
||||||
|
c-4.82,0-10.24-3.314-10.24-9.035l0.001-96.712h-0.355l-38.103,75.984C149.121,190.005,146.457,192.325,141.721,192.325"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
frontend/public/symbole_navette_fluviale_RVB.svg
Normal file
After Width: | Height: | Size: 6.3 KiB |
1
frontend/public/symbole_rer_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><g id="Layer_1-2"><g><g><path d="M171.5,181.43c0-4.26-2.84-8.86-7.79-8.86h-26.94v-24.45h22.32c4.96,0,7.8-4.26,7.8-8.15,0-4.25-2.84-8.15-7.8-8.15h-22.32v-22.32h24.81c4.96,0,7.79-4.96,7.79-8.85,0-4.26-2.83-8.86-7.79-8.86h-36.15c-4.96,0-7.79,3.89-7.79,8.15v82.92c0,4.96,4.96,7.44,9.56,7.44h36.5c4.96,0,7.79-4.96,7.79-8.85"/><path d="M226.42,147.06c9.22-4.62,17.02-12.76,17.02-27.29,0-19.13-14.19-27.99-27.99-27.99h-26.58c-4.96,0-7.79,3.89-7.79,8.15v83.27c0,4.96,4.96,7.43,9.55,7.43,5.32,0,9.57-2.48,9.57-7.43v-32.24h8.15l15.94,35.78c1.42,2.84,3.91,3.89,6.74,3.89,5.66,0,13.82-5.31,10.63-11.68l-15.24-31.89Zm-20.55-10.28h-5.67v-28.7h6.72c8.86,0,16.65,3.9,16.65,13.82,0,12.05-10.97,14.88-17.71,14.88Z"/><path d="M89.29,147.06c9.22-4.62,17.01-12.76,17.01-27.29,0-19.13-14.17-27.99-27.99-27.99h-26.57c-4.96,0-7.8,3.89-7.8,8.15v83.27c0,4.96,4.96,7.43,9.56,7.43,5.31,0,9.56-2.48,9.56-7.43v-32.24h8.15l15.95,35.78c1.42,2.84,3.91,3.89,6.74,3.89,5.67,0,13.81-5.31,10.63-11.68l-15.24-31.89Zm-20.55-10.28h-5.68v-28.7h6.74c8.85,0,16.65,3.9,16.65,13.82,0,12.05-10.98,14.88-17.72,14.88Z"/><path d="M219.69,0H63.78C28.55,0,0,28.56,0,63.78V219.68c0,35.22,28.55,63.78,63.78,63.78H219.69c35.22,0,63.78-28.56,63.78-63.78V63.78c0-35.22-28.55-63.78-63.78-63.78Zm45.35,219.68c0,25.05-20.31,45.35-45.35,45.35H63.78c-25.05,0-45.35-20.31-45.35-45.35V63.78c0-25.05,20.31-45.36,45.35-45.36H219.69c25.05,0,45.35,20.31,45.35,45.36V219.68Z"/></g><g><path d="M171.5,181.43c0-4.26-2.84-8.86-7.79-8.86h-26.94v-24.45h22.32c4.96,0,7.8-4.26,7.8-8.15,0-4.25-2.84-8.15-7.8-8.15h-22.32v-22.32h24.81c4.96,0,7.79-4.96,7.79-8.85,0-4.26-2.83-8.86-7.79-8.86h-36.15c-4.96,0-7.79,3.89-7.79,8.15v82.92c0,4.96,4.96,7.44,9.56,7.44h36.5c4.96,0,7.79-4.96,7.79-8.85"/><path d="M226.42,147.06c9.22-4.62,17.02-12.76,17.02-27.29,0-19.13-14.19-27.99-27.99-27.99h-26.58c-4.96,0-7.79,3.89-7.79,8.15v83.27c0,4.96,4.96,7.43,9.55,7.43,5.32,0,9.57-2.48,9.57-7.43v-32.24h8.15l15.94,35.78c1.42,2.84,3.91,3.89,6.74,3.89,5.66,0,13.82-5.31,10.63-11.68l-15.24-31.89Zm-20.55-10.28h-5.67v-28.7h6.72c8.86,0,16.65,3.9,16.65,13.82,0,12.05-10.97,14.88-17.71,14.88Z"/><path d="M89.29,147.06c9.22-4.62,17.01-12.76,17.01-27.29,0-19.13-14.17-27.99-27.99-27.99h-26.57c-4.96,0-7.8,3.89-7.8,8.15v83.27c0,4.96,4.96,7.43,9.56,7.43,5.31,0,9.56-2.48,9.56-7.43v-32.24h8.15l15.95,35.78c1.42,2.84,3.91,3.89,6.74,3.89,5.67,0,13.81-5.31,10.63-11.68l-15.24-31.89Zm-20.55-10.28h-5.68v-28.7h6.74c8.85,0,16.65,3.9,16.65,13.82,0,12.05-10.98,14.88-17.72,14.88Z"/><path d="M219.69,0H63.78C28.55,0,0,28.56,0,63.78V219.68c0,35.22,28.55,63.78,63.78,63.78H219.69c35.22,0,63.78-28.56,63.78-63.78V63.78c0-35.22-28.55-63.78-63.78-63.78Zm45.35,219.68c0,25.05-20.31,45.35-45.35,45.35H63.78c-25.05,0-45.35-20.31-45.35-45.35V63.78c0-25.05,20.31-45.36,45.35-45.36H219.69c25.05,0,45.35,20.31,45.35,45.36V219.68Z"/></g></g></g></svg>
|
After Width: | Height: | Size: 2.9 KiB |
26
frontend/public/symbole_rer_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M78.31,91.787c12.98,0,26.078,8.359,26.078,27.038c0,12.107-5.367,21.027-15.949,26.511
|
||||||
|
c-0.58,0.265-1.157,0.503-1.732,0.731l17.031,35.648c0.75,1.501,0.684,2.928-0.203,4.363c-1.556,2.516-5.465,4.563-8.716,4.563
|
||||||
|
c-2.456,0-4.052-0.903-5.021-2.841l-17.049-38.127H60.703v35.212c0,2.871-4.102,5.675-7.654,5.675
|
||||||
|
c-4.099,0-7.656-2.804-7.656-5.675V99.923c0-4.252,2.269-8.152,8.849-8.15L78.31,91.787z M45.393,184.885V99.923
|
||||||
|
c0-4.252,2.269-8.152,8.849-8.15 M68.742,136.917c9.056,0,19.63-4.399,19.63-16.795c0-9.705-7.114-15.733-18.567-15.733h-9.102
|
||||||
|
v32.528H68.742z"/>
|
||||||
|
<path fill="#FFFFFF" d="M215.438,91.787c12.98,0,26.078,8.359,26.078,27.038c0,12.107-5.367,21.027-15.949,26.511
|
||||||
|
c-0.58,0.265-1.157,0.503-1.732,0.731l17.031,35.648c0.75,1.501,0.684,2.928-0.203,4.363c-1.556,2.516-5.465,4.563-8.716,4.563
|
||||||
|
c-2.456,0-4.052-0.903-5.021-2.841l-17.049-38.127h-12.046v35.212c0,2.871-4.102,5.675-7.654,5.675
|
||||||
|
c-4.099,0-7.656-2.804-7.656-5.675V99.923c0-4.252,2.269-8.152,8.849-8.15L215.438,91.787z M205.87,136.917
|
||||||
|
c9.056,0,19.63-4.399,19.63-16.795c0-9.705-7.114-15.733-18.567-15.733h-9.102v32.528H205.87z"/>
|
||||||
|
<path fill="#FFFFFF" d="M163.987,190.277c3.786,0,5.881-4.188,5.881-7.084c0-2.942-1.825-7.087-5.881-7.088h-29.33v-29.623h24.724
|
||||||
|
c4.056,0,5.881-3.813,5.881-6.52c0-3.14-1.84-6.519-5.881-6.519h-24.724v-27.496h27.203c3.838,0,5.883-4.115,5.883-7.087
|
||||||
|
c0-2.942-1.825-7.086-5.883-7.086h-33.632c-6.58-0.002-8.848,3.898-8.848,8.15v82.913c0,5.455,4.577,7.435,7.653,7.435
|
||||||
|
L163.987,190.277z"/>
|
||||||
|
<path fill="#FFFFFF" d="M219.685,283.465H63.78C28.556,283.465,0,254.91,0,219.685V63.78C0,28.556,28.556,0,63.78,0h155.905
|
||||||
|
c35.225,0,63.78,28.556,63.78,63.78v155.905C283.465,254.91,254.91,283.465,219.685,283.465 M268.725,219.685V63.78
|
||||||
|
c0-27.084-21.956-49.039-49.04-49.039H63.78c-27.084,0-49.039,21.955-49.039,49.039v155.905c0,27.084,21.955,49.04,49.039,49.04
|
||||||
|
h155.905C246.769,268.725,268.725,246.769,268.725,219.685"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
frontend/public/symbole_rer_velo_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><path d="M274.25,18.42H9.5A9.39,9.39,0,0,1,.1,10.58,9.22,9.22,0,0,1,9.21,0H274a9.41,9.41,0,0,1,9.4,7.85,9.22,9.22,0,0,1-9.12,10.57m9.21,255.83h0a9.21,9.21,0,0,0-9.21-9.21H9.5a9.39,9.39,0,0,0-9.4,7.84,9.22,9.22,0,0,0,9.11,10.58h265a9.21,9.21,0,0,0,9.21-9.21" transform="translate(0)"/><path d="M124.42,141.74l2-7.49,4.83-18.14.94,1.34c.18.26.38.51.58.75a10.16,10.16,0,0,0,2.93,2.37,11.7,11.7,0,0,0,1.74.75l10.08,3.78a31.12,31.12,0,0,0,15.36-7.29l-21-7.89S134.51,97.78,131,94.49a7.82,7.82,0,0,0-4.33-2.16,10.55,10.55,0,0,0-2.39-.26h0a15.42,15.42,0,0,0-2.25.31l-.24.05a1.14,1.14,0,0,0-.25.06h0a18.27,18.27,0,0,0-12.67,12.81l-8,30.25a10.19,10.19,0,0,0,0,4.08,10.77,10.77,0,0,0,2.82,5.2l2.53,2.87,4.14,4.71,20.19,21.16-5.59,39.71a6.87,6.87,0,0,0,10.06,6.32,6.7,6.7,0,0,0,2.23-4.36l7-39.54a10.72,10.72,0,0,0,.17-2.71,10.17,10.17,0,0,0-1.76-5.07Z" transform="translate(0)"/><path d="M128.78,87c3.19.68,6.75.88,9.46-.83s3.93-5.12,4.61-8.31.88-6.75-.83-9.47-5.12-3.93-8.31-4.6-6.75-.88-9.47.82-3.92,5.12-4.6,8.32-.88,6.75.82,9.47S125.58,86.32,128.78,87Z" transform="translate(0)"/><path d="M241.83,197.62a37.21,37.21,0,0,0-31.63-35.34,36.43,36.43,0,0,0-18,1.87l-19.67-34.07a46.36,46.36,0,0,0,12.6-16.8,2.87,2.87,0,0,0-1.66-3.83l-4-1.46a2.82,2.82,0,0,0-3.54,1.53,36.76,36.76,0,0,1-34.05,21.71,2.86,2.86,0,0,0-2.95,2.57l-.37,4.25a2.86,2.86,0,0,0,2.76,3.12,46.69,46.69,0,0,0,20.41-4.23l5.41,9.37-16.63,26.55L144.94,204A7.81,7.81,0,0,0,148,201l26.35-42.09,6.75,11.68a37,37,0,1,0,60.73,27.08Zm-40,28.1a27,27,0,0,1-15.58-46.33l13.36,23.25a4.27,4.27,0,0,0,5.83,1.56l3.7-2.13a4.27,4.27,0,0,0,1.56-5.83L197.32,173a27,27,0,1,1,4.47,52.68Z" transform="translate(0)"/><path d="M116,192a36.89,36.89,0,0,0-9-18l7.09-9.36-9-9.4-8.22,10.91a37,37,0,0,0-25.29-3.38,37,37,0,1,0,44.57,42h4.18l1.8-12.8Zm-15.23-9.79A26.7,26.7,0,0,1,105.7,192H93.36Zm-21.1,43.71a27,27,0,0,1-7.42-53,26.54,26.54,0,0,1,18.47,1.36l-16.2,21.4a5.68,5.68,0,0,0,4.53,9.12h27A27,27,0,0,1,79.67,225.89Z" transform="translate(0)"/></svg>
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/public/symbole_ter_RVB.svg
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
symbole_transilien_RVB.svg
|
1
frontend/public/symbole_ter_support_fonce_RVB.svg
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
symbole_transilien_support_fonce_RVB.svg
|
1
frontend/public/symbole_train_RER_RVB.svg
Normal file
After Width: | Height: | Size: 6.1 KiB |
44
frontend/public/symbole_train_RER_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 28.346 28.346" enable-background="new 0 0 28.346 28.346" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M10.615,12.933c0.029-0.019,0.057-0.04,0.082-0.063c0.247-0.201,0.479-0.508,0.647-0.986
|
||||||
|
c0.236-0.668,0.402-1.265,0.447-2.001c0.064-0.905-0.309-2.313-0.555-2.969l-0.087-0.231c-0.053-0.146-0.256-0.461-0.616-0.836
|
||||||
|
c-0.309-0.323-0.607-0.346-1.06-0.346H7.634c-0.452,0-0.749,0.023-1.059,0.346C6.214,6.221,6.011,6.536,5.959,6.683L5.873,6.915
|
||||||
|
C5.625,7.57,5.253,8.978,5.315,9.886c0.045,0.733,0.212,1.33,0.448,2c0.168,0.478,0.4,0.784,0.648,0.985
|
||||||
|
c0.026,0.022,0.053,0.044,0.082,0.062l-1.339,1.488c-0.053,0.061-0.082,0.139-0.082,0.22c0,0.08,0.029,0.158,0.082,0.219
|
||||||
|
c0.025,0.028,0.055,0.051,0.088,0.066c0.035,0.016,0.071,0.024,0.108,0.024c0.038,0,0.074-0.008,0.109-0.024
|
||||||
|
c0.033-0.015,0.064-0.038,0.087-0.066l0.463-0.514h5.09l0.461,0.514c0.025,0.028,0.055,0.051,0.089,0.066
|
||||||
|
c0.034,0.016,0.07,0.024,0.108,0.024c0.037,0,0.074-0.008,0.108-0.024c0.034-0.015,0.064-0.038,0.088-0.066
|
||||||
|
c0.053-0.061,0.082-0.139,0.082-0.219c0-0.081-0.029-0.159-0.082-0.22L10.615,12.933z M10.166,12.39
|
||||||
|
c-0.091,0-0.18-0.027-0.255-0.077c-0.075-0.051-0.134-0.123-0.169-0.206c-0.035-0.085-0.044-0.177-0.026-0.266
|
||||||
|
c0.017-0.089,0.062-0.171,0.126-0.235c0.064-0.065,0.146-0.108,0.235-0.126c0.089-0.018,0.182-0.009,0.265,0.026
|
||||||
|
c0.084,0.034,0.156,0.094,0.206,0.169c0.051,0.076,0.078,0.165,0.078,0.255c0,0.122-0.049,0.239-0.135,0.325
|
||||||
|
C10.405,12.342,10.288,12.39,10.166,12.39 M6.004,9.443c-0.281,0,0.162-2.466,0.362-2.466h4.325c0.22,0,0.731,2.466,0.391,2.466
|
||||||
|
H6.004z M6.473,11.93c0-0.09,0.027-0.179,0.077-0.256c0.051-0.075,0.123-0.134,0.207-0.168c0.083-0.035,0.176-0.045,0.266-0.027
|
||||||
|
c0.089,0.018,0.17,0.061,0.235,0.126c0.064,0.064,0.108,0.146,0.126,0.235c0.018,0.09,0.009,0.182-0.026,0.267
|
||||||
|
c-0.035,0.083-0.094,0.155-0.17,0.206c-0.075,0.05-0.164,0.077-0.255,0.077c-0.061,0-0.121-0.012-0.176-0.034
|
||||||
|
c-0.056-0.024-0.108-0.058-0.149-0.1c-0.044-0.043-0.077-0.094-0.1-0.149C6.484,12.05,6.473,11.99,6.473,11.93 M6.566,13.726
|
||||||
|
l0.492-0.547c0.206,0.042,0.415,0.06,0.624,0.055h1.744c0.209,0.005,0.418-0.013,0.622-0.055l0.495,0.547H6.566z"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M21.942,7.531c0.184,0.184,0.184,0.483,0,0.668L7.769,22.371
|
||||||
|
c-0.184,0.186-0.484,0.186-0.668,0c-0.185-0.184-0.185-0.483,0-0.668L21.274,7.531C21.458,7.346,21.758,7.346,21.942,7.531"/>
|
||||||
|
<path fill="#FFFFFF" d="M21.968,28.346H6.377C2.855,28.346,0,25.491,0,21.968V6.378c0-3.523,2.856-6.379,6.378-6.379h15.591
|
||||||
|
c3.523,0,6.378,2.856,6.378,6.379v15.59C28.346,25.491,25.491,28.346,21.968,28.346 M26.872,21.968V6.378
|
||||||
|
c0-2.709-2.196-4.904-4.904-4.904H6.377c-2.708,0-4.903,2.195-4.903,4.904v15.59c0,2.708,2.195,4.904,4.903,4.904h15.591
|
||||||
|
C24.676,26.872,26.872,24.676,26.872,21.968"/>
|
||||||
|
<path fill="#FFFFFF" d="M16.165,18.045c0.631,0,1.267,0.407,1.267,1.315c0,0.588-0.26,1.022-0.774,1.288
|
||||||
|
c-0.029,0.013-0.057,0.025-0.085,0.036l0.828,1.732c0.036,0.073,0.033,0.143-0.01,0.213c-0.075,0.122-0.265,0.221-0.423,0.221
|
||||||
|
c-0.12,0-0.197-0.043-0.245-0.138l-0.828-1.853h-0.586v1.712c0,0.139-0.199,0.275-0.372,0.275c-0.199,0-0.372-0.136-0.372-0.275
|
||||||
|
v-4.13c0-0.207,0.111-0.396,0.43-0.396H16.165z M14.565,22.571v-4.13c0-0.207,0.111-0.396,0.43-0.396 M15.7,20.239
|
||||||
|
c0.44,0,0.954-0.214,0.954-0.816c0-0.472-0.346-0.765-0.902-0.765h-0.443v1.581H15.7z"/>
|
||||||
|
<path fill="#FFFFFF" d="M22.83,18.045c0.631,0,1.267,0.407,1.267,1.315c0,0.588-0.26,1.022-0.775,1.288
|
||||||
|
c-0.028,0.013-0.056,0.025-0.084,0.036l0.828,1.732c0.036,0.073,0.033,0.143-0.01,0.213c-0.076,0.122-0.266,0.221-0.424,0.221
|
||||||
|
c-0.119,0-0.197-0.043-0.244-0.138l-0.829-1.853h-0.585v1.712c0,0.139-0.199,0.275-0.372,0.275c-0.199,0-0.372-0.136-0.372-0.275
|
||||||
|
v-4.13c0-0.207,0.11-0.396,0.43-0.396H22.83z M22.365,20.239c0.44,0,0.954-0.214,0.954-0.816c0-0.472-0.346-0.765-0.903-0.765
|
||||||
|
h-0.442v1.581H22.365z"/>
|
||||||
|
<path fill="#FFFFFF" d="M20.329,22.832c0.184,0,0.286-0.203,0.286-0.344c0-0.143-0.089-0.345-0.286-0.345h-1.425v-1.44h1.201
|
||||||
|
c0.198,0,0.286-0.185,0.286-0.317c0-0.152-0.089-0.316-0.286-0.316h-1.201v-1.337h1.322c0.186,0,0.286-0.2,0.286-0.344
|
||||||
|
c0-0.143-0.089-0.345-0.286-0.345h-1.635c-0.32,0-0.43,0.19-0.43,0.396v4.03c0,0.266,0.222,0.362,0.372,0.362H20.329z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
1
frontend/public/symbole_tram_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><g id="Layer_1-2"><g><path d="M274.25,18.42H9.21C4.12,18.42,0,14.3,0,9.21S4.12,0,9.21,0H274.25c5.09,0,9.21,4.12,9.21,9.21s-4.12,9.21-9.21,9.21m9.21,255.83h0c0-5.09-4.12-9.22-9.21-9.22H9.21c-5.09,0-9.21,4.13-9.21,9.22s4.12,9.21,9.21,9.21H274.25c5.09,0,9.21-4.12,9.21-9.21m-32.32-38.41c2.19,0,3.97-1.78,3.97-3.97v-3.97c0-2.19-1.78-3.96-3.97-3.96H28.35v11.91H251.14M28.35,170.78h41.25c1.09,0,1.98-.89,1.98-1.98v-54.85c0-1.09-.89-1.98-1.98-1.98H28.35v-12.76H92.72l25.23-20.74-31.74-26.08c-1.37-1.12-1.57-3.15-.44-4.52l2.04-2.49c1.13-1.37,3.15-1.56,4.52-.44l36.14,29.69c2.41,1.98,2.42,5.68,0,7.66l-20.58,16.91h98.03c7.78,0,15.18,3.29,20.43,9.03,15.45,16.89,25.27,37.77,28.41,60.39,1,7.19-.09,14.31-3.16,20.56-7.09,14.44-21.47,23.41-37.55,23.41H28.35v-41.81Zm188.48-54.06c-2.8-3.05-6.77-4.75-10.91-4.75h-34.57c-1.09,0-1.98,.89-1.98,1.98v54.85c0,1.09,.89,1.98,1.98,1.98h59.11c6.94,0,11.07-5.46,9.31-12.17-4.1-15.56-11.91-29.87-22.94-41.89m-62.2,83.12c1.09,0,1.98-.89,1.98-1.98V113.95c0-1.09-.89-1.98-1.98-1.98h-25.79c-1.1,0-1.99,.89-1.99,1.98v83.91c0,1.09,.89,1.98,1.99,1.98h25.79M86.32,111.97c-1.09,0-1.98,.89-1.98,1.98v83.91c0,1.09,.89,1.98,1.98,1.98h25.79c1.1,0,1.99-.89,1.99-1.98V113.95c0-1.09-.89-1.98-1.99-1.98h-25.79"/><path d="M274.25,18.42H9.21C4.12,18.42,0,14.3,0,9.21S4.12,0,9.21,0H274.25c5.09,0,9.21,4.12,9.21,9.21s-4.12,9.21-9.21,9.21m9.21,255.83h0c0-5.09-4.12-9.22-9.21-9.22H9.21c-5.09,0-9.21,4.13-9.21,9.22s4.12,9.21,9.21,9.21H274.25c5.09,0,9.21-4.12,9.21-9.21m-32.32-38.41c2.19,0,3.97-1.78,3.97-3.97v-3.97c0-2.19-1.78-3.96-3.97-3.96H28.35v11.91H251.14M28.35,170.78h41.25c1.09,0,1.98-.89,1.98-1.98v-54.85c0-1.09-.89-1.98-1.98-1.98H28.35v-12.76H92.72l25.23-20.74-31.74-26.08c-1.37-1.12-1.57-3.15-.44-4.52l2.04-2.49c1.13-1.37,3.15-1.56,4.52-.44l36.14,29.69c2.41,1.98,2.42,5.68,0,7.66l-20.58,16.91h98.03c7.78,0,15.18,3.29,20.43,9.03,15.45,16.89,25.27,37.77,28.41,60.39,1,7.19-.09,14.31-3.16,20.56-7.09,14.44-21.47,23.41-37.55,23.41H28.35v-41.81Zm188.48-54.06c-2.8-3.05-6.77-4.75-10.91-4.75h-34.57c-1.09,0-1.98,.89-1.98,1.98v54.85c0,1.09,.89,1.98,1.98,1.98h59.11c6.94,0,11.07-5.46,9.31-12.17-4.1-15.56-11.91-29.87-22.94-41.89m-62.2,83.12c1.09,0,1.98-.89,1.98-1.98V113.95c0-1.09-.89-1.98-1.98-1.98h-25.79c-1.1,0-1.99,.89-1.99,1.98v83.91c0,1.09,.89,1.98,1.99,1.98h25.79M86.32,111.97c-1.09,0-1.98,.89-1.98,1.98v83.91c0,1.09,.89,1.98,1.98,1.98h25.79c1.1,0,1.99-.89,1.99-1.98V113.95c0-1.09-.89-1.98-1.99-1.98h-25.79"/></g></g></svg>
|
After Width: | Height: | Size: 2.5 KiB |
23
frontend/public/symbole_tram_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M276.095,14.741H7.37c-4.07,0-7.37-3.3-7.37-7.37c0-4.07,3.3-7.37,7.37-7.37h268.725
|
||||||
|
c4.07,0,7.37,3.3,7.37,7.37C283.465,11.441,280.165,14.741,276.095,14.741"/>
|
||||||
|
<path fill="#FFFFFF" d="M276.095,283.465H7.37c-4.07,0-7.37-3.3-7.37-7.37c0-4.07,3.3-7.37,7.37-7.37h268.725
|
||||||
|
c4.07,0,7.37,3.3,7.37,7.37C283.465,280.165,280.165,283.465,276.095,283.465"/>
|
||||||
|
<path fill="#FFFFFF" d="M251.149,235.841H28.346v-11.904h222.803c2.191,0,3.969,1.776,3.969,3.967v3.969
|
||||||
|
C255.118,234.066,253.34,235.841,251.149,235.841"/>
|
||||||
|
<path fill="#FFFFFF" d="M28.347,212.6h185.7c16.075,0,30.461-8.971,37.546-23.412c3.07-6.257,4.164-13.365,3.167-20.561
|
||||||
|
c-3.137-22.618-12.96-43.499-28.406-60.389c-5.247-5.738-12.654-9.024-20.43-9.024h-98.025l20.574-16.909
|
||||||
|
c2.415-1.984,2.414-5.681-0.001-7.665L92.332,44.949c-1.371-1.127-3.395-0.929-4.522,0.442l-2.04,2.483
|
||||||
|
c-1.126,1.371-0.928,3.395,0.443,4.522l31.743,26.077L92.719,99.214H28.347v12.756h41.243c1.096,0,1.985,0.889,1.985,1.984v54.85
|
||||||
|
c0,1.097-0.889,1.985-1.985,1.985H28.347V212.6z M216.829,116.724c11.03,12.022,18.841,26.331,22.941,41.892
|
||||||
|
c1.769,6.709-2.362,12.173-9.302,12.173h-59.113c-1.096,0-1.984-0.888-1.984-1.985v-54.85c0-1.095,0.888-1.984,1.984-1.984h34.569
|
||||||
|
C210.061,111.97,214.031,113.675,216.829,116.724 M154.63,199.844h-25.794c-1.096,0-1.985-0.889-1.985-1.984v-83.906
|
||||||
|
c0-1.095,0.889-1.984,1.985-1.984h25.794c1.096,0,1.985,0.889,1.985,1.984v83.906C156.615,198.955,155.725,199.844,154.63,199.844
|
||||||
|
M86.314,111.97h25.796c1.096,0,1.984,0.889,1.984,1.984v83.906c0,1.095-0.888,1.984-1.984,1.984H86.314
|
||||||
|
c-1.096,0-1.984-0.889-1.984-1.984v-83.906C84.33,112.859,85.218,111.97,86.314,111.97"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/public/symbole_transilien_RVB.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46"><g id="Layer_1-2"><g><path d="M219.68,283.46H63.78c-35.22,0-63.78-28.55-63.78-63.78V63.78C0,28.55,28.56,0,63.78,0H219.68c35.22,0,63.78,28.55,63.78,63.78V219.69c0,35.22-28.56,63.78-63.78,63.78m45.36-63.78V63.78c0-25.05-20.31-45.35-45.36-45.35H63.78c-25.05,0-45.35,20.31-45.35,45.35V219.69c0,25.05,20.31,45.35,45.35,45.35H219.68c25.05,0,45.36-20.31,45.36-45.35m-81.17-15.09c.62-.42,1.22-.86,1.66-1.29,5.09-4.12,9.86-10.45,13.31-20.24,4.83-13.74,8.24-26.02,9.18-41.12,1.31-18.6-6.35-47.54-11.43-61.04l-1.77-4.75c-1.09-3.04-5.26-9.47-12.65-17.18-6.35-6.62-12.47-7.11-21.77-7.11h-37.76c-9.3,0-15.4,.49-21.78,7.11-7.39,7.69-11.56,14.15-12.65,17.18l-1.78,4.78c-5.08,13.48-12.73,42.42-11.44,61.04,.94,15.06,4.36,27.32,9.21,41.1,3.45,9.82,8.19,16.12,13.29,20.24,.48,.42,1.06,.85,1.69,1.27l-27.51,30.58c-2.24,2.5-2.24,6.53,0,9.01,2.24,2.47,5.85,2.47,8.07,0l9.5-10.54h104.53l9.48,10.54c2.22,2.47,5.85,2.47,8.07,0,2.24-2.48,2.24-6.51,0-9.01l-27.48-30.56Zm-9.24-11.15c-5.21,0-9.45-4.22-9.45-9.45s4.24-9.42,9.45-9.42,9.46,4.22,9.46,9.42-4.22,9.45-9.46,9.45m-85.47-60.55c-5.77,0,3.29-50.66,7.43-50.66h88.85c4.49,0,15.02,50.66,7.96,50.66H89.16Zm9.62,51.1c0-5.2,4.23-9.42,9.45-9.42s9.44,4.22,9.44,9.42-4.21,9.45-9.44,9.45-9.45-4.22-9.45-9.45m1.92,36.9l10.13-11.25c4.94,1.14,9.48,1.14,12.79,1.14h35.82c3.31,0,7.86,0,12.79-1.14l10.14,11.25H100.7Z"/><path d="M219.68,283.46H63.78c-35.22,0-63.78-28.55-63.78-63.78V63.78C0,28.55,28.56,0,63.78,0H219.68c35.22,0,63.78,28.55,63.78,63.78V219.69c0,35.22-28.56,63.78-63.78,63.78m45.36-63.78V63.78c0-25.05-20.31-45.35-45.36-45.35H63.78c-25.05,0-45.35,20.31-45.35,45.35V219.69c0,25.05,20.31,45.35,45.35,45.35H219.68c25.05,0,45.36-20.31,45.36-45.35m-81.17-15.09c.62-.42,1.22-.86,1.66-1.29,5.09-4.12,9.86-10.45,13.31-20.24,4.83-13.74,8.24-26.02,9.18-41.12,1.31-18.6-6.35-47.54-11.43-61.04l-1.77-4.75c-1.09-3.04-5.26-9.47-12.65-17.18-6.35-6.62-12.47-7.11-21.77-7.11h-37.76c-9.3,0-15.4,.49-21.78,7.11-7.39,7.69-11.56,14.15-12.65,17.18l-1.78,4.78c-5.08,13.48-12.73,42.42-11.44,61.04,.94,15.06,4.36,27.32,9.21,41.1,3.45,9.82,8.19,16.12,13.29,20.24,.48,.42,1.06,.85,1.69,1.27l-27.51,30.58c-2.24,2.5-2.24,6.53,0,9.01,2.24,2.47,5.85,2.47,8.07,0l9.5-10.54h104.53l9.48,10.54c2.22,2.47,5.85,2.47,8.07,0,2.24-2.48,2.24-6.51,0-9.01l-27.48-30.56Zm-9.24-11.15c-5.21,0-9.45-4.22-9.45-9.45s4.24-9.42,9.45-9.42,9.46,4.22,9.46,9.42-4.22,9.45-9.46,9.45m-85.47-60.55c-5.77,0,3.29-50.66,7.43-50.66h88.85c4.49,0,15.02,50.66,7.96,50.66H89.16Zm9.62,51.1c0-5.2,4.23-9.42,9.45-9.42s9.44,4.22,9.44,9.42-4.21,9.45-9.44,9.45-9.45-4.22-9.45-9.45m1.92,36.9l10.13-11.25c4.94,1.14,9.48,1.14,12.79,1.14h35.82c3.31,0,7.86,0,12.79-1.14l10.14,11.25H100.7Z"/></g></g></svg>
|
After Width: | Height: | Size: 2.7 KiB |
23
frontend/public/symbole_transilien_support_fonce_RVB.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M183.87,204.599c0.622-0.424,1.223-0.858,1.666-1.293c5.097-4.122,9.857-10.449,13.313-20.242
|
||||||
|
c4.827-13.737,8.238-26.025,9.183-41.121c1.306-18.605-6.354-47.538-11.433-61.04l-1.767-4.757
|
||||||
|
c-1.092-3.031-5.257-9.47-12.655-17.179c-6.353-6.618-12.468-7.108-21.768-7.108h-37.757c-9.3,0-15.405,0.49-21.777,7.108
|
||||||
|
c-7.397,7.687-11.563,14.148-12.646,17.179l-1.776,4.779c-5.079,13.48-12.734,42.413-11.441,61.04
|
||||||
|
c0.94,15.063,4.36,27.318,9.209,41.099c3.447,9.814,8.194,16.12,13.295,20.242c0.483,0.423,1.057,0.847,1.693,1.271l-27.504,30.581
|
||||||
|
c-2.237,2.496-2.237,6.528,0,9.002c2.233,2.472,5.849,2.472,8.069,0l9.499-10.539h104.528l9.478,10.539
|
||||||
|
c2.215,2.472,5.845,2.472,8.068,0c2.237-2.474,2.237-6.506,0-9.002L183.87,204.599z M174.639,193.447
|
||||||
|
c-5.216,0-9.456-4.223-9.456-9.447c0-5.203,4.24-9.425,9.456-9.425c5.231,0,9.454,4.222,9.454,9.425
|
||||||
|
C184.093,189.224,179.87,193.447,174.639,193.447 M89.161,132.897c-5.771,0,3.294-50.657,7.432-50.657h88.847
|
||||||
|
c4.492,0,15.028,50.657,7.969,50.657H89.161z M98.782,184c0-5.203,4.226-9.425,9.448-9.425c5.231,0,9.439,4.222,9.439,9.425
|
||||||
|
c0,5.224-4.208,9.447-9.439,9.447C103.008,193.447,98.782,189.224,98.782,184 M100.697,220.898l10.127-11.253
|
||||||
|
c4.935,1.137,9.483,1.137,12.798,1.137h35.817c3.307,0,7.854,0,12.789-1.137l10.132,11.253H100.697z"/>
|
||||||
|
<path fill="#FFFFFF" d="M219.685,283.465H63.78C28.556,283.465,0,254.91,0,219.685V63.78C0,28.556,28.556,0,63.78,0h155.905
|
||||||
|
c35.225,0,63.78,28.556,63.78,63.78v155.905C283.465,254.91,254.91,283.465,219.685,283.465 M268.725,219.685V63.78
|
||||||
|
c0-27.084-21.956-49.039-49.04-49.039H63.78c-27.084,0-49.039,21.955-49.039,49.039v155.905c0,27.084,21.955,49.04,49.039,49.04
|
||||||
|
h155.905C246.769,268.725,268.725,246.769,268.725,219.685"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
21
frontend/src/App.module.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.App {
|
||||||
|
--idfm-black: #2c2e35;
|
||||||
|
--idfm-white: #ffffff;
|
||||||
|
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
overflow-x: scroll;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
min-width: 100%;
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
|
||||||
|
scroll-snap-align: center;
|
||||||
|
}
|
65
frontend/src/App.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Component } from 'solid-js';
|
||||||
|
import { MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction, CustomEvent, IVisibilityActionRequest } from 'matrix-widget-api';
|
||||||
|
|
||||||
|
import { HopeProvider } from "@hope-ui/solid";
|
||||||
|
|
||||||
|
import { BusinessDataProvider } from './businessData';
|
||||||
|
|
||||||
|
import { SearchProvider } from './search';
|
||||||
|
import { NextPassagesDisplay } from './nextPassagesDisplay';
|
||||||
|
import { StopsManager } from './stopsManager';
|
||||||
|
|
||||||
|
import styles from './App.module.css';
|
||||||
|
|
||||||
|
|
||||||
|
function parseFragment() {
|
||||||
|
const fragmentString = (window.location.hash || "?");
|
||||||
|
return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf('?'), 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: Component = () => {
|
||||||
|
|
||||||
|
console.log('App: New');
|
||||||
|
|
||||||
|
const qs = parseFragment();
|
||||||
|
const widgetId = qs.get('widgetId');
|
||||||
|
const userId = qs.get('userId');
|
||||||
|
|
||||||
|
console.log("App: widgetId:" + widgetId);
|
||||||
|
console.log("App: userId:" + userId);
|
||||||
|
|
||||||
|
const api = new WidgetApi(widgetId);
|
||||||
|
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
||||||
|
api.start();
|
||||||
|
api.on("ready", function() {
|
||||||
|
console.log("App: widget API is READY !!!!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seems to don´t be used...
|
||||||
|
api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent<IVisibilityActionRequest>) => {
|
||||||
|
console.log("App: Visibility change");
|
||||||
|
ev.preventDefault(); // we're handling it, so stop the widget API from doing something.
|
||||||
|
console.log("App: ", ev.detail); // custom handling here
|
||||||
|
/* api.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); */
|
||||||
|
api.transport.reply(ev.detail, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BusinessDataProvider>
|
||||||
|
<SearchProvider>
|
||||||
|
<HopeProvider>
|
||||||
|
<div class={styles.App} data-panelsnap-id="1">
|
||||||
|
<div class={styles.panel}>
|
||||||
|
<StopsManager />
|
||||||
|
</div>
|
||||||
|
<div class={styles.panel}>
|
||||||
|
<NextPassagesDisplay />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HopeProvider>
|
||||||
|
</SearchProvider>
|
||||||
|
</BusinessDataProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
BIN
frontend/src/assets/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
79
frontend/src/businessData.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { createContext, createSignal } from 'solid-js';
|
||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
|
import { Passages, Stops } from './types';
|
||||||
|
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
passages: () => Passages;
|
||||||
|
getLinePassages?: (lineId: string) => Passages;
|
||||||
|
addPassages?: (passages) => void;
|
||||||
|
clearPassages?: () => void;
|
||||||
|
|
||||||
|
stops: () => Stops;
|
||||||
|
addStops?: (stops) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BusinessDataContext = createContext<Store>();
|
||||||
|
|
||||||
|
export function BusinessDataProvider(props: { children: JSX.Element }) {
|
||||||
|
|
||||||
|
const [serverUrl, setServerUrl] = createSignal<string>("https://localhost:4443");
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({ lines: {}, passages: {}, stops: {} });
|
||||||
|
|
||||||
|
async function getLine(lineId: number) {
|
||||||
|
let line = store.lines[lineId];
|
||||||
|
if (line === undefined) {
|
||||||
|
console.log(`${lineId} not found... fetch it from backend.`);
|
||||||
|
const data = await fetch(`${serverUrl()}/line/${lineId}`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
line = await data.json();
|
||||||
|
setStore('lines', lineId, line);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passages = () => {
|
||||||
|
return store.passages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLinePassages = (lineId: string) => {
|
||||||
|
return store.passages[lineId];
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPassages = (passages) => {
|
||||||
|
setStore((s) => {
|
||||||
|
// console.log("s=", s);
|
||||||
|
setStore('passages', passages);
|
||||||
|
// console.log("s=", s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPassages = () => {
|
||||||
|
setStore((s) => {
|
||||||
|
// TODO: Really need to set to undefined to reset ?
|
||||||
|
console.log("s=", s);
|
||||||
|
console.log("s.passages=", s.passages);
|
||||||
|
// setStore('passages', undefined);
|
||||||
|
// setStore('passages', {});
|
||||||
|
console.log("Object.keys(s.passages)=", Object.keys(s.passages));
|
||||||
|
for (const lineId of Object.keys(s.passages)) {
|
||||||
|
console.log("lineId=", lineId);
|
||||||
|
setStore('passages', lineId, undefined);
|
||||||
|
}
|
||||||
|
console.log("s=", s);
|
||||||
|
});
|
||||||
|
// setStore('passages', undefined);
|
||||||
|
// setStore('passages', {});
|
||||||
|
// }
|
||||||
|
console.log("passages=", store.passages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BusinessDataContext.Provider value={{ getLine, passages, getLinePassages, addPassages, clearPassages, serverUrl }}>
|
||||||
|
{props.children}
|
||||||
|
</BusinessDataContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
28
frontend/src/index.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: IDFVoyageur-regular;
|
||||||
|
src: url(/public/fonts/IDFVoyageur-Regular.otf)
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
src: url(/public/fonts/IDFVoyageur-Bold.otf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: IDFVoyageur-medium;
|
||||||
|
src: url(/public/fonts/IDFVoyageur-Medium.otf);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-family: IDFVoyageur;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
}
|
7
frontend/src/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
render(() => (<App/>), document.getElementById('root') as HTMLElement);
|
226
frontend/src/nextPassagesDisplay.module.css
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
/* TODO: Remove this class */
|
||||||
|
.ar16x9 {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idfm: 1860x1080px */
|
||||||
|
.NextPassagesDisplay {
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
--reverse-aspect-ratio: 9/16;
|
||||||
|
/* height is set according to the aspect-ratio, don´t touch it */
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: var(--idfm-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idfm: 1800x100px (margin: 17px 60px) */
|
||||||
|
.header {
|
||||||
|
width: calc(1800/1920*100%);
|
||||||
|
height: calc(100/1080*100%);
|
||||||
|
/*Percentage margin are computed relatively to the nearest block container's width, not height */
|
||||||
|
/* cf. https://developer.mozilla.org/en-US/docs/Web/CSS/margin-bottom */
|
||||||
|
margin: calc(17/1080*var(--reverse-aspect-ratio)*100%) calc(60/1920*100%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .transportMode {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
margin-right: calc(23/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .title {
|
||||||
|
height: 50%;
|
||||||
|
width: 70%;
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .clock {
|
||||||
|
width: calc(175/1920*100%);
|
||||||
|
height: calc(80/100*100%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
border:solid var(--idfm-white) 3px;
|
||||||
|
border-radius: calc(9/86*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .clock svg {
|
||||||
|
aspect-ratio: 2.45;
|
||||||
|
height: calc(0.7*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idfm: 1860x892px (margin: 0px 30px) */
|
||||||
|
.panelsContainer {
|
||||||
|
width: calc(1860/1920*100%);
|
||||||
|
height: calc(892/1080*100%);
|
||||||
|
margin: 0 calc(30/1920*100%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
border-collapse:separate;
|
||||||
|
border:solid var(--idfm-black) 1px;
|
||||||
|
border-radius: calc(15/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextPassagesContainer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextPassagesContainer .line:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
/* To make up for the bottom border deletion */
|
||||||
|
padding-bottom: calc(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Idfm: 1880x176px (margin: 0px 20px) */
|
||||||
|
.line {
|
||||||
|
width: calc(1880/1920*100%);
|
||||||
|
height: calc(100% / 5);
|
||||||
|
margin: 0 calc(20/1920*100%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
/* TODO: compute the border weight according to the parent height */
|
||||||
|
/* TODO: Disable border-bottom for the last .line */
|
||||||
|
border-bottom: solid calc(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line svg {
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idfm: 100x100px (margin: 0px 15px) */
|
||||||
|
.transportMode {
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
margin: 0 calc(15/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tramLinePicto {
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
margin-right: calc(23/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.busLinePicto {
|
||||||
|
aspect-ratio : 2.25;
|
||||||
|
height: calc(70/176*100%);
|
||||||
|
margin-right: calc(23/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination {
|
||||||
|
height: calc(60/176*100%);
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.trafficStatus {
|
||||||
|
height: calc(50/176*100%);
|
||||||
|
aspect-ratio: 35/50;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trafficStatus svg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firstPassage {
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
aspect-ratio: 2.5;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding-right: calc(30/1920*100%);
|
||||||
|
|
||||||
|
/* TODO: compute the border weight according to the parent width */
|
||||||
|
border-right: solid calc(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailableFirstPassage {
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
aspect-ratio: calc(230/100);
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
|
||||||
|
/* TODO: compute the border weight according to the parent width */
|
||||||
|
border-right: solid calc(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.firstPassage svg {
|
||||||
|
aspect-ratio: 215/50;
|
||||||
|
height: calc(1/2*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondPassage {
|
||||||
|
height: calc(45/176*100%);
|
||||||
|
aspect-ratio: calc(230/45);
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondPassage svg {
|
||||||
|
font-family: IDFVoyageur-regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailableSecondPassage {
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
aspect-ratio: calc(230/100);
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailableSecondNextPassage svg {
|
||||||
|
font-family: IDFVoyageur-regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idfm: 1800x54px (margin: 0px 50px) */
|
||||||
|
.footer {
|
||||||
|
width: calc(1820/1920*100%);
|
||||||
|
height: calc(54/1080*100%);
|
||||||
|
margin: 0 calc(50/1920*100%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer div {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
height: 50%;
|
||||||
|
|
||||||
|
margin-left: calc(42/1920*100%);
|
||||||
|
}
|
253
frontend/src/nextPassagesDisplay.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import { Component, createEffect, createSignal, useContext } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
import { createDateNow } from "@solid-primitives/date";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { getTransportModeSrc } from "./types";
|
||||||
|
import { BusinessDataContext } from "./businessData";
|
||||||
|
import { NextPassagesPanel } from "./nextPassagesPanel";
|
||||||
|
|
||||||
|
import { SearchContext } from "./search";
|
||||||
|
|
||||||
|
import styles from "./nextPassagesDisplay.module.css";
|
||||||
|
|
||||||
|
|
||||||
|
export const NextPassagesDisplay: Component = () => {
|
||||||
|
const maxPassagePerPanel = 5;
|
||||||
|
const syncPeriodMsec = 20 * 1000;
|
||||||
|
|
||||||
|
const { passages, getLinePassages, addPassages, clearPassages, serverUrl } =
|
||||||
|
useContext(BusinessDataContext);
|
||||||
|
const { getDisplayedStop } = useContext(SearchContext);
|
||||||
|
|
||||||
|
const [panels, setPanels] = createStore([]);
|
||||||
|
const [displayedPanelId, setDisplayedPanelId] = createSignal<number>(0);
|
||||||
|
|
||||||
|
let _lines = new Map();
|
||||||
|
|
||||||
|
const [dateNow] = createDateNow(1000);
|
||||||
|
|
||||||
|
const panelSwapInterval = setInterval(() => {
|
||||||
|
let nextPanelId = displayedPanelId() + 1;
|
||||||
|
if (nextPanelId >= panels.length) {
|
||||||
|
nextPanelId = 0;
|
||||||
|
}
|
||||||
|
/* console.log(`Display panel #${nextPanelId}`); */
|
||||||
|
setDisplayedPanelId(nextPanelId);
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("######### onStopIdUpdate #########");
|
||||||
|
// Track local.stopIp to force dependency.
|
||||||
|
console.log("getDisplayedStop=", getDisplayedStop());
|
||||||
|
clearPassages();
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(async () => {
|
||||||
|
console.log(`## OnPassageUpdate ${passages()} ##`);
|
||||||
|
/* console.log(passages()); */
|
||||||
|
await requestPassages();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function _fetchLine(lineId: string) {
|
||||||
|
if (!_lines.has(lineId)) {
|
||||||
|
const data = await fetch(`${serverUrl()}/line/${lineId}`, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
const line = await data.json();
|
||||||
|
_lines.set(line.id, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestPassages() {
|
||||||
|
console.log("### requestPassages ###");
|
||||||
|
/* TODO: Manage several displays (one by stop) */
|
||||||
|
const stops = getDisplayedStop();
|
||||||
|
if (stops.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stop = stops[0];
|
||||||
|
|
||||||
|
const httpOptions = { headers: { "Content-Type": "application/json" } };
|
||||||
|
if (stop !== undefined) {
|
||||||
|
const stopId = stop.id;
|
||||||
|
console.log(`Fetching data for ${stopId}`);
|
||||||
|
const url = `${serverUrl()}/stop/nextPassages/${stopId}`;
|
||||||
|
/* console.log(`url=${url}`); */
|
||||||
|
const data = await fetch(url, httpOptions);
|
||||||
|
const response = await data.json();
|
||||||
|
/* console.log(response); */
|
||||||
|
const byLineByDstPassages = response.passages;
|
||||||
|
/* console.log(byLineByDstPassages); */
|
||||||
|
const linePromises = [];
|
||||||
|
for (const lineId of Object.keys(byLineByDstPassages)) {
|
||||||
|
linePromises.push(_fetchLine(lineId));
|
||||||
|
}
|
||||||
|
await Promise.all(linePromises);
|
||||||
|
console.log("byLineByDstPassages=", byLineByDstPassages);
|
||||||
|
// console.log("before addPassages passages=", passages());
|
||||||
|
addPassages(byLineByDstPassages);
|
||||||
|
console.log("AFTER passages=", passages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(
|
||||||
|
// const nextPassagesRequestsInterval = setTimeout(
|
||||||
|
async () => {
|
||||||
|
await requestPassages();
|
||||||
|
},
|
||||||
|
syncPeriodMsec
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Sort transport modes by weight
|
||||||
|
// TODO: Split this method to isolate the nextPassagesPanel part.
|
||||||
|
function _computeHeader(title: string): JSX.Element {
|
||||||
|
let transportModes = [];
|
||||||
|
transportModes = new Set(
|
||||||
|
Object.keys(passages()).map((lineId) => {
|
||||||
|
const line = _lines.get(lineId);
|
||||||
|
if (line !== undefined) {
|
||||||
|
return getTransportModeSrc(line.transportMode, false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div class={styles.header}>
|
||||||
|
<For each={Array.from(transportModes)}>
|
||||||
|
{(transportMode) => {
|
||||||
|
return (
|
||||||
|
<div class={styles.transportMode}>
|
||||||
|
<img src={transportMode} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
<div class={styles.title}>
|
||||||
|
<svg viewbox="0 0 1260 50">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="50%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
font-size="50"
|
||||||
|
style="fill: #ffffff"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class={styles.clock}>
|
||||||
|
<svg viewbox="0 0 115 43">
|
||||||
|
<text
|
||||||
|
x="50%"
|
||||||
|
y="55%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="43"
|
||||||
|
style="fill: #ffffff"
|
||||||
|
>
|
||||||
|
{format(dateNow(), "HH:mm")}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _computeFooter(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div class={styles.footer}>
|
||||||
|
<For each={panels}>
|
||||||
|
{(positioned) => {
|
||||||
|
const { position, panel } = positioned;
|
||||||
|
const circleStyle = {
|
||||||
|
fill: `var(--idfm-${position == displayedPanelId() ? "white" : "black"
|
||||||
|
})`,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<svg viewBox="0 0 29 29">
|
||||||
|
<circle
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="13"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="3"
|
||||||
|
style={circleStyle}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainDivClasses = `${styles.NextPassagesDisplay} ${styles.ar16x9}`;
|
||||||
|
return (
|
||||||
|
<div class={mainDivClasses}>
|
||||||
|
{_computeHeader("Prochains passages")}
|
||||||
|
<div class={styles.panelsContainer}>
|
||||||
|
{() => {
|
||||||
|
setPanels([]);
|
||||||
|
|
||||||
|
let newPanels = [];
|
||||||
|
let positioneds = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
let chunk = {};
|
||||||
|
let chunkSize = 0;
|
||||||
|
|
||||||
|
console.log("passages=", passages());
|
||||||
|
for (const lineId of Object.keys(passages())) {
|
||||||
|
console.log("lineId=", lineId);
|
||||||
|
const byLinePassages = getLinePassages(lineId);
|
||||||
|
console.log("byLinePassages=", byLinePassages);
|
||||||
|
const byLinePassagesKeys = Object.keys(byLinePassages);
|
||||||
|
console.log("byLinePassagesKeys=", byLinePassagesKeys);
|
||||||
|
|
||||||
|
if (byLinePassagesKeys.length <= maxPassagePerPanel - chunkSize) {
|
||||||
|
chunk[lineId] = byLinePassages;
|
||||||
|
chunkSize += byLinePassagesKeys.length;
|
||||||
|
} else {
|
||||||
|
console.log("chunk=", chunk);
|
||||||
|
const [store, setStore] = createStore(chunk);
|
||||||
|
const panelid = index++;
|
||||||
|
const panel = (
|
||||||
|
<NextPassagesPanel
|
||||||
|
show={panelid == displayedPanelId()}
|
||||||
|
nextPassages={store}
|
||||||
|
lines={_lines}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
newPanels.push(panel);
|
||||||
|
positioneds.push({ position: panelid, panel });
|
||||||
|
|
||||||
|
chunk = {};
|
||||||
|
chunk[lineId] = byLinePassages;
|
||||||
|
chunkSize = byLinePassagesKeys.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunkSize) {
|
||||||
|
const panelId = index++;
|
||||||
|
const [store, setStore] = createStore(chunk);
|
||||||
|
const panel = (
|
||||||
|
<NextPassagesPanel
|
||||||
|
show={panelId == displayedPanelId()}
|
||||||
|
nextPassages={store}
|
||||||
|
lines={_lines}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
newPanels.push(panel);
|
||||||
|
positioneds.push({ position: panelId, panel });
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanels(positioneds);
|
||||||
|
return newPanels;
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{_computeFooter()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
121
frontend/src/nextPassagesPanel.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { Component } from 'solid-js';
|
||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import { createDateNow, getTime } from '@solid-primitives/date';
|
||||||
|
import { Motion } from "@motionone/solid";
|
||||||
|
|
||||||
|
import { TrafficStatus } from './types';
|
||||||
|
import { renderLineTransportMode, renderLinePicto } from './utils';
|
||||||
|
|
||||||
|
import styles from './nextPassagesDisplay.module.css';
|
||||||
|
|
||||||
|
|
||||||
|
export const NextPassagesPanel: Component = (props) => {
|
||||||
|
|
||||||
|
/* TODO: Find where to get data to compute traffic status. */
|
||||||
|
const trafficStatusColor = new Map<TrafficStatus, string>([
|
||||||
|
[TrafficStatus.UNKNOWN, "#ffffff"],
|
||||||
|
[TrafficStatus.FLUID, "#00643c"],
|
||||||
|
[TrafficStatus.DISRUPTED, "#ffbe00"],
|
||||||
|
[TrafficStatus.VERY_DISRUPTED, "#ff5a00"],
|
||||||
|
[TrafficStatus.BYPASSED, "#ffffff"]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [dateNow] = createDateNow(5000);
|
||||||
|
|
||||||
|
function _computeTtwPassage(class_, passage, fontSize) {
|
||||||
|
const refTs = passage.expectedDepartTs !== null ? passage.expectedDepartTs : passage.expectedArrivalTs;
|
||||||
|
const ttwSec = refTs - (getTime(dateNow()) / 1000);
|
||||||
|
const isApproaching = ttwSec <= 60;
|
||||||
|
return (
|
||||||
|
<div class={class_}>
|
||||||
|
<svg viewBox={`0 0 215 ${fontSize}`}>
|
||||||
|
<Motion.text
|
||||||
|
x="100%" y="55%"
|
||||||
|
dominant-baseline="middle" text-anchor="end"
|
||||||
|
font-size={fontSize} style={{ fill: "#000000" }}
|
||||||
|
initial={isApproaching}
|
||||||
|
animate={{ opacity: [1, 0, 1] }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity }}>
|
||||||
|
{Math.floor(ttwSec / 60)} min
|
||||||
|
</Motion.text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _computeUnavailablePassage(class_) {
|
||||||
|
const textStyle = { fill: "#000000" };
|
||||||
|
return (
|
||||||
|
<div class={class_}>
|
||||||
|
<svg viewbox="0 0 230 110">
|
||||||
|
<text x="100%" y="26" font-size="25" text-anchor="end" style={textStyle}>Information</text>
|
||||||
|
<text x="100%" y="63" font-size="25" text-anchor="end" style={textStyle}>non</text>
|
||||||
|
<text x="100%" y="100" font-size="25" text-anchor="end" style={textStyle}>disponible</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _computeSecondPassage(passage): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Show when={passage !== undefined} fallback={_computeUnavailablePassage(styles.unavailableSecondPassage)}>
|
||||||
|
{_computeTtwPassage(styles.secondPassage, passage, 45)}
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _computeFirstPassage(passage): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Show when={passage !== undefined} fallback={_computeUnavailablePassage(styles.unavailableFirstPassage)}>
|
||||||
|
{_computeTtwPassage(styles.firstPassage, passage, 50)}
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: Manage end of service */
|
||||||
|
function _genNextPassages(nextPassages, line, destination) {
|
||||||
|
const nextPassagesLength = nextPassages.length;
|
||||||
|
const firstPassage = nextPassagesLength > 0 ? nextPassages[0] : undefined;
|
||||||
|
const secondPassage = nextPassagesLength > 1 ? nextPassages[1] : undefined;
|
||||||
|
const trafficStatusStyle = { fill: trafficStatusColor.get(line.trafficStatus) };
|
||||||
|
return (
|
||||||
|
<div class={styles.line}>
|
||||||
|
<div class={styles.transportMode}>
|
||||||
|
{renderLineTransportMode(line)}
|
||||||
|
</div>
|
||||||
|
{renderLinePicto(line, styles)}
|
||||||
|
<div class={styles.destination}>
|
||||||
|
<svg viewbox="0 0 600 40">
|
||||||
|
<text x="0" y="50%" dominant-baseline="middle" font-size="40" style={{ fill: "#000000" }}>
|
||||||
|
{destination}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class={styles.trafficStatus}>
|
||||||
|
<svg viewBox="0 0 51 51">
|
||||||
|
<circle cx="50%" cy="50%" r="24" stroke="#231f20" stroke-width="3" style={trafficStatusStyle} />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{firstPassage ? _computeFirstPassage(firstPassage) : null}
|
||||||
|
{secondPassage ? _computeSecondPassage(secondPassage) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div classList={{ [styles.nextPassagesContainer]: true, [styles.displayed]: props.show }} style={{ "top": `${100 * props.position}%` }}>
|
||||||
|
{() => {
|
||||||
|
const ret = [];
|
||||||
|
for (const lineId of Object.keys(props.nextPassages)) {
|
||||||
|
const line = props.lines.get(lineId);
|
||||||
|
const byLineNextPassages = props.nextPassages[lineId];
|
||||||
|
for (const destination of Object.keys(byLineNextPassages)) {
|
||||||
|
const nextPassages = byLineNextPassages[destination];
|
||||||
|
ret.push(_genNextPassages(nextPassages, line, destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
75
frontend/src/search.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { batch, createContext, createSignal } from 'solid-js';
|
||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
|
import { Stop, Stops } from './types';
|
||||||
|
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
getMarkers: () => Markers;
|
||||||
|
addMarkers?: (stopId, markers) => void;
|
||||||
|
setMarkers?: (markers) => void;
|
||||||
|
|
||||||
|
getStops: () => Stops;
|
||||||
|
setStops?: (stops) => void;
|
||||||
|
removeStops?: (stopIds) => void;
|
||||||
|
|
||||||
|
getDisplayedStop: () => Stop;
|
||||||
|
setDisplayedStop: (stop: Stop) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchContext = createContext<Store>();
|
||||||
|
|
||||||
|
export function SearchProvider(props: { children: JSX.Element }) {
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({stops: {}, markers: {}, displayedStop: []});
|
||||||
|
|
||||||
|
const getStops = () => {
|
||||||
|
return store.stops;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setStops = (stops) => {
|
||||||
|
setStore((s) => {
|
||||||
|
setStore('stops', stops);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeStops = (stopIds) => {
|
||||||
|
batch(() => {
|
||||||
|
for(const stopId of stopIds) {
|
||||||
|
setStore('stops', stopId, undefined);
|
||||||
|
setStore('markers', stopId, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMarkers = () => {
|
||||||
|
return store.markers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMarkers = (stopId, markers) => {
|
||||||
|
setStore('markers', stopId, markers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMarkers = (markers) => {
|
||||||
|
setStore('markers', markers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayedStop = () => {
|
||||||
|
/* console.log(store.displayedStop); */
|
||||||
|
return store.displayedStop;
|
||||||
|
};
|
||||||
|
const setDisplayedStop = (stop: Stop) => {
|
||||||
|
/* console.log(stop); */
|
||||||
|
setStore((s) => {
|
||||||
|
console.log("s.displayedStop=", s.displayedStop);
|
||||||
|
setStore('displayedStop', [stop]);
|
||||||
|
});
|
||||||
|
/* console.log(store.displayedStop); */
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchContext.Provider value={{addMarkers, getMarkers, setMarkers, getStops, removeStops, setStops, getDisplayedStop, setDisplayedStop}}>
|
||||||
|
{props.children}
|
||||||
|
</SearchContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
33
frontend/src/stopManager.module.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
svg {
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transportMode {
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
height: 70%;
|
||||||
|
margin-left: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tramLinePicto {
|
||||||
|
height: 70%;
|
||||||
|
margin-left: 1%;
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainLinePicto {
|
||||||
|
height: 70%;
|
||||||
|
margin-left: 1%;
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metroLinePicto {
|
||||||
|
height: 70%;
|
||||||
|
margin-left: 1%;
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.busLinePicto {
|
||||||
|
height: 70%;
|
||||||
|
margin-left: 1%;
|
||||||
|
aspect-ratio : 2.25;
|
||||||
|
}
|
224
frontend/src/stopsManager.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { batch, Component, createEffect, createResource, createSignal, onMount, Show, useContext } from 'solid-js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box, Button, Input, InputLeftAddon, InputGroup, HStack, List, ListItem, Progress,
|
||||||
|
ProgressIndicator, VStack
|
||||||
|
} from "@hope-ui/solid";
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import L from 'leaflet';
|
||||||
|
|
||||||
|
import { BusinessDataContext } from './businessData';
|
||||||
|
import { SearchContext } from './search';
|
||||||
|
|
||||||
|
import { renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils';
|
||||||
|
|
||||||
|
import styles from './stopManager.module.css';
|
||||||
|
|
||||||
|
|
||||||
|
const StopRepr: Component = (props) => {
|
||||||
|
|
||||||
|
const { getLine } = useContext(BusinessDataContext);
|
||||||
|
|
||||||
|
const [lineReprs] = createResource(props.stop.lines, fetchLinesRepr);
|
||||||
|
|
||||||
|
async function fetchLinesRepr(lineIds) {
|
||||||
|
const reprs = [];
|
||||||
|
for (const lineId of lineIds) {
|
||||||
|
const line = await getLine(lineId);
|
||||||
|
if (line !== undefined) {
|
||||||
|
reprs.push(<div class={styles.transportMode}>{renderLineTransportMode(line)}</div>);
|
||||||
|
reprs.push(renderLinePicto(line, styles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reprs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack height="100%">
|
||||||
|
{props.stop.name}
|
||||||
|
<For each={lineReprs()}>{(line) => line}</For>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StopAreaRepr: Component = (props) => {
|
||||||
|
|
||||||
|
const { getLine } = useContext(BusinessDataContext);
|
||||||
|
|
||||||
|
const [lineReprs] = createResource(props.stop, fetchLinesRepr);
|
||||||
|
|
||||||
|
async function fetchLinesRepr(stop) {
|
||||||
|
const lineIds = new Set(stop.lines);
|
||||||
|
const stops = stop.stops;
|
||||||
|
for (const stop of stops) {
|
||||||
|
stop.lines.forEach(lineIds.add, lineIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byModeReprs = {};
|
||||||
|
for (const lineId of lineIds) {
|
||||||
|
const line = await getLine(lineId);
|
||||||
|
if (line !== undefined) {
|
||||||
|
if (!(line.transportMode in byModeReprs)) {
|
||||||
|
byModeReprs[line.transportMode] = {
|
||||||
|
mode: <div class={styles.transportMode}>{renderLineTransportMode(line)}</div>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
byModeReprs[line.transportMode][line.shortName] = renderLinePicto(line, styles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reprs = [];
|
||||||
|
const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < TransportModeWeights[y]);
|
||||||
|
for (const transportMode of sortedTransportModes) {
|
||||||
|
const lines = byModeReprs[transportMode];
|
||||||
|
const repr = [lines.mode];
|
||||||
|
delete lines.mode;
|
||||||
|
for (const lineId of Object.keys(lines).sort((x, y) => x.localeCompare(y))) {
|
||||||
|
repr.push(lines[lineId]);
|
||||||
|
}
|
||||||
|
reprs.push(repr);
|
||||||
|
}
|
||||||
|
return reprs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack height="100%">
|
||||||
|
{props.stop.name}
|
||||||
|
<For each={lineReprs()}>{(line) => line}</For>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Map: Component = (props) => {
|
||||||
|
|
||||||
|
const mapCenter = [48.853, 2.35];
|
||||||
|
|
||||||
|
const { addMarkers, getStops } = useContext(SearchContext);
|
||||||
|
|
||||||
|
let mapDiv: any;
|
||||||
|
let map = null;
|
||||||
|
const stopsLayerGroup = L.featureGroup();
|
||||||
|
|
||||||
|
function buildMap(div: HTMLDivElement) {
|
||||||
|
map = L.map(div).setView(mapCenter, 11);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
stopsLayerGroup.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMarker(stop) {
|
||||||
|
const markers = [];
|
||||||
|
if (stop.lat !== undefined && stop.lon !== undefined) {
|
||||||
|
/* TODO: Add stop lines representation to popup. */
|
||||||
|
markers.push(L.marker([stop.lat, stop.lon]).bindPopup(`${stop.name}`).openPopup());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const _stop of stop.stops) {
|
||||||
|
markers.push(...setMarker(_stop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => buildMap(mapDiv));
|
||||||
|
|
||||||
|
const onStopUpdate = createEffect(() => {
|
||||||
|
/* TODO: Avoid to clear all layers... */
|
||||||
|
stopsLayerGroup.clearLayers();
|
||||||
|
|
||||||
|
for (const stop of Object.values(getStops())) {
|
||||||
|
const markers = setMarker(stop);
|
||||||
|
addMarkers(stop.id, markers);
|
||||||
|
for (const marker of markers) {
|
||||||
|
stopsLayerGroup.addLayer(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopsBound = stopsLayerGroup.getBounds();
|
||||||
|
if (Object.keys(stopsBound).length) {
|
||||||
|
map.fitBounds(stopsBound);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div ref={mapDiv} id='main-map' style={{ width: "100%", height: "100%" }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopsManager: Component = (props) => {
|
||||||
|
|
||||||
|
const [minCharactersNb, setMinCharactersNb] = createSignal<int>(4);
|
||||||
|
const [_inProgress, _setInProgress] = createSignal<bool>(false);
|
||||||
|
|
||||||
|
const { serverUrl } = useContext(BusinessDataContext);
|
||||||
|
const { getStops, removeStops, setStops, setDisplayedStop } = useContext(SearchContext);
|
||||||
|
|
||||||
|
async function _fetchStopByName(name) {
|
||||||
|
const data = await fetch(`${serverUrl()}/stop/?name=${name}`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
const stops = await data.json();
|
||||||
|
const stopIds = stops.map((stop) => stop.id);
|
||||||
|
|
||||||
|
const stopIdsToRemove = Object.keys(getStops()).filter(stopId => !(stopId in stopIds));
|
||||||
|
|
||||||
|
const byIdStops = {};
|
||||||
|
for (const stop of stops) {
|
||||||
|
byIdStops[stop.id] = stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
removeStops(stopIdsToRemove);
|
||||||
|
setStops(byIdStops);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _onStopNameInput(event) {
|
||||||
|
/* TODO: Add a tempo before fetching stop for giving time to user to finish his request */
|
||||||
|
const stopName = event.target.value;
|
||||||
|
if (stopName.length >= minCharactersNb()) {
|
||||||
|
console.log(`Fetching data for ${stopName}`);
|
||||||
|
_setInProgress(true);
|
||||||
|
await _fetchStopByName(stopName);
|
||||||
|
_setInProgress(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack h="100%">
|
||||||
|
<InputGroup w="50%" h="5%">
|
||||||
|
<InputLeftAddon>🚉 🚏</InputLeftAddon>
|
||||||
|
<Input onInput={_onStopNameInput} readOnly={_inProgress()} placeholder="Stop name..." />
|
||||||
|
</InputGroup>
|
||||||
|
<Progress size="xs" w="50%" indeterminate={_inProgress()}>
|
||||||
|
<ProgressIndicator striped animated />
|
||||||
|
</Progress>
|
||||||
|
<Box w="100%" h="40%" borderWidth="1px" borderColor="var(--idfm-black)" borderRadius="$lg" overflow="scroll" marginBottom="2px">
|
||||||
|
<List width="100%" height="100%">
|
||||||
|
{() => {
|
||||||
|
const items = [];
|
||||||
|
for (const stop of Object.values(getStops()).sort((x, y) => x.name.localeCompare(y.name))) {
|
||||||
|
items.push(
|
||||||
|
<ListItem h="10%" borderWidth="1px" mb="0px" color="var(--idfm-black)" borderRadius="$lg">
|
||||||
|
<Button fullWidth="true" color="var(--idfm-black)" bg="white" onClick={() => {
|
||||||
|
console.log(`${stop.id} clicked !!!`);
|
||||||
|
setDisplayedStop(stop);
|
||||||
|
}}>
|
||||||
|
<Box w="100%" h="100%">
|
||||||
|
<Show when={stop.stops !== undefined} fallback={<StopRepr stop={stop} />}>
|
||||||
|
<StopAreaRepr stop={stop} />
|
||||||
|
</Show>
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
</ListItem>);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
<Box borderWidth="1px" borderColor="var(--idfm-black)" borderRadius="$lg" h="55%" w="100%" overflow="scroll">
|
||||||
|
<Map />
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
43
frontend/src/types.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const validTransportModes = ["bus", "tram", "metro", "rer", "transilien", "funicular", "ter", "unknown"];
|
||||||
|
|
||||||
|
export function getTransportModeSrc(mode: string, color: bool = true): string {
|
||||||
|
let ret = null;
|
||||||
|
if (validTransportModes.includes(mode)) {
|
||||||
|
ret = `/public/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TrafficStatus {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
FLUID,
|
||||||
|
DISRUPTED,
|
||||||
|
VERY_DISRUPTED,
|
||||||
|
BYPASSED
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Passages { };
|
||||||
|
export interface Passage {
|
||||||
|
line: number,
|
||||||
|
operator: string,
|
||||||
|
destinations: Array<string>,
|
||||||
|
atStop: boolean,
|
||||||
|
aimedArrivalTs: number,
|
||||||
|
expectedArrivalTs: number,
|
||||||
|
arrivalPlatformName: string,
|
||||||
|
aimedDepartTs: number,
|
||||||
|
expectedDepartTs: number,
|
||||||
|
arrivalStatus: string,
|
||||||
|
departStatus: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export interface Stops { };
|
||||||
|
export interface Stop {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
town: string,
|
||||||
|
lat: number,
|
||||||
|
lon: number,
|
||||||
|
lines: Array<string>
|
||||||
|
};
|
106
frontend/src/utils.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { getTransportModeSrc } from './types';
|
||||||
|
|
||||||
|
export const TransportModeWeights = {
|
||||||
|
bus: 1,
|
||||||
|
tram: 2,
|
||||||
|
val: 3,
|
||||||
|
funicular: 4,
|
||||||
|
metro: 5,
|
||||||
|
rer: 6,
|
||||||
|
transilien: 7,
|
||||||
|
ter: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderLineTransportMode(line): JSX.Element {
|
||||||
|
return <img src={getTransportModeSrc(line.transportMode)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBusLinePicto(line, styles): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div class={styles.busLinePicto}>
|
||||||
|
<svg viewBox="0 0 31.5 14">
|
||||||
|
<rect x="0" y="0" width="31.5" height="14" rx="1.5" ry="1.5" style={{ fill: `#${line.backColorHexa}` }} />
|
||||||
|
<text x="50%"
|
||||||
|
y="55%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="7.4"
|
||||||
|
style={{ fill: `#${line.foreColorHexa}` }}>
|
||||||
|
{line.shortName}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTramLinePicto(line, styles): JSX.Element {
|
||||||
|
const lineStyle = { fill: `#${line.backColorHexa}` };
|
||||||
|
return (
|
||||||
|
<div class={styles.tramLinePicto}>
|
||||||
|
<svg viewBox="0 0 20 20">
|
||||||
|
<rect x="0" y="0" width="20" height="3" rx="1" ry="1" style={lineStyle} />
|
||||||
|
<rect x="0" y="17" width="20" height="3" rx="1" ry="1" style={lineStyle} />
|
||||||
|
<text x="50%"
|
||||||
|
y="55%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="11"
|
||||||
|
style={{ fill: "#00000" }}>
|
||||||
|
{line.shortName}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMetroLinePicto(line, styles): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div class={styles.metroLinePicto}>
|
||||||
|
<svg viewbox="0 0 20 20">
|
||||||
|
<circle cx="10" cy="10" r="10" style={{ fill: `#${line.backColorHexa}` }} />
|
||||||
|
<text x="50%"
|
||||||
|
y="55%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="11" style={{ fill: `#${line.foreColorHexa}` }}>
|
||||||
|
{line.shortName}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTrainLinePicto(line, styles): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div class={styles.trainLinePicto}>
|
||||||
|
<svg viewbox="0 0 20 20">
|
||||||
|
<rect x="0" y="0" width="20" height="20" rx="4.5" ry="4.5" style={{ fill: `#${line.backColorHexa}` }} />
|
||||||
|
<text x="50%"
|
||||||
|
y="55%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
text-anchor="middle"
|
||||||
|
font-size="11"
|
||||||
|
style={{ fill: `#${line.foreColorHexa}` }}>
|
||||||
|
{line.shortName}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderLinePicto(line, styles): JSX.Element {
|
||||||
|
switch (line.transportMode) {
|
||||||
|
case "bus":
|
||||||
|
case "funicular":
|
||||||
|
return renderBusLinePicto(line, styles);
|
||||||
|
case "tram":
|
||||||
|
return renderTramLinePicto(line, styles);
|
||||||
|
/* case "val": */
|
||||||
|
case "metro":
|
||||||
|
return renderMetroLinePicto(line, styles);
|
||||||
|
case "transilien":
|
||||||
|
case "rer":
|
||||||
|
case "ter":
|
||||||
|
return renderTrainLinePicto(line, styles);
|
||||||
|
}
|
||||||
|
}
|
21
frontend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"target": "ES6",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowJs": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"strict": true,
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "typescript-eslint-language-service"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
23
frontend/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import solidPlugin from 'vite-plugin-solid';
|
||||||
|
import basicSsl from '@vitejs/plugin-basic-ssl'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solidPlugin(), basicSsl()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
base: '/widget',
|
||||||
|
proxy: {
|
||||||
|
'/widget/.*': {
|
||||||
|
target: 'https://localhost:3000/',
|
||||||
|
rewrite: (path) => {
|
||||||
|
console.error("PATH: ", path);
|
||||||
|
return path.replace(/\/widget/, '#');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|