From dde835760a9500d6b14afbc85a7f44e63a6840b8 Mon Sep 17 00:00:00 2001 From: Adrien Date: Sun, 22 Jan 2023 16:53:45 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20First=20commit=20!!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.gitignore | 1 + backend/README.md | 0 backend/docker-compose.yml | 19 + .../init-user-db.sh | 11 + backend/idfm_matrix_backend/db/__init__.py | 4 + backend/idfm_matrix_backend/db/base_class.py | 34 ++ backend/idfm_matrix_backend/db/db.py | 80 ++++ .../idfm_interface/__init__.py | 2 + .../idfm_interface/idfm_interface.py | 447 ++++++++++++++++++ .../idfm_interface/idfm_types.py | 277 +++++++++++ .../idfm_interface/ratp_types.py | 25 + .../idfm_matrix_backend/models/__init__.py | 3 + backend/idfm_matrix_backend/models/line.py | 176 +++++++ backend/idfm_matrix_backend/models/stop.py | 144 ++++++ backend/idfm_matrix_backend/models/user.py | 25 + .../idfm_matrix_backend/schemas/__init__.py | 3 + backend/idfm_matrix_backend/schemas/line.py | 60 +++ .../schemas/next_passage.py | 22 + backend/idfm_matrix_backend/schemas/stop.py | 25 + backend/main.py | 208 ++++++++ backend/pyproject.toml | 58 +++ frontend/.eslintrc.js | 23 + frontend/.gitignore | 3 + frontend/README.md | 34 ++ frontend/index.html | 16 + frontend/package.json | 33 ++ frontend/public/Trafic_fluide_RVB.svg | 1 + frontend/public/Trafic_perturbe_RVB.svg | 1 + frontend/public/Trafic_tres_perturbe_RVB.svg | 1 + frontend/public/fonts/IDFVoyageur-Bold.otf | Bin 0 -> 59180 bytes frontend/public/fonts/IDFVoyageur-Medium.otf | Bin 0 -> 57932 bytes frontend/public/fonts/IDFVoyageur-Regular.otf | Bin 0 -> 57024 bytes frontend/public/symbole_bus_RVB.svg | 1 + .../public/symbole_bus_support_fonce_RVB.svg | 23 + frontend/public/symbole_cable_RVB.svg | 1 + .../symbole_cable_support_fonce_RVB.svg | 23 + frontend/public/symbole_funicular_RVB.svg | 1 + .../symbole_funicular_support_fonce_RVB.svg | 1 + frontend/public/symbole_metro_RVB.svg | 1 + .../symbole_metro_support_fonce_RVB.svg | 14 + .../public/symbole_navette_fluviale_RVB.svg | 1 + frontend/public/symbole_rer_RVB.svg | 1 + .../public/symbole_rer_support_fonce_RVB.svg | 26 + frontend/public/symbole_rer_velo_RVB.svg | 1 + frontend/public/symbole_ter_RVB.svg | 1 + .../public/symbole_ter_support_fonce_RVB.svg | 1 + frontend/public/symbole_train_RER_RVB.svg | 1 + .../symbole_train_RER_support_fonce_RVB.svg | 44 ++ frontend/public/symbole_tram_RVB.svg | 1 + .../public/symbole_tram_support_fonce_RVB.svg | 23 + frontend/public/symbole_transilien_RVB.svg | 1 + .../symbole_transilien_support_fonce_RVB.svg | 23 + frontend/src/App.module.css | 21 + frontend/src/App.tsx | 65 +++ frontend/src/assets/favicon.ico | Bin 0 -> 15086 bytes frontend/src/businessData.tsx | 79 ++++ frontend/src/index.css | 28 ++ frontend/src/index.tsx | 7 + frontend/src/nextPassagesDisplay.module.css | 226 +++++++++ frontend/src/nextPassagesDisplay.tsx | 253 ++++++++++ frontend/src/nextPassagesPanel.tsx | 121 +++++ frontend/src/search.tsx | 75 +++ frontend/src/stopManager.module.css | 33 ++ frontend/src/stopsManager.tsx | 224 +++++++++ frontend/src/types.tsx | 43 ++ frontend/src/utils.tsx | 106 +++++ frontend/tsconfig.json | 21 + frontend/vite.config.ts | 23 + 68 files changed, 3250 insertions(+) create mode 100644 backend/.gitignore create mode 100644 backend/README.md create mode 100644 backend/docker-compose.yml create mode 100755 backend/docker/database/docker-entrypoint-initdb.d/init-user-db.sh create mode 100644 backend/idfm_matrix_backend/db/__init__.py create mode 100644 backend/idfm_matrix_backend/db/base_class.py create mode 100644 backend/idfm_matrix_backend/db/db.py create mode 100644 backend/idfm_matrix_backend/idfm_interface/__init__.py create mode 100644 backend/idfm_matrix_backend/idfm_interface/idfm_interface.py create mode 100644 backend/idfm_matrix_backend/idfm_interface/idfm_types.py create mode 100644 backend/idfm_matrix_backend/idfm_interface/ratp_types.py create mode 100644 backend/idfm_matrix_backend/models/__init__.py create mode 100644 backend/idfm_matrix_backend/models/line.py create mode 100644 backend/idfm_matrix_backend/models/stop.py create mode 100644 backend/idfm_matrix_backend/models/user.py create mode 100644 backend/idfm_matrix_backend/schemas/__init__.py create mode 100644 backend/idfm_matrix_backend/schemas/line.py create mode 100644 backend/idfm_matrix_backend/schemas/next_passage.py create mode 100644 backend/idfm_matrix_backend/schemas/stop.py create mode 100644 backend/main.py create mode 100644 backend/pyproject.toml create mode 100644 frontend/.eslintrc.js create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/Trafic_fluide_RVB.svg create mode 100644 frontend/public/Trafic_perturbe_RVB.svg create mode 100644 frontend/public/Trafic_tres_perturbe_RVB.svg create mode 100644 frontend/public/fonts/IDFVoyageur-Bold.otf create mode 100644 frontend/public/fonts/IDFVoyageur-Medium.otf create mode 100644 frontend/public/fonts/IDFVoyageur-Regular.otf create mode 100644 frontend/public/symbole_bus_RVB.svg create mode 100644 frontend/public/symbole_bus_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_cable_RVB.svg create mode 100644 frontend/public/symbole_cable_support_fonce_RVB.svg create mode 120000 frontend/public/symbole_funicular_RVB.svg create mode 120000 frontend/public/symbole_funicular_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_metro_RVB.svg create mode 100644 frontend/public/symbole_metro_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_navette_fluviale_RVB.svg create mode 100644 frontend/public/symbole_rer_RVB.svg create mode 100644 frontend/public/symbole_rer_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_rer_velo_RVB.svg create mode 120000 frontend/public/symbole_ter_RVB.svg create mode 120000 frontend/public/symbole_ter_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_train_RER_RVB.svg create mode 100644 frontend/public/symbole_train_RER_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_tram_RVB.svg create mode 100644 frontend/public/symbole_tram_support_fonce_RVB.svg create mode 100644 frontend/public/symbole_transilien_RVB.svg create mode 100644 frontend/public/symbole_transilien_support_fonce_RVB.svg create mode 100644 frontend/src/App.module.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/favicon.ico create mode 100644 frontend/src/businessData.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/nextPassagesDisplay.module.css create mode 100644 frontend/src/nextPassagesDisplay.tsx create mode 100644 frontend/src/nextPassagesPanel.tsx create mode 100644 frontend/src/search.tsx create mode 100644 frontend/src/stopManager.module.css create mode 100644 frontend/src/stopsManager.tsx create mode 100644 frontend/src/types.tsx create mode 100644 frontend/src/utils.tsx create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..7fce5c8 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +!**/__pycache__/ diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..09d1ab1 --- /dev/null +++ b/backend/docker-compose.yml @@ -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 diff --git a/backend/docker/database/docker-entrypoint-initdb.d/init-user-db.sh b/backend/docker/database/docker-entrypoint-initdb.d/init-user-db.sh new file mode 100755 index 0000000..9b9f4ef --- /dev/null +++ b/backend/docker/database/docker-entrypoint-initdb.d/init-user-db.sh @@ -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 + diff --git a/backend/idfm_matrix_backend/db/__init__.py b/backend/idfm_matrix_backend/db/__init__.py new file mode 100644 index 0000000..8d9bf84 --- /dev/null +++ b/backend/idfm_matrix_backend/db/__init__.py @@ -0,0 +1,4 @@ +from .db import Database +from .base_class import Base + +db = Database() diff --git a/backend/idfm_matrix_backend/db/base_class.py b/backend/idfm_matrix_backend/db/base_class.py new file mode 100644 index 0000000..2334a75 --- /dev/null +++ b/backend/idfm_matrix_backend/db/base_class.py @@ -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) diff --git a/backend/idfm_matrix_backend/db/db.py b/backend/idfm_matrix_backend/db/db.py new file mode 100644 index 0000000..e050258 --- /dev/null +++ b/backend/idfm_matrix_backend/db/db.py @@ -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() diff --git a/backend/idfm_matrix_backend/idfm_interface/__init__.py b/backend/idfm_matrix_backend/idfm_interface/__init__.py new file mode 100644 index 0000000..c97a846 --- /dev/null +++ b/backend/idfm_matrix_backend/idfm_interface/__init__.py @@ -0,0 +1,2 @@ +from .idfm_interface import IdfmInterface +from .idfm_types import * diff --git a/backend/idfm_matrix_backend/idfm_interface/idfm_interface.py b/backend/idfm_matrix_backend/idfm_interface/idfm_interface.py new file mode 100644 index 0000000..5744be9 --- /dev/null +++ b/backend/idfm_matrix_backend/idfm_interface/idfm_interface.py @@ -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 diff --git a/backend/idfm_matrix_backend/idfm_interface/idfm_types.py b/backend/idfm_matrix_backend/idfm_interface/idfm_types.py new file mode 100644 index 0000000..b703e1f --- /dev/null +++ b/backend/idfm_matrix_backend/idfm_interface/idfm_types.py @@ -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 diff --git a/backend/idfm_matrix_backend/idfm_interface/ratp_types.py b/backend/idfm_matrix_backend/idfm_interface/ratp_types.py new file mode 100644 index 0000000..222fad7 --- /dev/null +++ b/backend/idfm_matrix_backend/idfm_interface/ratp_types.py @@ -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 diff --git a/backend/idfm_matrix_backend/models/__init__.py b/backend/idfm_matrix_backend/models/__init__.py new file mode 100644 index 0000000..c6060f7 --- /dev/null +++ b/backend/idfm_matrix_backend/models/__init__.py @@ -0,0 +1,3 @@ +from .line import Line, LinePicto +from .stop import Stop, StopArea +from .user import UserLastStopSearchResults diff --git a/backend/idfm_matrix_backend/models/line.py b/backend/idfm_matrix_backend/models/line.py new file mode 100644 index 0000000..805968c --- /dev/null +++ b/backend/idfm_matrix_backend/models/line.py @@ -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 diff --git a/backend/idfm_matrix_backend/models/stop.py b/backend/idfm_matrix_backend/models/stop.py new file mode 100644 index 0000000..6bb9543 --- /dev/null +++ b/backend/idfm_matrix_backend/models/stop.py @@ -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 diff --git a/backend/idfm_matrix_backend/models/user.py b/backend/idfm_matrix_backend/models/user.py new file mode 100644 index 0000000..4705d8e --- /dev/null +++ b/backend/idfm_matrix_backend/models/user.py @@ -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 + ) diff --git a/backend/idfm_matrix_backend/schemas/__init__.py b/backend/idfm_matrix_backend/schemas/__init__.py new file mode 100644 index 0000000..232658b --- /dev/null +++ b/backend/idfm_matrix_backend/schemas/__init__.py @@ -0,0 +1,3 @@ +from .line import Line, TransportMode +from .next_passage import NextPassage, NextPassages +from .stop import Stop, StopArea diff --git a/backend/idfm_matrix_backend/schemas/line.py b/backend/idfm_matrix_backend/schemas/line.py new file mode 100644 index 0000000..74f6369 --- /dev/null +++ b/backend/idfm_matrix_backend/schemas/line.py @@ -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] diff --git a/backend/idfm_matrix_backend/schemas/next_passage.py b/backend/idfm_matrix_backend/schemas/next_passage.py new file mode 100644 index 0000000..4bf32a9 --- /dev/null +++ b/backend/idfm_matrix_backend/schemas/next_passage.py @@ -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]]] diff --git a/backend/idfm_matrix_backend/schemas/stop.py b/backend/idfm_matrix_backend/schemas/stop.py new file mode 100644 index 0000000..f239223 --- /dev/null +++ b/backend/idfm_matrix_backend/schemas/stop.py @@ -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] diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..f9e58a1 --- /dev/null +++ b/backend/main.py @@ -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, + ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..06dcbcf --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,58 @@ +[tool.poetry] +name = "idfm-matrix-widget" +version = "0.1.0" +description = "" +authors = ["Adrien SUEUR "] +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 + diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 0000000..b54d687 --- /dev/null +++ b/frontend/.eslintrc.js @@ -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": { + } +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..16acd49 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +package-lock.json diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..434f7bb --- /dev/null +++ b/frontend/README.md @@ -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.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+ +### `npm run build` + +Builds the app for production to the `dist` folder.
+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.
+Your app is ready to be deployed! + +## Deployment + +You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9c6d652 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + Métro-Boulot-Dodo + + + +
+ + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..71f07ee --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/public/Trafic_fluide_RVB.svg b/frontend/public/Trafic_fluide_RVB.svg new file mode 100644 index 0000000..1d33d7e --- /dev/null +++ b/frontend/public/Trafic_fluide_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Trafic_perturbe_RVB.svg b/frontend/public/Trafic_perturbe_RVB.svg new file mode 100644 index 0000000..4243685 --- /dev/null +++ b/frontend/public/Trafic_perturbe_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/Trafic_tres_perturbe_RVB.svg b/frontend/public/Trafic_tres_perturbe_RVB.svg new file mode 100644 index 0000000..e085c30 --- /dev/null +++ b/frontend/public/Trafic_tres_perturbe_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/fonts/IDFVoyageur-Bold.otf b/frontend/public/fonts/IDFVoyageur-Bold.otf new file mode 100644 index 0000000000000000000000000000000000000000..f9cdb70f21c2d01b0f17363d2c0ca23f75508c46 GIT binary patch literal 59180 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

}& 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Μ(3zDiT?zwZn_z0%-T&~2$dtNi`G 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 AcmMzZ literal 0 HcmV?d00001 diff --git a/frontend/public/symbole_bus_RVB.svg b/frontend/public/symbole_bus_RVB.svg new file mode 100644 index 0000000..caefdda --- /dev/null +++ b/frontend/public/symbole_bus_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_bus_support_fonce_RVB.svg b/frontend/public/symbole_bus_support_fonce_RVB.svg new file mode 100644 index 0000000..ac3886a --- /dev/null +++ b/frontend/public/symbole_bus_support_fonce_RVB.svg @@ -0,0 +1,23 @@ + + + + + + + diff --git a/frontend/public/symbole_cable_RVB.svg b/frontend/public/symbole_cable_RVB.svg new file mode 100644 index 0000000..f9c4642 --- /dev/null +++ b/frontend/public/symbole_cable_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_cable_support_fonce_RVB.svg b/frontend/public/symbole_cable_support_fonce_RVB.svg new file mode 100644 index 0000000..b79a620 --- /dev/null +++ b/frontend/public/symbole_cable_support_fonce_RVB.svg @@ -0,0 +1,23 @@ + + + + + + + diff --git a/frontend/public/symbole_funicular_RVB.svg b/frontend/public/symbole_funicular_RVB.svg new file mode 120000 index 0000000..825b34e --- /dev/null +++ b/frontend/public/symbole_funicular_RVB.svg @@ -0,0 +1 @@ +symbole_metro_RVB.svg \ No newline at end of file diff --git a/frontend/public/symbole_funicular_support_fonce_RVB.svg b/frontend/public/symbole_funicular_support_fonce_RVB.svg new file mode 120000 index 0000000..d8caaa2 --- /dev/null +++ b/frontend/public/symbole_funicular_support_fonce_RVB.svg @@ -0,0 +1 @@ +symbole_metro_support_fonce_RVB.svg \ No newline at end of file diff --git a/frontend/public/symbole_metro_RVB.svg b/frontend/public/symbole_metro_RVB.svg new file mode 100644 index 0000000..e47a681 --- /dev/null +++ b/frontend/public/symbole_metro_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_metro_support_fonce_RVB.svg b/frontend/public/symbole_metro_support_fonce_RVB.svg new file mode 100644 index 0000000..ffdedd5 --- /dev/null +++ b/frontend/public/symbole_metro_support_fonce_RVB.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/frontend/public/symbole_navette_fluviale_RVB.svg b/frontend/public/symbole_navette_fluviale_RVB.svg new file mode 100644 index 0000000..0998f18 --- /dev/null +++ b/frontend/public/symbole_navette_fluviale_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_rer_RVB.svg b/frontend/public/symbole_rer_RVB.svg new file mode 100644 index 0000000..9e78172 --- /dev/null +++ b/frontend/public/symbole_rer_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_rer_support_fonce_RVB.svg b/frontend/public/symbole_rer_support_fonce_RVB.svg new file mode 100644 index 0000000..8dc8ab7 --- /dev/null +++ b/frontend/public/symbole_rer_support_fonce_RVB.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/frontend/public/symbole_rer_velo_RVB.svg b/frontend/public/symbole_rer_velo_RVB.svg new file mode 100644 index 0000000..2b462a9 --- /dev/null +++ b/frontend/public/symbole_rer_velo_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_ter_RVB.svg b/frontend/public/symbole_ter_RVB.svg new file mode 120000 index 0000000..ee316e9 --- /dev/null +++ b/frontend/public/symbole_ter_RVB.svg @@ -0,0 +1 @@ +symbole_transilien_RVB.svg \ No newline at end of file diff --git a/frontend/public/symbole_ter_support_fonce_RVB.svg b/frontend/public/symbole_ter_support_fonce_RVB.svg new file mode 120000 index 0000000..36f00bc --- /dev/null +++ b/frontend/public/symbole_ter_support_fonce_RVB.svg @@ -0,0 +1 @@ +symbole_transilien_support_fonce_RVB.svg \ No newline at end of file diff --git a/frontend/public/symbole_train_RER_RVB.svg b/frontend/public/symbole_train_RER_RVB.svg new file mode 100644 index 0000000..7aef724 --- /dev/null +++ b/frontend/public/symbole_train_RER_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_train_RER_support_fonce_RVB.svg b/frontend/public/symbole_train_RER_support_fonce_RVB.svg new file mode 100644 index 0000000..b740d6f --- /dev/null +++ b/frontend/public/symbole_train_RER_support_fonce_RVB.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/frontend/public/symbole_tram_RVB.svg b/frontend/public/symbole_tram_RVB.svg new file mode 100644 index 0000000..abe30fe --- /dev/null +++ b/frontend/public/symbole_tram_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_tram_support_fonce_RVB.svg b/frontend/public/symbole_tram_support_fonce_RVB.svg new file mode 100644 index 0000000..6e5d966 --- /dev/null +++ b/frontend/public/symbole_tram_support_fonce_RVB.svg @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/frontend/public/symbole_transilien_RVB.svg b/frontend/public/symbole_transilien_RVB.svg new file mode 100644 index 0000000..bd00ac1 --- /dev/null +++ b/frontend/public/symbole_transilien_RVB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/symbole_transilien_support_fonce_RVB.svg b/frontend/public/symbole_transilien_support_fonce_RVB.svg new file mode 100644 index 0000000..0222c7b --- /dev/null +++ b/frontend/public/symbole_transilien_support_fonce_RVB.svg @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/frontend/src/App.module.css b/frontend/src/App.module.css new file mode 100644 index 0000000..a606582 --- /dev/null +++ b/frontend/src/App.module.css @@ -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; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..c3083b3 --- /dev/null +++ b/frontend/src/App.tsx @@ -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) => { + 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, {}); */ + api.transport.reply(ev.detail, {}); + }); + + return ( + + + +

+
+ +
+
+ +
+
+ + + + ); +}; + +export default App; diff --git a/frontend/src/assets/favicon.ico b/frontend/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b836b2bccac650e0e7d90514083add91d2c027ff GIT binary patch literal 15086 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 WLaxim%Fn{v*G Passages; + getLinePassages?: (lineId: string) => Passages; + addPassages?: (passages) => void; + clearPassages?: () => void; + + stops: () => Stops; + addStops?: (stops) => void; +}; + +export const BusinessDataContext = createContext(); + +export function BusinessDataProvider(props: { children: JSX.Element }) { + + const [serverUrl, setServerUrl] = createSignal("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 ( + + {props.children} + + ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..5bfd736 --- /dev/null +++ b/frontend/src/index.css @@ -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; +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..3fe2a6a --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,7 @@ +/* @refresh reload */ +import { render } from 'solid-js/web'; + +import './index.css'; +import App from './App'; + +render(() => (), document.getElementById('root') as HTMLElement); diff --git a/frontend/src/nextPassagesDisplay.module.css b/frontend/src/nextPassagesDisplay.module.css new file mode 100644 index 0000000..b24889f --- /dev/null +++ b/frontend/src/nextPassagesDisplay.module.css @@ -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%); +} diff --git a/frontend/src/nextPassagesDisplay.tsx b/frontend/src/nextPassagesDisplay.tsx new file mode 100644 index 0000000..4706dc1 --- /dev/null +++ b/frontend/src/nextPassagesDisplay.tsx @@ -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(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 ( +
+ + {(transportMode) => { + return ( +
+ +
+ ); + }} +
+
+ + + {title} + + +
+
+ + + {format(dateNow(), "HH:mm")} + + +
+
+ ); + } + + function _computeFooter(): JSX.Element { + return ( +
+ + {(positioned) => { + const { position, panel } = positioned; + const circleStyle = { + fill: `var(--idfm-${position == displayedPanelId() ? "white" : "black" + })`, + }; + return ( +
+ + + +
+ ); + }} +
+
+ ); + } + + const mainDivClasses = `${styles.NextPassagesDisplay} ${styles.ar16x9}`; + return ( +
+ {_computeHeader("Prochains passages")} +
+ {() => { + 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 = ( + + ); + 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 = ( + + ); + newPanels.push(panel); + positioneds.push({ position: panelId, panel }); + } + + setPanels(positioneds); + return newPanels; + }} +
+ {_computeFooter()} +
+ ); +}; diff --git a/frontend/src/nextPassagesPanel.tsx b/frontend/src/nextPassagesPanel.tsx new file mode 100644 index 0000000..7da6798 --- /dev/null +++ b/frontend/src/nextPassagesPanel.tsx @@ -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.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 ( +
+ + + {Math.floor(ttwSec / 60)} min + + +
+ ); + } + + function _computeUnavailablePassage(class_) { + const textStyle = { fill: "#000000" }; + return ( +
+ + Information + non + disponible + +
+ ); + } + + function _computeSecondPassage(passage): JSX.Element { + return ( + + {_computeTtwPassage(styles.secondPassage, passage, 45)} + + ); + } + + function _computeFirstPassage(passage): JSX.Element { + return ( + + {_computeTtwPassage(styles.firstPassage, passage, 50)} + + ); + } + + /* 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 ( +
+
+ {renderLineTransportMode(line)} +
+ {renderLinePicto(line, styles)} +
+ + + {destination} + + +
+
+ + + +
+ {firstPassage ? _computeFirstPassage(firstPassage) : null} + {secondPassage ? _computeSecondPassage(secondPassage) : null} +
+ ); + } + + return ( +
+ {() => { + 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; + }} +
+ ); +} diff --git a/frontend/src/search.tsx b/frontend/src/search.tsx new file mode 100644 index 0000000..c9e2163 --- /dev/null +++ b/frontend/src/search.tsx @@ -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(); + +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 ( + + {props.children} + + ); +} diff --git a/frontend/src/stopManager.module.css b/frontend/src/stopManager.module.css new file mode 100644 index 0000000..276d6b2 --- /dev/null +++ b/frontend/src/stopManager.module.css @@ -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; +} diff --git a/frontend/src/stopsManager.tsx b/frontend/src/stopsManager.tsx new file mode 100644 index 0000000..db7ee00 --- /dev/null +++ b/frontend/src/stopsManager.tsx @@ -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(
{renderLineTransportMode(line)}
); + reprs.push(renderLinePicto(line, styles)); + } + } + return reprs; + } + + return ( + + {props.stop.name} + {(line) => line} + + ); +} + +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:
{renderLineTransportMode(line)}
+ }; + } + 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 ( + + {props.stop.name} + {(line) => line} + + ); +} + + +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: '© OpenStreetMap 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
; +} + +export const StopsManager: Component = (props) => { + + const [minCharactersNb, setMinCharactersNb] = createSignal(4); + const [_inProgress, _setInProgress] = createSignal(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 ( + + + 🚉 🚏 + + + + + + + + {() => { + const items = []; + for (const stop of Object.values(getStops()).sort((x, y) => x.name.localeCompare(y.name))) { + items.push( + + + ); + } + return items; + }} + + + + + + + ); +}; diff --git a/frontend/src/types.tsx b/frontend/src/types.tsx new file mode 100644 index 0000000..73607c0 --- /dev/null +++ b/frontend/src/types.tsx @@ -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, + 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 +}; diff --git a/frontend/src/utils.tsx b/frontend/src/utils.tsx new file mode 100644 index 0000000..64b9558 --- /dev/null +++ b/frontend/src/utils.tsx @@ -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 +} + +function renderBusLinePicto(line, styles): JSX.Element { + return ( +
+ + + + {line.shortName} + + +
+ ); +} + +function renderTramLinePicto(line, styles): JSX.Element { + const lineStyle = { fill: `#${line.backColorHexa}` }; + return ( +
+ + + + + {line.shortName} + + +
+ ); +} + +function renderMetroLinePicto(line, styles): JSX.Element { + return ( +
+ + + + {line.shortName} + + +
+ ); +} + +function renderTrainLinePicto(line, styles): JSX.Element { + return ( +
+ + + + {line.shortName} + + +
+ ); +} + +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); + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..8c967df --- /dev/null +++ b/frontend/tsconfig.json @@ -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"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..7d44c05 --- /dev/null +++ b/frontend/vite.config.ts @@ -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', + }, +});