From ebfea946e40312b7a61bc355389d61c7dce2ea6a Mon Sep 17 00:00:00 2001 From: Keith Cheng <15365495+chengkeith@users.noreply.github.com> Date: Thu, 4 Jul 2024 19:57:11 +0800 Subject: [PATCH 1/7] Add Stop Grouping --- .../bookmarked-stop/StopRouteList.tsx | 9 +- .../bookmarked-stop/SwipeableStopList.tsx | 1 + src/components/route-eta/StopDialog.tsx | 7 +- src/hooks/useStopGroup.tsx | 202 ++++++++++++++++++ src/pages/RouteEta.tsx | 1 + src/utils.ts | 13 ++ 6 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useStopGroup.tsx diff --git a/src/components/bookmarked-stop/StopRouteList.tsx b/src/components/bookmarked-stop/StopRouteList.tsx index 6f0f0957fd10..023be0d916ac 100644 --- a/src/components/bookmarked-stop/StopRouteList.tsx +++ b/src/components/bookmarked-stop/StopRouteList.tsx @@ -1,15 +1,18 @@ import { Box, CircularProgress, List, SxProps, Theme } from "@mui/material"; import SuccinctTimeReport from "../home/SuccinctTimeReport"; +import { useStopGroup } from "../../hooks/useStopGroup"; import { useStopEtas } from "../../hooks/useStopEtas"; -import { Company } from "hk-bus-eta"; +import { Company, RouteListEntry } from "hk-bus-eta"; interface StopRouteListProps { stops: Array<[Company, string]>; // [[co, stopId]] + routeId : string | undefined; isFocus: boolean; } -const StopRouteList = ({ stops, isFocus }: StopRouteListProps) => { - const stopEtas = useStopEtas({ stopKeys: stops, disabled: !isFocus }); +const StopRouteList = ({ stops, routeId = undefined, isFocus }: StopRouteListProps) => { + const stopGroup = useStopGroup({ stopKeys: stops, routeId : routeId }); + const stopEtas = useStopEtas({ stopKeys: stopGroup, disabled: !isFocus }); if (stopEtas.length === 0) { return ( diff --git a/src/components/bookmarked-stop/SwipeableStopList.tsx b/src/components/bookmarked-stop/SwipeableStopList.tsx index d40bd8298c2e..6d82abc54ac0 100644 --- a/src/components/bookmarked-stop/SwipeableStopList.tsx +++ b/src/components/bookmarked-stop/SwipeableStopList.tsx @@ -72,6 +72,7 @@ const SwipeableStopList = React.forwardRef< ))} diff --git a/src/components/route-eta/StopDialog.tsx b/src/components/route-eta/StopDialog.tsx index 5ac165c7d6cf..50c11ad078ca 100644 --- a/src/components/route-eta/StopDialog.tsx +++ b/src/components/route-eta/StopDialog.tsx @@ -16,18 +16,19 @@ import { } from "@mui/material"; import { useCallback, useContext, useMemo } from "react"; import StopRouteList from "../bookmarked-stop/StopRouteList"; -import { Company } from "hk-bus-eta"; +import { Company, RouteListEntry } from "hk-bus-eta"; import useLanguage from "../../hooks/useTranslation"; import DbContext from "../../context/DbContext"; import CollectionContext from "../../CollectionContext"; interface StopDialogProps { open: boolean; + routeId : string; stops: Array<[Company, string]>; onClose: () => void; } -const StopDialog = ({ open, stops, onClose }: StopDialogProps) => { +const StopDialog = ({ open, routeId, stops, onClose }: StopDialogProps) => { const { db: { stopList }, } = useContext(DbContext); @@ -83,7 +84,7 @@ const StopDialog = ({ open, stops, onClose }: StopDialogProps) => { - + ); diff --git a/src/hooks/useStopGroup.tsx b/src/hooks/useStopGroup.tsx new file mode 100644 index 000000000000..e33c176ff2ec --- /dev/null +++ b/src/hooks/useStopGroup.tsx @@ -0,0 +1,202 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Company, Eta, fetchEtas } from "hk-bus-eta"; +import type { RouteListEntry, StopListEntry } from "hk-bus-eta"; +import AppContext from "../context/AppContext"; +import { isRouteAvaliable } from "../timetable"; +import useLanguage from "./useTranslation"; +import DbContext from "../context/DbContext"; +import { getDistance, getBearing } from "../utils"; + +interface useStopGroupProps { + stopKeys: Array<[Company, string]>; + routeId : string | undefined; +} +interface StopListEntryExtended extends StopListEntry { + id : string; + distance : number; +} +interface RouteKey { + routeKey : string; + co : Company; + seq : number; +} + +// stopKey in format "|", e.g., "lightRail|LR140" +export const useStopGroup = ({ + stopKeys, routeId +}: useStopGroupProps) => { + const { + db: { routeList, stopList, serviceDayMap }, + isTodayHoliday, + } = useContext(DbContext); + const { isRouteFilter } = useContext(AppContext); + + // TODO: put it in AppContext user preference + const DISTANCE_THRESHOLD = 50; + const BEARING_THRESHOLD = 45; + const MAX_STOPS_LIMIT = 50; + + const getDistanceStop = (a : StopListEntry, b : StopListEntry) => { + return getDistance(a.location, b.location); + }; + const getBearingStops = (a : StopListEntry, b : StopListEntry) => { + return getBearing(a.location, b.location); + }; + + const findRouteStopBearings = (routeStops : RouteKey[]) => { + // routeStop example: {"routeKey":"101+1+KENNEDY TOWN+KWUN TONG (YUE MAN SQUARE)","co":"ctb","seq":12} + return routeStops.map(routeStop => { + const { routeKey, co, seq } = routeStop; + const stopLength = routeList[routeKey].stops[co].length; + let stopA, stopB; + if(seq == stopLength - 1) { // last stop + // stopA = stopList[routeList[routeKey].stops[co][seq - 1]]; + // stopB = stopList[routeList[routeKey].stops[co][seq]]; + return -1; + } else { + stopA = stopList[routeList[routeKey].stops[co][seq]]; + stopB = stopList[routeList[routeKey].stops[co][seq + 1]]; + } + return getBearingStops(stopA, stopB); + }).filter(brng => brng !== -1); + } + + const routeStops : RouteKey[]= []; + if(routeId !== undefined) { + // StopDialog + let targetRouteStops = routeList[routeId].stops; + stopKeys.forEach(([co, stopId]) => { + let seq = targetRouteStops[co].indexOf(stopId); + if(seq != -1) { + routeStops.push({ + routeKey : routeId, + co : co, + seq : seq + }); + } + }); + } else { + // SwipableStopList (saved stop list) + stopKeys.forEach(([co, stopId]) => { + Object.keys(routeList).forEach(routeKey => { + let seq = routeList[routeKey].stops[co].indexOf(stopId); + if(seq != -1) { + routeStops.push({ + routeKey : routeKey, + co : co, + seq : seq + }); + } + }) + }); + } + const bearingTargets = findRouteStopBearings(routeStops); + const isBearingAccepted = (bearing : number) => { + if(BEARING_THRESHOLD >= 180 || bearingTargets.length == 0) { + return true; + } else { + for(let i = 0; i < bearingTargets.length; ++i) { + let bearingMin = bearingTargets[i] - BEARING_THRESHOLD; + let bearingMax = bearingTargets[i] + BEARING_THRESHOLD; + if(bearingMin < 0) + bearingMin += 360; + if(bearingMax > 360) + bearingMax -= 360; + if((bearingMin <= bearingMax && bearingMin <= bearing && bearing <= bearingMax) + || (bearingMin > bearingMax && (bearingMin <= bearing || bearing <= bearingMax))) // crossing 0/360 degress, eg min=340,max=020 + return true; + } + return false; + } + } + const findNearbyStops = (targetId : string, excludeList : string[]) => { + let targetStop = stopList[targetId]; + + return Object.keys(stopList).filter((stopId) => { + // find stops that are within X metres of target stop and along similar direction + return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && + // filter out stops that have not been added into excludeList before + !excludeList.includes(stopId); + }) + .reduce( (acc, stopId) => { + // get all the routes that has stop with this stopId + const rs = Object.entries(routeList).map(([routeKey, routeListEntry]) => { + const stops = routeListEntry.stops ?? {}; + const companies = Object.keys(stops) as Company[]; + for(let co of companies) { + let stopPos = stops[co].indexOf(stopId); + if(stopPos > -1) + return { routeKey : routeKey, co : co, seq : stopPos } as RouteKey; + } + return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteKey; // use ctb as dummy value and seq = -1, will be discarded in next filter + }) + .filter((obj) => obj.seq != -1); + // if any of the stops is within acceptable bearing range, add to the list + const bearings = findRouteStopBearings(rs); + if(bearings.find(b => isBearingAccepted(b)) !== undefined) { + const thisStop : StopListEntryExtended = { + ...stopList[stopId], + id : stopId, + distance : 0 // dummy value + }; + acc.push(thisStop); + } + return acc + }, [] as Array); + } + + const stopGroup = useMemo>(() => { + const stopGroup : Array<[Company, string]> = []; + let stopListEntries : StopListEntryExtended[] = []; + + stopKeys.forEach((stopKey) => { + const [co, stopId] = stopKey; + stopListEntries = stopListEntries.concat(findNearbyStops(stopId, stopListEntries.map((stop) => stop.id))); + for(let i = 0; i < stopListEntries.length; ++i) { + stopListEntries = stopListEntries.concat(findNearbyStops(stopListEntries[i].id, stopListEntries.map((stop) => stop.id))); + if(stopListEntries.length >= MAX_STOPS_LIMIT) + break; + } + }); + + // sort by distance from first stop in stopMap (stopKeys[0]) + if(stopKeys.length > 0) { + let [, stopId] = stopKeys[0]; + stopListEntries = stopListEntries.map(stop => { + return { + ... stop, + distance : getDistanceStop(stopList[stopId], stop) + }; + }).sort((stopA, stopB) => { + return stopA.distance - stopB.distance + }); + stopListEntries.forEach((stopListEntry) => { + // find co of stop id from routeList + let found = false; + for(let [,routeListEntry] of Object.entries(routeList)) { + const companies = Object.keys(routeListEntry.stops) as Company[]; + for(let co of companies) { + if(routeListEntry.stops[co]?.includes(stopListEntry.id)) { + stopGroup.push([co, stopListEntry.id]); + found = true; + break; + } + } + if(found) { + break; + } + } + }); + } + return stopGroup; + }, [stopKeys, routeList, stopList, findNearbyStops]); + + return stopGroup; +}; diff --git a/src/pages/RouteEta.tsx b/src/pages/RouteEta.tsx index 71ce826999f4..63ecffe6e47f 100644 --- a/src/pages/RouteEta.tsx +++ b/src/pages/RouteEta.tsx @@ -253,6 +253,7 @@ const RouteEta = () => { /> diff --git a/src/utils.ts b/src/utils.ts index 3ac44629645c..f2a519c342ee 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,6 +34,19 @@ export const getDistanceWithUnit = (distanceInMetre: number) => { }; }; +export const getBearing = (a: GeoLocation, b: GeoLocation) => { + // Reference: https://www.movable-type.co.uk/scripts/latlong.html + const φ1 = a.lat * Math.PI / 180; // φ, λ = lat, lon in radians + const φ2 = b.lat * Math.PI / 180; + const λ1 = a.lng * Math.PI / 180; // φ, λ = lat, lon in radians + const λ2 = b.lng * Math.PI / 180; + const y = Math.sin(λ2 - λ1) * Math.cos(φ2); + const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2-λ1); + const θ = Math.atan2(y, x); + const brng = (θ * 180 / Math.PI + 360) % 360; // in degrees + return brng; +}; + export const DEFAULT_GEOLOCATION: GeoLocation = { lat: 22.302711, lng: 114.177216, From 3accae15d7d6114d41fc11b6b0a8dc91e9aa7dad Mon Sep 17 00:00:00 2001 From: Keith Cheng Date: Fri, 5 Jul 2024 01:26:02 +0800 Subject: [PATCH 2/7] Performance tuning - move findNearByStop inside useMemo to avoid refresh every render --- src/hooks/useStopGroup.tsx | 78 +++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/hooks/useStopGroup.tsx b/src/hooks/useStopGroup.tsx index e33c176ff2ec..5826d5749bb8 100644 --- a/src/hooks/useStopGroup.tsx +++ b/src/hooks/useStopGroup.tsx @@ -49,7 +49,7 @@ export const useStopGroup = ({ const getBearingStops = (a : StopListEntry, b : StopListEntry) => { return getBearing(a.location, b.location); }; - + const findRouteStopBearings = (routeStops : RouteKey[]) => { // routeStop example: {"routeKey":"101+1+KENNEDY TOWN+KWUN TONG (YUE MAN SQUARE)","co":"ctb","seq":12} return routeStops.map(routeStop => { @@ -73,7 +73,7 @@ export const useStopGroup = ({ // StopDialog let targetRouteStops = routeList[routeId].stops; stopKeys.forEach(([co, stopId]) => { - let seq = targetRouteStops[co].indexOf(stopId); + let seq = targetRouteStops[co]?.indexOf(stopId) ?? -1; if(seq != -1) { routeStops.push({ routeKey : routeId, @@ -86,7 +86,7 @@ export const useStopGroup = ({ // SwipableStopList (saved stop list) stopKeys.forEach(([co, stopId]) => { Object.keys(routeList).forEach(routeKey => { - let seq = routeList[routeKey].stops[co].indexOf(stopId); + let seq = routeList[routeKey]?.stops[co]?.indexOf(stopId) ?? -1; if(seq != -1) { routeStops.push({ routeKey : routeKey, @@ -116,45 +116,45 @@ export const useStopGroup = ({ return false; } } - const findNearbyStops = (targetId : string, excludeList : string[]) => { - let targetStop = stopList[targetId]; - - return Object.keys(stopList).filter((stopId) => { - // find stops that are within X metres of target stop and along similar direction - return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && - // filter out stops that have not been added into excludeList before - !excludeList.includes(stopId); - }) - .reduce( (acc, stopId) => { - // get all the routes that has stop with this stopId - const rs = Object.entries(routeList).map(([routeKey, routeListEntry]) => { - const stops = routeListEntry.stops ?? {}; - const companies = Object.keys(stops) as Company[]; - for(let co of companies) { - let stopPos = stops[co].indexOf(stopId); - if(stopPos > -1) - return { routeKey : routeKey, co : co, seq : stopPos } as RouteKey; - } - return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteKey; // use ctb as dummy value and seq = -1, will be discarded in next filter - }) - .filter((obj) => obj.seq != -1); - // if any of the stops is within acceptable bearing range, add to the list - const bearings = findRouteStopBearings(rs); - if(bearings.find(b => isBearingAccepted(b)) !== undefined) { - const thisStop : StopListEntryExtended = { - ...stopList[stopId], - id : stopId, - distance : 0 // dummy value - }; - acc.push(thisStop); - } - return acc - }, [] as Array); - } const stopGroup = useMemo>(() => { const stopGroup : Array<[Company, string]> = []; let stopListEntries : StopListEntryExtended[] = []; + const findNearbyStops = (targetId : string, excludeList : string[]) => { + let targetStop = stopList[targetId]; + + return Object.keys(stopList).filter((stopId) => { + // find stops that are within X metres of target stop and along similar direction + return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && + // filter out stops that have not been added into excludeList before + !excludeList.includes(stopId); + }) + .reduce( (acc, stopId) => { + // get all the routes that has stop with this stopId + const _routeStop = Object.entries(routeList).map(([routeKey, routeListEntry]) => { + const stops = routeListEntry.stops ?? {}; + const companies = Object.keys(stops) as Company[]; + for(let co of companies) { + let stopPos = stops[co]?.indexOf(stopId) ?? -1; + if(stopPos > -1) + return { routeKey : routeKey, co : co, seq : stopPos } as RouteKey; + } + return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteKey; // use ctb as dummy value and seq = -1, will be discarded in next filter + }) + .filter((obj) => obj.seq != -1); + // if any of the stops is within acceptable bearing range, add to the list + const bearings = findRouteStopBearings(_routeStop); + if(bearings.find(b => isBearingAccepted(b)) !== undefined) { + const thisStop : StopListEntryExtended = { + ...stopList[stopId], + id : stopId, + distance : 0 // dummy value + }; + acc.push(thisStop); + } + return acc + }, [] as Array); + } stopKeys.forEach((stopKey) => { const [co, stopId] = stopKey; @@ -196,7 +196,7 @@ export const useStopGroup = ({ }); } return stopGroup; - }, [stopKeys, routeList, stopList, findNearbyStops]); + }, [stopKeys, routeList, stopList]); return stopGroup; }; From 0037a83627c5f08954b75f33eee28ac1e91c87a1 Mon Sep 17 00:00:00 2001 From: Keith Cheng <15365495+chengkeith@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:19:56 +0800 Subject: [PATCH 3/7] - Remove unused declarations - Add comments for code readability - Rename variables and methods with more meaningful names --- .../bookmarked-stop/StopRouteList.tsx | 2 +- src/components/route-eta/StopDialog.tsx | 2 +- src/hooks/useStopGroup.tsx | 115 +++++++++--------- 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/src/components/bookmarked-stop/StopRouteList.tsx b/src/components/bookmarked-stop/StopRouteList.tsx index 023be0d916ac..6842e81ed52f 100644 --- a/src/components/bookmarked-stop/StopRouteList.tsx +++ b/src/components/bookmarked-stop/StopRouteList.tsx @@ -2,7 +2,7 @@ import { Box, CircularProgress, List, SxProps, Theme } from "@mui/material"; import SuccinctTimeReport from "../home/SuccinctTimeReport"; import { useStopGroup } from "../../hooks/useStopGroup"; import { useStopEtas } from "../../hooks/useStopEtas"; -import { Company, RouteListEntry } from "hk-bus-eta"; +import { Company } from "hk-bus-eta"; interface StopRouteListProps { stops: Array<[Company, string]>; // [[co, stopId]] diff --git a/src/components/route-eta/StopDialog.tsx b/src/components/route-eta/StopDialog.tsx index 50c11ad078ca..37efd40aa293 100644 --- a/src/components/route-eta/StopDialog.tsx +++ b/src/components/route-eta/StopDialog.tsx @@ -16,7 +16,7 @@ import { } from "@mui/material"; import { useCallback, useContext, useMemo } from "react"; import StopRouteList from "../bookmarked-stop/StopRouteList"; -import { Company, RouteListEntry } from "hk-bus-eta"; +import { Company } from "hk-bus-eta"; import useLanguage from "../../hooks/useTranslation"; import DbContext from "../../context/DbContext"; import CollectionContext from "../../CollectionContext"; diff --git a/src/hooks/useStopGroup.tsx b/src/hooks/useStopGroup.tsx index 5826d5749bb8..f2b7cc25f927 100644 --- a/src/hooks/useStopGroup.tsx +++ b/src/hooks/useStopGroup.tsx @@ -1,16 +1,6 @@ -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { Company, Eta, fetchEtas } from "hk-bus-eta"; -import type { RouteListEntry, StopListEntry } from "hk-bus-eta"; -import AppContext from "../context/AppContext"; -import { isRouteAvaliable } from "../timetable"; -import useLanguage from "./useTranslation"; +import { useContext, useMemo } from "react"; +import { Company } from "hk-bus-eta"; +import type { StopListEntry } from "hk-bus-eta"; import DbContext from "../context/DbContext"; import { getDistance, getBearing } from "../utils"; @@ -21,8 +11,9 @@ interface useStopGroupProps { interface StopListEntryExtended extends StopListEntry { id : string; distance : number; + routeStops : RouteStopCoSeq[]; } -interface RouteKey { +interface RouteStopCoSeq { routeKey : string; co : Company; seq : number; @@ -33,15 +24,13 @@ export const useStopGroup = ({ stopKeys, routeId }: useStopGroupProps) => { const { - db: { routeList, stopList, serviceDayMap }, - isTodayHoliday, + db: { routeList, stopList }, } = useContext(DbContext); - const { isRouteFilter } = useContext(AppContext); - // TODO: put it in AppContext user preference - const DISTANCE_THRESHOLD = 50; - const BEARING_THRESHOLD = 45; - const MAX_STOPS_LIMIT = 50; + // TODO: put these constants in AppContext user preference + const DISTANCE_THRESHOLD = 50; // in metres, will recursively find stops within this number of metres, so keep it as small as possible. Never choose larger than 300m. + const BEARING_THRESHOLD = 45; // in degrees (°), acceptable deviation to the left or right of current bearing + const STOP_LIST_LIMIT = 50; // max number of stops in a group, if more than that, the ETA list will be too long and meaningless const getDistanceStop = (a : StopListEntry, b : StopListEntry) => { return getDistance(a.location, b.location); @@ -50,7 +39,7 @@ export const useStopGroup = ({ return getBearing(a.location, b.location); }; - const findRouteStopBearings = (routeStops : RouteKey[]) => { + const getAllRouteStopsBearings = (routeStops : RouteStopCoSeq[]) => { // routeStop example: {"routeKey":"101+1+KENNEDY TOWN+KWUN TONG (YUE MAN SQUARE)","co":"ctb","seq":12} return routeStops.map(routeStop => { const { routeKey, co, seq } = routeStop; @@ -68,7 +57,7 @@ export const useStopGroup = ({ }).filter(brng => brng !== -1); } - const routeStops : RouteKey[]= []; + const routeStops : RouteStopCoSeq[]= []; if(routeId !== undefined) { // StopDialog let targetRouteStops = routeList[routeId].stops; @@ -97,8 +86,8 @@ export const useStopGroup = ({ }) }); } - const bearingTargets = findRouteStopBearings(routeStops); - const isBearingAccepted = (bearing : number) => { + const bearingTargets = getAllRouteStopsBearings(routeStops); + const isBearingInRange = (bearing : number) => { if(BEARING_THRESHOLD >= 180 || bearingTargets.length == 0) { return true; } else { @@ -110,7 +99,7 @@ export const useStopGroup = ({ if(bearingMax > 360) bearingMax -= 360; if((bearingMin <= bearingMax && bearingMin <= bearing && bearing <= bearingMax) - || (bearingMin > bearingMax && (bearingMin <= bearing || bearing <= bearingMax))) // crossing 0/360 degress, eg min=340,max=020 + || (bearingMin > bearingMax && (bearingMin <= bearing || bearing <= bearingMax))) // crossing 0/360° mark, eg min=340°,max=020° return true; } return false; @@ -118,36 +107,37 @@ export const useStopGroup = ({ } const stopGroup = useMemo>(() => { - const stopGroup : Array<[Company, string]> = []; let stopListEntries : StopListEntryExtended[] = []; - const findNearbyStops = (targetId : string, excludeList : string[]) => { - let targetStop = stopList[targetId]; + const searchNearbyStops = (targetStopId : string, excludedStopIdList : string[]) => { + const targetStop = stopList[targetStopId]; return Object.keys(stopList).filter((stopId) => { - // find stops that are within X metres of target stop and along similar direction + // find stops that are within DISTANCE_THRESHOLD metres of target stop and along similar direction return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && // filter out stops that have not been added into excludeList before - !excludeList.includes(stopId); + !excludedStopIdList.includes(stopId); }) .reduce( (acc, stopId) => { // get all the routes that has stop with this stopId - const _routeStop = Object.entries(routeList).map(([routeKey, routeListEntry]) => { + const _routeStops = Object.entries(routeList).map(([routeKey, routeListEntry]) => { const stops = routeListEntry.stops ?? {}; const companies = Object.keys(stops) as Company[]; for(let co of companies) { let stopPos = stops[co]?.indexOf(stopId) ?? -1; if(stopPos > -1) - return { routeKey : routeKey, co : co, seq : stopPos } as RouteKey; + return { routeKey : routeKey, co : co, seq : stopPos } as RouteStopCoSeq; } - return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteKey; // use ctb as dummy value and seq = -1, will be discarded in next filter + return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteStopCoSeq; // use ctb as dummy value and seq = -1, will be discarded in next filter }) - .filter((obj) => obj.seq != -1); - // if any of the stops is within acceptable bearing range, add to the list - const bearings = findRouteStopBearings(_routeStop); - if(bearings.find(b => isBearingAccepted(b)) !== undefined) { + .filter((_rs) => _rs.seq != -1); + // if any of the routes passing this stop is facing same direction (+/- BEARING_THRESHOLD), add the stop to the list + // Note: once the stop is added, other routes not facing same direction but passing this stop will also be shown in ETA (most commonly seen in railway lines) + const bearings = getAllRouteStopsBearings(_routeStops); + if(bearings.find(b => isBearingInRange(b)) !== undefined) { const thisStop : StopListEntryExtended = { ...stopList[stopId], id : stopId, + routeStops : _routeStops, // _routeStops.length must be > 0 here, as bearings.length must be > 0 to reach into this if-condition distance : 0 // dummy value }; acc.push(thisStop); @@ -156,19 +146,22 @@ export const useStopGroup = ({ }, [] as Array); } + // recursively search for nearby stops within thresholds (distance and bearing) + // stop searching when no new stops are found within range, or when stop list is getting too large stopKeys.forEach((stopKey) => { - const [co, stopId] = stopKey; - stopListEntries = stopListEntries.concat(findNearbyStops(stopId, stopListEntries.map((stop) => stop.id))); - for(let i = 0; i < stopListEntries.length; ++i) { - stopListEntries = stopListEntries.concat(findNearbyStops(stopListEntries[i].id, stopListEntries.map((stop) => stop.id))); - if(stopListEntries.length >= MAX_STOPS_LIMIT) + const [, stopId] = stopKey; // [co, stopId] + stopListEntries = stopListEntries.concat(searchNearbyStops(stopId, stopListEntries.map((stop) => stop.id))); + for(let i = 0; i < stopListEntries.length; ++i) { // use traditional for-loop as the length keeps expanding + stopListEntries = stopListEntries.concat(searchNearbyStops(stopListEntries[i].id, stopListEntries.map((stop) => stop.id))); + if(stopListEntries.length >= STOP_LIST_LIMIT) break; } }); // sort by distance from first stop in stopMap (stopKeys[0]) + const _stopGroup : Array<[Company, string]> = []; if(stopKeys.length > 0) { - let [, stopId] = stopKeys[0]; + let [, stopId] = stopKeys[0]; // [co, stopId] but don't use this co stopListEntries = stopListEntries.map(stop => { return { ... stop, @@ -178,24 +171,26 @@ export const useStopGroup = ({ return stopA.distance - stopB.distance }); stopListEntries.forEach((stopListEntry) => { - // find co of stop id from routeList - let found = false; - for(let [,routeListEntry] of Object.entries(routeList)) { - const companies = Object.keys(routeListEntry.stops) as Company[]; - for(let co of companies) { - if(routeListEntry.stops[co]?.includes(stopListEntry.id)) { - stopGroup.push([co, stopListEntry.id]); - found = true; - break; - } - } - if(found) { - break; - } - } + if(stopListEntry.routeStops.length > 0) + _stopGroup.push([stopListEntry.routeStops[0].co, stopListEntry.id]); + // // find co of stop id from routeList + // let found = false; + // for(let [,routeListEntry] of Object.entries(routeList)) { + // const companies = Object.keys(routeListEntry.stops) as Company[]; + // for(let co of companies) { + // if(routeListEntry.stops[co]?.includes(stopListEntry.id)) { + // _stopGroup.push([co, stopListEntry.id]); + // found = true; + // break; + // } + // } + // if(found) { + // break; + // } + // } }); } - return stopGroup; + return _stopGroup; }, [stopKeys, routeList, stopList]); return stopGroup; From e72f154ccd8016b099fc48de66485e088a5c82d5 Mon Sep 17 00:00:00 2001 From: Keith Cheng <15365495+chengkeith@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:24:00 +0800 Subject: [PATCH 4/7] remove deprecated code, add more meaningul comments --- src/hooks/useStopGroup.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/hooks/useStopGroup.tsx b/src/hooks/useStopGroup.tsx index f2b7cc25f927..b79a3b3de9b1 100644 --- a/src/hooks/useStopGroup.tsx +++ b/src/hooks/useStopGroup.tsx @@ -46,8 +46,7 @@ export const useStopGroup = ({ const stopLength = routeList[routeKey].stops[co].length; let stopA, stopB; if(seq == stopLength - 1) { // last stop - // stopA = stopList[routeList[routeKey].stops[co][seq - 1]]; - // stopB = stopList[routeList[routeKey].stops[co][seq]]; + // no next stop, hence no forward bearing, just use -1 as dummy value then discard it later return -1; } else { stopA = stopList[routeList[routeKey].stops[co][seq]]; @@ -173,21 +172,6 @@ export const useStopGroup = ({ stopListEntries.forEach((stopListEntry) => { if(stopListEntry.routeStops.length > 0) _stopGroup.push([stopListEntry.routeStops[0].co, stopListEntry.id]); - // // find co of stop id from routeList - // let found = false; - // for(let [,routeListEntry] of Object.entries(routeList)) { - // const companies = Object.keys(routeListEntry.stops) as Company[]; - // for(let co of companies) { - // if(routeListEntry.stops[co]?.includes(stopListEntry.id)) { - // _stopGroup.push([co, stopListEntry.id]); - // found = true; - // break; - // } - // } - // if(found) { - // break; - // } - // } }); } return _stopGroup; From 24427df3b2428a5fa10349fa7da2325288ef108c Mon Sep 17 00:00:00 2001 From: Keith Cheng Date: Tue, 9 Jul 2024 00:52:56 +0800 Subject: [PATCH 5/7] added useCallback to prevent re-render --- src/hooks/useStopGroup.tsx | 87 +++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/hooks/useStopGroup.tsx b/src/hooks/useStopGroup.tsx index b79a3b3de9b1..d74741e1a727 100644 --- a/src/hooks/useStopGroup.tsx +++ b/src/hooks/useStopGroup.tsx @@ -1,4 +1,4 @@ -import { useContext, useMemo } from "react"; +import { useCallback, useContext, useMemo } from "react"; import { Company } from "hk-bus-eta"; import type { StopListEntry } from "hk-bus-eta"; import DbContext from "../context/DbContext"; @@ -39,7 +39,7 @@ export const useStopGroup = ({ return getBearing(a.location, b.location); }; - const getAllRouteStopsBearings = (routeStops : RouteStopCoSeq[]) => { + const getAllRouteStopsBearings = useCallback((routeStops : RouteStopCoSeq[]) => { // routeStop example: {"routeKey":"101+1+KENNEDY TOWN+KWUN TONG (YUE MAN SQUARE)","co":"ctb","seq":12} return routeStops.map(routeStop => { const { routeKey, co, seq } = routeStop; @@ -54,7 +54,7 @@ export const useStopGroup = ({ } return getBearingStops(stopA, stopB); }).filter(brng => brng !== -1); - } + }, [routeList, stopList]); const routeStops : RouteStopCoSeq[]= []; if(routeId !== undefined) { @@ -86,7 +86,7 @@ export const useStopGroup = ({ }); } const bearingTargets = getAllRouteStopsBearings(routeStops); - const isBearingInRange = (bearing : number) => { + const isBearingInRange = useCallback((bearing : number) => { if(BEARING_THRESHOLD >= 180 || bearingTargets.length == 0) { return true; } else { @@ -103,47 +103,48 @@ export const useStopGroup = ({ } return false; } - } + }, [bearingTargets]); - const stopGroup = useMemo>(() => { - let stopListEntries : StopListEntryExtended[] = []; - const searchNearbyStops = (targetStopId : string, excludedStopIdList : string[]) => { - const targetStop = stopList[targetStopId]; + const searchNearbyStops = useCallback((targetStopId : string, excludedStopIdList : string[]) => { + const targetStop = stopList[targetStopId]; - return Object.keys(stopList).filter((stopId) => { - // find stops that are within DISTANCE_THRESHOLD metres of target stop and along similar direction - return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && - // filter out stops that have not been added into excludeList before - !excludedStopIdList.includes(stopId); - }) - .reduce( (acc, stopId) => { - // get all the routes that has stop with this stopId - const _routeStops = Object.entries(routeList).map(([routeKey, routeListEntry]) => { - const stops = routeListEntry.stops ?? {}; - const companies = Object.keys(stops) as Company[]; - for(let co of companies) { - let stopPos = stops[co]?.indexOf(stopId) ?? -1; - if(stopPos > -1) - return { routeKey : routeKey, co : co, seq : stopPos } as RouteStopCoSeq; - } - return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteStopCoSeq; // use ctb as dummy value and seq = -1, will be discarded in next filter - }) - .filter((_rs) => _rs.seq != -1); - // if any of the routes passing this stop is facing same direction (+/- BEARING_THRESHOLD), add the stop to the list - // Note: once the stop is added, other routes not facing same direction but passing this stop will also be shown in ETA (most commonly seen in railway lines) - const bearings = getAllRouteStopsBearings(_routeStops); - if(bearings.find(b => isBearingInRange(b)) !== undefined) { - const thisStop : StopListEntryExtended = { - ...stopList[stopId], - id : stopId, - routeStops : _routeStops, // _routeStops.length must be > 0 here, as bearings.length must be > 0 to reach into this if-condition - distance : 0 // dummy value - }; - acc.push(thisStop); + return Object.keys(stopList).filter((stopId) => { + // find stops that are within DISTANCE_THRESHOLD metres of target stop and along similar direction + return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && + // filter out stops that have not been added into excludeList before + !excludedStopIdList.includes(stopId); + }) + .reduce( (acc, stopId) => { + // get all the routes that has stop with this stopId + const _routeStops = Object.entries(routeList).map(([routeKey, routeListEntry]) => { + const stops = routeListEntry.stops ?? {}; + const companies = Object.keys(stops) as Company[]; + for(let co of companies) { + let stopPos = stops[co]?.indexOf(stopId) ?? -1; + if(stopPos > -1) + return { routeKey : routeKey, co : co, seq : stopPos } as RouteStopCoSeq; } - return acc - }, [] as Array); - } + return { routeKey : routeKey, co : 'ctb', seq : -1 } as RouteStopCoSeq; // use ctb as dummy value and seq = -1, will be discarded in next filter + }) + .filter((_rs) => _rs.seq != -1); + // if any of the routes passing this stop is facing same direction (+/- BEARING_THRESHOLD), add the stop to the list + // Note: once the stop is added, other routes not facing same direction but passing this stop will also be shown in ETA (most commonly seen in railway lines) + const bearings = getAllRouteStopsBearings(_routeStops); + if(bearings.find(b => isBearingInRange(b)) !== undefined) { + const thisStop : StopListEntryExtended = { + ...stopList[stopId], + id : stopId, + routeStops : _routeStops, // _routeStops.length must be > 0 here, as bearings.length must be > 0 to reach into this if-condition + distance : 0 // dummy value + }; + acc.push(thisStop); + } + return acc + }, [] as Array); + }, [routeList, stopList, getAllRouteStopsBearings, isBearingInRange]); + + const stopGroup = useMemo>(() => { + let stopListEntries : StopListEntryExtended[] = []; // recursively search for nearby stops within thresholds (distance and bearing) // stop searching when no new stops are found within range, or when stop list is getting too large @@ -175,7 +176,7 @@ export const useStopGroup = ({ }); } return _stopGroup; - }, [stopKeys, routeList, stopList]); + }, [stopKeys, stopList, searchNearbyStops]); return stopGroup; }; From eb5313f9f4147e0cee83ba1f0314f1c7da2f1532 Mon Sep 17 00:00:00 2001 From: Keith Cheng Date: Tue, 9 Jul 2024 01:09:02 +0800 Subject: [PATCH 6/7] rename getDistanceStop to getDistanceStops add useMemo for routeStops --- src/hooks/useStopGroup.tsx | 57 ++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/hooks/useStopGroup.tsx b/src/hooks/useStopGroup.tsx index d74741e1a727..d0911f281a52 100644 --- a/src/hooks/useStopGroup.tsx +++ b/src/hooks/useStopGroup.tsx @@ -32,7 +32,7 @@ export const useStopGroup = ({ const BEARING_THRESHOLD = 45; // in degrees (°), acceptable deviation to the left or right of current bearing const STOP_LIST_LIMIT = 50; // max number of stops in a group, if more than that, the ETA list will be too long and meaningless - const getDistanceStop = (a : StopListEntry, b : StopListEntry) => { + const getDistanceStops = (a : StopListEntry, b : StopListEntry) => { return getDistance(a.location, b.location); }; const getBearingStops = (a : StopListEntry, b : StopListEntry) => { @@ -56,35 +56,38 @@ export const useStopGroup = ({ }).filter(brng => brng !== -1); }, [routeList, stopList]); - const routeStops : RouteStopCoSeq[]= []; - if(routeId !== undefined) { - // StopDialog - let targetRouteStops = routeList[routeId].stops; - stopKeys.forEach(([co, stopId]) => { - let seq = targetRouteStops[co]?.indexOf(stopId) ?? -1; - if(seq != -1) { - routeStops.push({ - routeKey : routeId, - co : co, - seq : seq - }); - } - }); - } else { - // SwipableStopList (saved stop list) - stopKeys.forEach(([co, stopId]) => { - Object.keys(routeList).forEach(routeKey => { - let seq = routeList[routeKey]?.stops[co]?.indexOf(stopId) ?? -1; + const routeStops : RouteStopCoSeq[]= useMemo(() => { + let _routeStops: RouteStopCoSeq[] = []; + if(routeId !== undefined) { + // StopDialog + let targetRouteStops = routeList[routeId].stops; + stopKeys.forEach(([co, stopId]) => { + let seq = targetRouteStops[co]?.indexOf(stopId) ?? -1; if(seq != -1) { - routeStops.push({ - routeKey : routeKey, + _routeStops.push({ + routeKey : routeId, co : co, seq : seq }); } - }) - }); - } + }); + } else { + // SwipableStopList (saved stop list) + stopKeys.forEach(([co, stopId]) => { + Object.keys(routeList).forEach(routeKey => { + let seq = routeList[routeKey]?.stops[co]?.indexOf(stopId) ?? -1; + if(seq != -1) { + _routeStops.push({ + routeKey : routeKey, + co : co, + seq : seq + }); + } + }) + }); + } + return _routeStops; + }, [stopKeys, routeId, routeList]); const bearingTargets = getAllRouteStopsBearings(routeStops); const isBearingInRange = useCallback((bearing : number) => { if(BEARING_THRESHOLD >= 180 || bearingTargets.length == 0) { @@ -110,7 +113,7 @@ export const useStopGroup = ({ return Object.keys(stopList).filter((stopId) => { // find stops that are within DISTANCE_THRESHOLD metres of target stop and along similar direction - return getDistanceStop(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && + return getDistanceStops(targetStop, stopList[stopId]) <= DISTANCE_THRESHOLD && // filter out stops that have not been added into excludeList before !excludedStopIdList.includes(stopId); }) @@ -165,7 +168,7 @@ export const useStopGroup = ({ stopListEntries = stopListEntries.map(stop => { return { ... stop, - distance : getDistanceStop(stopList[stopId], stop) + distance : getDistanceStops(stopList[stopId], stop) }; }).sort((stopA, stopB) => { return stopA.distance - stopB.distance From 4f70eaaeeee55783411d6ba2b1205cd348b5d172 Mon Sep 17 00:00:00 2001 From: Keith Cheng Date: Thu, 11 Jul 2024 00:57:47 +0800 Subject: [PATCH 7/7] - remove hook usage - rename useStopGroup method as simple util method getStopGroup - use useMemo to cache result of getStopGroup to prevent infinite rendering --- .../bookmarked-stop/StopRouteList.tsx | 10 +- src/{hooks/useStopGroup.tsx => stopGroup.ts} | 172 +++++++++--------- 2 files changed, 89 insertions(+), 93 deletions(-) rename src/{hooks/useStopGroup.tsx => stopGroup.ts} (50%) diff --git a/src/components/bookmarked-stop/StopRouteList.tsx b/src/components/bookmarked-stop/StopRouteList.tsx index 6842e81ed52f..5441dd42bec8 100644 --- a/src/components/bookmarked-stop/StopRouteList.tsx +++ b/src/components/bookmarked-stop/StopRouteList.tsx @@ -1,8 +1,10 @@ import { Box, CircularProgress, List, SxProps, Theme } from "@mui/material"; import SuccinctTimeReport from "../home/SuccinctTimeReport"; -import { useStopGroup } from "../../hooks/useStopGroup"; +import { getStopGroup } from "../../stopGroup"; import { useStopEtas } from "../../hooks/useStopEtas"; import { Company } from "hk-bus-eta"; +import DbContext from "../../context/DbContext"; +import { useContext, useMemo } from "react"; interface StopRouteListProps { stops: Array<[Company, string]>; // [[co, stopId]] @@ -11,7 +13,11 @@ interface StopRouteListProps { } const StopRouteList = ({ stops, routeId = undefined, isFocus }: StopRouteListProps) => { - const stopGroup = useStopGroup({ stopKeys: stops, routeId : routeId }); + const { db: { routeList, stopList } } = useContext(DbContext); + const stopGroup = useMemo( + () => getStopGroup({ routeList, stopList, stopKeys: stops, routeId }), + [routeList, stopList, stops, routeId] + ); const stopEtas = useStopEtas({ stopKeys: stopGroup, disabled: !isFocus }); if (stopEtas.length === 0) { diff --git a/src/hooks/useStopGroup.tsx b/src/stopGroup.ts similarity index 50% rename from src/hooks/useStopGroup.tsx rename to src/stopGroup.ts index d0911f281a52..fc4f5f85464e 100644 --- a/src/hooks/useStopGroup.tsx +++ b/src/stopGroup.ts @@ -1,10 +1,10 @@ -import { useCallback, useContext, useMemo } from "react"; import { Company } from "hk-bus-eta"; -import type { StopListEntry } from "hk-bus-eta"; -import DbContext from "../context/DbContext"; -import { getDistance, getBearing } from "../utils"; +import type { RouteList, StopList, StopListEntry } from "hk-bus-eta"; +import { getDistance, getBearing } from "./utils"; interface useStopGroupProps { + routeList : RouteList; + stopList : StopList; stopKeys: Array<[Company, string]>; routeId : string | undefined; } @@ -20,12 +20,9 @@ interface RouteStopCoSeq { } // stopKey in format "|", e.g., "lightRail|LR140" -export const useStopGroup = ({ - stopKeys, routeId +export const getStopGroup = ({ + routeList, stopList, stopKeys, routeId }: useStopGroupProps) => { - const { - db: { routeList, stopList }, - } = useContext(DbContext); // TODO: put these constants in AppContext user preference const DISTANCE_THRESHOLD = 50; // in metres, will recursively find stops within this number of metres, so keep it as small as possible. Never choose larger than 300m. @@ -39,76 +36,71 @@ export const useStopGroup = ({ return getBearing(a.location, b.location); }; - const getAllRouteStopsBearings = useCallback((routeStops : RouteStopCoSeq[]) => { + const getAllRouteStopsBearings = (routeStops : RouteStopCoSeq[]) => { // routeStop example: {"routeKey":"101+1+KENNEDY TOWN+KWUN TONG (YUE MAN SQUARE)","co":"ctb","seq":12} return routeStops.map(routeStop => { const { routeKey, co, seq } = routeStop; const stopLength = routeList[routeKey].stops[co].length; - let stopA, stopB; if(seq == stopLength - 1) { // last stop // no next stop, hence no forward bearing, just use -1 as dummy value then discard it later return -1; - } else { - stopA = stopList[routeList[routeKey].stops[co][seq]]; - stopB = stopList[routeList[routeKey].stops[co][seq + 1]]; } + const stopA = stopList[routeList[routeKey].stops[co][seq]]; + const stopB = stopList[routeList[routeKey].stops[co][seq + 1]]; return getBearingStops(stopA, stopB); }).filter(brng => brng !== -1); - }, [routeList, stopList]); + }; - const routeStops : RouteStopCoSeq[]= useMemo(() => { - let _routeStops: RouteStopCoSeq[] = []; - if(routeId !== undefined) { - // StopDialog - let targetRouteStops = routeList[routeId].stops; - stopKeys.forEach(([co, stopId]) => { - let seq = targetRouteStops[co]?.indexOf(stopId) ?? -1; + const routeStops : RouteStopCoSeq[] = []; + if(routeId !== undefined) { + // StopDialog + let targetRouteStops = routeList[routeId].stops; + stopKeys.forEach(([co, stopId]) => { + let seq = targetRouteStops[co]?.indexOf(stopId) ?? -1; + if(seq != -1) { + routeStops.push({ + routeKey : routeId, + co : co, + seq : seq + }); + } + }); + } else { + // SwipableStopList (saved stop list) + stopKeys.forEach(([co, stopId]) => { + Object.keys(routeList).forEach(routeKey => { + let seq = routeList[routeKey]?.stops[co]?.indexOf(stopId) ?? -1; if(seq != -1) { - _routeStops.push({ - routeKey : routeId, + routeStops.push({ + routeKey : routeKey, co : co, seq : seq }); } - }); - } else { - // SwipableStopList (saved stop list) - stopKeys.forEach(([co, stopId]) => { - Object.keys(routeList).forEach(routeKey => { - let seq = routeList[routeKey]?.stops[co]?.indexOf(stopId) ?? -1; - if(seq != -1) { - _routeStops.push({ - routeKey : routeKey, - co : co, - seq : seq - }); - } - }) - }); - } - return _routeStops; - }, [stopKeys, routeId, routeList]); + }) + }); + } + const bearingTargets = getAllRouteStopsBearings(routeStops); - const isBearingInRange = useCallback((bearing : number) => { + const isBearingInRange = (bearing : number) => { if(BEARING_THRESHOLD >= 180 || bearingTargets.length == 0) { return true; - } else { - for(let i = 0; i < bearingTargets.length; ++i) { - let bearingMin = bearingTargets[i] - BEARING_THRESHOLD; - let bearingMax = bearingTargets[i] + BEARING_THRESHOLD; - if(bearingMin < 0) - bearingMin += 360; - if(bearingMax > 360) - bearingMax -= 360; - if((bearingMin <= bearingMax && bearingMin <= bearing && bearing <= bearingMax) - || (bearingMin > bearingMax && (bearingMin <= bearing || bearing <= bearingMax))) // crossing 0/360° mark, eg min=340°,max=020° - return true; - } - return false; } - }, [bearingTargets]); + for(let i = 0; i < bearingTargets.length; ++i) { + let bearingMin = bearingTargets[i] - BEARING_THRESHOLD; + let bearingMax = bearingTargets[i] + BEARING_THRESHOLD; + if(bearingMin < 0) + bearingMin += 360; + if(bearingMax > 360) + bearingMax -= 360; + if((bearingMin <= bearingMax && bearingMin <= bearing && bearing <= bearingMax) + || (bearingMin > bearingMax && (bearingMin <= bearing || bearing <= bearingMax))) // crossing 0/360° mark, eg min=340°,max=020° + return true; + } + return false; + }; - const searchNearbyStops = useCallback((targetStopId : string, excludedStopIdList : string[]) => { + const searchNearbyStops = (targetStopId : string, excludedStopIdList : string[]) => { const targetStop = stopList[targetStopId]; return Object.keys(stopList).filter((stopId) => { @@ -144,42 +136,40 @@ export const useStopGroup = ({ } return acc }, [] as Array); - }, [routeList, stopList, getAllRouteStopsBearings, isBearingInRange]); - - const stopGroup = useMemo>(() => { - let stopListEntries : StopListEntryExtended[] = []; + }; - // recursively search for nearby stops within thresholds (distance and bearing) - // stop searching when no new stops are found within range, or when stop list is getting too large - stopKeys.forEach((stopKey) => { - const [, stopId] = stopKey; // [co, stopId] - stopListEntries = stopListEntries.concat(searchNearbyStops(stopId, stopListEntries.map((stop) => stop.id))); - for(let i = 0; i < stopListEntries.length; ++i) { // use traditional for-loop as the length keeps expanding - stopListEntries = stopListEntries.concat(searchNearbyStops(stopListEntries[i].id, stopListEntries.map((stop) => stop.id))); - if(stopListEntries.length >= STOP_LIST_LIMIT) - break; - } - }); + const stopGroup : Array<[Company, string]> = []; + + let stopListEntries : StopListEntryExtended[] = []; - // sort by distance from first stop in stopMap (stopKeys[0]) - const _stopGroup : Array<[Company, string]> = []; - if(stopKeys.length > 0) { - let [, stopId] = stopKeys[0]; // [co, stopId] but don't use this co - stopListEntries = stopListEntries.map(stop => { - return { - ... stop, - distance : getDistanceStops(stopList[stopId], stop) - }; - }).sort((stopA, stopB) => { - return stopA.distance - stopB.distance - }); - stopListEntries.forEach((stopListEntry) => { - if(stopListEntry.routeStops.length > 0) - _stopGroup.push([stopListEntry.routeStops[0].co, stopListEntry.id]); - }); + // recursively search for nearby stops within thresholds (distance and bearing) + // stop searching when no new stops are found within range, or when stop list is getting too large + stopKeys.forEach((stopKey) => { + const [, stopId] = stopKey; // [co, stopId] + stopListEntries = stopListEntries.concat(searchNearbyStops(stopId, stopListEntries.map((stop) => stop.id))); + for(let i = 0; i < stopListEntries.length; ++i) { // use traditional for-loop as the length keeps expanding + stopListEntries = stopListEntries.concat(searchNearbyStops(stopListEntries[i].id, stopListEntries.map((stop) => stop.id))); + if(stopListEntries.length >= STOP_LIST_LIMIT) + break; } - return _stopGroup; - }, [stopKeys, stopList, searchNearbyStops]); + }); + + // sort by distance from first stop in stopMap (stopKeys[0]) + if(stopKeys.length > 0) { + let [, stopId] = stopKeys[0]; // [co, stopId] but don't use this co + stopListEntries = stopListEntries.map(stop => { + return { + ... stop, + distance : getDistanceStops(stopList[stopId], stop) + }; + }).sort((stopA, stopB) => { + return stopA.distance - stopB.distance + }); + stopListEntries.forEach((stopListEntry) => { + if(stopListEntry.routeStops.length > 0) + stopGroup.push([stopListEntry.routeStops[0].co, stopListEntry.id]); + }); + } return stopGroup; };