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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
## 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-18 - [O(1) Dictionary Lookup for Arrays]
**Learning:** In highly nested arrays and components, running `Object.values(railwayData).forEach()` multiple times to scan through `line.stations.find()` just to lookup a single ID results in $O(N \times M)$ overhead, blocking UI threads.
**Action:** Implemented a one-time cached `Map` in `src/core/railwayRouting.ts` (`stationIdIndexCache`) to index stations by their IDs for O(1) instantaneous lookups. The cache safely invalidates by comparing the reference identity of the underlying global immutable data.
13 changes: 7 additions & 6 deletions src/RailRound.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const { meta } = changelog;
import { useStore } from './store';
import toast from 'react-hot-toast';
import { ESLint } from 'eslint';
import { getStationById } from './core/railwayRouting';

const CURRENT_VERSION = meta["currentVersion"];
const LAST_MODIFIED = manifest.lastModified
Expand Down Expand Up @@ -1267,12 +1268,10 @@ const RecordsView = ({ trips, railwayData, setTrips, onEdit, onDelete, onAdd, se
if (isWalk) {
let startName = t.fromId || '';
let endName = t.toId || '';
Object.values(railwayData).forEach(line => {
const s = line.stations.find(st => st.id === t.fromId);
const s = getStationById(railwayData, t.fromId);
if (s) startName = s.name_ja;
const e = line.stations.find(st => st.id === t.toId);
const e = getStationById(railwayData, t.toId);
if (e) endName = e.name_ja;
});

const isTree = t.walkType === 'tree';
const cls = {
Expand Down Expand Up @@ -1462,9 +1461,11 @@ const StatsView = ({ trips, railwayData, geoData, user, userProfile, segmentGeom
let count = 0;
if (railwayData) {
const uniqueStations = new Set();
Object.values(railwayData).forEach(line => {
for (const lineKey in railwayData) {
if (!Object.prototype.hasOwnProperty.call(railwayData, lineKey)) continue;
const line = railwayData[lineKey];
if (line.stations) line.stations.forEach(s => uniqueStations.add(s.id));
});
}
count = uniqueStations.size;
}
return count;
Expand Down
21 changes: 9 additions & 12 deletions src/components/modals/WalkTripEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useUserData } from '../../hooks/useUserData';
import * as turf from '@turf/turf';
import { useTranslation } from 'react-i18next';
import { showAlert, showConfirm } from '../../utils/alerts';
import { getStationById } from '../../core/railwayRouting';

export const WalkTripEditor: React.FC = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -82,12 +83,10 @@ export const WalkTripEditor: React.FC = () => {
// Find coordinates for the Bezier curve
let startCoords = null;
let endCoords = null;
Object.values(railwayData).forEach(line => {
const s = line.stations.find(st => st.id === form.fromId);
if (s) startCoords = [s.lng, s.lat];
const e = line.stations.find(st => st.id === form.toId);
if (e) endCoords = [e.lng, e.lat];
});
const s = getStationById(railwayData, form.fromId);
if (s) startCoords = [s.lng, s.lat];
const e = getStationById(railwayData, form.toId);
if (e) endCoords = [e.lng, e.lat];

if (startCoords && endCoords) {
walkPath = generateBezierPath(startCoords as [number, number], endCoords as [number, number]);
Expand Down Expand Up @@ -130,12 +129,10 @@ export const WalkTripEditor: React.FC = () => {
// Resolving station names for read-only display
let startName = t('walk.unknownStart', "未知起点");
let endName = t('walk.unknownEnd', "未知终点");
Object.values(railwayData).forEach(line => {
const s = line.stations.find(st => st.id === form.fromId);
if (s) startName = s.name_ja;
const e = line.stations.find(st => st.id === form.toId);
if (e) endName = e.name_ja;
});
const s = getStationById(railwayData, form.fromId);
if (s) startName = s.name_ja;
const e = getStationById(railwayData, form.toId);
if (e) endName = e.name_ja;

const isTree = form.walkType === 'tree';

Expand Down
35 changes: 28 additions & 7 deletions src/core/railwayRouting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { calcDist } from '../core/tripCalculator'; // Ensure calcDist is exporte

// 预构建的换乘站索引缓存
let stationNameIndexCache: Map<string, {lineKey: string, stationIndex: number}[]> | null = null;
let stationIdIndexCache: Map<string, {lineKey: string, stationIndex: number}> | null = null;
let lastRailwayDataRef: RailwayMap | null = null;

export const isCompanyCompatible = (meta1: CompanyMeta | undefined, meta2: CompanyMeta | undefined) => {
Expand All @@ -13,25 +14,45 @@ export const isCompanyCompatible = (meta1: CompanyMeta | undefined, meta2: Compa
};

export const buildStationIndex = (railwayData: RailwayMap) => {
if (stationNameIndexCache && lastRailwayDataRef === railwayData) {
if (stationNameIndexCache && stationIdIndexCache && lastRailwayDataRef === railwayData) {
return stationNameIndexCache;
}

const index = new Map<string, {lineKey: string, stationIndex: number}[]>();
const nameIndex = new Map<string, {lineKey: string, stationIndex: number}[]>();
const idIndex = new Map<string, {lineKey: string, stationIndex: number}>();

for (const lineKey in railwayData) {
if (!Object.prototype.hasOwnProperty.call(railwayData, lineKey)) continue;
const line = railwayData[lineKey];
line.stations.forEach((st, idx) => {
if (!index.has(st.name_ja)) {
index.set(st.name_ja, []);
if (!nameIndex.has(st.name_ja)) {
nameIndex.set(st.name_ja, []);
}
nameIndex.get(st.name_ja)!.push({ lineKey, stationIndex: idx });

if (st.id) {
idIndex.set(st.id, { lineKey, stationIndex: idx });
}
index.get(st.name_ja)!.push({ lineKey, stationIndex: idx });
});
}

stationNameIndexCache = index;
stationNameIndexCache = nameIndex;
stationIdIndexCache = idIndex;
lastRailwayDataRef = railwayData;
return index;
return nameIndex;
};

export const getStationById = (railwayData: RailwayMap, stationId: string): Station | undefined => {
if (!stationId) return undefined;

// Ensure caches are built
buildStationIndex(railwayData);

if (stationIdIndexCache && stationIdIndexCache.has(stationId)) {
const info = stationIdIndexCache.get(stationId)!;
return railwayData[info.lineKey]?.stations[info.stationIndex];
}
return undefined;
};

export const getTransferableLines = (station: Station | undefined, currentLineKey: string, railwayData: RailwayMap, strictMode = true) => {
Expand Down
11 changes: 5 additions & 6 deletions src/pages/TripsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,10 @@ export const TripsPage: React.FC = () => {
if (isWalk) {
let startName = trip.fromId || '';
let endName = trip.toId || '';
Object.values(railwayData).forEach(line => {
const s = line.stations.find(st => st.id === trip.fromId);
if (s) startName = s.name_ja;
const e = line.stations.find(st => st.id === trip.toId);
if (e) endName = e.name_ja;
});
const s = getStationById(railwayData, trip.fromId);
if (s) startName = s.name_ja;
const e = getStationById(railwayData, trip.toId);
if (e) endName = e.name_ja;

const isTree = trip.walkType === 'tree';
const cls = {
Expand Down Expand Up @@ -288,6 +286,7 @@ export const TripsPage: React.FC = () => {
};

import { ArrowUp, ArrowDown } from 'lucide-react';
import { getStationById } from '../core/railwayRouting';

export const FloatingActionButtons: React.FC<{
fileInputRef: React.RefObject<HTMLInputElement>,
Expand Down