Compare commits
12 Commits
idfm-style
...
440a5faf3c
Author | SHA1 | Date | |
---|---|---|---|
440a5faf3c
|
|||
61097fe9e2
|
|||
62a9000ec2
|
|||
62b6425255
|
|||
ac06df9f87
|
|||
ecfb3c8cb3
|
|||
293a1391bc
|
|||
71e2530c01
|
|||
65f284bc25
|
|||
d3a689cefc
|
|||
726efd8e8c
|
|||
546ec5a89f
|
@@ -2,6 +2,7 @@ from .idfm_interface import IdfmInterface
|
|||||||
|
|
||||||
from .idfm_types import (
|
from .idfm_types import (
|
||||||
Coordinate,
|
Coordinate,
|
||||||
|
Destinations,
|
||||||
FramedVehicleJourney,
|
FramedVehicleJourney,
|
||||||
IdfmLineState,
|
IdfmLineState,
|
||||||
IdfmOperator,
|
IdfmOperator,
|
||||||
@@ -35,6 +36,7 @@ from .idfm_types import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Coordinate",
|
"Coordinate",
|
||||||
|
"Destinations",
|
||||||
"FramedVehicleJourney",
|
"FramedVehicleJourney",
|
||||||
"IdfmInterface",
|
"IdfmInterface",
|
||||||
"IdfmLineState",
|
"IdfmLineState",
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from re import compile as re_compile
|
from re import compile as re_compile
|
||||||
from time import time
|
from time import time
|
||||||
from typing import AsyncIterator, ByteString, Callable, Iterable, List, Type
|
from typing import (
|
||||||
|
AsyncIterator,
|
||||||
|
ByteString,
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Type,
|
||||||
|
)
|
||||||
|
|
||||||
from aiofiles import open as async_open
|
from aiofiles import open as async_open
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
@@ -8,10 +16,13 @@ from aiohttp import ClientSession
|
|||||||
from msgspec import ValidationError
|
from msgspec import ValidationError
|
||||||
from msgspec.json import Decoder
|
from msgspec.json import Decoder
|
||||||
from rich import print
|
from rich import print
|
||||||
|
from shapefile import Reader as ShapeFileReader, ShapeRecord
|
||||||
|
|
||||||
from ..db import Database
|
from ..db import Database
|
||||||
from ..models import Line, LinePicto, Stop, StopArea
|
from ..models import ConnectionArea, Line, LinePicto, Stop, StopArea, StopShape
|
||||||
from .idfm_types import (
|
from .idfm_types import (
|
||||||
|
ConnectionArea as IdfmConnectionArea,
|
||||||
|
Destinations as IdfmDestinations,
|
||||||
IdfmLineState,
|
IdfmLineState,
|
||||||
IdfmResponse,
|
IdfmResponse,
|
||||||
Line as IdfmLine,
|
Line as IdfmLine,
|
||||||
@@ -55,6 +66,7 @@ class IdfmInterface:
|
|||||||
|
|
||||||
self._json_stops_decoder = Decoder(type=List[IdfmStop])
|
self._json_stops_decoder = Decoder(type=List[IdfmStop])
|
||||||
self._json_stop_areas_decoder = Decoder(type=List[IdfmStopArea])
|
self._json_stop_areas_decoder = Decoder(type=List[IdfmStopArea])
|
||||||
|
self._json_connection_areas_decoder = Decoder(type=List[IdfmConnectionArea])
|
||||||
self._json_lines_decoder = Decoder(type=List[IdfmLine])
|
self._json_lines_decoder = Decoder(type=List[IdfmLine])
|
||||||
self._json_stops_lines_assos_decoder = Decoder(type=List[IdfmStopLineAsso])
|
self._json_stops_lines_assos_decoder = Decoder(type=List[IdfmStopLineAsso])
|
||||||
self._json_ratp_pictos_decoder = Decoder(type=List[RatpPicto])
|
self._json_ratp_pictos_decoder = Decoder(type=List[RatpPicto])
|
||||||
@@ -66,7 +78,24 @@ class IdfmInterface:
|
|||||||
|
|
||||||
async def startup(self) -> None:
|
async def startup(self) -> None:
|
||||||
BATCH_SIZE = 10000
|
BATCH_SIZE = 10000
|
||||||
STEPS: tuple[tuple[Type[Stop] | Type[StopArea], Callable, Callable], ...] = (
|
STEPS: tuple[
|
||||||
|
tuple[
|
||||||
|
Type[ConnectionArea] | Type[Stop] | Type[StopArea] | Type[StopShape],
|
||||||
|
Callable,
|
||||||
|
Callable,
|
||||||
|
],
|
||||||
|
...,
|
||||||
|
] = (
|
||||||
|
(
|
||||||
|
StopShape,
|
||||||
|
self._request_stop_shapes,
|
||||||
|
IdfmInterface._format_idfm_stop_shapes,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ConnectionArea,
|
||||||
|
self._request_idfm_connection_areas,
|
||||||
|
IdfmInterface._format_idfm_connection_areas,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
StopArea,
|
StopArea,
|
||||||
self._request_idfm_stop_areas,
|
self._request_idfm_stop_areas,
|
||||||
@@ -104,7 +133,7 @@ class IdfmInterface:
|
|||||||
print(f"Link Stops to Lines: {time() - begin_ts}s")
|
print(f"Link Stops to Lines: {time() - begin_ts}s")
|
||||||
|
|
||||||
begin_ts = time()
|
begin_ts = time()
|
||||||
await self._load_stop_areas_stops_assos()
|
await self._load_stop_assos()
|
||||||
print(f"Link Stops to StopAreas: {time() - begin_ts}s")
|
print(f"Link Stops to StopAreas: {time() - begin_ts}s")
|
||||||
|
|
||||||
async def _load_lines(self, batch_size: int = 5000) -> None:
|
async def _load_lines(self, batch_size: int = 5000) -> None:
|
||||||
@@ -167,25 +196,51 @@ class IdfmInterface:
|
|||||||
|
|
||||||
print(f"{total_found_nb} line <-> stop ({total_assos_nb = } found)")
|
print(f"{total_found_nb} line <-> stop ({total_assos_nb = } found)")
|
||||||
|
|
||||||
async def _load_stop_areas_stops_assos(self, batch_size: int = 5000) -> None:
|
async def _load_stop_assos(self, batch_size: int = 5000) -> None:
|
||||||
total_assos_nb = total_found_nb = 0
|
total_assos_nb = area_stop_assos_nb = conn_stop_assos_nb = 0
|
||||||
assos = []
|
area_stop_assos = []
|
||||||
|
connection_stop_assos = []
|
||||||
|
|
||||||
async for asso in self._request_idfm_stop_area_stop_associations():
|
async for asso in self._request_idfm_stop_area_stop_associations():
|
||||||
fields = asso.fields
|
fields = asso.fields
|
||||||
|
|
||||||
assos.append((int(fields.zdaid), int(fields.arrid)))
|
stop_id = int(fields.arrid)
|
||||||
if len(assos) == batch_size:
|
|
||||||
|
area_stop_assos.append((int(fields.zdaid), stop_id))
|
||||||
|
connection_stop_assos.append((int(fields.zdcid), stop_id))
|
||||||
|
|
||||||
|
if len(area_stop_assos) == batch_size:
|
||||||
total_assos_nb += batch_size
|
total_assos_nb += batch_size
|
||||||
if (found_nb := await StopArea.add_stops(assos)) is not None:
|
|
||||||
total_found_nb += found_nb
|
|
||||||
assos.clear()
|
|
||||||
|
|
||||||
if assos:
|
if (found_nb := await StopArea.add_stops(area_stop_assos)) is not None:
|
||||||
total_assos_nb += len(assos)
|
area_stop_assos_nb += found_nb
|
||||||
if (found_nb := await StopArea.add_stops(assos)) is not None:
|
area_stop_assos.clear()
|
||||||
total_found_nb += found_nb
|
|
||||||
|
|
||||||
print(f"{total_found_nb} stop area <-> stop ({total_assos_nb = } found)")
|
if (
|
||||||
|
found_nb := await ConnectionArea.add_stops(connection_stop_assos)
|
||||||
|
) is not None:
|
||||||
|
conn_stop_assos_nb += found_nb
|
||||||
|
connection_stop_assos.clear()
|
||||||
|
|
||||||
|
if area_stop_assos:
|
||||||
|
total_assos_nb += len(area_stop_assos)
|
||||||
|
if (found_nb := await StopArea.add_stops(area_stop_assos)) is not None:
|
||||||
|
area_stop_assos_nb += found_nb
|
||||||
|
if (
|
||||||
|
found_nb := await ConnectionArea.add_stops(connection_stop_assos)
|
||||||
|
) is not None:
|
||||||
|
conn_stop_assos_nb += found_nb
|
||||||
|
|
||||||
|
print(f"{area_stop_assos_nb} stop area <-> stop ({total_assos_nb = } found)")
|
||||||
|
print(f"{conn_stop_assos_nb} stop area <-> stop ({total_assos_nb = } found)")
|
||||||
|
|
||||||
|
# TODO: This method is synchronous due to the shapefile library.
|
||||||
|
# It's not a blocking issue but it could be nice to find an alternative.
|
||||||
|
async def _request_stop_shapes(self) -> AsyncIterator[ShapeRecord]:
|
||||||
|
# TODO: Use HTTP
|
||||||
|
with ShapeFileReader("./tests/datasets/REF_LDA.zip") as reader:
|
||||||
|
for record in reader.shapeRecords():
|
||||||
|
yield record
|
||||||
|
|
||||||
async def _request_idfm_stops(self) -> AsyncIterator[IdfmStop]:
|
async def _request_idfm_stops(self) -> AsyncIterator[IdfmStop]:
|
||||||
# headers = {"Accept": "application/json", "apikey": self._api_key}
|
# headers = {"Accept": "application/json", "apikey": self._api_key}
|
||||||
@@ -206,6 +261,13 @@ class IdfmInterface:
|
|||||||
for element in self._json_stop_areas_decoder.decode(await raw.read()):
|
for element in self._json_stop_areas_decoder.decode(await raw.read()):
|
||||||
yield element
|
yield element
|
||||||
|
|
||||||
|
async def _request_idfm_connection_areas(self) -> AsyncIterator[IdfmConnectionArea]:
|
||||||
|
async with async_open(
|
||||||
|
"./tests/datasets/zones-de-correspondance.json", "rb"
|
||||||
|
) as raw:
|
||||||
|
for element in self._json_connection_areas_decoder.decode(await raw.read()):
|
||||||
|
yield element
|
||||||
|
|
||||||
async def _request_idfm_lines(self) -> AsyncIterator[IdfmLine]:
|
async def _request_idfm_lines(self) -> AsyncIterator[IdfmLine]:
|
||||||
# TODO: Use HTTP
|
# TODO: Use HTTP
|
||||||
async with async_open("./tests/datasets/lines_dataset.json", "rb") as raw:
|
async with async_open("./tests/datasets/lines_dataset.json", "rb") as raw:
|
||||||
@@ -378,6 +440,34 @@ class IdfmInterface:
|
|||||||
changed_ts=int(fields.zdachanged.timestamp()),
|
changed_ts=int(fields.zdachanged.timestamp()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_idfm_connection_areas(
|
||||||
|
*connection_areas: IdfmConnectionArea,
|
||||||
|
) -> Iterable[ConnectionArea]:
|
||||||
|
for connection_area in connection_areas:
|
||||||
|
yield ConnectionArea(
|
||||||
|
id=int(connection_area.zdcid),
|
||||||
|
name=connection_area.zdcname,
|
||||||
|
town_name=connection_area.zdctown,
|
||||||
|
postal_region=connection_area.zdcpostalregion,
|
||||||
|
xepsg2154=connection_area.zdcxepsg2154,
|
||||||
|
yepsg2154=connection_area.zdcyepsg2154,
|
||||||
|
transport_mode=StopAreaType(connection_area.zdctype.value),
|
||||||
|
version=connection_area.zdcversion,
|
||||||
|
created_ts=int(connection_area.zdccreated.timestamp()),
|
||||||
|
changed_ts=int(connection_area.zdcchanged.timestamp()),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_idfm_stop_shapes(*shape_records: ShapeRecord) -> Iterable[StopShape]:
|
||||||
|
for shape_record in shape_records:
|
||||||
|
yield StopShape(
|
||||||
|
id=shape_record.record[1],
|
||||||
|
type=shape_record.shape.shapeType,
|
||||||
|
bounding_box=list(shape_record.shape.bbox),
|
||||||
|
points=shape_record.shape.points,
|
||||||
|
)
|
||||||
|
|
||||||
async def render_line_picto(self, line: Line) -> tuple[None | str, None | str]:
|
async def render_line_picto(self, line: Line) -> tuple[None | str, None | str]:
|
||||||
begin_ts = time()
|
begin_ts = time()
|
||||||
line_picto_path = line_picto_format = None
|
line_picto_path = line_picto_format = None
|
||||||
@@ -432,20 +522,40 @@ class IdfmInterface:
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def get_destinations(self, stop_point_id: str) -> Iterable[str]:
|
async def get_destinations(self, stop_id: int) -> IdfmDestinations | None:
|
||||||
# TODO: Store in database the destination for the given stop and line id.
|
|
||||||
begin_ts = time()
|
begin_ts = time()
|
||||||
destinations: dict[str, str] = {}
|
|
||||||
if (res := await self.get_next_passages(stop_point_id)) is not None:
|
destinations: IdfmDestinations = defaultdict(set)
|
||||||
|
|
||||||
|
if (stop := await Stop.get_by_id(stop_id)) is not None:
|
||||||
|
expected_stop_ids = {stop.id}
|
||||||
|
|
||||||
|
elif (stop_area := await StopArea.get_by_id(stop_id)) is not None:
|
||||||
|
expected_stop_ids = {stop.id for stop in stop_area.stops}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (res := await self.get_next_passages(stop_id)) is not None:
|
||||||
|
|
||||||
for delivery in res.Siri.ServiceDelivery.StopMonitoringDelivery:
|
for delivery in res.Siri.ServiceDelivery.StopMonitoringDelivery:
|
||||||
if delivery.Status == IdfmState.true:
|
if delivery.Status == IdfmState.true:
|
||||||
for stop_visit in delivery.MonitoredStopVisit:
|
for stop_visit in delivery.MonitoredStopVisit:
|
||||||
|
|
||||||
|
monitoring_ref = stop_visit.MonitoringRef.value
|
||||||
|
try:
|
||||||
|
monitored_stop_id = int(monitoring_ref.split(":")[-2])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
print(f"Unable to get stop id from {monitoring_ref}")
|
||||||
|
continue
|
||||||
|
|
||||||
journey = stop_visit.MonitoredVehicleJourney
|
journey = stop_visit.MonitoredVehicleJourney
|
||||||
if (destination_name := journey.DestinationName) and (
|
if (
|
||||||
line_ref := journey.LineRef
|
dst_names := journey.DestinationName
|
||||||
):
|
) and monitored_stop_id in expected_stop_ids:
|
||||||
line_id = line_ref.value.replace("STIF:Line::", "")[:-1]
|
|
||||||
print(f"{line_id = }")
|
line_id = journey.LineRef.value.split(":")[-2]
|
||||||
destinations[line_id] = destination_name[0].value
|
destinations[line_id].add(dst_names[0].value)
|
||||||
|
|
||||||
print(f"get_next_passages: {time() - begin_ts}")
|
print(f"get_next_passages: {time() - begin_ts}")
|
||||||
return destinations
|
return destinations
|
||||||
|
@@ -116,6 +116,19 @@ class StopArea(Struct):
|
|||||||
record_timestamp: datetime
|
record_timestamp: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionArea(Struct):
|
||||||
|
zdcid: str
|
||||||
|
zdcversion: str
|
||||||
|
zdccreated: datetime
|
||||||
|
zdcchanged: datetime
|
||||||
|
zdcname: str
|
||||||
|
zdcxepsg2154: int
|
||||||
|
zdcyepsg2154: int
|
||||||
|
zdctown: str
|
||||||
|
zdcpostalregion: str
|
||||||
|
zdctype: StopAreaType
|
||||||
|
|
||||||
|
|
||||||
class StopAreaStopAssociationFields(Struct, kw_only=True):
|
class StopAreaStopAssociationFields(Struct, kw_only=True):
|
||||||
arrid: str # TODO: use int ?
|
arrid: str # TODO: use int ?
|
||||||
artid: str | None = None
|
artid: str | None = None
|
||||||
@@ -184,6 +197,8 @@ class Line(Struct):
|
|||||||
|
|
||||||
Lines = dict[str, Line]
|
Lines = dict[str, Line]
|
||||||
|
|
||||||
|
Destinations = dict[str, set[str]]
|
||||||
|
|
||||||
|
|
||||||
# TODO: Set structs frozen
|
# TODO: Set structs frozen
|
||||||
class StopLineAssoFields(Struct):
|
class StopLineAssoFields(Struct):
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
from .line import Line, LinePicto
|
from .line import Line, LinePicto
|
||||||
from .stop import Stop, StopArea
|
from .stop import ConnectionArea, Stop, StopArea, StopShape
|
||||||
from .user import UserLastStopSearchResults
|
from .user import UserLastStopSearchResults
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Line", "LinePicto", "Stop", "StopArea", "UserLastStopSearchResults"]
|
__all__ = [
|
||||||
|
"ConnectionArea",
|
||||||
|
"Line",
|
||||||
|
"LinePicto",
|
||||||
|
"Stop",
|
||||||
|
"StopArea",
|
||||||
|
"StopShape",
|
||||||
|
"UserLastStopSearchResults",
|
||||||
|
]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Iterable, Self, Sequence, TYPE_CHECKING
|
from typing import Iterable, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
BigInteger,
|
BigInteger,
|
||||||
@@ -8,6 +8,8 @@ from sqlalchemy import (
|
|||||||
Enum,
|
Enum,
|
||||||
Float,
|
Float,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
JSON,
|
||||||
select,
|
select,
|
||||||
String,
|
String,
|
||||||
Table,
|
Table,
|
||||||
@@ -48,19 +50,26 @@ class _Stop(Base):
|
|||||||
postal_region = mapped_column(String, nullable=False)
|
postal_region = mapped_column(String, nullable=False)
|
||||||
xepsg2154 = mapped_column(BigInteger, nullable=False)
|
xepsg2154 = mapped_column(BigInteger, nullable=False)
|
||||||
yepsg2154 = mapped_column(BigInteger, nullable=False)
|
yepsg2154 = mapped_column(BigInteger, nullable=False)
|
||||||
|
|
||||||
version = mapped_column(String, nullable=False)
|
version = mapped_column(String, nullable=False)
|
||||||
created_ts = mapped_column(BigInteger)
|
created_ts = mapped_column(BigInteger)
|
||||||
changed_ts = mapped_column(BigInteger, nullable=False)
|
changed_ts = mapped_column(BigInteger, nullable=False)
|
||||||
|
|
||||||
lines: Mapped[list[Line]] = relationship(
|
lines: Mapped[list[Line]] = relationship(
|
||||||
"Line",
|
"Line",
|
||||||
secondary="line_stop_association_table",
|
secondary="line_stop_association_table",
|
||||||
back_populates="stops",
|
back_populates="stops",
|
||||||
# lazy="joined",
|
|
||||||
lazy="selectin",
|
lazy="selectin",
|
||||||
)
|
)
|
||||||
areas: Mapped[list["StopArea"]] = relationship(
|
areas: Mapped[list["StopArea"]] = relationship(
|
||||||
"StopArea", secondary=stop_area_stop_association_table, back_populates="stops"
|
"StopArea", secondary=stop_area_stop_association_table, back_populates="stops"
|
||||||
)
|
)
|
||||||
|
connection_area_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("connection_areas.id"), nullable=True
|
||||||
|
)
|
||||||
|
connection_area: Mapped["ConnectionArea"] = relationship(
|
||||||
|
back_populates="stops", lazy="selectin"
|
||||||
|
)
|
||||||
|
|
||||||
__tablename__ = "_stops"
|
__tablename__ = "_stops"
|
||||||
__mapper_args__ = {"polymorphic_identity": "_stops", "polymorphic_on": kind}
|
__mapper_args__ = {"polymorphic_identity": "_stops", "polymorphic_on": kind}
|
||||||
@@ -108,6 +117,7 @@ class Stop(_Stop):
|
|||||||
accessibility = mapped_column(Enum(IdfmState), nullable=False)
|
accessibility = mapped_column(Enum(IdfmState), nullable=False)
|
||||||
visual_signs_available = mapped_column(Enum(IdfmState), nullable=False)
|
visual_signs_available = mapped_column(Enum(IdfmState), nullable=False)
|
||||||
audible_signs_available = mapped_column(Enum(IdfmState), nullable=False)
|
audible_signs_available = mapped_column(Enum(IdfmState), nullable=False)
|
||||||
|
|
||||||
record_id = mapped_column(String, nullable=False)
|
record_id = mapped_column(String, nullable=False)
|
||||||
record_ts = mapped_column(BigInteger, nullable=False)
|
record_ts = mapped_column(BigInteger, nullable=False)
|
||||||
|
|
||||||
@@ -120,12 +130,12 @@ class StopArea(_Stop):
|
|||||||
id = mapped_column(BigInteger, ForeignKey("_stops.id"), primary_key=True)
|
id = mapped_column(BigInteger, ForeignKey("_stops.id"), primary_key=True)
|
||||||
|
|
||||||
type = mapped_column(Enum(StopAreaType), nullable=False)
|
type = mapped_column(Enum(StopAreaType), nullable=False)
|
||||||
stops: Mapped[list["_Stop"]] = relationship(
|
|
||||||
"_Stop",
|
stops: Mapped[list["Stop"]] = relationship(
|
||||||
|
"Stop",
|
||||||
secondary=stop_area_stop_association_table,
|
secondary=stop_area_stop_association_table,
|
||||||
back_populates="areas",
|
back_populates="areas",
|
||||||
lazy="selectin",
|
lazy="selectin",
|
||||||
# lazy="joined",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__tablename__ = "stop_areas"
|
__tablename__ = "stop_areas"
|
||||||
@@ -147,17 +157,17 @@ class StopArea(_Stop):
|
|||||||
stop_area_ids.add(stop_area_id)
|
stop_area_ids.add(stop_area_id)
|
||||||
stop_ids.add(stop_id)
|
stop_ids.add(stop_id)
|
||||||
|
|
||||||
stop_areas_res = await session.execute(
|
stop_areas_res = await session.scalars(
|
||||||
select(StopArea)
|
select(StopArea)
|
||||||
.where(StopArea.id.in_(stop_area_ids))
|
.where(StopArea.id.in_(stop_area_ids))
|
||||||
.options(selectinload(StopArea.stops))
|
.options(selectinload(StopArea.stops))
|
||||||
)
|
)
|
||||||
stop_areas: dict[int, StopArea] = {
|
stop_areas: dict[int, StopArea] = {
|
||||||
stop_area.id: stop_area for stop_area in stop_areas_res.scalars()
|
stop_area.id: stop_area for stop_area in stop_areas_res.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_res = await session.execute(select(_Stop).where(_Stop.id.in_(stop_ids)))
|
stop_res = await session.execute(select(Stop).where(Stop.id.in_(stop_ids)))
|
||||||
stops: dict[int, _Stop] = {stop.id: stop for stop in stop_res.scalars()}
|
stops: dict[int, Stop] = {stop.id: stop for stop in stop_res.scalars()}
|
||||||
|
|
||||||
found = 0
|
found = 0
|
||||||
for stop_area_id, stop_id in stop_area_to_stop_ids:
|
for stop_area_id, stop_id in stop_area_to_stop_ids:
|
||||||
@@ -173,3 +183,78 @@ class StopArea(_Stop):
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
class StopShape(Base):
|
||||||
|
|
||||||
|
db = db
|
||||||
|
|
||||||
|
id = mapped_column(BigInteger, primary_key=True) # Same id than ConnectionArea
|
||||||
|
type = mapped_column(Integer, nullable=False)
|
||||||
|
bounding_box = mapped_column(JSON)
|
||||||
|
points = mapped_column(JSON)
|
||||||
|
|
||||||
|
__tablename__ = "stop_shapes"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionArea(Base):
|
||||||
|
|
||||||
|
db = db
|
||||||
|
|
||||||
|
id = mapped_column(BigInteger, primary_key=True)
|
||||||
|
|
||||||
|
name = mapped_column(String, nullable=False)
|
||||||
|
town_name = mapped_column(String, nullable=False)
|
||||||
|
postal_region = mapped_column(String, nullable=False)
|
||||||
|
xepsg2154 = mapped_column(BigInteger, nullable=False)
|
||||||
|
yepsg2154 = mapped_column(BigInteger, nullable=False)
|
||||||
|
transport_mode = mapped_column(Enum(StopAreaType), nullable=False)
|
||||||
|
|
||||||
|
version = mapped_column(String, nullable=False)
|
||||||
|
created_ts = mapped_column(BigInteger)
|
||||||
|
changed_ts = mapped_column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
stops: Mapped[list["_Stop"]] = relationship(back_populates="connection_area")
|
||||||
|
|
||||||
|
__tablename__ = "connection_areas"
|
||||||
|
|
||||||
|
# TODO: Merge with StopArea.add_stops
|
||||||
|
@classmethod
|
||||||
|
async def add_stops(
|
||||||
|
cls, conn_area_to_stop_ids: Iterable[tuple[int, int]]
|
||||||
|
) -> int | None:
|
||||||
|
session = cls.db.session
|
||||||
|
if session is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
conn_area_ids, stop_ids = set(), set()
|
||||||
|
for conn_area_id, stop_id in conn_area_to_stop_ids:
|
||||||
|
conn_area_ids.add(conn_area_id)
|
||||||
|
stop_ids.add(stop_id)
|
||||||
|
|
||||||
|
conn_area_res = await session.execute(
|
||||||
|
select(ConnectionArea)
|
||||||
|
.where(ConnectionArea.id.in_(conn_area_ids))
|
||||||
|
.options(selectinload(ConnectionArea.stops))
|
||||||
|
)
|
||||||
|
conn_areas: dict[int, ConnectionArea] = {
|
||||||
|
conn.id: conn for conn in conn_area_res.scalars()
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_res = await session.execute(select(_Stop).where(_Stop.id.in_(stop_ids)))
|
||||||
|
stops: dict[int, _Stop] = {stop.id: stop for stop in stop_res.scalars()}
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for conn_area_id, stop_id in conn_area_to_stop_ids:
|
||||||
|
if (conn_area := conn_areas.get(conn_area_id)) is not None:
|
||||||
|
if (stop := stops.get(stop_id)) is not None:
|
||||||
|
conn_area.stops.append(stop)
|
||||||
|
found += 1
|
||||||
|
else:
|
||||||
|
print(f"No stop found for {stop_id} id")
|
||||||
|
else:
|
||||||
|
print(f"No connection area found for {conn_area_id}")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return found
|
||||||
|
@@ -1,5 +1,14 @@
|
|||||||
from .line import Line, TransportMode
|
from .line import Line, TransportMode
|
||||||
from .next_passage import NextPassage, NextPassages
|
from .next_passage import NextPassage, NextPassages
|
||||||
from .stop import Stop, StopArea
|
from .stop import Stop, StopArea, StopShape
|
||||||
|
|
||||||
__all__ = ["Line", "NextPassage", "NextPassages", "Stop", "StopArea", "TransportMode"]
|
|
||||||
|
__all__ = [
|
||||||
|
"Line",
|
||||||
|
"NextPassage",
|
||||||
|
"NextPassages",
|
||||||
|
"Stop",
|
||||||
|
"StopArea",
|
||||||
|
"StopShape",
|
||||||
|
"TransportMode",
|
||||||
|
]
|
||||||
|
@@ -23,3 +23,10 @@ class StopArea(BaseModel):
|
|||||||
type: StopAreaType
|
type: StopAreaType
|
||||||
lines: list[str] # SNCF lines are linked to stop areas and not stops.
|
lines: list[str] # SNCF lines are linked to stop areas and not stops.
|
||||||
stops: list[Stop]
|
stops: list[Stop]
|
||||||
|
|
||||||
|
|
||||||
|
class StopShape(BaseModel):
|
||||||
|
id: int
|
||||||
|
type: int
|
||||||
|
bbox: list[float]
|
||||||
|
points: list[tuple[float, float]]
|
||||||
|
@@ -9,8 +9,8 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
from backend.db import db
|
from backend.db import db
|
||||||
from backend.idfm_interface import IdfmInterface
|
from backend.idfm_interface import Destinations as IdfmDestinations, IdfmInterface
|
||||||
from backend.models import Line, Stop, StopArea
|
from backend.models import Line, Stop, StopArea, StopShape
|
||||||
from backend.schemas import (
|
from backend.schemas import (
|
||||||
Line as LineSchema,
|
Line as LineSchema,
|
||||||
TransportMode,
|
TransportMode,
|
||||||
@@ -18,6 +18,7 @@ from backend.schemas import (
|
|||||||
NextPassages as NextPassagesSchema,
|
NextPassages as NextPassagesSchema,
|
||||||
Stop as StopSchema,
|
Stop as StopSchema,
|
||||||
StopArea as StopAreaSchema,
|
StopArea as StopAreaSchema,
|
||||||
|
StopShape as StopShapeSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
API_KEY = environ.get("API_KEY")
|
API_KEY = environ.get("API_KEY")
|
||||||
@@ -214,3 +215,43 @@ async def get_next_passages(stop_id: str) -> NextPassagesSchema | None:
|
|||||||
ts=service_delivery.ResponseTimestamp.timestamp(),
|
ts=service_delivery.ResponseTimestamp.timestamp(),
|
||||||
passages=by_line_by_dst_passages,
|
passages=by_line_by_dst_passages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/stop/{stop_id}/destinations")
|
||||||
|
async def get_stop_destinations(
|
||||||
|
stop_id: int,
|
||||||
|
) -> IdfmDestinations | None:
|
||||||
|
destinations = await idfm_interface.get_destinations(stop_id)
|
||||||
|
|
||||||
|
return destinations
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Rename endpoint -> /stop/{stop_id}/shape
|
||||||
|
@app.get("/stop_shape/{stop_id}")
|
||||||
|
async def get_stop_shape(stop_id: int) -> StopShapeSchema | None:
|
||||||
|
connection_area = None
|
||||||
|
|
||||||
|
if (stop := await Stop.get_by_id(stop_id)) is not None:
|
||||||
|
connection_area = stop.connection_area
|
||||||
|
|
||||||
|
elif (stop_area := await StopArea.get_by_id(stop_id)) is not None:
|
||||||
|
connection_areas = {stop.connection_area for stop in stop_area.stops}
|
||||||
|
connection_areas_len = len(connection_areas)
|
||||||
|
if connection_areas_len == 1:
|
||||||
|
connection_area = connection_areas.pop()
|
||||||
|
|
||||||
|
else:
|
||||||
|
prefix = "More than one" if connection_areas_len else "No"
|
||||||
|
msg = f"{prefix} connection area has been found for stop area #{stop_id}"
|
||||||
|
raise HTTPException(status_code=500, detail=msg)
|
||||||
|
|
||||||
|
if (
|
||||||
|
connection_area is not None
|
||||||
|
and (shape := await StopShape.get_by_id(connection_area.id)) is not None
|
||||||
|
):
|
||||||
|
return StopShapeSchema(
|
||||||
|
id=shape.id, type=shape.type, bbox=shape.bounding_box, points=shape.points
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = f"No shape found for stop {stop_id}"
|
||||||
|
raise HTTPException(status_code=404, detail=msg)
|
||||||
|
@@ -16,6 +16,7 @@ fastapi = "^0.88.0"
|
|||||||
uvicorn = "^0.20.0"
|
uvicorn = "^0.20.0"
|
||||||
asyncpg = "^0.27.0"
|
asyncpg = "^0.27.0"
|
||||||
msgspec = "^0.12.0"
|
msgspec = "^0.12.0"
|
||||||
|
pyshp = "^2.3.1"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
@@ -24,6 +24,7 @@
|
|||||||
"@hope-ui/solid": "^0.6.7",
|
"@hope-ui/solid": "^0.6.7",
|
||||||
"@motionone/solid": "^10.15.5",
|
"@motionone/solid": "^10.15.5",
|
||||||
"@solid-primitives/date": "^2.0.5",
|
"@solid-primitives/date": "^2.0.5",
|
||||||
|
"@solid-primitives/scroll": "^2.0.10",
|
||||||
"@stitches/core": "^1.2.8",
|
"@stitches/core": "^1.2.8",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"leaflet": "^1.9.3",
|
"leaflet": "^1.9.3",
|
||||||
|
@@ -1,205 +1,208 @@
|
|||||||
@use "_utils.scss";
|
@use "_utils.scss";
|
||||||
|
|
||||||
.passagesContainer {
|
.body {
|
||||||
height: 100%;
|
.passagesContainer {
|
||||||
width: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
.displayed {
|
/* TODO: Remove the bottom border only if there are 5 displayed lines. */
|
||||||
display: block;
|
.line:last-child {
|
||||||
}
|
border-bottom: 0;
|
||||||
|
/* To make up for the bottom border deletion */
|
||||||
/* TODO: Remove the bottom border only if there are 5 displayed lines. */
|
padding-bottom: calc(2px);
|
||||||
.passagesContainer .line:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
/* To make up for the bottom border deletion */
|
|
||||||
padding-bottom: calc(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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 {
|
|
||||||
@extend %transportMode;
|
|
||||||
|
|
||||||
height: calc(100/176*100%);
|
|
||||||
margin: 0 calc(15/1920*100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.busLinePicto {
|
|
||||||
@extend %busLinePicto;
|
|
||||||
|
|
||||||
height: calc(70/176*100%);
|
|
||||||
margin-right: calc(23/1920*100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metroLinePicto, .tramLinePicto, .trainLinePicto {
|
|
||||||
aspect-ratio : 1 / 1;
|
|
||||||
height: calc(100/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%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.unavailableSecondPassage svg {
|
|
||||||
font-family: IDFVoyageur-regular;
|
|
||||||
}
|
|
||||||
|
|
||||||
%withPlatformPassage {
|
|
||||||
height: calc(120/176*100%);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.withPlatformFirstPassage {
|
|
||||||
@extend %withPlatformPassage;
|
|
||||||
|
|
||||||
aspect-ratio: 250/120;
|
|
||||||
|
|
||||||
padding-right: calc(30/1920*100%);
|
|
||||||
/* TODO: compute the border weight according to the parent width */
|
|
||||||
border-right: solid calc(5px);
|
|
||||||
|
|
||||||
.passage {
|
|
||||||
aspect-ratio: 215/50;
|
|
||||||
height: calc(1/2*100%);
|
|
||||||
|
|
||||||
font-family: IDFVoyageur-bold;
|
|
||||||
margin-top: calc(5/176*100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.platform {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: calc(5/176*100%);
|
|
||||||
|
|
||||||
rect {
|
|
||||||
background-color: var(--idfm-black);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
text {
|
/* Idfm: 1880x176px (margin: 0px 20px) */
|
||||||
vertical-align: middle;
|
.line {
|
||||||
font-family: IDFVoyageur-bold;
|
width: calc(1880/1920*100%);
|
||||||
}
|
height: calc(100% / 5);
|
||||||
}
|
margin: 0 calc(20/1920*100%);
|
||||||
}
|
|
||||||
|
display: flex;
|
||||||
.withPlatformSecondPassage {
|
align-items: center;
|
||||||
@extend %withPlatformPassage;
|
|
||||||
|
/* TODO: compute the border weight according to the parent height */
|
||||||
aspect-ratio: 215/120;
|
/* TODO: Disable border-bottom for the last .line */
|
||||||
|
border-bottom: solid calc(2px);
|
||||||
align-items: end;
|
|
||||||
justify-content: center;
|
svg {
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
margin-right: calc(30/1920*100%);
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
.passage {
|
}
|
||||||
aspect-ratio: 215/45;
|
|
||||||
height: calc(45/120*100%);
|
}
|
||||||
/* 5px + (first passage font size - second passage font size/2) to align passages... */
|
|
||||||
/* There must exist a better way to align them. */
|
/* Idfm: 100x100px (margin: 0px 15px) */
|
||||||
margin-top: calc(7.5/176*100%);
|
.transportMode {
|
||||||
}
|
@extend %transportMode;
|
||||||
|
|
||||||
svg {
|
height: calc(100/176*100%);
|
||||||
font-family: IDFVoyageur-regular;
|
margin: 0 calc(15/1920*100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform {
|
.busLinePicto {
|
||||||
rect {
|
@extend %busLinePicto;
|
||||||
background-color: var(--idfm-black);
|
|
||||||
}
|
height: calc(70/176*100%);
|
||||||
|
margin-right: calc(23/1920*100%);
|
||||||
text {
|
}
|
||||||
vertical-align: middle;
|
|
||||||
|
.metroLinePicto, .tramLinePicto, .trainLinePicto {
|
||||||
|
aspect-ratio : 1 / 1;
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
margin-right: calc(23/1920*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination {
|
||||||
|
height: calc(60/176*100%);
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
font-family: IDFVoyageur-bold;
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
aspect-ratio: 215/50;
|
||||||
|
height: calc(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailableFirstPassage {
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
aspect-ratio: calc(230/100);
|
||||||
|
|
||||||
|
/* TODO: compute the border weight according to the parent width */
|
||||||
|
border-right: solid calc(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondPassage {
|
||||||
|
height: calc(45/176*100%);
|
||||||
|
aspect-ratio: calc(230/45);
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-family: IDFVoyageur-regular;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailableSecondPassage {
|
||||||
|
height: calc(100/176*100%);
|
||||||
|
aspect-ratio: calc(230/100);
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-family: IDFVoyageur-regular;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%withPlatformPassage {
|
||||||
|
height: calc(120/176*100%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withPlatformFirstPassage {
|
||||||
|
@extend %withPlatformPassage;
|
||||||
|
|
||||||
|
aspect-ratio: 250/120;
|
||||||
|
|
||||||
|
padding-right: calc(30/1920*100%);
|
||||||
|
/* TODO: compute the border weight according to the parent width */
|
||||||
|
border-right: solid calc(5px);
|
||||||
|
|
||||||
|
.passage {
|
||||||
|
aspect-ratio: 215/50;
|
||||||
|
height: calc(1/2*100%);
|
||||||
|
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
margin-top: calc(5/176*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: calc(5/176*100%);
|
||||||
|
|
||||||
|
rect {
|
||||||
|
background-color: var(--idfm-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
vertical-align: middle;
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withPlatformSecondPassage {
|
||||||
|
@extend %withPlatformPassage;
|
||||||
|
|
||||||
|
aspect-ratio: 215/120;
|
||||||
|
|
||||||
|
align-items: end;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-right: calc(30/1920*100%);
|
||||||
|
|
||||||
|
.passage {
|
||||||
|
aspect-ratio: 215/45;
|
||||||
|
height: calc(45/120*100%);
|
||||||
|
/* 5px + (first passage font size - second passage font size/2) to align passages... */
|
||||||
|
/* There must exist a better way to align them. */
|
||||||
|
margin-top: calc(7.5/176*100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-family: IDFVoyageur-regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform {
|
||||||
|
rect {
|
||||||
|
background-color: var(--idfm-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
vertical-align: middle;
|
||||||
|
font-family: IDFVoyageur-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayed {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import { VoidComponent, createResource, onMount, ParentComponent, ParentProps, Show, useContext, For } from 'solid-js';
|
import { VoidComponent, createResource, onMount, ParentComponent, ParentProps, Show, useContext, For } from 'solid-js';
|
||||||
import { createDateNow, getTime } from '@solid-primitives/date';
|
import { createDateNow, getTime } from '@solid-primitives/date';
|
||||||
import { timeline } from '@motionone/dom';
|
|
||||||
import { AnimationOptions } from '@motionone/types';
|
import { AnimationOptions } from '@motionone/types';
|
||||||
import { Motion } from "@motionone/solid";
|
import { Motion } from "@motionone/solid";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { Line, TrafficStatus } from './types';
|
import { Line, TrafficStatus } from './types';
|
||||||
import { renderLineTransportMode, renderLinePicto } from './utils';
|
import { renderLineTransportMode, renderLinePicto, ScrollingText } from './utils';
|
||||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||||
|
|
||||||
import "./passagesPanel.scss";
|
import "./passagesPanel.scss";
|
||||||
@@ -123,24 +122,6 @@ const DestinationPassages: VoidComponent<{ line: Line, destination: string }> =
|
|||||||
// const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
|
// const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
|
||||||
const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) };
|
const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) };
|
||||||
|
|
||||||
let destinationViewboxRef: SVGSVGElement | undefined = undefined;
|
|
||||||
let destinationTextRef: SVGTextElement | undefined = undefined;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (destinationViewboxRef !== undefined && destinationTextRef !== undefined) {
|
|
||||||
const overlap = destinationTextRef.getComputedTextLength() - destinationViewboxRef.viewBox.baseVal.width;
|
|
||||||
if (overlap > 0) {
|
|
||||||
timeline(
|
|
||||||
[
|
|
||||||
[destinationTextRef, { x: [-overlap] }, { duration: 5 }],
|
|
||||||
[destinationTextRef, { x: [0] }, { duration: 2 }],
|
|
||||||
],
|
|
||||||
{ repeat: Infinity },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="line">
|
<div class="line">
|
||||||
<div class="transportMode">
|
<div class="transportMode">
|
||||||
@@ -148,14 +129,7 @@ const DestinationPassages: VoidComponent<{ line: Line, destination: string }> =
|
|||||||
</div>
|
</div>
|
||||||
{renderLinePicto(props.line)}
|
{renderLinePicto(props.line)}
|
||||||
<div class="destination">
|
<div class="destination">
|
||||||
<svg ref={destinationViewboxRef} viewBox="0 0 600 40">
|
<ScrollingText height={40} width={600} content={props.destination} />
|
||||||
<text ref={destinationTextRef} x="0" y="50%"
|
|
||||||
dominant-baseline="middle"
|
|
||||||
font-size="40"
|
|
||||||
style={{ fill: "#000000" }}>
|
|
||||||
{props.destination}
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="trafficStatus">
|
<div class="trafficStatus">
|
||||||
<svg viewBox="0 0 51 51">
|
<svg viewBox="0 0 51 51">
|
||||||
|
@@ -35,7 +35,6 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
.stopPanel {
|
.stopPanel {
|
||||||
// display: None;
|
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
|
|
||||||
.stop {
|
.stop {
|
||||||
@@ -53,16 +52,19 @@
|
|||||||
border-bottom: solid calc(2px);
|
border-bottom: solid calc(2px);
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
margin-left: calc(40/1920*100%);
|
||||||
width: 60%;
|
width: 60%;
|
||||||
|
aspect-ratio: 2.5;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
font-family: IDFVoyageur-bold;
|
font-family: IDFVoyageur-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lineRepr {
|
.lineRepr {
|
||||||
// height: 100%;
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
aspect-ratio: 2.5;
|
aspect-ratio: 2.5;
|
||||||
// margin-left: auto;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -75,11 +77,6 @@
|
|||||||
height: 50%;
|
height: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.break {
|
|
||||||
flex-basis: 100%;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linesRepresentationMatrix {
|
.linesRepresentationMatrix {
|
||||||
@extend %busLinePicto; // Use the larger picto aspect-ratio
|
@extend %busLinePicto; // Use the larger picto aspect-ratio
|
||||||
width: 75%;
|
width: 75%;
|
||||||
@@ -88,12 +85,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
// justify-content: space-around;
|
|
||||||
|
|
||||||
// .break {
|
|
||||||
// flex-basis: 100%;
|
|
||||||
// height: 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
%picto {
|
%picto {
|
||||||
margin-left: 1%;
|
margin-left: 1%;
|
||||||
@@ -123,22 +114,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.metroLinePicto {
|
.metroLinePicto {
|
||||||
@extendnd %metroLinePicto;
|
@extend %metroLinePicto;
|
||||||
@extend %singleLinePicto;
|
@extend %singleLinePicto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.busLinePicto {
|
.busLinePicto {
|
||||||
@extend %busLinePicto;
|
@extend %busLinePicto;
|
||||||
@extend %picto;
|
@extend %picto;
|
||||||
|
|
||||||
|
height: 40%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .stop:last-child {
|
|
||||||
// border-bottom: 0;
|
|
||||||
// /* to make up for the bottom border deletion */
|
|
||||||
// padding-bottom: calc(2px);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
.displayed {
|
.displayed {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -150,93 +138,4 @@
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// .lineTable {
|
|
||||||
// height: 100%;
|
|
||||||
// width: 50%;
|
|
||||||
|
|
||||||
// tr {
|
|
||||||
// height: 100%;
|
|
||||||
|
|
||||||
// display: flex;
|
|
||||||
// flex-flow: row;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// td {
|
|
||||||
// height: 100%;
|
|
||||||
|
|
||||||
// display: flex;
|
|
||||||
// flex-flow: row;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .stop {
|
|
||||||
// height: 100%;
|
|
||||||
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: row;
|
|
||||||
// align-items: center;
|
|
||||||
|
|
||||||
// .lineRepr {
|
|
||||||
// height: 100%;
|
|
||||||
// aspect-ratio: 5;
|
|
||||||
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: row;
|
|
||||||
// flex-wrap: wrap;
|
|
||||||
// align-items: center;
|
|
||||||
|
|
||||||
// .break {
|
|
||||||
// flex-basis: 100%;
|
|
||||||
// height: 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// .linesRepresentationMatrix {
|
|
||||||
// @extend %busLinePicto; // Use the larger picto aspect-ratio
|
|
||||||
// height: 100%;
|
|
||||||
// aspect-ratio: 3;
|
|
||||||
|
|
||||||
// display: flex;
|
|
||||||
// flex-flow: row;
|
|
||||||
// flex-wrap: wrap;
|
|
||||||
|
|
||||||
// .break {
|
|
||||||
// flex-basis: 100%;
|
|
||||||
// height: 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// %picto {
|
|
||||||
// margin-left: 1%;
|
|
||||||
// align-self: center;
|
|
||||||
// justify-self: center;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .transportMode {
|
|
||||||
// @extend %transportMode;
|
|
||||||
// @extend %picto;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .tramLinePicto {
|
|
||||||
// @extendnd %tramLinePicto;
|
|
||||||
// @extend %picto;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .trainLinePicto {
|
|
||||||
// @extend %trainLinePicto;
|
|
||||||
// @extend %picto;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .metroLinePicto {
|
|
||||||
// @extendnd %metroLinePicto;
|
|
||||||
// @extend %picto;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .busLinePicto {
|
|
||||||
// @extend %busLinePicto;
|
|
||||||
// @extend %picto;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
} from 'leaflet';
|
} from 'leaflet';
|
||||||
|
|
||||||
import { Stop } from './types';
|
import { Stop } from './types';
|
||||||
import { PositionedPanel, renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils';
|
import { PositionedPanel, renderLineTransportMode, renderLinePicto, ScrollingText, TransportModeWeights } from './utils';
|
||||||
|
|
||||||
import { AppContextContext, AppContextStore } from "./appContext";
|
import { AppContextContext, AppContextStore } from "./appContext";
|
||||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||||
@@ -191,10 +191,10 @@ const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
|||||||
type ByTransportModeReprs = {
|
type ByTransportModeReprs = {
|
||||||
mode: JSX.Element | undefined;
|
mode: JSX.Element | undefined;
|
||||||
lines: Record<string, JSX.Element | JSX.Element[] | undefined>;
|
lines: Record<string, JSX.Element | JSX.Element[] | undefined>;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
||||||
|
const fontSize: number = 10;
|
||||||
|
|
||||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||||
const appContextStore: AppContextStore | undefined = useContext(AppContextContext);
|
const appContextStore: AppContextStore | undefined = useContext(AppContextContext);
|
||||||
@@ -248,7 +248,9 @@ const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="stop" onClick={() => setDisplayedStops([props.stop])}>
|
<div class="stop" onClick={() => setDisplayedStops([props.stop])}>
|
||||||
<div class="name">{props.stop.name}</div>
|
<div class="name" >
|
||||||
|
<ScrollingText height={fontSize} width={100} content={props.stop.name} />
|
||||||
|
</div>
|
||||||
{lineReprs()}
|
{lineReprs()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { JSX } from 'solid-js';
|
import { JSX, onMount, VoidComponent } from 'solid-js';
|
||||||
|
import { timeline } from '@motionone/dom';
|
||||||
|
|
||||||
import { Line } from './types';
|
import { Line } from './types';
|
||||||
|
|
||||||
@@ -129,3 +130,37 @@ export type PositionedPanel = {
|
|||||||
// TODO: Should be PassagesPanelComponent ?
|
// TODO: Should be PassagesPanelComponent ?
|
||||||
panel: JSX.Element;
|
panel: JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const ScrollingText: VoidComponent<{ height: number, width: number, content: string }> = (props) => {
|
||||||
|
|
||||||
|
let viewBoxRef: SVGSVGElement | undefined = undefined;
|
||||||
|
let textRef: SVGTextElement | undefined = undefined;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (viewBoxRef !== undefined && textRef !== undefined) {
|
||||||
|
const overlap = textRef.getComputedTextLength() - viewBoxRef.viewBox.baseVal.width;
|
||||||
|
if (overlap > 0) {
|
||||||
|
timeline(
|
||||||
|
[
|
||||||
|
[textRef, { x: [-overlap] }, { duration: 5 }],
|
||||||
|
[textRef, { x: [0] }, { duration: 2 }],
|
||||||
|
],
|
||||||
|
{ repeat: Infinity },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg ref={viewBoxRef} viewBox={`0 0 ${props.width} ${props.height}`}>
|
||||||
|
<text
|
||||||
|
ref={textRef}
|
||||||
|
x="0%" y="55%"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
font-size={`${props.height}px`}>
|
||||||
|
{props.content}
|
||||||
|
</text>
|
||||||
|
</svg >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user