8 Commits

28 changed files with 1941 additions and 2017 deletions

View File

@@ -1,10 +1,7 @@
from asyncio import sleep from asyncio import sleep
from logging import getLogger from logging import getLogger
from typing import Annotated, AsyncIterator
from fastapi import Depends
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from sqlalchemy import text
from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.exc import OperationalError, SQLAlchemyError
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (
async_sessionmaker, async_sessionmaker,
@@ -16,6 +13,7 @@ from sqlalchemy.ext.asyncio import (
from .base_class import Base from .base_class import Base
from ..settings import DatabaseSettings from ..settings import DatabaseSettings
logger = getLogger(__name__) logger = getLogger(__name__)
@@ -30,8 +28,7 @@ class Database:
except (SQLAlchemyError, AttributeError) as e: except (SQLAlchemyError, AttributeError) as e:
logger.exception(e) logger.exception(e)
raise
return None
# TODO: Preserve UserLastStopSearchResults table from drop. # TODO: Preserve UserLastStopSearchResults table from drop.
async def connect( async def connect(

View File

@@ -60,7 +60,7 @@ types-aiofiles = "^22.1.0.2"
wrapt = "^1.14.1" wrapt = "^1.14.1"
pydocstyle = "^6.2.2" pydocstyle = "^6.2.2"
dill = "^0.3.6" dill = "^0.3.6"
python-lsp-ruff = "^1.0.5" python-lsp-ruff = "^2.1.0"
python-lsp-server = "^1.7.1" python-lsp-server = "^1.7.1"
autopep8 = "^2.0.1" autopep8 = "^2.0.1"
pyflakes = "^3.0.1" pyflakes = "^3.0.1"

View File

@@ -30,8 +30,9 @@
"@stitches/core": "^1.2.8", "@stitches/core": "^1.2.8",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"matrix-widget-api": "^1.1.1", "matrix-widget-api": "^1.1.1",
"ol": "^7.3.0", "ol": "^8.2.0",
"solid-js": "^1.6.6", "solid-js": "^1.6.6",
"solid-transition-group": "^0.0.10" "solid-transition-group": "^0.0.10",
"solidjs-lazily": "^0.1.2"
} }
} }

View File

