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$.
## 2026-03-31 - [Optimize station ID lookups and replace Object.values iterators]
**Learning:** Found multiple places in React render paths (`TripsPage.tsx`, `WalkTripEditor.tsx`, `RailRound.jsx`) where looking up a station by ID required an $O(N \times M)$ search over all lines and stations using nested `Object.values(railwayData).forEach(...)` calls. This allocated multiple arrays each time and wasted cycles.
**Action:** Implemented a reusable $O(1)$ lookup `getStationById` in `src/core/railwayRouting.ts` using a cached `Map` (similar to `buildStationIndex`). Also converted expensive `Object.values(currentRailwayData).some()` patterns to standard zero-allocation `for...in` loops.
**Learning 2:** Never use brittle regex replacements (e.g. `}); }); });` -> `} } }`) via ad-hoc `.cjs` scripts when refactoring deeply nested code, as it easily leads to unmatched braces and broken builds.
14 changes: 11 additions & 3 deletions src/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,17 @@ export const AppLayout: React.FC = () => {
const currentRailwayData = useStore.getState().railwayData;

// Only trigger if we have data and missing distances
const needsCalc = Object.values(currentRailwayData).some(line =>
line.stations.length > 1 && line.stations[0].distToNext === undefined
);

let needsCalc = false;
for (const lineKey in currentRailwayData) {
if (!Object.prototype.hasOwnProperty.call(currentRailwayData, lineKey)) continue;
const line = currentRailwayData[lineKey];
if (line.stations.length > 1 && line.stations[0].distToNext === undefined) {
needsCalc = true;
break;
}
}


if (needsCalc) {
const showFakeProgress = useStore.getState().showFakeProgress;
Expand Down
44 changes: 29 additions & 15 deletions src/RailRound.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ 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 { getStationById } from './core/railwayRouting';
import { VersionBadge } from './components/VersionBadge';
import manifest from '../public/geojson_manifest.json';

Expand Down Expand Up @@ -384,9 +386,14 @@ const LineSelector = ({ isOpen, onClose, onSelect, railwayData, allowedLines })
groups[category][normRegion][compKey].lines.push({ key, icon });
});

Object.values(groups).forEach(regionGroup => {
Object.values(regionGroup).forEach(companyGroup => {
Object.values(companyGroup).forEach(companyData => {

for (const catKey in groups) {
const regionGroup = groups[catKey];
for (const regionKey in regionGroup) {
const companyGroup = regionGroup[regionKey];
for (const compKey in companyGroup) {
const companyData = companyGroup[compKey];

// θΎ…εŠ©οΌšεˆ€ζ–­ηΊΏθ·―ζ˜―ε¦ζœ‰θ‡ͺθΊ«ηš„ηΊΏθ·― logoοΌˆθ€Œιžδ»…δΈΊε…¬εΈ logoοΌ‰
const hasLineLogo = (lineKey) => {
const meta = railwayData[lineKey]?.meta || {};
Expand Down Expand Up @@ -512,9 +519,10 @@ const LineSelector = ({ isOpen, onClose, onSelect, railwayData, allowedLines })
}

return result;
});
});
});

}
}
}
});

return { groups };
Expand Down Expand Up @@ -1267,12 +1275,11 @@ 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 Expand Up @@ -1462,9 +1469,16 @@ 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
23 changes: 11 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,11 @@ 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 +130,11 @@ 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 @@ -12,6 +12,39 @@ export const isCompanyCompatible = (meta1: CompanyMeta | undefined, meta2: Compa
return false;
};


// ι’„ζž„ε»Ίηš„η«™η‚ΉIDη΄’εΌ•ηΌ“ε­˜
let stationIdIndexCache: Map<string, {lineKey: string, stationIndex: number}> | null = null;
let lastRailwayDataRefForId: RailwayMap | null = null;

export const buildStationIdIndex = (railwayData: RailwayMap) => {
if (stationIdIndexCache && lastRailwayDataRefForId === railwayData) {
return stationIdIndexCache;
}

const index = new Map<string, {lineKey: string, stationIndex: number}>();
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++) {
index.set(line.stations[i].id, { lineKey, stationIndex: i });
}
}

stationIdIndexCache = index;
lastRailwayDataRefForId = railwayData;
return index;
};

export const getStationById = (railwayData: RailwayMap, stationId: string): Station | undefined => {
if (!stationId) return undefined;
const index = buildStationIdIndex(railwayData);
const entry = index.get(stationId);
if (!entry) return undefined;
return railwayData[entry.lineKey].stations[entry.stationIndex];
};

export const buildStationIndex = (railwayData: RailwayMap) => {
if (stationNameIndexCache && lastRailwayDataRef === railwayData) {
return stationNameIndexCache;
Expand Down
13 changes: 6 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,11 @@ 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