mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 22:11:09 +00:00
[AZ-666] [AZ-673] [AZ-648] ignored set + UDS VLM + mission FSM batch 5
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
AZ-666 mapobjects_store: - internal/ignored.rs (HashSet<(mgrs, class_group)> for O(1) suppression) - internal/passes.rs (per-region PassTracker with observed-id set and end-of-pass removed-candidate sweep) - Classification::Ignored wired into classify; apply_decline + is_ignored + pass_start + end_of_pass on MapObjectsStoreHandle - new tests/ignored_and_sweep.rs (3 AC + 2 supplementary) AZ-673 vlm_client: - internal/peer_cred.rs (Linux SO_PEERCRED via libc getsockopt; PeerCredOutcome::SkippedNonLinux on macOS dev hosts per description.md §8) - internal/prompt.rs (pre-send ROI size + format + prompt non-emptiness validation) - internal/wire.rs (length-prefixed JSON envelope with base64 ROI) - internal/uds_client.rs (tokio UnixStream client; bounded reconnect; hard-stop on peer-cred mismatch; per-request deadline) - VlmClient with both eager (open/connect) and lazy (new) ctor - workspace Cargo.toml: base64 + libc as workspace deps AZ-648 mission_executor: - internal/types.rs (Variant, MissionState, TransitionKey, Telemetry, TransitionEvent, StepOutcome) - internal/driver.rs (MissionDriver trait + DriverError + DriverAction) - internal/fsm.rs (variant-agnostic Transition + FsmCore + step_one with per-transition retry budget keyed by TransitionKey) - internal/multirotor.rs + internal/fixed_wing.rs (typed transition tables; multirotor has Armed/TakeOff, fixed-wing parks in WaitAuto for operator AUTO) - public API: MissionExecutor::run spawns the FSM task and returns a clone-safe MissionExecutorHandle (state, health, subscribe, paused_reason, retry_count) - new tests/state_machine.rs (AC-1..AC-4 via ScriptedDriver fake; SITL conformance lands with AZ-649 telemetry forwarding) Workspace: cargo fmt + clippy -D warnings clean; full cargo test --workspace --all-features green (1 ignored = AZ-665 perf gate). Tasks moved todo/ → done/, autodev state set to batch 6 selection. Refs: _docs/03_implementation/batch_05_cycle1_report.md Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//! `IgnoredSet` — operator-declined POIs are suppressed before they reach
|
||||
//! the scan controller's POI queue (see `system-flows.md §F7` and the
|
||||
//! `mapobjects_store` component description §"Ignored set").
|
||||
//!
|
||||
//! Keyed by `(mgrs, class_group)` because that is the literal call shape
|
||||
//! the AC mandates (`is_ignored(mgrs, class_group)`). The `IgnoredItem`
|
||||
//! payload also carries an H3 cell + retention metadata; we keep the
|
||||
//! full payload separately so callers can read it later (e.g. for
|
||||
//! pending-upload sync in AZ-667) without re-fetching from the central
|
||||
//! service.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use shared::models::mapobject::IgnoredItem;
|
||||
|
||||
/// In-memory ignored-suppression index.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IgnoredSet {
|
||||
/// O(1) suppression lookup. Multiple `IgnoredItem`s may share the
|
||||
/// same `(mgrs, class_group)` key — the set still answers `true`
|
||||
/// for the pair.
|
||||
keys: HashSet<(String, String)>,
|
||||
/// Full payloads, keyed by their UUID, retained for sync /
|
||||
/// pending-upload paths in AZ-667.
|
||||
items: HashMap<uuid::Uuid, IgnoredItem>,
|
||||
}
|
||||
|
||||
impl IgnoredSet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Append an `IgnoredItem`. Re-appending the same UUID overwrites
|
||||
/// the prior payload (e.g. when the central sync echoes back a
|
||||
/// record the device just appended locally).
|
||||
pub fn append(&mut self, item: IgnoredItem) {
|
||||
self.keys
|
||||
.insert((item.mgrs.clone(), item.class_group.clone()));
|
||||
self.items.insert(item.id, item);
|
||||
}
|
||||
|
||||
/// O(1) suppression check used by `scan_controller`'s POI gate.
|
||||
pub fn is_ignored(&self, mgrs: &str, class_group: &str) -> bool {
|
||||
// HashSet does not support borrowed-tuple lookup against
|
||||
// `(String, String)` keys, so build the lookup tuple. The
|
||||
// strings are short (MGRS ≤ ~15 chars, class_group ≤ ~32) so
|
||||
// the clone cost is well inside the ≤ 1 ms p99 budget.
|
||||
self.keys
|
||||
.contains(&(mgrs.to_string(), class_group.to_string()))
|
||||
}
|
||||
|
||||
/// Number of distinct `(mgrs, class_group)` pairs currently
|
||||
/// suppressed. Useful for health surfaces and tests.
|
||||
pub fn len(&self) -> usize {
|
||||
self.keys.len()
|
||||
}
|
||||
|
||||
/// Clippy companion to `len`. Kept available because callers
|
||||
/// (health surfaces, sync paths) may want a quick empty check
|
||||
/// instead of a length comparison.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
|
||||
/// Full payload iterator. Reserved for AZ-667 (pending-upload
|
||||
/// dump) and AZ-668 (persistence snapshot).
|
||||
#[allow(dead_code)]
|
||||
pub fn items(&self) -> impl Iterator<Item = &IgnoredItem> {
|
||||
self.items.values()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
use shared::models::mapobject::{IgnoredItemSource, RetentionScope};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn ignored(mgrs: &str, class_group: &str) -> IgnoredItem {
|
||||
IgnoredItem {
|
||||
id: Uuid::new_v4(),
|
||||
mgrs: mgrs.into(),
|
||||
h3_cell: 0,
|
||||
class_group: class_group.into(),
|
||||
decline_time: Utc::now(),
|
||||
operator_id: None,
|
||||
mission_id: "m1".into(),
|
||||
retention_scope: RetentionScope::Mission,
|
||||
expires_at: None,
|
||||
source: IgnoredItemSource::LocalAppended,
|
||||
pending_upload: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appended_pair_is_ignored() {
|
||||
// Arrange
|
||||
let mut s = IgnoredSet::new();
|
||||
|
||||
// Act
|
||||
s.append(ignored("38TUL12345", "concealed_position_group"));
|
||||
|
||||
// Assert
|
||||
assert!(s.is_ignored("38TUL12345", "concealed_position_group"));
|
||||
assert!(!s.is_ignored("38TUL12345", "movement_candidate"));
|
||||
assert!(!s.is_ignored("38TUL99999", "concealed_position_group"));
|
||||
assert_eq!(s.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn re_append_does_not_inflate_distinct_count() {
|
||||
// Arrange
|
||||
let mut s = IgnoredSet::new();
|
||||
let it = ignored("38TUL12345", "concealed_position_group");
|
||||
let id = it.id;
|
||||
|
||||
// Act
|
||||
s.append(it);
|
||||
s.append(IgnoredItem {
|
||||
id,
|
||||
..ignored("38TUL12345", "concealed_position_group")
|
||||
});
|
||||
|
||||
// Assert
|
||||
assert_eq!(s.len(), 1);
|
||||
assert_eq!(s.items.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_set_returns_false() {
|
||||
// Assert
|
||||
let s = IgnoredSet::new();
|
||||
assert!(!s.is_ignored("foo", "bar"));
|
||||
assert_eq!(s.len(), 0);
|
||||
assert!(s.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Internal-only modules. Not part of the public `mapobjects_store` API.
|
||||
|
||||
pub mod h3_index;
|
||||
pub mod ignored;
|
||||
pub mod passes;
|
||||
pub mod store;
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Per-region pass tracker for `end_of_pass(region)` sweeps.
|
||||
//!
|
||||
//! `scan_controller` / `mission_executor` open a pass with `pass_start`
|
||||
//! when the UAV begins traversing a region; classify calls that fall
|
||||
//! inside any open pass automatically register the matched MapObject
|
||||
//! `id` as "observed during this pass". When the pass closes, the
|
||||
//! `end_of_pass` sweep returns objects in the region that were known
|
||||
//! at pass start but *not* observed during the pass — they become
|
||||
//! `RemovedCandidate`s that the operator (not the device) decides on.
|
||||
//!
|
||||
//! Bounding-box test uses the half-open WGS-84 rectangle `[NW, SE]`
|
||||
//! convention used everywhere else in the project (see
|
||||
//! `data_model.md`).
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use shared::models::mission::Coordinate;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Operator-supplied region bounding box. `corners[0]` is NW, `corners[1]`
|
||||
/// is SE — same orientation as `MapObjectsBundle.bbox`.
|
||||
pub type RegionBbox = [Coordinate; 2];
|
||||
|
||||
/// Generate the deterministic per-region key from the bbox corners.
|
||||
///
|
||||
/// We rely on bit-for-bit equality of the `f64` corners because the
|
||||
/// `scan_controller` always re-uses the exact same region descriptor
|
||||
/// across `pass_start` / `end_of_pass`. Floating-point equality is
|
||||
/// fine for that producer; callers that round-trip through JSON should
|
||||
/// re-use the same struct.
|
||||
fn key(bbox: &RegionBbox) -> RegionKey {
|
||||
let nw = bbox[0];
|
||||
let se = bbox[1];
|
||||
RegionKey {
|
||||
nw_lat_bits: nw.latitude.to_bits(),
|
||||
nw_lon_bits: nw.longitude.to_bits(),
|
||||
se_lat_bits: se.latitude.to_bits(),
|
||||
se_lon_bits: se.longitude.to_bits(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct RegionKey {
|
||||
nw_lat_bits: u64,
|
||||
nw_lon_bits: u64,
|
||||
se_lat_bits: u64,
|
||||
se_lon_bits: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OpenPass {
|
||||
bbox: RegionBbox,
|
||||
started_at: DateTime<Utc>,
|
||||
observed: HashSet<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PassTracker {
|
||||
open: HashMap<RegionKey, OpenPass>,
|
||||
}
|
||||
|
||||
impl PassTracker {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Open a new pass over `bbox`. If a pass is already open over the
|
||||
/// same bbox we restart it (the `scan_controller` is the only
|
||||
/// producer and this matches its retry behaviour).
|
||||
pub fn pass_start(&mut self, bbox: RegionBbox, started_at: DateTime<Utc>) {
|
||||
let k = key(&bbox);
|
||||
self.open.insert(
|
||||
k,
|
||||
OpenPass {
|
||||
bbox,
|
||||
started_at,
|
||||
observed: HashSet::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Mark `id` as observed during every open pass whose bbox
|
||||
/// contains `(lat, lon)`. The classify path calls this on every
|
||||
/// `Existing` / `Moved` / `New` outcome so the caller does not
|
||||
/// have to thread the bbox through.
|
||||
pub fn note_observed(&mut self, id: Uuid, lat: f64, lon: f64) {
|
||||
for pass in self.open.values_mut() {
|
||||
if bbox_contains(&pass.bbox, lat, lon) {
|
||||
pass.observed.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the pass over `bbox` and return the observed ids that the
|
||||
/// caller needs to compare against the store's known-in-region set.
|
||||
/// Returns `None` if no pass was open over that bbox.
|
||||
pub fn pass_end(&mut self, bbox: &RegionBbox) -> Option<PassResult> {
|
||||
let k = key(bbox);
|
||||
let open = self.open.remove(&k)?;
|
||||
Some(PassResult {
|
||||
started_at: open.started_at,
|
||||
observed: open.observed,
|
||||
})
|
||||
}
|
||||
|
||||
/// Number of currently-open passes (health surface).
|
||||
pub fn open_passes(&self) -> usize {
|
||||
self.open.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PassResult {
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub observed: HashSet<Uuid>,
|
||||
}
|
||||
|
||||
/// Half-open bbox containment: lat in `[se_lat, nw_lat]`, lon in `[nw_lon, se_lon]`.
|
||||
///
|
||||
/// `NW` is north-west (highest lat, lowest lon), `SE` is south-east
|
||||
/// (lowest lat, highest lon). We use closed-closed because tests
|
||||
/// commonly place points exactly on a corner and there is no risk of
|
||||
/// double-counting in this code path (a point on a shared boundary
|
||||
/// belongs to every covering pass, by design).
|
||||
pub fn bbox_contains(bbox: &RegionBbox, lat: f64, lon: f64) -> bool {
|
||||
let nw = bbox[0];
|
||||
let se = bbox[1];
|
||||
let (lat_min, lat_max) = (se.latitude.min(nw.latitude), se.latitude.max(nw.latitude));
|
||||
let (lon_min, lon_max) = (
|
||||
nw.longitude.min(se.longitude),
|
||||
nw.longitude.max(se.longitude),
|
||||
);
|
||||
(lat_min..=lat_max).contains(&lat) && (lon_min..=lon_max).contains(&lon)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bbox(nw_lat: f64, nw_lon: f64, se_lat: f64, se_lon: f64) -> RegionBbox {
|
||||
[
|
||||
Coordinate {
|
||||
latitude: nw_lat,
|
||||
longitude: nw_lon,
|
||||
altitude_m: 0.0,
|
||||
},
|
||||
Coordinate {
|
||||
latitude: se_lat,
|
||||
longitude: se_lon,
|
||||
altitude_m: 0.0,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bbox_contains_inside_point() {
|
||||
// Arrange
|
||||
let b = bbox(51.0, 30.0, 50.0, 31.0);
|
||||
// Assert
|
||||
assert!(bbox_contains(&b, 50.5, 30.5));
|
||||
assert!(!bbox_contains(&b, 49.9, 30.5));
|
||||
assert!(!bbox_contains(&b, 50.5, 31.1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_observed_only_inside_open_pass() {
|
||||
// Arrange
|
||||
let mut t = PassTracker::new();
|
||||
let b = bbox(51.0, 30.0, 50.0, 31.0);
|
||||
t.pass_start(b, Utc::now());
|
||||
let inside = Uuid::new_v4();
|
||||
let outside = Uuid::new_v4();
|
||||
|
||||
// Act
|
||||
t.note_observed(inside, 50.5, 30.5);
|
||||
t.note_observed(outside, 49.5, 30.5);
|
||||
|
||||
// Assert
|
||||
let result = t.pass_end(&b).expect("pass open");
|
||||
assert!(result.observed.contains(&inside));
|
||||
assert!(!result.observed.contains(&outside));
|
||||
assert_eq!(t.open_passes(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_end_returns_none_when_no_pass_open() {
|
||||
// Arrange
|
||||
let mut t = PassTracker::new();
|
||||
let b = bbox(51.0, 30.0, 50.0, 31.0);
|
||||
// Assert
|
||||
assert!(t.pass_end(&b).is_none());
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,12 @@ use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use h3o::CellIndex;
|
||||
use shared::error::Result;
|
||||
use shared::models::mapobject::IgnoredItem;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::h3_index::{cell_of, grid_disk, haversine_m, DEFAULT_K_RING, DEFAULT_RESOLUTION};
|
||||
use super::ignored::IgnoredSet;
|
||||
use super::passes::{bbox_contains, PassTracker, RegionBbox};
|
||||
|
||||
/// Per-detection input to `classify`. This bundles the georeferenced
|
||||
/// payload the architecture-level "detection" carries (gps, class, conf,
|
||||
@@ -86,14 +89,26 @@ pub enum Classification {
|
||||
Existing {
|
||||
id: Uuid,
|
||||
},
|
||||
/// Reserved for AZ-666 end-of-pass sweep.
|
||||
RemovedCandidate {
|
||||
id: Uuid,
|
||||
},
|
||||
/// Reserved for AZ-666 ignored-suppression.
|
||||
/// Suppressed because the `(mgrs, class_group)` pair is in the
|
||||
/// `IgnoredSet` — the operator previously declined this POI.
|
||||
/// `scan_controller` must drop the detection without queueing it.
|
||||
Ignored,
|
||||
}
|
||||
|
||||
/// Object that the store knew about at pass start but did not see
|
||||
/// re-observed before `end_of_pass`. See `system-flows.md §F7`
|
||||
/// "end-of-pass sweep" — operator (not device) decides removal.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemovedCandidate {
|
||||
pub id: Uuid,
|
||||
pub mgrs: String,
|
||||
pub class: String,
|
||||
pub class_group: String,
|
||||
pub gps_lat: f64,
|
||||
pub gps_lon: f64,
|
||||
pub last_seen: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Stored shape. Fields beyond what `classify` reads are kept for the
|
||||
/// next batch in the same component (AZ-666 ignored-suppression / sweep,
|
||||
/// AZ-667 hydrate / dump_pending) which will surface them via the engine
|
||||
@@ -122,6 +137,8 @@ pub struct Store {
|
||||
by_cell: HashMap<CellIndex, Vec<StoredMapObject>>,
|
||||
/// Total object count, maintained alongside `by_cell` for O(1) metrics.
|
||||
len: usize,
|
||||
ignored: IgnoredSet,
|
||||
passes: PassTracker,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
@@ -130,6 +147,8 @@ impl Store {
|
||||
config,
|
||||
by_cell: HashMap::new(),
|
||||
len: 0,
|
||||
ignored: IgnoredSet::new(),
|
||||
passes: PassTracker::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +156,79 @@ impl Store {
|
||||
self.len
|
||||
}
|
||||
|
||||
/// Exposed for AZ-666/AZ-667 engine plug-points (`internal::engine::*`).
|
||||
/// Forward-use hook for AZ-667 / AZ-668 engine plug-points.
|
||||
#[allow(dead_code)]
|
||||
pub fn config(&self) -> &MapObjectsStoreConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Suppression query used by `scan_controller`'s POI gate.
|
||||
pub fn is_ignored(&self, mgrs: &str, class_group: &str) -> bool {
|
||||
self.ignored.is_ignored(mgrs, class_group)
|
||||
}
|
||||
|
||||
/// Append an `IgnoredItem` (operator declined a POI, or a hydrate
|
||||
/// from `mission_client` pulled it down).
|
||||
pub fn append_ignored(&mut self, item: IgnoredItem) {
|
||||
self.ignored.append(item);
|
||||
}
|
||||
|
||||
/// Number of distinct ignored `(mgrs, class_group)` pairs.
|
||||
pub fn ignored_len(&self) -> usize {
|
||||
self.ignored.len()
|
||||
}
|
||||
|
||||
/// Open a scan pass over `bbox`. `scan_controller` / `mission_executor`
|
||||
/// call this when entering a region; the matching `end_of_pass`
|
||||
/// returns un-observed objects as `RemovedCandidate`s.
|
||||
pub fn pass_start(&mut self, bbox: RegionBbox, started_at: DateTime<Utc>) {
|
||||
self.passes.pass_start(bbox, started_at);
|
||||
}
|
||||
|
||||
/// Close the pass over `bbox` and return objects in the region that
|
||||
/// were not observed since the pass started, excluding ignored
|
||||
/// objects. Returns an empty vec if no pass was open.
|
||||
pub fn end_of_pass(&mut self, bbox: &RegionBbox) -> Vec<RemovedCandidate> {
|
||||
let Some(result) = self.passes.pass_end(bbox) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for objects in self.by_cell.values() {
|
||||
for obj in objects {
|
||||
if !bbox_contains(bbox, obj.gps_lat, obj.gps_lon) {
|
||||
continue;
|
||||
}
|
||||
if result.observed.contains(&obj.id) {
|
||||
continue;
|
||||
}
|
||||
// Filter out ignored — operator already said "no" on
|
||||
// this pair; surfacing it again would be noise.
|
||||
if self.ignored.is_ignored(&obj.mgrs, &obj.class_group) {
|
||||
continue;
|
||||
}
|
||||
// Pass started after the object's last_seen → object
|
||||
// was known at pass start.
|
||||
if obj.last_seen > result.started_at {
|
||||
continue;
|
||||
}
|
||||
out.push(RemovedCandidate {
|
||||
id: obj.id,
|
||||
mgrs: obj.mgrs.clone(),
|
||||
class: obj.class.clone(),
|
||||
class_group: obj.class_group.clone(),
|
||||
gps_lat: obj.gps_lat,
|
||||
gps_lon: obj.gps_lon,
|
||||
last_seen: obj.last_seen,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn open_passes(&self) -> usize {
|
||||
self.passes.open_passes()
|
||||
}
|
||||
|
||||
/// Resolve a raw class string to its canonical group key.
|
||||
///
|
||||
/// The first class listed in a `similar_classes` group is the group
|
||||
@@ -162,11 +248,20 @@ impl Store {
|
||||
|
||||
/// Classify a single detection input. Mutates the store on `New` /
|
||||
/// `Moved` / `Existing` (insert / position-update / last_seen-update
|
||||
/// respectively). Returns the classification.
|
||||
/// respectively). Returns `Ignored` and DOES NOT mutate when the
|
||||
/// resolved `(mgrs, class_group)` is in the ignored set.
|
||||
///
|
||||
/// Also notes the matched id into every open pass whose bbox
|
||||
/// contains the input GPS so end-of-pass sweeps see this object
|
||||
/// as observed.
|
||||
pub fn classify(&mut self, input: ClassifyInput) -> Result<Classification> {
|
||||
let query_cell = cell_of(input.gps_lat, input.gps_lon, self.config.h3_resolution)?;
|
||||
let group = self.group_key(&input.class);
|
||||
|
||||
if self.ignored.is_ignored(&input.mgrs, &group) {
|
||||
return Ok(Classification::Ignored);
|
||||
}
|
||||
|
||||
// Find the nearest matching object across the k-ring.
|
||||
let mut best: Option<(CellIndex, usize, f64)> = None;
|
||||
let disk = grid_disk(query_cell, self.config.k_ring);
|
||||
@@ -217,6 +312,7 @@ impl Store {
|
||||
..moved
|
||||
});
|
||||
}
|
||||
self.passes.note_observed(id, input.gps_lat, input.gps_lon);
|
||||
Ok(Classification::Moved {
|
||||
id,
|
||||
from_mgrs,
|
||||
@@ -231,7 +327,9 @@ impl Store {
|
||||
.expect("cell present during best-match scan");
|
||||
let obj = &mut bucket[idx];
|
||||
obj.last_seen = input.observed_at;
|
||||
Ok(Classification::Existing { id: obj.id })
|
||||
let id = obj.id;
|
||||
self.passes.note_observed(id, input.gps_lat, input.gps_lon);
|
||||
Ok(Classification::Existing { id })
|
||||
}
|
||||
None => {
|
||||
// NEW — insert.
|
||||
@@ -253,6 +351,7 @@ impl Store {
|
||||
};
|
||||
self.by_cell.entry(query_cell).or_default().push(stored);
|
||||
self.len += 1;
|
||||
self.passes.note_observed(id, input.gps_lat, input.gps_lon);
|
||||
Ok(Classification::New { id })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user