@@ -1,15 +1,12 @@
import { Component, createSignal } from 'solid-js'; import { Component, createSignal, onCleanup, onMount } from 'solid-js';
import { IVisibilityActionRequest, MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction } from 'matrix-widget-api'; // import { IVisibilityActionRequest, MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction } from 'matrix-widget-api';
import { HopeProvider } from "@hope-ui/solid";
import { BusinessDataProvider } from './businessData'; import { BusinessDataProvider } from './businessData';
import { AppContextProvider } from './appContext'; import { AppContextProvider } from './appContext';
import { PassagesDisplay } from './passagesDisplay'; import { PassagesDisplay } from './passagesDisplay';
import { StopsSearchMenu } from './stopsSearchMenu/stopsSearchMenu'; import { StopsSearchMenu } from './stopsSearchMenu/stopsSearchMenu';
import "./App.scss"; import "./App.scss";
import { onCleanup, onMount } from 'solid-js';
function parseFragment() { function parseFragment() {
@@ -28,21 +25,21 @@ const App: Component = () => {
console.log("App: widgetId:" + widgetId); console.log("App: widgetId:" + widgetId);
console.log("App: userId:" + userId); console.log("App: userId:" + userId);
const api = new WidgetApi(widgetId != null ? widgetId : undefined); // const api = new WidgetApi(widgetId != null ? widgetId : undefined);
api.requestCapability(MatrixCapabilities.AlwaysOnScreen); // api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
api.start(); // api.start();
api.on("ready", function() { // api.on("ready", function() {
console.log("App: widget API is READY !!!!"); // console.log("App: widget API is READY !!!!");
}); // });
// Seems to don´t be used... // Seems to don´t be used...
api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent<IVisibilityActionRequest>) => { // api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent<IVisibilityActionRequest>) => {
console.log("App: Visibility change"); // console.log("App: Visibility change");
ev.preventDefault(); // we're handling it, so stop the widget API from doing something. // ev.preventDefault(); // we're handling it, so stop the widget API from doing something.
console.log("App: ", ev.detail); // custom handling here // console.log("App: ", ev.detail); // custom handling here
/* api.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); */ // /* api.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); */
api.transport.reply(ev.detail, {}); // api.transport.reply(ev.detail, {});
}); // });
createSignal({ createSignal({
height: window.innerHeight, height: window.innerHeight,
@@ -71,10 +68,8 @@ const App: Component = () => {
window.removeEventListener('resize', onResize); window.removeEventListener('resize', onResize);
}) })
return ( return <BusinessDataProvider>
<BusinessDataProvider>
<AppContextProvider> <AppContextProvider>
<HopeProvider>
<div class="App"> <div class="App">
<div class="panel"> <div class="panel">
<StopsSearchMenu /> <StopsSearchMenu />
@@ -83,10 +78,8 @@ const App: Component = () => {
<PassagesDisplay /> <PassagesDisplay />
</div> </div>
</div> </div>
</HopeProvider>
</AppContextProvider> </AppContextProvider>
</BusinessDataProvider> </BusinessDataProvider>;
);
}; };
export default App; export default App;

View File

@@ -6,7 +6,7 @@ import { Stop } from './types';
export interface AppContextStore { export interface AppContextStore {
getDisplayedStops: () => Stop[]; getDisplayedStops: () => Stop[];
setDisplayedStops: (stops: Stop[]) => void; setDisplayedStops: (stops: Stop[]) => void;
}; }
export const AppContextContext = createContext<AppContextStore>(); export const AppContextContext = createContext<AppContextStore>();
@@ -26,10 +26,7 @@ export function AppContextProvider(props: { children: JSX.Element }) {
const setDisplayedStops = (stops: Stop[]): void => { const setDisplayedStops = (stops: Stop[]): void => {
console.log("setDisplayedStops=", stops); console.log("setDisplayedStops=", stops);
// setStore((s: Store) => {
setStore('displayedStops', stops); setStore('displayedStops', stops);
// return s;
// });
} }
return ( return (
@@ -39,5 +36,4 @@ export function AppContextProvider(props: { children: JSX.Element }) {
{props.children} {props.children}
</AppContextContext.Provider> </AppContextContext.Provider>
); );
}
};

View File

@@ -6,6 +6,7 @@ import { Line, Lines, Passage, Passages, Stop, StopShape, StopShapes, Stops } fr
export type StopDestinations = Record<string, string[]>; export type StopDestinations = Record<string, string[]>;
export interface BusinessDataStore { export interface BusinessDataStore {
getLine: (lineId: string) => Promise<Line>; getLine: (lineId: string) => Promise<Line>;
getLinePassages: (lineId: string) => Record<string, Passage[]>; getLinePassages: (lineId: string) => Record<string, Passage[]>;
@@ -23,7 +24,7 @@ export interface BusinessDataStore {
getStopDestinations: (stopId: number) => Promise<StopDestinations | undefined>; getStopDestinations: (stopId: number) => Promise<StopDestinations | undefined>;
getStopShape: (stopId: number) => Promise<StopShape | undefined>; getStopShape: (stopId: number) => Promise<StopShape | undefined>;
}; }
export const BusinessDataContext = createContext<BusinessDataStore>(); export const BusinessDataContext = createContext<BusinessDataStore>();
@@ -120,7 +121,7 @@ export function BusinessDataProvider(props: { children: JSX.Element }) {
for (const lineId of Object.keys(passages)) { for (const lineId of Object.keys(passages)) {
const newLinePassages = passages[lineId]; const newLinePassages = passages[lineId];
const linePassages = storePassages[lineId]; const linePassages = storePassages[lineId];
if (linePassages === undefined) { if (linePassages === undefined || Object.keys(linePassages).length == 0) {
setStore('passages', lineId, newLinePassages); setStore('passages', lineId, newLinePassages);
} }
else { else {
@@ -159,7 +160,7 @@ ${linePassagesDestination.length} here... refresh all them.`);
const clearPassages = (): void => { const clearPassages = (): void => {
setStore((s: Store): Store => { setStore((s: Store): Store => {
for (const lineId of Object.keys(s.passages)) { for (const lineId of Object.keys(s.passages)) {
setStore('passages', lineId, undefined); setStore('passages', lineId, {});
} }
return s; return s;
}); });
@@ -231,21 +232,9 @@ ${linePassagesDestination.length} here... refresh all them.`);
getLine, getLinePassages, getLineDestinations, getDestinationPassages, passages, getPassagesLineIds, getLine, getLinePassages, getLineDestinations, getDestinationPassages, passages, getPassagesLineIds,
refreshPassages, addPassages, clearPassages, refreshPassages, addPassages, clearPassages,
getStop, getStopDestinations, getStopShape, searchStopByName getStop, getStopDestinations, getStopShape, searchStopByName
}}> }}>
{props.children} {props.children}
</BusinessDataContext.Provider> </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>;
};

View File

@@ -4,10 +4,11 @@ import { VoidComponent } from "solid-js";
export const IconHamburgerMenu: VoidComponent<{}> = () => { export const IconHamburgerMenu: VoidComponent<{}> = () => {
return ( return (
<svg class="iconHamburgerMenu" viewBox="0 0 15 15"> <svg class="iconHamburgerMenu" viewBox="0 0 15 15">
<path d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 <path
13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386
8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5
13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761
13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z"
fill="currentColor" fill="currentColor"
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"

View File

@@ -14,6 +14,7 @@
} }
html, body { html, body {
height: 100vh;
aspect-ratio: 16/9; aspect-ratio: 16/9;
margin: 0; margin: 0;

View File

@@ -1,7 +1,6 @@
import { createContext, createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent } from "solid-js"; import { createContext, createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent } from "solid-js";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { createDateNow } from "@solid-primitives/date"; import { createDateNow } from "@solid-primitives/date";
import { IconButton, Menu, MenuTrigger, MenuContent, MenuItem } from "@hope-ui/solid";
import { format } from "date-fns"; import { format } from "date-fns";
import { BusinessDataContext, BusinessDataStore } from "./businessData"; import { BusinessDataContext, BusinessDataStore } from "./businessData";
@@ -9,7 +8,6 @@ import { AppContextContext, AppContextStore } from "./appContext";
import { getTransportModeSrc, PositionedPanel } from "./utils"; import { getTransportModeSrc, PositionedPanel } from "./utils";
import { PassagesPanel } from "./passagesPanel"; import { PassagesPanel } from "./passagesPanel";
import { IconHamburgerMenu } from './extra/iconHamburgerMenu';
import "./passagesDisplay.scss"; import "./passagesDisplay.scss";
@@ -25,7 +23,7 @@ interface PassagesDisplayStore {
getDisplayedPanelId: () => number; getDisplayedPanelId: () => number;
setDisplayedPanelId: (panelId: number) => void; setDisplayedPanelId: (panelId: number) => void;
}; }
const PassagesDisplayContext = createContext<PassagesDisplayStore>(); const PassagesDisplayContext = createContext<PassagesDisplayStore>();
@@ -91,7 +89,6 @@ const Header: VoidComponent<{ title: string }> = (props) => {
return <div />; return <div />;
const { getLine, passages } = businessDataStore; const { getLine, passages } = businessDataStore;
const { isPassagesRefreshEnabled, togglePassagesRefresh } = passagesDisplayStore;
const [dateNow] = createDateNow(1000); const [dateNow] = createDateNow(1000);
@@ -113,16 +110,14 @@ const Header: VoidComponent<{ title: string }> = (props) => {
setLinesIds(Object.keys(passages())); setLinesIds(Object.keys(passages()));
}); });
return ( return <div class="header">
<div class="header">
<Show when={transportModeUrls() !== undefined} > <Show when={transportModeUrls() !== undefined} >
<For each={transportModeUrls()}> <For each={transportModeUrls()}>
{(url) => {(url) =>
<div class="transportMode"> <div class="transportMode">
<img src={url} /> <img src={url} />
</div> </div>
} }</For>
</For>
</Show> </Show>
<div class="title"> <div class="title">
<svg viewBox="0 0 1260 50"> <svg viewBox="0 0 1260 50">
@@ -132,15 +127,6 @@ const Header: VoidComponent<{ title: string }> = (props) => {
</svg> </svg>
</div> </div>
<div class="menu"> <div class="menu">
<Menu>
<MenuTrigger
as={IconButton}
icon=<IconHamburgerMenu />
/>
<MenuContent>
<MenuItem onSelect={() => togglePassagesRefresh()}>{isPassagesRefreshEnabled() ? "Disable" : "Enable"}</MenuItem>
</MenuContent>
</Menu>
</div> </div>
<div class="clock"> <div class="clock">
<svg viewBox="0 0 115 43"> <svg viewBox="0 0 115 43">
@@ -149,8 +135,7 @@ const Header: VoidComponent<{ title: string }> = (props) => {
</text> </text>
</svg> </svg>
</div> </div>
</div > </div>;
);
}; };
const Footer: VoidComponent<{}> = () => { const Footer: VoidComponent<{}> = () => {
@@ -176,8 +161,7 @@ const Footer: VoidComponent<{}> = () => {
</svg> </svg>
</div> </div>
); );
}} }}</For>
</For>
</div> </div>
); );
} }
@@ -286,13 +270,11 @@ export const PassagesDisplay: ParentComponent = () => {
const syncPeriodMsec = 20 * 1000; const syncPeriodMsec = 20 * 1000;
const panelSwitchPeriodMsec = 4 * 1000; const panelSwitchPeriodMsec = 4 * 1000;
return ( return <div class="passagesDisplay">
<div class="passagesDisplay">
<PassagesDisplayProvider> <PassagesDisplayProvider>
<Header title="Prochains passages" /> <Header title="Prochains passages" />
<Body maxPassagesPerPanel={MAX_PASSAGES_PER_PANEL} syncPeriodMsec={syncPeriodMsec} panelSwitchPeriodMsec={panelSwitchPeriodMsec} /> <Body maxPassagesPerPanel={MAX_PASSAGES_PER_PANEL} syncPeriodMsec={syncPeriodMsec} panelSwitchPeriodMsec={panelSwitchPeriodMsec} />
<Footer /> <Footer />
</PassagesDisplayProvider> </PassagesDisplayProvider>
</div> </div>;
);
}; };

