Skip to content
2 changes: 1 addition & 1 deletion stationapi/proto
Submodule proto updated 1 files
+2 −2 stationapi.proto
12 changes: 7 additions & 5 deletions stationapi/src/domain/arrival_estimation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,10 @@ mod tests {

#[test]
fn run_margin_scales_run_time_but_not_dwell() {
let mut p = EstimationParams::default();
p.run_margin = 1.0;
let mut p = EstimationParams {
run_margin: 1.0,
..Default::default()
};
let base = segment_run_minutes(5_000.0, 80.0, &p);
p.run_margin = 1.15;
let with_margin = segment_run_minutes(5_000.0, 80.0, &p);
Expand Down Expand Up @@ -838,7 +840,7 @@ mod tests {
// 実乗車時間(5〜6分)より大幅に長い約 6.9 分と推定されてしまう。
let a = station(1, 11341, 36.326849, 139.193704, Some(4866.8));
let b = station(2, 11341, 36.359018, 139.242463, Some(4866.8));
let stations = vec![a, b];
let stations = [a, b];
let refs: Vec<&Station> = stations.iter().collect();
let est = estimate_arrival_minutes(&refs, &p);

Expand Down Expand Up @@ -924,7 +926,7 @@ mod tests {
let p = EstimationParams::default();
// 停留所間隔 3km の直行区間。地下鉄扱い(75km/h × 1.2 = 90km/h)のままだと
// 約 3.4 分と過小評価される。バス上限 50km/h では約 5.7 分。
let stations = vec![
let stations = [
bus_station(1, 100_000_001, 35.72, 139.69),
bus_station(2, 100_000_001, 35.747, 139.69), // 0.027 度 ≈ 3km
];
Expand All @@ -944,7 +946,7 @@ mod tests {
let mut b = bus_station(2, 100_000_001, 35.747, 139.69);
a.kind = kind;
b.kind = kind;
let stations = vec![a, b];
let stations = [a, b];
let refs: Vec<&Station> = stations.iter().collect();
estimate_arrival_minutes(&refs, &p)[1].cumulative_minutes
};
Expand Down
2 changes: 1 addition & 1 deletion stationapi/src/domain/entity/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ mod tests {
None,
None,
None,
symbols.clone(),
symbols,
Some("JY".to_string()),
Some("JR".to_string()),
None,
Expand Down
25 changes: 15 additions & 10 deletions stationapi/src/domain/ipa.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::sync::{LazyLock, RwLock};
use std::sync::{Arc, LazyLock, RwLock};

/// Cached IPA computation result for a single name.
#[derive(Clone, Debug)]
Expand All @@ -11,10 +11,10 @@ pub struct IpaResult {

type IpaCacheKey = (String, Option<String>);

static STATION_IPA_CACHE: LazyLock<RwLock<HashMap<IpaCacheKey, IpaResult>>> =
static STATION_IPA_CACHE: LazyLock<RwLock<HashMap<IpaCacheKey, Arc<IpaResult>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));

static LINE_IPA_CACHE: LazyLock<RwLock<HashMap<IpaCacheKey, IpaResult>>> =
static LINE_IPA_CACHE: LazyLock<RwLock<HashMap<IpaCacheKey, Arc<IpaResult>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));

/// Compute all three IPA outputs in a single pass, eliminating the redundant
Expand Down Expand Up @@ -45,30 +45,35 @@ fn compute_line_ipa(name_katakana: &str, name_roman: Option<&str>) -> IpaResult
}

fn cached_lookup(
cache: &LazyLock<RwLock<HashMap<IpaCacheKey, IpaResult>>>,
cache: &LazyLock<RwLock<HashMap<IpaCacheKey, Arc<IpaResult>>>>,
key: &IpaCacheKey,
compute: impl FnOnce() -> IpaResult,
) -> IpaResult {
) -> Arc<IpaResult> {
// Fast path: read lock
if let Some(result) = cache.read().unwrap().get(key) {
return result.clone();
return Arc::clone(result);
}
// Slow path: compute and insert
let result = compute();
cache.write().unwrap().insert(key.clone(), result.clone());
let result = Arc::new(compute());
cache
.write()
.unwrap()
.insert(key.clone(), Arc::clone(&result));
result
}

/// Compute IPA for station/train-type names with memoization.
pub fn compute_ipa_cached(name_katakana: &str, name_roman: Option<&str>) -> IpaResult {
/// Arcで返すことでキャッシュヒット時にTTSセグメントのディープクローンを避ける。
pub fn compute_ipa_cached(name_katakana: &str, name_roman: Option<&str>) -> Arc<IpaResult> {
let key = (name_katakana.to_string(), name_roman.map(str::to_string));
cached_lookup(&STATION_IPA_CACHE, &key, || {
compute_ipa(name_katakana, name_roman)
})
}

/// Compute IPA for line names (with suffix replacement) with memoization.
pub fn compute_line_ipa_cached(name_katakana: &str, name_roman: Option<&str>) -> IpaResult {
/// Arcで返すことでキャッシュヒット時にTTSセグメントのディープクローンを避ける。
pub fn compute_line_ipa_cached(name_katakana: &str, name_roman: Option<&str>) -> Arc<IpaResult> {
let key = (name_katakana.to_string(), name_roman.map(str::to_string));
cached_lookup(&LINE_IPA_CACHE, &key, || {
compute_line_ipa(name_katakana, name_roman)
Expand Down
6 changes: 3 additions & 3 deletions stationapi/src/domain/repository/line_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ mod tests {
let keihin_line = Line::new(
11303,
1,
Some(company_jr_east.clone()),
Some(company_jr_east),
"京浜東北線".to_string(),
"ケイヒントウホクセン".to_string(),
"京浜東北線".to_string(),
Expand Down Expand Up @@ -176,8 +176,8 @@ mod tests {
lines_by_line_group_id.insert(1, vec![yamanote_line.clone()]);
lines_by_line_group_id.insert(2, vec![keihin_line.clone()]);

lines_by_name.insert("山手線".to_string(), vec![yamanote_line.clone()]);
lines_by_name.insert("京浜東北線".to_string(), vec![keihin_line.clone()]);
lines_by_name.insert("山手線".to_string(), vec![yamanote_line]);
lines_by_name.insert("京浜東北線".to_string(), vec![keihin_line]);

Self {
lines,
Expand Down
7 changes: 3 additions & 4 deletions stationapi/src/domain/repository/station_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ mod tests {
.filter(|station| {
transport_type
.as_ref()
.map_or(true, |tt| station.transport_type == *tt)
.is_none_or(|tt| station.transport_type == *tt)
})
.map(|station| {
let mut s = station.clone();
Expand Down Expand Up @@ -261,7 +261,7 @@ mod tests {
station.station_name.contains(&station_name)
&& transport_type
.as_ref()
.map_or(true, |tt| station.transport_type == *tt)
.is_none_or(|tt| station.transport_type == *tt)
})
.cloned()
.collect();
Expand Down Expand Up @@ -317,8 +317,7 @@ mod tests {

if !via_line_ids.is_empty() {
let line_match = |s: &Station| via_line_ids.contains(&(s.line_cd as u32));
if !from_station.map_or(false, line_match) || !to_station.map_or(false, line_match)
{
if !from_station.is_some_and(line_match) || !to_station.is_some_and(line_match) {
return Ok(result);
}
}
Expand Down
83 changes: 82 additions & 1 deletion stationapi/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,55 @@ type StopTimeBatchRow = (
Option<i32>,
);

/// 検索性能に直結するインデックス。create_table.sql内のDOブロックは
/// 拡張が使えない環境向けに例外をNOTICEで握り潰すため、作成に失敗しても
/// 起動ログからは分からない(実際に稼働DBでtrigramインデックスだけが
/// 欠落する事例があった)。必要な拡張はcreate_schemaの冒頭で必須として
/// 作成しているので、ここに列挙したインデックスは明示的に作成し直し、
/// 欠落があればERRORログで可視化する。
const PERFORMANCE_INDEXES: &[(&str, &str)] = &[
(
"idx_performance_stations_point",
"CREATE INDEX IF NOT EXISTS idx_performance_stations_point ON public.stations USING gist ((point(lat, lon)))",
),
(
"idx_performance_stations_bus_point",
"CREATE INDEX IF NOT EXISTS idx_performance_stations_bus_point ON public.stations USING gist ((point(lat, lon))) WHERE e_status = 0 AND transport_type = 1",
),
(
"idx_performance_station_name_trgm",
"CREATE INDEX IF NOT EXISTS idx_performance_station_name_trgm ON public.stations USING gin (station_name gin_trgm_ops)",
),
(
"idx_performance_station_name_k_trgm",
"CREATE INDEX IF NOT EXISTS idx_performance_station_name_k_trgm ON public.stations USING gin (station_name_k gin_trgm_ops)",
),
(
"idx_performance_station_name_rn_trgm",
"CREATE INDEX IF NOT EXISTS idx_performance_station_name_rn_trgm ON public.stations USING gin (station_name_rn gin_trgm_ops)",
),
(
"idx_performance_station_name_zh_trgm",
"CREATE INDEX IF NOT EXISTS idx_performance_station_name_zh_trgm ON public.stations USING gin (station_name_zh gin_trgm_ops)",
),
(
"idx_performance_station_name_ko_trgm",
"CREATE INDEX IF NOT EXISTS idx_performance_station_name_ko_trgm ON public.stations USING gin (station_name_ko gin_trgm_ops)",
),
(
"idx_gtfs_stops_point",
"CREATE INDEX IF NOT EXISTS idx_gtfs_stops_point ON public.gtfs_stops USING gist ((point(stop_lat, stop_lon)))",
),
(
"idx_gtfs_stops_name_trgm",
"CREATE INDEX IF NOT EXISTS idx_gtfs_stops_name_trgm ON public.gtfs_stops USING gin (stop_name gin_trgm_ops)",
),
(
"idx_gtfs_stops_name_k_trgm",
"CREATE INDEX IF NOT EXISTS idx_gtfs_stops_name_k_trgm ON public.gtfs_stops USING gin (stop_name_k gin_trgm_ops)",
),
];

/// Create required extensions and tables before running data imports.
/// Must be called before `import_csv` and `import_gtfs` can run in parallel.
pub async fn create_schema() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -61,7 +110,39 @@ pub async fn create_schema() -> Result<(), Box<dyn std::error::Error>> {
let create_sql: String = String::from_utf8_lossy(&create_sql_content).parse()?;
sqlx::raw_sql(&create_sql).execute(&mut conn).await?;

info!("Schema creation completed.");
for (name, ddl) in PERFORMANCE_INDEXES {
if let Err(e) = sqlx::query(ddl).execute(&mut conn).await {
tracing::error!("Failed to create performance index {}: {}", name, e);
}
}

// 作成結果を検証し、欠落があれば起動ログに残す
let expected: Vec<String> = PERFORMANCE_INDEXES
.iter()
.map(|(name, _)| name.to_string())
.collect();
let existing: Vec<String> = sqlx::query_scalar::<_, String>(
"SELECT indexname FROM pg_indexes WHERE indexname = ANY($1)",
)
.bind(&expected)
.fetch_all(&mut conn)
.await?;
let missing: Vec<&str> = PERFORMANCE_INDEXES
.iter()
.map(|(name, _)| *name)
.filter(|name| !existing.iter().any(|e| e == name))
.collect();
if missing.is_empty() {
info!(
"Schema creation completed. All {} performance indexes are present.",
expected.len()
);
} else {
tracing::error!(
"Schema creation completed, but performance indexes are missing: {:?}",
missing
);
}

Ok(())
}
Expand Down
21 changes: 8 additions & 13 deletions stationapi/src/presentation/controller/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,11 +432,9 @@ impl StationApi for MyApi {
request: tonic::Request<GetTrainRouteRequest>,
) -> Result<tonic::Response<TrainRouteResponse>, tonic::Status> {
let req = request.get_ref();
let from_id = req.from_station_group_id;
let to_id = req.to_station_group_id;
let line_group_id = req
.line_group_id
.ok_or_else(|| tonic::Status::invalid_argument("line_group_id is required"))?;
let from_id = req.from_station_id;
let to_id = req.to_station_id;
let line_group_id = req.line_group_id;

match self
.query_use_case
Expand Down Expand Up @@ -591,14 +589,11 @@ mod tests {
}

fn get_captured_coordinates_transport_type(&self) -> Option<TransportTypeFilter> {
self.captured_coordinates_transport_type
.lock()
.unwrap()
.clone()
*self.captured_coordinates_transport_type.lock().unwrap()
}

fn get_captured_name_transport_type(&self) -> Option<TransportTypeFilter> {
self.captured_name_transport_type.lock().unwrap().clone()
*self.captured_name_transport_type.lock().unwrap()
}
}

Expand Down Expand Up @@ -907,9 +902,9 @@ mod tests {

async fn get_train_route(
&self,
_from_station_group_id: u32,
_to_station_group_id: u32,
_line_group_id: u32,
_from_station_id: u32,
_to_station_id: u32,
_line_group_id: Option<u32>,
) -> Result<Vec<crate::proto::TrainRouteSegment>, UseCaseError> {
Ok(vec![])
}
Expand Down
6 changes: 3 additions & 3 deletions stationapi/src/use_case/dto/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use crate::{
impl From<Line> for GrpcLine {
fn from(line: Line) -> Self {
let ipa = compute_line_ipa_cached(&line.line_name_k, line.line_name_r.as_deref());
let name_ipa = ipa.name_ipa;
let name_roman_ipa = ipa.name_roman_ipa;
let name_tts_segments = to_proto_tts_segments(ipa.tts_segments);
let name_ipa = ipa.name_ipa.clone();
let name_roman_ipa = ipa.name_roman_ipa.clone();
let name_tts_segments = to_proto_tts_segments(&ipa.tts_segments);
// バス路線の場合は line_type を OtherLineType (0) に強制
// (鉄道用の line_type が誤って設定されている可能性があるため)
let line_type = if line.transport_type == TransportType::Bus {
Expand Down
29 changes: 23 additions & 6 deletions stationapi/src/use_case/dto/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const SUBWAY_MAX_SPEED: f64 = 22.222_222_222_22; // 80 km/h
const NORMAL_MAX_SPEED: f64 = 25.0; // 90 km/h
const TRAM_MAX_SPEED: f64 = 11.111_111_111_11; // 40 km/h

const LIMITED_EXPRESS_KIND_MAX_SPEED: f64 = 36.111_111_111_1; // 130 km/h
const LIMITED_EXPRESS_KIND_FLOOR_SPEED: f64 = 36.111_111_111_1; // 130 km/h

pub fn resolve_speed_profile(
line_type: Option<i32>,
Expand All @@ -40,9 +40,12 @@ pub fn resolve_speed_profile(
_ => (NORMAL_MAX_SPEED, 0.83, 0.69),
};

// 種別による 130km/h は在来線の速達種別を底上げするための下限値。
// 新幹線(のぞみ等は kind=LimitedExpress)では路線種別の上限の方が速いため、
// 種別値で引き下げない。
let max_speed = match kind.and_then(|v| TrainTypeKind::try_from(v).ok()) {
Some(TrainTypeKind::LimitedExpress) | Some(TrainTypeKind::HighSpeedRapid) => {
LIMITED_EXPRESS_KIND_MAX_SPEED
line_max_speed.max(LIMITED_EXPRESS_KIND_FLOOR_SPEED)
}
_ => line_max_speed,
};
Expand Down Expand Up @@ -90,25 +93,39 @@ mod tests {
}

#[test]
fn limited_express_kind_caps_speed_over_line_type() {
fn limited_express_kind_raises_speed_over_line_type() {
let p = resolve_speed_profile(
Some(LineType::Normal as i32),
false,
Some(TrainTypeKind::LimitedExpress as i32),
);
assert_eq!(p.max_speed, LIMITED_EXPRESS_KIND_MAX_SPEED);
assert_eq!(p.max_speed, LIMITED_EXPRESS_KIND_FLOOR_SPEED);
assert_eq!(p.max_acceleration, 0.83);
assert_eq!(p.max_deceleration, 0.69);
}

#[test]
fn high_speed_rapid_kind_caps_speed() {
fn limited_express_kind_does_not_slow_down_bullet_train() {
// のぞみ等は kind=LimitedExpress で登録されているが、
// 新幹線の路線上限(320km/h)を 130km/h に引き下げてはいけない。
let p = resolve_speed_profile(
Some(LineType::BulletTrain as i32),
false,
Some(TrainTypeKind::LimitedExpress as i32),
);
assert_eq!(p.max_speed, BULLET_TRAIN_MAX_SPEED);
assert_eq!(p.max_acceleration, 0.72);
assert_eq!(p.max_deceleration, 0.56);
}

#[test]
fn high_speed_rapid_kind_raises_speed() {
let p = resolve_speed_profile(
Some(LineType::Normal as i32),
false,
Some(TrainTypeKind::HighSpeedRapid as i32),
);
assert_eq!(p.max_speed, LIMITED_EXPRESS_KIND_MAX_SPEED);
assert_eq!(p.max_speed, LIMITED_EXPRESS_KIND_FLOOR_SPEED);
}

#[test]
Expand Down
6 changes: 3 additions & 3 deletions stationapi/src/use_case/dto/station.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ impl From<TransportType> for i32 {
impl From<Station> for GrpcStation {
fn from(station: Station) -> Self {
let ipa = compute_ipa_cached(&station.station_name_k, station.station_name_r.as_deref());
let name_ipa = ipa.name_ipa;
let name_roman_ipa = ipa.name_roman_ipa;
let name_tts_segments = to_proto_tts_segments(ipa.tts_segments);
let name_ipa = ipa.name_ipa.clone();
let name_roman_ipa = ipa.name_roman_ipa.clone();
let name_tts_segments = to_proto_tts_segments(&ipa.tts_segments);
Self {
id: station.station_cd as u32,
group_id: station.station_g_cd as u32,
Expand Down
Loading
Loading