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 (
|
||||
Coordinate,
|
||||
Destinations,
|
||||
FramedVehicleJourney,
|
||||
IdfmLineState,
|
||||
IdfmOperator,
|
||||
@@ -35,6 +36,7 @@ from .idfm_types import (
|
||||
|
||||
__all__ = [
|
||||
"Coordinate",
|
||||
"Destinations",
|
||||
"FramedVehicleJourney",
|
||||
"IdfmInterface",
|
||||
"IdfmLineState",
|
||||
|
@@ -1,6 +1,14 @@
|
||||
from collections import defaultdict
|
||||
from re import compile as re_compile
|
||||
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 aiohttp import ClientSession
|
||||
@@ -8,10 +16,13 @@ from aiohttp import ClientSession
|
||||
from msgspec import ValidationError
|
||||
from msgspec.json import Decoder
|
||||
from rich import print
|
||||
from shapefile import Reader as ShapeFileReader, ShapeRecord
|
||||
|
||||
from ..db import Database
|
||||
from ..models import Line, LinePicto, Stop, StopArea
|
||||
from ..models import ConnectionArea, Line, LinePicto, Stop, StopArea, StopShape
|
||||
from .idfm_types import (
|
||||
ConnectionArea as IdfmConnectionArea,
|
||||
Destinations as IdfmDestinations,
|
||||
IdfmLineState,
|
||||
IdfmResponse,
|
||||
Line as IdfmLine,
|
||||
@@ -55,6 +66,7 @@ class IdfmInterface:
|
||||
|
||||
self._json_stops_decoder = Decoder(type=List[IdfmStop])
|
||||
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_stops_lines_assos_decoder = Decoder(type=List[IdfmStopLineAsso])
|
||||
self._json_ratp_pictos_decoder = Decoder(type=List[RatpPicto])
|
||||
@@ -66,7 +78,24 @@ class IdfmInterface:
|
||||
|
||||
async def startup(self) -> None:
|
||||
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,
|
||||
self._request_idfm_stop_areas,
|
||||
@@ -104,7 +133,7 @@ class IdfmInterface:
|
||||
print(f"Link Stops to Lines: {time() - begin_ts}s")
|
||||
|
||||
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")
|
||||
|
||||
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)")
|
||||
|
||||
async def _load_stop_areas_stops_assos(self, batch_size: int = 5000) -> None:
|
||||
total_assos_nb = total_found_nb = 0
|
||||
assos = []
|
||||
async def _load_stop_assos(self, batch_size: int = 5000) -> None:
|
||||
total_assos_nb = area_stop_assos_nb = conn_stop_assos_nb = 0
|
||||
area_stop_assos = []
|
||||
connection_stop_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:
|
||||
stop_id = int(fields.arrid)
|
||||
|
||||
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
|
||||
if (found_nb := await StopArea.add_stops(assos)) is not None:
|
||||
total_found_nb += found_nb
|
||||
assos.clear()
|
||||
|
||||
if assos:
|
||||
total_assos_nb += len(assos)
|
||||
if (found_nb := await StopArea.add_stops(assos)) is not None:
|
||||
total_found_nb += found_nb
|
||||
if (found_nb := await StopArea.add_stops(area_stop_assos)) is not None:
|
||||
area_stop_assos_nb += found_nb
|
||||
area_stop_assos.clear()
|
||||
|
||||
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]:
|
||||
# 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()):
|
||||
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]:
|
||||
# TODO: Use HTTP
|
||||
async with async_open("./tests/datasets/lines_dataset.json", "rb") as raw:
|
||||
@@ -378,6 +440,34 @@ class IdfmInterface:
|
||||
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]:
|
||||
begin_ts = time()
|
||||
line_picto_path = line_picto_format = None
|
||||
@@ -432,20 +522,40 @@ class IdfmInterface:
|
||||
|
||||
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.
|
||||
async def get_destinations(self, stop_id: int) -> IdfmDestinations | None:
|
||||
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:
|
||||
if delivery.Status == IdfmState.true:
|
||||
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
|
||||
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
|
||||
if (
|
||||
dst_names := journey.DestinationName
|
||||
) and monitored_stop_id in expected_stop_ids:
|
||||
|
||||
line_id = journey.LineRef.value.split(":")[-2]
|
||||
destinations[line_id].add(dst_names[0].value)
|
||||
|
||||
print(f"get_next_passages: {time() - begin_ts}")
|
||||
return destinations
|
||||
|
@@ -116,6 +116,19 @@ class StopArea(Struct):
|
||||
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):
|
||||
arrid: str # TODO: use int ?
|
||||
artid: str | None = None
|
||||
@@ -184,6 +197,8 @@ class Line(Struct):
|
||||
|
||||
Lines = dict[str, Line]
|
||||
|
||||
Destinations = dict[str, set[str]]
|
||||
|
||||
|
||||
# TODO: Set structs frozen
|
||||
class StopLineAssoFields(Struct):
|
||||
|
@@ -1,6 +1,14 @@
|
||||
from .line import Line, LinePicto
|
||||
from .stop import Stop, StopArea
|
||||
from .stop import ConnectionArea, Stop, StopArea, StopShape
|
||||
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 typing import Iterable, Self, Sequence, TYPE_CHECKING
|
||||
from typing import Iterable, Sequence, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
@@ -8,6 +8,8 @@ from sqlalchemy import (
|
||||
Enum,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
JSON,
|
||||
select,
|
||||
String,
|
||||
Table,
|
||||
@@ -48,19 +50,26 @@ class _Stop(Base):
|
||||
postal_region = mapped_column(String, nullable=False)
|
||||
xepsg2154 = mapped_column(BigInteger, nullable=False)
|
||||
yepsg2154 = mapped_column(BigInteger, nullable=False)
|
||||
|
||||
version = mapped_column(String, nullable=False)
|
||||
created_ts = mapped_column(BigInteger)
|
||||
changed_ts = mapped_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"
|
||||
)
|
||||
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"
|
||||
__mapper_args__ = {"polymorphic_identity": "_stops", "polymorphic_on": kind}
|
||||
@@ -108,6 +117,7 @@ class Stop(_Stop):
|
||||
accessibility = mapped_column(Enum(IdfmState), nullable=False)
|
||||
visual_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_ts = mapped_column(BigInteger, nullable=False)
|
||||
|
||||
@@ -120,12 +130,12 @@ class StopArea(_Stop):
|
||||
id = mapped_column(BigInteger, ForeignKey("_stops.id"), primary_key=True)
|
||||
|
||||
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,
|
||||
back_populates="areas",
|
||||
lazy="selectin",
|
||||
# lazy="joined",
|
||||
)
|
||||
|
||||
__tablename__ = "stop_areas"
|
||||
@@ -147,17 +157,17 @@ class StopArea(_Stop):
|
||||
stop_area_ids.add(stop_area_id)
|
||||
stop_ids.add(stop_id)
|
||||
|
||||
stop_areas_res = await session.execute(
|
||||
stop_areas_res = await session.scalars(
|
||||
select(StopArea)
|
||||
.where(StopArea.id.in_(stop_area_ids))
|
||||
.options(selectinload(StopArea.stops))
|
||||
)
|
||||
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)))
|
||||
stops: dict[int, _Stop] = {stop.id: stop for stop in stop_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 stop_area_id, stop_id in stop_area_to_stop_ids:
|
||||
@@ -173,3 +183,78 @@ class StopArea(_Stop):
|
||||
await session.commit()
|
||||
|
||||
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 .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
|
||||
lines: list[str] # SNCF lines are linked to stop areas and not stops.
|
||||
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 backend.db import db
|
||||
from backend.idfm_interface import IdfmInterface
|
||||
from backend.models import Line, Stop, StopArea
|
||||
from backend.idfm_interface import Destinations as IdfmDestinations, IdfmInterface
|
||||
from backend.models import Line, Stop, StopArea, StopShape
|
||||
from backend.schemas import (
|
||||
Line as LineSchema,
|
||||
TransportMode,
|
||||
@@ -18,6 +18,7 @@ from backend.schemas import (
|
||||
NextPassages as NextPassagesSchema,
|
||||
Stop as StopSchema,
|
||||
StopArea as StopAreaSchema,
|
||||
StopShape as StopShapeSchema,
|
||||
)
|
||||
|
||||
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(),
|
||||
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"
|
||||
asyncpg = "^0.27.0"
|
||||
msgspec = "^0.12.0"
|
||||
pyshp = "^2.3.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"@hope-ui/solid": "^0.6.7",
|
||||
"@motionone/solid": "^10.15.5",
|
||||
"@solid-primitives/date": "^2.0.5",
|
||||
"@solid-primitives/scroll": "^2.0.10",
|
||||
"@stitches/core": "^1.2.8",
|
||||
"date-fns": "^2.29.3",
|
||||
"leaflet": "^1.9.3",
|
||||
|
@@ -1,205 +1,208 @@
|
||||
@use "_utils.scss";
|
||||
|
||||
.passagesContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.body {
|
||||
.passagesContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
display: none;
|
||||
display: none;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
position: relative;
|
||||
|
||||
.displayed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* TODO: Remove the bottom border only if there are 5 displayed lines. */
|
||||
.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);
|
||||
/* TODO: Remove the bottom border only if there are 5 displayed lines. */
|
||||
.line:last-child {
|
||||
border-bottom: 0;
|
||||
/* To make up for the bottom border deletion */
|
||||
padding-bottom: calc(2px);
|
||||
}
|
||||
|
||||
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;
|
||||
/* 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);
|
||||
|
||||
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;
|
||||
|
||||
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 { createDateNow, getTime } from '@solid-primitives/date';
|
||||
import { timeline } from '@motionone/dom';
|
||||
import { AnimationOptions } from '@motionone/types';
|
||||
import { Motion } from "@motionone/solid";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Line, TrafficStatus } from './types';
|
||||
import { renderLineTransportMode, renderLinePicto } from './utils';
|
||||
import { renderLineTransportMode, renderLinePicto, ScrollingText } from './utils';
|
||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||
|
||||
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(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 (
|
||||
<div class="line">
|
||||
<div class="transportMode">
|
||||
@@ -148,14 +129,7 @@ const DestinationPassages: VoidComponent<{ line: Line, destination: string }> =
|
||||
</div>
|
||||
{renderLinePicto(props.line)}
|
||||
<div class="destination">
|
||||
<svg ref={destinationViewboxRef} viewBox="0 0 600 40">
|
||||
<text ref={destinationTextRef} x="0" y="50%"
|
||||
dominant-baseline="middle"
|
||||
font-size="40"
|
||||
style={{ fill: "#000000" }}>
|
||||
{props.destination}
|
||||
</text>
|
||||
</svg>
|
||||
<ScrollingText height={40} width={600} content={props.destination} />
|
||||
</div>
|
||||
<div class="trafficStatus">
|
||||
<svg viewBox="0 0 51 51">
|
||||
|
@@ -35,7 +35,6 @@
|
||||
overflow-y: scroll;
|
||||
|
||||
.stopPanel {
|
||||
// display: None;
|
||||
scroll-snap-align: center;
|
||||
|
||||
.stop {
|
||||
@@ -53,16 +52,19 @@
|
||||
border-bottom: solid calc(2px);
|
||||
|
||||
.name {
|
||||
margin-left: calc(40/1920*100%);
|
||||
width: 60%;
|
||||
aspect-ratio: 2.5;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-family: IDFVoyageur-bold;
|
||||
}
|
||||
|
||||
.lineRepr {
|
||||
// height: 100%;
|
||||
width: 40%;
|
||||
aspect-ratio: 2.5;
|
||||
// margin-left: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -75,11 +77,6 @@
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.break {
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.linesRepresentationMatrix {
|
||||
@extend %busLinePicto; // Use the larger picto aspect-ratio
|
||||
width: 75%;
|
||||
@@ -88,12 +85,6 @@
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-wrap: wrap;
|
||||
// justify-content: space-around;
|
||||
|
||||
// .break {
|
||||
// flex-basis: 100%;
|
||||
// height: 0;
|
||||
// }
|
||||
|
||||
%picto {
|
||||
margin-left: 1%;
|
||||
@@ -123,22 +114,19 @@
|
||||
}
|
||||
|
||||
.metroLinePicto {
|
||||
@extendnd %metroLinePicto;
|
||||
@extend %metroLinePicto;
|
||||
@extend %singleLinePicto;
|
||||
}
|
||||
|
||||
.busLinePicto {
|
||||
@extend %busLinePicto;
|
||||
@extend %picto;
|
||||
|
||||
height: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .stop:last-child {
|
||||
// border-bottom: 0;
|
||||
// /* to make up for the bottom border deletion */
|
||||
// padding-bottom: calc(2px);
|
||||
// }
|
||||
}
|
||||
.displayed {
|
||||
display: block;
|
||||
@@ -150,93 +138,4 @@
|
||||
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';
|
||||
|
||||
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 { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||
@@ -191,10 +191,10 @@ const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
||||
type ByTransportModeReprs = {
|
||||
mode: JSX.Element | undefined;
|
||||
lines: Record<string, JSX.Element | JSX.Element[] | undefined>;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
||||
const fontSize: number = 10;
|
||||
|
||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||
const appContextStore: AppContextStore | undefined = useContext(AppContextContext);
|
||||
@@ -248,7 +248,9 @@ const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
||||
|
||||
return (
|
||||
<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()}
|
||||
</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';
|
||||
|
||||
@@ -129,3 +130,37 @@ export type PositionedPanel = {
|
||||
// TODO: Should be PassagesPanelComponent ?
|
||||
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