View File

@@ -14,15 +14,13 @@ import "./passagesPanel.scss";
const UnavailablePassage: VoidComponent<{ style: string }> = (props) => { const UnavailablePassage: VoidComponent<{ style: string }> = (props) => {
const textStyle = { fill: "#000000" }; const textStyle = { fill: "#000000" };
return ( return <div class={props.style}>
<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="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="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> <text x="100%" y="100" font-size="25" text-anchor="end" style={textStyle}>disponible</text>
</svg> </svg>
</div> </div>;
);
} }
const Platform: VoidComponent<{ name: string }> = (props) => { const Platform: VoidComponent<{ name: string }> = (props) => {
@@ -43,14 +41,12 @@ const Platform: VoidComponent<{ name: string }> = (props) => {
} }
}); });
return ( return <svg class="platform" viewBox={`0 0 ${viewBoxWidthPx} 40`}>
<svg class="platform" viewBox={`0 0 ${viewBoxWidthPx} 40`}>
<rect ref={rectRef} x="0" y="0" height="100%" rx="9" ry="9" /> <rect ref={rectRef} x="0" y="0" height="100%" rx="9" ry="9" />
<text ref={textRef} x="100%" y="55%" dominant-baseline="middle" text-anchor="end" font-size="25" style={{ fill: "#ffffff" }}> <text ref={textRef} x="100%" y="55%" dominant-baseline="middle" text-anchor="end" font-size="25" style={{ fill: "#ffffff" }}>
QUAI {props.name} QUAI {props.name}
</text> </text>
</svg> </svg>;
);
} }
const TtwPassage: VoidComponent<{ const TtwPassage: VoidComponent<{
@@ -88,8 +84,7 @@ const TtwPassage: VoidComponent<{
</Motion.text> </Motion.text>
</svg>; </svg>;
return ( return <Show when={passage !== undefined} fallback={<UnavailablePassage style={props.fallbackStyle} />}>
<Show when={passage !== undefined} fallback=<UnavailablePassage style={props.fallbackStyle} />>
<Show <Show
when={passage.arrivalPlatformName !== null} when={passage.arrivalPlatformName !== null}
fallback={ fallback={
@@ -100,9 +95,8 @@ const TtwPassage: VoidComponent<{
{text} {text}
<Platform name={passage.arrivalPlatformName} /> <Platform name={passage.arrivalPlatformName} />
</div> </div>
</Show > </Show>
</Show > </Show>;
);
}); });
} }
@@ -122,8 +116,7 @@ const DestinationPassages: VoidComponent<{ line: Line, destination: string }> =
// const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) }; // const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) };
const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) }; const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) };
return ( return <div class="line">
<div class="line">
<div class="transportMode"> <div class="transportMode">
{renderLineTransportMode(props.line)} {renderLineTransportMode(props.line)}
</div> </div>
@@ -142,8 +135,7 @@ const DestinationPassages: VoidComponent<{ line: Line, destination: string }> =
<TtwPassage line={props.line} destination={props.destination} index={1} <TtwPassage line={props.line} destination={props.destination} index={1}
style="secondPassage" withPlatformStyle="withPlatformSecondPassage" style="secondPassage" withPlatformStyle="withPlatformSecondPassage"
fontSize={45} fallbackStyle="unavailableSecondPassage" /> fontSize={45} fallbackStyle="unavailableSecondPassage" />
</div > </div>;
);
} }
export type PassagesPanelComponentProps = ParentProps & { stopId: number, lineIds: string[], show: boolean }; export type PassagesPanelComponentProps = ParentProps & { stopId: number, lineIds: string[], show: boolean };
@@ -162,19 +154,16 @@ export const PassagesPanel: PassagesPanelComponent = (props) => {
} }
const [lines] = createResource<Line[], string[]>(props.lineIds, getLines); const [lines] = createResource<Line[], string[]>(props.lineIds, getLines);
return ( return <div classList={{ "passagesContainer": true, "displayed": props.show }} >
<div classList={{ ["passagesContainer"]: true, ["displayed"]: props.show }} >
<Show when={lines() !== undefined} > <Show when={lines() !== undefined} >
<For each={lines()}> <For each={lines()}>
{(line) => {(line) =>
<Show when={getLineDestinations(line.id) !== undefined}> <Show when={getLineDestinations(line.id) !== undefined}>
<For each={getLineDestinations(line.id)}> <For each={getLineDestinations(line.id)}>{
{(destination) => <DestinationPassages line={line} destination={destination} />} (destination) => <DestinationPassages line={line} destination={destination} />
</For> }</For>
</Show> </Show>
} }</For>
</For>
</Show> </Show>
</div > </div>;
);
} }

