13 Commits

Author SHA1 Message Date
43cbfc17b6 🚨 Make ts linter less depressed 2023-01-28 16:27:40 +01:00
29ba26e80b ♻️ Replace methods called to render Component with small Components 2023-01-28 16:26:50 +01:00
e141aa15e5 🚚 Remove business logic from Component instances 2023-01-28 16:18:55 +01:00
207fe12842 🚚 Move utils function from types.tsx to utils.tsx 2023-01-27 20:23:43 +01:00
cc5205c318 🐛 Fix PassageDisplay footer bullets behavior
The bullets was not updated according to the displayed panel.
2023-01-27 20:20:17 +01:00
495b2bafe2 🔥 Remove ar16x9 CSS class 2023-01-23 22:50:29 +01:00
e96e7aeae0 🚚 Rename NextPassagesDisplay/NextPassagesPanel (remove Next prefix) 2023-01-23 22:34:33 +01:00
b8984e455c 🚚 Add a dedicated CSS file for NextPassagePanel component 2023-01-23 21:16:47 +01:00
f8786fc863 Merge branch 'remove-idfm-extra-references' into develop 2023-01-23 21:09:20 +01:00
dea6b928e1 🗃️ Create and use a db dedicated user for backend
* Rename the db name (idfm_matrix_bot -> cer_db).
 * Remove unused bot database.
 * Create a dedicated user (cer_user/cer_password).
2023-01-22 19:02:54 +01:00
76d4c8a641 🚚 Rename backend project (idfm-matrix-widget -> carrramba-encore-rate)
Update the pyproject.toml file.
2023-01-22 19:01:15 +01:00
7423511a51 🚚 Rename backend component (idfm_matrix_backend -> backend) 2023-01-22 18:58:47 +01:00
c81d234426 🙈 Add __pycache__ and lock files 2023-01-22 17:25:44 +01:00
32 changed files with 636 additions and 659 deletions

3
backend/.gitignore vendored
View File

@@ -1 +1,2 @@
!**/__pycache__/ **/__pycache__/
poetry.lock

View File

@@ -2,10 +2,10 @@
set -e set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER idfm_matrix_bot; CREATE USER cer_user WITH ENCRYPTED password 'cer_password';
CREATE DATABASE bot; CREATE DATABASE cer_db;
CREATE DATABASE idfm; \c cer_db;
GRANT ALL PRIVILEGES ON DATABASE bot TO idfm_matrix_bot; CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA "public";
GRANT ALL PRIVILEGES ON DATABASE idfm TO idfm_matrix_bot; GRANT CREATE ON SCHEMA public to cer_user;
EOSQL EOSQL

View File

