diff --git a/src/data/transport-data.tsx b/src/data/transport-data.tsx index b82b646..3391540 100644 --- a/src/data/transport-data.tsx +++ b/src/data/transport-data.tsx @@ -139,6 +139,9 @@ export type TransportData = { geoData: Accessor; filter: Accessor; + selectedEntry: Accessor; + setSelectedEntry: (entry: SelectedEntry) => void; + rightPopover: Accessor; closeRightPopover: () => void; setSelectedBusStop: (id: string) => void; @@ -169,6 +172,15 @@ export type PopOverBusStop = { }; export type PopOverState = PopOverBusStop | null; +export type SelectedEntry = { + category: "Bus Stop" | "Bus Route"; + id: string; + name: string; + latitude?: number; + longitude?: number; + color?: string; +} | null; + export const TransportDataContext = createContext(); export const useTransportData = () => useContext(TransportDataContext)!; @@ -190,6 +202,8 @@ export const TransportDataProvider: ParentComponent = (props) => { selectedRouteIds: new Set(), selectedRouteTypes: new Set(), }); + const [selectedEntry, setSelectedEntry] = + createSignal(null); onMount(async () => { // Load route data @@ -314,6 +328,8 @@ export const TransportDataProvider: ParentComponent = (props) => { geoData, filter, rightPopover, + selectedEntry, + setSelectedEntry, setSelectedBusStop: (id: string) => { const data = tjData().data; if (data === undefined) return; diff --git a/src/ui/home/map/map.tsx b/src/ui/home/map/map.tsx index 96ae924..6883514 100644 --- a/src/ui/home/map/map.tsx +++ b/src/ui/home/map/map.tsx @@ -6,14 +6,56 @@ import { NavigationControl, } from "maplibre-gl"; import { jakartaCoordinate } from "../../../constants"; -import { useTransportData } from "../../../data/transport-data"; +import { BusStop, useTransportData } from "../../../data/transport-data"; import { useSidebarState } from "../../../data/sidebar-state"; export const MapCanvas: Component = () => { - const { geoData, tjDataSource, setSelectedBusStop } = useTransportData(); + const { geoData, tjDataSource, setSelectedBusStop, selectedEntry } = + useTransportData(); const { setIsExpanded } = useSidebarState(); const [libreMap, setLibreMap] = createSignal(null); + const updateBusStopSources = (map: Map, selectedStopId: string | null) => { + const busStopsSource = map.getSource( + "opentije_bus_stops", + ) as GeoJSONSource; + const selectedStopSource = map.getSource( + "selected_bus_stop", + ) as GeoJSONSource; + + if (selectedStopId) { + const selectedStop = geoData().busStops.find( + (stop) => stop.id === selectedStopId, + ); + + if (selectedStop) { + selectedStopSource.setData({ + type: "FeatureCollection", + features: [selectedStop.geoJson], + }); + + const filteredSameStop = geoData() + .busStops.filter((stop) => stop.id !== selectedStop.id) + .map((stop) => stop.geoJson); + + busStopsSource.setData({ + type: "FeatureCollection", + features: filteredSameStop, + }); + } + } else { + selectedStopSource.setData({ + type: "FeatureCollection", + features: [], + }); + + busStopsSource.setData({ + type: "FeatureCollection", + features: geoData().busStops.map((stop) => stop.geoJson), + }); + } + }; + createEffect(() => { const map = libreMap(); if (map === null) return; @@ -35,6 +77,35 @@ export const MapCanvas: Component = () => { route.routeToGeoJson(), ), }); + + const entry = selectedEntry(); + if (entry && entry.category === "Bus Stop") { + updateBusStopSources(map, entry.id); + + const selectedStop = geoData().busStops.find( + (stop) => stop.id === entry.id, + ); + + if (selectedStop) { + map.flyTo({ + center: [selectedStop.longitude, selectedStop.latitude], + zoom: 16, + speed: 0.5, + curve: 2.5, + }); + } + } else { + updateBusStopSources(map, null); + map.flyTo({ + center: [ + jakartaCoordinate.longitude, + jakartaCoordinate.latitude, + ], + zoom: 12, + speed: 0.5, + curve: 2.5, + }); + } }); onMount(() => { @@ -68,7 +139,7 @@ export const MapCanvas: Component = () => { id: "opentije_bus_stops-label", type: "symbol", source: "opentije_bus_stops", - minzoom: 13, + minzoom: 15, layout: { "text-field": ["get", "name"], "text-font": ["Noto Sans Regular"], @@ -81,6 +152,7 @@ export const MapCanvas: Component = () => { id: "opentije_bus_stops", type: "circle", source: "opentije_bus_stops", + minzoom: 12, paint: { "circle-color": ["get", "color"], }, @@ -103,20 +175,58 @@ export const MapCanvas: Component = () => { }, }); + map.addSource("selected_bus_stop", { + type: "geojson", + data: { + type: "FeatureCollection", + features: [], + }, + }); + + map.addLayer({ + id: "selected_bus_stop", + type: "circle", + source: "selected_bus_stop", + paint: { + "circle-color": "#FF0000", + "circle-radius": 10, + }, + }); + + map.addLayer({ + id: "selected_bus_stop-label", + type: "symbol", + source: "selected_bus_stop", + layout: { + "text-field": ["get", "name"], + "text-font": ["Noto Sans Bold"], + "text-size": 20, + "text-variable-anchor": ["left", "right"], + "text-radial-offset": 0.8, + "text-justify": "auto", + }, + paint: { + "text-color": "#000000", + "text-opacity": 1, + }, + }); + map.on("click", "opentije_bus_stops", (target) => { const busStopId = target.features![0].properties.id; + setSelectedBusStop(busStopId); + updateBusStopSources(map, busStopId); }); setLibreMap(map); }); }); - return
setIsExpanded(false)} - > - -
; + return ( +
setIsExpanded(false)} + >
+ ); }; diff --git a/src/ui/home/sidebar/routes.tsx b/src/ui/home/sidebar/routes.tsx index 63401c3..0ec54bd 100644 --- a/src/ui/home/sidebar/routes.tsx +++ b/src/ui/home/sidebar/routes.tsx @@ -6,7 +6,27 @@ type RouteProps = { }; export const Route = ({ route }: RouteProps) => { - const { filter, setSelectedRouteId } = useTransportData(); + const { filter, setSelectedRouteId, setSelectedEntry } = useTransportData(); + + const handleCheckboxChange = (e: Event) => { + const isChecked = (e.target as HTMLInputElement).checked; + + setSelectedRouteId(route.id, isChecked); + + if (isChecked) { + setSelectedEntry({ + category: "Bus Route", + id: route.id, + name: route.fullName, + color: route.color, + }); + } else { + const selectedRoutes = filter().selectedRouteIds; + if (selectedRoutes.size === 0) { + setSelectedEntry(null); + } + } + }; return (
  • @@ -18,9 +38,7 @@ export const Route = ({ route }: RouteProps) => { { - setSelectedRouteId(route.id, e.target.checked); - }} + onChange={handleCheckboxChange} />
  • diff --git a/src/ui/home/sidebar/sidebar.module.scss b/src/ui/home/sidebar/sidebar.module.scss index ea9bd07..7baab20 100644 --- a/src/ui/home/sidebar/sidebar.module.scss +++ b/src/ui/home/sidebar/sidebar.module.scss @@ -64,7 +64,7 @@ display: none; &:checked + p { - background: #72BF78; + background: #72bf78; color: white; } } @@ -131,3 +131,35 @@ background: white; } } + +.autocompleteList { + position: absolute; + background: white; + border: 1px solid #ccc; + width: 95%; + z-index: 1000; + list-style: none; + margin: 0; + padding: 0; + max-height: 500px; + overflow-y: auto; + filter: drop-shadow(0 0 1rem #00000040); +} + +.autocompleteItem { + padding: 12px 20px; + cursor: pointer; +} + +.autocompleteItem:hover { + background-color: #f0f0f0; +} + +.autocompleteItem div { + display: flex; + gap: 12px; +} + +.autocompleteItem div span:nth-child(1) { + color: grey; +} \ No newline at end of file diff --git a/src/ui/home/sidebar/sidebar.tsx b/src/ui/home/sidebar/sidebar.tsx index 147614c..2a37a43 100644 --- a/src/ui/home/sidebar/sidebar.tsx +++ b/src/ui/home/sidebar/sidebar.tsx @@ -3,16 +3,22 @@ import { createStore } from "solid-js/store"; import { Route } from "./routes"; import style from "./sidebar.module.scss"; import { RouteType } from "../../../data/consumer"; -import { useTransportData } from "../../../data/transport-data"; +import { + BusRoute, + BusStop, + useTransportData, +} from "../../../data/transport-data"; import { AboutModal } from "./about"; import { useSidebarState } from "../../../data/sidebar-state"; export const Sidebar = () => { const { tjDataSource } = useTransportData(); - const {isExpanded, setIsExpanded} = useSidebarState(); + const { isExpanded, setIsExpanded } = useSidebarState(); return ( -
    +
    Loading...

    }> setIsExpanded(true)} /> @@ -25,22 +31,143 @@ export const Sidebar = () => { ); }; -const Content = ({ onSearchBarFocused }: { onSearchBarFocused: () => void }) => { - const { tjDataSource, geoData, filter, setSelectedRouteTypes, setQuery } = - useTransportData(); +type FilteredResult = { + category: "Bus Stop" | "Bus Route"; + id: string; + name: string; + latitude?: number; + longitude?: number; + color?: string; +}; + +const Content = ({ + onSearchBarFocused, +}: { + onSearchBarFocused: () => void; +}) => { + const { + tjDataSource, + geoData, + filter, + setSelectedRouteTypes, + setSelectedRouteId, + setQuery, + setSelectedEntry, + } = useTransportData(); const routeTypes = Object.values(RouteType) as Array; const [showAboutModal, setShowAboutModal] = createSignal(false); + const [filteredResults, setFilteredResults] = createSignal< + FilteredResult[] + >([]); + + const handleInputChange = (e: Event) => { + const query = (e.target as HTMLInputElement).value; + setQuery(query); + + if (!geoData().busStops || !geoData().busRoutes) return; + + const stopMatches: FilteredResult[] = geoData() + .busStops.filter((stop) => + stop.name?.toLowerCase().includes(query.toLowerCase()), + ) + .map((stop) => ({ + category: "Bus Stop", + id: stop.id, + name: stop.name, + latitude: stop.latitude, + longitude: stop.longitude, + })); + + const routeMatches: FilteredResult[] = geoData() + .busRoutes.filter((route) => + route.fullName?.toLowerCase().includes(query.toLowerCase()), + ) + .map((route) => ({ + category: "Bus Route", + id: route.id, + name: route.fullName, + color: route.color, + })); + + const matches: FilteredResult[] = [...routeMatches, ...stopMatches]; + + setFilteredResults(matches); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + if (filter().query === "") { + setFilteredResults([]); + setSelectedEntry(null); + } + } + }; return ( <>
    setQuery(e.target.value)} + onInput={handleInputChange} + onKeyDown={handleKeyDown} onFocus={() => onSearchBarFocused()} value={filter().query} class={style.searchInput} /> + {filteredResults().length > 0 && ( +
      + + {(matchedEntry) => ( +
    • { + setQuery(matchedEntry.name); + setSelectedEntry(matchedEntry); + + let route = geoData().busRoutes.find( + (route) => + route.id === matchedEntry.id, + ); + + if (route) { + if ( + filter().selectedRouteIds.has( + route.id, + ) + ) { + setSelectedRouteId( + route.id, + false, + ); + } else { + setSelectedRouteId( + route.id, + true, + ); + } + } + setFilteredResults([]); + }} + > + {matchedEntry.category === "Bus Stop" ? ( +
      + Halte: + {matchedEntry.name} +
      + ) : ( +
      + Rute: + + {matchedEntry.id} |{" "} + {matchedEntry.name} + +
      + )} +
    • + )} +
      +
    + )}
    {(routeType) => (