View File

@@ -1,8 +1,8 @@
import { batch, createContext, JSX } from 'solid-js'; import { createContext, JSX } from 'solid-js';
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { Marker as LeafletMarker } from 'leaflet'; import { Marker as LeafletMarker } from 'leaflet';
import { Stop, Stops } from './types'; import { Stop } from './types';
export type ByStopIdMarkers = Record<number, LeafletMarker[] | undefined>; export type ByStopIdMarkers = Record<number, LeafletMarker[] | undefined>;
@@ -52,9 +52,7 @@ export function SearchProvider(props: { children: JSX.Element }) {
setStore('markers', stopId, markers); setStore('markers', stopId, markers);
} }
return ( return <SearchContext.Provider value={{ getFoundStops, setFoundStops, getDisplayedStops, setDisplayedStops, addMarkers }}>
<SearchContext.Provider value={{ getFoundStops, setFoundStops, getDisplayedStops, setDisplayedStops, addMarkers }}>
{props.children} {props.children}
</SearchContext.Provider> </SearchContext.Provider>;
);
} }

View File

@@ -41,56 +41,13 @@ export const Map: ParentComponent<{}> = () => {
// TODO: Set padding according to the marker design. // TODO: Set padding according to the marker design.
const fitPointsPadding = [50, 50, 50, 50]; const fitPointsPadding = [50, 50, 50, 50];
let mapDiv: HTMLDivElement | undefined = undefined;
let popup: StopPopup | undefined = undefined; let popup: StopPopup | undefined = undefined;
const [mapRef, setMapRef] = createSignal<HTMLDivElement>();
const stopVectorSource = new OlVectorSource({ features: [] }); const stopVectorSource = new OlVectorSource({ features: [] });
const stopVectorLayer = new OlVectorLayer({ source: stopVectorSource }); const stopVectorLayer = new OlVectorLayer({ source: stopVectorSource });
let overlay: OlOverlay | undefined = undefined;
let map: OlMap | undefined = undefined;
const displayedFeatures: Record<number, OlFeature> = {};
const buildMap = (div: HTMLDivElement): void => {
overlay = new OlOverlay({
element: popup,
autoPan: {
animation: {
duration: 250,
},
},
});
map = new OlMap({
target: div,
controls: [], // remove controls
view: new OlView({
center: mapCenter,
zoom: 10,
}),
layers: [
new OlTileLayer({
source: new OlOSM(),
}),
stopVectorLayer,
],
overlays: [overlay],
});
map.on('singleclick', onClickedMap);
}
const onClickedMap = async (event): Promise<void> => {
const features = await stopVectorLayer.getFeatures(event.pixel);
// Handle only the first feature
if (features.length > 0) {
await onClickedFeature(features[0]);
}
else {
setPopupDisplayed(false);
setSelectedMapStop(undefined);
}
}
const onClickedFeature = async (feature: OlFeatureLike): Promise<void> => { const onClickedFeature = async (feature: OlFeatureLike): Promise<void> => {
const stopId: number = feature.getId(); const stopId: number = feature.getId();
const stop = getStop(stopId); const stop = getStop(stopId);
@@ -108,7 +65,48 @@ export const Map: ParentComponent<{}> = () => {
} }
} }
onMount(() => buildMap(mapDiv)); const onClickedMap = async (event): Promise<void> => {
const features = await stopVectorLayer.getFeatures(event.pixel);
// Handle only the first feature
if (features.length > 0) {
await onClickedFeature(features[0]);
}
else {
setPopupDisplayed(false);
setSelectedMapStop(undefined);
}
}
const displayedFeatures: Record<number, OlFeature> = {};
const overlay = new OlOverlay({
element: popup,
autoPan: {
animation: {
duration: 250,
},
},
});
const map = new OlMap({
target: "map",
controls: [], // remove controls
view: new OlView({
center: mapCenter,
zoom: 10,
}),
layers: [
new OlTileLayer({
source: new OlOSM(),
}),
stopVectorLayer,
],
overlays: [overlay],
});
map.on('singleclick', onClickedMap);
createEffect(() => {
map.setTarget(mapRef());
});
// Filling the map with stops shape // Filling the map with stops shape
createEffect(() => { createEffect(() => {
@@ -207,8 +205,8 @@ export const Map: ParentComponent<{}> = () => {
} }
return <> return <>
<div ref={mapDiv} class="map"> <div ref={setMapRef!} class="map">
<StopPopup ref={popup} stop={selectedMapStop()} show={isPopupDisplayed()} /> <StopPopup ref={popup!} stop={selectedMapStop()} show={isPopupDisplayed()} />
</div> </div>
<For each={getFoundStops()}>{(stop) => <MapStop stop={stop} selected={selectedMapStop()} />}</For> <For each={getFoundStops()}>{(stop) => <MapStop stop={stop} selected={selectedMapStop()} />}</For>
</>; </>;

View File

@@ -149,8 +149,7 @@ export function SearchProvider(props: { children: JSX.Element }) {
setStore('mapFeatures', stopId, feature); setStore('mapFeatures', stopId, feature);
}; };
return ( return <SearchContext.Provider value={{
<SearchContext.Provider value={{
getSearchText, setSearchText, getSearchText, setSearchText,
getFoundStops, setFoundStops, getFoundStops, setFoundStops,
getDisplayedPanelId, setDisplayedPanelId, getDisplayedPanelId, setDisplayedPanelId,
@@ -160,6 +159,5 @@ export function SearchProvider(props: { children: JSX.Element }) {
getMapFeature, getAllMapFeatures, setMapFeature, getMapFeature, getAllMapFeatures, setMapFeature,
}}> }}>
{props.children} {props.children}
</SearchContext.Provider> </SearchContext.Provider>;
);
} }

View File

@@ -33,8 +33,7 @@ const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {
const [lineReprs] = createResource<JSX.Element[], string[]>(props.stop.lines, fetchLinesRepr); const [lineReprs] = createResource<JSX.Element[], string[]>(props.stop.lines, fetchLinesRepr);
return ( return <div class="stop">
<div class="stop">
<svg class="name" viewBox={`0 0 215 ${fontSize}`}> <svg class="name" viewBox={`0 0 215 ${fontSize}`}>
<text <text
x="100%" y="55%" x="100%" y="55%"
@@ -44,8 +43,7 @@ const StopRepr: VoidComponent<{ stop: Stop }> = (props) => {
</text> </text>
</svg> </svg>
<For each={lineReprs()}>{(line: JSX.Element) => line}</For> <For each={lineReprs()}>{(line: JSX.Element) => line}</For>
</div> </div>;
);
} }
@@ -83,7 +81,7 @@ const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
if (line !== undefined) { if (line !== undefined) {
if (!(line.transportMode in byModeReprs)) { if (!(line.transportMode in byModeReprs)) {
byModeReprs[line.transportMode] = { byModeReprs[line.transportMode] = {
mode: <div class="transportMode">{renderLineTransportMode(line)}</div>, mode: <div class="transportMode"> {renderLineTransportMode(line)}</div>,
lines: {} lines: {}
}; };
} }
@@ -91,12 +89,11 @@ const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
} }
} }
const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < TransportModeWeights[y] ? 1 : -1);
TransportModeWeights[y] ? 1 : -1);
return ( return <div class="lineRepr">
<div class="lineRepr"> <For each={sortedTransportModes}>
<For each={sortedTransportModes}>{(transportMode) => { {(transportMode) => {
const reprs = byModeReprs[transportMode]; const reprs = byModeReprs[transportMode];
const lineNames = Object.keys(reprs.lines).sort((x, y) => x.localeCompare(y)); const lineNames = Object.keys(reprs.lines).sort((x, y) => x.localeCompare(y));
return <> return <>
@@ -104,40 +101,34 @@ const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => {
<div class="linesRepresentationMatrix"> <div class="linesRepresentationMatrix">
<For each={lineNames}>{(lineName) => reprs.lines[lineName]}</For> <For each={lineNames}>{(lineName) => reprs.lines[lineName]}</For>
</div> </div>
</> </>;
}} }}</For>
</For> </div>;
</div >
);
} }
const [lineReprs] = createResource(props.stop, fetchLinesRepr); const [lineReprs] = createResource(props.stop, fetchLinesRepr);
return ( return <div
<div
class="stop" class="stop"
onClick={() => setDisplayedStops([props.stop])} onClick={() => setDisplayedStops([props.stop])}
onMouseEnter={() => setHighlightedStop(props.stop)} onMouseEnter={() => setHighlightedStop(props.stop)}
onMouseLeave={resetHighlightedStop} onMouseLeave={resetHighlightedStop}
> >
<div class="name" > <div class="name">
<ScrollingText height={fontSize} width={100} content={props.stop.name} /> <ScrollingText height={fontSize} width={100} content={props.stop.name} />
</div> </div>
{lineReprs()} {lineReprs()}
</div> </div>;
);
} }
export const StopsPanel: ParentComponent<{ stops: Stop[], show: boolean }> = (props) => { export const StopsPanel: ParentComponent<{ stops: Stop[], show: boolean }> = (props) => {
return ( return <div classList={{ "stopPanel": true, "displayed": props.show }}>
<div classList={{ "stopPanel": true, "displayed": props.show }}>
<For each={props.stops.sort((x, y) => x.name.localeCompare(y.name))}> <For each={props.stops.sort((x, y) => x.name.localeCompare(y.name))}>
{(stop) => { {(stop) => {
return <Show when={stop.stops !== undefined} fallback={<StopRepr stop={stop} />}> return <Show when={stop.stops !== undefined} fallback={<StopRepr stop={stop} />}>
<StopAreaRepr stop={stop} /> <StopAreaRepr stop={stop} />
</Show>; </Show>;
}} }}</For>
</For> </div>;
</div>
);
} }

