1#![deny(missing_docs)]
41
42pub mod blob;
43pub mod morton;
44pub mod sampling;
45
46use std::collections::{HashMap, HashSet};
47use std::path::Path;
48
49use rusqlite::{Connection, OpenFlags};
50use umsh_core::RegionCode;
51
52pub use blob::GeometryError;
53pub use morton::MortonError;
54
55pub const FORMAT_VERSION: i64 = 1;
57
58#[derive(Debug)]
60pub enum RegionDbError {
61 Sqlite(rusqlite::Error),
63 UnsupportedFormat {
65 found: i64,
67 supported: i64,
69 },
70 NotARegionDatabase,
72 MissingSpatialIndex,
75 Geometry(GeometryError),
77 Position(MortonError),
79}
80
81impl core::fmt::Display for RegionDbError {
82 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83 match self {
84 Self::Sqlite(error) => write!(formatter, "region database error: {error}"),
85 Self::UnsupportedFormat { found, supported } => write!(
86 formatter,
87 "region database declares format version {found}; this build understands \
88 up to {supported}"
89 ),
90 Self::NotARegionDatabase => write!(formatter, "file is not a region database"),
91 Self::MissingSpatialIndex => write!(
92 formatter,
93 "database has no lookup cache and this SQLite build has no R-tree module"
94 ),
95 Self::Geometry(error) => write!(formatter, "region geometry: {error}"),
96 Self::Position(error) => write!(formatter, "lookup position: {error}"),
97 }
98 }
99}
100
101impl std::error::Error for RegionDbError {}
102
103impl From<rusqlite::Error> for RegionDbError {
104 fn from(error: rusqlite::Error) -> Self {
105 Self::Sqlite(error)
106 }
107}
108
109impl From<GeometryError> for RegionDbError {
110 fn from(error: GeometryError) -> Self {
111 Self::Geometry(error)
112 }
113}
114
115impl From<MortonError> for RegionDbError {
116 fn from(error: MortonError) -> Self {
117 Self::Position(error)
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
124pub enum Membership {
125 Core,
127 Expanded,
130}
131
132#[derive(Clone, Debug, PartialEq)]
134pub struct RegionMatch {
135 pub region_id: u32,
137 pub region_key: String,
139 pub namespace: String,
141 pub code: String,
143 pub radio_name: String,
145 pub wire_code: RegionCode,
147 pub layer: String,
149 pub priority: i64,
151 pub membership: Membership,
153 pub site: Option<(f64, f64)>,
155 pub default_rank: Option<i64>,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct RadioRegion {
162 pub name: String,
164 pub code: RegionCode,
166}
167
168#[derive(Clone, Debug, PartialEq)]
170pub struct RegionLookup {
171 pub latitude: f64,
173 pub longitude: f64,
175 pub matches: Vec<RegionMatch>,
178 pub radio_regions: Vec<RadioRegion>,
180 pub suggested_default_region: Option<RadioRegion>,
182 pub dataset_version: String,
184}
185
186#[derive(Clone, Debug)]
187struct RegionRow {
188 region_id: u32,
189 namespace: String,
190 code: String,
191 radio_name: String,
192 wire_code: RegionCode,
193 layer: String,
194 priority: i64,
195 default_rank: Option<i64>,
196 expansion_m: i64,
197 site: Option<(f64, f64)>,
198}
199
200impl RegionRow {
201 fn region_key(&self) -> String {
202 format!("{}:{}", self.namespace, self.code)
203 }
204}
205
206pub struct RegionDb {
208 connection: Connection,
209 regions: HashMap<u32, RegionRow>,
210 metadata: HashMap<String, String>,
211 format_version: i64,
212 has_lookup_ranges: bool,
213}
214
215impl RegionDb {
216 pub fn open(path: impl AsRef<Path>) -> Result<Self, RegionDbError> {
218 let connection = Connection::open_with_flags(
219 path,
220 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
221 )?;
222 Self::from_connection(connection)
223 }
224
225 fn from_connection(connection: Connection) -> Result<Self, RegionDbError> {
226 let format_version: i64 =
227 connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
228 if format_version < 1 {
229 return Err(RegionDbError::NotARegionDatabase);
230 }
231 if format_version > FORMAT_VERSION {
232 return Err(RegionDbError::UnsupportedFormat {
233 found: format_version,
234 supported: FORMAT_VERSION,
235 });
236 }
237
238 let metadata = connection
239 .prepare("SELECT key, value FROM metadata")?
240 .query_map([], |row| {
241 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
242 })?
243 .collect::<Result<HashMap<_, _>, _>>()?;
244
245 let regions = connection
246 .prepare(
247 "SELECT id, namespace, code, radio_name, wire_code, layer, priority, \
248 default_rank, expansion_m, site_lon, site_lat FROM regions",
249 )?
250 .query_map([], |row| {
251 let code: String = row.get(2)?;
252 let radio_name: Option<String> = row.get(3)?;
256 let longitude: Option<f64> = row.get(9)?;
257 let latitude: Option<f64> = row.get(10)?;
258 Ok(RegionRow {
259 region_id: row.get::<_, i64>(0)? as u32,
260 namespace: row.get(1)?,
261 radio_name: radio_name.unwrap_or_else(|| code.clone()),
262 code,
263 wire_code: RegionCode::from_u16(row.get::<_, i64>(4)? as u16),
264 layer: row.get(5)?,
265 priority: row.get(6)?,
266 default_rank: row.get(7)?,
267 expansion_m: row.get(8)?,
268 site: longitude.zip(latitude),
269 })
270 })?
271 .map(|row| row.map(|row| (row.region_id, row)))
272 .collect::<Result<HashMap<_, _>, _>>()?;
273
274 let has_lookup_ranges: bool = connection
275 .query_row("SELECT 1 FROM lookup_ranges LIMIT 1", [], |_| Ok(()))
276 .map(|_| true)
277 .or_else(|error| match error {
278 rusqlite::Error::QueryReturnedNoRows => Ok(false),
279 other => Err(other),
280 })?;
281
282 if !has_lookup_ranges {
289 match connection.query_row("SELECT 1 FROM effective_rtree LIMIT 1", [], |_| Ok(())) {
290 Ok(()) | Err(rusqlite::Error::QueryReturnedNoRows) => {}
291 Err(rusqlite::Error::SqliteFailure(_, Some(message)))
292 if message.contains("no such module") =>
293 {
294 return Err(RegionDbError::MissingSpatialIndex);
295 }
296 Err(other) => return Err(other.into()),
297 }
298 }
299
300 Ok(Self {
301 connection,
302 regions,
303 metadata,
304 format_version,
305 has_lookup_ranges,
306 })
307 }
308
309 pub fn format_version(&self) -> i64 {
311 self.format_version
312 }
313
314 pub fn dataset_version(&self) -> &str {
316 self.metadata
317 .get("dataset_version")
318 .map(String::as_str)
319 .unwrap_or("unknown")
320 }
321
322 pub fn metadata(&self) -> &HashMap<String, String> {
324 &self.metadata
325 }
326
327 pub fn region_count(&self) -> usize {
329 self.regions.len()
330 }
331
332 pub fn lookup_codes(
334 &self,
335 latitude: f64,
336 longitude: f64,
337 ) -> Result<RegionLookup, RegionDbError> {
338 self.lookup(latitude, longitude, false)
339 }
340
341 pub fn lookup_detailed(
343 &self,
344 latitude: f64,
345 longitude: f64,
346 ) -> Result<RegionLookup, RegionDbError> {
347 self.lookup(latitude, longitude, true)
348 }
349
350 fn lookup(
351 &self,
352 latitude: f64,
353 longitude: f64,
354 detailed: bool,
355 ) -> Result<RegionLookup, RegionDbError> {
356 self.quantize(latitude, longitude)?;
358 let memberships = self.memberships(latitude, longitude)?;
359 let matches = self.build_matches(memberships);
360
361 let radio_regions = radio_regions(&matches);
362 let suggested = suggested_default(&matches, latitude, longitude);
363 Ok(RegionLookup {
364 latitude,
365 longitude,
366 matches: if detailed { matches } else { Vec::new() },
367 radio_regions,
368 suggested_default_region: suggested,
369 dataset_version: self.dataset_version().to_owned(),
370 })
371 }
372
373 pub fn lookup_exhaustive(
380 &self,
381 latitude: f64,
382 longitude: f64,
383 ) -> Result<RegionLookup, RegionDbError> {
384 self.quantize(latitude, longitude)?;
385 let mut memberships: HashMap<u32, Membership> = HashMap::new();
386 for region_id in self.regions.keys().copied() {
387 if let Some(membership) = self.membership(region_id, latitude, longitude)? {
388 memberships.insert(region_id, membership);
389 }
390 }
391 let matches = self.build_matches(memberships);
392 let radio_regions = radio_regions(&matches);
393 let suggested = suggested_default(&matches, latitude, longitude);
394 Ok(RegionLookup {
395 latitude,
396 longitude,
397 matches,
398 radio_regions,
399 suggested_default_region: suggested,
400 dataset_version: self.dataset_version().to_owned(),
401 })
402 }
403
404 fn build_matches(&self, memberships: HashMap<u32, Membership>) -> Vec<RegionMatch> {
405 let mut matches = Vec::with_capacity(memberships.len());
406 for (region_id, membership) in memberships {
407 let Some(row) = self.regions.get(®ion_id) else {
408 continue;
409 };
410 matches.push(RegionMatch {
411 region_id: row.region_id,
412 region_key: row.region_key(),
413 namespace: row.namespace.clone(),
414 code: row.code.clone(),
415 radio_name: row.radio_name.clone(),
416 wire_code: row.wire_code,
417 layer: row.layer.clone(),
418 priority: row.priority,
419 membership,
420 site: row.site,
421 default_rank: row.default_rank,
422 });
423 }
424 matches.sort_by(|first, second| {
425 first
426 .priority
427 .cmp(&second.priority)
428 .then(first.membership.cmp(&second.membership))
429 .then_with(|| first.region_key.cmp(&second.region_key))
430 });
431 matches
432 }
433
434 fn quantize(&self, latitude: f64, longitude: f64) -> Result<(i32, i32), RegionDbError> {
435 Ok((
436 blob::to_e6(morton::normalize_longitude(longitude)?),
437 blob::to_e6(morton::check_latitude(latitude)?),
438 ))
439 }
440
441 fn core_hit(
443 &self,
444 region_id: u32,
445 latitude: f64,
446 longitude: f64,
447 ) -> Result<bool, RegionDbError> {
448 let (longitude_e6, latitude_e6) = self.quantize(latitude, longitude)?;
449 let mut statement = self.connection.prepare_cached(
450 "SELECT geometry, min_lon, min_lat, max_lon, max_lat FROM geometry_parts \
451 WHERE region_id = ?1 ORDER BY id",
452 )?;
453 let mut rows = statement.query([i64::from(region_id)])?;
454 while let Some(row) = rows.next()? {
455 let min_lon: f64 = row.get(1)?;
456 let min_lat: f64 = row.get(2)?;
457 let max_lon: f64 = row.get(3)?;
458 let max_lat: f64 = row.get(4)?;
459 if longitude_e6 < blob::to_e6(min_lon)
462 || longitude_e6 > blob::to_e6(max_lon)
463 || latitude_e6 < blob::to_e6(min_lat)
464 || latitude_e6 > blob::to_e6(max_lat)
465 {
466 continue;
467 }
468 let payload: Vec<u8> = row.get(0)?;
469 if blob::point_in_rings(&blob::decode(&payload)?, longitude_e6, latitude_e6) {
470 return Ok(true);
471 }
472 }
473 Ok(false)
474 }
475
476 fn membership(
478 &self,
479 region_id: u32,
480 latitude: f64,
481 longitude: f64,
482 ) -> Result<Option<Membership>, RegionDbError> {
483 let expansion = self
484 .regions
485 .get(®ion_id)
486 .map(|row| row.expansion_m)
487 .unwrap_or(0);
488 for (index, (sample_lat, sample_lon)) in
489 sampling::sample_positions(latitude, longitude, expansion)
490 .into_iter()
491 .enumerate()
492 {
493 if self.core_hit(region_id, sample_lat, sample_lon)? {
494 return Ok(Some(if index == 0 {
495 Membership::Core
496 } else {
497 Membership::Expanded
498 }));
499 }
500 }
501 Ok(None)
502 }
503
504 fn memberships(
506 &self,
507 latitude: f64,
508 longitude: f64,
509 ) -> Result<HashMap<u32, Membership>, RegionDbError> {
510 if !self.has_lookup_ranges {
511 return self.rtree_memberships(latitude, longitude);
512 }
513
514 let key = morton::key(latitude, longitude)?;
515 let mut statement = self.connection.prepare_cached(
516 "SELECT end_key, base_set_id, candidate_region_ids FROM lookup_ranges \
517 WHERE start_key <= ?1 ORDER BY start_key DESC LIMIT 1",
518 )?;
519 let row = statement
520 .query_row([i64::from(key)], |row| {
521 Ok((
522 row.get::<_, i64>(0)?,
523 row.get::<_, i64>(1)?,
524 row.get::<_, Option<Vec<u8>>>(2)?,
525 ))
526 })
527 .ok();
528
529 let mut memberships = HashMap::new();
530 let Some((end_key, base_set_id, candidates)) = row else {
531 return Ok(memberships);
532 };
533 if i64::from(key) > end_key {
534 return Ok(memberships);
535 }
536
537 let payload: Option<Vec<u8>> = self
538 .connection
539 .prepare_cached("SELECT region_ids FROM region_sets WHERE id = ?1")?
540 .query_row([base_set_id], |row| row.get(0))
541 .ok();
542 for region_id in payload
545 .as_deref()
546 .map(decode_region_ids)
547 .unwrap_or_default()
548 {
549 let membership = self
550 .membership(region_id, latitude, longitude)?
551 .unwrap_or(Membership::Expanded);
552 memberships.insert(region_id, membership);
553 }
554 if let Some(candidates) = candidates {
555 for region_id in decode_region_ids(&candidates) {
556 if let Some(membership) = self.membership(region_id, latitude, longitude)? {
557 memberships.insert(region_id, membership);
558 }
559 }
560 }
561 Ok(memberships)
562 }
563
564 fn rtree_memberships(
571 &self,
572 latitude: f64,
573 longitude: f64,
574 ) -> Result<HashMap<u32, Membership>, RegionDbError> {
575 let (longitude_e6, latitude_e6) = self.quantize(latitude, longitude)?;
576 let lon = blob::from_e6(longitude_e6);
577 let lat = blob::from_e6(latitude_e6);
578
579 let mut candidates: HashSet<u32> = HashSet::new();
580 let mut statement = self.connection.prepare_cached(
581 "SELECT DISTINCT p.region_id FROM effective_rtree r \
582 JOIN geometry_parts p ON p.id = r.part_id \
583 WHERE r.min_lon <= ?1 AND r.max_lon >= ?1 \
584 AND r.min_lat <= ?2 AND r.max_lat >= ?2",
585 )?;
586 for wrapped in [lon - 360.0, lon, lon + 360.0] {
587 let mut rows = statement.query((wrapped, lat))?;
588 while let Some(row) = rows.next()? {
589 candidates.insert(row.get::<_, i64>(0)? as u32);
590 }
591 }
592
593 let mut memberships = HashMap::new();
594 for region_id in candidates {
595 if let Some(membership) = self.membership(region_id, latitude, longitude)? {
596 memberships.insert(region_id, membership);
597 }
598 }
599 Ok(memberships)
600 }
601}
602
603fn decode_region_ids(data: &[u8]) -> Vec<u32> {
605 let mut identifiers = Vec::new();
606 let mut value: u64 = 0;
607 let mut shift = 0u32;
608 let mut current: u64 = 0;
609 for byte in data {
610 value |= u64::from(byte & 0x7F) << shift;
611 if byte & 0x80 != 0 {
612 shift += 7;
613 continue;
614 }
615 current += value;
616 identifiers.push(current as u32);
617 value = 0;
618 shift = 0;
619 }
620 identifiers
621}
622
623fn radio_regions(matches: &[RegionMatch]) -> Vec<RadioRegion> {
629 let mut seen = HashSet::new();
630 let mut out = Vec::new();
631 for entry in matches {
632 if seen.insert(entry.wire_code) {
633 out.push(RadioRegion {
634 name: entry.radio_name.clone(),
635 code: entry.wire_code,
636 });
637 }
638 }
639 out
640}
641
642fn suggested_default(
650 matches: &[RegionMatch],
651 latitude: f64,
652 longitude: f64,
653) -> Option<RadioRegion> {
654 matches
655 .iter()
656 .filter(|entry| entry.default_rank.is_some())
657 .min_by(|first, second| {
658 first
659 .default_rank
660 .cmp(&second.default_rank)
661 .then(first.membership.cmp(&second.membership))
662 .then_with(|| {
663 site_distance(first, latitude, longitude)
664 .total_cmp(&site_distance(second, latitude, longitude))
665 })
666 .then_with(|| first.region_key.cmp(&second.region_key))
667 })
668 .map(|entry| RadioRegion {
669 name: entry.radio_name.clone(),
670 code: entry.wire_code,
671 })
672}
673
674fn site_distance(entry: &RegionMatch, latitude: f64, longitude: f64) -> f64 {
682 let Some((site_longitude, site_latitude)) = entry.site else {
683 return f64::INFINITY;
684 };
685 let (lat1, lat2) = (latitude.to_radians(), site_latitude.to_radians());
686 let delta_lat = lat2 - lat1;
687 let delta_lon = (site_longitude - longitude).to_radians();
688 let haversine =
689 (delta_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
690 2.0 * haversine.sqrt().asin() * sampling::EARTH_RADIUS_M
691}