1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::OnceLock;
4
5use crate::data::{BodyRow, EngineRow, EuModelRow, ModelRow};
6use crate::maps::{FstMap, FstSet, data_dir};
7use crate::types::{BodyType, DriveType, FuelType, Transmission};
8
9pub struct Catalog {
15 makes: FstSet,
16 make_models: FstMap<ModelRow>,
17 eu_brand_models: Option<FstMap<EuModelRow>>,
18 eu_engines: Option<FstMap<EngineRow>>,
19 eu_body: Option<FstMap<BodyRow>>,
20 model_to_makes: OnceLock<HashMap<String, Vec<String>>>,
22}
23
24impl Catalog {
25 pub fn new() -> crate::Result<Self> {
27 let dir = data_dir();
28 #[cfg(feature = "embedded")]
29 crate::embedded::ensure_installed(&dir).ok();
30 Self::open(&dir)
31 }
32
33 pub fn open(dir: &Path) -> crate::Result<Self> {
36 let eu_brand_models = if dir.join("eu_brand_models.fst").exists() {
37 Some(FstMap::open(dir)?)
38 } else {
39 None
40 };
41 let eu_engines = if dir.join("eu_engines.fst").exists() {
42 Some(FstMap::open(dir)?)
43 } else {
44 None
45 };
46 let eu_body = if dir.join("eu_body.fst").exists() {
47 Some(FstMap::open(dir)?)
48 } else {
49 None
50 };
51 Ok(Catalog {
52 makes: FstSet::open(&dir.join("makes.fst"))?,
53 make_models: FstMap::open(dir)?,
54 eu_brand_models,
55 eu_engines,
56 eu_body,
57 model_to_makes: OnceLock::new(),
58 })
59 }
60
61 pub fn all_makes(&self) -> Vec<String> {
63 self.makes.keys()
64 }
65
66 pub fn has_make(&self, make: &str) -> bool {
68 self.makes.contains(&make.to_ascii_uppercase())
69 }
70
71 pub fn resolve_make(&self, raw: &str) -> Option<String> {
82 resolve_make_with(raw, |k| self.makes.contains(k))
83 }
84
85 pub fn make_count(&self) -> u64 {
87 self.makes.len()
88 }
89
90 pub fn models_for_make(&self, make: &str) -> Vec<String> {
95 let key = crate::decoder::normalize_make(make);
96 let mut models: Vec<String> = self
97 .make_models
98 .get(&key)
99 .map(|rows| rows.into_iter().map(|r| r.name).collect())
100 .unwrap_or_default();
101 if let Some(eu) = &self.eu_brand_models {
102 if let Some(rows) = eu.get(&key) {
103 for r in rows {
104 models.push(r.name);
105 }
106 }
107 }
108 models.sort();
109 models.dedup();
110 models
111 }
112
113 pub fn make_for_model(&self, model: &str) -> Vec<String> {
120 self.model_to_makes
121 .get_or_init(|| self.build_model_index())
122 .get(&model.to_ascii_uppercase())
123 .cloned()
124 .unwrap_or_default()
125 }
126
127 fn build_model_index(&self) -> HashMap<String, Vec<String>> {
128 let mut idx: HashMap<String, Vec<String>> = HashMap::new();
129 for make in self.make_models.keys() {
130 if let Some(rows) = self.make_models.get(&make) {
131 for r in rows {
132 idx.entry(r.name.to_ascii_uppercase())
133 .or_default()
134 .push(make.clone());
135 }
136 }
137 }
138 if let Some(eu) = &self.eu_brand_models {
139 for make in eu.keys() {
140 if let Some(rows) = eu.get(&make) {
141 for r in rows {
142 idx.entry(r.name.to_ascii_uppercase())
143 .or_default()
144 .push(make.clone());
145 }
146 }
147 }
148 }
149 for makes in idx.values_mut() {
150 makes.sort();
151 makes.dedup();
152 }
153 idx
154 }
155
156 pub fn eu_models_for(&self, brand: &str) -> Vec<EuModelRow> {
160 let key = crate::decoder::normalize_make(brand);
161 self.eu_brand_models
162 .as_ref()
163 .and_then(|m| m.get(&key))
164 .unwrap_or_default()
165 }
166
167 pub fn engines_for_brand(&self, brand: &str) -> Vec<EngineRow> {
170 let key = crate::decoder::normalize_make(brand);
171 self.eu_engines
172 .as_ref()
173 .and_then(|m| m.get(&key))
174 .unwrap_or_default()
175 }
176
177 pub fn engines_for(&self, brand: &str, model: &str) -> Vec<EngineRow> {
180 let model_key = model.to_ascii_uppercase();
181 self.engines_for_brand(brand)
182 .into_iter()
183 .filter(|r| r.model == model_key)
184 .collect()
185 }
186
187 pub fn body_for(&self, make: &str, model: &str) -> Vec<BodyType> {
194 let key = crate::decoder::normalize_make(make);
195 let model_key = model.to_ascii_uppercase();
196 self.eu_body
197 .as_ref()
198 .and_then(|m| m.get(&key))
199 .unwrap_or_default()
200 .into_iter()
201 .find(|r| r.model == model_key)
202 .map(|r| r.bodies)
203 .unwrap_or_default()
204 }
205
206 pub fn body_types() -> &'static [BodyType] {
210 crate::types::BODY_TYPES
211 }
212
213 pub fn fuel_types() -> &'static [FuelType] {
215 &[
216 FuelType::Gasoline,
217 FuelType::Diesel,
218 FuelType::Electric,
219 FuelType::Hybrid,
220 FuelType::PluginHybrid,
221 FuelType::Ethanol,
222 FuelType::FlexFuel,
223 FuelType::Cng,
224 FuelType::Lng,
225 FuelType::Lpg,
226 FuelType::Hydrogen,
227 FuelType::FuelCell,
228 FuelType::Methanol,
229 FuelType::NaturalGas,
230 FuelType::Other,
231 ]
232 }
233
234 pub fn drive_types() -> &'static [DriveType] {
236 &[
237 DriveType::Fwd,
238 DriveType::Rwd,
239 DriveType::Awd,
240 DriveType::FourWheelDrive,
241 DriveType::Other,
242 ]
243 }
244
245 pub fn transmissions() -> &'static [Transmission] {
247 &[
248 Transmission::Manual,
249 Transmission::Automatic,
250 Transmission::DualClutch,
251 Transmission::Cvt,
252 Transmission::SingleSpeed,
253 Transmission::AutomatedManual,
254 Transmission::Other,
255 ]
256 }
257}
258
259pub(crate) const MAKE_ALIASES: &[(&str, &str)] = &[
271 ("KG", "KGM"),
272 ("KG MOBILITY", "KGM"),
273 ("LINK", "LYNK & CO"),
274 ("LYNK", "LYNK & CO"),
275 ("LINK & CO", "LYNK & CO"),
276 ("LEOPARD", "FANGCHENGBAO"),
277 ("BAO", "FANGCHENGBAO"),
278 ("MHERO", "M-HERO"),
279 ("MENGSHI", "M-HERO"),
280 ("ZUNJIE", "MAEXTRO"),
281 ("DFM", "DONGFENG"),
282 ("DS", "DS AUTOMOBILES"),
285 ("MERCEDES-AMG", "MERCEDES-BENZ"),
286 ("ROLLS ROYCE", "ROLLS-ROYCE"),
287];
288
289fn make_alias(upper: &str) -> Option<&'static str> {
291 MAKE_ALIASES
292 .iter()
293 .find(|(from, _)| *from == upper)
294 .map(|(_, to)| *to)
295}
296
297fn resolve_make_with(raw: &str, contains: impl Fn(&str) -> bool) -> Option<String> {
300 let up = raw.trim().to_ascii_uppercase();
301 if up.is_empty() {
302 return None;
303 }
304 let try_one = |s: &str| -> Option<String> {
305 if contains(s) {
306 return Some(s.to_string());
307 }
308 match make_alias(s) {
309 Some(canon) if contains(canon) => Some(canon.to_string()),
310 _ => None,
311 }
312 };
313 if let Some(hit) = try_one(&up) {
314 return Some(hit);
315 }
316 let toks: Vec<&str> = up.split_whitespace().collect();
318 for n in (1..toks.len()).rev() {
319 if let Some(hit) = try_one(&toks[..n].join(" ")) {
320 return Some(hit);
321 }
322 }
323 None
324}
325
326#[cfg(test)]
327mod resolve_tests {
328 use super::resolve_make_with;
329 use std::collections::HashSet;
330
331 fn fake(items: &[&str]) -> HashSet<String> {
332 items.iter().map(|s| s.to_string()).collect()
333 }
334
335 #[test]
336 fn resolve_make_exact_alias_and_glued() {
337 let makes = fake(&[
338 "KGM",
339 "LYNK & CO",
340 "FANGCHENGBAO",
341 "DONGFENG",
342 "M-HERO",
343 "LINKTOUR",
344 ]);
345 let c = |k: &str| makes.contains(k);
346 assert_eq!(resolve_make_with("dongfeng", c).as_deref(), Some("DONGFENG"));
348 assert_eq!(resolve_make_with("KG", c).as_deref(), Some("KGM"));
350 assert_eq!(resolve_make_with("KG Mobility", c).as_deref(), Some("KGM"));
351 assert_eq!(resolve_make_with("LINK", c).as_deref(), Some("LYNK & CO"));
352 assert_eq!(resolve_make_with("Leopard", c).as_deref(), Some("FANGCHENGBAO"));
353 assert_eq!(resolve_make_with("BAO", c).as_deref(), Some("FANGCHENGBAO"));
354 assert_eq!(
356 resolve_make_with("LEOPARD 5", c).as_deref(),
357 Some("FANGCHENGBAO")
358 );
359 assert_eq!(
360 resolve_make_with("LINKTOUR ALUMI-PLUS", c).as_deref(),
361 Some("LINKTOUR")
362 );
363 assert_eq!(resolve_make_with("M-HERO 917", c).as_deref(), Some("M-HERO"));
364 assert_eq!(resolve_make_with("WALDO", c), None);
366 assert_eq!(resolve_make_with("", c), None);
367 }
368}