View File

@@ -29,21 +29,19 @@ export const StopPopup: ParentComponent<{ stop: Stop, show: boolean }> = (props)
} }
const [destinations] = createResource(() => props.stop, getDestinations); const [destinations] = createResource(() => props.stop, getDestinations);
return (
<div ref={popupDiv} classList={{ "popup": true, "displayed": props.show }}> return <div ref={popupDiv} classList={{ "popup": true, "displayed": props.show }}>
<div class="header">{props.stop?.name}</div> <div class="header">{props.stop?.name}</div>
<div class="body"> <div class="body" >
<For each={destinations()}> <For each={destinations()}>
{(dst) => { {(dst) => {
return <div class='line'> return <div class='line' >
{dst.lineId} {dst.lineId}
<div class="name"> < div class="name" >
<ScrollingText height={10} width={130} content={dst.destinations.join('/')} /> <ScrollingText height={10} width={130} content={dst.destinations.join('/')} />
</div> </div>
</div>; </div>;
}} }}</For>
</For>
</div> </div>
</div > </div>;
);
} }

View File

@@ -33,6 +33,7 @@
background-color: transparent; background-color: transparent;
.leftAddon { .leftAddon {
width: 17%; width: 17%;

View File

@@ -1,4 +1,4 @@
import { createEffect, For, JSX, lazy, ParentComponent, useContext, Show, VoidComponent } from 'solid-js'; import { createEffect, For, JSX, ParentComponent, useContext, Show, VoidComponent } from 'solid-js';
import { lazily } from 'solidjs-lazily'; import { lazily } from 'solidjs-lazily';
import { createScrollPosition } from "@solid-primitives/scroll"; import { createScrollPosition } from "@solid-primitives/scroll";
@@ -16,11 +16,10 @@ import "./stopsSearchMenu.scss";
const StopNameInput: VoidComponent<{ onInput: JSX.EventHandler<HTMLInputElement, InputEvent>, leftAddon: string, placeholder: string }> = (props) => { const StopNameInput: VoidComponent<{ onInput: JSX.EventHandler<HTMLInputElement, InputEvent>, leftAddon: string, placeholder: string }> = (props) => {
return ( return <div class="stopNameInput">
<div class="stopNameInput">
<div class="leftAddon">{props.leftAddon}</div> <div class="leftAddon">{props.leftAddon}</div>
<input type="text" oninput={props.onInput} placeholder={props.placeholder} /> <input type="text" oninput={props.onInput} placeholder={props.placeholder} />
</div>); </div>;
}; };
@@ -40,8 +39,7 @@ const Header: VoidComponent<{ title: string, minCharsNb: number }> = (props) =>
} }
} }
return ( return <div class="header">
<div class="header">
<div class="title"> <div class="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"> <text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
@@ -50,8 +48,7 @@ const Header: VoidComponent<{ title: string, minCharsNb: number }> = (props) =>
</svg> </svg>
</div> </div>
<StopNameInput onInput={onStopNameInput} leftAddon="🚉 🚏" placeholder="Stop name..." /> <StopNameInput onInput={onStopNameInput} leftAddon="🚉 🚏" placeholder="Stop name..." />
</div > </div>;
);
}; };
@@ -71,13 +68,15 @@ const StopsPanels: ParentComponent<{ maxStopsPerPanel: number }> = (props) => {
yStopsPanelsScroll(); yStopsPanelsScroll();
for (const panel of getPanels()) { for (const panel of getPanels()) {
const panelDiv = panel.panel(); const panelDiv = panel.panel;
if (panelDiv != null) {
const panelDivClientRect = panelDiv.getBoundingClientRect(); const panelDivClientRect = panelDiv.getBoundingClientRect();
if (panelDivClientRect.y > 0) { if (panelDivClientRect.y > 0) {
setDisplayedPanelId(panel.position); setDisplayedPanelId(panel.position);
break; break;
} }
} }
}
}); });
return ( return (
@@ -133,7 +132,8 @@ const MapPlaceholder: VoidComponent<{}> = () => {
} }
return <div return <div
class="mapPlaceholder" ondblclick={() => onDoubleClick()}> class="mapPlaceholder"
ondblclick={() => onDoubleClick()}>
Double-clic pour activer la carte Double-clic pour activer la carte
</div>; </div>;
}; };
@@ -166,35 +166,31 @@ const Footer: VoidComponent<{}> = () => {
const { getDisplayedPanelId, getPanels } = searchStore; const { getDisplayedPanelId, getPanels } = searchStore;
return ( return <div class="footer">
<div class="footer">
<For each={getPanels()}> <For each={getPanels()}>
{(panel) => { {(panel) => {
const position = panel.position; const position = panel.position;
return ( return (
<div> <div>
<svg viewBox="0 0 29 29"> <svg viewBox="0 0 29 29">
<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3" <circle
cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
style={{ fill: `var(--idfm-${position == getDisplayedPanelId() ? "white" : "black"})` }} style={{ fill: `var(--idfm-${position == getDisplayedPanelId() ? "white" : "black"})` }}
/> />
</svg> </svg>
</div> </div>
); );
}} }}</For>
</For> </div>;
</div>
);
}; };
export const StopsSearchMenu: VoidComponent = () => { export const StopsSearchMenu: VoidComponent = () => {
return ( return <div class="stopSearchMenu">
<div class="stopSearchMenu">
<SearchProvider> <SearchProvider>
<Header title="Recherche de l'arrêt..." minCharsNb={4} /> <Header title="Recherche de l'arrêt..." minCharsNb={4} />
<Body /> <Body />
<Footer /> <Footer />
</SearchProvider> </SearchProvider>
</div> </div>;
);
}; };

