1use std::sync::{Arc, Mutex};
16
17use umsh_core::RegionCode;
18use umsh_node::location::NodeLocation;
19use umsh_regiondb::{Membership, RegionDb, RegionDbError, RegionLookup, RegionMatch, sampling};
20
21const MAX_PROPOSAL_UNCERTAINTY_M: f64 = 25_000.0;
29
30const EQUATOR_METERS_PER_DEGREE: f64 = 111_320.0;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Error)]
36pub enum MobileRegionError {
37 DatabaseUnavailable,
39 NotARegionDatabase,
41 UnsupportedFormat,
43 MissingSpatialIndex,
45 Corrupt,
47 InvalidPosition,
50 PositionTooCoarse,
53 InvalidRegionCode,
56}
57
58impl MobileRegionError {
59 pub const fn summary_key(self) -> &'static str {
62 match self {
63 Self::DatabaseUnavailable => "mobile.error.regiondb.unavailable",
64 Self::NotARegionDatabase => "mobile.error.regiondb.not_a_region_database",
65 Self::UnsupportedFormat => "mobile.error.regiondb.unsupported_format",
66 Self::MissingSpatialIndex => "mobile.error.regiondb.missing_spatial_index",
67 Self::Corrupt => "mobile.error.regiondb.corrupt",
68 Self::InvalidPosition => "mobile.error.regiondb.invalid_position",
69 Self::PositionTooCoarse => "mobile.error.regiondb.position_too_coarse",
70 Self::InvalidRegionCode => "mobile.error.region_code.invalid",
71 }
72 }
73
74 pub const fn diagnostic_code(self) -> &'static str {
76 match self {
77 Self::DatabaseUnavailable => "REGIONDB_UNAVAILABLE",
78 Self::NotARegionDatabase => "REGIONDB_NOT_A_REGION_DATABASE",
79 Self::UnsupportedFormat => "REGIONDB_UNSUPPORTED_FORMAT",
80 Self::MissingSpatialIndex => "REGIONDB_MISSING_SPATIAL_INDEX",
81 Self::Corrupt => "REGIONDB_CORRUPT",
82 Self::InvalidPosition => "REGION_POSITION_INVALID",
83 Self::PositionTooCoarse => "REGION_POSITION_TOO_COARSE",
84 Self::InvalidRegionCode => "REGION_CODE_INVALID",
85 }
86 }
87}
88
89impl core::fmt::Display for MobileRegionError {
90 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91 formatter.write_str(self.diagnostic_code())
92 }
93}
94
95impl std::error::Error for MobileRegionError {}
96
97impl From<RegionDbError> for MobileRegionError {
98 fn from(error: RegionDbError) -> Self {
99 match error {
103 RegionDbError::Sqlite(_) => Self::DatabaseUnavailable,
104 RegionDbError::NotARegionDatabase => Self::NotARegionDatabase,
105 RegionDbError::UnsupportedFormat { .. } => Self::UnsupportedFormat,
106 RegionDbError::MissingSpatialIndex => Self::MissingSpatialIndex,
107 RegionDbError::Geometry(_) => Self::Corrupt,
108 RegionDbError::Position(_) => Self::InvalidPosition,
109 }
110 }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
116pub enum MobileRegionMembership {
117 Core,
119 Expanded,
121}
122
123impl From<Membership> for MobileRegionMembership {
124 fn from(membership: Membership) -> Self {
125 match membership {
126 Membership::Core => Self::Core,
127 Membership::Expanded => Self::Expanded,
128 }
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, uniffi::Record)]
140pub struct MobileRegionPositionRecord {
141 pub latitude: f64,
143 pub longitude: f64,
145 pub location_bytes: Option<Vec<u8>>,
148 pub accuracy_m: Option<f64>,
150}
151
152#[derive(Clone, Debug, PartialEq, uniffi::Record)]
155pub struct MobileRegionMatchRecord {
156 pub region_key: String,
158 pub namespace: String,
160 pub radio_name: String,
162 pub wire_code: Vec<u8>,
164 pub layer: String,
166 pub membership: MobileRegionMembership,
168 pub site_latitude: Option<f64>,
170 pub site_longitude: Option<f64>,
172}
173
174#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
177pub struct MobileRegionRecord {
178 pub name: String,
180 pub code: Vec<u8>,
182}
183
184#[derive(Clone, Debug, PartialEq, uniffi::Record)]
186pub struct MobileRegionLookupRecord {
187 pub latitude: f64,
189 pub longitude: f64,
191 pub matches: Vec<MobileRegionMatchRecord>,
193 pub radio_regions: Vec<MobileRegionRecord>,
195 pub suggested_default_region: Option<MobileRegionRecord>,
197 pub dataset_version: String,
199}
200
201#[derive(Clone, Debug, PartialEq, uniffi::Record)]
205pub struct MobileRegionOutcomeRecord {
206 pub regions: Vec<String>,
208 pub default_region: Option<Vec<u8>>,
210 pub changes_anything: bool,
214}
215
216#[derive(Clone, Debug, PartialEq, uniffi::Record)]
219pub struct MobileRegionProposalRecord {
220 pub lookup: MobileRegionLookupRecord,
225 pub replace: MobileRegionOutcomeRecord,
228 pub add_missing: MobileRegionOutcomeRecord,
232 pub already_present: Vec<String>,
234 pub not_suggested: Vec<String>,
237 pub uncertain_regions: Vec<String>,
241 pub cell_meters: Option<f64>,
244}
245
246#[derive(uniffi::Object)]
253pub struct MobileRegionDatabase {
254 inner: Mutex<RegionDb>,
255}
256
257#[uniffi::export]
258impl MobileRegionDatabase {
259 #[uniffi::constructor]
261 pub fn open(path: String) -> Result<Arc<Self>, MobileRegionError> {
262 if !std::path::Path::new(&path).is_absolute() {
263 return Err(MobileRegionError::DatabaseUnavailable);
264 }
265 let db = RegionDb::open(&path)?;
266 Ok(Arc::new(Self {
267 inner: Mutex::new(db),
268 }))
269 }
270
271 pub fn dataset_version(&self) -> String {
273 self.locked(|db| db.dataset_version().to_owned())
274 }
275
276 pub fn format_version(&self) -> u32 {
278 self.locked(|db| db.format_version() as u32)
279 }
280
281 pub fn region_count(&self) -> u32 {
283 self.locked(|db| db.region_count() as u32)
284 }
285
286 pub fn lookup(
288 &self,
289 latitude: f64,
290 longitude: f64,
291 ) -> Result<MobileRegionLookupRecord, MobileRegionError> {
292 check_coordinates(latitude, longitude)?;
293 let lookup = self.locked(|db| db.lookup_detailed(latitude, longitude))?;
294 Ok(lookup_record(&lookup))
295 }
296
297 pub fn propose(
308 &self,
309 position: MobileRegionPositionRecord,
310 current_regions: Vec<String>,
311 current_default_region: Option<Vec<u8>>,
312 ) -> Result<MobileRegionProposalRecord, MobileRegionError> {
313 check_coordinates(position.latitude, position.longitude)?;
314 let plan = sample_points(&position)?;
315
316 let current_codes = current_regions
317 .iter()
318 .map(|text| {
319 text.parse::<RegionCode>()
320 .map_err(|_| MobileRegionError::InvalidRegionCode)
321 })
322 .collect::<Result<Vec<_>, _>>()?;
323 let current_default = current_default_region
324 .as_deref()
325 .map(|bytes| {
326 let bytes: [u8; 2] = bytes
327 .try_into()
328 .map_err(|_| MobileRegionError::InvalidRegionCode)?;
329 Ok::<_, MobileRegionError>(RegionCode::from_bytes(bytes))
330 })
331 .transpose()?;
332
333 let lookups = self.locked(|db| {
334 plan.points
335 .iter()
336 .map(|&(latitude, longitude)| db.lookup_detailed(latitude, longitude))
337 .collect::<Result<Vec<_>, _>>()
338 })?;
339
340 let mut merged: Vec<RegionMatch> = Vec::new();
343 for lookup in &lookups {
344 for candidate in &lookup.matches {
345 match merged
346 .iter_mut()
347 .find(|held| held.region_key == candidate.region_key)
348 {
349 Some(held) => held.membership = held.membership.min(candidate.membership),
350 None => merged.push(candidate.clone()),
351 }
352 }
353 }
354 merged.sort_by(|a, b| {
355 (a.priority, a.membership, &a.region_key).cmp(&(
356 b.priority,
357 b.membership,
358 &b.region_key,
359 ))
360 });
361
362 let mut suggested: Vec<MobileRegionRecord> = Vec::new();
365 for candidate in &merged {
366 if !suggested
367 .iter()
368 .any(|held| held.code == candidate.wire_code.to_bytes())
369 {
370 suggested.push(MobileRegionRecord {
371 name: candidate.radio_name.clone(),
372 code: candidate.wire_code.to_bytes().to_vec(),
373 });
374 }
375 }
376
377 let uncertain_regions = suggested
380 .iter()
381 .filter(|region| {
382 !lookups.iter().all(|lookup| {
383 lookup
384 .radio_regions
385 .iter()
386 .any(|held| held.code.to_bytes().as_slice() == region.code.as_slice())
387 })
388 })
389 .map(|region| region.name.clone())
390 .collect();
391
392 let suggested_default =
393 lookups[0]
394 .suggested_default_region
395 .as_ref()
396 .map(|region| MobileRegionRecord {
397 name: region.name.clone(),
398 code: region.code.to_bytes().to_vec(),
399 });
400
401 let already_present: Vec<String> = suggested
402 .iter()
403 .filter(|region| {
404 current_codes
405 .iter()
406 .any(|held| held.to_bytes().as_slice() == region.code.as_slice())
407 })
408 .map(|region| region.name.clone())
409 .collect();
410 let not_suggested: Vec<String> = current_regions
411 .iter()
412 .zip(¤t_codes)
413 .filter(|(_, code)| {
414 !suggested
415 .iter()
416 .any(|region| region.code.as_slice() == code.to_bytes().as_slice())
417 })
418 .map(|(text, _)| text.clone())
419 .collect();
420
421 let suggested_default_code = suggested_default
422 .as_ref()
423 .map(|region| RegionCode::from_bytes([region.code[0], region.code[1]]));
424 let suggested_codes: Vec<[u8; 2]> = suggested
425 .iter()
426 .map(|region| [region.code[0], region.code[1]])
427 .collect();
428 let current_code_bytes: Vec<[u8; 2]> =
429 current_codes.iter().map(|code| code.to_bytes()).collect();
430 let replace = MobileRegionOutcomeRecord {
431 regions: suggested.iter().map(|region| region.name.clone()).collect(),
432 default_region: suggested_default.as_ref().map(|region| region.code.clone()),
433 changes_anything: suggested_codes != current_code_bytes
434 || suggested_default_code != current_default,
435 };
436
437 let missing: Vec<&MobileRegionRecord> = suggested
438 .iter()
439 .filter(|region| {
440 !current_codes
441 .iter()
442 .any(|held| held.to_bytes().as_slice() == region.code.as_slice())
443 })
444 .collect();
445 let adopts_default = current_default.is_none() && suggested_default_code.is_some();
446 let add_missing = MobileRegionOutcomeRecord {
447 regions: current_regions
448 .iter()
449 .cloned()
450 .chain(missing.iter().map(|region| region.name.clone()))
451 .collect(),
452 default_region: if adopts_default {
453 suggested_default.as_ref().map(|region| region.code.clone())
454 } else {
455 current_default_region.clone()
456 },
457 changes_anything: !missing.is_empty() || adopts_default,
458 };
459
460 Ok(MobileRegionProposalRecord {
461 lookup: MobileRegionLookupRecord {
462 latitude: position.latitude,
463 longitude: position.longitude,
464 matches: merged.iter().map(match_record).collect(),
465 radio_regions: suggested,
466 suggested_default_region: suggested_default,
467 dataset_version: lookups[0].dataset_version.clone(),
468 },
469 replace,
470 add_missing,
471 already_present,
472 not_suggested,
473 uncertain_regions,
474 cell_meters: plan.width_m,
475 })
476 }
477}
478
479impl MobileRegionDatabase {
480 fn locked<T>(&self, operation: impl FnOnce(&RegionDb) -> T) -> T {
481 let guard = match self.inner.lock() {
484 Ok(guard) => guard,
485 Err(poisoned) => poisoned.into_inner(),
486 };
487 operation(&guard)
488 }
489}
490
491fn check_coordinates(latitude: f64, longitude: f64) -> Result<(), MobileRegionError> {
492 if !latitude.is_finite()
493 || latitude.abs() > 90.0
494 || !longitude.is_finite()
495 || longitude.abs() > 180.0
496 {
497 return Err(MobileRegionError::InvalidPosition);
498 }
499 Ok(())
500}
501
502struct SamplePlan {
504 points: Vec<(f64, f64)>,
505 width_m: Option<f64>,
508}
509
510fn sample_points(position: &MobileRegionPositionRecord) -> Result<SamplePlan, MobileRegionError> {
511 let center = (position.latitude, position.longitude);
512
513 if let Some(bytes) = position.location_bytes.as_deref() {
514 if bytes.is_empty() || bytes.len() > 7 {
515 return Err(MobileRegionError::InvalidPosition);
516 }
517 let cell_meters = 360.0 * EQUATOR_METERS_PER_DEGREE / 16f64.powi(bytes.len() as i32);
519 if cell_meters > MAX_PROPOSAL_UNCERTAINTY_M {
520 return Err(MobileRegionError::PositionTooCoarse);
521 }
522 let location = NodeLocation::from_bytes(bytes);
523 let ((lat_lo, lon_lo), (lat_hi, lon_hi)) = location.bounds();
524 let (lat_lo, lon_lo, lat_hi, lon_hi) =
525 (lat_lo as f64, lon_lo as f64, lat_hi as f64, lon_hi as f64);
526 return Ok(SamplePlan {
527 points: vec![
528 center,
529 (lat_lo, lon_lo),
530 (lat_lo, lon_hi),
531 (lat_hi, lon_lo),
532 (lat_hi, lon_hi),
533 ],
534 width_m: Some(cell_meters),
535 });
536 }
537
538 if let Some(accuracy_m) = position.accuracy_m {
539 if !accuracy_m.is_finite() || accuracy_m < 0.0 {
540 return Err(MobileRegionError::InvalidPosition);
541 }
542 if accuracy_m > MAX_PROPOSAL_UNCERTAINTY_M {
543 return Err(MobileRegionError::PositionTooCoarse);
544 }
545 if accuracy_m > 0.0 {
546 let mut samples = vec![center];
551 for bearing in [0.0, 90.0, 180.0, 270.0] {
552 samples.push(sampling::destination(
553 position.latitude,
554 position.longitude,
555 bearing,
556 accuracy_m,
557 ));
558 }
559 return Ok(SamplePlan {
560 points: samples,
561 width_m: Some(accuracy_m * 2.0),
562 });
563 }
564 }
565
566 Ok(SamplePlan {
567 points: vec![center],
568 width_m: None,
569 })
570}
571
572fn lookup_record(lookup: &RegionLookup) -> MobileRegionLookupRecord {
573 MobileRegionLookupRecord {
574 latitude: lookup.latitude,
575 longitude: lookup.longitude,
576 matches: lookup.matches.iter().map(match_record).collect(),
577 radio_regions: lookup
578 .radio_regions
579 .iter()
580 .map(|region| MobileRegionRecord {
581 name: region.name.clone(),
582 code: region.code.to_bytes().to_vec(),
583 })
584 .collect(),
585 suggested_default_region: lookup.suggested_default_region.as_ref().map(|region| {
586 MobileRegionRecord {
587 name: region.name.clone(),
588 code: region.code.to_bytes().to_vec(),
589 }
590 }),
591 dataset_version: lookup.dataset_version.clone(),
592 }
593}
594
595fn match_record(entry: &RegionMatch) -> MobileRegionMatchRecord {
596 MobileRegionMatchRecord {
597 region_key: entry.region_key.clone(),
598 namespace: entry.namespace.clone(),
599 radio_name: entry.radio_name.clone(),
600 wire_code: entry.wire_code.to_bytes().to_vec(),
601 layer: entry.layer.clone(),
602 membership: entry.membership.into(),
603 site_latitude: entry.site.map(|(_, latitude)| latitude),
604 site_longitude: entry.site.map(|(longitude, _)| longitude),
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 fn fixture() -> Arc<MobileRegionDatabase> {
613 let path = concat!(
614 env!("CARGO_MANIFEST_DIR"),
615 "/../../regions/tests/fixture/fixture.regiondb"
616 );
617 MobileRegionDatabase::open(path.to_owned()).expect("fixture opens")
618 }
619
620 fn exact(latitude: f64, longitude: f64) -> MobileRegionPositionRecord {
621 MobileRegionPositionRecord {
622 latitude,
623 longitude,
624 location_bytes: None,
625 accuracy_m: None,
626 }
627 }
628
629 const SAN_CARLOS: (f64, f64) = (37.5119, -122.2495);
631 const SFO: (f64, f64) = (37.6189, -122.3750);
633
634 #[test]
635 fn opens_and_describes_the_fixture() {
636 let db = fixture();
637 assert_eq!(db.dataset_version(), "fixture-1");
638 assert_eq!(db.format_version(), 1);
639 assert_eq!(db.region_count(), 12);
640 }
641
642 #[test]
643 fn relative_path_is_refused() {
644 assert_eq!(
645 MobileRegionDatabase::open("fixture.regiondb".to_owned()).err(),
646 Some(MobileRegionError::DatabaseUnavailable)
647 );
648 }
649
650 #[test]
651 fn lookup_matches_the_reference_answers() {
652 let lookup = fixture().lookup(SAN_CARLOS.0, SAN_CARLOS.1).unwrap();
653 let keys: Vec<&str> = lookup
654 .matches
655 .iter()
656 .map(|entry| entry.region_key.as_str())
657 .collect();
658 assert!(keys.contains(&"iata-location:SQL"));
659 assert!(keys.contains(&"iata-airport:SFO"));
660 assert!(!keys.contains(&"iata-airport:SQL"));
661 assert_eq!(
662 lookup
663 .suggested_default_region
664 .as_ref()
665 .map(|r| r.name.as_str()),
666 Some("XSF")
667 );
668 }
669
670 #[test]
671 fn exact_position_proposes_the_lookup_verbatim() {
672 let db = fixture();
673 let proposal = db.propose(exact(SFO.0, SFO.1), Vec::new(), None).unwrap();
674 let direct = db.lookup(SFO.0, SFO.1).unwrap();
675 assert_eq!(proposal.lookup.radio_regions, direct.radio_regions);
676 assert!(proposal.uncertain_regions.is_empty());
677 assert_eq!(proposal.cell_meters, None);
678 assert_eq!(
679 proposal.replace.regions,
680 direct
681 .radio_regions
682 .iter()
683 .map(|region| region.name.clone())
684 .collect::<Vec<_>>()
685 );
686 assert!(proposal.replace.changes_anything);
687 assert_eq!(proposal.replace, proposal.add_missing);
688 }
689
690 #[test]
691 fn comparison_is_by_derived_wire_code() {
692 let proposal = fixture()
695 .propose(
696 exact(SFO.0, SFO.1),
697 vec!["sfo".to_owned(), "0x98FE".to_owned()],
698 None,
699 )
700 .unwrap();
701 assert_eq!(proposal.already_present, vec!["SFO", "XSF"]);
702 assert!(proposal.not_suggested.is_empty());
703 assert_eq!(proposal.add_missing.regions[..2], ["sfo", "0x98FE"]);
706 assert!(!proposal.add_missing.regions.contains(&"SFO".to_owned()));
707 assert!(proposal.add_missing.regions.contains(&"US".to_owned()));
708 assert!(proposal.add_missing.changes_anything);
709 }
710
711 #[test]
712 fn foreign_regions_are_kept_by_add_and_dropped_by_replace() {
713 let proposal = fixture()
714 .propose(exact(SFO.0, SFO.1), vec!["LAX".to_owned()], None)
715 .unwrap();
716 assert_eq!(proposal.not_suggested, vec!["LAX"]);
717 assert!(proposal.add_missing.regions.contains(&"LAX".to_owned()));
718 assert!(!proposal.replace.regions.contains(&"LAX".to_owned()));
719 }
720
721 #[test]
722 fn default_tag_is_adopted_only_when_unset() {
723 let db = fixture();
724 let xsf = vec![0x98, 0xFE];
725 let sfo = vec![0x77, 0xBF];
726
727 let unset = db.propose(exact(SFO.0, SFO.1), Vec::new(), None).unwrap();
728 assert_eq!(unset.add_missing.default_region, Some(xsf.clone()));
729 assert_eq!(unset.replace.default_region, Some(xsf.clone()));
730
731 let held = db
732 .propose(
733 exact(SFO.0, SFO.1),
734 vec!["SFO".to_owned()],
735 Some(sfo.clone()),
736 )
737 .unwrap();
738 assert_eq!(held.add_missing.default_region, Some(sfo));
739 assert_eq!(held.replace.default_region, Some(xsf));
740 }
741
742 #[test]
743 fn matching_configuration_changes_nothing() {
744 let db = fixture();
745 let direct = db.lookup(SFO.0, SFO.1).unwrap();
746 let current: Vec<String> = direct
747 .radio_regions
748 .iter()
749 .map(|region| region.name.clone())
750 .collect();
751 let default = direct
752 .suggested_default_region
753 .as_ref()
754 .map(|r| r.code.clone());
755 let proposal = db.propose(exact(SFO.0, SFO.1), current, default).unwrap();
756 assert!(!proposal.replace.changes_anything);
757 assert!(!proposal.add_missing.changes_anything);
758 }
759
760 #[test]
761 fn accuracy_circle_reaching_a_boundary_is_uncertain() {
762 let position = MobileRegionPositionRecord {
768 accuracy_m: Some(20_000.0),
769 ..exact(37.70, -122.27)
770 };
771 let proposal = fixture().propose(position, Vec::new(), None).unwrap();
772 let names: Vec<&str> = proposal
773 .lookup
774 .radio_regions
775 .iter()
776 .map(|region| region.name.as_str())
777 .collect();
778 assert!(names.contains(&"OAK"));
779 assert!(names.contains(&"SFO"));
780 assert!(proposal.uncertain_regions.contains(&"SFO".to_owned()));
781 assert!(proposal.uncertain_regions.contains(&"OAK".to_owned()));
782 assert!(!proposal.uncertain_regions.contains(&"XSF".to_owned()));
783 assert!(!proposal.uncertain_regions.contains(&"US".to_owned()));
784 assert_eq!(proposal.cell_meters, Some(40_000.0));
785 }
786
787 #[test]
788 fn advert_cell_samples_its_corners() {
789 let cell = NodeLocation::from_lat_lon(SAN_CARLOS.0 as f32, SAN_CARLOS.1 as f32, 4);
790 let position = MobileRegionPositionRecord {
791 location_bytes: Some(cell.as_bytes().to_vec()),
792 ..exact(SAN_CARLOS.0, SAN_CARLOS.1)
793 };
794 let proposal = fixture().propose(position, Vec::new(), None).unwrap();
795 let width = proposal.cell_meters.unwrap();
798 assert!((610.0..613.0).contains(&width), "width {width}");
799 assert!(
800 proposal
801 .lookup
802 .radio_regions
803 .iter()
804 .any(|r| r.name == "SQL")
805 );
806 }
807
808 #[test]
809 fn coarse_positions_are_refused() {
810 let db = fixture();
811 let cell = NodeLocation::from_lat_lon(SAN_CARLOS.0 as f32, SAN_CARLOS.1 as f32, 2);
812 let coarse_cell = MobileRegionPositionRecord {
813 location_bytes: Some(cell.as_bytes().to_vec()),
814 ..exact(SAN_CARLOS.0, SAN_CARLOS.1)
815 };
816 assert_eq!(
817 db.propose(coarse_cell, Vec::new(), None).err(),
818 Some(MobileRegionError::PositionTooCoarse)
819 );
820 let coarse_fix = MobileRegionPositionRecord {
821 accuracy_m: Some(30_000.0),
822 ..exact(SAN_CARLOS.0, SAN_CARLOS.1)
823 };
824 assert_eq!(
825 db.propose(coarse_fix, Vec::new(), None).err(),
826 Some(MobileRegionError::PositionTooCoarse)
827 );
828 }
829
830 #[test]
831 fn invalid_inputs_are_named() {
832 let db = fixture();
833 assert_eq!(
834 db.lookup(91.0, 0.0).err(),
835 Some(MobileRegionError::InvalidPosition)
836 );
837 assert_eq!(
838 db.propose(
839 MobileRegionPositionRecord {
840 location_bytes: Some(Vec::new()),
841 ..exact(0.0, 0.0)
842 },
843 Vec::new(),
844 None,
845 )
846 .err(),
847 Some(MobileRegionError::InvalidPosition)
848 );
849 assert_eq!(
850 db.propose(exact(0.0, 0.0), Vec::new(), Some(vec![0x12]))
851 .err(),
852 Some(MobileRegionError::InvalidRegionCode)
853 );
854 assert_eq!(
855 db.propose(exact(0.0, 0.0), vec![String::new()], None).err(),
856 Some(MobileRegionError::InvalidRegionCode)
857 );
858 }
859}