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
10 changes: 3 additions & 7 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
## 2026-03-31 - [Optimized Iteration over large collections]
**Learning:** Found an opportunity to replace chained array methods like flatMap, map, reduce, and Object.values/keys with single-pass manual 'for' and 'for...in' loops when processing arrays and objects to avoid allocating large temporary data structures. Used ES6 Maps/Sets where efficient counting/deduplication was needed.
**Action:** Apply this pattern to other performance sensitive areas where objects and arrays map over large datasets, and ensure that iteration checks 'Object.prototype.hasOwnProperty.call()' when utilizing 'for...in'. Note: This project lacks a package.json at the root so standard npm/pnpm lint tools might not be readily available.

## 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-06-25 - [React List Rendering O(N*M) Lookup Optimization]
**Learning:** Found a major bottleneck in list rendering (TripsPage, WalkTripEditor, etc.) where components were resolving station names/coordinates by iterating over the entire deeply nested `railwayData` object ($O(N \times M)$) inside the render flow. Replacing this with an $O(1)$ reference-cached `Map` lookup drastically improves performance.
**Action:** When working with large graph/network datasets in React, proactively build and pass $O(1)$ lookup maps (caches) instead of letting downstream components recursively search for entity metadata.
29 changes: 20 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 { getStationById } from './core/railwayRouting';
import { VersionBadge } from './components/VersionBadge';
import manifest from '../public/geojson_manifest.json';

Expand Down Expand Up @@ -1267,12 +1268,16 @@ 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;
});
if (railwayData) {
if (t.fromId) {
const startSt = getStationById(railwayData, t.fromId);
if (startSt) startName = startSt.station.name_ja;
}
if (t.toId) {
const endSt = getStationById(railwayData, t.toId);
if (endSt) endName = endSt.station.name_ja;
}
}

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

export const WalkTripEditor: React.FC = () => {
Expand Down Expand Up @@ -82,12 +83,16 @@ 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];
});
if (railwayData) {
if (form.fromId) {
const startSt = getStationById(railwayData, form.fromId);
if (startSt) startCoords = [startSt.station.lng, startSt.station.lat];
}
if (form.toId) {
const endSt = getStationById(railwayData, form.toId);
if (endSt) endCoords = [endSt.station.lng, endSt.station.lat];
}
}

if (startCoords && endCoords) {
walkPath = generateBezierPath(startCoords as [number, number], endCoords as [number, number]);
Expand Down Expand Up @@ -130,12 +135,16 @@ 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;
});
if (railwayData) {
if (form.fromId) {
const startSt = getStationById(railwayData, form.fromId);
if (startSt) startName = startSt.station.name_ja;
}
if (form.toId) {
const endSt = getStationById(railwayData, form.toId);
if (endSt) endName = endSt.station.name_ja;
}
}

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

Expand Down
34 changes: 34 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: Station, lineKey: string }> | 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,37 @@ export const buildStationIndex = (railwayData: RailwayMap) => {
return index;
};

/**
* O(1) Station lookup by ID.
* Caches the mapping of station IDs to their objects to prevent O(N*M) nested loops
* across rendering cycles. Automatically invalidates if the railwayData reference changes.
* ⚡ Performance Boost: Reduces component re-render time drastically for list items.
*/
export const getStationById = (railwayData: RailwayMap, stationId: string): { station: Station, lineKey: string } | null => {
if (stationIdIndexCache && lastRailwayDataForIdRef === railwayData) {
return stationIdIndexCache.get(stationId) || null;
}

const index = new Map<string, { station: Station, lineKey: string }>();
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];
if (!index.has(st.id)) {
index.set(st.id, { station: st, lineKey });
}
}
}

stationIdIndexCache = index;
lastRailwayDataForIdRef = railwayData;

return stationIdIndexCache.get(stationId) || null;
};


export const getTransferableLines = (station: Station | undefined, currentLineKey: string, railwayData: RailwayMap, strictMode = true) => {
if (!station) return [];
const currentMeta = railwayData[currentLineKey]?.meta;
Expand Down
17 changes: 11 additions & 6 deletions src/pages/TripsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { computeLoopVia, getLandmarks } from '../core/railwayRouting';
import { isMobile } from 'react-device-detect';
import { useShallow } from 'zustand/react/shallow';
import { useTranslation } from 'react-i18next';
import { getStationById } from '../core/railwayRouting';
import { LineLogo } from '../components/LineLogo';

const RouteSlice = React.memo(({ segments }: { segments: any[] }) => {
Expand Down Expand Up @@ -172,12 +173,16 @@ 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;
});
if (railwayData) {
if (trip.fromId) {
const startSt = getStationById(railwayData, trip.fromId);
if (startSt) startName = startSt.station.name_ja;
}
if (trip.toId) {
const endSt = getStationById(railwayData, trip.toId);
if (endSt) endName = endSt.station.name_ja;
}
}

const isTree = trip.walkType === 'tree';
const cls = {
Expand Down