View File

@@ -34,7 +34,7 @@ export class Passage {
this.arrivalStatus = arrivalStatus; this.arrivalStatus = arrivalStatus;
this.departStatus = departStatus; this.departStatus = departStatus;
} }
}; }
export type Passages = Record<string, Record<string, Passage[]>>; export type Passages = Record<string, Record<string, Passage[]>>;
@@ -59,7 +59,7 @@ export class Stop {
this.lines.push(...stop.lines); this.lines.push(...stop.lines);
} }
} }
}; }
export type Stops = Record<number, Stop>; export type Stops = Record<number, Stop>;
@@ -77,7 +77,7 @@ export class StopShape {
this.epsg3857_bbox = epsg3857_bbox; this.epsg3857_bbox = epsg3857_bbox;
this.epsg3857_points = epsg3857_points; this.epsg3857_points = epsg3857_points;
} }
}; }
export type StopShapes = Record<number, StopShape>; export type StopShapes = Record<number, StopShape>;
@@ -111,6 +111,6 @@ export class Line {
this.audibleSignsAvailable = audibleSignsAvailable; this.audibleSignsAvailable = audibleSignsAvailable;
this.stopIds = stopIds; this.stopIds = stopIds;
} }
}; }
export type Lines = Record<string, Line>; export type Lines = Record<string, Line>;