@@ -8,10 +8,10 @@ from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from rich import print from rich import print
from idfm_matrix_backend.db import db from backend.db import db
from idfm_matrix_backend.idfm_interface import IdfmInterface from backend.idfm_interface import IdfmInterface
from idfm_matrix_backend.models import Line, Stop, StopArea from backend.models import Line, Stop, StopArea
from idfm_matrix_backend.schemas import ( from backend.schemas import (
Line as LineSchema, Line as LineSchema,
TransportMode, TransportMode,
NextPassage as NextPassageSchema, NextPassage as NextPassageSchema,
@@ -24,7 +24,7 @@ API_KEY = environ.get("API_KEY")
# TODO: Add error message if no key is given. # TODO: Add error message if no key is given.
# TODO: Remove postgresql+asyncpg from environ variable # TODO: Remove postgresql+asyncpg from environ variable
DB_PATH = "postgresql+asyncpg://postgres:postgres@127.0.0.1:5438/idfm" DB_PATH = "postgresql+asyncpg://cer_user:cer_password@127.0.0.1:5438/cer_db"
app = FastAPI() app = FastAPI()

View File

@@ -1,10 +1,10 @@
[tool.poetry] [tool.poetry]
name = "idfm-matrix-widget" name = "carrramba-encore-rate"
version = "0.1.0" version = "0.1.0"
description = "" description = ""
authors = ["Adrien SUEUR <me@adrien.run>"] authors = ["Adrien SUEUR <me@adrien.run>"]
readme = "README.md" readme = "README.md"
packages = [{include = "idfm_matrix_backend"}] packages = [{include = "backend"}]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"

1
frontend/.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules node_modules
dist dist
package-lock.json package-lock.json
pnpm-lock.yaml

View File

@@ -6,7 +6,7 @@ import { HopeProvider } from "@hope-ui/solid";
import { BusinessDataProvider } from './businessData'; import { BusinessDataProvider } from './businessData';
import { SearchProvider } from './search'; import { SearchProvider } from './search';
import { NextPassagesDisplay } from './nextPassagesDisplay'; import { PassagesDisplay } from './passagesDisplay';
import { StopsManager } from './stopsManager'; import { StopsManager } from './stopsManager';
import styles from './App.module.css'; import styles from './App.module.css';
@@ -53,7 +53,7 @@ const App: Component = () => {
<StopsManager /> <StopsManager />
</div> </div>
<div class={styles.panel}> <div class={styles.panel}>
<NextPassagesDisplay /> <PassagesDisplay />
</div> </div>
</div> </div>
</HopeProvider> </HopeProvider>

View File

@@ -7,11 +7,11 @@ import { Passages, Stops } from './types';
interface Store { interface Store {
passages: () => Passages; passages: () => Passages;
getLinePassages?: (lineId: string) => Passages; getLinePassages?: (lineId: string) => Passages;
addPassages?: (passages) => void; addPassages?: (passages: Passages) => void;
clearPassages?: () => void; clearPassages?: () => void;
stops: () => Stops; stops: () => Stops;
addStops?: (stops) => void; addStops?: (stops: Stops) => void;
}; };
export const BusinessDataContext = createContext<Store>(); export const BusinessDataContext = createContext<Store>();
@@ -22,7 +22,7 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
const [store, setStore] = createStore({ lines: {}, passages: {}, stops: {} }); const [store, setStore] = createStore({ lines: {}, passages: {}, stops: {} });
async function getLine(lineId: number) { const getLine: Line = async (lineId: string) => {
let line = store.lines[lineId]; let line = store.lines[lineId];
if (line === undefined) { if (line === undefined) {
console.log(`${lineId} not found... fetch it from backend.`); console.log(`${lineId} not found... fetch it from backend.`);
@@ -35,44 +35,59 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
return line; return line;
} }
const passages = () => {
return store.passages;
};
const getLinePassages = (lineId: string) => { const getLinePassages = (lineId: string) => {
return store.passages[lineId]; return store.passages[lineId];
}; };
const passages = () => {
return store.passages;
}
const refreshPassages = async (stopId: number) => {
const httpOptions = { headers: { "Content-Type": "application/json" } };
console.log(`Fetching data for ${stopId}`);
const data = await fetch(`${serverUrl()}/stop/nextPassages/${stopId}`, httpOptions);
const response = await data.json();
addPassages(response.passages);
}
const addPassages = (passages) => { const addPassages = (passages) => {
setStore((s) => { setStore((s) => {
// console.log("s=", s);
setStore('passages', passages); setStore('passages', passages);
// console.log("s=", s);
}); });
} }
const clearPassages = () => { const clearPassages = () => {
setStore((s) => { 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)) { for (const lineId of Object.keys(s.passages)) {
console.log("lineId=", lineId);
setStore('passages', lineId, undefined); setStore('passages', lineId, undefined);
} }
console.log("s=", s);
}); });
// setStore('passages', undefined); }
// setStore('passages', {});
// } const getStop = (stopId: int) => {
console.log("passages=", store.passages); return store.stops[stopId];
}
const searchStopByName = async (name: string) => {
const data = await fetch(`${serverUrl()}/stop/?name=${name}`, {
headers: { 'Content-Type': 'application/json' }
});
const stops = await data.json();
const byIdStops = {};
for (const stop of stops) {
byIdStops[stop.id] = stop;
setStore('stops', stop.id, stop);
}
return byIdStops;
} }
return ( return (
<BusinessDataContext.Provider value={{ getLine, passages, getLinePassages, addPassages, clearPassages, serverUrl }}> <BusinessDataContext.Provider value={{
getLine, getLinePassages, passages, refreshPassages, clearPassages,
getStop, searchStopByName
}}>
{props.children} {props.children}
</BusinessDataContext.Provider> </BusinessDataContext.Provider>
); );

View File

@@ -1,253 +0,0 @@
import { Component, createEffect, createSignal, useContext } from "solid-js";
import { createStore } from "solid-js/store";
import { createDateNow } from "@solid-primitives/date";
import { format } from "date-fns";
import { getTransportModeSrc } from "./types";
import { BusinessDataContext } from "./businessData";
import { NextPassagesPanel } from "./nextPassagesPanel";
import { SearchContext } from "./search";
import styles from "./nextPassagesDisplay.module.css";
export const NextPassagesDisplay: Component = () => {
const maxPassagePerPanel = 5;
const syncPeriodMsec = 20 * 1000;
const { passages, getLinePassages, addPassages, clearPassages, serverUrl } =
useContext(BusinessDataContext);
const { getDisplayedStop } = useContext(SearchContext);
const [panels, setPanels] = createStore([]);
const [displayedPanelId, setDisplayedPanelId] = createSignal<number>(0);
let _lines = new Map();
const [dateNow] = createDateNow(1000);
const panelSwapInterval = setInterval(() => {
let nextPanelId = displayedPanelId() + 1;
if (nextPanelId >= panels.length) {
nextPanelId = 0;
}
/* console.log(`Display panel #${nextPanelId}`); */
setDisplayedPanelId(nextPanelId);
}, 4000);
createEffect(() => {
console.log("######### onStopIdUpdate #########");
// Track local.stopIp to force dependency.
console.log("getDisplayedStop=", getDisplayedStop());
clearPassages();
});
createEffect(async () => {
console.log(`## OnPassageUpdate ${passages()} ##`);
/* console.log(passages()); */
await requestPassages();
});
async function _fetchLine(lineId: string) {
if (!_lines.has(lineId)) {
const data = await fetch(`${serverUrl()}/line/${lineId}`, {
headers: { "Content-Type": "application/json" },
});
const line = await data.json();
_lines.set(line.id, line);
}
}
async function requestPassages() {
console.log("### requestPassages ###");
/* TODO: Manage several displays (one by stop) */
const stops = getDisplayedStop();
if (stops.length == 0) {
return;
}
const stop = stops[0];
const httpOptions = { headers: { "Content-Type": "application/json" } };
if (stop !== undefined) {
const stopId = stop.id;
console.log(`Fetching data for ${stopId}`);
const url = `${serverUrl()}/stop/nextPassages/${stopId}`;
/* console.log(`url=${url}`); */
const data = await fetch(url, httpOptions);
const response = await data.json();
/* console.log(response); */
const byLineByDstPassages = response.passages;
/* console.log(byLineByDstPassages); */
const linePromises = [];
for (const lineId of Object.keys(byLineByDstPassages)) {
linePromises.push(_fetchLine(lineId));
}
await Promise.all(linePromises);
console.log("byLineByDstPassages=", byLineByDstPassages);
// console.log("before addPassages passages=", passages());
addPassages(byLineByDstPassages);
console.log("AFTER passages=", passages());
}
}
setInterval(
// const nextPassagesRequestsInterval = setTimeout(
async () => {
await requestPassages();
},
syncPeriodMsec
);
// TODO: Sort transport modes by weight
// TODO: Split this method to isolate the nextPassagesPanel part.
function _computeHeader(title: string): JSX.Element {
let transportModes = [];
transportModes = new Set(
Object.keys(passages()).map((lineId) => {
const line = _lines.get(lineId);
if (line !== undefined) {
return getTransportModeSrc(line.transportMode, false);
}
return null;
})
);
return (
<div class={styles.header}>
<For each={Array.from(transportModes)}>
{(transportMode) => {
return (
<div class={styles.transportMode}>
<img src={transportMode} />
</div>
);
}}
</For>
<div class={styles.title}>
<svg viewbox="0 0 1260 50">
<text
x="0"
y="50%"
dominant-baseline="middle"
font-size="50"
style="fill: #ffffff"
>
{title}
</text>
</svg>
</div>
<div class={styles.clock}>
<svg viewbox="0 0 115 43">
<text
x="50%"
y="55%"
dominant-baseline="middle"
text-anchor="middle"
font-size="43"
style="fill: #ffffff"
>
{format(dateNow(), "HH:mm")}
</text>
</svg>
</div>
</div>
);
}
function _computeFooter(): JSX.Element {
return (
<div class={styles.footer}>
<For each={panels}>
{(positioned) => {
const { position, panel } = positioned;
const circleStyle = {
fill: `var(--idfm-${position == displayedPanelId() ? "white" : "black"
})`,
};
return (
<div>
<svg viewBox="0 0 29 29">
<circle
cx="50%"
cy="50%"
r="13"
stroke="#ffffff"
stroke-width="3"
style={circleStyle}
/>
</svg>
</div>
);
}}
</For>
</div>
);
}
const mainDivClasses = `${styles.NextPassagesDisplay} ${styles.ar16x9}`;
return (
<div class={mainDivClasses}>
{_computeHeader("Prochains passages")}
<div class={styles.panelsContainer}>
{() => {
setPanels([]);
let newPanels = [];
let positioneds = [];
let index = 0;
let chunk = {};
let chunkSize = 0;
console.log("passages=", passages());
for (const lineId of Object.keys(passages())) {
console.log("lineId=", lineId);
const byLinePassages = getLinePassages(lineId);
console.log("byLinePassages=", byLinePassages);
const byLinePassagesKeys = Object.keys(byLinePassages);
console.log("byLinePassagesKeys=", byLinePassagesKeys);
if (byLinePassagesKeys.length <= maxPassagePerPanel - chunkSize) {
chunk[lineId] = byLinePassages;
chunkSize += byLinePassagesKeys.length;
} else {
console.log("chunk=", chunk);
const [store, setStore] = createStore(chunk);
const panelid = index++;
const panel = (
<NextPassagesPanel
show={panelid == displayedPanelId()}
nextPassages={store}
lines={_lines}
/>
);
newPanels.push(panel);
positioneds.push({ position: panelid, panel });
chunk = {};
chunk[lineId] = byLinePassages;
chunkSize = byLinePassagesKeys.length;
}
}
if (chunkSize) {
const panelId = index++;
const [store, setStore] = createStore(chunk);
const panel = (
<NextPassagesPanel
show={panelId == displayedPanelId()}
nextPassages={store}
lines={_lines}
/>
);
newPanels.push(panel);
positioneds.push({ position: panelId, panel });
}
setPanels(positioneds);
return newPanels;
}}
</div>
{_computeFooter()}
</div>
);
};

View File

@@ -1,121 +0,0 @@
import { Component } from 'solid-js';
import { createStore } from 'solid-js/store';
import { createDateNow, getTime } from '@solid-primitives/date';
import { Motion } from "@motionone/solid";
import { TrafficStatus } from './types';
import { renderLineTransportMode, renderLinePicto } from './utils';
import styles from './nextPassagesDisplay.module.css';
export const NextPassagesPanel: Component = (props) => {
/* TODO: Find where to get data to compute traffic status. */
const trafficStatusColor = new Map<TrafficStatus, string>([
[TrafficStatus.UNKNOWN, "#ffffff"],
[TrafficStatus.FLUID, "#00643c"],
[TrafficStatus.DISRUPTED, "#ffbe00"],
[TrafficStatus.VERY_DISRUPTED, "#ff5a00"],
[TrafficStatus.BYPASSED, "#ffffff"]
]);
const [dateNow] = createDateNow(5000);
function _computeTtwPassage(class_, passage, fontSize) {
const refTs = passage.expectedDepartTs !== null ? passage.expectedDepartTs : passage.expectedArrivalTs;
const ttwSec = refTs - (getTime(dateNow()) / 1000);
const isApproaching = ttwSec <= 60;
return (
<div class={class_}>
<svg viewBox={`0 0 215 ${fontSize}`}>
<Motion.text
x="100%" y="55%"
dominant-baseline="middle" text-anchor="end"
font-size={fontSize} style={{ fill: "#000000" }}
initial={isApproaching}
animate={{ opacity: [1, 0, 1] }}
transition={{ duration: 3, repeat: Infinity }}>
{Math.floor(ttwSec / 60)} min
</Motion.text>
</svg>
</div>
);
}
function _computeUnavailablePassage(class_) {
const textStyle = { fill: "#000000" };
return (
<div class={class_}>
<svg viewbox="0 0 230 110">
<text x="100%" y="26" font-size="25" text-anchor="end" style={textStyle}>Information</text>
<text x="100%" y="63" font-size="25" text-anchor="end" style={textStyle}>non</text>
<text x="100%" y="100" font-size="25" text-anchor="end" style={textStyle}>disponible</text>
</svg>
</div>
);
}
function _computeSecondPassage(passage): JSX.Element {
return (
<Show when={passage !== undefined} fallback={_computeUnavailablePassage(styles.unavailableSecondPassage)}>
{_computeTtwPassage(styles.secondPassage, passage, 45)}
</Show>
);
}
function _computeFirstPassage(passage): JSX.Element {
return (
<Show when={passage !== undefined} fallback={_computeUnavailablePassage(styles.unavailableFirstPassage)}>
{_computeTtwPassage(styles.firstPassage, passage, 50)}
</Show>
);
}
/* TODO: Manage end of service */
function _genNextPassages(nextPassages, line, destination) {
const nextPassagesLength = nextPassages.length;
const firstPassage = nextPassagesLength > 0 ? nextPassages[0] : undefined;
const secondPassage = nextPassagesLength > 1 ? nextPassages[1] : undefined;
const trafficStatusStyle = { fill: trafficStatusColor.get(line.trafficStatus) };
return (
<div class={styles.line}>
<div class={styles.transportMode}>
{renderLineTransportMode(line)}
</div>
{renderLinePicto(line, styles)}
<div class={styles.destination}>
<svg viewbox="0 0 600 40">
<text x="0" y="50%" dominant-baseline="middle" font-size="40" style={{ fill: "#000000" }}>
{destination}
</text>
</svg>
</div>
<div class={styles.trafficStatus}>
<svg viewBox="0 0 51 51">
<circle cx="50%" cy="50%" r="24" stroke="#231f20" stroke-width="3" style={trafficStatusStyle} />
</svg>
</div>
{firstPassage ? _computeFirstPassage(firstPassage) : null}
{secondPassage ? _computeSecondPassage(secondPassage) : null}
</div>
);
}
return (
<div classList={{ [styles.nextPassagesContainer]: true, [styles.displayed]: props.show }} style={{ "top": `${100 * props.position}%` }}>
{() => {
const ret = [];
for (const lineId of Object.keys(props.nextPassages)) {
const line = props.lines.get(lineId);
const byLineNextPassages = props.nextPassages[lineId];
for (const destination of Object.keys(byLineNextPassages)) {
const nextPassages = byLineNextPassages[destination];
ret.push(_genNextPassages(nextPassages, line, destination));
}
}
return ret;
}}
</div>
);
}

View File

@@ -0,0 +1,90 @@
/* Idfm: 1860x1080px */
.passagesDisplay {
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%);
}
/* 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%);
}

View File

@@ -0,0 +1,179 @@
import { Component, createEffect, createResource, createSignal, useContext } from "solid-js";
import { createStore } from "solid-js/store";
import { createDateNow } from "@solid-primitives/date";
import { format } from "date-fns";
import { BusinessDataContext } from "./businessData";
import { SearchContext } from "./search";
import { PassagesPanel } from "./passagesPanel";
import { getTransportModeSrc } from "./utils";
import styles from "./passagesDisplay.module.css";
export const PassagesDisplay: Component = () => {
const maxPassagePerPanel = 5;
const syncPeriodMsec = 20 * 1000;
const { passages, getLine, getLinePassages, refreshPassages, clearPassages } = useContext(BusinessDataContext);
// TODO: Use props instead
const { getDisplayedStops } = useContext(SearchContext);
const [displayedPanelId, setDisplayedPanelId] = createSignal<number>(0);
const [panels, setPanels] = createStore([]);
const [dateNow] = createDateNow(1000);
setInterval(() => {
let nextPanelId = displayedPanelId() + 1;
if (nextPanelId >= panels.length) {
nextPanelId = 0;
}
setDisplayedPanelId(nextPanelId);
}, 4000);
createEffect(() => {
console.log("######### onStopIdUpdate #########");
// Track local.stopIp to force dependency.
console.log("getDisplayedStop=", getDisplayedStops());
clearPassages();
});
createEffect(async () => {
console.log(`## OnPassageUpdate ${passages()} ##`);
const stops = getDisplayedStops();
if (stops.length > 0) {
refreshPassages(stops[0].id);
}
});
setInterval(
async () => {
const stops = getDisplayedStops();
if (stops.length > 0) {
refreshPassages(stops[0].id);
}
},
syncPeriodMsec
);
// TODO: Sort transport modes by weight
const Header: Component = (props) => {
const computeTransportModes = async (lineIds: Array<number>) => {
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
return new Set(lines.map((line) => getTransportModeSrc(line.transportMode, false)));
}
const [linesIds, setLinesIds] = createSignal([]);
const [transportModeUrls] = createResource(linesIds, computeTransportModes);
createEffect(() => {
setLinesIds(Object.keys(props.passages));
});
return (
<div class={styles.header}>
<Show when={transportModeUrls() !== undefined} >
<For each={Array.from(transportModeUrls())}>
{(url) =>
<div class={styles.transportMode}>
<img src={url} />
</div>
}
</For>
</Show>
<div class={styles.title}>
<svg viewbox="0 0 1260 50">
<text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
{props.title}
</text>
</svg>
</div>
<div class={styles.clock}>
<svg viewbox="0 0 115 43">
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-size="43" style="fill: #ffffff">
{format(dateNow(), "HH:mm")}
</text>
</svg>
</div>
</div >
);
};
const Footer: Component = (props) => {
return (
<div class={styles.footer}>
<For each={props.panels}>
{(positioned) => {
const { position } = positioned;
return (
<div>
<svg viewBox="0 0 29 29">
<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
style={{ fill: `var(--idfm-${position == displayedPanelId() ? "white" : "black"})` }}
/>
</svg>
</div>
);
}}
</For>
</div>
);
}
return (
<div class={styles.passagesDisplay}>
<Header title="Prochains passages" passages={passages()} />
<div class={styles.panelsContainer}>
{() => {
setPanels([]);
let newPanels = [];
let positioneds = [];
let index = 0;
let chunk = {};
let chunkSize = 0;
console.log("passages=", passages());
for (const lineId of Object.keys(passages())) {
console.log("lineId=", lineId);
const byLinePassages = getLinePassages(lineId);
console.log("byLinePassages=", byLinePassages);
const byLinePassagesKeys = Object.keys(byLinePassages);
console.log("byLinePassagesKeys=", byLinePassagesKeys);
if (byLinePassagesKeys.length <= maxPassagePerPanel - chunkSize) {
chunk[lineId] = byLinePassages;
chunkSize += byLinePassagesKeys.length;
}
else {
const [store] = createStore(chunk);
const panelid = index++;
const panel = <PassagesPanel show={panelid == displayedPanelId()} passages={store} />;
newPanels.push(panel);
positioneds.push({ position: panelid, panel });
chunk = {};
chunk[lineId] = byLinePassages;
chunkSize = byLinePassagesKeys.length;
}
}
if (chunkSize) {
const panelId = index++;
const [store] = createStore(chunk);
const panel = <PassagesPanel show={panelId == displayedPanelId()} passages={store} />;
newPanels.push(panel);
positioneds.push({ position: panelId, panel });
}
setPanels(positioneds);
return newPanels;
}}
</div>
<Footer panels={panels} />
</div>
);
};

View File

@@ -1,101 +1,22 @@
.passagesContainer {
/* 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%; height: 100%;
width: 100%; width: 100%;
display: none; display: none;
position: relative;
}
.nextPassagesContainer .line:last-child { position: relative;
border-bottom: 0;
/* To make up for the bottom border deletion */
padding-bottom: calc(2px);
} }
.displayed { .displayed {
display: block; 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) */ /* Idfm: 1880x176px (margin: 0px 20px) */
.line { .line {
@@ -120,7 +41,7 @@
/* Idfm: 100x100px (margin: 0px 15px) */ /* Idfm: 100x100px (margin: 0px 15px) */
.transportMode { .transportMode {
aspect-ratio : 1 / 1; aspect-ratio : 1 / 1;
height: calc(100/176*100%); height: calc(100/176*100%);
margin: 0 calc(15/1920*100%); margin: 0 calc(15/1920*100%);
} }
@@ -139,7 +60,7 @@
.destination { .destination {
height: calc(60/176*100%); height: calc(60/176*100%);
width: 50%; width: 50%;
font-family: IDFVoyageur-bold; font-family: IDFVoyageur-bold;
text-align: left; text-align: left;
} }
@@ -162,7 +83,7 @@
.firstPassage { .firstPassage {
height: calc(100/176*100%); height: calc(100/176*100%);
aspect-ratio: 2.5; aspect-ratio: 2.5;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -203,24 +124,6 @@
margin-right: calc(30/1920*100%); margin-right: calc(30/1920*100%);
} }
.unavailableSecondNextPassage svg { .unavailableSecondPassage svg {
font-family: IDFVoyageur-regular; 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%);
}

View File

@@ -0,0 +1,131 @@
import { Component, createEffect, createResource, createSignal, useContext } from 'solid-js';
import { createDateNow, getTime } from '@solid-primitives/date';
import { Motion } from "@motionone/solid";
import { TrafficStatus } from './types';
import { renderLineTransportMode, renderLinePicto } from './utils';
import { BusinessDataContext } from "./businessData";
import styles from "./passagesPanel.module.css";
const TtwPassage: Component = (props) => {
const [dateNow] = createDateNow(5000);
const refTs = props.passage.expectedDepartTs !== null ? props.passage.expectedDepartTs : props.passage.expectedArrivalTs;
const ttwSec = refTs - (getTime(dateNow()) / 1000);
const isApproaching = ttwSec <= 60;
return (
<div class={props.style}>
<svg viewBox={`0 0 215 ${props.fontSize}`}>
<Motion.text
x="100%" y="55%"
dominant-baseline="middle" text-anchor="end"
font-size={props.fontSize} style={{ fill: "#000000" }}
initial={isApproaching}
animate={{ opacity: [1, 0, 1] }}
transition={{ duration: 3, repeat: Infinity }}>
{Math.floor(ttwSec / 60)} min
</Motion.text>
</svg>
</div>
);
}
const UnavailablePassage: Component = (props) => {
const textStyle = { fill: "#000000" };
return (
<div class={props.style}>
<svg viewbox="0 0 230 110">
<text x="100%" y="26" font-size="25" text-anchor="end" style={textStyle}>Information</text>
<text x="100%" y="63" font-size="25" text-anchor="end" style={textStyle}>non</text>
<text x="100%" y="100" font-size="25" text-anchor="end" style={textStyle}>disponible</text>
</svg>
</div>
);
}
/* TODO: Manage end of service */
const Passages: Component = (props) => {
/* TODO: Find where to get data to compute traffic status. */
const trafficStatusColor = new Map<TrafficStatus, string>([
[TrafficStatus.UNKNOWN, "#ffffff"],
[TrafficStatus.FLUID, "#00643c"],
[TrafficStatus.DISRUPTED, "#ffbe00"],
[TrafficStatus.VERY_DISRUPTED, "#ff5a00"],
[TrafficStatus.BYPASSED, "#ffffff"]
]);
const passagesLength = props.passages.length;
const firstPassage = passagesLength > 0 ? props.passages[0] : undefined;
const secondPassage = passagesLength > 1 ? props.passages[1] : undefined;
const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
return (
<div class={styles.line}>
<div class={styles.transportMode}>
{renderLineTransportMode(props.line)}
</div>
{renderLinePicto(props.line, styles)}
<div class={styles.destination}>
<svg viewbox="0 0 600 40">
<text x="0" y="50%" dominant-baseline="middle" font-size="40" style={{ fill: "#000000" }}>
{props.destination}
</text>
</svg>
</div>
<div class={styles.trafficStatus}>
<svg viewBox="0 0 51 51">
<circle cx="50%" cy="50%" r="24" stroke="#231f20" stroke-width="3" style={trafficStatusStyle} />
</svg>
</div>
<Show when={firstPassage !== undefined} fallback=<UnavailablePassage style={styles.unavailableFirstPassage} />>
<TtwPassage style={styles.firstPassage} passage={firstPassage} fontSize="50" />
</Show>
<Show when={secondPassage !== undefined} fallback=<UnavailablePassage style={styles.unavailableSecondPassage} />>
<TtwPassage style={styles.secondPassage} passage={secondPassage} fontSize="45" />
</Show>
</div >
);
}
export const PassagesPanel: Component = (props) => {
const { getLine } = useContext(BusinessDataContext);
const getLines = async (lineIds: Array<number>) => {
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
return lines;
}
const [lineIds, setLinesIds] = createSignal([]);
const [lines] = createResource(lineIds, getLines);
createEffect(async () => {
setLinesIds(Object.keys(props.passages));
});
return (
<div classList={{ [styles.passagesContainer]: true, [styles.displayed]: props.show }} style={{ "top": `${100 * props.position}%` }}>
<Show when={lines() !== undefined} >
{() => {
const ret = [];
for (const line of lines()) {
const byLinePassages = props.passages[line.id];
if (byLinePassages !== undefined) {
for (const destination of Object.keys(byLinePassages)) {
ret.push(<Passages passages={byLinePassages[destination]} line={line} destination={destination} />);
}
}
}
return ret;
}}
</Show>
</div >
);
}

View File

@@ -1,75 +1,62 @@
import { batch, createContext, createSignal } from 'solid-js'; import { batch, createContext } from 'solid-js';
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { Stop, Stops } from './types'; import { Stop, Stops } from './types';
interface Store { interface Store {
getMarkers: () => Markers; getMarkers: () => Markers;
addMarkers?: (stopId, markers) => void; addMarkers?: (stopId, markers) => void;
setMarkers?: (markers) => void; setMarkers?: (markers) => void;
getStops: () => Stops; getStops: () => Stops;
setStops?: (stops) => void; setStops?: (stops) => void;
removeStops?: (stopIds) => void; removeStops?: (stopIds: Array<number>) => void;
getDisplayedStop: () => Stop; getDisplayedStops: () => Array<Stop>;
setDisplayedStop: (stop: Stop) => void; setDisplayedStops: (stops: Array<Stop>) => void;
}; };
export const SearchContext = createContext<Store>(); export const SearchContext = createContext<Store>();
export function SearchProvider(props: { children: JSX.Element }) { export function SearchProvider(props: { children: JSX.Element }) {
const [store, setStore] = createStore({stops: {}, markers: {}, displayedStop: []}); const [store, setStore] = createStore({ stops: {}, markers: {}, displayedStops: [] });
const getStops = () => { const getDisplayedStops = () => {
return store.stops; return store.displayedStops;
}; }
const setStops = (stops) => { const setDisplayedStops = (stops: Array<Stop>) => {
setStore((s) => { setStore((s) => {
setStore('stops', stops); setStore('displayedStops', stops);
}); });
}; }
const removeStops = (stopIds) => { const removeStops = (stopIds: Array<number>) => {
batch(() => { batch(() => {
for(const stopId of stopIds) { for (const stopId of stopIds) {
setStore('stops', stopId, undefined); setStore('stops', stopId, undefined);
setStore('markers', stopId, undefined); setStore('markers', stopId, undefined);
} }
}); });
}; }
const getMarkers = () => { const getMarkers = () => {
return store.markers; return store.markers;
}; }
const addMarkers = (stopId, markers) => { const addMarkers = (stopId: number, markers) => {
setStore('markers', stopId, markers); setStore('markers', stopId, markers);
}; }
const setMarkers = (markers) => { const setMarkers = (markers) => {
setStore('markers', markers); setStore('markers', markers);
}; }
const getDisplayedStop = () => { return (
/* console.log(store.displayedStop); */ <SearchContext.Provider value={{ getDisplayedStops, setDisplayedStops, removeStops, getMarkers, addMarkers, setMarkers }}>
return store.displayedStop; {props.children}
}; </SearchContext.Provider>
const setDisplayedStop = (stop: Stop) => { );
/* console.log(stop); */
setStore((s) => {
console.log("s.displayedStop=", s.displayedStop);
setStore('displayedStop', [stop]);
});
/* console.log(store.displayedStop); */
};
return (
<SearchContext.Provider value={{addMarkers, getMarkers, setMarkers, getStops, removeStops, setStops, getDisplayedStop, setDisplayedStop}}>
{props.children}
</SearchContext.Provider>
);
} }

View File

@@ -1,15 +1,13 @@
import { batch, Component, createEffect, createResource, createSignal, onMount, Show, useContext } from 'solid-js'; import { Component, createEffect, createResource, createSignal, onMount, Show, useContext } from 'solid-js';
import { import { Box, Button, Input, InputLeftAddon, InputGroup, HStack, List, ListItem, Progress, ProgressIndicator, VStack } from "@hope-ui/solid";
Box, Button, Input, InputLeftAddon, InputGroup, HStack, List, ListItem, Progress,
ProgressIndicator, VStack
} from "@hope-ui/solid";
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import L from 'leaflet'; import L from 'leaflet';
import { BusinessDataContext } from './businessData'; import { BusinessDataContext } from './businessData';
import { SearchContext } from './search'; import { SearchContext } from './search';
import { Stop } from './types';
import { renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils'; import { renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils';
import styles from './stopManager.module.css'; import styles from './stopManager.module.css';
@@ -21,7 +19,7 @@ const StopRepr: Component = (props) => {
const [lineReprs] = createResource(props.stop.lines, fetchLinesRepr); const [lineReprs] = createResource(props.stop.lines, fetchLinesRepr);
async function fetchLinesRepr(lineIds) { const fetchLinesRepr = async (lineIds: Array<string>) => {
const reprs = []; const reprs = [];
for (const lineId of lineIds) { for (const lineId of lineIds) {
const line = await getLine(lineId); const line = await getLine(lineId);
@@ -45,9 +43,7 @@ const StopAreaRepr: Component = (props) => {
const { getLine } = useContext(BusinessDataContext); const { getLine } = useContext(BusinessDataContext);
const [lineReprs] = createResource(props.stop, fetchLinesRepr); const fetchLinesRepr = async (stop: Stop) => {
async function fetchLinesRepr(stop) {
const lineIds = new Set(stop.lines); const lineIds = new Set(stop.lines);
const stops = stop.stops; const stops = stop.stops;
for (const stop of stops) { for (const stop of stops) {
@@ -81,6 +77,8 @@ const StopAreaRepr: Component = (props) => {
return reprs; return reprs;
} }
const [lineReprs] = createResource(props.stop, fetchLinesRepr);
return ( return (
<HStack height="100%"> <HStack height="100%">
{props.stop.name} {props.stop.name}
@@ -94,13 +92,13 @@ const Map: Component = (props) => {
const mapCenter = [48.853, 2.35]; const mapCenter = [48.853, 2.35];
const { addMarkers, getStops } = useContext(SearchContext); const { addMarkers } = useContext(SearchContext);
let mapDiv: any; let mapDiv: any;
let map = null; let map = null;
const stopsLayerGroup = L.featureGroup(); const stopsLayerGroup = L.featureGroup();
function buildMap(div: HTMLDivElement) { const buildMap = (div: HTMLDivElement) => {
map = L.map(div).setView(mapCenter, 11); map = L.map(div).setView(mapCenter, 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
@@ -108,7 +106,7 @@ const Map: Component = (props) => {
stopsLayerGroup.addTo(map); stopsLayerGroup.addTo(map);
} }
function setMarker(stop) { const setMarker = (stop: Stop): Array<L.Marker> => {
const markers = []; const markers = [];
if (stop.lat !== undefined && stop.lon !== undefined) { if (stop.lat !== undefined && stop.lon !== undefined) {
/* TODO: Add stop lines representation to popup. */ /* TODO: Add stop lines representation to popup. */
@@ -124,11 +122,11 @@ const Map: Component = (props) => {
onMount(() => buildMap(mapDiv)); onMount(() => buildMap(mapDiv));
const onStopUpdate = createEffect(() => { createEffect(() => {
/* TODO: Avoid to clear all layers... */ /* TODO: Avoid to clear all layers... */
stopsLayerGroup.clearLayers(); stopsLayerGroup.clearLayers();
for (const stop of Object.values(getStops())) { for (const stop of props.stops) {
const markers = setMarker(stop); const markers = setMarker(stop);
addMarkers(stop.id, markers); addMarkers(stop.id, markers);
for (const marker of markers) { for (const marker of markers) {
@@ -145,42 +143,24 @@ const Map: Component = (props) => {
return <div ref={mapDiv} id='main-map' style={{ width: "100%", height: "100%" }} />; return <div ref={mapDiv} id='main-map' style={{ width: "100%", height: "100%" }} />;
} }
export const StopsManager: Component = (props) => { export const StopsManager: Component = () => {
const [minCharactersNb, setMinCharactersNb] = createSignal<int>(4); const [minCharactersNb, setMinCharactersNb] = createSignal<number>(4);
const [_inProgress, _setInProgress] = createSignal<bool>(false); const [inProgress, setInProgress] = createSignal<boolean>(false);
const [foundStops, setFoundStops] = createSignal<Array<number>>([]);
const { serverUrl } = useContext(BusinessDataContext); const { getStop, searchStopByName } = useContext(BusinessDataContext);
const { getStops, removeStops, setStops, setDisplayedStop } = useContext(SearchContext); const { setDisplayedStops } = useContext(SearchContext);
async function _fetchStopByName(name) { const onStopNameInput = async (event) => {
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 */ /* TODO: Add a tempo before fetching stop for giving time to user to finish his request */
const stopName = event.target.value; const stopName = event.target.value;
if (stopName.length >= minCharactersNb()) { if (stopName.length >= minCharactersNb()) {
console.log(`Fetching data for ${stopName}`); console.log(`Fetching data for ${stopName}`);
_setInProgress(true); setInProgress(true);
await _fetchStopByName(stopName); const stopsById = await searchStopByName(stopName);
_setInProgress(false); setFoundStops(Object.values(stopsById));
setInProgress(false);
} }
} }
@@ -188,21 +168,21 @@ export const StopsManager: Component = (props) => {
<VStack h="100%"> <VStack h="100%">
<InputGroup w="50%" h="5%"> <InputGroup w="50%" h="5%">
<InputLeftAddon>🚉 🚏</InputLeftAddon> <InputLeftAddon>🚉 🚏</InputLeftAddon>
<Input onInput={_onStopNameInput} readOnly={_inProgress()} placeholder="Stop name..." /> <Input onInput={onStopNameInput} readOnly={inProgress()} placeholder="Stop name..." />
</InputGroup> </InputGroup>
<Progress size="xs" w="50%" indeterminate={_inProgress()}> <Progress size="xs" w="50%" indeterminate={inProgress()}>
<ProgressIndicator striped animated /> <ProgressIndicator striped animated />
</Progress> </Progress>
<Box w="100%" h="40%" borderWidth="1px" borderColor="var(--idfm-black)" borderRadius="$lg" overflow="scroll" marginBottom="2px"> <Box w="100%" h="40%" borderWidth="1px" borderColor="var(--idfm-black)" borderRadius="$lg" overflow="scroll" marginBottom="2px">
<List width="100%" height="100%"> <List width="100%" height="100%">
{() => { {() => {
const items = []; const items = [];
for (const stop of Object.values(getStops()).sort((x, y) => x.name.localeCompare(y.name))) { for (const stop of foundStops().sort((x, y) => x.name.localeCompare(y.name))) {
items.push( items.push(
<ListItem h="10%" borderWidth="1px" mb="0px" color="var(--idfm-black)" borderRadius="$lg"> <ListItem h="10%" borderWidth="1px" mb="0px" color="var(--idfm-black)" borderRadius="$lg">
<Button fullWidth="true" color="var(--idfm-black)" bg="white" onClick={() => { <Button fullWidth="true" color="var(--idfm-black)" bg="white" onClick={() => {
console.log(`${stop.id} clicked !!!`); console.log(`${stop.id} clicked !!!`);
setDisplayedStop(stop); setDisplayedStops([stop]);
}}> }}>
<Box w="100%" h="100%"> <Box w="100%" h="100%">
<Show when={stop.stops !== undefined} fallback={<StopRepr stop={stop} />}> <Show when={stop.stops !== undefined} fallback={<StopRepr stop={stop} />}>
@@ -217,7 +197,7 @@ export const StopsManager: Component = (props) => {
</List> </List>
</Box> </Box>
<Box borderWidth="1px" borderColor="var(--idfm-black)" borderRadius="$lg" h="55%" w="100%" overflow="scroll"> <Box borderWidth="1px" borderColor="var(--idfm-black)" borderRadius="$lg" h="55%" w="100%" overflow="scroll">
<Map /> <Map stops={foundStops()} />
</Box> </Box>
</VStack> </VStack>
); );

View File

@@ -1,13 +1,3 @@
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 { export enum TrafficStatus {
UNKNOWN = 0, UNKNOWN = 0,
FLUID, FLUID,
@@ -16,28 +6,91 @@ export enum TrafficStatus {
BYPASSED BYPASSED
} }
export interface Passages { }; export class Passages { };
export interface Passage {
line: number, export class Passage {
operator: string, line: number;
destinations: Array<string>, operator: string;
atStop: boolean, destinations: Array<string>;
aimedArrivalTs: number, atStop: boolean;
expectedArrivalTs: number, aimedArrivalTs: number;
arrivalPlatformName: string, expectedArrivalTs: number;
aimedDepartTs: number, arrivalPlatformName: string;
expectedDepartTs: number, aimedDepartTs: number;
arrivalStatus: string, expectedDepartTs: number;
departStatus: string, arrivalStatus: string;
departStatus: string;
constructor(line: number, operator: string, destinations: Array<string>, atStop: boolean, aimedArrivalTs: number,
expectedArrivalTs: number, arrivalPlatformName: string, aimedDepartTs: number, expectedDepartTs: number,
arrivalStatus: string, departStatus: string) {
this.line = line;
this.operator = operator;
this.destinations = destinations;
this.atStop = atStop;
this.aimedArrivalTs = aimedArrivalTs;
this.expectedArrivalTs = expectedArrivalTs;
this.arrivalPlatformName = arrivalPlatformName;
this.aimedDepartTs = aimedDepartTs;
this.expectedDepartTs = expectedDepartTs;
this.arrivalStatus = arrivalStatus;
this.departStatus = departStatus;
}
}; };
export class Stops { };
export interface Stops { }; export class Stop {
export interface Stop { id: number;
id: number, name: string;
name: string, town: string;
town: string, lat: number;
lat: number, lon: number;
lon: number, stops: Array<Stop>;
lines: Array<string> lines: Array<string>;
constructor(id: number, name: string, town: string, lat: number, lon: number, stops: Array<Stop>, lines: Array<string>) {
this.id = id;
this.name = name;
this.town = town;
this.lat = lat;
this.lon = lon;
this.stops = stops;
this.lines = lines;
for (const stop of this.stops) {
this.lines.push(...stop.lines);
}
}
};
export class Line {
id: string;
shortName: string;
name: string;
status: string; // TODO: Use an enum
transportMode: string; // TODO: Use an enum
backColorHexa: string;
foreColorHexa: string;
operatorId: number;
accessibility: boolean;
visualSignsAvailable: string; // TODO: Use an enum
audibleSignsAvailable: string; // TODO: Use an enum
stopIds: Array<number>;
constructor(id: string, shortName: string, name: string, status: string, transportMode: string, backColorHexa: string,
foreColorHexa: string, operatorId: number, accessibility: boolean, visualSignsAvailable: string,
audibleSignsAvailable: string, stopIds: Array<number>) {
this.id = id;
this.shortName = shortName;
this.name = name;
this.status = status;
this.transportMode = transportMode;
this.backColorHexa = backColorHexa;
this.foreColorHexa = foreColorHexa;
this.operatorId = operatorId;
this.accessibility = accessibility;
this.visualSignsAvailable = visualSignsAvailable;
this.audibleSignsAvailable = audibleSignsAvailable;
this.stopIds = stopIds;
}
}; };

View File

@@ -1,4 +1,6 @@
import { getTransportModeSrc } from './types'; import { Line } from './types';
const validTransportModes = ["bus", "tram", "metro", "rer", "transilien", "funicular", "ter", "unknown"];
export const TransportModeWeights = { export const TransportModeWeights = {
bus: 1, bus: 1,
@@ -11,11 +13,20 @@ export const TransportModeWeights = {
ter: 8, ter: 8,
}; };
export function renderLineTransportMode(line): JSX.Element { export function renderLineTransportMode(line: Line): JSX.Element {
return <img src={getTransportModeSrc(line.transportMode)} /> return <img src={getTransportModeSrc(line.transportMode)} />
} }
function renderBusLinePicto(line, styles): JSX.Element { export function getTransportModeSrc(mode: string, color: boolean = true): string | null {
let ret = null;
if (validTransportModes.includes(mode)) {
ret = `/public/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`;
}
return ret;
}
function renderBusLinePicto(line: Line, styles): JSX.Element {
return ( return (
<div class={styles.busLinePicto}> <div class={styles.busLinePicto}>
<svg viewBox="0 0 31.5 14"> <svg viewBox="0 0 31.5 14">
@@ -33,7 +44,7 @@ function renderBusLinePicto(line, styles): JSX.Element {
); );
} }
function renderTramLinePicto(line, styles): JSX.Element { function renderTramLinePicto(line: Line, styles): JSX.Element {
const lineStyle = { fill: `#${line.backColorHexa}` }; const lineStyle = { fill: `#${line.backColorHexa}` };
return ( return (
<div class={styles.tramLinePicto}> <div class={styles.tramLinePicto}>
@@ -53,7 +64,7 @@ function renderTramLinePicto(line, styles): JSX.Element {
); );
} }
function renderMetroLinePicto(line, styles): JSX.Element { function renderMetroLinePicto(line: Line, styles): JSX.Element {
return ( return (
<div class={styles.metroLinePicto}> <div class={styles.metroLinePicto}>
<svg viewbox="0 0 20 20"> <svg viewbox="0 0 20 20">
@@ -70,7 +81,7 @@ function renderMetroLinePicto(line, styles): JSX.Element {
); );
} }
function renderTrainLinePicto(line, styles): JSX.Element { function renderTrainLinePicto(line: Line, styles): JSX.Element {
return ( return (
<div class={styles.trainLinePicto}> <div class={styles.trainLinePicto}>
<svg viewbox="0 0 20 20"> <svg viewbox="0 0 20 20">
@@ -88,7 +99,7 @@ function renderTrainLinePicto(line, styles): JSX.Element {
); );
} }
export function renderLinePicto(line, styles): JSX.Element { export function renderLinePicto(line: Line, styles): JSX.Element {
switch (line.transportMode) { switch (line.transportMode) {
case "bus": case "bus":
case "funicular": case "funicular":