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$.
## 2026-04-28 - [O(1) Station Lookup for List Rendering]
**Learning:** Found an opportunity to replace O(N) array loops inside O(T) trip list rendering loops with a single O(1) indexed lookup Map. Previously, fetching station names by their ID within UI components involved iterating over the entire massive `railwayData` object on every render cycle, severely impacting performance.
**Action:** Created `getStationById` in `src/core/railwayRouting.ts` utilizing a local cached Map object tied to `railwayData` reference equality. Replaced all `Object.values(railwayData).forEach` station name searches in `TripsPage.tsx`, `WalkTripEditor.tsx`, and `RailRound.jsx` with this O(1) lookup method.
13 changes: 7 additions & 6 deletions src/RailRound.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import * as turf from '@turf/turf';
import { isMobile } from 'react-device-detect';
import { getStationById } from './core/railwayRouting';

// Quick import-time log to ensure the module loads when Vite imports it.
try { console.log('[icon] module loaded'); } catch { }
Expand Down Expand Up @@ -1267,12 +1268,12 @@ 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);
if (s) startName = s.name_ja;
const e = line.stations.find(st => st.id === t.toId);
if (e) endName = e.name_ja;
});

const s = getStationById(railwayData, t.fromId || '');
if (s) startName = s.name_ja;

const e = getStationById(railwayData, t.toId || '');
if (e) endName = e.name_ja;

const isTree = t.walkType === 'tree';
const cls = {
Expand Down
25 changes: 13 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,12 @@ 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 +131,12 @@ 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
33 changes: 33 additions & 0 deletions src/core/railwayRouting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { calcDist } from '../core/tripCalculator'; // Ensure calcDist is exporte
let stationNameIndexCache: Map<string, {lineKey: string, stationIndex: number}[]> | null = null;
let lastRailwayDataRef: RailwayMap | null = null;

let stationIdIndexCache: Map<string, Station> | null = null;
let lastRailwayDataForIdRef: RailwayMap | null = null;

export const isCompanyCompatible = (meta1: CompanyMeta | undefined, meta2: CompanyMeta | undefined) => {
if (!meta1 || !meta2) return false;
if (meta1.company === meta2.company && meta1.company !== "上传数据" && meta1.company !== "未知") return true;
Expand Down Expand Up @@ -34,6 +37,36 @@ export const buildStationIndex = (railwayData: RailwayMap) => {
return index;
};

/**
* O(1) Station Lookup by ID
*
* Replaces expensive O(N) nested iterations across all railway lines/stations.
* Caches the station index map and invalidates when `railwayData` reference changes.
* Measurably improves React rendering performance in lists mapping trip items.
*/
export const getStationById = (railwayData: RailwayMap, stationId: string): Station | undefined => {
if (!stationId) return undefined;

if (stationIdIndexCache && lastRailwayDataForIdRef === railwayData) {
return stationIdIndexCache.get(stationId);
}

const index = new Map<string, Station>();
for (const lineKey in railwayData) {
if (!Object.prototype.hasOwnProperty.call(railwayData, lineKey)) continue;
const line = railwayData[lineKey];
if (!line.stations) continue;
for (let i = 0; i < line.stations.length; i++) {
const st = line.stations[i];
index.set(st.id, st);
}
}

stationIdIndexCache = index;
lastRailwayDataForIdRef = railwayData;
return index.get(stationId);
};

export const getTransferableLines = (station: Station | undefined, currentLineKey: string, railwayData: RailwayMap, strictMode = true) => {
if (!station) return [];
const currentMeta = railwayData[currentLineKey]?.meta;
Expand Down
14 changes: 7 additions & 7 deletions src/pages/TripsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Train, Edit2, Trash2, Star, Plus, MapPin, Upload } from 'lucide-react';
import { useStore } from '../store';
import { DropZone } from '../components/DragContext';
import { getRouteVisualData } from '../core/tripCalculator';
import { computeLoopVia, getLandmarks } from '../core/railwayRouting';
import { computeLoopVia, getLandmarks, getStationById } from '../core/railwayRouting';
import { isMobile } from 'react-device-detect';
import { useShallow } from 'zustand/react/shallow';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -172,12 +172,12 @@ 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