Skip to content
Merged
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
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This guide explains how automation agents and human contributors should work wit
## Data Management
- CSV import order depends on the numeric prefix (`1!`, `2!`, ...). When adding datasets, choose a prefix that preserves foreign-key dependencies.
- `data/create_table.sql` drops and recreates tables, indexes, and foreign keys. Update this script alongside any schema or CSV column changes.
- **Through-service junction stations** – When a train type runs through a station where its lines connect, add a `5!station_station_types.csv` row for every line-specific `station_cd` at that station, even when those rows share one `station_g_cd`. The only exception is when the train type explicitly identifies a direction or line-specific operation that excludes one side. Omitting either ID makes the train type selectable from only one line in the app. For example, Hida at Gifu must include both the Takayama Main Line station (`1141601`) and the Tokaido Main Line station (`1150239`). Audit both sides whenever adding or editing a through-service pattern.
- `data_validator` currently verifies that `5!station_station_types.csv` references valid station and type IDs, and that order-sensitive station sequences in `3!stations.csv` stay intact under `ORDER BY e_sort, station_cd` (e.g. the Toei Oedo Line's Tochomae rows, whose misordering silently drops the station from ETA estimation). Extend the validator when new cross-references or order-sensitive spots are introduced and keep the process fail-fast (panic on invalid data).

## Testing and Quality
Expand All @@ -52,16 +53,19 @@ This guide explains how automation agents and human contributors should work wit
## gRPC Endpoint Overview
- **Stations** – `GetStationById`, `GetStationByIdList`, `GetStationsByGroupId`, `GetStationsByCoordinates`, `GetStationsByLineId`, `GetStationsByName`, `GetStationsByLineGroupId`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types.
- **Lines** – `GetLineById`, `GetLinesByIdList`, `GetLinesByName`. Results include company data and computed line symbols based on repository helpers.
- **Routes** – `GetRoutes`, `GetRoutesMinimal`. The minimal variant returns `RouteMinimalResponse` with deduplicated `LineMinimal` data; paging tokens are currently empty (pagination not implemented).
- **Routes** – `GetRoutes`, `GetRoutesMinimal`, `GetConnectedRoutes`. The minimal variant returns `RouteMinimalResponse` with deduplicated `LineMinimal` data; paging tokens are currently empty (pagination not implemented).
- **Train types** – `GetTrainTypesByStationId`, `GetRouteTypes`. Train types aggregate by line group and include related lines plus optional train type metadata. Rail variants use `TrainTypeKind::{Default, Branch, Rapid, Express, LimitedExpress, HighSpeedRapid, CommuterRapid}` (0-6); bus variants use `BusRoute` (7), which represents a `(route_id, shape_id)` operation pattern (e.g. 循環 / 短ターン / 支線) generated automatically from the configured GTFS bus feeds (Toei Bus, Seibu Bus, Keio Bus) and the converted Tokyu Bus JSON.
- **Default rail train types** – After the canonical CSV import, startup fills every active rail line containing at least one station with no `station_station_types` row with a deterministic, complete all-stop group. The generated rows exist only in PostgreSQL; canonical CSV files remain unchanged. `type_cd=100` represents 「普通」 and `type_cd=101` represents 「各駅停車」. An existing 100/101 assignment on the line takes precedence; otherwise startup selects the label per line through `LOCAL_SERVICE_RAIL_LINE_IDS` in `src/import.rs`. Generated `line_group_cd` values use `1,000,000,000 + line_cd`; startup fails on a collision. Bus lines are excluded and continue to use their GTFS-derived `BusRoute` groups.
- **GTFS bus integration** – At startup, `src/import.rs::integrate_gtfs_to_stations()` ingests GTFS feeds into `gtfs_*` tables and then projects them onto the shared `stations` / `lines` / `types` / `station_station_types` tables. Every configured GTFS feed is imported, including Seibu Bus and Keio Bus (both downloaded from ODPT with `ODPT_ACCESS_TOKEN`). Tokyu Bus ordinary-route `BusroutePattern`, `BusstopPole`, and `BusTimetable` JSON are converted into the same `gtfs_*` representation; pattern IDs become `shape_id` values so route variants remain queryable as bus TrainTypes. The Tokyu-operated Ota, Shinagawa, and Meguro community buses use their official GTFS feeds and matching JSON routes are excluded to prevent duplicates. `ODPT_ACCESS_TOKEN` is required for authenticated sources. Stops whose Tokyu JSON records omit coordinates remain available to name and route queries but not coordinate searches. `transport_type` (0: rail, 1: bus) on both `stations` and `lines` keeps rail and bus records queryable side by side. GTFS IDs are namespaced per feed before import to avoid cross-operator collisions. `line_cd` (100,000,000+), `station_cd` / `station_g_cd` (200,000,000+), and bus `type_cd` / `line_group_cd` (100,000,000+) are all deterministic fnv1a hashes that stay clear of the rail data ranges. Disable the entire bus pipeline with `DISABLE_BUS_FEATURE=true`.
- **Bus stop translations (readings & English)** – GTFS-JP `translations.txt` layouts differ per feed, so `load_gtfs_translations` resolves columns by header name (Seibu ships 6 columns without `record_sub_id`; Keio and the Tokyu community feeds ship 7) and indexes each `stop_name` translation under both keys it may use: `record_id` (== the stop_id, Seibu — with the "-NN" pole suffix also mapped to the parent stop_id) and `field_value` (== the Japanese stop_name, Keio / Tokyu community, where `record_id` is left empty). `import_gtfs_stops` then looks a stop's translation up by stop_id first, then by name. Keying only by `record_id` (the previous behavior) silently dropped every field_value-keyed feed, leaving `station_name_k` filled with the kanji stop_name and `station_name_r` empty. Readings arriving as half-width katakana (`ニシハチオウジ`, Keio / Tokyu community) are folded to full-width via `romaji::to_fullwidth_katakana()` before storage.
- **Bus English-name fallback** – When a feed provides no English (`en`) translation for a stop — e.g. Tokyu Bus ordinary-route JSON, which carries only `dc:title` and `odpt:kana` — `src/domain/romaji.rs::romaji_display_name()` derives a modified-Hepburn romanization (with macrons for long vowels, matching the curated rail style: Tōkyō / Kyōto / Shin-Ōsaka) from the kana reading, and `import.rs` fills `stop_name_r` with it. The fallback never overwrites a real `en` value, and a reading with no convertible kana stays `NULL` rather than emitting a partial transcription. Because `stop_name_r` is the single upstream source that fans out into the `stations` projection, `search_by_name`, and the romanized bus route/headsign names, this supplements every English-facing surface at once. When projecting into `stations`, `station_name_rn` is filled with the plain-ASCII spelling via `romaji::strip_macrons()` (Tōkyō → Tokyo), mirroring the rail dataset's `_r` (macron) / `_rn` (macron-free) column pair.
- **TTS metadata** – `Station`, `StationMinimal`, `Line`, and `TrainType` expose `name_ipa` / `name_roman_ipa` plus `name_tts_segments` for multi-segment pronunciation output. Use `name_tts_segments` when clients need per-token SSML construction for mixed-language names such as `Kasai-Rinkai Park`.
- **Connected routes** – `GetConnectedRoutes`. `QueryInteractor::get_connected_stations` is not implemented yet and returns an empty vector; update the use-case and infrastructure layers together when adding real logic.
- **Connected routes** – `GetConnectedRoutes` performs a bounded breadth-first search across train-type line groups. Transfers join at a shared station group, route order and per-stop pass metadata are preserved, and each returned candidate receives a deterministic virtual line-group ID in the upper half of the `uint32` range. Revisiting station groups and already-used train types is rejected to prevent cycles. Exploration loads only line-group ID, station-station-type ID, station-group ID, and pass metadata; full station rows are fetched after the result set is fixed. The search is additionally capped at eight train types, 4,096 expanded states, 65,536 evaluated candidates, and 32 results to bound computation and result size.
- Changes to the service contract require coordinated updates to `proto/stationapi.proto`, regenerated code via `tonic-build`, and corresponding adjustments in both presentation and use-case layers.

## Contribution Guidelines
- **Git-flow** – Follow Git-flow with `dev` serving as this repository's `develop` branch. Create ordinary work branches from the latest `origin/dev`, use the `feature/<description>` naming convention, and target their pull requests to `dev`. Do not create or target a branch named `develop`.
- **Pull requests** – Assign every pull request to `@TinyKitten` when creating it, open it as ready for review rather than as a draft, and use `.github/pull_request_template.md` without omitting or replacing its sections or checklists.
- **Prioritize quality and performance over implementation speed** – Always favor code quality and runtime performance over velocity. Be mindful of algorithmic complexity and look for opportunities to replace O(n×m) linear scans with O(n+m) indexed lookups (e.g., HashMaps). Avoid unnecessary JOINs and redundant queries at the SQL level. When a change affects performance, document the before/after complexity and query plan impact in the pull request.
- Document the commands you executed (for example, ``cargo fmt && cargo clippy --all-targets --all-features && make test-unit``) and their outcomes in every pull request.
- For database, gRPC, or schema updates, add architectural notes under `docs/` and synchronize README references so onboarding materials stay accurate.
Expand Down
12 changes: 6 additions & 6 deletions data/5!station_station_types.csv
Original file line number Diff line number Diff line change
Expand Up @@ -13854,12 +13854,12 @@ DEFAULT,1141604,225,348,1,蘇原
DEFAULT,1141603,225,348,1,那加
DEFAULT,1141602,225,348,1,長森
DEFAULT,1141601,225,348,0,岐阜
DEFAULT,1150238,225,348,0,岐阜
DEFAULT,1150237,225,348,1,木曽川
DEFAULT,1150236,225,348,0,尾張一宮
DEFAULT,1150235,225,348,1,稲沢
DEFAULT,1150234,225,348,1,清洲
DEFAULT,1141101,225,348,1,枇杷島
DEFAULT,1150239,225,348,0,岐阜
DEFAULT,1150238,225,348,1,木曽川
DEFAULT,1150237,225,348,2,尾張一宮
DEFAULT,1150236,225,348,1,稲沢
DEFAULT,1150235,225,348,1,清洲
DEFAULT,1150234,225,348,1,枇杷島
DEFAULT,1150233,225,348,0,名古屋
DEFAULT,9940101,306,349,0,大月
DEFAULT,9940102,306,349,1,上大月
Expand Down
32 changes: 32 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,38 @@ CREATE INDEX idx_performance_station_name_trgm ON stations
| 経路検索 | `GetRoutes`, `GetRoutesMinimal`, `GetConnectedRoutes` |
| 列車種別 | `GetTrainTypesByStationId`, `GetRouteTypes` |

### 接続経路探索

`GetConnectedRoutes` は、始点の駅グループに停車する列車種別から幅優先で探索し、
同じ駅グループに停車する別の列車種別へ接続します。Repository は探索階層ごとの
駅グループをまとめて問い合わせ、該当する `line_group_cd` の駅列も一括取得するため、
候補ごとの N+1 クエリを発生させません。

探索中は通過駅を乗換地点にせず、利用済みの列車種別および訪問済みの駅グループを
再訪しません。探索は最大 8 列車種別、4,096 展開状態、65,536 評価候補、
32 返却候補に制限します。
完成した経路は接続駅を一度だけ含む駅列へ連結し、各区間の `stop_condition` を保持します。
返却時には経路の列車種別列と駅グループ列から決定的な仮想 `lineGroupId` を生成し、
経路内の全 `station.train_type.group_id` に同じ値を設定します。仮想 ID は既存の
PostgreSQL `INTEGER` ID と衝突しない `uint32` 上位半分を使用します。

各探索階層では未取得の駅グループを 1 回のクエリへまとめ、そこで判明した未取得の
`line_group_cd` の駅列も 1 回で取得します。このため Repository 呼び出しは最大でも
階層あたり 2 回で、候補経路ごとの N+1 クエリや同じ列車種別の再取得はありません。
探索用の駅列は `line_group_cd`、`station_station_types.id`、`station_g_cd`、`pass` だけを取得し、経路状態にも
この軽量な参照だけを保持します。駅名、住所、座標、路線属性、列車種別属性を含む完全な
`Station` は探索中に生成・複製せず、返却候補が確定した後、その候補が実際に使用する
`line_group_cd` に限定して一括取得します。これにより探索状態数と駅エンティティの大きさの
積に比例していたメモリ使用量を避けます。
取得後は駅グループと `line_group_cd` を `HashMap` に一度だけ分類し、状態と駅列を
毎回総当たりする O(n×m) の処理を、入力件数に比例する O(n+m) の参照へ置き換えます。
探索そのものの最悪計算量は各状態の始点候補数と駅列長の積にも依存するため、状態数とは
独立した評価候補数の上限で `start_indices × pattern.len()` の走査も制御します。

SQL は `stations.station_g_cd`、`station_station_types.station_cd`、
`station_station_types.line_group_cd` の既存 btree index を利用できます。列車種別の存在確認に
必要な `types` との JOIN のみを行い、路線・会社など探索に不要なテーブルは JOIN しません。

### Proto 更新時の注意点

1. **後方互換性**: 新フィールドには `optional` キーワードを使用
Expand Down
2 changes: 0 additions & 2 deletions docs/technical_debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ pub struct Station {
|----------|--------|------|
| `stationapi/src/use_case/interactor/query.rs` | 604 | `// TODO: SQLで同等の処理を行う` - 経路検証がアプリケーション側で実行 |
| `stationapi/src/use_case/interactor/query.rs` | 702 | `// TODO: SQLで同等の処理を行う` - 経路フィルタリングがアプリケーション層で処理 |
| `stationapi/src/use_case/interactor/query.rs` | 843 | `// TODO: 未実装` - `get_connected_stations()` が空配列を返却 |

```rust
// query.rs:604-610
Expand Down Expand Up @@ -319,5 +318,4 @@ let station_numbers_raw = [
| **中** | Row 構造体のコード生成検討 | `src/infrastructure/*.rs` | メンテナンス性 |
| **中** | メソッド命名の改善 | `src/domain/repository/line_repository.rs:23` | 可読性 |
| **中** | ハードコード値の定数化 | 複数ファイル | 保守性 |
| **低** | get_connected_stations の実装 | `src/use_case/interactor/query.rs:843` | 機能完成度 |
| **低** | UI レイヤーのテスト追加 | `src/presentation/` | テストカバレッジ |
31 changes: 31 additions & 0 deletions stationapi/src/domain/repository/station_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ use crate::domain::{
error::DomainError,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConnectedRoutePatternStop {
pub line_group_id: u32,
pub station_station_type_id: i32,
pub station_group_id: u32,
pub pass: Option<i32>,
}

#[async_trait]
pub trait StationRepository: Send + Sync + 'static {
async fn find_by_id(&self, id: u32) -> Result<Option<Station>, DomainError>;
Expand Down Expand Up @@ -51,6 +59,29 @@ pub trait StationRepository: Send + Sync + 'static {
&self,
line_group_ids: &[u32],
) -> Result<Vec<Station>, DomainError>;
/// Fetch only the fields needed while exploring connected routes.
///
/// The default keeps lightweight test repositories source-compatible.
/// Production repositories should override this to avoid materializing full
/// `Station` entities for every explored line group.
async fn get_connected_route_pattern_stops(
&self,
line_group_ids: &[u32],
) -> Result<Vec<ConnectedRoutePatternStop>, DomainError> {
Ok(self
.get_by_line_group_id_vec(line_group_ids)
.await?
.into_iter()
.filter_map(|stop| {
Some(ConnectedRoutePatternStop {
line_group_id: stop.line_group_cd? as u32,
station_station_type_id: stop.sst_id?,
station_group_id: stop.station_g_cd as u32,
pass: stop.pass,
})
})
.collect())
}
async fn get_bus_stops_near_stations(
&self,
coords: &[(u32, f64, f64)], // (station_g_cd, lat, lon)
Expand Down
11 changes: 11 additions & 0 deletions stationapi/src/domain/repository/train_type_repository.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
use async_trait::async_trait;
use std::collections::HashMap;

use crate::domain::{entity::train_type::TrainType, error::DomainError};

#[async_trait]
pub trait TrainTypeRepository: Send + Sync + 'static {
/// Return train-type line groups that stop at each requested station group.
///
/// The default keeps existing lightweight test repositories source-compatible;
/// repositories used by connected-route search must override it.
async fn get_line_group_ids_by_station_group_ids(
&self,
_station_group_ids: &[u32],
) -> Result<HashMap<u32, Vec<u32>>, DomainError> {
Ok(HashMap::new())
}
async fn get_by_line_group_id(&self, line_group_id: u32)
-> Result<Vec<TrainType>, DomainError>;
async fn get_by_station_id(&self, station_id: u32) -> Result<Vec<TrainType>, DomainError>;
Expand Down
Loading
Loading