🚨 Try to make TS linter happy
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.0",
|
||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-solid": "^0.9.3",
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import { Component } from 'solid-js';
|
||||
import { MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction, CustomEvent, IVisibilityActionRequest } from 'matrix-widget-api';
|
||||
|
||||
import { IVisibilityActionRequest, MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction } from 'matrix-widget-api';
|
||||
import { HopeProvider } from "@hope-ui/solid";
|
||||
|
||||
import { BusinessDataProvider } from './businessData';
|
||||
|
||||
import { SearchProvider } from './search';
|
||||
import { PassagesDisplay } from './passagesDisplay';
|
||||
import { StopsManager } from './stopsManager';
|
||||
@@ -28,7 +26,7 @@ const App: Component = () => {
|
||||
console.log("App: widgetId:" + widgetId);
|
||||
console.log("App: userId:" + userId);
|
||||
|
||||
const api = new WidgetApi(widgetId);
|
||||
const api = new WidgetApi(widgetId != null ? widgetId : undefined);
|
||||
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
||||
api.start();
|
||||
api.on("ready", function() {
|
||||
|
@@ -1,28 +1,37 @@
|
||||
import { createContext, createSignal } from 'solid-js';
|
||||
import { createContext, createSignal, JSX } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
|
||||
import { Passages, Stops } from './types';
|
||||
import { Line, Lines, Passage, Passages, Stop, Stops } from './types';
|
||||
|
||||
|
||||
interface Store {
|
||||
export interface BusinessDataStore {
|
||||
getLine: (lineId: string) => Promise<Line>;
|
||||
getLinePassages: (lineId: string) => Record<string, Passage[]>;
|
||||
|
||||
passages: () => Passages;
|
||||
getLinePassages?: (lineId: string) => Passages;
|
||||
addPassages?: (passages: Passages) => void;
|
||||
clearPassages?: () => void;
|
||||
refreshPassages: (stopId: number) => Promise<void>;
|
||||
addPassages: (passages: Passages) => void;
|
||||
clearPassages: () => void;
|
||||
|
||||
stops: () => Stops;
|
||||
addStops?: (stops: Stops) => void;
|
||||
getStop: (stopId: number) => Stop | undefined;
|
||||
searchStopByName: (name: string) => Promise<Stops>;
|
||||
};
|
||||
|
||||
export const BusinessDataContext = createContext<Store>();
|
||||
export const BusinessDataContext = createContext<BusinessDataStore>();
|
||||
|
||||
export function BusinessDataProvider(props: { children: JSX.Element }) {
|
||||
|
||||
const [serverUrl, setServerUrl] = createSignal<string>("https://localhost:4443");
|
||||
const [serverUrl] = createSignal<string>("https://localhost:4443");
|
||||
|
||||
const [store, setStore] = createStore({ lines: {}, passages: {}, stops: {} });
|
||||
type Store = {
|
||||
lines: Lines;
|
||||
passages: Passages;
|
||||
stops: Stops;
|
||||
};
|
||||
|
||||
const getLine: Line = async (lineId: string) => {
|
||||
const [store, setStore] = createStore<Store>({ lines: {}, passages: {}, stops: {} });
|
||||
|
||||
const getLine = async (lineId: string): Promise<Line> => {
|
||||
let line = store.lines[lineId];
|
||||
if (line === undefined) {
|
||||
console.log(`${lineId} not found... fetch it from backend.`);
|
||||
@@ -35,15 +44,15 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const getLinePassages = (lineId: string) => {
|
||||
const getLinePassages = (lineId: string): Record<string, Passage[]> => {
|
||||
return store.passages[lineId];
|
||||
};
|
||||
|
||||
const passages = () => {
|
||||
const passages = (): Passages => {
|
||||
return store.passages;
|
||||
}
|
||||
|
||||
const refreshPassages = async (stopId: number) => {
|
||||
const refreshPassages = async (stopId: number): Promise<void> => {
|
||||
const httpOptions = { headers: { "Content-Type": "application/json" } };
|
||||
console.log(`Fetching data for ${stopId}`);
|
||||
const data = await fetch(`${serverUrl()}/stop/nextPassages/${stopId}`, httpOptions);
|
||||
@@ -51,31 +60,30 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
||||
addPassages(response.passages);
|
||||
}
|
||||
|
||||
const addPassages = (passages) => {
|
||||
setStore((s) => {
|
||||
setStore('passages', passages);
|
||||
});
|
||||
const addPassages = (passages: Passages): void => {
|
||||
setStore('passages', passages);
|
||||
}
|
||||
|
||||
const clearPassages = () => {
|
||||
setStore((s) => {
|
||||
const clearPassages = (): void => {
|
||||
setStore((s: Store): Store => {
|
||||
for (const lineId of Object.keys(s.passages)) {
|
||||
setStore('passages', lineId, undefined);
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
const getStop = (stopId: int) => {
|
||||
const getStop = (stopId: number): Stop | undefined => {
|
||||
return store.stops[stopId];
|
||||
}
|
||||
|
||||
const searchStopByName = async (name: string) => {
|
||||
const searchStopByName = async (name: string): Promise<Stops> => {
|
||||
const data = await fetch(`${serverUrl()}/stop/?name=${name}`, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
const stops = await data.json();
|
||||
|
||||
const byIdStops = {};
|
||||
const byIdStops: Stops = {};
|
||||
for (const stop of stops) {
|
||||
byIdStops[stop.id] = stop;
|
||||
setStore('stops', stop.id, stop);
|
||||
@@ -85,10 +93,22 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
|
||||
|
||||
return (
|
||||
<BusinessDataContext.Provider value={{
|
||||
getLine, getLinePassages, passages, refreshPassages, clearPassages,
|
||||
getStop, searchStopByName
|
||||
getLine, getLinePassages, passages, refreshPassages, addPassages, clearPassages, getStop, searchStopByName
|
||||
}}>
|
||||
{props.children}
|
||||
</BusinessDataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export interface BusinessDataStore {
|
||||
getLine: (lineId: string) => Promise<Line>;
|
||||
getLinePassages: (lineId: string) => Record<string, Passage[]>;
|
||||
|
||||
passages: () => Passages;
|
||||
refreshPassages: (stopId: number) => Promise<void>;
|
||||
addPassages: (passages: Passages) => void;
|
||||
clearPassages: () => void;
|
||||
|
||||
getStop: (stopId: number) => Stop | undefined;
|
||||
searchStopByName: (name: string) => Promise<Stops>;
|
||||
};
|
||||
|
@@ -1,28 +1,43 @@
|
||||
import { Component, createEffect, createResource, createSignal, useContext } from "solid-js";
|
||||
import { createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent } 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 { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||
import { SearchContext, SearchStore } from "./search";
|
||||
|
||||
import { PassagesPanel } from "./passagesPanel";
|
||||
import { Passage, Passages } from "./types";
|
||||
import { getTransportModeSrc } from "./utils";
|
||||
import { PassagesPanel } from "./passagesPanel";
|
||||
|
||||
|
||||
import styles from "./passagesDisplay.module.css";
|
||||
|
||||
|
||||
export const PassagesDisplay: Component = () => {
|
||||
export const PassagesDisplay: ParentComponent = () => {
|
||||
|
||||
const maxPassagePerPanel = 5;
|
||||
const syncPeriodMsec = 20 * 1000;
|
||||
const panelSwitchPeriodMsec = 4 * 1000;
|
||||
|
||||
const { passages, getLine, getLinePassages, refreshPassages, clearPassages } = useContext(BusinessDataContext);
|
||||
|
||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||
// TODO: Use props instead
|
||||
const { getDisplayedStops } = useContext(SearchContext);
|
||||
const searchStore: SearchStore | undefined = useContext(SearchContext);
|
||||
|
||||
if (businessDataStore === undefined || searchStore === undefined)
|
||||
return <div />;
|
||||
|
||||
const { passages, getLine, getLinePassages, refreshPassages, clearPassages } = businessDataStore;
|
||||
const { getDisplayedStops } = searchStore;
|
||||
|
||||
const [displayedPanelId, setDisplayedPanelId] = createSignal<number>(0);
|
||||
const [panels, setPanels] = createStore([]);
|
||||
|
||||
type PositionnedPanel = {
|
||||
position: number;
|
||||
// TODO: Should be PassagesPanelComponent ?
|
||||
panel: JSX.Element;
|
||||
};
|
||||
const [panels, setPanels] = createStore<PositionnedPanel[]>([]);
|
||||
|
||||
const [dateNow] = createDateNow(1000);
|
||||
|
||||
@@ -32,7 +47,7 @@ export const PassagesDisplay: Component = () => {
|
||||
nextPanelId = 0;
|
||||
}
|
||||
setDisplayedPanelId(nextPanelId);
|
||||
}, 4000);
|
||||
}, panelSwitchPeriodMsec);
|
||||
|
||||
createEffect(() => {
|
||||
console.log("######### onStopIdUpdate #########");
|
||||
@@ -60,14 +75,22 @@ export const PassagesDisplay: Component = () => {
|
||||
);
|
||||
|
||||
// TODO: Sort transport modes by weight
|
||||
const Header: Component = (props) => {
|
||||
const computeTransportModes = async (lineIds: Array<number>) => {
|
||||
const Header: VoidComponent<{ passages: Passages, title: string }> = (props) => {
|
||||
|
||||
const computeTransportModes = async (lineIds: string[]): Promise<string[]> => {
|
||||
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
|
||||
return new Set(lines.map((line) => getTransportModeSrc(line.transportMode, false)));
|
||||
const urls: Set<string> = new Set();
|
||||
for (const line of lines) {
|
||||
const src = getTransportModeSrc(line.transportMode, false);
|
||||
if (src !== undefined) {
|
||||
urls.add(src);
|
||||
}
|
||||
}
|
||||
return Array.from(urls);
|
||||
}
|
||||
|
||||
const [linesIds, setLinesIds] = createSignal([]);
|
||||
const [transportModeUrls] = createResource(linesIds, computeTransportModes);
|
||||
const [linesIds, setLinesIds] = createSignal<string[]>([]);
|
||||
const [transportModeUrls] = createResource<string[], string[]>(linesIds, computeTransportModes);
|
||||
|
||||
createEffect(() => {
|
||||
setLinesIds(Object.keys(props.passages));
|
||||
@@ -76,7 +99,7 @@ export const PassagesDisplay: Component = () => {
|
||||
return (
|
||||
<div class={styles.header}>
|
||||
<Show when={transportModeUrls() !== undefined} >
|
||||
<For each={Array.from(transportModeUrls())}>
|
||||
<For each={transportModeUrls()}>
|
||||
{(url) =>
|
||||
<div class={styles.transportMode}>
|
||||
<img src={url} />
|
||||
@@ -85,14 +108,14 @@ export const PassagesDisplay: Component = () => {
|
||||
</For>
|
||||
</Show>
|
||||
<div class={styles.title}>
|
||||
<svg viewbox="0 0 1260 50">
|
||||
<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">
|
||||
<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>
|
||||
@@ -102,12 +125,12 @@ export const PassagesDisplay: Component = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Footer: Component = (props) => {
|
||||
const Footer: VoidComponent<{ panels: PositionnedPanel[] }> = (props) => {
|
||||
return (
|
||||
<div class={styles.footer}>
|
||||
<For each={props.panels}>
|
||||
{(positioned) => {
|
||||
const { position } = positioned;
|
||||
{(panel) => {
|
||||
const position = panel.position;
|
||||
return (
|
||||
<div>
|
||||
<svg viewBox="0 0 29 29">
|
||||
@@ -131,10 +154,10 @@ export const PassagesDisplay: Component = () => {
|
||||
setPanels([]);
|
||||
|
||||
let newPanels = [];
|
||||
let positioneds = [];
|
||||
let positioneds: PositionnedPanel[] = [];
|
||||
let index = 0;
|
||||
|
||||
let chunk = {};
|
||||
let chunk: Record<string, Record<string, Passage[]>> = {};
|
||||
let chunkSize = 0;
|
||||
|
||||
console.log("passages=", passages());
|
||||
@@ -154,7 +177,7 @@ export const PassagesDisplay: Component = () => {
|
||||
const panelid = index++;
|
||||
const panel = <PassagesPanel show={panelid == displayedPanelId()} passages={store} />;
|
||||
newPanels.push(panel);
|
||||
positioneds.push({ position: panelid, panel });
|
||||
positioneds.push({ position: panelid, panel: panel });
|
||||
|
||||
chunk = {};
|
||||
chunk[lineId] = byLinePassages;
|
||||
@@ -166,7 +189,7 @@ export const PassagesDisplay: Component = () => {
|
||||
const [store] = createStore(chunk);
|
||||
const panel = <PassagesPanel show={panelId == displayedPanelId()} passages={store} />;
|
||||
newPanels.push(panel);
|
||||
positioneds.push({ position: panelId, panel });
|
||||
positioneds.push({ position: panelId, panel: panel });
|
||||
}
|
||||
|
||||
setPanels(positioneds);
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import { Component, createEffect, createResource, createSignal, useContext } from 'solid-js';
|
||||
import { VoidComponent, createEffect, createResource, createSignal, ParentComponent, ParentProps, Show, useContext } from 'solid-js';
|
||||
import { createDateNow, getTime } from '@solid-primitives/date';
|
||||
import { AnimationOptions } from '@motionone/types';
|
||||
import { Motion } from "@motionone/solid";
|
||||
|
||||
import { TrafficStatus } from './types';
|
||||
import { Line, Passage, Passages, TrafficStatus } from './types';
|
||||
import { renderLineTransportMode, renderLinePicto } from './utils';
|
||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||
|
||||
import { BusinessDataContext } from "./businessData";
|
||||
|
||||
import styles from "./passagesPanel.module.css";
|
||||
import styles from './passagesPanel.module.css';
|
||||
|
||||
|
||||
const TtwPassage: Component = (props) => {
|
||||
const TtwPassage: VoidComponent<{ passage: Passage, style: string, fontSize: number }> = (props) => {
|
||||
|
||||
const [dateNow] = createDateNow(5000);
|
||||
|
||||
@@ -18,6 +18,7 @@ const TtwPassage: Component = (props) => {
|
||||
const ttwSec = refTs - (getTime(dateNow()) / 1000);
|
||||
const isApproaching = ttwSec <= 60;
|
||||
|
||||
const transition: AnimationOptions = { duration: 3, repeat: Infinity };
|
||||
return (
|
||||
<div class={props.style}>
|
||||
<svg viewBox={`0 0 215 ${props.fontSize}`}>
|
||||
@@ -25,9 +26,9 @@ const TtwPassage: Component = (props) => {
|
||||
x="100%" y="55%"
|
||||
dominant-baseline="middle" text-anchor="end"
|
||||
font-size={props.fontSize} style={{ fill: "#000000" }}
|
||||
initial={isApproaching}
|
||||
initial={isApproaching ? undefined : false}
|
||||
animate={{ opacity: [1, 0, 1] }}
|
||||
transition={{ duration: 3, repeat: Infinity }}>
|
||||
transition={transition}>
|
||||
{Math.floor(ttwSec / 60)} min
|
||||
</Motion.text>
|
||||
</svg>
|
||||
@@ -35,12 +36,12 @@ const TtwPassage: Component = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const UnavailablePassage: Component = (props) => {
|
||||
const UnavailablePassage: VoidComponent<{ style: string }> = (props) => {
|
||||
const textStyle = { fill: "#000000" };
|
||||
|
||||
return (
|
||||
<div class={props.style}>
|
||||
<svg viewbox="0 0 230 110">
|
||||
<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>
|
||||
@@ -50,7 +51,7 @@ const UnavailablePassage: Component = (props) => {
|
||||
}
|
||||
|
||||
/* TODO: Manage end of service */
|
||||
const Passages: Component = (props) => {
|
||||
const DestinationPassages: VoidComponent<{ passages: Passage[], line: Line, destination: string }> = (props) => {
|
||||
|
||||
/* TODO: Find where to get data to compute traffic status. */
|
||||
const trafficStatusColor = new Map<TrafficStatus, string>([
|
||||
@@ -64,7 +65,10 @@ const Passages: Component = (props) => {
|
||||
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) };
|
||||
|
||||
// TODO: Manage traffic status
|
||||
// const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
|
||||
const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) };
|
||||
|
||||
return (
|
||||
<div class={styles.line}>
|
||||
@@ -73,7 +77,7 @@ const Passages: Component = (props) => {
|
||||
</div>
|
||||
{renderLinePicto(props.line, styles)}
|
||||
<div class={styles.destination}>
|
||||
<svg viewbox="0 0 600 40">
|
||||
<svg viewBox="0 0 600 40">
|
||||
<text x="0" y="50%" dominant-baseline="middle" font-size="40" style={{ fill: "#000000" }}>
|
||||
{props.destination}
|
||||
</text>
|
||||
@@ -85,33 +89,39 @@ const Passages: Component = (props) => {
|
||||
</svg>
|
||||
</div>
|
||||
<Show when={firstPassage !== undefined} fallback=<UnavailablePassage style={styles.unavailableFirstPassage} />>
|
||||
<TtwPassage style={styles.firstPassage} passage={firstPassage} fontSize="50" />
|
||||
<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" />
|
||||
<TtwPassage style={styles.secondPassage} passage={secondPassage} fontSize={45} />
|
||||
</Show>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
export const PassagesPanel: Component = (props) => {
|
||||
export type PassagesPanelComponentProps = ParentProps & { passages: Passages, show: boolean };
|
||||
export type PassagesPanelComponent = ParentComponent<PassagesPanelComponentProps>;
|
||||
export const PassagesPanel: PassagesPanelComponent = (props) => {
|
||||
|
||||
const { getLine } = useContext(BusinessDataContext);
|
||||
const businessDataContext: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||
if (businessDataContext === undefined)
|
||||
return <div />;
|
||||
|
||||
const getLines = async (lineIds: Array<number>) => {
|
||||
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
|
||||
const { getLine } = businessDataContext;
|
||||
|
||||
const getLines = async (lineIds: string[]): Promise<Line[]> => {
|
||||
const lines = await Promise.all<Promise<Line>[]>(lineIds.map((lineId) => getLine(lineId)));
|
||||
return lines;
|
||||
}
|
||||
|
||||
const [lineIds, setLinesIds] = createSignal([]);
|
||||
const [lines] = createResource(lineIds, getLines);
|
||||
const [lineIds, setLinesIds] = createSignal<string[]>([]);
|
||||
const [lines] = createResource<Line[], string[]>(lineIds, getLines);
|
||||
|
||||
createEffect(async () => {
|
||||
setLinesIds(Object.keys(props.passages));
|
||||
});
|
||||
|
||||
return (
|
||||
<div classList={{ [styles.passagesContainer]: true, [styles.displayed]: props.show }} style={{ "top": `${100 * props.position}%` }}>
|
||||
<div classList={{ [styles.passagesContainer]: true, [styles.displayed]: props.show }} >
|
||||
<Show when={lines() !== undefined} >
|
||||
{() => {
|
||||
const ret = [];
|
||||
@@ -119,7 +129,7 @@ export const PassagesPanel: Component = (props) => {
|
||||
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} />);
|
||||
ret.push(<DestinationPassages passages={byLinePassages[destination]} line={line} destination={destination} />);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,35 +1,39 @@
|
||||
import { batch, createContext } from 'solid-js';
|
||||
import { createContext, JSX } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Marker as LeafletMarker } from 'leaflet';
|
||||
|
||||
import { Stop, Stops } from './types';
|
||||
import { Stop } from './types';
|
||||
|
||||
|
||||
interface Store {
|
||||
getMarkers: () => Markers;
|
||||
addMarkers?: (stopId, markers) => void;
|
||||
setMarkers?: (markers) => void;
|
||||
export type ByStopIdMarkers = Record<number, LeafletMarker[] | undefined>;
|
||||
|
||||
getStops: () => Stops;
|
||||
setStops?: (stops) => void;
|
||||
removeStops?: (stopIds: Array<number>) => void;
|
||||
export interface SearchStore {
|
||||
getDisplayedStops: () => Stop[];
|
||||
setDisplayedStops: (stops: Stop[]) => void;
|
||||
|
||||
getDisplayedStops: () => Array<Stop>;
|
||||
setDisplayedStops: (stops: Array<Stop>) => void;
|
||||
addMarkers: (stopId: number, markers: LeafletMarker[]) => void;
|
||||
};
|
||||
|
||||
export const SearchContext = createContext<Store>();
|
||||
export const SearchContext = createContext<SearchStore>();
|
||||
|
||||
export function SearchProvider(props: { children: JSX.Element }) {
|
||||
|
||||
const [store, setStore] = createStore({ stops: {}, markers: {}, displayedStops: [] });
|
||||
type Store = {
|
||||
stops: Record<number, Stop | undefined>;
|
||||
markers: ByStopIdMarkers;
|
||||
displayedStops: Stop[];
|
||||
};
|
||||
|
||||
const getDisplayedStops = () => {
|
||||
const [store, setStore] = createStore<Store>({ stops: {}, markers: {}, displayedStops: [] });
|
||||
|
||||
const getDisplayedStops = (): Stop[] => {
|
||||
return store.displayedStops;
|
||||
}
|
||||
|
||||
const setDisplayedStops = (stops: Array<Stop>) => {
|
||||
setStore((s) => {
|
||||
const setDisplayedStops = (stops: Stop[]): void => {
|
||||
setStore((s: Store) => {
|
||||
setStore('displayedStops', stops);
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +50,7 @@ export function SearchProvider(props: { children: JSX.Element }) {
|
||||
return store.markers;
|
||||
}
|
||||
|
||||
const addMarkers = (stopId: number, markers) => {
|
||||
const addMarkers = (stopId: number, markers: L.Marker[]): void => {
|
||||
setStore('markers', stopId, markers);
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,15 @@
|
||||
import { Component, createEffect, createResource, createSignal, onMount, Show, useContext } from 'solid-js';
|
||||
import { createEffect, createResource, createSignal, For, JSX, onMount, Show, useContext, VoidComponent } from 'solid-js';
|
||||
|
||||
import { Box, Button, Input, InputLeftAddon, InputGroup, HStack, List, ListItem, Progress, ProgressIndicator, VStack } from "@hope-ui/solid";
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L from 'leaflet';
|
||||
|
||||
import { BusinessDataContext } from './businessData';
|
||||
import { SearchContext } from './search';
|
||||
import {
|
||||
featureGroup as leafletFeatureGroup, LatLngLiteral as LeafletLatLngLiteral, Map as LeafletMap,
|
||||
Marker as LeafletMarker, tileLayer as leafletTileLayer
|
||||
} from 'leaflet';
|
||||
|
||||
import { BusinessDataContext, BusinessDataStore } from "./businessData";
|
||||
import { SearchContext, SearchStore } from './search';
|
||||
|
||||
import { Stop } from './types';
|
||||
import { renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils';
|
||||
@@ -13,13 +17,15 @@ import { renderLineTransportMode, renderLinePicto, TransportModeWeights } from '
|
||||
import styles from './stopManager.module.css';
|
||||
|
||||
|
||||
const StopRepr: Component = (props) => {
|
||||
const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
||||
|
||||
const { getLine } = useContext(BusinessDataContext);
|
||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||
if (businessDataStore === undefined)
|
||||
return <div />;
|
||||
|
||||
const [lineReprs] = createResource(props.stop.lines, fetchLinesRepr);
|
||||
const { getLine } = businessDataStore;
|
||||
|
||||
const fetchLinesRepr = async (lineIds: Array<string>) => {
|
||||
const fetchLinesRepr = async (lineIds: string[]): Promise<JSX.Element[]> => {
|
||||
const reprs = [];
|
||||
for (const lineId of lineIds) {
|
||||
const line = await getLine(lineId);
|
||||
@@ -31,26 +37,37 @@ const StopRepr: Component = (props) => {
|
||||
return reprs;
|
||||
}
|
||||
|
||||
const [lineReprs] = createResource<JSX.Element[], string[]>(props.stop.lines, fetchLinesRepr);
|
||||
|
||||
return (
|
||||
<HStack height="100%">
|
||||
{props.stop.name}
|
||||
<For each={lineReprs()}>{(line) => line}</For>
|
||||
<For each={lineReprs()}>{(line: JSX.Element) => line}</For>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
const StopAreaRepr: Component = (props) => {
|
||||
const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
|
||||
|
||||
const { getLine } = useContext(BusinessDataContext);
|
||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||
if (businessDataStore === undefined)
|
||||
return <div />;
|
||||
|
||||
const fetchLinesRepr = async (stop: Stop) => {
|
||||
const { getLine } = businessDataStore;
|
||||
|
||||
type ByTransportModeReprs = {
|
||||
mode: JSX.Element | undefined;
|
||||
[key: string]: JSX.Element | JSX.Element[] | undefined;
|
||||
};
|
||||
|
||||
const fetchLinesRepr = async (stop: Stop): Promise<JSX.Element[]> => {
|
||||
const lineIds = new Set(stop.lines);
|
||||
const stops = stop.stops;
|
||||
for (const stop of stops) {
|
||||
stop.lines.forEach(lineIds.add, lineIds);
|
||||
}
|
||||
|
||||
const byModeReprs = {};
|
||||
const byModeReprs: Record<string, ByTransportModeReprs> = {};
|
||||
for (const lineId of lineIds) {
|
||||
const line = await getLine(lineId);
|
||||
if (line !== undefined) {
|
||||
@@ -64,7 +81,7 @@ const StopAreaRepr: Component = (props) => {
|
||||
}
|
||||
|
||||
const reprs = [];
|
||||
const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < TransportModeWeights[y]);
|
||||
const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < TransportModeWeights[y] ? 1 : -1);
|
||||
for (const transportMode of sortedTransportModes) {
|
||||
const lines = byModeReprs[transportMode];
|
||||
const repr = [lines.mode];
|
||||
@@ -88,29 +105,34 @@ const StopAreaRepr: Component = (props) => {
|
||||
}
|
||||
|
||||
|
||||
const Map: Component = (props) => {
|
||||
const Map: VoidComponent<{ stops: Stop[] }> = (props) => {
|
||||
|
||||
const mapCenter = [48.853, 2.35];
|
||||
const mapCenter: LeafletLatLngLiteral = { lat: 48.853, lng: 2.35 };
|
||||
|
||||
const searchStore: SearchStore | undefined = useContext(SearchContext);
|
||||
if (searchStore === undefined)
|
||||
return <div />;
|
||||
|
||||
const { addMarkers } = searchStore;
|
||||
|
||||
const { addMarkers } = useContext(SearchContext);
|
||||
|
||||
let mapDiv: any;
|
||||
let map = null;
|
||||
const stopsLayerGroup = L.featureGroup();
|
||||
let map: LeafletMap | undefined = undefined;
|
||||
const stopsLayerGroup = leafletFeatureGroup();
|
||||
|
||||
const buildMap = (div: HTMLDivElement) => {
|
||||
map = L.map(div).setView(mapCenter, 11);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
map = new LeafletMap(div).setView(mapCenter, 11);
|
||||
leafletTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
stopsLayerGroup.addTo(map);
|
||||
}
|
||||
|
||||
const setMarker = (stop: Stop): Array<L.Marker> => {
|
||||
const setMarker = (stop: Stop): L.Marker[] => {
|
||||
const markers = [];
|
||||
if (stop.lat !== undefined && stop.lon !== undefined) {
|
||||
/* TODO: Add stop lines representation to popup. */
|
||||
markers.push(L.marker([stop.lat, stop.lon]).bindPopup(`${stop.name}`).openPopup());
|
||||
markers.push(new LeafletMarker([stop.lat, stop.lon]).bindPopup(`${stop.name}`).openPopup());
|
||||
}
|
||||
else {
|
||||
for (const _stop of stop.stops) {
|
||||
@@ -135,7 +157,7 @@ const Map: Component = (props) => {
|
||||
}
|
||||
|
||||
const stopsBound = stopsLayerGroup.getBounds();
|
||||
if (Object.keys(stopsBound).length) {
|
||||
if (map !== undefined && Object.keys(stopsBound).length) {
|
||||
map.fitBounds(stopsBound);
|
||||
}
|
||||
});
|
||||
@@ -143,18 +165,26 @@ const Map: Component = (props) => {
|
||||
return <div ref={mapDiv} id='main-map' style={{ width: "100%", height: "100%" }} />;
|
||||
}
|
||||
|
||||
export const StopsManager: Component = () => {
|
||||
export const StopsManager: VoidComponent = () => {
|
||||
|
||||
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
|
||||
const searchStore: SearchStore | undefined = useContext(SearchContext);
|
||||
|
||||
if (businessDataStore === undefined || searchStore === undefined)
|
||||
return <div />;
|
||||
|
||||
const { searchStopByName } = businessDataStore;
|
||||
const { setDisplayedStops } = searchStore;
|
||||
|
||||
// TODO: Use props instead
|
||||
const [minCharactersNb] = createSignal<number>(4);
|
||||
|
||||
const [minCharactersNb, setMinCharactersNb] = createSignal<number>(4);
|
||||
const [inProgress, setInProgress] = createSignal<boolean>(false);
|
||||
const [foundStops, setFoundStops] = createSignal<Array<number>>([]);
|
||||
const [foundStops, setFoundStops] = createSignal<Stop[]>([]);
|
||||
|
||||
const { getStop, searchStopByName } = useContext(BusinessDataContext);
|
||||
const { setDisplayedStops } = useContext(SearchContext);
|
||||
|
||||
const onStopNameInput = async (event) => {
|
||||
const onStopNameInput: JSX.EventHandler<HTMLInputElement, InputEvent> = async (event): Promise<void> => {
|
||||
/* TODO: Add a tempo before fetching stop for giving time to user to finish his request */
|
||||
const stopName = event.target.value;
|
||||
const stopName = event.currentTarget.value;
|
||||
if (stopName.length >= minCharactersNb()) {
|
||||
console.log(`Fetching data for ${stopName}`);
|
||||
setInProgress(true);
|
||||
|
@@ -6,12 +6,10 @@ export enum TrafficStatus {
|
||||
BYPASSED
|
||||
}
|
||||
|
||||
export class Passages { };
|
||||
|
||||
export class Passage {
|
||||
line: number;
|
||||
operator: string;
|
||||
destinations: Array<string>;
|
||||
destinations: string[];
|
||||
atStop: boolean;
|
||||
aimedArrivalTs: number;
|
||||
expectedArrivalTs: number;
|
||||
@@ -21,7 +19,7 @@ export class Passage {
|
||||
arrivalStatus: string;
|
||||
departStatus: string;
|
||||
|
||||
constructor(line: number, operator: string, destinations: Array<string>, atStop: boolean, aimedArrivalTs: number,
|
||||
constructor(line: number, operator: string, destinations: string[], atStop: boolean, aimedArrivalTs: number,
|
||||
expectedArrivalTs: number, arrivalPlatformName: string, aimedDepartTs: number, expectedDepartTs: number,
|
||||
arrivalStatus: string, departStatus: string) {
|
||||
this.line = line;
|
||||
@@ -38,7 +36,7 @@ export class Passage {
|
||||
}
|
||||
};
|
||||
|
||||
export class Stops { };
|
||||
export type Passages = Record<string, Record<string, Passage[]>>;
|
||||
|
||||
export class Stop {
|
||||
id: number;
|
||||
@@ -46,10 +44,10 @@ export class Stop {
|
||||
town: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
stops: Array<Stop>;
|
||||
lines: Array<string>;
|
||||
stops: Stop[];
|
||||
lines: string[];
|
||||
|
||||
constructor(id: number, name: string, town: string, lat: number, lon: number, stops: Array<Stop>, lines: Array<string>) {
|
||||
constructor(id: number, name: string, town: string, lat: number, lon: number, stops: Stop[], lines: string[]) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.town = town;
|
||||
@@ -63,6 +61,8 @@ export class Stop {
|
||||
}
|
||||
};
|
||||
|
||||
export type Stops = Record<number, Stop>;
|
||||
|
||||
export class Line {
|
||||
id: string;
|
||||
shortName: string;
|
||||
@@ -75,11 +75,11 @@ export class Line {
|
||||
accessibility: boolean;
|
||||
visualSignsAvailable: string; // TODO: Use an enum
|
||||
audibleSignsAvailable: string; // TODO: Use an enum
|
||||
stopIds: Array<number>;
|
||||
stopIds: 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>) {
|
||||
audibleSignsAvailable: string, stopIds: number[]) {
|
||||
this.id = id;
|
||||
this.shortName = shortName;
|
||||
this.name = name;
|
||||
@@ -94,3 +94,5 @@ export class Line {
|
||||
this.stopIds = stopIds;
|
||||
}
|
||||
};
|
||||
|
||||
export type Lines = Record<string, Line>;
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { JSX } from 'solid-js';
|
||||
|
||||
import { Line } from './types';
|
||||
|
||||
const validTransportModes = ["bus", "tram", "metro", "rer", "transilien", "funicular", "ter", "unknown"];
|
||||
|
||||
export const TransportModeWeights = {
|
||||
export const TransportModeWeights: Record<string, number> = {
|
||||
bus: 1,
|
||||
tram: 2,
|
||||
val: 3,
|
||||
@@ -13,20 +15,19 @@ export const TransportModeWeights = {
|
||||
ter: 8,
|
||||
};
|
||||
|
||||
export function renderLineTransportMode(line: Line): JSX.Element {
|
||||
return <img src={getTransportModeSrc(line.transportMode)} />
|
||||
}
|
||||
|
||||
export function getTransportModeSrc(mode: string, color: boolean = true): string | null {
|
||||
let ret = null;
|
||||
export function getTransportModeSrc(mode: string, color: boolean = true): string | undefined {
|
||||
let ret = undefined;
|
||||
if (validTransportModes.includes(mode)) {
|
||||
ret = `/public/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function renderLineTransportMode(line: Line): JSX.Element {
|
||||
return <img src={getTransportModeSrc(line.transportMode)} />
|
||||
}
|
||||
|
||||
function renderBusLinePicto(line: Line, styles): JSX.Element {
|
||||
function renderBusLinePicto(line: Line, styles: CSSModuleClasses): JSX.Element {
|
||||
return (
|
||||
<div class={styles.busLinePicto}>
|
||||
<svg viewBox="0 0 31.5 14">
|
||||
@@ -44,7 +45,7 @@ function renderBusLinePicto(line: Line, styles): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function renderTramLinePicto(line: Line, styles): JSX.Element {
|
||||
function renderTramLinePicto(line: Line, styles: CSSModuleClasses): JSX.Element {
|
||||
const lineStyle = { fill: `#${line.backColorHexa}` };
|
||||
return (
|
||||
<div class={styles.tramLinePicto}>
|
||||
@@ -64,10 +65,10 @@ function renderTramLinePicto(line: Line, styles): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function renderMetroLinePicto(line: Line, styles): JSX.Element {
|
||||
function renderMetroLinePicto(line: Line, styles: CSSModuleClasses): JSX.Element {
|
||||
return (
|
||||
<div class={styles.metroLinePicto}>
|
||||
<svg viewbox="0 0 20 20">
|
||||
<svg viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="10" r="10" style={{ fill: `#${line.backColorHexa}` }} />
|
||||
<text x="50%"
|
||||
y="55%"
|
||||
@@ -81,10 +82,10 @@ function renderMetroLinePicto(line: Line, styles): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function renderTrainLinePicto(line: Line, styles): JSX.Element {
|
||||
function renderTrainLinePicto(line: Line, styles: CSSModuleClasses): JSX.Element {
|
||||
return (
|
||||
<div class={styles.trainLinePicto}>
|
||||
<svg viewbox="0 0 20 20">
|
||||
<svg viewBox="0 0 20 20">
|
||||
<rect x="0" y="0" width="20" height="20" rx="4.5" ry="4.5" style={{ fill: `#${line.backColorHexa}` }} />
|
||||
<text x="50%"
|
||||
y="55%"
|
||||
@@ -99,7 +100,7 @@ function renderTrainLinePicto(line: Line, styles): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export function renderLinePicto(line: Line, styles): JSX.Element {
|
||||
export function renderLinePicto(line: Line, styles: CSSModuleClasses): JSX.Element {
|
||||
switch (line.transportMode) {
|
||||
case "bus":
|
||||
case "funicular":
|
||||
|
@@ -15,7 +15,8 @@
|
||||
{
|
||||
"name": "typescript-eslint-language-service"
|
||||
}
|
||||
]
|
||||
],
|
||||
"lib": ["ES2021", "DOM"],
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user