Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
## 2024-04-15 - [Avoid O(N log N) Sorting on Massive Geographical Collections]
**Learning:** In spatial queries like `findNearbyStations` where we scan `railwayData` containing thousands of stations to find the top K nearest points, allocating all elements to an array and running `Array.prototype.sort()` results in massive temporary object allocation and $O(N \log N)$ execution time (taking ~8.5ms in benchmarks).
**Action:** Replace full array sorts with a bounded Top-K array using a simple $O(K)$ insertion sort during the $O(N)$ iteration phase. This brings the time complexity effectively down to $O(N)$, speeding up operations by ~36x (taking ~0.24ms). Remember to apply a final sort if total elements found are less than $K$.

## 2024-05-20 - [Optimize N*M Lookup in getTransferableLines]
**Learning:** Found a major performance bottleneck where `getTransferableLines` performed an $O(N \times M)$ search over all lines and their stations on every call just to match a station by name. Instead of iterating globally, utilizing an existing $O(1)$ lookup cache (`stationIndexMap` generated by `buildStationIndex`) significantly reduces overhead.
**Action:** When searching across a large collection of items (like `railwayData`) to find occurrences of a specific item, favor building an index map to perform $O(1)$ lookups instead of globally traversing the data structure repeatedly.
30 changes: 21 additions & 9 deletions src/RailRound.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Tutorial from './components/Tutorial';
import { api } from './services/api';
import { db } from './utils/db';
import { calcDist, sliceGeoJsonPath, getRouteVisualData, calculateLatestStats, stitchRoutes } from './core/tripCalculator';
import { buildStationIndex } from './core/railwayRouting';
import { VersionBadge } from './components/VersionBadge';
import manifest from '../public/geojson_manifest.json';

Expand Down Expand Up @@ -256,17 +257,27 @@ const getTransferableLines = (station, currentLineKey, railwayData, strictMode =
});
}

Object.keys(railwayData).forEach(lineKey => {
if (lineKey === currentLineKey) return;
if (validLines.has(lineKey)) return;
const nextMeta = railwayData[lineKey].meta;
if (strictMode && !isCompanyCompatible(currentMeta, nextMeta)) return;
const sameNameStation = railwayData[lineKey].stations.find(s => s.name_ja === station.name_ja);
const stationIndexMap = buildStationIndex(railwayData);
const sameNameNodes = stationIndexMap.get(station.name_ja) || [];

for (let i = 0; i < sameNameNodes.length; i++) {
const tNode = sameNameNodes[i];
const lineKey = tNode.lineKey;
if (lineKey === currentLineKey) continue;
if (validLines.has(lineKey)) continue;

const nextLine = railwayData[lineKey];
if (!nextLine || !nextLine.stations) continue;

const nextMeta = nextLine.meta;
if (strictMode && !isCompanyCompatible(currentMeta, nextMeta)) continue;

const sameNameStation = nextLine.stations[tNode.stationIndex];
if (sameNameStation) {
const dist = calcDist(station.lat, station.lng, sameNameStation.lat, sameNameStation.lng);
if (dist < 2.0) validLines.add(lineKey);
}
});
}
return Array.from(validLines);
};