View File

@@ -36,11 +36,11 @@ export function renderLineTransportMode(line: Line): JSX.Element {
} }
function renderBusLinePicto(line: Line): JSX.Element { function renderBusLinePicto(line: Line): JSX.Element {
return ( return <div class="busLinePicto">
<div class="busLinePicto">
<svg viewBox="0 0 31.5 14"> <svg viewBox="0 0 31.5 14">
<rect x="0" y="0" width="31.5" height="14" rx="1.5" ry="1.5" style={{ fill: `#${line.backColorHexa}` }} /> <rect x="0" y="0" width="31.5" height="14" rx="1.5" ry="1.5" style={{ fill: `#${line.backColorHexa}` }} />
<text x="50%" <text
x="50%"
y="55%" y="55%"
dominant-baseline="middle" dominant-baseline="middle"
text-anchor="middle" text-anchor="middle"
@@ -49,18 +49,17 @@ function renderBusLinePicto(line: Line): JSX.Element {
{line.shortName} {line.shortName}
</text> </text>
</svg> </svg>
</div> </div>;
);
} }
function renderTramLinePicto(line: Line): JSX.Element { function renderTramLinePicto(line: Line): JSX.Element {
const lineStyle = { fill: `#${line.backColorHexa}` }; const lineStyle = { fill: `#${line.backColorHexa}` };
return ( return <div class="tramLinePicto">
<div class="tramLinePicto">
<svg viewBox="0 0 20 20"> <svg viewBox="0 0 20 20">
<rect x="0" y="0" width="20" height="3" rx="1" ry="1" style={lineStyle} /> <rect x="0" y="0" width="20" height="3" rx="1" ry="1" style={lineStyle} />
<rect x="0" y="17" width="20" height="3" rx="1" ry="1" style={lineStyle} /> <rect x="0" y="17" width="20" height="3" rx="1" ry="1" style={lineStyle} />
<text x="50%" <text
x="50%"
y="55%" y="55%"
dominant-baseline="middle" dominant-baseline="middle"
text-anchor="middle" text-anchor="middle"
@@ -69,16 +68,15 @@ function renderTramLinePicto(line: Line): JSX.Element {
{line.shortName} {line.shortName}
</text> </text>
</svg> </svg>
</div> </div>;
);
} }
function renderMetroLinePicto(line: Line): JSX.Element { function renderMetroLinePicto(line: Line): JSX.Element {
return ( return <div class="metroLinePicto">
<div class="metroLinePicto">
<svg viewBox="0 0 20 20"> <svg viewBox="0 0 20 20">
<circle cx="10" cy="10" r="10" style={{ fill: `#${line.backColorHexa}` }} /> <circle cx="10" cy="10" r="10" style={{ fill: `#${line.backColorHexa}` }} />
<text x="50%" <text
x="50%"
y="55%" y="55%"
dominant-baseline="middle" dominant-baseline="middle"
text-anchor="middle" text-anchor="middle"
@@ -86,16 +84,15 @@ function renderMetroLinePicto(line: Line): JSX.Element {
{line.shortName} {line.shortName}
</text> </text>
</svg> </svg>
</div> </div>;
);
} }
function renderTrainLinePicto(line: Line): JSX.Element { function renderTrainLinePicto(line: Line): JSX.Element {
return ( return <div class="trainLinePicto">
<div class="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}` }} /> <rect x="0" y="0" width="20" height="20" rx="4.5" ry="4.5" style={{ fill: `#${line.backColorHexa}` }} />
<text x="50%" <text
x="50%"
y="55%" y="55%"
dominant-baseline="middle" dominant-baseline="middle"
text-anchor="middle" text-anchor="middle"
@@ -104,8 +101,7 @@ function renderTrainLinePicto(line: Line): JSX.Element {
{line.shortName} {line.shortName}
</text> </text>
</svg> </svg>
</div> </div>;
);
} }
export function renderLinePicto(line: Line): JSX.Element { export function renderLinePicto(line: Line): JSX.Element {
@@ -152,8 +148,7 @@ export const ScrollingText: VoidComponent<{ height: number, width: number, conte
} }
}); });
return ( return <svg ref={viewBoxRef} viewBox={`0 0 ${props.width} ${props.height}`}>
<svg ref={viewBoxRef} viewBox={`0 0 ${props.width} ${props.height}`}>
<text <text
ref={textRef} ref={textRef}
x="0%" y="55%" x="0%" y="55%"
@@ -161,6 +156,5 @@ export const ScrollingText: VoidComponent<{ height: number, width: number, conte
font-size={`${props.height}px`}> font-size={`${props.height}px`}>
{props.content} {props.content}
</text> </text>
</svg > </svg>;
);
} }

View File

@@ -4,6 +4,7 @@
"jsxImportSource": "solid-js", "jsxImportSource": "solid-js",
"noImplicitAny": true, "noImplicitAny": true,
"target": "ES6", "target": "ES6",
"module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"allowJs": true, "allowJs": true,
"outDir": "build", "outDir": "build",