From e98b24303c9dd1f1e9c82efe01d3a41a82744f20 Mon Sep 17 00:00:00 2001 From: Adrien Date: Fri, 2 Feb 2024 23:02:39 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Cleanup=20code=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 3 +- frontend/src/App.scss | 34 +- frontend/src/App.tsx | 123 +++-- frontend/src/_common.scss | 74 +-- frontend/src/_utils.scss | 20 +- frontend/src/appContext.tsx | 52 +-- frontend/src/businessData.tsx | 397 ++++++++-------- frontend/src/extra/iconHamburgerMenu.tsx | 23 +- frontend/src/index.scss | 21 +- frontend/src/passagesDisplay.scss | 96 ++-- frontend/src/passagesDisplay.tsx | 432 +++++++++--------- frontend/src/passagesPanel.scss | 310 ++++++------- frontend/src/passagesPanel.tsx | 247 +++++----- frontend/src/search.tsx | 70 ++- frontend/src/stopsSearchMenu/map.scss | 118 ++--- frontend/src/stopsSearchMenu/map.tsx | 330 ++++++------- frontend/src/stopsSearchMenu/mapStop.tsx | 118 ++--- frontend/src/stopsSearchMenu/searchStore.tsx | 234 +++++----- frontend/src/stopsSearchMenu/stopPanel.scss | 148 +++--- frontend/src/stopsSearchMenu/stopPanel.tsx | 197 ++++---- frontend/src/stopsSearchMenu/stopPopup.tsx | 68 ++- .../src/stopsSearchMenu/stopsSearchMenu.scss | 109 ++--- .../src/stopsSearchMenu/stopsSearchMenu.tsx | 266 ++++++----- frontend/src/types.tsx | 184 ++++---- frontend/src/utils.tsx | 238 +++++----- frontend/tsconfig.json | 41 +- 26 files changed, 1943 insertions(+), 2010 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 1049d89..9921c16 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "matrix-widget-api": "^1.1.1", "ol": "^7.3.0", "solid-js": "^1.6.6", - "solid-transition-group": "^0.0.10" + "solid-transition-group": "^0.0.10", + "solidjs-lazily": "^0.1.2" } } diff --git a/frontend/src/App.scss b/frontend/src/App.scss index 0d0fbef..314d48b 100644 --- a/frontend/src/App.scss +++ b/frontend/src/App.scss @@ -1,27 +1,27 @@ .App { - --idfm-black: #2c2e35; - --idfm-white: #ffffff; + --idfm-black: #2c2e35; + --idfm-white: #ffffff; - --neutral-color: #d7dbdf; + --neutral-color: #d7dbdf; - --border-radius: calc(15/1920*100%); + --border-radius: calc(15/1920*100%); - height: inherit; - width: inherit; + height: inherit; + width: inherit; - scroll-snap-type: x mandatory; - overflow-x: scroll; + scroll-snap-type: x mandatory; + overflow-x: scroll; - display: flex; - text-align: center; + display: flex; + text-align: center; - .panel { - min-width: 100%; - height: inherit; - width: inherit; + .panel { + min-width: 100%; + height: inherit; + width: inherit; - scroll-snap-align: center; + scroll-snap-align: center; - background-color: var(--idfm-black); - } + background-color: var(--idfm-black); + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 862bc3d..4e531f8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,92 +1,85 @@ -import { Component, createSignal } from 'solid-js'; -import { IVisibilityActionRequest, MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction } from 'matrix-widget-api'; -import { HopeProvider } from "@hope-ui/solid"; +import { Component, createSignal, onCleanup, onMount } from 'solid-js'; +// import { IVisibilityActionRequest, MatrixCapabilities, WidgetApi, WidgetApiToWidgetAction } from 'matrix-widget-api'; import { BusinessDataProvider } from './businessData'; import { AppContextProvider } from './appContext'; - import { PassagesDisplay } from './passagesDisplay'; import { StopsSearchMenu } from './stopsSearchMenu/stopsSearchMenu'; import "./App.scss"; -import { onCleanup, onMount } from 'solid-js'; function parseFragment() { - const fragmentString = (window.location.hash || "?"); - return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf('?'), 0))); + const fragmentString = (window.location.hash || "?"); + return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf('?'), 0))); } const App: Component = () => { - console.log('App: New'); + console.log('App: New'); - const qs = parseFragment(); - const widgetId = qs.get('widgetId'); - const userId = qs.get('userId'); + const qs = parseFragment(); + const widgetId = qs.get('widgetId'); + const userId = qs.get('userId'); - console.log("App: widgetId:" + widgetId); - console.log("App: userId:" + userId); + console.log("App: widgetId:" + widgetId); + console.log("App: userId:" + userId); - const api = new WidgetApi(widgetId != null ? widgetId : undefined); - api.requestCapability(MatrixCapabilities.AlwaysOnScreen); - api.start(); - api.on("ready", function() { - console.log("App: widget API is READY !!!!"); - }); + // const api = new WidgetApi(widgetId != null ? widgetId : undefined); + // api.requestCapability(MatrixCapabilities.AlwaysOnScreen); + // api.start(); + // api.on("ready", function() { + // console.log("App: widget API is READY !!!!"); + // }); - // Seems to don´t be used... - api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent) => { - console.log("App: Visibility change"); - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log("App: ", ev.detail); // custom handling here - /* api.transport.reply(ev.detail, {}); */ - api.transport.reply(ev.detail, {}); - }); + // Seems to don´t be used... + // api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent) => { + // console.log("App: Visibility change"); + // ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + // console.log("App: ", ev.detail); // custom handling here + // /* api.transport.reply(ev.detail, {}); */ + // api.transport.reply(ev.detail, {}); + // }); - createSignal({ - height: window.innerHeight, - width: window.innerWidth - }); + createSignal({ + height: window.innerHeight, + width: window.innerWidth + }); - const onResize = () => { - const body = document.body; + const onResize = () => { + const body = document.body; - if (window.innerWidth * 9 / 16 < window.innerHeight) { - body.style['height'] = 'auto'; - body.style['width'] = '100vw'; - } - else { - body.style['height'] = '100vh'; - body.style['width'] = 'auto'; - } - }; + if (window.innerWidth * 9 / 16 < window.innerHeight) { + body.style['height'] = 'auto'; + body.style['width'] = '100vw'; + } + else { + body.style['height'] = '100vh'; + body.style['width'] = 'auto'; + } + }; - onMount(() => { - window.addEventListener('resize', onResize); - onResize(); - }); + onMount(() => { + window.addEventListener('resize', onResize); + onResize(); + }); - onCleanup(() => { - window.removeEventListener('resize', onResize); - }) + onCleanup(() => { + window.removeEventListener('resize', onResize); + }) - return ( - - - -
-
- -
-
- -
-
-
-
-
- ); + return + +
+
+ +
+
+ +
+
+
+
; }; export default App; diff --git a/frontend/src/_common.scss b/frontend/src/_common.scss index cb2c37b..200747f 100644 --- a/frontend/src/_common.scss +++ b/frontend/src/_common.scss @@ -1,74 +1,74 @@ /* Idfm: 1860x1080px */ %widget { - aspect-ratio: 16/9; - --reverse-aspect-ratio: 9/16; - /* height is set according to the aspect-ratio, don´t touch it */ - width: 100%; + 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; + display: flex; + flex-direction: column; } /* 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%); + 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; + display: flex; + align-items: center; - font-family: IDFVoyageur-bold; + font-family: IDFVoyageur-bold; } .header { - @extend %header; + @extend %header; } %title { - height: 50%; - width: 70%; + height: 50%; + width: 70%; - margin-right: auto; + margin-right: auto; } /* Idfm: 1860x892px (margin: 0px 30px) */ %body { - width: calc(1860/1920*100%); - height: calc(892/1080*100%); - margin: 0 calc(30/1920*100%); + width: calc(1860/1920*100%); + height: calc(892/1080*100%); + margin: 0 calc(30/1920*100%); - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; - background-color: white; + background-color: white; - border-collapse:separate; - // border:solid var(--idfm-black) 1px; - border-radius: calc(15/1920*100%); + 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%); + width: calc(1820/1920*100%); + height: calc(54/1080*100%); + margin: 0 calc(50/1920*100%); - display: flex; - align-items: center; - justify-content: right; + display: flex; + align-items: center; + justify-content: right; } .footer { - @extend %footer; + @extend %footer; } .footer div { - aspect-ratio: 1; - height: 50%; + aspect-ratio: 1; + height: 50%; - margin-left: calc(42/1920*100%); + margin-left: calc(42/1920*100%); } diff --git a/frontend/src/_utils.scss b/frontend/src/_utils.scss index 7c0d7f0..cc1fe25 100644 --- a/frontend/src/_utils.scss +++ b/frontend/src/_utils.scss @@ -1,31 +1,31 @@ %transportMode { - aspect-ratio : 1 / 1; + aspect-ratio : 1 / 1; } %linePicto { - font-family: IDFVoyageur-bold; + font-family: IDFVoyageur-bold; } %tramLinePicto { - @extend %linePicto; + @extend %linePicto; - aspect-ratio : 1 / 1; + aspect-ratio : 1 / 1; } %trainLinePicto { - @extend %linePicto; + @extend %linePicto; - aspect-ratio : 1 / 1; + aspect-ratio : 1 / 1; } %metroLinePicto { - @extend %linePicto; + @extend %linePicto; - aspect-ratio : 1 / 1; + aspect-ratio : 1 / 1; } %busLinePicto { - @extend %linePicto; + @extend %linePicto; - aspect-ratio : 2.25; + aspect-ratio : 2.25; } diff --git a/frontend/src/appContext.tsx b/frontend/src/appContext.tsx index ae58c0e..d1e6108 100644 --- a/frontend/src/appContext.tsx +++ b/frontend/src/appContext.tsx @@ -4,40 +4,36 @@ import { createStore } from "solid-js/store"; import { Stop } from './types'; export interface AppContextStore { - getDisplayedStops: () => Stop[]; - setDisplayedStops: (stops: Stop[]) => void; -}; + getDisplayedStops: () => Stop[]; + setDisplayedStops: (stops: Stop[]) => void; +} export const AppContextContext = createContext(); export function AppContextProvider(props: { children: JSX.Element }) { - type Store = { - displayedStops: Stop[]; - }; + type Store = { + displayedStops: Stop[]; + }; - const [store, setStore] = createStore({ - displayedStops: [], - }); + const [store, setStore] = createStore({ + displayedStops: [], + }); - const getDisplayedStops = (): Stop[] => { - return store.displayedStops; - } + const getDisplayedStops = (): Stop[] => { + return store.displayedStops; + } - const setDisplayedStops = (stops: Stop[]): void => { - console.log("setDisplayedStops=", stops); - // setStore((s: Store) => { - setStore('displayedStops', stops); - // return s; - // }); - } + const setDisplayedStops = (stops: Stop[]): void => { + console.log("setDisplayedStops=", stops); + setStore('displayedStops', stops); + } - return ( - - {props.children} - - ); - -}; + return ( + + {props.children} + + ); +} diff --git a/frontend/src/businessData.tsx b/frontend/src/businessData.tsx index d33df0a..2a80107 100644 --- a/frontend/src/businessData.tsx +++ b/frontend/src/businessData.tsx @@ -6,246 +6,235 @@ import { Line, Lines, Passage, Passages, Stop, StopShape, StopShapes, Stops } fr export type StopDestinations = Record; + export interface BusinessDataStore { - getLine: (lineId: string) => Promise; - getLinePassages: (lineId: string) => Record; - getLineDestinations: (lineId: string) => string[]; - getDestinationPassages: (lineId: string, destination: string) => Passage[]; + getLine: (lineId: string) => Promise; + getLinePassages: (lineId: string) => Record; + getLineDestinations: (lineId: string) => string[]; + getDestinationPassages: (lineId: string, destination: string) => Passage[]; - passages: () => Passages; - getPassagesLineIds: () => string[]; - refreshPassages: (stopId: number) => Promise; - addPassages: (passages: Passages) => void; - clearPassages: () => void; + passages: () => Passages; + getPassagesLineIds: () => string[]; + refreshPassages: (stopId: number) => Promise; + addPassages: (passages: Passages) => void; + clearPassages: () => void; - getStop: (stopId: number) => Stop | undefined; - searchStopByName: (name: string) => Promise; + getStop: (stopId: number) => Stop | undefined; + searchStopByName: (name: string) => Promise; - getStopDestinations: (stopId: number) => Promise; - getStopShape: (stopId: number) => Promise; -}; + getStopDestinations: (stopId: number) => Promise; + getStopShape: (stopId: number) => Promise; +} export const BusinessDataContext = createContext(); export function BusinessDataProvider(props: { children: JSX.Element }) { - const [serverUrl] = createSignal("https://carrramba.adrien.run/api"); + const [serverUrl] = createSignal("https://carrramba.adrien.run/api"); - type Store = { - lines: Lines; - passages: Passages; - stops: Stops; - stopShapes: StopShapes; - }; + type Store = { + lines: Lines; + passages: Passages; + stops: Stops; + stopShapes: StopShapes; + }; - const [store, setStore] = createStore({ lines: {}, passages: {}, stops: {}, stopShapes: {} }); + const [store, setStore] = createStore({ lines: {}, passages: {}, stops: {}, stopShapes: {} }); - const getLine = async (lineId: string): Promise => { - let line = store.lines[lineId]; - if (line === undefined) { - console.log(`${lineId} not found... fetch it from backend.`); + const getLine = async (lineId: string): Promise => { + let line = store.lines[lineId]; + if (line === undefined) { + console.log(`${lineId} not found... fetch it from backend.`); - const response = await fetch(`${serverUrl()}/line/${lineId}`, { - headers: { 'Content-Type': 'application/json' } - }); + const response = await fetch(`${serverUrl()}/line/${lineId}`, { + headers: { 'Content-Type': 'application/json' } + }); - const json = await response.json(); + const json = await response.json(); - if (response.ok) { - setStore('lines', lineId, json); - line = json; - } - else { - console.warn(`No line found for ${lineId} line id:`, json); - } - } - return line; - } + if (response.ok) { + setStore('lines', lineId, json); + line = json; + } + else { + console.warn(`No line found for ${lineId} line id:`, json); + } + } + return line; + } - const getLinePassages = (lineId: string): Record => { - return store.passages[lineId]; - } + const getLinePassages = (lineId: string): Record => { + return store.passages[lineId]; + } - const getLineDestinations = (lineId: string): string[] => { - return Object.keys(store.passages[lineId]); - } + const getLineDestinations = (lineId: string): string[] => { + return Object.keys(store.passages[lineId]); + } - // TODO: Remove this method: it's based on the next passages and return nothing until the refreshPassages is called. - const getDestinationPassages = (lineId: string, destination: string): Passage[] => { - return store.passages[lineId][destination]; - } + // TODO: Remove this method: it's based on the next passages and return nothing until the refreshPassages is called. + const getDestinationPassages = (lineId: string, destination: string): Passage[] => { + return store.passages[lineId][destination]; + } - const passages = (): Passages => { - return store.passages; - } + const passages = (): Passages => { + return store.passages; + } - const getPassagesLineIds = (): string[] => { - return Object.keys(store.passages); - } + const getPassagesLineIds = (): string[] => { + return Object.keys(store.passages); + } - const _cleanupPassages = (passages: Passages): void => { - const deadline = Math.floor(Date.now() / 1000) - 60; - for (const linePassages of Object.values(passages)) { - for (const destination of Object.keys(linePassages)) { - const destinationPassages = linePassages[destination]; - const cleaned = []; - for (const passage of destinationPassages) { - if (passage.expectedDepartTs > deadline) { - cleaned.push(passage); - } - } - linePassages[destination] = cleaned; - } - } - } + const _cleanupPassages = (passages: Passages): void => { + const deadline = Math.floor(Date.now() / 1000) - 60; + for (const linePassages of Object.values(passages)) { + for (const destination of Object.keys(linePassages)) { + const destinationPassages = linePassages[destination]; + const cleaned = []; + for (const passage of destinationPassages) { + if (passage.expectedDepartTs > deadline) { + cleaned.push(passage); + } + } + linePassages[destination] = cleaned; + } + } + } - const refreshPassages = async (stopId: number): Promise => { - console.log(`Fetching data for ${stopId}`); - const httpOptions = { headers: { "Content-Type": "application/json" } }; - const response = await fetch(`${serverUrl()}/stop/${stopId}/nextPassages`, httpOptions); + const refreshPassages = async (stopId: number): Promise => { + console.log(`Fetching data for ${stopId}`); + const httpOptions = { headers: { "Content-Type": "application/json" } }; + const response = await fetch(`${serverUrl()}/stop/${stopId}/nextPassages`, httpOptions); - const json = await response.json(); + const json = await response.json(); - if (response.ok) { - _cleanupPassages(json.passages); - addPassages(json.passages); - } - else { - console.warn(`No passage found for ${stopId} stop:`, json); - } - } + if (response.ok) { + _cleanupPassages(json.passages); + addPassages(json.passages); + } + else { + console.warn(`No passage found for ${stopId} stop:`, json); + } + } - const addPassages = (passages: Passages): void => { - batch(() => { - const storePassages = store.passages; - for (const lineId of Object.keys(passages)) { - const newLinePassages = passages[lineId]; - const linePassages = storePassages[lineId]; - if (linePassages === undefined) { - setStore('passages', lineId, newLinePassages); - } - else { - for (const destination of Object.keys(newLinePassages)) { - const newLinePassagesDestination = newLinePassages[destination]; - const linePassagesDestination = linePassages[destination]; - if (linePassagesDestination === undefined) { - setStore('passages', lineId, destination, newLinePassagesDestination); - } - else { - if (linePassagesDestination.length - newLinePassagesDestination.length != 0) { - console.log(`Server provides ${newLinePassagesDestination.length} passages, \ + const addPassages = (passages: Passages): void => { + batch(() => { + const storePassages = store.passages; + for (const lineId of Object.keys(passages)) { + const newLinePassages = passages[lineId]; + const linePassages = storePassages[lineId]; + if (linePassages === undefined || Object.keys(linePassages).length == 0) { + setStore('passages', lineId, newLinePassages); + } + else { + for (const destination of Object.keys(newLinePassages)) { + const newLinePassagesDestination = newLinePassages[destination]; + const linePassagesDestination = linePassages[destination]; + if (linePassagesDestination === undefined) { + setStore('passages', lineId, destination, newLinePassagesDestination); + } + else { + if (linePassagesDestination.length - newLinePassagesDestination.length != 0) { + console.log(`Server provides ${newLinePassagesDestination.length} passages, \ ${linePassagesDestination.length} here... refresh all them.`); - setStore('passages', lineId, destination, newLinePassagesDestination); - } - else { - linePassagesDestination.forEach((passage, index) => { - const newPassage = newLinePassagesDestination[index]; - if (passage.expectedDepartTs != newPassage.expectedDepartTs) { - console.log(`Refresh expectedDepartTs (${passage.expectedDepartTs} -> ${newPassage.expectedDepartTs}`); - setStore('passages', lineId, destination, index, 'expectedDepartTs', newPassage.expectedDepartTs); - } - if (passage.expectedArrivalTs != newPassage.expectedArrivalTs) { - console.log(`Refresh expectedArrivalTs (${passage.expectedArrivalTs} -> ${newPassage.expectedArrivalTs}`); - setStore('passages', lineId, destination, index, 'expectedArrivalTs', newPassage.expectedArrivalTs); - } - }); - } - } - } - } - } - }); - } + setStore('passages', lineId, destination, newLinePassagesDestination); + } + else { + linePassagesDestination.forEach((passage, index) => { + const newPassage = newLinePassagesDestination[index]; + if (passage.expectedDepartTs != newPassage.expectedDepartTs) { + console.log(`Refresh expectedDepartTs (${passage.expectedDepartTs} -> ${newPassage.expectedDepartTs}`); + setStore('passages', lineId, destination, index, 'expectedDepartTs', newPassage.expectedDepartTs); + } + if (passage.expectedArrivalTs != newPassage.expectedArrivalTs) { + console.log(`Refresh expectedArrivalTs (${passage.expectedArrivalTs} -> ${newPassage.expectedArrivalTs}`); + setStore('passages', lineId, destination, index, 'expectedArrivalTs', newPassage.expectedArrivalTs); + } + }); + } + } + } + } + } + }); + } - const clearPassages = (): void => { - setStore((s: Store): Store => { - for (const lineId of Object.keys(s.passages)) { - setStore('passages', lineId, undefined); - } - return s; - }); - } + const clearPassages = (): void => { + setStore((s: Store): Store => { + for (const lineId of Object.keys(s.passages)) { + setStore('passages', lineId, {}); + } + return s; + }); + } - const getStop = (stopId: number): Stop | undefined => { - return store.stops[stopId]; - } + const getStop = (stopId: number): Stop | undefined => { + return store.stops[stopId]; + } - const searchStopByName = async (name: string): Promise => { - const byIdStops: Stops = {}; + const searchStopByName = async (name: string): Promise => { + const byIdStops: Stops = {}; - const response = await fetch(`${serverUrl()}/stop/?name=${name}`, { - headers: { 'Content-Type': 'application/json' } - }); + const response = await fetch(`${serverUrl()}/stop/?name=${name}`, { + headers: { 'Content-Type': 'application/json' } + }); - const json = await response.json(); + const json = await response.json(); - if (response.ok) { - for (const stop of json) { - byIdStops[stop.id] = stop; - setStore('stops', stop.id, stop); + if (response.ok) { + for (const stop of json) { + byIdStops[stop.id] = stop; + setStore('stops', stop.id, stop); - if (stop.stops !== undefined) { - for (const innerStop of stop.stops) { - setStore('stops', innerStop.id, innerStop); - } - } - } - } - else { - console.warn(`No stop found for '${name}' query:`, json); - } + if (stop.stops !== undefined) { + for (const innerStop of stop.stops) { + setStore('stops', innerStop.id, innerStop); + } + } + } + } + else { + console.warn(`No stop found for '${name}' query:`, json); + } - return byIdStops; - } + return byIdStops; + } - const getStopDestinations = async (stopId: number): Promise => { - const response = await fetch(`${serverUrl()}/stop/${stopId}/destinations`, { - headers: { 'Content-Type': 'application/json' } - }); - const destinations = response.ok ? await response.json() : undefined; - return destinations; - } + const getStopDestinations = async (stopId: number): Promise => { + const response = await fetch(`${serverUrl()}/stop/${stopId}/destinations`, { + headers: { 'Content-Type': 'application/json' } + }); + const destinations = response.ok ? await response.json() : undefined; + return destinations; + } - const getStopShape = async (stopId: number): Promise => { - let shape = store.stopShapes[stopId]; - if (shape === undefined) { - console.log(`No shape found for ${stopId} stop... fetch it from backend.`); - const response = await fetch(`${serverUrl()}/stop/${stopId}/shape`, { - headers: { 'Content-Type': 'application/json' } - }); + const getStopShape = async (stopId: number): Promise => { + let shape = store.stopShapes[stopId]; + if (shape === undefined) { + console.log(`No shape found for ${stopId} stop... fetch it from backend.`); + const response = await fetch(`${serverUrl()}/stop/${stopId}/shape`, { + headers: { 'Content-Type': 'application/json' } + }); - const json = await response.json(); + const json = await response.json(); - if (response.ok) { - setStore('stopShapes', stopId, json); - shape = json; - } - else { - console.warn(`No shape found for ${stopId} stop:`, json); - } - } - return shape; - } + if (response.ok) { + setStore('stopShapes', stopId, json); + shape = json; + } + else { + console.warn(`No shape found for ${stopId} stop:`, json); + } + } + return shape; + } - return ( - - {props.children} - - ); + return ( + + {props.children} + + ); } - -export interface BusinessDataStore { - getLine: (lineId: string) => Promise; - getLinePassages: (lineId: string) => Record; - - passages: () => Passages; - refreshPassages: (stopId: number) => Promise; - addPassages: (passages: Passages) => void; - clearPassages: () => void; - - getStop: (stopId: number) => Stop | undefined; - searchStopByName: (name: string) => Promise; -}; diff --git a/frontend/src/extra/iconHamburgerMenu.tsx b/frontend/src/extra/iconHamburgerMenu.tsx index a00a97f..d88003f 100644 --- a/frontend/src/extra/iconHamburgerMenu.tsx +++ b/frontend/src/extra/iconHamburgerMenu.tsx @@ -2,15 +2,16 @@ import { VoidComponent } from "solid-js"; // Inspired by https://github.com/hope-ui/hope-ui/blob/main/apps/docs/src/icons/IconHamburgerMenu.tsx export const IconHamburgerMenu: VoidComponent<{}> = () => { - return ( - - - ); + return ( + + + ); } diff --git a/frontend/src/index.scss b/frontend/src/index.scss index 20f4e94..37febc1 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -1,26 +1,27 @@ @font-face { - font-family: IDFVoyageur-regular; - src: url(/public/fonts/IDFVoyageur-Regular.otf) + font-family: IDFVoyageur-regular; + src: url(/public/fonts/IDFVoyageur-Regular.otf) } @font-face { - font-family: IDFVoyageur-bold; - src: url(/public/fonts/IDFVoyageur-Bold.otf); + font-family: IDFVoyageur-bold; + src: url(/public/fonts/IDFVoyageur-Bold.otf); } @font-face { - font-family: IDFVoyageur-medium; - src: url(/public/fonts/IDFVoyageur-Medium.otf); + font-family: IDFVoyageur-medium; + src: url(/public/fonts/IDFVoyageur-Medium.otf); } html, body { - aspect-ratio: 16/9; + height: 100vh; + aspect-ratio: 16/9; - margin: 0; + margin: 0; - font-family: IDFVoyageur; + font-family: IDFVoyageur; } #root { - width: inherit; + width: inherit; } diff --git a/frontend/src/passagesDisplay.scss b/frontend/src/passagesDisplay.scss index c523cfa..e1e2c97 100644 --- a/frontend/src/passagesDisplay.scss +++ b/frontend/src/passagesDisplay.scss @@ -3,65 +3,65 @@ .passagesDisplay { - @extend %widget; + @extend %widget; - .header { - $header-element-height: calc(80/100*100%); - $component-border: solid var(--idfm-white) calc(0.25*1vh); - $component-border-radius: calc(9/86*100%); - .transportMode { - @extend %transportMode; + .header { + $header-element-height: calc(80/100*100%); + $component-border: solid var(--idfm-white) calc(0.25*1vh); + $component-border-radius: calc(9/86*100%); + .transportMode { + @extend %transportMode; - height: 100%; - margin: 0; - margin-right: calc(23/1920*100%); - } + height: 100%; + margin: 0; + margin-right: calc(23/1920*100%); + } - .title { - @extend %title; - } + .title { + @extend %title; + } - .menu { - aspect-ratio: 0.75; - height: $header-element-height; + .menu { + aspect-ratio: 0.75; + height: $header-element-height; - margin-right: calc(30/1920*100%); - margin-left: auto; - border: $component-border; - border-radius: $component-border-radius; + margin-right: calc(30/1920*100%); + margin-left: auto; + border: $component-border; + border-radius: $component-border-radius; - button { - height: 100%; + button { + height: 100%; - border: 0; - color: var(--idfm-white); - background-color: transparent; + border: 0; + color: var(--idfm-white); + background-color: transparent; - .iconHamburgerMenu { - width: 75%; - } - } - } + .iconHamburgerMenu { + width: 75%; + } + } + } - .clock { - width: calc(175/1920*100%); - height: $header-element-height; + .clock { + width: calc(175/1920*100%); + height: $header-element-height; - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; - border: $component-border; - border-radius: $component-border-radius; + border: $component-border; + border-radius: $component-border-radius; - svg { - aspect-ratio: 2.45; - height: calc(0.7*100%); - } - } - } + svg { + aspect-ratio: 2.45; + height: calc(0.7*100%); + } + } + } - .body { - @extend %body - } + .body { + @extend %body + } } diff --git a/frontend/src/passagesDisplay.tsx b/frontend/src/passagesDisplay.tsx index f2f312f..58e20bb 100644 --- a/frontend/src/passagesDisplay.tsx +++ b/frontend/src/passagesDisplay.tsx @@ -1,7 +1,6 @@ import { createContext, 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 { IconButton, Menu, MenuTrigger, MenuContent, MenuItem } from "@hope-ui/solid"; import { format } from "date-fns"; import { BusinessDataContext, BusinessDataStore } from "./businessData"; @@ -9,290 +8,273 @@ import { AppContextContext, AppContextStore } from "./appContext"; import { getTransportModeSrc, PositionedPanel } from "./utils"; import { PassagesPanel } from "./passagesPanel"; -import { IconHamburgerMenu } from './extra/iconHamburgerMenu'; import "./passagesDisplay.scss"; interface PassagesDisplayStore { - isPassagesRefreshEnabled: () => boolean; - enablePassagesRefresh: () => void; - disablePassagesRefresh: () => void; - togglePassagesRefresh: () => void; + isPassagesRefreshEnabled: () => boolean; + enablePassagesRefresh: () => void; + disablePassagesRefresh: () => void; + togglePassagesRefresh: () => void; - getPanels: () => PositionedPanel[]; - setPanels: (panels: PositionedPanel[]) => void; + getPanels: () => PositionedPanel[]; + setPanels: (panels: PositionedPanel[]) => void; - getDisplayedPanelId: () => number; - setDisplayedPanelId: (panelId: number) => void; -}; + getDisplayedPanelId: () => number; + setDisplayedPanelId: (panelId: number) => void; +} const PassagesDisplayContext = createContext(); function PassagesDisplayProvider(props: { children: JSX.Element }) { - type Store = { - refreshEnabled: boolean; - panels: PositionedPanel[]; - displayedPanelId: number; - }; + type Store = { + refreshEnabled: boolean; + panels: PositionedPanel[]; + displayedPanelId: number; + }; - const [store, setStore] = createStore({ refreshEnabled: true, panels: [], displayedPanelId: 0 }); + const [store, setStore] = createStore({ refreshEnabled: true, panels: [], displayedPanelId: 0 }); - const isPassagesRefreshEnabled = (): boolean => { - return store.refreshEnabled; - } + const isPassagesRefreshEnabled = (): boolean => { + return store.refreshEnabled; + } - const enablePassagesRefresh = (): void => { - setStore('refreshEnabled', true); - } + const enablePassagesRefresh = (): void => { + setStore('refreshEnabled', true); + } - const disablePassagesRefresh = (): void => { - setStore('refreshEnabled', false); - } + const disablePassagesRefresh = (): void => { + setStore('refreshEnabled', false); + } - const togglePassagesRefresh = (): void => { - setStore('refreshEnabled', !store.refreshEnabled); - } + const togglePassagesRefresh = (): void => { + setStore('refreshEnabled', !store.refreshEnabled); + } - const getPanels = (): PositionedPanel[] => { - return store.panels; - } - const setPanels = (panels: PositionedPanel[]): void => { - setStore('panels', panels); - } + const getPanels = (): PositionedPanel[] => { + return store.panels; + } + const setPanels = (panels: PositionedPanel[]): void => { + setStore('panels', panels); + } - const getDisplayedPanelId = (): number => { - return store.displayedPanelId; - } - const setDisplayedPanelId = (panelId: number): void => { - setStore('displayedPanelId', panelId); - } + const getDisplayedPanelId = (): number => { + return store.displayedPanelId; + } + const setDisplayedPanelId = (panelId: number): void => { + setStore('displayedPanelId', panelId); + } - return ( - - {props.children} - - ); + return ( + + {props.children} + + ); } // TODO: Sort transport modes by weight const Header: VoidComponent<{ title: string }> = (props) => { - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext); + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext); - if (businessDataStore === undefined || passagesDisplayStore === undefined) - return
; + if (businessDataStore === undefined || passagesDisplayStore === undefined) + return
; - const { getLine, passages } = businessDataStore; - const { isPassagesRefreshEnabled, togglePassagesRefresh } = passagesDisplayStore; + const { getLine, passages } = businessDataStore; - const [dateNow] = createDateNow(1000); + const [dateNow] = createDateNow(1000); - const computeTransportModes = async (lineIds: string[]): Promise => { - const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId))); - const urls: Set = 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 computeTransportModes = async (lineIds: string[]): Promise => { + const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId))); + const urls: Set = 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); - createEffect(() => { - setLinesIds(Object.keys(passages())); - }); + createEffect(() => { + setLinesIds(Object.keys(passages())); + }); - return ( -
- - - {(url) => -
- -
- } -
-
-
- - - {props.title} - - -
- -
- - - {format(dateNow(), "HH:mm")} - - -
-
- ); + return
+ + + {(url) => +
+ +
+ }
+
+
+ + + {props.title} + + +
+ +
+ + + {format(dateNow(), "HH:mm")} + + +
+
; }; const Footer: VoidComponent<{}> = () => { - const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext); + const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext); - if (passagesDisplayStore === undefined) - return
; + if (passagesDisplayStore === undefined) + return
; - const { getDisplayedPanelId, getPanels } = passagesDisplayStore; + const { getDisplayedPanelId, getPanels } = passagesDisplayStore; - return ( - - ); + return ( + + ); } const Body: ParentComponent<{ maxPassagesPerPanel: number, syncPeriodMsec: number, panelSwitchPeriodMsec: number }> = (props) => { - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - const appContextStore: AppContextStore | undefined = useContext(AppContextContext); - const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext); + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + const appContextStore: AppContextStore | undefined = useContext(AppContextContext); + const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext); - if (businessDataStore === undefined || appContextStore === undefined || passagesDisplayStore === undefined) { - return
; - } + if (businessDataStore === undefined || appContextStore === undefined || passagesDisplayStore === undefined) { + return
; + } - const { getLineDestinations, passages, getPassagesLineIds, clearPassages, refreshPassages } = businessDataStore; - const { isPassagesRefreshEnabled, getDisplayedPanelId, setDisplayedPanelId, getPanels, setPanels } = passagesDisplayStore; - const { getDisplayedStops } = appContextStore; + const { getLineDestinations, passages, getPassagesLineIds, clearPassages, refreshPassages } = businessDataStore; + const { isPassagesRefreshEnabled, getDisplayedPanelId, setDisplayedPanelId, getPanels, setPanels } = passagesDisplayStore; + const { getDisplayedStops } = appContextStore; - setInterval(() => { - let nextPanelId = getDisplayedPanelId() + 1; - if (nextPanelId >= getPanels().length) { - nextPanelId = 0; - } - setDisplayedPanelId(nextPanelId); - }, props.panelSwitchPeriodMsec); + setInterval(() => { + let nextPanelId = getDisplayedPanelId() + 1; + if (nextPanelId >= getPanels().length) { + nextPanelId = 0; + } + setDisplayedPanelId(nextPanelId); + }, props.panelSwitchPeriodMsec); - setInterval( - async () => { - if (isPassagesRefreshEnabled()) { - const stops = getDisplayedStops(); - if (stops.length > 0) { - refreshPassages(stops[0].id); - } - } - else { - console.log("Passages refresh disabled... skip it."); - } - }, - props.syncPeriodMsec - ); + setInterval( + async () => { + if (isPassagesRefreshEnabled()) { + const stops = getDisplayedStops(); + if (stops.length > 0) { + refreshPassages(stops[0].id); + } + } + else { + console.log("Passages refresh disabled... skip it."); + } + }, + props.syncPeriodMsec + ); - createEffect(() => { - console.log("######### onStopIdUpdate #########"); - // Track local.stopIp to force dependency. - console.log("getDisplayedStop=", getDisplayedStops()); - clearPassages(); - }); + 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); - } - }); + createEffect(async () => { + console.log(`## OnPassageUpdate ${passages()} ##`); + const stops = getDisplayedStops(); + if (stops.length > 0) { + refreshPassages(stops[0].id); + } + }); - return ( -
- {() => { - setPanels([]); + return ( +
+ {() => { + setPanels([]); - let newPanels = []; - let positioneds: PositionedPanel[] = []; - let index = 0; + let newPanels = []; + let positioneds: PositionedPanel[] = []; + let index = 0; - let lineIds: string[] = []; - let destinationsNb = 0; + let lineIds: string[] = []; + let destinationsNb = 0; - for (const lineId of getPassagesLineIds()) { - const lineDestinations = getLineDestinations(lineId); + for (const lineId of getPassagesLineIds()) { + const lineDestinations = getLineDestinations(lineId); - if (lineDestinations.length <= props.maxPassagesPerPanel - destinationsNb) { - lineIds.push(lineId); - destinationsNb += lineDestinations.length; - } - else { - const panelid = index++; - const panel = ; - newPanels.push(panel); - positioneds.push({ position: panelid, panel: panel }); + if (lineDestinations.length <= props.maxPassagesPerPanel - destinationsNb) { + lineIds.push(lineId); + destinationsNb += lineDestinations.length; + } + else { + const panelid = index++; + const panel = ; + newPanels.push(panel); + positioneds.push({ position: panelid, panel: panel }); - lineIds = [lineId]; - destinationsNb = lineDestinations.length; - } - } - if (destinationsNb) { - const panelId = index++; - const panel = ; - newPanels.push(panel); - positioneds.push({ position: panelId, panel: panel }); - } + lineIds = [lineId]; + destinationsNb = lineDestinations.length; + } + } + if (destinationsNb) { + const panelId = index++; + const panel = ; + newPanels.push(panel); + positioneds.push({ position: panelId, panel: panel }); + } - setPanels(positioneds); + setPanels(positioneds); - return newPanels; - }} -
- ); + return newPanels; + }} +
+ ); } export const PassagesDisplay: ParentComponent = () => { - const MAX_PASSAGES_PER_PANEL = 5; + const MAX_PASSAGES_PER_PANEL = 5; - // TODO: Use props. - const syncPeriodMsec = 20 * 1000; - const panelSwitchPeriodMsec = 4 * 1000; + // TODO: Use props. + const syncPeriodMsec = 20 * 1000; + const panelSwitchPeriodMsec = 4 * 1000; - return ( -
- -
- -
- -
- ); + return
+ +
+ +
+ +
; }; diff --git a/frontend/src/passagesPanel.scss b/frontend/src/passagesPanel.scss index 1852727..b68f1b3 100644 --- a/frontend/src/passagesPanel.scss +++ b/frontend/src/passagesPanel.scss @@ -1,208 +1,208 @@ @use "_utils.scss"; .body { - .passagesContainer { - height: 100%; - width: 100%; + .passagesContainer { + height: 100%; + width: 100%; - display: none; + display: none; - position: relative; + position: relative; - /* TODO: Remove the bottom border only if there are 5 displayed lines. */ - .line:last-child { - border-bottom: 0; - /* To make up for the bottom border deletion */ - padding-bottom: calc(2px); - } + /* TODO: Remove the bottom border only if there are 5 displayed lines. */ + .line:last-child { + border-bottom: 0; + /* To make up for the bottom border deletion */ + padding-bottom: calc(2px); + } - /* Idfm: 1880x176px (margin: 0px 20px) */ - .line { - width: calc(1880/1920*100%); - height: calc(100% / 5); - margin: 0 calc(20/1920*100%); + /* Idfm: 1880x176px (margin: 0px 20px) */ + .line { + width: calc(1880/1920*100%); + height: calc(100% / 5); + margin: 0 calc(20/1920*100%); - display: flex; - align-items: center; + display: flex; + align-items: center; - /* TODO: compute the border weight according to the parent height */ - /* TODO: Disable border-bottom for the last .line */ - border-bottom: solid calc(2px); + /* TODO: compute the border weight according to the parent height */ + /* TODO: Disable border-bottom for the last .line */ + border-bottom: solid calc(2px); - svg { - font-family: IDFVoyageur-bold; - max-width: 100%; - max-height: 100%; - } + svg { + font-family: IDFVoyageur-bold; + max-width: 100%; + max-height: 100%; + } - } + } - /* Idfm: 100x100px (margin: 0px 15px) */ - .transportMode { - @extend %transportMode; + /* Idfm: 100x100px (margin: 0px 15px) */ + .transportMode { + @extend %transportMode; - height: calc(100/176*100%); - margin: 0 calc(15/1920*100%); - } + height: calc(100/176*100%); + margin: 0 calc(15/1920*100%); + } - .busLinePicto { - @extend %busLinePicto; + .busLinePicto { + @extend %busLinePicto; - height: calc(70/176*100%); - margin-right: calc(23/1920*100%); - } + height: calc(70/176*100%); + margin-right: calc(23/1920*100%); + } - .metroLinePicto, .tramLinePicto, .trainLinePicto { - aspect-ratio : 1 / 1; - height: calc(100/176*100%); - margin-right: calc(23/1920*100%); - } + .metroLinePicto, .tramLinePicto, .trainLinePicto { + aspect-ratio : 1 / 1; + height: calc(100/176*100%); + margin-right: calc(23/1920*100%); + } - .destination { - height: calc(60/176*100%); - width: 50%; + .destination { + height: calc(60/176*100%); + width: 50%; - font-family: IDFVoyageur-bold; - text-align: left; - } + font-family: IDFVoyageur-bold; + text-align: left; + } - .trafficStatus { - height: calc(50/176*100%); - aspect-ratio: 35/50; - margin-left: auto; + .trafficStatus { + height: calc(50/176*100%); + aspect-ratio: 35/50; + margin-left: auto; - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; - svg { - width: 100%; - } - } + svg { + width: 100%; + } + } - .firstPassage { - height: calc(100/176*100%); - aspect-ratio: 2.5; + .firstPassage { + height: calc(100/176*100%); + aspect-ratio: 2.5; - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; - padding-right: calc(30/1920*100%); + padding-right: calc(30/1920*100%); - /* TODO: compute the border weight according to the parent width */ - border-right: solid calc(5px); + /* TODO: compute the border weight according to the parent width */ + border-right: solid calc(5px); - svg { - aspect-ratio: 215/50; - height: calc(50%); - } - } + svg { + aspect-ratio: 215/50; + height: calc(50%); + } + } - .unavailableFirstPassage { - height: calc(100/176*100%); - aspect-ratio: calc(230/100); + .unavailableFirstPassage { + height: calc(100/176*100%); + aspect-ratio: calc(230/100); - /* TODO: compute the border weight according to the parent width */ - border-right: solid calc(5px); - } + /* TODO: compute the border weight according to the parent width */ + border-right: solid calc(5px); + } - .secondPassage { - height: calc(45/176*100%); - aspect-ratio: calc(230/45); - margin-right: calc(30/1920*100%); + .secondPassage { + height: calc(45/176*100%); + aspect-ratio: calc(230/45); + margin-right: calc(30/1920*100%); - svg { - font-family: IDFVoyageur-regular; - } - } + svg { + font-family: IDFVoyageur-regular; + } + } - .unavailableSecondPassage { - height: calc(100/176*100%); - aspect-ratio: calc(230/100); - margin-right: calc(30/1920*100%); + .unavailableSecondPassage { + height: calc(100/176*100%); + aspect-ratio: calc(230/100); + margin-right: calc(30/1920*100%); - svg { - font-family: IDFVoyageur-regular; - } - } + svg { + font-family: IDFVoyageur-regular; + } + } - %withPlatformPassage { - height: calc(120/176*100%); + %withPlatformPassage { + height: calc(120/176*100%); - display: flex; - flex-direction: column; - } + display: flex; + flex-direction: column; + } - .withPlatformFirstPassage { - @extend %withPlatformPassage; + .withPlatformFirstPassage { + @extend %withPlatformPassage; - aspect-ratio: 250/120; + aspect-ratio: 250/120; - padding-right: calc(30/1920*100%); - /* TODO: compute the border weight according to the parent width */ - border-right: solid calc(5px); + padding-right: calc(30/1920*100%); + /* TODO: compute the border weight according to the parent width */ + border-right: solid calc(5px); - .passage { - aspect-ratio: 215/50; - height: calc(1/2*100%); + .passage { + aspect-ratio: 215/50; + height: calc(1/2*100%); - font-family: IDFVoyageur-bold; - margin-top: calc(5/176*100%); - } + font-family: IDFVoyageur-bold; + margin-top: calc(5/176*100%); + } - .platform { - margin-top: auto; - margin-bottom: calc(5/176*100%); + .platform { + margin-top: auto; + margin-bottom: calc(5/176*100%); - rect { - background-color: var(--idfm-black); - } + rect { + background-color: var(--idfm-black); + } - text { - vertical-align: middle; - font-family: IDFVoyageur-bold; - } - } - } + text { + vertical-align: middle; + font-family: IDFVoyageur-bold; + } + } + } - .withPlatformSecondPassage { - @extend %withPlatformPassage; + .withPlatformSecondPassage { + @extend %withPlatformPassage; - aspect-ratio: 215/120; + aspect-ratio: 215/120; - align-items: end; - justify-content: center; + align-items: end; + justify-content: center; - margin-right: calc(30/1920*100%); + margin-right: calc(30/1920*100%); - .passage { - aspect-ratio: 215/45; - height: calc(45/120*100%); - /* 5px + (first passage font size - second passage font size/2) to align passages... */ - /* There must exist a better way to align them. */ - margin-top: calc(7.5/176*100%); - } + .passage { + aspect-ratio: 215/45; + height: calc(45/120*100%); + /* 5px + (first passage font size - second passage font size/2) to align passages... */ + /* There must exist a better way to align them. */ + margin-top: calc(7.5/176*100%); + } - svg { - font-family: IDFVoyageur-regular; - } + svg { + font-family: IDFVoyageur-regular; + } - .platform { - rect { - background-color: var(--idfm-black); - } + .platform { + rect { + background-color: var(--idfm-black); + } - text { - vertical-align: middle; - font-family: IDFVoyageur-bold; - } - } - } + text { + vertical-align: middle; + font-family: IDFVoyageur-bold; + } + } + } - } + } - .displayed { - display: block; - } + .displayed { + display: block; + } } diff --git a/frontend/src/passagesPanel.tsx b/frontend/src/passagesPanel.tsx index 143111a..41f6d15 100644 --- a/frontend/src/passagesPanel.tsx +++ b/frontend/src/passagesPanel.tsx @@ -12,169 +12,158 @@ import "./passagesPanel.scss"; const UnavailablePassage: VoidComponent<{ style: string }> = (props) => { - const textStyle = { fill: "#000000" }; + const textStyle = { fill: "#000000" }; - return ( -
- - Information - non - disponible - -
- ); + return
+ + Information + non + disponible + +
; } const Platform: VoidComponent<{ name: string }> = (props) => { - const platformTextPaddingPx: number = 20; - const viewBoxWidthPx: number = 215; + const platformTextPaddingPx: number = 20; + const viewBoxWidthPx: number = 215; - let rectRef: SVGSVGElement | undefined = undefined; - let textRef: SVGTextElement | undefined = undefined; + let rectRef: SVGSVGElement | undefined = undefined; + let textRef: SVGTextElement | undefined = undefined; - onMount(() => { - if (rectRef !== undefined && textRef !== undefined) { - const textWidth = textRef.getComputedTextLength(); - const rectWidth = textWidth + platformTextPaddingPx * 2; - rectRef.setAttribute("width", `${rectWidth}px`); - rectRef.setAttribute("x", `${viewBoxWidthPx - rectWidth}px`); - textRef.setAttribute("x", `${viewBoxWidthPx - platformTextPaddingPx}px`); - } - }); + onMount(() => { + if (rectRef !== undefined && textRef !== undefined) { + const textWidth = textRef.getComputedTextLength(); + const rectWidth = textWidth + platformTextPaddingPx * 2; + rectRef.setAttribute("width", `${rectWidth}px`); + rectRef.setAttribute("x", `${viewBoxWidthPx - rectWidth}px`); + textRef.setAttribute("x", `${viewBoxWidthPx - platformTextPaddingPx}px`); + } + }); - return ( - - - - QUAI {props.name} - - - ); + return + + + QUAI {props.name} + + ; } const TtwPassage: VoidComponent<{ - line: Line, destination: string, index: number, style: string, - withPlatformStyle: string, fontSize: number, fallbackStyle: string + line: Line, destination: string, index: number, style: string, + withPlatformStyle: string, fontSize: number, fallbackStyle: string }> = (props) => { - const businessDataContext: BusinessDataStore | undefined = useContext(BusinessDataContext); - if (businessDataContext === undefined) - return
; + const businessDataContext: BusinessDataStore | undefined = useContext(BusinessDataContext); + if (businessDataContext === undefined) + return
; - const { getDestinationPassages } = businessDataContext; + const { getDestinationPassages } = businessDataContext; - const [dateNow] = createDateNow(10000); + const [dateNow] = createDateNow(10000); - const transition: AnimationOptions = { duration: 3, repeat: Infinity }; + const transition: AnimationOptions = { duration: 3, repeat: Infinity }; - return (() => { - const passage = getDestinationPassages(props.line.id, props.destination)[props.index]; + return (() => { + const passage = getDestinationPassages(props.line.id, props.destination)[props.index]; - const refTs = passage !== undefined ? (passage.expectedDepartTs !== null ? passage.expectedDepartTs : passage.expectedArrivalTs) : 0; - const ttwSec = refTs - (getTime(dateNow()) / 1000); - const ttwRepr = ttwSec < 3600 ? `${Math.floor(ttwSec / 60).toString().padStart(2, "0")} min` : format(refTs * 1000, "HH:mm"); - const isApproaching = ttwSec <= 60; + const refTs = passage !== undefined ? (passage.expectedDepartTs !== null ? passage.expectedDepartTs : passage.expectedArrivalTs) : 0; + const ttwSec = refTs - (getTime(dateNow()) / 1000); + const ttwRepr = ttwSec < 3600 ? `${Math.floor(ttwSec / 60).toString().padStart(2, "0")} min` : format(refTs * 1000, "HH:mm"); + const isApproaching = ttwSec <= 60; - const text = - - {ttwRepr} - - ; + const text = + + {ttwRepr} + + ; - return ( - > - - {text} -
}> -
- {text} - -
- - - ); - }); + return }> + + {text} +
}> +
+ {text} + +
+ + ; + }); } /* TODO: Manage end of service */ const DestinationPassages: VoidComponent<{ line: Line, destination: string }> = (props) => { - /* TODO: Find where to get data to compute traffic status. */ - const trafficStatusColor = new Map([ - [TrafficStatus.UNKNOWN, "#ffffff"], - [TrafficStatus.FLUID, "#00643c"], - [TrafficStatus.DISRUPTED, "#ffbe00"], - [TrafficStatus.VERY_DISRUPTED, "#ff5a00"], - [TrafficStatus.BYPASSED, "#ffffff"] - ]); + /* TODO: Find where to get data to compute traffic status. */ + const trafficStatusColor = new Map([ + [TrafficStatus.UNKNOWN, "#ffffff"], + [TrafficStatus.FLUID, "#00643c"], + [TrafficStatus.DISRUPTED, "#ffbe00"], + [TrafficStatus.VERY_DISRUPTED, "#ff5a00"], + [TrafficStatus.BYPASSED, "#ffffff"] + ]); - // TODO: Manage traffic status - // const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) }; - const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) }; + // TODO: Manage traffic status + // const trafficStatusStyle = { fill: trafficStatusColor.get(props.line.trafficStatus) }; + const trafficStatusStyle = { fill: trafficStatusColor.get(TrafficStatus.UNKNOWN) }; - return ( -
-
- {renderLineTransportMode(props.line)} -
- {renderLinePicto(props.line)} -
- -
-
- - - -
- - -
- ); + return
+
+ {renderLineTransportMode(props.line)} +
+ {renderLinePicto(props.line)} +
+ +
+
+ + + +
+ + +
; } export type PassagesPanelComponentProps = ParentProps & { stopId: number, lineIds: string[], show: boolean }; export type PassagesPanelComponent = ParentComponent; export const PassagesPanel: PassagesPanelComponent = (props) => { - const businessDataContext: BusinessDataStore | undefined = useContext(BusinessDataContext); - if (businessDataContext === undefined) - return
; + const businessDataContext: BusinessDataStore | undefined = useContext(BusinessDataContext); + if (businessDataContext === undefined) + return
; - const { getLine, getLineDestinations } = businessDataContext; + const { getLine, getLineDestinations } = businessDataContext; - const getLines = async (lineIds: string[]): Promise => { - const lines = await Promise.all[]>(lineIds.map((lineId) => getLine(lineId))); - return lines; - } - const [lines] = createResource(props.lineIds, getLines); + const getLines = async (lineIds: string[]): Promise => { + const lines = await Promise.all[]>(lineIds.map((lineId) => getLine(lineId))); + return lines; + } + const [lines] = createResource(props.lineIds, getLines); - return ( -
- - - {(line) => - - - {(destination) => } - - - } - - -
- ); + return
+ + + {(line) => + + { + (destination) => + } + + } + +
; } diff --git a/frontend/src/search.tsx b/frontend/src/search.tsx index 3a49e07..99b7c35 100644 --- a/frontend/src/search.tsx +++ b/frontend/src/search.tsx @@ -1,60 +1,58 @@ -import { batch, createContext, JSX } 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'; export type ByStopIdMarkers = Record; export interface SearchStore { - getFoundStops: () => Stop[]; - setFoundStops: (stops: Stop[]) => void; + getFoundStops: () => Stop[]; + setFoundStops: (stops: Stop[]) => void; - getDisplayedStops: () => Stop[]; - setDisplayedStops: (stops: Stop[]) => void; + getDisplayedStops: () => Stop[]; + setDisplayedStops: (stops: Stop[]) => void; - addMarkers: (stopId: number, markers: LeafletMarker[]) => void; + addMarkers: (stopId: number, markers: LeafletMarker[]) => void; }; export const SearchContext = createContext(); export function SearchProvider(props: { children: JSX.Element }) { - type Store = { - foundStops: Stop[]; - markers: ByStopIdMarkers; - displayedStops: Stop[]; - }; + type Store = { + foundStops: Stop[]; + markers: ByStopIdMarkers; + displayedStops: Stop[]; + }; - const [store, setStore] = createStore({ foundStops: [], markers: {}, displayedStops: [] }); + const [store, setStore] = createStore({ foundStops: [], markers: {}, displayedStops: [] }); - const getFoundStops = (): Stop[] => { - return store.foundStops; - } + const getFoundStops = (): Stop[] => { + return store.foundStops; + } - const setFoundStops = (stops: Stop[]): void => { - setStore('foundStops', stops); - } + const setFoundStops = (stops: Stop[]): void => { + setStore('foundStops', stops); + } - const getDisplayedStops = (): Stop[] => { - return store.displayedStops; - } + const getDisplayedStops = (): Stop[] => { + return store.displayedStops; + } - const setDisplayedStops = (stops: Stop[]): void => { - setStore((s: Store) => { - setStore('displayedStops', stops); - return s; - }); - } + const setDisplayedStops = (stops: Stop[]): void => { + setStore((s: Store) => { + setStore('displayedStops', stops); + return s; + }); + } - const addMarkers = (stopId: number, markers: L.Marker[]): void => { - setStore('markers', stopId, markers); - } + const addMarkers = (stopId: number, markers: L.Marker[]): void => { + setStore('markers', stopId, markers); + } - return ( - - {props.children} - - ); + return + {props.children} + ; } diff --git a/frontend/src/stopsSearchMenu/map.scss b/frontend/src/stopsSearchMenu/map.scss index d744b2a..ce08320 100644 --- a/frontend/src/stopsSearchMenu/map.scss +++ b/frontend/src/stopsSearchMenu/map.scss @@ -3,85 +3,85 @@ .map { - position: relative; + position: relative; - height: 100%; - width: 50%; + height: 100%; + width: 50%; - .ol-viewport { - @extend %body; - position: absolute; - margin: 0; - } + .ol-viewport { + @extend %body; + position: absolute; + margin: 0; + } - .popup { - @extend %body; - margin: 0; + .popup { + @extend %body; + margin: 0; - position: absolute; - width: 100%; - height: 35%; + position: absolute; + width: 100%; + height: 35%; - border: solid var(--idfm-white) calc(0.2*1vh); + border: solid var(--idfm-white) calc(0.2*1vh); - background-color: var(--idfm-black); + background-color: var(--idfm-black); - z-index: 1; - visibility: hidden; + z-index: 1; + visibility: hidden; - .header { - @extend %header; + .header { + @extend %header; - color: var(--idfm-white); - } + color: var(--idfm-white); + } - .body { - @extend %body; + .body { + @extend %body; - scroll-snap-type: y mandatory; - overflow-y: scroll; + scroll-snap-type: y mandatory; + overflow-y: scroll; - .line { - scroll-snap-align: center; + .line { + scroll-snap-align: center; - height: calc(100% / 3); - margin: 0 calc(10/1920*100%); + height: calc(100% / 3); + margin: 0 calc(10/1920*100%); - display: flex; - flex-direction: row; - align-items: center; + display: flex; + flex-direction: row; + align-items: center; - font-family: IDFVoyageur-bold; + font-family: IDFVoyageur-bold; - .busLinePicto { - @extend %busLinePicto; + .busLinePicto { + @extend %busLinePicto; - height: 80%; - width: 30%; - } + height: 80%; + width: 30%; + } - .name { - width: 100%; - height: 60%; - } + .name { + width: 100%; + height: 60%; + } - div { - height: 100%; + div { + height: 100%; - svg { - max-width: 100%; - max-height: 100%; - } - } - } - } + svg { + max-width: 100%; + max-height: 100%; + } + } + } + } - .footer { - @extend %footer; - } - } + .footer { + @extend %footer; + } + } - .displayed { - visibility: visible; - } + .displayed { + visibility: visible; + } } diff --git a/frontend/src/stopsSearchMenu/map.tsx b/frontend/src/stopsSearchMenu/map.tsx index 7293ef1..2fd8869 100644 --- a/frontend/src/stopsSearchMenu/map.tsx +++ b/frontend/src/stopsSearchMenu/map.tsx @@ -24,192 +24,196 @@ import "./map.scss"; export const Map: ParentComponent<{}> = () => { - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - const searchStore: SearchStore | undefined = useContext(SearchContext); - if (businessDataStore === undefined || searchStore === undefined) - return
; + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + const searchStore: SearchStore | undefined = useContext(SearchContext); + if (businessDataStore === undefined || searchStore === undefined) + return
; - const { getStop } = businessDataStore; - const { getAllMapFeatures, getFoundStops, getHighlightedStop } = searchStore; + const { getStop } = businessDataStore; + const { getAllMapFeatures, getFoundStops, getHighlightedStop } = searchStore; - const [selectedMapStop, setSelectedMapStop] = createSignal(undefined); - const [isPopupDisplayed, setPopupDisplayed] = createSignal(false); + const [selectedMapStop, setSelectedMapStop] = createSignal(undefined); + const [isPopupDisplayed, setPopupDisplayed] = createSignal(false); - const mapCenter = [260769.80336542107, 6250587.867330259]; // EPSG:3857 - const fitDurationMs = 1500; - const flashDurationMs = 2000; - // TODO: Set padding according to the marker design. - const fitPointsPadding = [50, 50, 50, 50]; + const mapCenter = [260769.80336542107, 6250587.867330259]; // EPSG:3857 + const fitDurationMs = 1500; + const flashDurationMs = 2000; + // TODO: Set padding according to the marker design. + const fitPointsPadding = [50, 50, 50, 50]; - let mapDiv: HTMLDivElement | undefined = undefined; - let popup: StopPopup | undefined = undefined; + let mapDiv: HTMLDivElement | undefined; + let popup: StopPopup | undefined = undefined; - const stopVectorSource = new OlVectorSource({ features: [] }); - const stopVectorLayer = new OlVectorLayer({ source: stopVectorSource }); + const stopVectorSource = new OlVectorSource({ features: [] }); + const stopVectorLayer = new OlVectorLayer({ source: stopVectorSource }); - let overlay: OlOverlay | undefined = undefined; - let map: OlMap | undefined = undefined; + let overlay: OlOverlay | undefined = undefined; + let map: OlMap | undefined = undefined; - const displayedFeatures: Record = {}; + const displayedFeatures: Record = {}; - 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 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], + }); + console.log("map=", map); + map.on('singleclick', onClickedMap); + } - const onClickedMap = async (event): Promise => { - 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 onClickedMap = async (event): Promise => { + 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 => { - const stopId: number = feature.getId(); - const stop = getStop(stopId); - // TODO: Handle StopArea (use center given by the backend) - if (stop?.epsg3857_x !== undefined && stop?.epsg3857_y !== undefined) { - setSelectedMapStop(stop); - map?.getView().animate( - { - center: [stop.epsg3857_x, stop.epsg3857_y], - duration: 1000 - }, - // Display the popup once the animation finished - () => setPopupDisplayed(true) - ); - } - } + const onClickedFeature = async (feature: OlFeatureLike): Promise => { + const stopId: number = feature.getId(); + const stop = getStop(stopId); + // TODO: Handle StopArea (use center given by the backend) + if (stop?.epsg3857_x !== undefined && stop?.epsg3857_y !== undefined) { + setSelectedMapStop(stop); + map?.getView().animate( + { + center: [stop.epsg3857_x, stop.epsg3857_y], + duration: 1000 + }, + // Display the popup once the animation finished + () => setPopupDisplayed(true) + ); + } + } - onMount(() => buildMap(mapDiv)); + onMount(() => { + buildMap(mapDiv); + }) + ; - // Filling the map with stops shape - createEffect(() => { - const stops = getFoundStops(); - const foundStopIds = new Set(); - for (const foundStop of stops) { - foundStopIds.add(foundStop.id); - if (foundStop.stops !== undefined) { - foundStop.stops.forEach(s => foundStopIds.add(s.id)); - } - } + // Filling the map with stops shape + createEffect(() => { + const stops = getFoundStops(); + const foundStopIds = new Set(); + for (const foundStop of stops) { + foundStopIds.add(foundStop.id); + if (foundStop.stops !== undefined) { + foundStop.stops.forEach(s => foundStopIds.add(s.id)); + } + } - for (const [stopIdStr, feature] of Object.entries(displayedFeatures)) { - const stopId = parseInt(stopIdStr); - if (!foundStopIds.has(stopId)) { - console.log(`Remove feature for ${stopId}`); - stopVectorSource.removeFeature(feature); - delete displayedFeatures[stopId]; - } - } + for (const [stopIdStr, feature] of Object.entries(displayedFeatures)) { + const stopId = parseInt(stopIdStr); + if (!foundStopIds.has(stopId)) { + console.log(`Remove feature for ${stopId}`); + stopVectorSource.removeFeature(feature); + delete displayedFeatures[stopId]; + } + } - const features = getAllMapFeatures(); - for (const [stopIdStr, feature] of Object.entries(features)) { - const stopId = parseInt(stopIdStr); - if (foundStopIds.has(stopId) && !(stopId in displayedFeatures)) { - console.log(`Add feature for ${stopId}`); - stopVectorSource.addFeature(feature); - displayedFeatures[stopId] = feature; - } - } + const features = getAllMapFeatures(); + for (const [stopIdStr, feature] of Object.entries(features)) { + const stopId = parseInt(stopIdStr); + if (foundStopIds.has(stopId) && !(stopId in displayedFeatures)) { + console.log(`Add feature for ${stopId}`); + stopVectorSource.addFeature(feature); + displayedFeatures[stopId] = feature; + } + } - const extend = stopVectorSource.getExtent(); - if (map !== undefined && !isEmptyExtend(extend)) { - map.getView().fit(extend, { duration: fitDurationMs, padding: fitPointsPadding }); - } - }); + const extend = stopVectorSource.getExtent(); + if (map !== undefined && !isEmptyExtend(extend)) { + map.getView().fit(extend, { duration: fitDurationMs, padding: fitPointsPadding }); + } + }); - // Flashing effect - createEffect(() => { - const highlightedStopId = getHighlightedStop()?.id; - if (highlightedStopId !== undefined) { - const stop = getStop(highlightedStopId); - if (stop !== undefined) { - const stops = stop.stops ? stop.stops : [stop]; - stops.forEach((s) => { - const feature = displayedFeatures[s.id]; - if (feature !== undefined) { - flash(feature); - } - }); - } - } - }); + // Flashing effect + createEffect(() => { + const highlightedStopId = getHighlightedStop()?.id; + if (highlightedStopId !== undefined) { + const stop = getStop(highlightedStopId); + if (stop !== undefined) { + const stops = stop.stops ? stop.stops : [stop]; + stops.forEach((s) => { + const feature = displayedFeatures[s.id]; + if (feature !== undefined) { + flash(feature); + } + }); + } + } + }); - const flash = (feature: OlFeature) => { - const start = Date.now(); - const flashGeom = feature.getGeometry()?.clone(); - const listenerKey = stopVectorLayer.on('postrender', animate); + const flash = (feature: OlFeature) => { + const start = Date.now(); + const flashGeom = feature.getGeometry()?.clone(); + const listenerKey = stopVectorLayer.on('postrender', animate); - // Force postrender raising. - feature.changed(); + // Force postrender raising. + feature.changed(); - function animate(event) { - const frameState = event.frameState; - const elapsed = frameState.time - start; - const vectorContext = getVectorContext(event); + function animate(event) { + const frameState = event.frameState; + const elapsed = frameState.time - start; + const vectorContext = getVectorContext(event); - if (elapsed >= flashDurationMs) { - unByKey(listenerKey); - return; - } + if (elapsed >= flashDurationMs) { + unByKey(listenerKey); + return; + } - if (flashGeom !== undefined && map !== undefined) { - const elapsedRatio = elapsed / flashDurationMs; - // radius will be 5 at start and 30 at end. - const radius = easeOut(elapsedRatio) * 25 + 5; - const opacity = easeOut(1 - elapsedRatio); + if (flashGeom !== undefined && map !== undefined) { + const elapsedRatio = elapsed / flashDurationMs; + // radius will be 5 at start and 30 at end. + const radius = easeOut(elapsedRatio) * 25 + 5; + const opacity = easeOut(1 - elapsedRatio); - const style = new Style({ - image: new Circle({ - radius: radius, - stroke: new Stroke({ - color: `rgba(255, 0, 0, ${opacity})`, - width: 0.25 + opacity, - }), - }), - }); + const style = new Style({ + image: new Circle({ + radius: radius, + stroke: new Stroke({ + color: `rgba(255, 0, 0, ${opacity})`, + width: 0.25 + opacity, + }), + }), + }); - vectorContext.setStyle(style); - vectorContext.drawGeometry(flashGeom); + vectorContext.setStyle(style); + vectorContext.drawGeometry(flashGeom); - // tell OpenLayers to continue postrender animation - map.render(); - } - } - } + // tell OpenLayers to continue postrender animation + map.render(); + } + } + } - return <> -
- -
- {(stop) => } - ; + return <> +
+ +
+ {(stop) => } + ; } diff --git a/frontend/src/stopsSearchMenu/mapStop.tsx b/frontend/src/stopsSearchMenu/mapStop.tsx index e2dc744..f3c08a8 100644 --- a/frontend/src/stopsSearchMenu/mapStop.tsx +++ b/frontend/src/stopsSearchMenu/mapStop.tsx @@ -11,75 +11,75 @@ import { SearchContext, SearchStore } from "./searchStore"; // TODO: Use boolean to set MapStop selected export const MapStop: VoidComponent<{ stop: Stop, selected: Stop | undefined }> = (props) => { - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - const searchStore: SearchStore | undefined = useContext(SearchContext); - if (businessDataStore === undefined || searchStore === undefined) - return
; + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + const searchStore: SearchStore | undefined = useContext(SearchContext); + if (businessDataStore === undefined || searchStore === undefined) + return
; - const { getStopShape } = businessDataStore; - const { setMapFeature } = searchStore; + const { getStopShape } = businessDataStore; + const { setMapFeature } = searchStore; - const stopStyle = new Style({ - image: new Circle({ - fill: undefined, - stroke: new Stroke({ color: '#3399CC', width: 1.5 }), - radius: 10, - }), - }); + const stopStyle = new Style({ + image: new Circle({ + fill: undefined, + stroke: new Stroke({ color: '#3399CC', width: 1.5 }), + radius: 10, + }), + }); - const selectedStopStyle = new Style({ - image: new Circle({ - fill: undefined, - stroke: new Stroke({ color: 'purple', width: 2 }), - radius: 10, - }), - }); + const selectedStopStyle = new Style({ + image: new Circle({ + fill: undefined, + stroke: new Stroke({ color: 'purple', width: 2 }), + radius: 10, + }), + }); - const stopAreaStyle = new Style({ - stroke: new Stroke({ color: 'red' }), - fill: new Fill({ color: 'rgba(255,255,255,0.2)' }), - }); + const stopAreaStyle = new Style({ + stroke: new Stroke({ color: 'red' }), + fill: new Fill({ color: 'rgba(255,255,255,0.2)' }), + }); - const getShape = async (stopId: number): Promise => { - return await getStopShape(stopId); - }; - const [shape] = createResource(props.stop.id, getShape); + const getShape = async (stopId: number): Promise => { + return await getStopShape(stopId); + }; + const [shape] = createResource(props.stop.id, getShape); - createEffect(() => { - const shape_ = shape(); + createEffect(() => { + const shape_ = shape(); - if (shape_ === undefined) { - return; - } + if (shape_ === undefined) { + return; + } - let feature = undefined; + let feature = undefined; - if (props.stop.epsg3857_x !== undefined && props.stop.epsg3857_y !== undefined) { - const selectStopStyle = () => { - return (props.selected?.id === props.stop.id ? selectedStopStyle : stopStyle); - } - feature = new OlFeature({ - geometry: new OlPoint([props.stop.epsg3857_x, props.stop.epsg3857_y]), - }); - feature.setStyle(selectStopStyle); - } + if (props.stop.epsg3857_x !== undefined && props.stop.epsg3857_y !== undefined) { + const selectStopStyle = () => { + return (props.selected?.id === props.stop.id ? selectedStopStyle : stopStyle); + } + feature = new OlFeature({ + geometry: new OlPoint([props.stop.epsg3857_x, props.stop.epsg3857_y]), + }); + feature.setStyle(selectStopStyle); + } - else { - let geometry = undefined; - const areaShape = shape(); - if (areaShape !== undefined) { - geometry = new OlPolygon([areaShape.epsg3857_points.slice(0, -1)]); - } - else { - geometry = new OlPoint([props.stop.epsg3857_x, props.stop.epsg3857_y]); - } - feature = new OlFeature({ geometry: geometry }); - feature.setStyle(stopAreaStyle); - } - feature.setId(props.stop.id); + else { + let geometry = undefined; + const areaShape = shape(); + if (areaShape !== undefined) { + geometry = new OlPolygon([areaShape.epsg3857_points.slice(0, -1)]); + } + else { + geometry = new OlPoint([props.stop.epsg3857_x, props.stop.epsg3857_y]); + } + feature = new OlFeature({ geometry: geometry }); + feature.setStyle(stopAreaStyle); + } + feature.setId(props.stop.id); - setMapFeature(props.stop.id, feature); - }); + setMapFeature(props.stop.id, feature); + }); - return {stop => }; + return {stop => }; } diff --git a/frontend/src/stopsSearchMenu/searchStore.tsx b/frontend/src/stopsSearchMenu/searchStore.tsx index e666eed..e7c0ec9 100644 --- a/frontend/src/stopsSearchMenu/searchStore.tsx +++ b/frontend/src/stopsSearchMenu/searchStore.tsx @@ -10,27 +10,27 @@ type ByStopIdMapFeatures = Record; export interface SearchStore { - getSearchText: () => string; - setSearchText: (text: string, businessDataStore: BusinessDataStore) => Promise; + getSearchText: () => string; + setSearchText: (text: string, businessDataStore: BusinessDataStore) => Promise; - getFoundStops: () => Stop[]; - setFoundStops: (stops: Stop[]) => void; + getFoundStops: () => Stop[]; + setFoundStops: (stops: Stop[]) => void; - getDisplayedPanelId: () => number; - setDisplayedPanelId: (panelId: number) => void; + getDisplayedPanelId: () => number; + setDisplayedPanelId: (panelId: number) => void; - getPanels: () => PositionedPanel[]; - setPanels: (panels: PositionedPanel[]) => void; + getPanels: () => PositionedPanel[]; + setPanels: (panels: PositionedPanel[]) => void; - getHighlightedStop: () => Stop | undefined; - setHighlightedStop: (stop: Stop) => void; - resetHighlightedStop: () => void; + getHighlightedStop: () => Stop | undefined; + setHighlightedStop: (stop: Stop) => void; + resetHighlightedStop: () => void; - enableMap: (enable: boolean) => void; - isMapEnabled: () => boolean; - getMapFeature: (stopId: number) => OlFeature | undefined; - getAllMapFeatures: () => ByStopIdMapFeatures; - setMapFeature: (stopId: number, feature: OlFeature) => void; + enableMap: (enable: boolean) => void; + isMapEnabled: () => boolean; + getMapFeature: (stopId: number) => OlFeature | undefined; + getAllMapFeatures: () => ByStopIdMapFeatures; + setMapFeature: (stopId: number, feature: OlFeature) => void; }; export const SearchContext = createContext(); @@ -38,128 +38,126 @@ export const SearchContext = createContext(); export function SearchProvider(props: { children: JSX.Element }) { - const searchTextDelayMs = 1500; + const searchTextDelayMs = 1500; - type Store = { - searchText: string; - searchPromise: Promise | undefined; - foundStops: Stop[]; - displayedPanelId: number; - panels: PositionedPanel[]; - highlightedStop: Stop | undefined; - mapEnabled: boolean; - mapFeatures: ByStopIdMapFeatures; - }; + type Store = { + searchText: string; + searchPromise: Promise | undefined; + foundStops: Stop[]; + displayedPanelId: number; + panels: PositionedPanel[]; + highlightedStop: Stop | undefined; + mapEnabled: boolean; + mapFeatures: ByStopIdMapFeatures; + }; - const [store, setStore] = createStore({ - searchText: "", - searchPromise: undefined, - foundStops: [], - displayedPanelId: 0, - panels: [], - highlightedStop: undefined, - // mapEnabled: false, - mapFeatures: {}, - }); + const [store, setStore] = createStore({ + searchText: "", + searchPromise: undefined, + foundStops: [], + displayedPanelId: 0, + panels: [], + highlightedStop: undefined, + // mapEnabled: false, + mapFeatures: {}, + }); - const getSearchText = (): string => { - return store.searchText; - } + const getSearchText = (): string => { + return store.searchText; + } - const debounce = async (fn: (...args: any[]) => Promise, delayMs: number) => { - let timerId: number; - return new Promise((...args) => { - clearTimeout(timerId); - timerId = setTimeout(fn, delayMs, ...args); - }); - } + const debounce = async (fn: (...args: any[]) => Promise, delayMs: number) => { + let timerId: number; + return new Promise((...args) => { + clearTimeout(timerId); + timerId = setTimeout(fn, delayMs, ...args); + }); + } - const setSearchText = async (text: string, businessDataStore: BusinessDataStore): Promise => { - setStore('searchText', text); + const setSearchText = async (text: string, businessDataStore: BusinessDataStore): Promise => { + setStore('searchText', text); - if (store.searchPromise === undefined) { - const { searchStopByName } = businessDataStore; - const promise: Promise = debounce(async (onSuccess: () => void) => { - console.log(`Fetching data for "${store.searchText}" stop name`); - const stopsById = await searchStopByName(store.searchText); - console.log("stopsById=", stopsById); - setFoundStops(Object.values(stopsById)); - onSuccess(); - }, searchTextDelayMs).then(() => { - setStore('searchPromise', undefined); - }); - setStore('searchPromise', promise); - } - } + if (store.searchPromise === undefined) { + const { searchStopByName } = businessDataStore; + const promise: Promise = debounce(async (onSuccess: () => void) => { + console.log(`Fetching data for "${store.searchText}" stop name`); + const stopsById = await searchStopByName(store.searchText); + console.log("stopsById=", stopsById); + setFoundStops(Object.values(stopsById)); + onSuccess(); + }, searchTextDelayMs).then(() => { + setStore('searchPromise', undefined); + }); + setStore('searchPromise', promise); + } + } - const getFoundStops = (): Stop[] => { - return store.foundStops; - } + const getFoundStops = (): Stop[] => { + return store.foundStops; + } - const setFoundStops = (stops: Stop[]): void => { - setStore('foundStops', stops); - } + const setFoundStops = (stops: Stop[]): void => { + setStore('foundStops', stops); + } - const getDisplayedPanelId = (): number => { - return store.displayedPanelId; - } + const getDisplayedPanelId = (): number => { + return store.displayedPanelId; + } - const setDisplayedPanelId = (panelId: number): void => { - setStore('displayedPanelId', panelId); - } + const setDisplayedPanelId = (panelId: number): void => { + setStore('displayedPanelId', panelId); + } - const getPanels = (): PositionedPanel[] => { - return store.panels; - } + const getPanels = (): PositionedPanel[] => { + return store.panels; + } - const setPanels = (panels: PositionedPanel[]): void => { - setStore('panels', panels); - } + const setPanels = (panels: PositionedPanel[]): void => { + setStore('panels', panels); + } - const getHighlightedStop = (): Stop | undefined => { - return store.highlightedStop; - } + const getHighlightedStop = (): Stop | undefined => { + return store.highlightedStop; + } - const setHighlightedStop = (stop: Stop): void => { - setStore('highlightedStop', stop); - } + const setHighlightedStop = (stop: Stop): void => { + setStore('highlightedStop', stop); + } - const resetHighlightedStop = (): void => { - setStore('highlightedStop', undefined); - } + const resetHighlightedStop = (): void => { + setStore('highlightedStop', undefined); + } - const enableMap = (enable: boolean): void => { - setStore("mapEnabled", enable); - } + const enableMap = (enable: boolean): void => { + setStore("mapEnabled", enable); + } - const isMapEnabled = (): boolean => { - return store.mapEnabled; - } + const isMapEnabled = (): boolean => { + return store.mapEnabled; + } - const getAllMapFeatures = (): ByStopIdMapFeatures => { - return store.mapFeatures; - } + const getAllMapFeatures = (): ByStopIdMapFeatures => { + return store.mapFeatures; + } - const getMapFeature = (stopId: number): OlFeature | undefined => { - return store.mapFeatures[stopId]; - } + const getMapFeature = (stopId: number): OlFeature | undefined => { + return store.mapFeatures[stopId]; + } - const setMapFeature = (stopId: number, feature: OlFeature): void => { - setStore('mapFeatures', stopId, feature); - }; + const setMapFeature = (stopId: number, feature: OlFeature): void => { + setStore('mapFeatures', stopId, feature); + }; - return ( - - {props.children} - - ); + return + {props.children} + ; } diff --git a/frontend/src/stopsSearchMenu/stopPanel.scss b/frontend/src/stopsSearchMenu/stopPanel.scss index 0f3986e..fb64023 100644 --- a/frontend/src/stopsSearchMenu/stopPanel.scss +++ b/frontend/src/stopsSearchMenu/stopPanel.scss @@ -3,98 +3,98 @@ .stopPanel { - scroll-snap-align: center; + scroll-snap-align: center; - .stop { - width: calc(1880/1920*100%); - height: calc(100% / 5); - // margin: 0 calc(20/1920*100%); - margin: 0 calc(10/1920*100%); + .stop { + width: calc(1880/1920*100%); + height: calc(100% / 5); + // margin: 0 calc(20/1920*100%); + margin: 0 calc(10/1920*100%); - display: flex; - align-items: center; - flex-direction: row; + display: flex; + align-items: center; + flex-direction: row; - /* TODO: compute the border weight according to the parent height */ - /* TODO: Disable border-bottom for the last .line */ - border-bottom: solid calc(2px); + /* TODO: compute the border weight according to the parent height */ + /* TODO: Disable border-bottom for the last .line */ + border-bottom: solid calc(2px); - cursor: default; - - .name { - margin-left: calc(40/1920*100%); - width: 60%; - aspect-ratio: 2.5; - - display: flex; - align-items: center; + cursor: default; - font-family: IDFVoyageur-bold; - } + .name { + margin-left: calc(40/1920*100%); + width: 60%; + aspect-ratio: 2.5; - .lineRepr { - width: 40%; - aspect-ratio: 2.5; + display: flex; + align-items: center; - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; + font-family: IDFVoyageur-bold; + } - .transportMode { - @extend %transportMode; + .lineRepr { + width: 40%; + aspect-ratio: 2.5; - height: 50%; - } + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; - .linesRepresentationMatrix { - @extend %busLinePicto; // Use the larger picto aspect-ratio - width: 75%; - aspect-ratio: 3; + .transportMode { + @extend %transportMode; - display: flex; - flex-flow: row; - flex-wrap: wrap; + height: 50%; + } - %picto { - margin-left: 1%; - align-self: center; - justify-self: center; - } + .linesRepresentationMatrix { + @extend %busLinePicto; // Use the larger picto aspect-ratio + width: 75%; + aspect-ratio: 3; - %singleLinePicto { - @extend %picto; + display: flex; + flex-flow: row; + flex-wrap: wrap; - height: 80%; - } + %picto { + margin-left: 1%; + align-self: center; + justify-self: center; + } - .transportMode { - @extend %transportMode; - @extend %picto; - } + %singleLinePicto { + @extend %picto; - .tramLinePicto { - @extend %tramLinePicto; - @extend %singleLinePicto; - } + height: 80%; + } - .trainLinePicto { - @extend %trainLinePicto; - @extend %singleLinePicto; - } + .transportMode { + @extend %transportMode; + @extend %picto; + } - .metroLinePicto { - @extend %metroLinePicto; - @extend %singleLinePicto; - } + .tramLinePicto { + @extend %tramLinePicto; + @extend %singleLinePicto; + } - .busLinePicto { - @extend %busLinePicto; - @extend %picto; + .trainLinePicto { + @extend %trainLinePicto; + @extend %singleLinePicto; + } - height: 40%; - } - } - } - } + .metroLinePicto { + @extend %metroLinePicto; + @extend %singleLinePicto; + } + + .busLinePicto { + @extend %busLinePicto; + @extend %picto; + + height: 40%; + } + } + } + } } diff --git a/frontend/src/stopsSearchMenu/stopPanel.tsx b/frontend/src/stopsSearchMenu/stopPanel.tsx index 2c0d903..f95a60d 100644 --- a/frontend/src/stopsSearchMenu/stopPanel.tsx +++ b/frontend/src/stopsSearchMenu/stopPanel.tsx @@ -11,133 +11,124 @@ import "./stopPanel.scss"; const StopRepr: VoidComponent<{ stop: Stop }> = (props) => { - const fontSize: number = 40; + const fontSize: number = 40; - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - if (businessDataStore === undefined) - return
; + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + if (businessDataStore === undefined) + return
; - const { getLine } = businessDataStore; + const { getLine } = businessDataStore; - const fetchLinesRepr = async (lineIds: string[]): Promise => { - const reprs = []; - for (const lineId of lineIds) { - const line = await getLine(lineId); - if (line !== undefined) { - reprs.push(
{renderLineTransportMode(line)}
); - reprs.push(renderLinePicto(line)); - } - } - return reprs; - } + const fetchLinesRepr = async (lineIds: string[]): Promise => { + const reprs = []; + for (const lineId of lineIds) { + const line = await getLine(lineId); + if (line !== undefined) { + reprs.push(
{renderLineTransportMode(line)}
); + reprs.push(renderLinePicto(line)); + } + } + return reprs; + } - const [lineReprs] = createResource(props.stop.lines, fetchLinesRepr); + const [lineReprs] = createResource(props.stop.lines, fetchLinesRepr); - return ( -
- - - {props.stop.name} - - - {(line: JSX.Element) => line} -
- ); + return
+ + + {props.stop.name} + + + {(line: JSX.Element) => line} +
; } type ByTransportModeReprs = { - mode: JSX.Element | undefined; - lines: Record; + mode: JSX.Element | undefined; + lines: Record; } const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => { - const fontSize: number = 10; + const fontSize: number = 10; - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - const appContextStore: AppContextStore | undefined = useContext(AppContextContext); - const searchStore: SearchStore | undefined = useContext(SearchContext); + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + const appContextStore: AppContextStore | undefined = useContext(AppContextContext); + const searchStore: SearchStore | undefined = useContext(SearchContext); - if (businessDataStore === undefined || appContextStore === undefined || searchStore === undefined) - return
; + if (businessDataStore === undefined || appContextStore === undefined || searchStore === undefined) + return
; - const { getLine } = businessDataStore; - const { setDisplayedStops } = appContextStore; - const { setHighlightedStop, resetHighlightedStop } = searchStore; + const { getLine } = businessDataStore; + const { setDisplayedStops } = appContextStore; + const { setHighlightedStop, resetHighlightedStop } = searchStore; - const fetchLinesRepr = async (stop: Stop): Promise => { - const lineIds = new Set(stop.lines); - const stops = stop.stops; - for (const stop of stops) { - stop.lines.forEach(lineIds.add, lineIds); - } + const fetchLinesRepr = async (stop: Stop): Promise => { + const lineIds = new Set(stop.lines); + const stops = stop.stops; + for (const stop of stops) { + stop.lines.forEach(lineIds.add, lineIds); + } - const byModeReprs: Record = {}; - for (const lineId of lineIds) { - const line = await getLine(lineId); - if (line !== undefined) { - if (!(line.transportMode in byModeReprs)) { - byModeReprs[line.transportMode] = { - mode:
{renderLineTransportMode(line)}
, - lines: {} - }; - } - byModeReprs[line.transportMode].lines[line.shortName] = renderLinePicto(line); - } - } + const byModeReprs: Record = {}; + for (const lineId of lineIds) { + const line = await getLine(lineId); + if (line !== undefined) { + if (!(line.transportMode in byModeReprs)) { + byModeReprs[line.transportMode] = { + mode:
{renderLineTransportMode(line)}
, + lines: {} + }; + } + byModeReprs[line.transportMode].lines[line.shortName] = renderLinePicto(line); + } + } - const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < - TransportModeWeights[y] ? 1 : -1); + const sortedTransportModes = Object.keys(byModeReprs).sort((x, y) => TransportModeWeights[x] < TransportModeWeights[y] ? 1 : -1); - return ( -
- {(transportMode) => { - const reprs = byModeReprs[transportMode]; - const lineNames = Object.keys(reprs.lines).sort((x, y) => x.localeCompare(y)); - return <> - {reprs.mode} -
- {(lineName) => reprs.lines[lineName]} -
- - }} -
-
- ); - } - const [lineReprs] = createResource(props.stop, fetchLinesRepr); + return
+ + {(transportMode) => { + const reprs = byModeReprs[transportMode]; + const lineNames = Object.keys(reprs.lines).sort((x, y) => x.localeCompare(y)); + return <> + {reprs.mode} +
+ {(lineName) => reprs.lines[lineName]} +
+ ; + }}
+
; + } - return ( -
setDisplayedStops([props.stop])} - onMouseEnter={() => setHighlightedStop(props.stop)} - onMouseLeave={resetHighlightedStop} - > -
- -
- {lineReprs()} -
- ); + const [lineReprs] = createResource(props.stop, fetchLinesRepr); + + return
setDisplayedStops([props.stop])} + onMouseEnter={() => setHighlightedStop(props.stop)} + onMouseLeave={resetHighlightedStop} + > +
+ +
+ {lineReprs()} +
; } export const StopsPanel: ParentComponent<{ stops: Stop[], show: boolean }> = (props) => { - return ( -
- x.name.localeCompare(y.name))}> - {(stop) => { - return }> - - ; - }} - -
- ); + return
+ x.name.localeCompare(y.name))}> + {(stop) => { + return }> + + ; + }} +
; } diff --git a/frontend/src/stopsSearchMenu/stopPopup.tsx b/frontend/src/stopsSearchMenu/stopPopup.tsx index 6c0f278..122db5d 100644 --- a/frontend/src/stopsSearchMenu/stopPopup.tsx +++ b/frontend/src/stopsSearchMenu/stopPopup.tsx @@ -5,45 +5,43 @@ import { BusinessDataContext, BusinessDataStore } from "../businessData"; import { renderLinePicto, ScrollingText } from '../utils'; export const StopPopup: ParentComponent<{ stop: Stop, show: boolean }> = (props) => { - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - if (businessDataStore === undefined) - return
; + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + if (businessDataStore === undefined) + return
; - const { getLine, getStopDestinations } = businessDataStore; + const { getLine, getStopDestinations } = businessDataStore; - let popupDiv: HTMLDivElement | undefined = undefined; + let popupDiv: HTMLDivElement | undefined = undefined; - const getDestinations = async (stop: Stop): Promise<{ lineId: string, destinations: string[] }[]> => { - let ret = []; + const getDestinations = async (stop: Stop): Promise<{ lineId: string, destinations: string[] }[]> => { + let ret = []; - if (stop !== undefined) { - const result = await getStopDestinations(stop.id); - for (const [lineId, destinations] of Object.entries(result)) { - const line = await getLine(lineId); - const linePicto = renderLinePicto(line); - ret.push({ lineId: linePicto, destinations: destinations }); - } - } + if (stop !== undefined) { + const result = await getStopDestinations(stop.id); + for (const [lineId, destinations] of Object.entries(result)) { + const line = await getLine(lineId); + const linePicto = renderLinePicto(line); + ret.push({ lineId: linePicto, destinations: destinations }); + } + } - return ret; - } - const [destinations] = createResource(() => props.stop, getDestinations); + return ret; + } + const [destinations] = createResource(() => props.stop, getDestinations); - return ( -
-
{props.stop?.name}
-
- - {(dst) => { - return
- {dst.lineId} -
- -
-
; - }} -
-
-
- ); + + return
+
{props.stop?.name}
+
+ + {(dst) => { + return
+ {dst.lineId} + < div class="name" > + +
+
; + }} +
+
; } diff --git a/frontend/src/stopsSearchMenu/stopsSearchMenu.scss b/frontend/src/stopsSearchMenu/stopsSearchMenu.scss index 3a439d0..e17b66f 100644 --- a/frontend/src/stopsSearchMenu/stopsSearchMenu.scss +++ b/frontend/src/stopsSearchMenu/stopsSearchMenu.scss @@ -3,83 +3,84 @@ .mapPlaceholder { - --border-width: 0.1vh; + --border-width: 0.1vh; - height: calc(100% - 2*var(--border-width)); - width: 50%; + height: calc(100% - 2*var(--border-width)); + width: 50%; - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; - border: solid var(--neutral-color) var(--border-width); - border-radius: var(--border-radius); + border: solid var(--neutral-color) var(--border-width); + border-radius: var(--border-radius); - background-color: var(--idfm-black); - font-family: IDFVoyageur-bold; - font-size: 2vh; - color: var(--idfm-white); + background-color: var(--idfm-black); + font-family: IDFVoyageur-bold; + font-size: 2vh; + color: var(--idfm-white); } .stopNameInput { - width: 50%; - height: 60%; + width: 50%; + height: 60%; - display: flex; - flex-flow: row; + display: flex; + flex-flow: row; - border: solid var(--neutral-color) calc(0.01vh); - border-radius: var(--border-radius); + border: solid var(--neutral-color) calc(0.01vh); + border-radius: var(--border-radius); - background-color: transparent; + background-color: transparent; - .leftAddon { - width: 17%; - display: flex; - align-items: center; - justify-content: center; + .leftAddon { + width: 17%; - background-color: var(--idfm-white); - } + display: flex; + align-items: center; + justify-content: center; - input { - width: 83%; + background-color: var(--idfm-white); + } - padding-left: 3%; - padding-right: 3%; - - color: var(--idfm-white); - font-family: IDFVoyageur-regular; - background-color: transparent; - } + input { + width: 83%; + + padding-left: 3%; + padding-right: 3%; + + color: var(--idfm-white); + font-family: IDFVoyageur-regular; + background-color: transparent; + } } .title { - @extend %title; - - display: flex; - justify-content: center; + @extend %title; + + display: flex; + justify-content: center; } .stopSearchMenu { - @extend %widget; - - .body { - @extend %body; + @extend %widget; - flex-direction: row; + .body { + @extend %body; - .stopsPanels { - width: 50%; - height: 100%; + flex-direction: row; - scroll-snap-type: y mandatory; - overflow-y: scroll; + .stopsPanels { + width: 50%; + height: 100%; - .displayed { - display: block; - } - } - } + scroll-snap-type: y mandatory; + overflow-y: scroll; + + .displayed { + display: block; + } + } + } } diff --git a/frontend/src/stopsSearchMenu/stopsSearchMenu.tsx b/frontend/src/stopsSearchMenu/stopsSearchMenu.tsx index bb5fcdf..0d3eb97 100644 --- a/frontend/src/stopsSearchMenu/stopsSearchMenu.tsx +++ b/frontend/src/stopsSearchMenu/stopsSearchMenu.tsx @@ -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 { createScrollPosition } from "@solid-primitives/scroll"; @@ -16,185 +16,181 @@ import "./stopsSearchMenu.scss"; const StopNameInput: VoidComponent<{ onInput: JSX.EventHandler, leftAddon: string, placeholder: string }> = (props) => { - return ( -
-
{props.leftAddon}
- -
); + return
+
{props.leftAddon}
+ +
; }; const Header: VoidComponent<{ title: string, minCharsNb: number }> = (props) => { - const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); - const searchStore: SearchStore | undefined = useContext(SearchContext); + const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); + const searchStore: SearchStore | undefined = useContext(SearchContext); - if (businessDataStore === undefined || searchStore === undefined) - return
; + if (businessDataStore === undefined || searchStore === undefined) + return
; - const { setSearchText } = searchStore; + const { setSearchText } = searchStore; - const onStopNameInput: JSX.EventHandler = async (event): Promise => { - const stopName = event.currentTarget.value; - if (stopName.length >= props.minCharsNb) { - await setSearchText(stopName, businessDataStore); - } - } + const onStopNameInput: JSX.EventHandler = async (event): Promise => { + const stopName = event.currentTarget.value; + if (stopName.length >= props.minCharsNb) { + await setSearchText(stopName, businessDataStore); + } + } - return ( -
-
- - - {props.title} - - -
- -
- ); + return
+
+ + + {props.title} + + +
+ +
; }; const StopsPanels: ParentComponent<{ maxStopsPerPanel: number }> = (props) => { - const searchStore: SearchStore | undefined = useContext(SearchContext); + const searchStore: SearchStore | undefined = useContext(SearchContext); - if (searchStore === undefined) { - return
; - } + if (searchStore === undefined) { + return
; + } - const { getDisplayedPanelId, getFoundStops, getPanels, setDisplayedPanelId, setPanels } = searchStore; - let stopsPanelsRef: HTMLDivElement | undefined = undefined - const stopsPanelsScroll = createScrollPosition(() => stopsPanelsRef); - const yStopsPanelsScroll = () => stopsPanelsScroll.y; + const { getDisplayedPanelId, getFoundStops, getPanels, setDisplayedPanelId, setPanels } = searchStore; + let stopsPanelsRef: HTMLDivElement | undefined = undefined + const stopsPanelsScroll = createScrollPosition(() => stopsPanelsRef); + const yStopsPanelsScroll = () => stopsPanelsScroll.y; - createEffect(() => { - yStopsPanelsScroll(); + createEffect(() => { + yStopsPanelsScroll(); - for (const panel of getPanels()) { - const panelDiv = panel.panel(); - const panelDivClientRect = panelDiv.getBoundingClientRect(); - if (panelDivClientRect.y > 0) { - setDisplayedPanelId(panel.position); - break; - } - } - }); + for (const panel of getPanels()) { + const panelDiv = panel.panel; + if (panelDiv != null) { + const panelDivClientRect = panelDiv.getBoundingClientRect(); + if (panelDivClientRect.y > 0) { + setDisplayedPanelId(panel.position); + break; + } + } + } + }); - return ( -
- {() => { - setPanels([]); + return ( +
+ {() => { + setPanels([]); - let newPanels = []; - let positioneds: PositionedPanel[] = []; + let newPanels = []; + let positioneds: PositionedPanel[] = []; - let stops: Stop[] = []; + let stops: Stop[] = []; - for (const stop of getFoundStops()) { - if (stops.length < props.maxStopsPerPanel) { - stops.push(stop); - } - else { - const panelId = newPanels.length; - const panel = ; - newPanels.push(panel); - positioneds.push({ position: panelId, panel: panel }); - stops = [stop]; - } - } - if (stops.length) { - const panelId = newPanels.length; - const panel = ; - newPanels.push(panel); - positioneds.push({ position: panelId, panel: panel }); - } + for (const stop of getFoundStops()) { + if (stops.length < props.maxStopsPerPanel) { + stops.push(stop); + } + else { + const panelId = newPanels.length; + const panel = ; + newPanels.push(panel); + positioneds.push({ position: panelId, panel: panel }); + stops = [stop]; + } + } + if (stops.length) { + const panelId = newPanels.length; + const panel = ; + newPanels.push(panel); + positioneds.push({ position: panelId, panel: panel }); + } - setPanels(positioneds); + setPanels(positioneds); - return newPanels; - }} -
- ); + return newPanels; + }} +
+ ); }; const MapPlaceholder: VoidComponent<{}> = () => { - const searchStore: SearchStore | undefined = useContext(SearchContext); + const searchStore: SearchStore | undefined = useContext(SearchContext); - if (searchStore === undefined) { - return
; - } + if (searchStore === undefined) { + return
; + } - const { enableMap } = searchStore; + const { enableMap } = searchStore; - const onDoubleClick = (): void => { - console.log('!!! ON DOUBLE CLICK'); - enableMap(true); - } + const onDoubleClick = (): void => { + console.log('!!! ON DOUBLE CLICK'); + enableMap(true); + } - return
onDoubleClick()}> - Double-clic pour activer la carte -
; + return
onDoubleClick()}> + Double-clic pour activer la carte +
; }; const Body: VoidComponent<{}> = () => { - const searchStore: SearchStore | undefined = useContext(SearchContext); - if (searchStore === undefined) { - return
; - } + const searchStore: SearchStore | undefined = useContext(SearchContext); + if (searchStore === undefined) { + return
; + } - const { isMapEnabled } = searchStore; + const { isMapEnabled } = searchStore; - const maxStopsPerPanel = 5; + const maxStopsPerPanel = 5; - return
- - }> - - -
; + return
+ + }> + + +
; }; const Footer: VoidComponent<{}> = () => { - const searchStore: SearchStore | undefined = useContext(SearchContext); - if (searchStore === undefined) { - return
; - } + const searchStore: SearchStore | undefined = useContext(SearchContext); + if (searchStore === undefined) { + return
; + } - const { getDisplayedPanelId, getPanels } = searchStore; + const { getDisplayedPanelId, getPanels } = searchStore; - return ( - - ); + return ; }; export const StopsSearchMenu: VoidComponent = () => { - return ( -
- -
- -
- -
- ); + return
+ +
+ +
+ +
; }; diff --git a/frontend/src/types.tsx b/frontend/src/types.tsx index 63d68d9..ac39f40 100644 --- a/frontend/src/types.tsx +++ b/frontend/src/types.tsx @@ -1,116 +1,116 @@ export enum TrafficStatus { - UNKNOWN = 0, - FLUID, - DISRUPTED, - VERY_DISRUPTED, - BYPASSED + UNKNOWN = 0, + FLUID, + DISRUPTED, + VERY_DISRUPTED, + BYPASSED } export class Passage { - line: number; - operator: number; - destinations: string[]; - atStop: boolean; - aimedArrivalTs: number; - expectedArrivalTs: number; - arrivalPlatformName: string; - aimedDepartTs: number; - expectedDepartTs: number; - arrivalStatus: string; - departStatus: string; + line: number; + operator: number; + destinations: string[]; + atStop: boolean; + aimedArrivalTs: number; + expectedArrivalTs: number; + arrivalPlatformName: string; + aimedDepartTs: number; + expectedDepartTs: number; + arrivalStatus: string; + departStatus: string; - constructor(line: number, operator: number, destinations: 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; - } -}; + constructor(line: number, operator: number, destinations: 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 type Passages = Record>; export class Stop { - id: number; - name: string; - town: string; - epsg3857_x: number; - epsg3857_y: number; - stops: Stop[]; - lines: number[]; + id: number; + name: string; + town: string; + epsg3857_x: number; + epsg3857_y: number; + stops: Stop[]; + lines: number[]; - constructor(id: number, name: string, town: string, epsg3857_x: number, epsg3857_y: number, stops: Stop[], lines: number[]) { - this.id = id; - this.name = name; - this.town = town; - this.epsg3857_x = epsg3857_x; - this.epsg3857_y = epsg3857_y; - this.stops = stops; - this.lines = lines; - for (const stop of this.stops) { - this.lines.push(...stop.lines); - } - } -}; + constructor(id: number, name: string, town: string, epsg3857_x: number, epsg3857_y: number, stops: Stop[], lines: number[]) { + this.id = id; + this.name = name; + this.town = town; + this.epsg3857_x = epsg3857_x; + this.epsg3857_y = epsg3857_y; + this.stops = stops; + this.lines = lines; + for (const stop of this.stops) { + this.lines.push(...stop.lines); + } + } +} export type Stops = Record; export type Points = [number, number][]; export class StopShape { - stop_id: number; - type_: number; - epsg3857_bbox: number[]; - epsg3857_points: Points; + stop_id: number; + type_: number; + epsg3857_bbox: number[]; + epsg3857_points: Points; - constructor(stop_id: number, type_: number, epsg3857_bbox: number[], epsg3857_points: Points) { - this.stop_id = stop_id; - this.type_ = type_; - this.epsg3857_bbox = epsg3857_bbox; - this.epsg3857_points = epsg3857_points; - } -}; + constructor(stop_id: number, type_: number, epsg3857_bbox: number[], epsg3857_points: Points) { + this.stop_id = stop_id; + this.type_ = type_; + this.epsg3857_bbox = epsg3857_bbox; + this.epsg3857_points = epsg3857_points; + } +} export type StopShapes = Record; export class Line { - id: number; - 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: number[]; + id: number; + 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: number[]; - constructor(id: number, shortName: string, name: string, status: string, transportMode: string, backColorHexa: string, - foreColorHexa: string, operatorId: number, accessibility: boolean, visualSignsAvailable: string, - audibleSignsAvailable: string, stopIds: 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; - } -}; + constructor(id: number, shortName: string, name: string, status: string, transportMode: string, backColorHexa: string, + foreColorHexa: string, operatorId: number, accessibility: boolean, visualSignsAvailable: string, + audibleSignsAvailable: string, stopIds: 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; + } +} export type Lines = Record; diff --git a/frontend/src/utils.tsx b/frontend/src/utils.tsx index 2828ac7..e302a51 100644 --- a/frontend/src/utils.tsx +++ b/frontend/src/utils.tsx @@ -5,162 +5,156 @@ import { Line } from './types'; // Thanks to https://dev.to/ycmjason/how-to-create-range-in-javascript-539i export function* range(start: number, end: number): Generator { - for (let i = start; i <= end; i++) { - yield i; - } + for (let i = start; i <= end; i++) { + yield i; + } } const validTransportModes = ["bus", "tram", "metro", "rer", "transilien", "funicular", "ter", "unknown"]; export const TransportModeWeights: Record = { - bus: 1, - tram: 2, - val: 3, - funicular: 4, - metro: 5, - rer: 6, - transilien: 7, - ter: 8, + bus: 1, + tram: 2, + val: 3, + funicular: 4, + metro: 5, + rer: 6, + transilien: 7, + ter: 8, }; export function getTransportModeSrc(mode: string, color: boolean = true): string | undefined { - let ret = undefined; - if (validTransportModes.includes(mode)) { - return `/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`; - } - return ret; + let ret = undefined; + if (validTransportModes.includes(mode)) { + return `/symbole_${mode}_${color ? "" : "support_fonce_"}RVB.svg`; + } + return ret; } export function renderLineTransportMode(line: Line): JSX.Element { - return + return } function renderBusLinePicto(line: Line): JSX.Element { - return ( -
- - - - {line.shortName} - - -
- ); + return
+ + + + {line.shortName} + + +
; } function renderTramLinePicto(line: Line): JSX.Element { - const lineStyle = { fill: `#${line.backColorHexa}` }; - return ( -
- - - - - {line.shortName} - - -
- ); + const lineStyle = { fill: `#${line.backColorHexa}` }; + return
+ + + + + {line.shortName} + + +
; } function renderMetroLinePicto(line: Line): JSX.Element { - return ( -
- - - - {line.shortName} - - -
- ); + return
+ + + + {line.shortName} + + +
; } function renderTrainLinePicto(line: Line): JSX.Element { - return ( -
- - - - {line.shortName} - - -
- ); + return
+ + + + {line.shortName} + + +
; } export function renderLinePicto(line: Line): JSX.Element { - switch (line.transportMode) { - case "bus": - case "funicular": - return renderBusLinePicto(line); - case "tram": - return renderTramLinePicto(line); - /* case "val": */ - case "metro": - return renderMetroLinePicto(line); - case "transilien": - case "rer": - case "ter": - return renderTrainLinePicto(line); - } + switch (line.transportMode) { + case "bus": + case "funicular": + return renderBusLinePicto(line); + case "tram": + return renderTramLinePicto(line); + /* case "val": */ + case "metro": + return renderMetroLinePicto(line); + case "transilien": + case "rer": + case "ter": + return renderTrainLinePicto(line); + } } export type PositionedPanel = { - position: number; - // TODO: Should be PassagesPanelComponent ? - panel: JSX.Element; + position: number; + // TODO: Should be PassagesPanelComponent ? + panel: JSX.Element; }; export const ScrollingText: VoidComponent<{ height: number, width: number, content: string }> = (props) => { - let viewBoxRef: SVGSVGElement | undefined = undefined; - let textRef: SVGTextElement | undefined = undefined; + let viewBoxRef: SVGSVGElement | undefined = undefined; + let textRef: SVGTextElement | undefined = undefined; - onMount(() => { - if (viewBoxRef !== undefined && textRef !== undefined) { - const overlap = textRef.getComputedTextLength() - viewBoxRef.viewBox.baseVal.width; - if (overlap > 0) { - timeline( - [ - [textRef, { x: [-overlap] }, { duration: 5 }], - [textRef, { x: [0] }, { duration: 2 }], - ], - { repeat: Infinity }, - ); - } - } - }); + onMount(() => { + if (viewBoxRef !== undefined && textRef !== undefined) { + const overlap = textRef.getComputedTextLength() - viewBoxRef.viewBox.baseVal.width; + if (overlap > 0) { + timeline( + [ + [textRef, { x: [-overlap] }, { duration: 5 }], + [textRef, { x: [0] }, { duration: 2 }], + ], + { repeat: Infinity }, + ); + } + } + }); - return ( - - - {props.content} - - - ); + return + + {props.content} + + ; } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d0acf50..baf7527 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,22 +1,23 @@ { - "compilerOptions": { - "jsx": "preserve", - "jsxImportSource": "solid-js", - "noImplicitAny": true, - "target": "ES6", - "moduleResolution": "node", - "allowJs": true, - "outDir": "build", - "strict": true, - "types": ["vite/client"], - "noEmit": true, - "isolatedModules": true, - "plugins": [ - { - "name": "typescript-eslint-language-service" - } - ], - "lib": ["ES2021", "DOM"], - }, - "include": ["src"] + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "noImplicitAny": true, + "target": "ES6", + "module": "esnext", + "moduleResolution": "node", + "allowJs": true, + "outDir": "build", + "strict": true, + "types": ["vite/client"], + "noEmit": true, + "isolatedModules": true, + "plugins": [ + { + "name": "typescript-eslint-language-service" + } + ], + "lib": ["ES2021", "DOM"], + }, + "include": ["src"] }