Expand Down Expand Up @@ -743,14 +754,15 @@ const TripEditor = ({
let warning = null;

if (prevLineData && prevEndStName) {
const prevEndSt = prevLineData.stations.find(s => s.id === prevSegment.toId);

const transferable = getTransferableLines(prevEndSt, prevSegment.lineKey, railwayData, true);
const allKeys = Object.keys(railwayData);
currentAllowed = allKeys.filter(lineKey => {
// FIX: 必须包含已选
if (lineKey === segment.lineKey) return true;
const currentMeta = railwayData[lineKey].meta;
if (!isCompanyCompatible(prevLineData.meta, currentMeta)) return false;
const prevEndSt = prevLineData.stations.find(s => s.id === prevSegment.toId);
const transferable = getTransferableLines(prevEndSt, prevSegment.lineKey, railwayData, true);
return transferable.includes(lineKey);
});
if (currentAllowed.length === 0 && !segment.lineKey) warning = "无可换乘的同公司/JR线路";
Expand Down
8 changes: 5 additions & 3 deletions src/components/modals/TripEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useStore, EditorMode } from '../../store';
import { DropZone } from '../DragContext';
import { StationLineSearchModal, SearchModalMode } from './StationSearchModal';
import { LineLogo } from '../LineLogo';
import { isCompanyCompatible, getTransferableLines, findRoute, computeLoopVia, getLandmarks } from '../../core/railwayRouting'; // Will need to ensure these are typed
import { isCompanyCompatible, getTransferableLines, findRoute, computeLoopVia, getLandmarks, buildStationIndex } from '../../core/railwayRouting'; // Will need to ensure these are typed
import { calcDist } from '../../core/tripCalculator';
import { useShallow } from 'zustand/react/shallow';
import { useUserData } from '../../hooks/useUserData';
Expand Down Expand Up @@ -264,11 +264,12 @@ export const TripEditor: React.FC = () => {
const prevLineData = railwayData[prevSegment.lineKey];
const prevEndSt = prevLineData?.stations.find((s: any) => s.id === prevSegment.toId);
if (prevLineData && prevEndSt) {
const stationIndex = buildStationIndex(railwayData);
const transferable = getTransferableLines(prevEndSt, prevSegment.lineKey, railwayData, true, stationIndex);
const allKeys = Object.keys(railwayData);
currentAllowed = allKeys.filter(lineKey => {
const currentMeta = railwayData[lineKey]?.meta;
if (!currentMeta || !isCompanyCompatible(prevLineData.meta, currentMeta)) return false;
const transferable = getTransferableLines(prevEndSt, prevSegment.lineKey, railwayData, true);
return transferable.includes(lineKey) || lineKey === prevSegment.lineKey;
});
}
Expand Down Expand Up @@ -410,12 +411,13 @@ export const TripEditor: React.FC = () => {
let isDisconnected = false;

if (prevLineData && prevEndStName && prevEndSt) {
const stationIndex = buildStationIndex(railwayData);
const transferable = getTransferableLines(prevEndSt, prevSegment!.lineKey, railwayData, true, stationIndex);
const allKeys = Object.keys(railwayData);
currentAllowed = allKeys.filter(lineKey => {
if (lineKey === segment.lineKey) return true;
const currentMeta = railwayData[lineKey]?.meta;
if (!currentMeta || !isCompanyCompatible(prevLineData.meta, currentMeta)) return false;
const transferable = getTransferableLines(prevEndSt, prevSegment!.lineKey, railwayData, true);
return transferable.includes(lineKey);
});
if (currentAllowed.length === 0 && !segment.lineKey) warning = t('tripEdit.noTransferWarning', '无可换乘的同公司/JR线路');
Expand Down
30 changes: 22 additions & 8 deletions src/core/railwayRouting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,47 @@ export const buildStationIndex = (railwayData: RailwayMap) => {
return index;
};

export const getTransferableLines = (station: Station | undefined, currentLineKey: string, railwayData: RailwayMap, strictMode = true) => {
export const getTransferableLines = (
station: Station | undefined,
currentLineKey: string,
railwayData: RailwayMap,
strictMode = true,
optionalStationIndex?: Map<string, { lineKey: string; stationIndex: number }[]>
) => {
if (!station) return [];
const currentMeta = railwayData[currentLineKey]?.meta;
if (!currentMeta) return [];
const validLines = new Set<string>();

if (station.transfers && Array.isArray(station.transfers)) {
station.transfers.forEach(lineKey => {
station.transfers.forEach((lineKey) => {
if (railwayData[lineKey]) {
const nextMeta = railwayData[lineKey].meta;
validLines.add(lineKey);

}
});
}

for (const lineKey in railwayData) {
if (!Object.prototype.hasOwnProperty.call(railwayData, lineKey)) continue;
// Optimization: Use prebuilt index if provided, or rely on the internally memoized buildStationIndex.
// The index effectively eliminates the O(N x M) global search loop across all lines and stations.
const stationIndexMap = optionalStationIndex || buildStationIndex(railwayData);
const sameNameNodes = stationIndexMap.get(station.name_ja) || [];

for (let i = 0; i < sameNameNodes.length; i++) {
const tNode = sameNameNodes[i];
const lineKey = tNode.lineKey;
if (lineKey === currentLineKey) continue;
if (validLines.has(lineKey)) continue;
const nextMeta = railwayData[lineKey].meta;
const sameNameStation = railwayData[lineKey].stations.find(s => s.name_ja === station.name_ja);

const nextLine = railwayData[lineKey];
if (!nextLine || !nextLine.stations) continue;

const sameNameStation = nextLine.stations[tNode.stationIndex];
if (sameNameStation) {
const dist = calcDist(station.lat, station.lng, sameNameStation.lat, sameNameStation.lng);
if (dist < 0.5) validLines.add(lineKey);
}
}

return Array.from(validLines);
};

Expand Down