Merge branch 'frontend-file-reorg' into develop
This commit is contained in:
@@ -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>
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
90
frontend/src/passagesDisplay.module.css
Normal file
90
frontend/src/passagesDisplay.module.css
Normal 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%);
|
||||||
|
}
|
179
frontend/src/passagesDisplay.tsx
Normal file
179
frontend/src/passagesDisplay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,83 +1,4 @@
|
|||||||
|
.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%;
|
||||||
|
|
||||||
@@ -86,16 +7,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nextPassagesContainer .line:last-child {
|
|
||||||
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 {
|
||||||
@@ -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%);
|
|
||||||
}
|
|
131
frontend/src/passagesPanel.tsx
Normal file
131
frontend/src/passagesPanel.tsx
Normal 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 >
|
||||||
|
);
|
||||||
|
}
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution: '© <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>
|
||||||
);
|
);
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@@ -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":
|
||||||
|
Reference in New Issue
Block a user