import { createContext, createEffect, createResource, For, JSX, onMount, ParentComponent, Show, useContext, VoidComponent } from 'solid-js'; import { createStore } from "solid-js/store"; import { createScrollPosition } from "@solid-primitives/scroll"; import { Input, InputLeftAddon, InputGroup } from "@hope-ui/solid"; import { featureGroup as leafletFeatureGroup, LatLngLiteral as LeafletLatLngLiteral, Map as LeafletMap, Marker as LeafletMarker, tileLayer as leafletTileLayer } from 'leaflet'; import { Stop } from './types'; import { PositionedPanel, renderLineTransportMode, renderLinePicto, TransportModeWeights } from './utils'; import { AppContextContext, AppContextStore } from "./appContext"; import { BusinessDataContext, BusinessDataStore } from "./businessData"; import "leaflet/dist/leaflet.css"; import "./stopsSearchMenu.scss"; type ByStopIdMarkers = Record; interface SearchStore { getSearchText: () => string; setSearchText: (text: string, businessDataStore: BusinessDataStore) => Promise; isSearchInProgress: () => boolean; getFoundStops: () => Stop[]; setFoundStops: (stops: Stop[]) => void; getDisplayedPanelId: () => number; setDisplayedPanelId: (panelId: number) => void; addMarkers: (stopId: number, markers: LeafletMarker[]) => void; getPanels: () => PositionedPanel[]; setPanels: (panels: PositionedPanel[]) => void; }; const SearchContext = createContext(); function SearchProvider(props: { children: JSX.Element }) { type Store = { searchText: string; searchInProgress: boolean; foundStops: Stop[]; markers: ByStopIdMarkers; displayedPanelId: number; panels: PositionedPanel[]; }; const [store, setStore] = createStore({ searchText: "", searchInProgress: false, foundStops: [], markers: {}, displayedPanelId: 0, panels: [] }); const getSearchText = (): string => { return store.searchText; } const setSearchText = async (text: string, businessDataStore: BusinessDataStore): Promise => { setStore('searchInProgress', true); setStore('searchText', text); const { searchStopByName } = businessDataStore; console.log("store.searchText=", store.searchText); const stopsById = await searchStopByName(store.searchText); console.log("stopsById=", stopsById); setFoundStops(Object.values(stopsById)); setStore('searchInProgress', false); } const isSearchInProgress = (): boolean => { return store.searchInProgress; } const getFoundStops = (): Stop[] => { return store.foundStops; } const setFoundStops = (stops: Stop[]): void => { setStore('foundStops', stops); } const getDisplayedPanelId = (): number => { return store.displayedPanelId; } const setDisplayedPanelId = (panelId: number): void => { setStore('displayedPanelId', panelId); } const addMarkers = (stopId: number, markers: L.Marker[]): void => { setStore('markers', stopId, markers); } const getPanels = (): PositionedPanel[] => { return store.panels; } const setPanels = (panels: PositionedPanel[]): void => { setStore('panels', panels); } return ( {props.children} ); } const Header: VoidComponent<{ title: string, minCharsNb: number }> = (props) => { const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); const searchStore: SearchStore | undefined = useContext(SearchContext); if (businessDataStore === undefined || searchStore === undefined) return
; const { isSearchInProgress, setSearchText } = searchStore; const onStopNameInput: JSX.EventHandler = async (event): Promise => { /* TODO: Add a tempo before fetching stop for giving time to user to finish his request */ const stopName = event.currentTarget.value; if (stopName.length >= props.minCharsNb) { console.log(`Fetching data for "${stopName}" stop name`); await setSearchText(stopName, businessDataStore); } } return (
{props.title}
🚉 🚏
); }; const StopRepr: VoidComponent<{ stop: Stop }> = (props) => { const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); if (businessDataStore === undefined) return
; 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 [lineReprs] = createResource(props.stop.lines, fetchLinesRepr); return (
{props.stop.name}
{(line: JSX.Element) => line}
); } type ByTransportModeReprs = { mode: JSX.Element | undefined; lines: Record; }; const StopAreaRepr: VoidComponent<{ stop: Stop }> = (props) => { const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); const appContextStore: AppContextStore | undefined = useContext(AppContextContext); if (businessDataStore === undefined || appContextStore === undefined) return
; const { getLine } = businessDataStore; const { setDisplayedStops } = appContextStore; 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 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 (
setDisplayedStops([props.stop])}>
{props.stop.name}
{lineReprs()}
); } const StopsPanel: ParentComponent<{ stops: Stop[], show: boolean }> = (props) => { return (
x.name.localeCompare(y.name))}> {(stop) => { return }> ; }}
); } const StopsPanels: ParentComponent<{ maxStopsPerPanel: number }> = (props) => { const searchStore: SearchStore | undefined = useContext(SearchContext); if (searchStore === undefined) { return
; } const { getDisplayedPanelId, getFoundStops, getPanels, setDisplayedPanelId, setPanels } = searchStore; let stopsPanelsRef: HTMLDivElement | undefined = undefined const stopsPanelsScroll = createScrollPosition(() => stopsPanelsRef); const yStopsPanelsScroll = () => stopsPanelsScroll.y; createEffect(() => { yStopsPanelsScroll(); for (const panel of Object.values(getPanels())) { if (panel.panel) { const panelDiv = panel.panel(); const panelDivClientRect = panelDiv.getBoundingClientRect(); if (panelDivClientRect.y > 0) { setDisplayedPanelId(panel.position); break; } } } }); return (
{() => { setPanels([]); let newPanels = []; let positioneds: PositionedPanel[] = []; 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 }); } setPanels(positioneds); return newPanels; }}
); } const Map: VoidComponent<{}> = () => { const mapCenter: LeafletLatLngLiteral = { lat: 48.853, lng: 2.35 }; const searchStore: SearchStore | undefined = useContext(SearchContext); if (searchStore === undefined) return
; const { addMarkers, getFoundStops } = searchStore; let mapDiv: any; let map: LeafletMap | undefined = undefined; const stopsLayerGroup = leafletFeatureGroup(); const buildMap = (div: HTMLDivElement) => { map = new LeafletMap(div).setView(mapCenter, 11); leafletTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); stopsLayerGroup.addTo(map); } const setMarker = (stop: Stop): L.Marker[] => { const markers = []; if (stop.lat !== undefined && stop.lon !== undefined) { /* TODO: Add stop lines representation to popup. */ markers.push(new LeafletMarker([stop.lat, stop.lon]).bindPopup(`${stop.name}`).openPopup()); } else { for (const _stop of stop.stops) { markers.push(...setMarker(_stop)); } } return markers; } onMount(() => buildMap(mapDiv)); createEffect(() => { /* TODO: Avoid to clear all layers... */ stopsLayerGroup.clearLayers(); for (const stop of getFoundStops()) { const markers = setMarker(stop); addMarkers(stop.id, markers); for (const marker of markers) { stopsLayerGroup.addLayer(marker); } } const stopsBound = stopsLayerGroup.getBounds(); if (map !== undefined && Object.keys(stopsBound).length) { map.fitBounds(stopsBound); } }); return
; } const Footer: VoidComponent<{}> = () => { const searchStore: SearchStore | undefined = useContext(SearchContext); if (searchStore === undefined) { return
; } const { getDisplayedPanelId, getPanels } = searchStore; return ( ); } export const StopsSearchMenu: VoidComponent = () => { const MAX_STOPS_PER_PANEL = 5; return (
); };