From da97924ad2cedc3b06c105606547b267b22d2073 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Wed, 16 Sep 2026 23:20:53 -0400 Subject: [PATCH] =?UTF-8?q?v0.5.8=20=E2=80=94=20AIRCRAFT=20source,=20wider?= =?UTF-8?q?=20Overpass=20query,=20distance-graded=20scoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a sixth source: police and surveillance aircraft overhead, via the free community ADS-B networks (opendata.adsb.fi, api.adsb.lol fallback, no API key). Contacts are matched by ICAO 24-bit address against a bundled registry of 1,971 US law-enforcement airframes generated by scripts/gen-le-aircraft.py from ADSBexchange's basic-ac-db and committed as res/raw/le_aircraft.csv. Community feeds are used deliberately: FlightAware and Flightradar24 filter law-enforcement flights at government request, which is the traffic this source exists to see. Two parse traps handled — the envelope is {"ac":[..]} on radius queries but {"aircraft":[..]} on wider ones, and alt_baro is the string "ground" when parked. Registry coverage is imperfect, so the scanner also detects loiter/orbit behaviour (3+ samples over 4+ min within 5 km of their centroid, below 12,000 ft and under 200 kt). Surveillance aircraft circle; airliners and medevac flights going somewhere do not. Behaviour-only hits are capped at 69 and must be within 8 km, so ordinary traffic never alerts. Rejected with measurements: plane-alert-db matched 0 of 394 live aircraft over a 250 nm sweep of DC while 15 helicopters were aloft (only 255 of its entries are US-registered); registry.faa.gov is Akamai-403 and N-number keyed; OpenSky's CSV is 94 MB for the same data; EFF's Atlas of Surveillance has no aircraft identities at all, only a 2022 FAA drone lookup. Overpass query widened from ALPR-only to man_made=surveillance plus highway=speed_camera, classified into ALPR / speed camera / generic camera with separate falloff curves so a shop's CCTV cannot alarm like a Flock install. Cache key bumped to deflock2_ so v1 ALPR-only entries are not served for another 24 h. DeFlock, Waze and aircraft are now scored by continuous distance falloff rather than flat values. The anchors are absolute metres and deliberately independent of the detection-radius setting: moving a slider must not move the threat level. New SOURCES.md documents every endpoint, identifier and scoring table, including the sources that were tried and rejected and the measurements that killed them. Verified against live traffic: detected San Diego PD's AS50 at 1580 m / 1000 ft scoring 83 (ORANGE) and a San Diego County Sheriff B407 at 11742 m scoring 44, with the score tracking the helicopter from 81 to 83 as it closed. Both match the falloff formula exactly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR --- README.md | 94 +- SOURCES.md | 437 ++++ app/build.gradle.kts | 4 +- .../org/soulstone/overwatch/MainActivity.kt | 14 +- .../data/location/LocationProvider.kt | 13 +- .../overwatch/data/settings/Settings.kt | 59 +- .../overwatch/data/targets/LeAircraft.kt | 66 + .../overwatch/fusion/ConfidenceEngine.kt | 210 +- .../overwatch/fusion/DetectionEvent.kt | 6 +- .../overwatch/fusion/SourceHealth.kt | 10 +- .../soulstone/overwatch/fusion/ThreatLevel.kt | 2 +- .../overwatch/scan/AircraftClient.kt | 146 ++ .../overwatch/scan/AircraftScanner.kt | 195 ++ .../soulstone/overwatch/scan/CitizenClient.kt | 168 -- .../overwatch/scan/CitizenScanner.kt | 195 -- .../soulstone/overwatch/scan/DeflockClient.kt | 57 +- .../overwatch/scan/DeflockScanner.kt | 7 +- .../soulstone/overwatch/scan/WazeScanner.kt | 7 +- .../overwatch/service/DetectionService.kt | 64 +- .../org/soulstone/overwatch/ui/MainScreen.kt | 125 +- .../org/soulstone/overwatch/ui/MarkerIcons.kt | 6 +- .../soulstone/overwatch/ui/OverlayBubble.kt | 92 +- .../soulstone/overwatch/ui/PulseVisuals.kt | 118 + .../soulstone/overwatch/ui/SettingsScreen.kt | 38 +- app/src/main/res/raw/le_aircraft.csv | 1972 +++++++++++++++++ docs/index.html | 54 +- scripts/emu-gps.sh | 49 + scripts/gen-le-aircraft.py | 89 + 28 files changed, 3552 insertions(+), 745 deletions(-) create mode 100644 SOURCES.md create mode 100644 app/src/main/kotlin/org/soulstone/overwatch/data/targets/LeAircraft.kt create mode 100644 app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftClient.kt create mode 100644 app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftScanner.kt delete mode 100644 app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenClient.kt delete mode 100644 app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenScanner.kt create mode 100644 app/src/main/kotlin/org/soulstone/overwatch/ui/PulseVisuals.kt create mode 100644 app/src/main/res/raw/le_aircraft.csv create mode 100755 scripts/emu-gps.sh create mode 100755 scripts/gen-le-aircraft.py diff --git a/README.md b/README.md index 733ff13..68ab3de 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ on upward escalations — you don't have to be looking at the screen. > advertise/fuzz code from one of the reference projects is intentionally > excluded. -Website: **[overwatch.netslum.io](https://overwatch.netslum.io)** · Latest release: [v0.5.6](https://github.com/KaraZajac/OVERWATCH/releases) (debug-signed APK, sideload). +Website: **[overwatch.netslum.io](https://overwatch.netslum.io)** · Latest release: [v0.5.7](https://github.com/KaraZajac/OVERWATCH/releases) (debug-signed APK, sideload). --- @@ -36,27 +36,29 @@ Website: **[overwatch.netslum.io](https://overwatch.netslum.io)** · Latest re |---|---|---| | **BLE** | Bluetooth-LE advertisements: vendor MAC OUIs (Axon, Flock Penguin / Raven, XUNTONG mfg id `0x09C8`, "TN" serial pattern), Raven service UUIDs, device-name patterns — plus 18 IEEE-verified surveillance-vendor OUIs (ShotSpotter, WatchGuard/Motorola, Verkada, Avigilon Alta, Axis body cams, FLIR, Hanwha, March Networks, GeoVision, Mobotix, Sunell) with vendor-named labels | Local radio scan (BLE callback API). Iterates every manufacturer-specific data entry to find XUNTONG, not just the first. Police-exclusive OUIs (WatchGuard, ShotSpotter) score ORANGE on sight, same rationale as Axon. | | **WiFi** | BSSID OUI prefixes for Flock infrastructure (31-prefix superset) + the same 18 vendor OUIs (WatchGuard 4RE in-car APs, Openpath/Alta readers, WiFi-capable cameras), `Flock-XXXX` and other generic SSID patterns | `WifiManager.getScanResults()` polled every 35 s (just under the Android 11+ 4-scans/2-min throttle) | -| **DEFLOCK** | Crowdsourced ALPR locations within configurable proximity (default 200 m) | POST to Overpass API (`overpass.deflock.org` → fallback `overpass-api.de`) for `man_made=surveillance + surveillance:type=ALPR` in a 5 km bbox; 24 h on-disk cache by 0.05° grid cell. Refetches when the user moves > 1.5 km from the last fetch center. Backoffs after Overpass failures; treats `{"remark": "...timed out..."}` 200-responses as failure so timeouts don't poison the cache. | -| **CITIZEN** | ⚠️ **Feed retired upstream (June 2026)** — see the note below. Previously: real-time public-safety incidents (police-relevant only) within proximity, < 30 min old | `citizen.com/api/incident/trending` (bbox) polled every 60 s, then per-incident detail via `/api/incident/{id}`. The endpoints now return empty stubs, so the source reports "feed retired upstream" and backs off to a 30-min heartbeat instead of polling a dead endpoint. | -| **WAZE** | User-reported `POLICE` alerts still active in the feed within configurable proximity (default 500 m), up to ~45 min old | `api.openwebninja.com/waze/alerts-and-jams` — [OpenWeb Ninja](https://www.openwebninja.com)'s hosted Waze feed, called directly with **your own API key** (`X-API-Key`) entered in Settings and stored encrypted on-device. Sidesteps the reCAPTCHA gating that 403s direct `live-map/api/georss` calls. Polled every ~4 min with `alert_types=POLICE&max_jams=0` (server-side filtering, ~1.5 KB/poll), and the client re-filters by type so a silent upstream change can't let other alert types through. Alerts carry confidence (0–5) + reliability (0–10); high values nudge the score up. No key → source shows "not configured" in the drill-down. | +| **DEFLOCK** | Crowdsourced ALPR locations within the detection radius (default 500 m), scored by how close each one actually is | POST to Overpass API (`overpass.deflock.org` → fallback `overpass-api.de`) for `man_made=surveillance + surveillance:type=ALPR` in a 5 km bbox; 24 h on-disk cache by 0.05° grid cell. Refetches when the user moves > 1.5 km from the last fetch center. Backoffs after Overpass failures; treats `{"remark": "...timed out..."}` 200-responses as failure so timeouts don't poison the cache. | +| **WAZE** | User-reported `POLICE` alerts still active in the feed within the detection radius (default 500 m), up to ~45 min old, scored by distance and age | `api.openwebninja.com/waze/alerts-and-jams` — [OpenWeb Ninja](https://www.openwebninja.com)'s hosted Waze feed, called directly with **your own API key** (`X-API-Key`) entered in Settings and stored encrypted on-device. Sidesteps the reCAPTCHA gating that 403s direct `live-map/api/georss` calls. Polled every ~4 min with `alert_types=POLICE&max_jams=0` (server-side filtering, ~1.5 KB/poll), and the client re-filters by type so a silent upstream change can't let other alert types through. Alerts carry confidence (0–5) + reliability (0–10); high values nudge the score up. No key → source shows "not configured" in the drill-down. | +| **AIRCRAFT** | Police / surveillance aircraft overhead — identified from a bundled registry of 1,971 US law-enforcement airframes, plus loiter detection for unlisted ones | `opendata.adsb.fi` → fallback `api.adsb.lol`, polled every 60 s. **No API key.** Community ADS-B networks are used specifically because the commercial trackers filter law-enforcement flights at government request. Matched on the ICAO 24-bit address; scored by ground distance, altitude and orbit behaviour. | | **COMMERCIAL** | Nearby consumer smart-home / voice gear (Nest, Ring, Echo, hidden cams) and camera-bearing smart glasses (Meta, Snap, Vuzix) as a secondary situational signal | Rides the BLE + WiFi scans — OUI / device-name / service-UUID / SSID matches plus Bluetooth SIG company IDs from `MicTargets`. Score-capped at ORANGE so a cluster of doorbells (or a passing pair of Ray-Bans) never reads as ALPR-grade certainty. | -> **Citizen went dark (v0.5.5).** Citizen ended the police-dispatch data -> partnership behind its public feed in June 2026 and stubbed the endpoints. -> Verified 2026-08-28: `/api/incident/trending` answers HTTP 200 with a bare -> JSON empty string, `/api/incident/{id}` returns `{}` for every id (real or -> invented), and `data.sp0n.io/v1/incidents/trending` — the host the current web -> app uses — returns a zero-byte body even with no query params at all. Every -> sibling path (`nearby`, `recent`, `latest`, `map`, `list`, `active`) returns -> `{}`. That is a decommissioned surface, not a changed contract, so there is no -> parameter or host fix; structured incident data is now Citizen's paid -> Enterprise API only. The app no longer leaks the resulting JSON parse error -> into the drill-down — it reports the shutdown plainly and stops hammering the -> endpoint. Police presence is still covered by Waze (denser for roadway stops -> anyway) and DeFlock. +> **Citizen was removed (v0.5.7).** Citizen ended the police-dispatch data +> partnership behind its public feed in June 2026. It first degraded to silent +> empty stubs, and as of **2026-09-16** `citizen.com/api/incident/trending` +> returns **HTTP 410 Gone — `{"error":"This endpoint has been removed."}`**, +> which is upstream formally tombstoning it. `data.sp0n.io` (the host the +> current web app uses) answers 200 with a zero-byte body even with no query +> params. Structured incident data is now Citizen's paid Enterprise API only, +> which needs a business use-case application — not something this app can +> ship. The source and all its code were therefore deleted rather than left +> as a permanently-failing row. Police presence is still covered by Waze +> (denser for roadway stops anyway) and DeFlock. > **Waze needs your own API key (v0.5.6+).** Waze reCAPTCHA-gated its `live-map/api/georss` endpoint in 2025/2026 — automated calls get HTTP 403 regardless of IP or headless-vs-headful browser (it scores browser *reputation*, verified by direct testing), which is why v0.1.5 removed the original integration and why no free scraper survives. OVERWATCH reads Waze POLICE alerts through [OpenWeb Ninja](https://www.openwebninja.com)'s hosted feed. Earlier builds routed this through a private proxy that injected a shared key, which meant handing out a credential that wasn't the user's; **v0.5.6 calls OpenWeb Ninja directly with a key you supply yourself** — see [Waze setup](#waze-setup-bring-your-own-api-key) below. Nothing is baked into the APK, so a published build carries no credential and each install bills to its own account. The Waze for Cities partner feed was ruled out — it excludes POLICE and is agency-only. +> **Full reference:** [SOURCES.md](SOURCES.md) documents every endpoint, every +> BLE/WiFi identifier, the scoring tables, and the sources that were tried and +> rejected (and why). + Every observation is scored 0–100 by `ConfidenceEngine`. The on-screen tier is the maximum live score across all sources: @@ -67,6 +69,29 @@ ORANGE 70 – 84 high confidence RED 85 + certain ``` +**DeFlock and Waze are scored on a sliding scale, not a flat value.** Both +carry real coordinates, so the score is a continuous falloff over the actual +distance — drive toward a camera and the number climbs, drive past and it +drops. The anchors are absolute metres and deliberately independent of the +detection-radius setting: a camera 200 m away is equally close whether the +slider reads 300 m or 5 km, so moving a setting must never move the threat +level. A Waze report additionally decays by up to 12 points across its 45-min +freshness window, because police move and a surveyed camera does not. + +| Distance | DeFlock ALPR | Waze police (fresh) | +|---|---|---| +| 0 m | 92 RED | 80 ORANGE | +| 50 m | 85 RED | 75 ORANGE | +| 100 m | 77 ORANGE | 70 ORANGE | +| 200 m | 60 YELLOW | 62 YELLOW | +| 600 m | 45 YELLOW | 49 YELLOW | +| 1200 m | 35 GREEN | 41 YELLOW | +| 3000 m | 22 GREEN | 28 GREEN | + +A fixed ALPR peaks higher and decays faster than a crowd-sourced police pin: +its position is surveyed and exact, while a Waze report is a coarse pin on a +car that may be moving toward you. + The user-facing circle uses the full 4-tier mapping. Cross-source corroboration naturally pushes the global max upward (a BLE OUI hit *and* a DeFlock map match in the same area produce a higher tier than either alone). When idle, @@ -76,7 +101,7 @@ glance from "scanning, all clear." While scanning, the circle becomes a live OpenStreetMap centered on you, wrapped in a **threat-color ring** (the current tier at a glance) and marked with a ⌖ crosshair for your position. Map geodata is color-coded by source — **Flock / -DeFlock cameras red, Waze police blue, Citizen incidents purple** — so each dot +DeFlock cameras red, Waze police blue** — so each dot is self-explanatory. The same map renders in a smaller floating overlay bubble (Settings → Display over other apps) so it works over other apps. @@ -86,7 +111,7 @@ is self-explanatory. The same map renders in a smaller floating overlay bubble - **In-app**: the threat circle shows a live map with a threat-color ring and source-color dots while scanning; tap it to open the bottom-sheet drill-down - with per-source rows. DeFlock, Citizen, and Waze events carry coordinates — + with per-source rows. DeFlock and Waze events carry coordinates — each row has a tap-to-open Maps icon. - **Foreground notification**: rebuilt on every threat-tier change. Title becomes `OVERWATCH • RED` (or whatever tier); text shows the top @@ -114,11 +139,12 @@ scan/BleScanner.kt BLE callback scanner scan/WifiScanner.kt WifiManager poller + SCAN_RESULTS receiver scan/DeflockClient.kt Overpass POST (deflock.org → overpass-api.de) + 24h cache scan/DeflockScanner.kt location-driven proximity check + failure backoff -scan/CitizenClient.kt GET /api/incident/trending + /api/incident/{id} -scan/CitizenScanner.kt 60 s poller, fire/medical filter, per-id cache scan/WazeClient.kt GET api.openwebninja.com (X-API-Key, user's own key) +scan/AircraftClient.kt GET adsb.fi → adsb.lol (no key), live ADS-B contacts +scan/AircraftScanner.kt 60 s poller, registry match + loiter/orbit detection +data/targets/LeAircraft.kt bundled ICAO-hex table of law-enforcement aircraft scan/WazeScanner.kt ~4 min poller, 200-alert page, client-side POLICE filter -fusion/ConfidenceEngine.kt scoring (BLE / WiFi / DeFlock / Citizen / Waze / Commercial) +fusion/ConfidenceEngine.kt scoring (BLE / WiFi / DeFlock / Waze / Commercial) fusion/RssiTracker.kt rise-peak-fall stationary-signal detector fusion/DetectionStore.kt in-memory dedup, 5-min retention, max-tier flow fusion/SourceHealth.kt per-source OK/FAILED registry for the drill-down @@ -212,10 +238,10 @@ without an uninstall. |---|---| | `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` (API 31+) | BLE scanning | | `BLUETOOTH`, `BLUETOOTH_ADMIN` (≤ API 30) | BLE scanning, legacy | -| `ACCESS_FINE_LOCATION` | Required for BLE pre-S, WiFi pre-T, and DeFlock/Citizen proximity | +| `ACCESS_FINE_LOCATION` | Required for BLE pre-S, WiFi pre-T, and DeFlock/Waze proximity | | `NEARBY_WIFI_DEVICES` (API 33+) | WiFi scan results without using location | | `ACCESS_WIFI_STATE`, `CHANGE_WIFI_STATE` | Trigger and read scan results | -| `INTERNET`, `ACCESS_NETWORK_STATE` | DeFlock Overpass, Citizen API, OpenWeb Ninja Waze feed | +| `INTERNET`, `ACCESS_NETWORK_STATE` | DeFlock Overpass, OpenWeb Ninja Waze feed, ADS-B aircraft feeds | | `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_CONNECTED_DEVICE`, `FOREGROUND_SERVICE_LOCATION` | Keep scanning with the screen off | | `POST_NOTIFICATIONS` (API 33+) | Foreground-service notification | | `VIBRATE` | Haptic alert on threat-tier escalation | @@ -232,13 +258,13 @@ so you can grant manually. Tap the gear icon in the top-right. -- **Detection sources**: toggle BLE / WiFi / DeFlock / Citizen / Waze / Commercial independently. +- **Detection sources**: toggle BLE / WiFi / DeFlock / Waze / Aircraft / Commercial independently. Changes take effect on the next Start. While scanning, a **Restart scan to apply** button appears that does `stop()` + `start()` in one tap. -- **Proximity thresholds** (sliders commit on release, not per-pixel): - - DeFlock: 50 m – 1600 m (default 200 m) - - Citizen: 100 m – 5000 m (default 500 m) - - Waze: 100 m – 5000 m (default 500 m) +- **Detection radius** (one slider for both location sources; commits on + release, not per-pixel): 100 m – 5000 m, default 500 m. It sets what gets + reported and what the map circle draws — *not* how alarming a hit is, which + is purely a function of real distance (see below). - **Waze police feed**: paste your own OpenWeb Ninja API key — see [Waze setup](#waze-setup-bring-your-own-api-key). Stored encrypted on-device (Android Keystore), never baked into the APK. Empty = Waze source off. @@ -264,12 +290,12 @@ These live under `REFERENCES/` (gitignored): ## Status -Phases 1–5 (skeleton, BLE, WiFi, DeFlock, Citizen, polish) complete and -field-tested. Current release **v0.5.6**. Notable changes: +Phases 1–5 (skeleton, BLE, WiFi, DeFlock, polish) complete and +field-tested. Current release **v0.5.7**. Notable changes: - v0.1.2 — Android 14+ foreground service type fix; NaN-coordinate filter on map data. - v0.1.3 — DeFlock CDN replaced by direct Overpass calls (Cloudflare-blocked). -- v0.1.4 — Citizen.com added as 5th source, per-source health registry. +- v0.1.4 — Citizen.com added as 5th source, per-source health registry. *(source removed in v0.5.7 — feed retired upstream)* - v0.1.5 — Waze removed (reCAPTCHA-gated; no clean mobile workaround at the time). - v0.1.6 — Dynamic notification with tier + label, haptic alerts, Open-in-Maps for geo events. - v0.1.7 — System back from Settings returns to MAIN instead of exiting. @@ -277,12 +303,14 @@ field-tested. Current release **v0.5.6**. Notable changes: - v0.2.1–v0.2.2 — Live proximity refresh, map dot markers, map + Settings UI polish. - v0.3.0–v0.3.2 — Floating threat-circle overlay (chat-bubble style) + drag/crash fixes. - v0.5.0 — Waze re-added via a key-protected Caddy proxy (`api.blackflagintel.com`): the OpenWeb Ninja key stays server-side, the app uses an encrypted per-device token. GitHub Actions release pipeline added. -- v0.5.1 — UI: larger map circle with a threat-color ring, ⌖ user crosshair, source-color dots (Flock red / Waze blue / Citizen purple), START moved to the bottom. +- v0.5.1 — UI: larger map circle with a threat-color ring, ⌖ user crosshair, source-color dots (Flock red / Waze blue), START moved to the bottom. - v0.5.2 — Committed a fixed debug keystore so CI + local builds sign identically; updates now install in place (no functional changes). - v0.5.3 — Detect Meta / Snap / Vuzix smart glasses in the COMMERCIAL source (BLE company-id + name vectors); new radar app icon (launcher, themed, and notification). - v0.5.4 — 18 IEEE-verified surveillance-vendor OUIs across BLE + WiFi (ShotSpotter, WatchGuard/Motorola, Verkada, Avigilon Alta, Axis, FLIR, Hanwha, March Networks, GeoVision, Mobotix, Sunell); police-exclusive vendors (WatchGuard, ShotSpotter) score ORANGE on sight; vendor-named drill-down labels. - v0.5.5 — Citizen feed confirmed retired upstream; the source now reports the shutdown instead of leaking a JSON parse error, and backs off to a 30-min heartbeat. Toolchain modernized: AGP 9.3.2, Gradle 9.7.1, Kotlin 2.4.10, Compose BOM 2026.08.00, `compileSdk` 37 (`targetSdk` held at 35); CI actions bumped off deprecated Node-20 versions. - v0.5.6 — Waze now calls OpenWeb Ninja directly with **your own API key** instead of a shared proxy token; the `api.blackflagintel.com` proxy is no longer used and the stale token is purged from the secret store on upgrade. Requests add `alert_types=POLICE&max_jams=0` (~18 KB → ~1.5 KB per poll). See [Waze setup](#waze-setup-bring-your-own-api-key). +- v0.5.7 — Citizen source **removed entirely** (client, scanner, scoring, settings, map dots and the drill-down row). Its endpoint now returns HTTP 410 Gone, so there was nothing left to degrade gracefully into. OVERWATCH is now a five-source app: BLE, WiFi, DeFlock, Waze, Commercial. +- v0.5.8 — **AIRCRAFT source**: police / surveillance aircraft overhead via free community ADS-B feeds, matched against a bundled 1,971-entry registry of US law-enforcement airframes (regenerate with `scripts/gen-le-aircraft.py`), plus loiter/orbit detection so unlisted aircraft circling overhead still register. Overpass query widened to speed cameras and generic surveillance nodes, each scored on its own curve. DeFlock/Waze/aircraft all scored by continuous distance falloff. New [SOURCES.md](SOURCES.md) reference. ## License diff --git a/SOURCES.md b/SOURCES.md new file mode 100644 index 0000000..91b52d5 --- /dev/null +++ b/SOURCES.md @@ -0,0 +1,437 @@ +# OVERWATCH — sources, identifiers and endpoints + +Every external data source, every signature the app matches on, and every +scoring table, in one place. Written to be the reference behind a talk, so it +records **what was tried and failed** alongside what works — the dead ends are +usually the more interesting half. + +Dates are when a claim was last verified against the live service. Anything +network-facing rots; re-verify before quoting it. + +--- + +## 1. Detection sources at a glance + +| Source | What it sees | Transport | Cost | Verified | +|---|---|---|---|---| +| **BLE** | Bluetooth-LE advertisements from surveillance hardware | Local radio | free | 2026-09 | +| **WIFI** | BSSIDs + SSIDs of surveillance infrastructure | Local radio | free | 2026-09 | +| **DEFLOCK** | Mapped fixed surveillance: ALPR, speed cameras, CCTV | Overpass / OSM | free | 2026-09-17 | +| **WAZE** | Live crowd-sourced police reports | OpenWeb Ninja | ~$1–3/mo | 2026-09-16 | +| **AIRCRAFT** | Police / surveillance aircraft overhead | ADS-B community feeds | free | 2026-09-17 | +| **COMMERCIAL** | Consumer cameras, voice assistants, smart glasses | Rides BLE + WiFi | free | 2026-09 | + +Two radio sources (passive, local) and three network sources (position-driven). +The app **never transmits** to any detected device — it only listens, and only +queries third-party APIs about coordinates. + +--- + +## 2. Network endpoints + +### 2.1 Overpass / OpenStreetMap — the DEFLOCK source + +``` +POST https://overpass.deflock.org/api/interpreter (preferred) +POST https://overpass-api.de/api/interpreter (fallback) + +[out:json][timeout:25]; +(node["man_made"="surveillance"](S,W,N,E); + node["highway"="speed_camera"](S,W,N,E););out body; +``` + +**DeFlock and OSM are the same dataset.** DeFlock is a crowdsourcing project +whose contributors map ALPR cameras *into OpenStreetMap* as +`man_made=surveillance` + `surveillance:type=ALPR` nodes. There is no separate +DeFlock database to query — the only DeFlock-specific piece is their Overpass +mirror, which the app prefers because it is less rate-limited for this use. +The OSM ALPR registry passed **336,000 nodes** worldwide in early 2026, covering +Flock plus Motorola Vigilant, Axon Fleet and Genetec. + +`man_made=surveillance` is the superset that already contains the ALPR nodes, +so the query does not ask for ALPR separately; the parser classifies by tag. + +Measured node counts in a 5 km bbox (2026-09-17): + +| Area | ALPR | speed_camera | all surveillance | +|---|---|---|---| +| Northern Virginia | 166 | 31 | 189 | +| Manhattan | 292 | 12 | 515 | + +Widening past ALPR roughly doubles the worst case rather than exploding it, +which is why generic cameras are worth carrying. + +- Response cached on disk 24 h, keyed by 0.05° grid cell (`deflock2__`). +- Refetch when the user moves > 1.5 km from the last fetch centre. +- A `200 OK` body containing `{"remark": "...timed out..."}` is treated as a + failure, so an Overpass timeout cannot poison the cache. +- **Dead:** `cdn.deflock.me/regions/*.json` — behind Cloudflare bot mitigation, + unusable from a mobile HTTP client. + +### 2.2 OpenWeb Ninja — the WAZE source + +``` +GET https://api.openwebninja.com/waze/alerts-and-jams + ?bottom_left=,&top_right=, + &max_alerts=200&max_jams=0&alert_types=POLICE +Header: X-API-Key: +``` + +**Bring your own key** — sign up at openwebninja.com, subscribe to the Waze API, +paste the key into Settings. Stored encrypted (Android Keystore AES/GCM via +`SecureStore`); nothing ships in the APK. Pay-as-you-go ≈ **$0.005/request**, +≈ 15 requests/active hour at the ~4-minute poll, so **$1–3/month**. The free +tier's 100 requests/month verifies the integration but will not run it. + +Verified behaviour (2026-09-16): + +- `alert_types=POLICE` **is** honoured server-side now. It was echoed-but-ignored + when the integration was first written, so the client still re-filters by type + — a silent revert must not let other alert types through. +- `max_jams=0` suppresses the jam array. With both parameters a typical response + drops from **~17.9 KB to ~1.5 KB**. +- Auth failure is `HTTP 401 {"message":"Unauthorized"}` for both a missing and an + invalid key. +- `URLEncoder` renders the bbox comma as `%2C`; the API parses that correctly. +- Police alerts very often carry `alert_confidence: 0` and `alert_reliability: 0`, + so the distance floor matters more than the crowd-trust bonuses. + +**Why not scrape Waze directly:** `waze.com/live-map/api/georss` is gated by +reCAPTCHA Enterprise *reputation* scoring. Tested 2026-07 from a residential IP — +plain curl, headless Chromium, **headful** Chromium on a real display, and +undetected-chromedriver all returned **HTTP 403**, including Waze's own page +requests. It scores browser reputation, not automation flags, so no scraper +survives. The Waze for Cities partner feed excludes POLICE and is agency-only. + +### 2.3 ADS-B community networks — the AIRCRAFT source + +``` +GET https://opendata.adsb.fi/api/v2/lat//lon//dist/30 (preferred) +GET https://api.adsb.lol/v2/lat//lon//dist/30 (fallback) +``` + +**No API key, no account.** Both are volunteer data-in/data-out networks running +the same readsb-derived schema, so one parser covers both. + +Community networks matter specifically here: the commercial trackers +(FlightAware, Flightradar24) filter military and sensitive law-enforcement +flights at government request — exactly the traffic this source exists to see. +ADSBexchange, adsb.fi, adsb.lol and airplanes.live do not filter. + +- Envelope is `{"ac":[...]}` on radius queries and `{"aircraft":[...]}` on wider + ones — **both shapes occur**, and reading only one silently returns zero + aircraft. The parser accepts either. +- Fields used: `hex` (ICAO 24-bit address), `flight`, `lat`, `lon`, `alt_baro`, + `gs`, `track`, `r` (registration), `t` (type). +- `alt_baro` is the **string** `"ground"` for parked aircraft, so a naive + integer read reports that as 0 ft. +- Rapid sequential queries across many metros get refused; the app polls once + per minute from one point, well inside courtesy limits. + +### 2.4 Aircraft identity — ADSBexchange `basic-ac-db` + +``` +https://downloads.adsbexchange.com/downloads/basic-ac-db.json.gz +``` + +14.5 MB gzipped, **618,270 aircraft**, newline-delimited JSON, derived from the +FAA registry and **already keyed by ICAO hex** — which is what ADS-B broadcasts, +so no N-number→hex conversion is needed. Fields: `icao`, `reg`, `ownop` +(owner/operator), `icaotype`, `model`, `faa_pia`, `faa_ladd`, `mil`. + +Filtering `ownop` for law-enforcement patterns yields **1,971 US aircraft** +(213 LADD-flagged), bundled as `app/src/main/res/raw/le_aircraft.csv` (~83 KB). +Regenerate with `scripts/gen-le-aircraft.py`. It runs at development time and +commits its output; the app never downloads it. + +Owner-match patterns: `SHERIFF`, `POLICE`, `STATE PATROL`, `HIGHWAY PATROL`, +`STATE TROOPER`, `PUBLIC SAFETY`, `US MARSHAL`, `MARSHALS SERVICE`, `CONSTABLE`, +`DEPT/DEPARTMENT OF CORRECTION`, `BORDER PATROL`, `CUSTOMS AND BORDER`, +`HOMELAND SECURITY`, `DRUG ENFORCEMENT`, `FEDERAL BUREAU OF INVESTIGATION`. + +Excluded to avoid false positives: `MARSHALL SPACE` (NASA), plus `FIRE`, +`AMBULANCE`, `MEDICAL`, `EMS`, `AIR METHODS`, `LIFE FLIGHT`, `MEDEVAC`, +`HOSPITAL` — air-ambulance operators share a lot of vocabulary with police +aviation, and a medevac helicopter is not what this app exists to warn about. + +Top operators by airframe count: DHS 98, US CBP 54, Arizona DPS 35, CHP 24, +Texas DPS 23, US Border Patrol 20. + +**Rejected alternatives:** + +- **FAA releasable registry** (`registry.faa.gov/database/...`) — HTTP **403**, + Akamai bot mitigation. Also keyed by N-number, not hex. +- **OpenSky `aircraftDatabase.csv`** — works, has an `owner` column, but 94 MB + for the same information and *not* hex-keyed as cleanly. 6× the download. +- **plane-alert-db** (`sdr-enthusiasts`) — the obvious community choice, and the + wrong one for US coverage. `plane-alert-pol.csv` holds 996 police aircraft but + only **255 US-registered**; matching all 2,760 of its police+government hexes + against **394 live aircraft** over a 250 nm sweep of Washington DC produced + **zero hits**, while 15 helicopters were aloft. Good for international and + head-of-state airframes, thin for US local police. +- **EFF Atlas of Surveillance** — no aircraft identities. Its only aircraft + dataset is an FAA **drone** registration lookup, last updated **April 2022**, + and drones do not broadcast ADS-B. Still useful as per-agency context + ("what does this department deploy"), not as a live source. + +### 2.5 Retired: Citizen + +``` +GET https://citizen.com/api/incident/trending -> HTTP 410 Gone + {"error":"This endpoint has been removed."} +``` + +Citizen ended the police-dispatch data partnership behind its public feed in +**June 2026**. Timeline: the endpoint first degraded to a silent stub returning +a bare JSON empty string `""` (verified 2026-08-28), then to a formal **410 Gone** +(verified 2026-09-16). `/api/incident/{id}` returns `{}` for every id, real or +invented, and `data.sp0n.io/v1/incidents/trending` — the host the current web app +uses — answers 200 with a **zero-byte body even with no query parameters at all**, +which rules out a parameter-shape mismatch. Every sibling path (`nearby`, +`recent`, `latest`, `map`, `list`, `active`) returns `{}`. + +That is a decommissioned surface, not a changed contract. Structured incident +data is now Citizen's paid Enterprise API only. The source was removed entirely +in v0.5.7 rather than left as a permanently-failing row. + +*(Historical footnote for the talk: passing that empty string to `JSONObject` +throws `Value of type java.lang.String cannot be converted to JSONObject` — +the doubled space in the message is where the empty value interpolates. That +exact string, leaked into the app's UI, is how the shutdown was first noticed.)* + +--- + +## 3. Radio identifiers + +### 3.1 BLE OUIs — surveillance + +`data/targets/BleOuis.kt`. Matched on the lowercased `xx:xx:xx` MAC prefix. + +**Axon** (flagged separately, scores 80): `00:25:df` + +**Flock + supply chain** (24, from flock-detection): +``` +58:8e:81 cc:cc:cc ec:1b:bd 90:35:ea f0:82:c0 1c:34:f1 38:5b:44 94:34:69 +b4:e3:f9 3c:91:80 d8:f3:bc 80:30:49 14:5a:fc 9c:2f:9d 94:08:53 e4:aa:ea +48:e7:29 c8:c9:a3 74:4c:a1 70:c9:4e 04:0d:84 08:3a:88 a4:cf:12 d8:a0:d8 +``` + +### 3.2 WiFi OUIs — Flock infrastructure + +`data/targets/WifiOuis.kt`. 31-prefix superset (flock-you research by +NitekryDPaul + DeFlockJoplin), overlapping flock-detection's 24: +``` +70:c9:4e 3c:91:80 d8:f3:bc 80:30:49 b8:35:32 14:5a:fc 74:4c:a1 08:3a:88 +9c:2f:9d c0:35:32 94:08:53 e4:aa:ea f4:6a:dd f8:a2:d6 24:b2:b9 00:f4:8d +d0:39:57 e8:d0:fc e0:4f:43 b8:1e:a4 70:08:94 58:8e:81 ec:1b:bd 3c:71:bf +58:00:e3 90:35:ea 5c:93:a2 64:6e:69 48:27:ea a4:cf:12 82:6b:f2 +``` + +Android exposes only the BSSID — flock-you's promiscuous-mode `addr1` and +wildcard-probe tricks are not portable to a userspace Android app. + +### 3.3 Vendor OUIs — enterprise / municipal surveillance + +`data/targets/VendorOuis.kt`. All 18 verified against the IEEE registry +(Wireshark `manuf` snapshot, 2026-08-28), so a hit names the manufacturer +directly. Merged into **both** the BLE and WiFi match sets. + +| OUI | Vendor | Radio reality | +|---|---|---| +| `d4:11:d6` | ShotSpotter / SoundThinking | Acoustic gunshot sensors; patents cite WiFi/BT/Zigbee/LPWAN backhaul. **Police-exclusive.** | +| `00:1d:96` | WatchGuard Video (Motorola) | V300 body cams (BT 5.0 + WiFi), 4RE in-car systems run cruiser-local APs. **Police-exclusive.** | +| `e0:a7:00` | Verkada | AD33/AD34 readers advertise BLE for app unlock; SV sensors carry BLE. Cameras are PoE. | +| `70:1a:d5` | Avigilon Alta (Openpath) | Readers use BLE + WiFi (+ UWB) for mobile credentials — constant advertisers. | +| `00:40:8c` `ac:cc:8e` `b8:a4:4f` `e8:27:25` | Axis Communications | W110/W120 **body cams** have BLE 5.1 + dual-band WiFi; fixed cameras are wired. An Axis hit over the air is disproportionately likely to be a body cam. | +| `00:40:7f` `00:1b:d8` | FLIR Systems | Handhelds carry Bluetooth (METERLiNK) + WiFi; fixed thermal is wired. | +| `44:b4:23` `8c:1d:55` `e4:30:22` | Hanwha Vision | Predominantly wired IP video. | +| `00:10:be` `00:12:81` | March Networks | Wired; transit recorders may carry WiFi. | +| `00:13:e2` | GeoVision | Predominantly wired. | +| `00:03:c5` | Mobotix | Predominantly wired. | +| `00:1c:27` | Sunell Electronics | Predominantly wired. | + +**Police-exclusive subset** (`ShotSpotter`, `WatchGuard`) scores 75 on sight — +same rationale as the Axon OUI: these prefixes appear on nothing consumer. + +### 3.4 BLE service UUIDs — Flock Raven (gunshot detection) + +`data/targets/RavenUuids.kt`. 16-bit UUIDs expanded to the Bluetooth base +`0000xxxx-0000-1000-8000-00805f9b34fb`: + +| UUID | Meaning | Firmware | +|---|---|---| +| `180a` | Device Information | all | +| `3100` | GPS Location | 1.2.x+ | +| `3200` | Power Management | 1.2.x+ | +| `3300` | Network Status | 1.2.x+ | +| `3400` | Upload Statistics | 1.3.x | +| `3500` | Error Diagnostics | 1.3.x | +| `1809` | Health Thermometer | 1.1.x | +| `1819` | Location & Navigation | 1.1.x | + +Three or more matching UUIDs scores 90; one or two scores 70. + +### 3.5 Manufacturer-specific data + +`data/targets/Manufacturers.kt`. Company ID **`0x09C8` (XUNTONG)** scores 60; a +`"TN"`-prefixed serial in the payload adds 20. + +The BLE scanner iterates **every** manufacturer-data entry in an advertisement, +not just the first — devices advertise multiple and the target is often not +first in the list. + +### 3.6 Name and SSID patterns + +`data/targets/Patterns.kt`. + +- **BLE local names** (case-sensitive substring): `FS Ext Battery`, `Penguin`, + `Flock`, `Pigvision`, `FlockCam`, `FS-` +- **Penguin numeric**: post-March-2025 firmware advertises a bare 8–12 digit + decimal ID and nothing else — matched as its own weak signal (15). +- **Generic SSID** (case-insensitive): `flock`, `FS_`, `Penguin`, `Pigvision`, + `FlockOS`, `flocksafety`, `FS Ext Battery` +- **Flock SSID format**: `^Flock-[0-9A-Fa-f]{4}$` — the specific form, scored + higher (65) than a generic substring hit (50). + +### 3.7 COMMERCIAL / consumer gear + +`data/targets/MicTargets.kt`. Families: `ECHO`, `RING`, `GOOGLE`, `HIDDEN_CAM`, +`GLASSES`. + +Bluetooth SIG company IDs: + +| ID | Vendor | +|---|---| +| `0x00E0` | Google | +| `0x0171` | Amazon | +| `0x05A7` | Yingxin | +| `0x01AB` | Meta Platforms | +| `0x058E` | Meta Platforms Technologies (Reality Labs / Quest) | +| `0x0D53` | Luxottica — Ray-Ban / Oakley Meta frames | +| `0x03C2` | Snap Inc. — Spectacles | +| `0x060C` | Vuzix | + +`0x004C` (Apple) is **deliberately excluded** — every iPhone and AirPod in range +would match, drowning the signal. + +Hidden-camera OUIs: `fc:b4:67` (Yingxin/SmartLife), `00:e0:4c` (Realtek, in many +cheap cams), `dc:4f:22` (Tuya-affiliated modules), `a4:c1:38` (Telink, common in +cheap BLE mics), `8c:ce:4e` (Shenzhen iComm, frequent in spy-cam BOMs). + +Scores are **capped at 84** so a cluster of doorbells — or a passing pair of +Ray-Bans — can never read as ALPR-grade certainty. RED is reserved for +ALPR/Axon-grade evidence. + +--- + +## 4. Scoring + +Score 0–100 per observation; the on-screen tier is the **maximum live score** +across all sources. Events expire after 5 minutes. + +``` +GREEN < 40 nothing credible +YELLOW 40 – 69 single weak indicator +ORANGE 70 – 84 high confidence +RED 85 + certain +``` + +### 4.1 Distance falloff + +Sources that carry real coordinates are scored on a **continuous falloff over +actual distance**, linearly interpolated between anchors. The anchors are +**absolute metres and deliberately independent of the user's detection-radius +setting**: a camera 200 m away is equally close whether the slider reads 300 m +or 5 km, so moving a setting must never move the threat level. The radius +decides what gets *reported*, never how alarming it is. + +| Distance | ALPR | Speed camera | Generic camera | Waze police | Aircraft | +|---|---|---|---|---|---| +| 0 m | 92 | 75 | 55 | 80 | 88 | +| 50 m | 85 | 70 | 48 | — | — | +| 200 m | 60 | 52 | 38 | 62 | — | +| 600 m | 45 | 40 | 30 | 49 | — | +| 1000 m | — | — | — | — | 80 | +| 1500 m | 30 | 28 | 22 | — | — | +| 3000 m | 22 | 20 | 18 | 28 | 68 | +| 6000 m | — | — | — | — | 55 | +| 15000 m | — | — | — | — | 30 | + +A surveyed ALPR peaks highest and decays fastest — its position is exact and it +cannot see you from a kilometre away. A Waze report is a coarse pin on a car +that may be moving toward you. An aircraft decays slowest of all, because one +orbiting 5 km away is still watching you. + +### 4.2 Modifiers + +- **Waze age decay** — up to −12 points across the 45-minute freshness window. + Police move; a surveyed camera does not. +- **Waze crowd trust** — `reliability ≥ 7` +5, `confidence ≥ 4` +5. +- **Aircraft altitude** — ≤3,000 ft +6; ≤8,000 ft +0; ≤15,000 ft −12; + above −30. A known police airframe at 30,000 ft is an airliner's neighbour, + not an observer. +- **Aircraft loitering** — +10. Surveillance aircraft orbit; airliners and + medevac flights heading somewhere do not. +- **Aircraft LADD** — +4. The operator asked public trackers to hide it. +- **Unidentified aircraft cap** — 69. Behaviour-only evidence never reaches the + "certain" band; circling could still be news or survey work. +- **Multi-method bonus** — +20 when two independent BLE/WiFi methods agree. +- **Strong RSSI** (> −50 dBm) +10; **stationary** (rise-peak-fall) +15. + +### 4.3 Loiter detection + +An aircraft is "loitering" when, over the last 15 minutes of tracked history: +at least **3 samples** spanning at least **4 minutes**, every one within +**5 km** of their own centroid, while below **12,000 ft** and under **200 kt**. + +A registry hit is reported wherever it is, within 15 km. An *unregistered* +aircraft is reported only when it is both loitering **and** within 8 km, so +ordinary traffic passing overhead never raises an alert. + +--- + +## 5. Testing notes + +Emulator GPS is the main obstacle to testing the position-driven sources, and it +has a trap worth recording: + +`adb emu geo fix` appears to do nothing — it returns `OK` and the position never +changes. The real cause is that **nothing has the GPS provider started**. With a +`PRIORITY_BALANCED_POWER_ACCURACY` request, Play services prefers the network +provider and parks the fused provider at `ProviderRequest[OFF]`, so the injected +NMEA has nowhere to land. Once the app requests `PRIORITY_HIGH_ACCURACY` and a +scan is running, `geo fix` lands immediately. + +Two working injection methods, both needing an active GPS consumer: + +```sh +# 1. emulator console — note LON comes first +adb -s emulator-5558 emu geo fix + +# 2. Android test provider — note LAT comes first +adb -s shell appops set --uid 0 android:mock_location allow +adb -s shell cmd location providers add-test-provider gps +adb -s shell cmd location providers set-test-provider-enabled gps true +adb -s shell cmd location providers set-test-provider-location gps --location , +``` + +Wrapped as `scripts/emu-gps.sh [serial] [seconds]`. Always confirm +with `adb shell dumpsys location | grep -m1 "last location"`, which prints the +coordinates **and** an `et=` age — a large age means the fix did not land. A +reboot resets the `appops` grant, and the argument order differs between the two +methods, which is easy to transpose. + +--- + +## 6. Provenance + +Reference projects studied while building (kept under a gitignored `REFERENCES/`): + +- **AxonCadabra** — BLE scanner skeleton. Scan side only; its advertise/fuzz + code is deliberately excluded, since OVERWATCH never transmits. +- **flock-detection** — the confidence-scoring algorithm, RSSI rise-peak-fall + stationary detection, OUIs, UUIDs and name patterns. +- **flock-you** — the 31-OUI WiFi superset. +- **deflock / deflock-app** — the Overpass query shape and proximity-alert + pattern. +- **wazepolice** — the original `live-map/api/georss` recipe, now dead. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1a0f5d..4a3f402 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "org.soulstone.overwatch" minSdk = 26 targetSdk = 35 - versionCode = 22 - versionName = "0.5.6" + versionCode = 24 + versionName = "0.5.8" } // Fixed debug keystore committed to the repo (a debug key is non-secret — its diff --git a/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt b/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt index d3024ba..40cf32a 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt @@ -92,15 +92,11 @@ class MainActivity : ComponentActivity() { val maxScore by DetectionService.store.maxScore.collectAsState() val mapPoints by DetectionService.mapPoints.collectAsState() val userLocation by DetectionService.location.collectAsState() - // Visible map radius = max of the two proximity sliders - // so the user sees the full area where a detection - // can fire. Using the raw setting values regardless of - // enabled-state keeps the visualization stable when a - // source is briefly toggled. - val deflockProx by settings.deflockProximityM.collectAsState() - val citizenProx by settings.citizenProximityM.collectAsState() - val wazeProx by settings.wazeProximityM.collectAsState() - val mapRadiusM = maxOf(deflockProx, citizenProx, wazeProx).toFloat() + // The map shows exactly the radius the sources use — + // one setting, so the circle and the detection area can + // no longer disagree. + val detectionRadius by settings.detectionRadiusM.collectAsState() + val mapRadiusM = detectionRadius.toFloat() val granted by permissionsGranted val denied by permanentlyDenied diff --git a/app/src/main/kotlin/org/soulstone/overwatch/data/location/LocationProvider.kt b/app/src/main/kotlin/org/soulstone/overwatch/data/location/LocationProvider.kt index f81853b..d0acfbd 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/data/location/LocationProvider.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/data/location/LocationProvider.kt @@ -39,8 +39,19 @@ class LocationProvider(private val context: Context) { private val _location = MutableStateFlow(null) val location: StateFlow = _location.asStateFlow() + // HIGH_ACCURACY, not BALANCED. Two reasons: + // - Proximity thresholds go down to 50 m (DeFlock's "very near" band). + // BALANCED resolves via wifi/cell to roughly a city block, which is + // coarser than the distances this app makes decisions at. + // - BALANCED asks Play services for the network provider and never + // escalates to GPS, so on a device with no usable wifi-scan geolocation + // (notably an emulator) the fused provider parks at ProviderRequest[OFF] + // and no fix is ever delivered — every location-driven source then sits + // at "no detections" forever. + // GPS cost is minor next to the continuous SCAN_MODE_LOW_LATENCY BLE scan + // this service already runs, and scanning is explicitly user-started. private val request: LocationRequest = LocationRequest.Builder( - Priority.PRIORITY_BALANCED_POWER_ACCURACY, + Priority.PRIORITY_HIGH_ACCURACY, INTERVAL_MS ) .setMinUpdateIntervalMillis(MIN_INTERVAL_MS) diff --git a/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt b/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt index e31e932..1dbc6c6 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt @@ -32,29 +32,22 @@ class Settings private constructor( private val _deflockEnabled = MutableStateFlow(prefs.getBoolean(KEY_DEFLOCK, true)) val deflockEnabled: StateFlow = _deflockEnabled.asStateFlow() - private val _citizenEnabled = MutableStateFlow(prefs.getBoolean(KEY_CITIZEN, true)) - val citizenEnabled: StateFlow = _citizenEnabled.asStateFlow() - private val _wazeEnabled = MutableStateFlow(prefs.getBoolean(KEY_WAZE, true)) val wazeEnabled: StateFlow = _wazeEnabled.asStateFlow() + private val _aircraftEnabled = MutableStateFlow(prefs.getBoolean(KEY_AIRCRAFT, true)) + val aircraftEnabled: StateFlow = _aircraftEnabled.asStateFlow() + private val _micEnabled = MutableStateFlow(prefs.getBoolean(KEY_MIC, true)) val micEnabled: StateFlow = _micEnabled.asStateFlow() - private val _deflockProximityM = MutableStateFlow( - prefs.getInt(KEY_DEFLOCK_PROX, DEFAULT_DEFLOCK_PROX) + // One radius for every location-driven source (DeFlock + Waze). Two + // separate sliders meant the map had to visualise the larger of them, so + // the circle rarely matched what either source was actually using. + private val _detectionRadiusM = MutableStateFlow( + prefs.getInt(KEY_DETECTION_RADIUS, DEFAULT_DETECTION_RADIUS) ) - val deflockProximityM: StateFlow = _deflockProximityM.asStateFlow() - - private val _citizenProximityM = MutableStateFlow( - prefs.getInt(KEY_CITIZEN_PROX, DEFAULT_CITIZEN_PROX) - ) - val citizenProximityM: StateFlow = _citizenProximityM.asStateFlow() - - private val _wazeProximityM = MutableStateFlow( - prefs.getInt(KEY_WAZE_PROX, DEFAULT_WAZE_PROX) - ) - val wazeProximityM: StateFlow = _wazeProximityM.asStateFlow() + val detectionRadiusM: StateFlow = _detectionRadiusM.asStateFlow() // The user's own OpenWeb Ninja API key for the Waze feed. Stored encrypted // (Keystore), never baked into the APK, so a published build carries no @@ -76,26 +69,14 @@ class Settings private constructor( fun setBleEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_BLE, v) }; _bleEnabled.value = v } fun setWifiEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_WIFI, v) }; _wifiEnabled.value = v } fun setDeflockEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_DEFLOCK, v) }; _deflockEnabled.value = v } - fun setCitizenEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_CITIZEN, v) }; _citizenEnabled.value = v } fun setWazeEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_WAZE, v) }; _wazeEnabled.value = v } + fun setAircraftEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_AIRCRAFT, v) }; _aircraftEnabled.value = v } fun setMicEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_MIC, v) }; _micEnabled.value = v } - fun setDeflockProximityM(v: Int) { - val clamped = v.coerceIn(50, 1600) - prefs.edit { putInt(KEY_DEFLOCK_PROX, clamped) } - _deflockProximityM.value = clamped - } - - fun setCitizenProximityM(v: Int) { - val clamped = v.coerceIn(100, 5000) - prefs.edit { putInt(KEY_CITIZEN_PROX, clamped) } - _citizenProximityM.value = clamped - } - - fun setWazeProximityM(v: Int) { - val clamped = v.coerceIn(100, 5000) - prefs.edit { putInt(KEY_WAZE_PROX, clamped) } - _wazeProximityM.value = clamped + fun setDetectionRadiusM(v: Int) { + val clamped = v.coerceIn(RADIUS_MIN, RADIUS_MAX) + prefs.edit { putInt(KEY_DETECTION_RADIUS, clamped) } + _detectionRadiusM.value = clamped } fun setWazeApiKey(v: String) { @@ -132,21 +113,19 @@ class Settings private constructor( private const val KEY_BLE = "src_ble" private const val KEY_WIFI = "src_wifi" private const val KEY_DEFLOCK = "src_deflock" - private const val KEY_CITIZEN = "src_citizen" private const val KEY_WAZE = "src_waze" + private const val KEY_AIRCRAFT = "src_aircraft" private const val KEY_MIC = "src_mic" - private const val KEY_DEFLOCK_PROX = "deflock_proximity_m" - private const val KEY_CITIZEN_PROX = "citizen_proximity_m" - private const val KEY_WAZE_PROX = "waze_proximity_m" + private const val KEY_DETECTION_RADIUS = "detection_radius_m" private const val KEY_WAZE_API_KEY = "waze_api_key" private const val KEY_LEGACY_WAZE_PROXY_TOKEN = "waze_proxy_token" private const val KEY_THEME = "theme_mode" private const val KEY_VIBRATE = "vibrate_on_alert" private const val KEY_OVERLAY = "overlay_enabled" - const val DEFAULT_DEFLOCK_PROX = 200 - const val DEFAULT_CITIZEN_PROX = 500 - const val DEFAULT_WAZE_PROX = 500 + const val DEFAULT_DETECTION_RADIUS = 500 + const val RADIUS_MIN = 100 + const val RADIUS_MAX = 5000 @Volatile private var INSTANCE: Settings? = null diff --git a/app/src/main/kotlin/org/soulstone/overwatch/data/targets/LeAircraft.kt b/app/src/main/kotlin/org/soulstone/overwatch/data/targets/LeAircraft.kt new file mode 100644 index 0000000..281ee96 --- /dev/null +++ b/app/src/main/kotlin/org/soulstone/overwatch/data/targets/LeAircraft.kt @@ -0,0 +1,66 @@ +package org.soulstone.overwatch.data.targets + +import android.content.Context +import android.util.Log +import org.soulstone.overwatch.R + +/** + * Known law-enforcement aircraft, keyed by the ICAO 24-bit address that every + * ADS-B transmission carries. + * + * Backed by `res/raw/le_aircraft.csv`, generated at development time by + * `scripts/gen-le-aircraft.py` from ADSBexchange's registry-derived database. + * ~1,970 entries, ~83 KB; it ships with the APK and is never downloaded, so + * this source works with no key, no account and no extra network call beyond + * the live position feed itself. + */ +object LeAircraft { + + private const val TAG = "LeAircraft" + + data class Entry( + val icaoHex: String, + val owner: String, + /** FAA "Limiting Aircraft Data Displayed" — the operator asked to be + * hidden from public trackers, which is itself worth knowing. */ + val ladd: Boolean + ) + + @Volatile private var table: Map? = null + + /** Parsed on first use and cached; the file is small enough to hold whole. */ + fun load(context: Context): Map { + table?.let { return it } + return synchronized(this) { + table ?: parse(context).also { table = it } + } + } + + fun lookup(context: Context, icaoHex: String): Entry? = + load(context)[icaoHex.trim().lowercase()] + + private fun parse(context: Context): Map = try { + val out = HashMap(2048) + context.resources.openRawResource(R.raw.le_aircraft).bufferedReader().useLines { lines -> + for (line in lines) { + if (line.isBlank() || line.startsWith("#")) continue + val parts = line.split(',') + if (parts.size < 2) continue + val hex = parts[0].trim().lowercase() + if (hex.length != 6) continue + out[hex] = Entry( + icaoHex = hex, + owner = parts[1].trim(), + ladd = parts.getOrNull(2)?.trim() == "1" + ) + } + } + Log.i(TAG, "Loaded ${out.size} law-enforcement aircraft") + out + } catch (e: Exception) { + // A missing or corrupt asset must not take the scanner down; without + // the table every contact simply falls back to behaviour-only scoring. + Log.w(TAG, "Failed to load aircraft table: ${e.message}") + emptyMap() + } +} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt index cb9ad57..96bbad5 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt @@ -1,5 +1,8 @@ package org.soulstone.overwatch.fusion +import kotlin.math.roundToInt +import org.soulstone.overwatch.scan.DeflockClient + /** * Confidence scoring — port of flock-detection's algorithm with weights from the OVERWATCH plan. * @@ -29,17 +32,15 @@ object ConfidenceEngine { const val W_WIFI_SSID_GENERIC = 50 const val W_WIFI_SSID_FLOCK_FMT = 65 - // Map (Phase 3) - const val W_DEFLOCK_NEAR = 60 // <= 200m - const val W_DEFLOCK_VERY_NEAR = 85 // <= 50m + // DeFlock + Waze are scored by a continuous distance falloff — see the + // FALLOFF tables below. A flat "inside the radius" score made the tier + // meaningless once the radius became user-set up to 5 km: in a city there + // is always an ALPR within a mile, so the app sat on YELLOW permanently. - // Citizen (real-time incident feed) - const val W_CITIZEN_INCIDENT = 55 - const val B_CITIZEN_LEVEL_BUMP = 5 // level >= 2 - const val B_CITIZEN_POLICE_TITLE = 5 // title contains a police-action keyword - - // Waze (live POLICE alerts via the OpenWeb Ninja hosted feed) - const val W_WAZE_POLICE = 55 + /** Waze freshness window — shared with WazeScanner so the two can't drift. */ + const val WAZE_MAX_AGE_MS = 45L * 60L * 1000L + /** Points shed across the full freshness window; police move, old reports rot. */ + const val B_WAZE_AGE_DECAY = 12f // Bonuses const val B_MULTI_METHOD = 20 @@ -78,24 +79,27 @@ object ConfidenceEngine { val isStationary: Boolean = false ) - /** A DeFlock map ALPR observed within proximity threshold. */ + /** A mapped surveillance node observed within the detection radius. */ data class DeflockObservation( val osmId: Long, val distanceMeters: Float, + val kind: DeflockClient.Kind, val operator: String?, val manufacturer: String? ) - /** A Citizen incident observed within proximity + freshness, after the - * fire/medical filter is applied. */ - data class CitizenObservation( - val incidentId: String, + /** An aircraft seen overhead by [org.soulstone.overwatch.scan.AircraftScanner]. */ + data class AircraftObservation( + val icaoHex: String, val distanceMeters: Float, - val ageMs: Long, - val level: Int, // 0-5 severity (Citizen's own scale) - val title: String, - val isPoliceTitled: Boolean, - val precinct: String? + val altitudeFt: Int?, + val isKnownLawEnforcement: Boolean, + val isLoitering: Boolean, + val isLadd: Boolean, + val owner: String?, + val registration: String?, + val aircraftType: String?, + val callsign: String? ) /** A Waze POLICE alert observed within proximity + freshness thresholds. */ @@ -214,47 +218,155 @@ object ConfidenceEngine { return Scored(score, methods.toString().trim(), label, isAxon) } - fun scoreCitizen(obs: CitizenObservation): Scored { - var score = W_CITIZEN_INCIDENT - val tags = StringBuilder("citizen ") - if (obs.level >= 2) { - score += B_CITIZEN_LEVEL_BUMP - tags.append("L${obs.level} ") + fun scoreAircraft(obs: AircraftObservation): Scored { + var score = falloff(obs.distanceMeters, AIRCRAFT_FALLOFF) + val tags = StringBuilder("aircraft ") + + // Altitude is the strongest discriminator after identity: a known + // police airframe at 30,000 ft is an airliner's neighbour, not an + // observer. Penalise height rather than filtering, so a high orbit + // still shows up quietly. + val alt = obs.altitudeFt + when { + alt == null -> Unit + alt <= 3_000 -> { score += 6f; tags.append("very_low ") } + alt <= 8_000 -> tags.append("low ") + alt <= 15_000 -> { score -= 12f; tags.append("mid_alt ") } + else -> { score -= 30f; tags.append("high_alt ") } } - if (obs.isPoliceTitled) { - score += B_CITIZEN_POLICE_TITLE - tags.append("police_title ") + + if (obs.isLoitering) { score += 10f; tags.append("loitering ") } + if (obs.isKnownLawEnforcement) { + tags.append("known_le ") + } else { + // Behaviour-only: cap it. Circling is suggestive, not proof. + score = minOf(score, AIRCRAFT_UNKNOWN_CAP.toFloat()) + tags.append("unidentified ") } - if (!obs.precinct.isNullOrBlank()) tags.append("precinct=${obs.precinct} ") - score = score.coerceAtMost(100) - val ageMin = (obs.ageMs / 60_000L).toInt() - val label = "${obs.title} @ ${obs.distanceMeters.toInt()}m, ${ageMin}min ago" - return Scored(score, tags.toString().trim(), label, isAxon = false) + // The operator asked public trackers to hide this airframe. + if (obs.isLadd) { score += 4f; tags.append("ladd ") } + + val final = score.roundToInt().coerceIn(0, 100) + val who = obs.owner + ?: obs.registration + ?: obs.callsign + ?: "Unidentified aircraft" + val what = obs.aircraftType?.let { " ($it)" } ?: "" + val altText = obs.altitudeFt?.let { ", ${it} ft" } ?: "" + val verb = if (obs.isLoitering) "circling" else "overhead" + val label = "$who$what $verb @ ${obs.distanceMeters.toInt()}m$altText" + tags.append("d=${obs.distanceMeters.toInt()}m hex=${obs.icaoHex}") + return Scored(final, tags.toString().trim(), label, isAxon = false) } fun scoreWaze(obs: WazeObservation): Scored { - // Baseline 55 for any POLICE alert within the proximity + age gate the - // caller already applied. Small crowd-trust nudges for high reliability - // and high confidence, capped well under the multi-method bonus so a - // corroborating BLE/WiFi/DeFlock hit still dominates the global tier. - var score = W_WAZE_POLICE - if (obs.reliability >= 7) score += 5 - if (obs.confidence >= 4) score += 5 - score = score.coerceAtMost(100) - val methods = "waze_police rel=${obs.reliability} conf=${obs.confidence}" + // Distance first, then the two crowd-trust nudges, then age. Kept well + // under the multi-method bonus so a corroborating BLE/WiFi/DeFlock hit + // still dominates the global tier. + var score = falloff(obs.distanceMeters, WAZE_FALLOFF) + if (obs.reliability >= 7) score += 5f + if (obs.confidence >= 4) score += 5f + // Police move; a report at the far end of the freshness window is much + // weaker evidence than one a minute old. + val ageFraction = (obs.ageMs.toFloat() / WAZE_MAX_AGE_MS).coerceIn(0f, 1f) + score -= B_WAZE_AGE_DECAY * ageFraction + val final = score.roundToInt().coerceIn(0, 100) val ageMin = (obs.ageMs / 60_000L).toInt() + val methods = "waze_police d=${obs.distanceMeters.toInt()}m age=${ageMin}min " + + "rel=${obs.reliability} conf=${obs.confidence}" val sub = obs.subtype?.let { " ($it)" } ?: "" val label = "Police report$sub @ ${obs.distanceMeters.toInt()}m, ${ageMin}min ago" - return Scored(score, methods, label, isAxon = false) + return Scored(final, methods, label, isAxon = false) + } + + /** + * Distance falloff anchors, as (metres, score) pairs interpolated linearly + * by [falloff]. + * + * These are **absolute distances, deliberately not a fraction of the user's + * detection radius.** A camera 200 m away is exactly as close whether the + * radius slider reads 300 m or 5 km, so it has to score the same either + * way; keying off the radius would make the threat level move when the + * user touched a setting, which is the opposite of what the number means. + * The radius decides what gets *reported*, never how alarming it is. + * + * A fixed ALPR's position is surveyed and exact, so it stays alarming + * closer in and decays slowly. The 50 m and 200 m values are carried over + * from the old step scoring so calibration at typical distances is + * unchanged. + */ + private val DEFLOCK_FALLOFF = arrayOf( + 0f to 92f, 50f to 85f, 200f to 60f, 600f to 45f, 1500f to 30f, 3000f to 22f + ) + + /** + * A fixed speed camera is enforcement infrastructure, but it only acts on + * you if you're speeding past it — relevant, not alarming. + */ + private val SPEED_CAMERA_FALLOFF = arrayOf( + 0f to 75f, 50f to 70f, 200f to 52f, 600f to 40f, 1500f to 28f, 3000f to 20f + ) + + /** + * A generic `man_made=surveillance` node is as likely to be a shop's CCTV + * as a street camera, so it peaks below the YELLOW line at any real + * distance and never drives the tier on its own. + */ + private val CAMERA_FALLOFF = arrayOf( + 0f to 55f, 50f to 48f, 200f to 38f, 600f to 30f, 1500f to 22f, 3000f to 18f + ) + + /** + * A Waze police report is a crowd-sourced pin on a moving car: coarser + * than a surveyed camera, so it peaks lower and decays faster. 300 m holds + * the old flat baseline of 55. + */ + private val WAZE_FALLOFF = arrayOf( + 0f to 80f, 100f to 70f, 300f to 55f, 800f to 45f, 2000f to 32f, 4000f to 25f + ) + + /** + * Aircraft falloff, over ground distance. Far wider than the ground + * sources because an aircraft orbiting 5 km away is still watching you — + * unlike a camera 5 km away, which cannot see you at all. + */ + private val AIRCRAFT_FALLOFF = arrayOf( + 0f to 88f, 1000f to 80f, 3000f to 68f, 6000f to 55f, 10000f to 42f, 15000f to 30f + ) + + /** An unregistered aircraft is judged on behaviour alone, so it is capped + * below the "certain" band — circling could still be news or survey work. */ + const val AIRCRAFT_UNKNOWN_CAP = 69 + + /** Linear interpolation across [anchors]; clamps outside the first/last. */ + private fun falloff(distanceMeters: Float, anchors: Array>): Float { + val d = distanceMeters.coerceAtLeast(0f) + if (d <= anchors.first().first) return anchors.first().second + for (i in 0 until anchors.size - 1) { + val (d0, s0) = anchors[i] + val (d1, s1) = anchors[i + 1] + if (d <= d1) return s0 + (d - d0) / (d1 - d0) * (s1 - s0) + } + return anchors.last().second } fun scoreDeflock(obs: DeflockObservation): Scored { - val score = if (obs.distanceMeters <= 50f) W_DEFLOCK_VERY_NEAR else W_DEFLOCK_NEAR - val rangeTag = if (obs.distanceMeters <= 50f) "deflock<=50m" else "deflock<=200m" + val d = obs.distanceMeters + val (curve, fallbackName, tag) = when (obs.kind) { + DeflockClient.Kind.ALPR -> Triple(DEFLOCK_FALLOFF, "ALPR", "alpr") + DeflockClient.Kind.SPEED_CAMERA -> + Triple(SPEED_CAMERA_FALLOFF, "Speed camera", "speed_cam") + DeflockClient.Kind.CAMERA -> + Triple(CAMERA_FALLOFF, "Surveillance camera", "camera") + } + val score = falloff(d, curve).roundToInt().coerceIn(0, 100) val descriptor = listOfNotNull(obs.manufacturer, obs.operator) - .joinToString(" / ").ifBlank { "ALPR" } - val label = "%s @ %dm (osm:%d)".format(descriptor, obs.distanceMeters.toInt(), obs.osmId) - return Scored(score, rangeTag, label, isAxon = false) + .joinToString(" / ").ifBlank { fallbackName } + // The OSM id rides in the methods line rather than the label: it is + // what you need to look a camera up, and nothing you want shouting + // from the one-line status on the main screen. + val label = "%s @ %dm".format(descriptor, d.toInt()) + return Scored(score, "$tag d=${d.toInt()}m osm:${obs.osmId}", label, isAxon = false) } /** A BLE mic-bearing-device observation, score-capped at ORANGE. */ diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/DetectionEvent.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/DetectionEvent.kt index a64dec9..3ea97e3 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/DetectionEvent.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/DetectionEvent.kt @@ -4,12 +4,12 @@ package org.soulstone.overwatch.fusion * One observation from one source at one moment. * * @param source which scanner produced this - * @param key stable per-device identifier (MAC for BLE/WiFi, OSM id for DeFlock, uuid for Citizen) + * @param key stable per-device identifier (MAC for BLE/WiFi, OSM id for DeFlock, alert id for Waze) * @param label short human-readable description shown in the drill-down ("Axon body cam", "FS-1A2B") * @param score 0-100 confidence assigned by the engine * @param matchedMethods space-separated short tags for what triggered ("axon_oui mfg_0x09C8 tn_serial") - * @param rssi signal strength if applicable (BLE/WiFi); null for map/Citizen sources - * @param lat / lon real-world coordinates for events that have them (DEFLOCK, CITIZEN); null for radio-only sources + * @param rssi signal strength if applicable (BLE/WiFi); null for map/feed sources + * @param lat / lon real-world coordinates for events that have them (DEFLOCK, WAZE); null for radio-only sources * @param timestampMs wall-clock millis when this event was produced */ data class DetectionEvent( diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt index 476af2e..98a7e68 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt @@ -26,23 +26,23 @@ object SourceHealth { private val _ble = MutableStateFlow(Health()) private val _wifi = MutableStateFlow(Health()) private val _deflock = MutableStateFlow(Health()) - private val _citizen = MutableStateFlow(Health()) private val _waze = MutableStateFlow(Health()) + private val _aircraft = MutableStateFlow(Health()) private val _mic = MutableStateFlow(Health()) val ble: StateFlow = _ble.asStateFlow() val wifi: StateFlow = _wifi.asStateFlow() val deflock: StateFlow = _deflock.asStateFlow() - val citizen: StateFlow = _citizen.asStateFlow() val waze: StateFlow = _waze.asStateFlow() + val aircraft: StateFlow = _aircraft.asStateFlow() val mic: StateFlow = _mic.asStateFlow() fun flowFor(source: DetectionSource): StateFlow = when (source) { DetectionSource.BLE -> ble DetectionSource.WIFI -> wifi DetectionSource.DEFLOCK -> deflock - DetectionSource.CITIZEN -> citizen DetectionSource.WAZE -> waze + DetectionSource.AIRCRAFT -> aircraft DetectionSource.MIC -> mic } @@ -51,8 +51,8 @@ object SourceHealth { DetectionSource.BLE -> _ble DetectionSource.WIFI -> _wifi DetectionSource.DEFLOCK -> _deflock - DetectionSource.CITIZEN -> _citizen DetectionSource.WAZE -> _waze + DetectionSource.AIRCRAFT -> _aircraft DetectionSource.MIC -> _mic } target.value = Health( @@ -66,8 +66,8 @@ object SourceHealth { _ble.value = Health() _wifi.value = Health() _deflock.value = Health() - _citizen.value = Health() _waze.value = Health() + _aircraft.value = Health() _mic.value = Health() } } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt index b30a560..52c5541 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt @@ -21,4 +21,4 @@ enum class ThreatLevel(val minScore: Int) { } /** Logical signal channel — used in the drill-down UI. */ -enum class DetectionSource { BLE, WIFI, DEFLOCK, CITIZEN, WAZE, MIC } +enum class DetectionSource { BLE, WIFI, DEFLOCK, WAZE, AIRCRAFT, MIC } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftClient.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftClient.kt new file mode 100644 index 0000000..50b1d34 --- /dev/null +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftClient.kt @@ -0,0 +1,146 @@ +package org.soulstone.overwatch.scan + +import android.util.Log +import java.net.HttpURLConnection +import java.net.URL +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +/** + * Live aircraft positions from the community ADS-B networks. + * + * GET https://opendata.adsb.fi/api/v2/lat//lon//dist/ + * GET https://api.adsb.lol/v2/lat//lon//dist/ (fallback) + * + * **No API key, no account.** Both are volunteer, data-in/data-out networks + * running the same readsb-derived schema, so one parser covers both and either + * can carry the source alone — the same belt-and-braces shape as the two + * Overpass endpoints in [DeflockClient]. + * + * Community networks matter here specifically: the commercial trackers + * (FlightAware, Flightradar24) filter military and sensitive law-enforcement + * flights at government request, which is exactly the traffic this source + * exists to see. These don't filter. + * + * Response is `{ "ac": [...] }` (adsb.fi) or `{ "aircraft": [...] }` (wider + * queries and adsb.lol) — verified both shapes live 2026-09-17, and the parser + * accepts either because reading only one silently returns zero aircraft. + * Fields used: `hex` (the ICAO address matched against [LeAircraft]), `flight`, + * `lat`, `lon`, `alt_baro`, `gs`, `track`, `r` (registration), `t` (type). + */ +class AircraftClient { + + companion object { + private const val TAG = "AircraftClient" + private const val TIMEOUT_MS = 15_000 + private const val USER_AGENT = "OVERWATCH/0.5 (+github.com/KaraZajac/OVERWATCH)" + /** Query radius. Aircraft are fast and visible from far off, so this is + * deliberately much wider than any ground-source radius; the scanner + * decides what is actually close enough to matter. */ + const val QUERY_RADIUS_NM = 30 + } + + data class Contact( + val icaoHex: String, + val callsign: String?, + val registration: String?, + val type: String?, + val lat: Double, + val lon: Double, + /** Barometric altitude in feet; null when the aircraft reports "ground". */ + val altitudeFt: Int?, + val groundSpeedKt: Double?, + val trackDeg: Double? + ) + + sealed class FetchResult { + data class Success(val contacts: List) : FetchResult() + data class Failed(val reason: String) : FetchResult() + } + + private val endpoints = listOf( + "https://opendata.adsb.fi/api/v2", + "https://api.adsb.lol/v2" + ) + + suspend fun fetchAround(lat: Double, lon: Double): FetchResult = withContext(Dispatchers.IO) { + var lastError: String? = null + for (base in endpoints) { + val url = "$base/lat/${"%.4f".format(lat)}/lon/${"%.4f".format(lon)}/dist/$QUERY_RADIUS_NM" + when (val r = get(url)) { + is Raw.Ok -> return@withContext FetchResult.Success(parse(r.body)) + is Raw.Err -> lastError = r.reason + } + } + FetchResult.Failed(lastError ?: "No ADS-B endpoint reachable") + } + + private sealed class Raw { + data class Ok(val body: String) : Raw() + data class Err(val reason: String) : Raw() + } + + private fun get(url: String): Raw { + val conn = (URL(url).openConnection() as HttpURLConnection).apply { + connectTimeout = TIMEOUT_MS + readTimeout = TIMEOUT_MS + requestMethod = "GET" + setRequestProperty("User-Agent", USER_AGENT) + setRequestProperty("Accept", "application/json") + } + return try { + val code = conn.responseCode + if (code in 200..299) { + Raw.Ok(conn.inputStream.bufferedReader().use { it.readText() }) + } else { + Log.w(TAG, "$url returned $code") + Raw.Err("HTTP $code") + } + } catch (e: Exception) { + Log.w(TAG, "$url failed: ${e.message}") + Raw.Err(e.message ?: e.javaClass.simpleName) + } finally { + conn.disconnect() + } + } + + private fun parse(body: String): List { + if (body.isBlank()) return emptyList() + return try { + val root = JSONObject(body) + val arr = root.optJSONArray("ac") ?: root.optJSONArray("aircraft") ?: return emptyList() + val out = ArrayList(arr.length()) + for (i in 0 until arr.length()) { + val a = arr.optJSONObject(i) ?: continue + val hex = a.optString("hex").trim().lowercase() + if (hex.length != 6) continue + val lat = a.optDouble("lat", Double.NaN) + val lon = a.optDouble("lon", Double.NaN) + if (lat.isNaN() || lon.isNaN()) continue + out.add( + Contact( + icaoHex = hex, + callsign = a.optString("flight").trim().ifBlank { null }, + registration = a.optString("r").trim().ifBlank { null }, + type = a.optString("t").trim().ifBlank { null }, + lat = lat, + lon = lon, + // alt_baro is "ground" (a string) for parked aircraft, + // so optInt would quietly turn that into 0 ft. + // alt_baro is the string "ground" for parked aircraft, + // so optInt would quietly report that as 0 ft. + altitudeFt = if (a.opt("alt_baro") is Number) a.optInt("alt_baro") else null, + groundSpeedKt = a.optDouble("gs", Double.NaN) + .takeIf { !it.isNaN() }, + trackDeg = a.optDouble("track", Double.NaN).takeIf { !it.isNaN() } + ) + ) + } + out + } catch (e: Exception) { + Log.w(TAG, "Failed to parse ADS-B response: ${e.message}") + emptyList() + } + } +} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftScanner.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftScanner.kt new file mode 100644 index 0000000..9ba0419 --- /dev/null +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/AircraftScanner.kt @@ -0,0 +1,195 @@ +package org.soulstone.overwatch.scan + +import android.content.Context +import android.location.Location +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.soulstone.overwatch.data.location.LocationProvider +import org.soulstone.overwatch.data.targets.LeAircraft +import org.soulstone.overwatch.fusion.ConfidenceEngine +import org.soulstone.overwatch.fusion.DetectionEvent +import org.soulstone.overwatch.fusion.DetectionSource +import org.soulstone.overwatch.fusion.DetectionStore +import org.soulstone.overwatch.fusion.SourceHealth + +/** + * Watches overhead air traffic for surveillance aircraft. + * + * Two independent signals, because either alone misses real cases: + * + * 1. **Registry match** — the contact's ICAO address is in [LeAircraft]. + * Authoritative when it fires, but the bundled list only covers aircraft + * whose registered owner names them: measured 2026-09-17, a 250 nm sweep of + * Washington DC returned 394 aircraft and matched none of 2,760 community + * police/government hexes, while 15 helicopters were aloft. + * + * 2. **Loiter/orbit behaviour** — surveillance aircraft circle; airliners, + * and medevac flights heading somewhere, do not. An aircraft that stays + * inside [LOITER_RADIUS_M] of its own track centroid across several + * minutes, low and slow, is behaving like a surveillance asset whatever + * the registry says. This is what catches the unlisted ones. + * + * A registry hit is reported wherever it is; an unregistered aircraft is only + * reported when it is both loitering *and* close, so ordinary traffic passing + * overhead never raises an alert. + */ +class AircraftScanner( + private val context: Context, + private val store: DetectionStore, + private val locationProvider: LocationProvider, + private val client: AircraftClient = AircraftClient() +) { + + companion object { + private const val TAG = "AircraftScanner" + /** Aircraft move fast, but the feed is a courtesy — don't hammer it. */ + private const val POLL_INTERVAL_MS = 60_000L + + /** Ignore anything further out than this; the query itself is wider. */ + private const val MAX_RANGE_M = 15_000f + + /** An unregistered aircraft has to be at least this close to report. */ + private const val UNKNOWN_MAX_RANGE_M = 8_000f + + /** Above this, it's transiting airspace, not watching anything. */ + private const val LOW_ALTITUDE_FT = 12_000 + + /** Surveillance orbits are slow; airliners are not. */ + private const val SLOW_GROUND_SPEED_KT = 200.0 + + /** Track history older than this is dropped. */ + private const val HISTORY_TTL_MS = 15L * 60L * 1000L + + /** Loiter test: samples must span at least this long… */ + private const val LOITER_MIN_SPAN_MS = 4L * 60L * 1000L + + /** …and stay within this radius of their own centroid. */ + private const val LOITER_RADIUS_M = 5_000f + + private const val LOITER_MIN_SAMPLES = 3 + } + + private data class Sample(val lat: Double, val lon: Double, val atMs: Long) + + private var job: Job? = null + /** Per-aircraft recent track, for the loiter test. Cleared on stop. */ + private val history = mutableMapOf>() + + fun start(scope: CoroutineScope): Boolean { + if (job != null) return true + job = scope.launch { + locationProvider.location.first { it != null } + while (isActive) { + locationProvider.location.value?.let { pollOnce(it) } + delay(POLL_INTERVAL_MS) + } + } + Log.i(TAG, "AircraftScanner started (interval=${POLL_INTERVAL_MS}ms)") + return true + } + + fun stop() { + job?.cancel() + job = null + history.clear() + Log.i(TAG, "AircraftScanner stopped") + } + + private suspend fun pollOnce(fix: Location) { + when (val result = client.fetchAround(fix.latitude, fix.longitude)) { + is AircraftClient.FetchResult.Failed -> SourceHealth.record( + DetectionSource.AIRCRAFT, ok = false, + message = "ADS-B feed unreachable: ${result.reason}" + ) + is AircraftClient.FetchResult.Success -> { + SourceHealth.record(DetectionSource.AIRCRAFT, ok = true) + handle(fix, result.contacts) + } + } + } + + private fun handle(fix: Location, contacts: List) { + val now = System.currentTimeMillis() + recordHistory(contacts, now) + + val out = FloatArray(1) + for (c in contacts) { + Location.distanceBetween(fix.latitude, fix.longitude, c.lat, c.lon, out) + val ground = out[0] + if (ground > MAX_RANGE_M) continue + + val known = LeAircraft.lookup(context, c.icaoHex) + val low = (c.altitudeFt ?: Int.MAX_VALUE) <= LOW_ALTITUDE_FT + val slow = (c.groundSpeedKt ?: Double.MAX_VALUE) <= SLOW_GROUND_SPEED_KT + val loitering = low && slow && isLoitering(c.icaoHex, now) + + // An unknown aircraft is only interesting if it is behaving like a + // surveillance asset AND is overhead; otherwise it's just traffic. + if (known == null && !(loitering && ground <= UNKNOWN_MAX_RANGE_M)) continue + + val obs = ConfidenceEngine.AircraftObservation( + icaoHex = c.icaoHex, + distanceMeters = ground, + altitudeFt = c.altitudeFt, + isKnownLawEnforcement = known != null, + isLoitering = loitering, + isLadd = known?.ladd == true, + owner = known?.owner, + registration = c.registration, + aircraftType = c.type, + callsign = c.callsign + ) + val scored = ConfidenceEngine.scoreAircraft(obs) + store.submit( + DetectionEvent( + source = DetectionSource.AIRCRAFT, + key = "air:${c.icaoHex}", + label = scored.label, + score = scored.score, + matchedMethods = scored.methods, + rssi = null, + lat = c.lat, + lon = c.lon + ) + ) + } + } + + private fun recordHistory(contacts: List, now: Long) { + for (c in contacts) { + history.getOrPut(c.icaoHex) { mutableListOf() } + .add(Sample(c.lat, c.lon, now)) + } + val cutoff = now - HISTORY_TTL_MS + val it = history.entries.iterator() + while (it.hasNext()) { + val e = it.next() + e.value.removeAll { s -> s.atMs < cutoff } + if (e.value.isEmpty()) it.remove() + } + } + + /** + * True when the aircraft's recent track stays bunched around its own + * centroid — i.e. it is going round rather than going somewhere. + */ + private fun isLoitering(hex: String, now: Long): Boolean { + val samples = history[hex] ?: return false + if (samples.size < LOITER_MIN_SAMPLES) return false + val span = now - samples.first().atMs + if (span < LOITER_MIN_SPAN_MS) return false + val cLat = samples.sumOf { it.lat } / samples.size + val cLon = samples.sumOf { it.lon } / samples.size + val out = FloatArray(1) + for (s in samples) { + Location.distanceBetween(cLat, cLon, s.lat, s.lon, out) + if (out[0] > LOITER_RADIUS_M) return false + } + return true + } +} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenClient.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenClient.kt deleted file mode 100644 index 619842f..0000000 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenClient.kt +++ /dev/null @@ -1,168 +0,0 @@ -package org.soulstone.overwatch.scan - -import android.util.Log -import java.net.HttpURLConnection -import java.net.URL -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject - -/** - * Public Citizen.com endpoints (originally verified 2026-04-29): - * - * GET /api/incident/trending?lowerLatitude=&upperLatitude=&lowerLongitude=&upperLongitude=&limit=20 - * → { "results": ["", ...] } - * - * GET /api/incident/{id} - * → { "title", "level", "ll": [lat, lon], "ts" (ms), "police", "raw", ... } - * - * No auth, no rate-limit headers observed. Be a good citizen (heh) — only fetch - * detail for IDs we haven't already seen. - * - * **Feed retired upstream — re-verified 2026-08-28.** Citizen ended the - * police-dispatch data partnership behind this feed in June 2026 and stubbed the - * public endpoints. They now answer HTTP 200 with no usable payload: - * - `/api/incident/trending` → a bare JSON empty string (`""`) - * - `/api/incident/{id}` → `{}` for every id, real or invented - * - `data.sp0n.io/v1/incidents/trending` (the host the current web app uses) - * → 200 with a zero-byte body, even with no query params at all - * Every sibling path (`nearby`, `recent`, `latest`, `map`, `list`, `active`) - * returns `{}`, so this is a decommissioned surface, not a changed contract. - * Structured incident data is now Citizen's paid Enterprise API only. - * - * That `""` is why this source used to surface a raw Java parse error: passing - * an empty JSON string to JSONObject throws "Value of type java.lang.String - * cannot be converted to JSONObject". [TrendingResult.Retired] now models the - * stub explicitly so the UI can say what actually happened, and so the scanner - * can back off instead of polling a dead endpoint every 60 s. - */ -class CitizenClient { - - companion object { - private const val TAG = "CitizenClient" - private const val BASE = "https://citizen.com/api/incident" - private const val USER_AGENT = - "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/121.0.0.0 Mobile Safari/537.36" - private const val TIMEOUT_MS = 10_000 - - /** Bounding-box half-width in degrees — ~5.5 km N-S, varies E-W. */ - private const val BBOX_HALF_DEG = 0.05 - private const val LIMIT = 30 - } - - data class Incident( - val id: String, - val title: String, - val level: Int, - val lat: Double, - val lon: Double, - val pubMillis: Long, - val precinct: String? - ) - - sealed class TrendingResult { - data class Success(val ids: List) : TrendingResult() - /** HTTP 200 carrying no usable payload — the retired-stub signature - * described in the class KDoc. Distinct from [Failed] (a network or - * HTTP error, which may be transient) and from a [Success] holding an - * empty list (feed alive, genuinely nothing nearby). */ - object Retired : TrendingResult() - data class Failed(val reason: String) : TrendingResult() - } - - suspend fun trendingNear(lat: Double, lon: Double): TrendingResult = withContext(Dispatchers.IO) { - val top = lat + BBOX_HALF_DEG - val bottom = lat - BBOX_HALF_DEG - val left = lon - BBOX_HALF_DEG - val right = lon + BBOX_HALF_DEG - val url = URL( - "$BASE/trending?lowerLatitude=$bottom&upperLatitude=$top" + - "&lowerLongitude=$left&upperLongitude=$right&limit=$LIMIT" - ) - when (val raw = httpGetJson(url)) { - is RawResult.Success -> parseTrending(raw.body) - is RawResult.Failed -> TrendingResult.Failed(raw.reason) - } - } - - /** - * A well-formed object carrying a `results` array is the only shape that - * counts as a live feed. Anything else the stubbed endpoint can hand back — - * an empty body, a bare JSON string, a bare `{}`, or unparseable text — is - * reported as [TrendingResult.Retired] rather than thrown, so a dead - * upstream never surfaces as a JSON exception in the drill-down. - */ - private fun parseTrending(body: String): TrendingResult { - val trimmed = body.trim() - if (trimmed.isEmpty()) return TrendingResult.Retired - val root = try { - JSONObject(trimmed) - } catch (e: Exception) { - Log.w(TAG, "trending returned non-object payload (${trimmed.take(40)})") - return TrendingResult.Retired - } - // Key absent → stub. Key present but empty → feed alive, nothing nearby. - val arr = root.optJSONArray("results") ?: return TrendingResult.Retired - val out = ArrayList(arr.length()) - for (i in 0 until arr.length()) { - val id = arr.optString(i) - if (id.isNotBlank()) out.add(id) - } - return TrendingResult.Success(out) - } - - /** Returns null on any failure (parse, network, missing fields). */ - suspend fun fetchIncident(id: String): Incident? = withContext(Dispatchers.IO) { - val url = URL("$BASE/$id") - val body = (httpGetJson(url) as? RawResult.Success)?.body ?: return@withContext null - try { - val o = JSONObject(body) - val ll = o.optJSONArray("ll") - val lat = ll?.optDouble(0) ?: o.optDouble("latitude") - val lon = ll?.optDouble(1) ?: o.optDouble("longitude") - if (lat.isNaN() || lon.isNaN()) return@withContext null - Incident( - id = id, - title = o.optString("title").ifBlank { "Citizen incident" }, - level = o.optInt("level", 0), - lat = lat, - lon = lon, - pubMillis = o.optLong("ts", System.currentTimeMillis()), - precinct = o.optString("police").ifBlank { null } - ) - } catch (e: Exception) { - Log.w(TAG, "Failed to parse Citizen incident $id: ${e.message}") - null - } - } - - private sealed class RawResult { - data class Success(val body: String) : RawResult() - data class Failed(val reason: String) : RawResult() - } - - private fun httpGetJson(url: URL): RawResult { - val conn = (url.openConnection() as HttpURLConnection).apply { - connectTimeout = TIMEOUT_MS - readTimeout = TIMEOUT_MS - requestMethod = "GET" - setRequestProperty("User-Agent", USER_AGENT) - setRequestProperty("Accept", "application/json,*/*") - } - return try { - val code = conn.responseCode - if (code in 200..299) { - RawResult.Success(conn.inputStream.bufferedReader().use { it.readText() }) - } else { - Log.w(TAG, "$url returned $code") - RawResult.Failed("HTTP $code") - } - } catch (e: Exception) { - Log.w(TAG, "$url failed: ${e.message}") - RawResult.Failed(e.message ?: e.javaClass.simpleName) - } finally { - conn.disconnect() - } - } -} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenScanner.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenScanner.kt deleted file mode 100644 index 69f8f96..0000000 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/CitizenScanner.kt +++ /dev/null @@ -1,195 +0,0 @@ -package org.soulstone.overwatch.scan - -import android.location.Location -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.soulstone.overwatch.data.location.LocationProvider -import org.soulstone.overwatch.fusion.ConfidenceEngine -import org.soulstone.overwatch.fusion.DetectionEvent -import org.soulstone.overwatch.fusion.DetectionSource -import org.soulstone.overwatch.fusion.DetectionStore -import org.soulstone.overwatch.fusion.SourceHealth - -/** - * Polls Citizen.com for nearby active incidents, filters out pure fire/medical - * (no police presence implied), and submits a detection event for each - * remaining incident inside [proximityMeters] and younger than [MAX_AGE_MS]. - * - * Detail responses are cached in-memory by incident id for the life of the - * scanner — Citizen incidents don't mutate after creation, so we only need to - * fetch each id once per session. - */ -class CitizenScanner( - private val store: DetectionStore, - private val locationProvider: LocationProvider, - private val client: CitizenClient = CitizenClient(), - private val proximityMeters: () -> Float = { 500f } -) { - - companion object { - private const val TAG = "CitizenScanner" - private const val POLL_INTERVAL_MS = 60_000L - /** Poll interval once the feed has answered with the retired-stub shape. - * Citizen shut the public feed down in June 2026 (see [CitizenClient]), - * so this is almost certainly permanent — but it's cheap to keep a slow - * heartbeat so the source recovers on its own if it ever returns. */ - private const val RETIRED_RETRY_MS = 30L * 60L * 1000L - private const val MAX_AGE_MS = 30L * 60L * 1000L - - /** Skip incidents whose title is purely fire/medical with no police implication. */ - private val FIRE_MEDICAL_RX = Regex( - "\\b(fire|smoke|gas\\s+(odor|leak)|medical|cardiac|ambulance|" + - "ems|injury|alarm|odor)\\b", - RegexOption.IGNORE_CASE - ) - - /** Title contains an explicit police-action keyword → score bump. */ - private val POLICE_TITLE_RX = Regex( - "\\b(police|officer|patrol|arrest|swat|tactical|raid|pursuit|" + - "stop|search\\s+warrant)\\b", - RegexOption.IGNORE_CASE - ) - } - - private var job: Job? = null - /** Detail cache for the lifetime of one start/stop cycle. */ - private val incidentCache = mutableMapOf() - /** Set once the feed answers with the retired-stub shape; slows the poll. */ - private var feedRetired = false - - fun start(scope: CoroutineScope): Boolean { - if (job != null) return true - job = scope.launch { - // Wait for the first non-null location fix so the first poll fires - // immediately when location arrives, instead of after a 60 s delay. - locationProvider.location.first { it != null } - while (isActive) { - val fix = locationProvider.location.value - if (fix != null) pollOnce(fix) - delay(if (feedRetired) RETIRED_RETRY_MS else POLL_INTERVAL_MS) - } - } - Log.i(TAG, "CitizenScanner started (interval=${POLL_INTERVAL_MS}ms)") - return true - } - - fun stop() { - job?.cancel() - job = null - incidentCache.clear() - feedRetired = false - Log.i(TAG, "CitizenScanner stopped") - } - - private suspend fun pollOnce(fix: Location) { - when (val trending = client.trendingNear(fix.latitude, fix.longitude)) { - is CitizenClient.TrendingResult.Retired -> { - // Upstream shutdown, not a fault on this device — say so plainly - // so the row doesn't read like a fixable connectivity problem. - if (!feedRetired) { - feedRetired = true - Log.w(TAG, "Citizen public feed retired — backing off to ${RETIRED_RETRY_MS}ms") - } - SourceHealth.record( - DetectionSource.CITIZEN, - ok = false, - message = "Citizen public feed retired upstream (June 2026) — no data available" - ) - return - } - is CitizenClient.TrendingResult.Failed -> { - SourceHealth.record( - DetectionSource.CITIZEN, - ok = false, - message = "Citizen unreachable: ${trending.reason}" - ) - return - } - is CitizenClient.TrendingResult.Success -> { - feedRetired = false - SourceHealth.record(DetectionSource.CITIZEN, ok = true) - handleIds(fix, trending.ids) - } - } - } - - private suspend fun handleIds(fix: Location, ids: List) { - // Drop cache entries that no longer appear in the trending list (resolved). - incidentCache.keys.retainAll(ids.toSet()) - - // Fetch any ids we haven't seen yet — Citizen incidents don't mutate, - // so a single fetch per id per session is enough. - for (id in ids) { - if (incidentCache[id] == null) { - client.fetchIncident(id)?.also { incidentCache[id] = it } - } - } - - emitProximityEvents(fix, ids.mapNotNull { incidentCache[it] }) - } - - /** - * Re-evaluate the cached incident set against the current proximity + age - * thresholds and the latest fix, *without* a network refetch. Used when - * the user moves the proximity slider — events outside a tightened radius - * would otherwise linger and detections inside a widened radius wouldn't - * appear until the next poll cycle. - */ - fun refresh() { - val fix = locationProvider.location.value ?: return - store.clearSource(DetectionSource.CITIZEN) - emitProximityEvents(fix, incidentCache.values.toList()) - } - - private fun emitProximityEvents(fix: Location, incidents: Collection) { - val now = System.currentTimeMillis() - val limit = proximityMeters() - val out = FloatArray(1) - - for (incident in incidents) { - // Title-based pre-filter: drop pure fire/medical events. - if (FIRE_MEDICAL_RX.containsMatchIn(incident.title) && - !POLICE_TITLE_RX.containsMatchIn(incident.title)) { - continue - } - - val age = now - incident.pubMillis - if (age > MAX_AGE_MS) continue - Location.distanceBetween( - fix.latitude, fix.longitude, - incident.lat, incident.lon, - out - ) - val dist = out[0] - if (dist > limit) continue - - val obs = ConfidenceEngine.CitizenObservation( - incidentId = incident.id, - distanceMeters = dist, - ageMs = age, - level = incident.level, - title = incident.title, - isPoliceTitled = POLICE_TITLE_RX.containsMatchIn(incident.title), - precinct = incident.precinct - ) - val scored = ConfidenceEngine.scoreCitizen(obs) - store.submit( - DetectionEvent( - source = DetectionSource.CITIZEN, - key = "citizen:${incident.id}", - label = scored.label, - score = scored.score, - matchedMethods = scored.methods, - rssi = null, - lat = incident.lat, - lon = incident.lon - ) - ) - } - } -} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt index 04221c3..6e493b5 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt @@ -17,9 +17,16 @@ import org.json.JSONObject * gated behind Cloudflare bot mitigation that we cannot pass from a mobile HTTP * client. * + * Covers three kinds of mapped fixed surveillance, not just ALPR — see [Kind]. + * Measured node counts in a 5 km bbox (2026-09-17): northern Virginia + * ALPR 166 / speed 31 / all-surveillance 189; Manhattan ALPR 292 / speed 12 / + * all-surveillance 515. So pulling every `man_made=surveillance` node roughly + * doubles the worst-case payload rather than exploding it, which is why the + * generic camera class is worth carrying. + * * Strategy: - * - POST an Overpass-QL query for `man_made=surveillance + surveillance:type=ALPR` - * inside a small bbox around the user. + * - POST one Overpass-QL union for `man_made=surveillance` (ALPR is a subtype) + * and `highway=speed_camera` inside a small bbox around the user. * - Try `overpass.deflock.org` first (less rate-limited for this use case), * fall back to public `overpass-api.de`. * - Cache the JSON response on disk by 0.05° grid cell (24h TTL). Revisits to @@ -40,17 +47,28 @@ class DeflockClient(context: Context) { ) } - data class AlprPoint( + /** + * What a mapped node actually is. These are scored very differently: an + * ALPR reads and records your plate into a searchable network, a speed + * camera is fixed enforcement that only matters if you're speeding, and a + * generic `man_made=surveillance` node is as likely to be a shop's CCTV as + * anything aimed at the street. Lumping them together would let a corner + * store's camera raise the same alarm as a Flock installation. + */ + enum class Kind { ALPR, SPEED_CAMERA, CAMERA } + + data class SurveillancePoint( val id: Long, val lat: Double, val lon: Double, + val kind: Kind = Kind.ALPR, val operator: String? = null, val manufacturer: String? = null ) /** Outcome of a fetch — distinguishes "no ALPRs in area" from "couldn't reach the API." */ sealed class FetchResult { - data class Success(val points: List) : FetchResult() + data class Success(val points: List) : FetchResult() data class Failed(val reason: String) : FetchResult() } @@ -85,7 +103,9 @@ class DeflockClient(context: Context) { // same cache key, so micro-movements don't refetch. val latStep = floor(lat / FETCH_RADIUS_DEG).toInt() val lonStep = floor(lon / FETCH_RADIUS_DEG).toInt() - return "deflock_${latStep}_${lonStep}" + // v2 prefix: v1 cached ALPR-only responses, which would otherwise be + // served for up to 24 h after this build widened the query. + return "deflock2_${latStep}_${lonStep}" } private fun cachedJson(key: String): String? { @@ -95,10 +115,15 @@ class DeflockClient(context: Context) { return try { f.readText() } catch (e: Exception) { null } } - private fun buildQuery(south: Double, west: Double, north: Double, east: Double): String = - "[out:json][timeout:$OVERPASS_QUERY_TIMEOUT_S];" + - "(node[\"man_made\"=\"surveillance\"][\"surveillance:type\"=\"ALPR\"]" + - "($south,$west,$north,$east););out body;" + private fun buildQuery(south: Double, west: Double, north: Double, east: Double): String { + val bbox = "($south,$west,$north,$east)" + // man_made=surveillance is the superset that already contains the + // surveillance:type=ALPR nodes, so asking for ALPR separately would + // just duplicate them; the parser sorts the classes out by tag. + return "[out:json][timeout:$OVERPASS_QUERY_TIMEOUT_S];" + + "(node[\"man_made\"=\"surveillance\"]$bbox;" + + "node[\"highway\"=\"speed_camera\"]$bbox;);out body;" + } /** Try each endpoint in order until one returns 2xx. Returns body + last error message. */ private fun downloadFromAny(query: String): Pair { @@ -159,12 +184,12 @@ class DeflockClient(context: Context) { lower.contains("rate_limited") } - private fun parseSafely(json: String): List { + private fun parseSafely(json: String): List { if (json.isBlank()) return emptyList() return try { val root = JSONObject(json) val elements = root.optJSONArray("elements") ?: return emptyList() - val out = ArrayList(elements.length()) + val out = ArrayList(elements.length()) for (i in 0 until elements.length()) { val el = elements.optJSONObject(i) ?: continue if (el.optString("type") != "node") continue @@ -172,11 +197,19 @@ class DeflockClient(context: Context) { val lon = el.optDouble("lon") if (lat.isNaN() || lon.isNaN()) continue val tags = el.optJSONObject("tags") + val surveillanceType = tags?.optString("surveillance:type").orEmpty() + val kind = when { + surveillanceType.equals("ALPR", ignoreCase = true) -> Kind.ALPR + tags?.optString("highway").orEmpty() + .equals("speed_camera", ignoreCase = true) -> Kind.SPEED_CAMERA + else -> Kind.CAMERA + } out.add( - AlprPoint( + SurveillancePoint( id = el.optLong("id", 0L), lat = lat, lon = lon, + kind = kind, operator = tags?.optString("operator")?.ifBlank { null } ?: tags?.optString("surveillance:operator")?.ifBlank { null }, manufacturer = tags?.optString("manufacturer")?.ifBlank { null } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockScanner.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockScanner.kt index 30dd4b8..ef9a2fa 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockScanner.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockScanner.kt @@ -44,10 +44,10 @@ class DeflockScanner( private var lastFetchLon: Double? = null private var lastAttemptMs: Long = 0L private var lastAttemptOk: Boolean = false - private val _cachedPoints = MutableStateFlow>(emptyList()) + private val _cachedPoints = MutableStateFlow>(emptyList()) /** All ALPR points in the current cell — exposed so the UI map can render them. * Distinct from the proximity-filtered DetectionEvents on [DetectionStore]. */ - val cachedPoints: StateFlow> = _cachedPoints.asStateFlow() + val cachedPoints: StateFlow> = _cachedPoints.asStateFlow() fun start(scope: CoroutineScope): Boolean { if (job != null) return true @@ -85,7 +85,7 @@ class DeflockScanner( SourceHealth.record(DetectionSource.DEFLOCK, ok = true) Log.i( TAG, - "Loaded ${result.points.size} ALPRs around " + + "Loaded ${result.points.size} surveillance nodes around " + "(${fix.latitude}, ${fix.longitude})" ) } @@ -134,6 +134,7 @@ class DeflockScanner( val obs = ConfidenceEngine.DeflockObservation( osmId = p.id, distanceMeters = dist, + kind = p.kind, operator = p.operator, manufacturer = p.manufacturer ) diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt index a84da15..87a86af 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt @@ -29,7 +29,7 @@ import org.soulstone.overwatch.fusion.SourceHealth * clear reason) and skips the network call rather than hammering a 401. * * The last fetched alert set is cached so [refresh] can re-evaluate against a - * moved proximity slider without a network refetch (mirrors CitizenScanner). + * moved proximity slider without a network refetch. */ class WazeScanner( private val store: DetectionStore, @@ -45,7 +45,8 @@ class WazeScanner( // real police sightings routinely arrive already 20-30 min old. A 10-min // cutoff (fine for the old direct-live feed) would drop nearly all of // them; 45 min matches what the feed actually serves as "current." - private const val MAX_AGE_MS = 45L * 60L * 1000L + /** Shared with ConfidenceEngine, which decays the score across this window. */ + private const val MAX_AGE_MS = ConfidenceEngine.WAZE_MAX_AGE_MS } private var job: Job? = null @@ -99,7 +100,7 @@ class WazeScanner( // still-present alerts by key (dedup) and lets vanished ones age // out via the store's 5-min TTL. Clearing every poll would briefly // drop the tier and re-raise it, double-firing the escalation - // vibration each cycle. (Mirrors CitizenScanner.) + // vibration each cycle. emitProximityEvents(fix, result.alerts) } } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt b/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt index 0264a1e..bbfaa4f 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt @@ -34,17 +34,17 @@ import org.soulstone.overwatch.fusion.DetectionSource import org.soulstone.overwatch.fusion.DetectionStore import org.soulstone.overwatch.fusion.SourceHealth import org.soulstone.overwatch.fusion.ThreatLevel +import org.soulstone.overwatch.scan.AircraftScanner import org.soulstone.overwatch.scan.BleScanner -import org.soulstone.overwatch.scan.CitizenScanner import org.soulstone.overwatch.scan.DeflockClient import org.soulstone.overwatch.scan.DeflockScanner -import org.soulstone.overwatch.scan.DeflockClient.AlprPoint +import org.soulstone.overwatch.scan.DeflockClient.SurveillancePoint import org.soulstone.overwatch.scan.WazeClient import org.soulstone.overwatch.scan.WazeScanner import org.soulstone.overwatch.scan.WifiScanner /** - * Foreground service that owns all four scanners (BLE, WiFi, DeFlock, Citizen) + * Foreground service that owns all four scanners (BLE, WiFi, DeFlock, Waze) * and the [DetectionStore]. UI observes companion-object state flows directly. * * Responsibilities beyond scanner orchestration: @@ -75,8 +75,8 @@ class DetectionService : LifecycleService() { /** Latest ALPR cell cache — UI map renders these as pins. Mirrored from * the active DeflockScanner while the service is running; cleared on stop. */ - private val _mapPoints = MutableStateFlow>(emptyList()) - val mapPoints: StateFlow> = _mapPoints.asStateFlow() + private val _mapPoints = MutableStateFlow>(emptyList()) + val mapPoints: StateFlow> = _mapPoints.asStateFlow() /** Latest fused location fix — UI map centers on this. */ private val _location = MutableStateFlow(null) @@ -106,22 +106,21 @@ class DetectionService : LifecycleService() { private lateinit var wifiScanner: WifiScanner private lateinit var locationProvider: LocationProvider private lateinit var deflockScanner: DeflockScanner - private lateinit var citizenScanner: CitizenScanner private lateinit var wazeScanner: WazeScanner + private lateinit var aircraftScanner: AircraftScanner private lateinit var overlayManager: OverlayManager private var pruneJob: Job? = null private var observerJob: Job? = null private var mapPointsJob: Job? = null private var locationJob: Job? = null private var deflockProxJob: Job? = null - private var citizenProxJob: Job? = null private var wazeProxJob: Job? = null private var overlayJob: Job? = null private var bleStarted = false private var wifiStarted = false private var deflockStarted = false - private var citizenStarted = false private var wazeStarted = false + private var aircraftStarted = false /** Last threat tier the notification displayed; tracks upward transitions for vibration. */ private var lastNotifiedTier: ThreatLevel = ThreatLevel.GREEN @@ -133,17 +132,14 @@ class DetectionService : LifecycleService() { locationProvider = LocationProvider(this) deflockScanner = DeflockScanner( store, locationProvider, DeflockClient(this), - proximityMeters = { settings.deflockProximityM.value.toFloat() } - ) - citizenScanner = CitizenScanner( - store, locationProvider, - proximityMeters = { settings.citizenProximityM.value.toFloat() } + proximityMeters = { settings.detectionRadiusM.value.toFloat() } ) wazeScanner = WazeScanner( store, locationProvider, client = WazeClient(apiKey = { settings.wazeApiKey.value }), - proximityMeters = { settings.wazeProximityM.value.toFloat() } + proximityMeters = { settings.detectionRadiusM.value.toFloat() } ) + aircraftScanner = AircraftScanner(this, store, locationProvider) overlayManager = OverlayManager( context = this, // User dragged the bubble onto the X — flip the persisted toggle @@ -182,8 +178,8 @@ class DetectionService : LifecycleService() { wifiStarted = wifiScanner.start(lifecycleScope) if (!wifiStarted) Log.w(TAG, "WifiScanner.start() returned false (permission/adapter)") } - val needsLocation = settings.deflockEnabled.value || settings.citizenEnabled.value || - settings.wazeEnabled.value + val needsLocation = settings.deflockEnabled.value || settings.wazeEnabled.value || + settings.aircraftEnabled.value if (needsLocation) { val locOk = locationProvider.start() if (!locOk) { @@ -192,16 +188,17 @@ class DetectionService : LifecycleService() { if (settings.deflockEnabled.value) { deflockScanner.start(lifecycleScope); deflockStarted = true } - if (settings.citizenEnabled.value) { - citizenScanner.start(lifecycleScope); citizenStarted = true - } if (settings.wazeEnabled.value) { wazeScanner.start(lifecycleScope); wazeStarted = true } + if (settings.aircraftEnabled.value) { + aircraftScanner.start(lifecycleScope); aircraftStarted = true + } } } - val anyStarted = bleStarted || wifiStarted || deflockStarted || citizenStarted || wazeStarted + val anyStarted = bleStarted || wifiStarted || deflockStarted || wazeStarted || + aircraftStarted if (!anyStarted) { Log.w(TAG, "No scanner started — endScanning + stopSelf") endScanning() @@ -259,22 +256,14 @@ class DetectionService : LifecycleService() { // Live re-eval when the user moves a proximity slider. drop(1) skips // the StateFlow's initial replay so we don't redundantly clear+re-emit // the events the scanner just produced from its first handleFix call. + // One radius now drives both location sources, so one collector + // re-evaluates whichever of them is running. deflockProxJob?.cancel() - if (deflockStarted) { - deflockProxJob = lifecycleScope.launch { - settings.deflockProximityM.drop(1).collect { deflockScanner.refresh() } - } - } - citizenProxJob?.cancel() - if (citizenStarted) { - citizenProxJob = lifecycleScope.launch { - settings.citizenProximityM.drop(1).collect { citizenScanner.refresh() } - } - } wazeProxJob?.cancel() - if (wazeStarted) { - wazeProxJob = lifecycleScope.launch { - settings.wazeProximityM.drop(1).collect { wazeScanner.refresh() } + deflockProxJob = lifecycleScope.launch { + settings.detectionRadiusM.drop(1).collect { + if (deflockStarted) deflockScanner.refresh() + if (wazeStarted) wazeScanner.refresh() } } @@ -290,15 +279,15 @@ class DetectionService : LifecycleService() { } private fun endScanning() { - if (!_running.value && !bleStarted && !wifiStarted && !deflockStarted && !citizenStarted) { + if (!_running.value && !bleStarted && !wifiStarted && !deflockStarted && !wazeStarted) { return } _running.value = false if (bleStarted) { bleScanner.stop(); bleStarted = false } if (wifiStarted) { wifiScanner.stop(); wifiStarted = false } if (deflockStarted) { deflockScanner.stop(); deflockStarted = false } - if (citizenStarted) { citizenScanner.stop(); citizenStarted = false } if (wazeStarted) { wazeScanner.stop(); wazeStarted = false } + if (aircraftStarted) { aircraftScanner.stop(); aircraftStarted = false } locationProvider.stop() store.clear() SourceHealth.reset() @@ -307,7 +296,6 @@ class DetectionService : LifecycleService() { mapPointsJob?.cancel(); mapPointsJob = null locationJob?.cancel(); locationJob = null deflockProxJob?.cancel(); deflockProxJob = null - citizenProxJob?.cancel(); citizenProxJob = null wazeProxJob?.cancel(); wazeProxJob = null overlayJob?.cancel(); overlayJob = null overlayManager.hide() @@ -371,7 +359,7 @@ class DetectionService : LifecycleService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+ requires the runtime type to cover every capability the // service uses. We declare both in the manifest; pass both here so - // location-using sources (DeFlock, Citizen) keep working with the + // location-using sources (DeFlock, Waze) keep working with the // screen off. val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt index e447ff7..b8fcce2 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt @@ -3,11 +3,6 @@ package org.soulstone.overwatch.ui import android.content.Intent import android.location.Location import android.net.Uri -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -48,7 +43,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -75,10 +69,10 @@ fun MainScreen( threat: ThreatLevel, score: Int, events: List, - mapPoints: List, + mapPoints: List, userLocation: Location?, /** Visible radius of the map circle, in meters. Driven by the larger of - * the DeFlock and Citizen proximity sliders so the user sees the full + * the DeFlock and Waze proximity sliders so the user sees the full * area where a detection could fire. */ mapRadiusMeters: Float, onStartStop: () -> Unit, @@ -239,7 +233,7 @@ private fun ThreatMapCircle( level: ThreatLevel, animating: Boolean, userLocation: Location?, - mapPoints: List, + mapPoints: List, events: List, mapRadiusMeters: Float, onTap: () -> Unit @@ -255,16 +249,11 @@ private fun ThreatMapCircle( // muted gray when idle. Gives the current level at a glance even over the map. val ringColor = if (animating) activeColor else idleColor - val transition = rememberInfiniteTransition(label = "pulse") - val pulse by transition.animateFloat( - initialValue = if (animating) 0.5f else 1.0f, - targetValue = 1.0f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1200), - repeatMode = RepeatMode.Reverse - ), - label = "pulse" - ) + // No pulse value is read in this scope on purpose — see PulseVisuals.kt. + // Reading the infinite animation here recomposed the map host every frame, + // which re-ran the AndroidView update block (clearing and reallocating + // every marker, and queuing a zoom) ~60 times a second. + val camera = remember { MapCamera() } Box( modifier = Modifier @@ -278,33 +267,13 @@ private fun ThreatMapCircle( // solid pulsing circle — a blank/loading map mid-tile-fetch reads as // broken. The map only renders once we actually have something to show. if (!animating || userLocation == null) { - val color = if (animating) activeColor else idleColor - val alpha = if (animating) pulse else 1.0f - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - color.copy(alpha = alpha), - color.copy(alpha = alpha * 0.6f) - ) - ) - ), - contentAlignment = Alignment.Center - ) { - val labelText = when { - !animating -> "IDLE" - else -> "WAITING FIX" - } - Text( - text = labelText, - color = if (animating) Color.White else MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 22.sp, - fontWeight = FontWeight.Black, - fontFamily = FontFamily.Monospace - ) - } + PulsingDisc( + color = if (animating) activeColor else idleColor, + animating = animating, + label = if (animating) "WAITING FIX" else "IDLE", + labelColor = if (animating) Color.White + else MaterialTheme.colorScheme.onSurfaceVariant + ) } else { // OSM map snapshot, centered on the user, with red ALPR pins and // a blue user-position dot. Non-interactive — touches are captured @@ -318,8 +287,10 @@ private fun ThreatMapCircle( // every recomposition — bitmap allocation isn't free. val userMark = remember(ctx) { crosshairDrawable(ctx.resources, 46, MARK_USER_WHITE) } val flockDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_FLOCK_RED) } + val speedDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_SPEED_AMBER) } + val cameraDot = remember(ctx) { dotDrawable(ctx.resources, 18, DOT_CAMERA_GRAY) } val wazeDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_WAZE_BLUE) } - val citizenDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_CITIZEN_PURPLE) } + val aircraftDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_AIRCRAFT_VIOLET) } AndroidView( modifier = Modifier.fillMaxSize(), factory = { c -> @@ -332,18 +303,21 @@ private fun ThreatMapCircle( } }, update = { map -> - map.controller.setCenter(GeoPoint(fix.latitude, fix.longitude)) map.overlays.clear() - // Source dots first (Flock red, Waze blue, Citizen purple), + // Source dots first (Flock red, Waze blue), // user position last so the crosshair always draws on top. for (p in mapPoints) { map.overlays.add( Marker(map).apply { position = GeoPoint(p.lat, p.lon) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) - icon = flockDot - title = p.operator ?: p.manufacturer ?: "ALPR" + icon = when (p.kind) { + DeflockClient.Kind.ALPR -> flockDot + DeflockClient.Kind.SPEED_CAMERA -> speedDot + DeflockClient.Kind.CAMERA -> cameraDot + } + title = p.operator ?: p.manufacturer ?: p.kind.name setInfoWindow(null) } ) @@ -353,7 +327,7 @@ private fun ThreatMapCircle( val lon = e.lon ?: continue val dot = when (e.source) { DetectionSource.WAZE -> wazeDot - DetectionSource.CITIZEN -> citizenDot + DetectionSource.AIRCRAFT -> aircraftDot else -> null } ?: continue map.overlays.add( @@ -375,35 +349,32 @@ private fun ThreatMapCircle( } ) - // Fit the visible radius to the larger of the two proximity - // settings. Defer to map.post so the call lands after layout - // — zoomToBoundingBox needs measured dimensions to compute - // the right zoom level. Latitude-aware longitude scaling so - // the bbox stays roughly square in real meters at any lat. - val r = mapRadiusMeters.toDouble().coerceAtLeast(50.0) - val latDegPerMeter = 1.0 / 111_000.0 - val lonDegPerMeter = 1.0 / - (111_000.0 * cos(Math.toRadians(fix.latitude)).coerceAtLeast(0.01)) - val bbox = BoundingBox( - fix.latitude + r * latDegPerMeter, - fix.longitude + r * lonDegPerMeter, - fix.latitude - r * latDegPerMeter, - fix.longitude - r * lonDegPerMeter - ) - map.post { map.zoomToBoundingBox(bbox, false, 0) } + // Move the camera only when the position or the radius + // actually changed. zoomToBoundingBox has to be deferred to + // map.post because it needs measured dimensions, so issuing + // one per pass built a backlog of stale zooms that fought + // each other — that was the lurching when a slider moved. + if (camera.needsMove(fix.latitude, fix.longitude, mapRadiusMeters)) { + val r = mapRadiusMeters.toDouble().coerceAtLeast(50.0) + val latDegPerMeter = 1.0 / 111_000.0 + val lonDegPerMeter = 1.0 / + (111_000.0 * cos(Math.toRadians(fix.latitude)).coerceAtLeast(0.01)) + val bbox = BoundingBox( + fix.latitude + r * latDegPerMeter, + fix.longitude + r * lonDegPerMeter, + fix.latitude - r * latDegPerMeter, + fix.longitude - r * lonDegPerMeter + ) + map.controller.setCenter(GeoPoint(fix.latitude, fix.longitude)) + map.post { map.zoomToBoundingBox(bbox, false, 0) } + } map.invalidate() }, onRelease = { map -> map.onDetach() } ) - // Threat-tier scrim — pulses while scanning. Heavier alpha than - // the first cut so the tier color reads at a glance over OSM - // tiles, which are themselves cream/light by default. - val scrimAlpha = (0.55f * pulse).coerceIn(0.40f, 0.65f) - Box( - modifier = Modifier - .fillMaxSize() - .background(activeColor.copy(alpha = scrimAlpha)) - ) + // Threat-tier scrim — pulses while scanning, in its own leaf so + // the animation never recomposes the map above it. + TierScrim(color = activeColor, animating = animating) } // Click capture sits on top so taps reach onTap regardless of which // visual layer was painted underneath. diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt index 3bdbaf4..2b6eafe 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt @@ -62,7 +62,9 @@ internal fun crosshairDrawable( } // Per-source marker colors so geodata reads at a glance. -internal const val DOT_FLOCK_RED = 0xFFD7263D.toInt() // Flock / DeFlock ALPR cameras +internal const val DOT_FLOCK_RED = 0xFFD7263D.toInt() // ALPR — reads and records plates +internal const val DOT_SPEED_AMBER = 0xFFFF9800.toInt() // fixed speed / traffic enforcement +internal const val DOT_CAMERA_GRAY = 0xFF9E9E9E.toInt() // generic mapped surveillance camera internal const val DOT_WAZE_BLUE = 0xFF2196F3.toInt() // Waze police reports -internal const val DOT_CITIZEN_PURPLE = 0xFF9C27B0.toInt() // Citizen incidents +internal const val DOT_AIRCRAFT_VIOLET = 0xFF7C4DFF.toInt() // police / surveillance aircraft internal const val MARK_USER_WHITE = 0xFFFFFFFF.toInt() // the user's own position (⌖) diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt index 76740b5..d8463a8 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt @@ -1,10 +1,5 @@ package org.soulstone.overwatch.ui -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -17,7 +12,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -28,6 +22,7 @@ import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker +import org.soulstone.overwatch.scan.DeflockClient import org.soulstone.overwatch.data.settings.Settings import org.soulstone.overwatch.fusion.DetectionSource import org.soulstone.overwatch.fusion.ThreatLevel @@ -56,10 +51,8 @@ fun OverlayBubble() { val userLocation by DetectionService.location.collectAsState() val mapPoints by DetectionService.mapPoints.collectAsState() val events by DetectionService.store.events.collectAsState() - val deflockProx by settings.deflockProximityM.collectAsState() - val citizenProx by settings.citizenProximityM.collectAsState() - val wazeProx by settings.wazeProximityM.collectAsState() - val radius = max(max(deflockProx, citizenProx), wazeProx).toFloat() + val detectionRadius by settings.detectionRadiusM.collectAsState() + val radius = detectionRadius.toFloat() val activeColor = when (threat) { ThreatLevel.GREEN -> ThreatColors.Green @@ -68,21 +61,16 @@ fun OverlayBubble() { ThreatLevel.RED -> ThreatColors.Red } - val transition = rememberInfiniteTransition(label = "overlay-pulse") - val pulse by transition.animateFloat( - initialValue = 0.55f, - targetValue = 1.0f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1200), - repeatMode = RepeatMode.Reverse - ), - label = "overlay-pulse" - ) + // The pulse lives in PulseVisuals' leaves now; reading it here would + // recompose the map host every frame. + val camera = remember { MapCamera() } val userMark = remember(ctx) { crosshairDrawable(ctx.resources, 34, MARK_USER_WHITE) } val flockDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_FLOCK_RED) } + val speedDot = remember(ctx) { dotDrawable(ctx.resources, 18, DOT_SPEED_AMBER) } + val cameraDot = remember(ctx) { dotDrawable(ctx.resources, 15, DOT_CAMERA_GRAY) } val wazeDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_WAZE_BLUE) } - val citizenDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_CITIZEN_PURPLE) } + val aircraftDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_AIRCRAFT_VIOLET) } Box( modifier = Modifier @@ -95,17 +83,11 @@ fun OverlayBubble() { // map if a future code path lets the composition outlive the service. val fix = userLocation if (!running || fix == null) { - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - activeColor.copy(alpha = pulse), - activeColor.copy(alpha = pulse * 0.6f) - ) - ) - ) + PulsingDisc( + color = activeColor, + animating = running, + label = null, + labelColor = activeColor ) } else { AndroidView( @@ -120,15 +102,18 @@ fun OverlayBubble() { } }, update = { map -> - map.controller.setCenter(GeoPoint(fix.latitude, fix.longitude)) map.overlays.clear() for (p in mapPoints) { map.overlays.add( Marker(map).apply { position = GeoPoint(p.lat, p.lon) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) - icon = flockDot - title = p.operator ?: p.manufacturer ?: "ALPR" + icon = when (p.kind) { + DeflockClient.Kind.ALPR -> flockDot + DeflockClient.Kind.SPEED_CAMERA -> speedDot + DeflockClient.Kind.CAMERA -> cameraDot + } + title = p.operator ?: p.manufacturer ?: p.kind.name setInfoWindow(null) } ) @@ -138,7 +123,7 @@ fun OverlayBubble() { val lon = e.lon ?: continue val dot = when (e.source) { DetectionSource.WAZE -> wazeDot - DetectionSource.CITIZEN -> citizenDot + DetectionSource.AIRCRAFT -> aircraftDot else -> null } ?: continue map.overlays.add( @@ -158,28 +143,27 @@ fun OverlayBubble() { setInfoWindow(null) } ) - val r = radius.toDouble().coerceAtLeast(50.0) - val latDegPerMeter = 1.0 / 111_000.0 - val lonDegPerMeter = 1.0 / - (111_000.0 * cos(Math.toRadians(fix.latitude)).coerceAtLeast(0.01)) - val bbox = BoundingBox( - fix.latitude + r * latDegPerMeter, - fix.longitude + r * lonDegPerMeter, - fix.latitude - r * latDegPerMeter, - fix.longitude - r * lonDegPerMeter - ) - map.post { map.zoomToBoundingBox(bbox, false, 0) } + // Same camera guard as the in-app circle — see MapCamera. + if (camera.needsMove(fix.latitude, fix.longitude, radius)) { + val r = radius.toDouble().coerceAtLeast(50.0) + val latDegPerMeter = 1.0 / 111_000.0 + val lonDegPerMeter = 1.0 / + (111_000.0 * cos(Math.toRadians(fix.latitude)).coerceAtLeast(0.01)) + val bbox = BoundingBox( + fix.latitude + r * latDegPerMeter, + fix.longitude + r * lonDegPerMeter, + fix.latitude - r * latDegPerMeter, + fix.longitude - r * lonDegPerMeter + ) + map.controller.setCenter(GeoPoint(fix.latitude, fix.longitude)) + map.post { map.zoomToBoundingBox(bbox, false, 0) } + } map.invalidate() }, onRelease = { map -> map.onDetach() } ) - // Tier scrim — same pulse alpha range as the in-app circle. - val scrimAlpha = (0.55f * pulse).coerceIn(0.40f, 0.65f) - Box( - modifier = Modifier - .fillMaxSize() - .background(activeColor.copy(alpha = scrimAlpha)) - ) + // Tier scrim — own leaf, so the pulse never recomposes the map. + TierScrim(color = activeColor, animating = running) } } } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/PulseVisuals.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/PulseVisuals.kt new file mode 100644 index 0000000..828d82c --- /dev/null +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/PulseVisuals.kt @@ -0,0 +1,118 @@ +package org.soulstone.overwatch.ui + +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.material3.Text +import androidx.compose.ui.unit.sp + +/** + * The pulsing threat visuals, deliberately split into their own leaf + * composables. + * + * **Why this file exists.** The pulse is an infinite animation, so anything + * that reads it recomposes on every frame. Both the in-app circle and the + * floating bubble used to read it in the same scope that hosted the map's + * `AndroidView`, which meant the map's `update` block re-ran ~60 times a + * second: it cleared every overlay and reallocated a `Marker` for all of them + * (152 ALPRs in a dense area) each frame, and queued a `zoomToBoundingBox` + * through `map.post` on every one of those passes while `setCenter` fought it. + * That is what made the map flicker and lurch — worst when a proximity slider + * moved, because the new bounding box landed on top of a backlog of stale + * zoom runnables. + * + * Keeping the animated read inside these leaves means the map host recomposes + * only when the fix, the points, the events or the radius actually change. + * Anything that hosts a map must therefore render the pulse through these and + * never read a pulse value in its own scope. + */ +@Composable +private fun rememberPulse(animating: Boolean): State { + val transition = rememberInfiniteTransition(label = "threat-pulse") + return transition.animateFloat( + initialValue = if (animating) 0.5f else 1.0f, + targetValue = 1.0f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1200), + repeatMode = RepeatMode.Reverse + ), + label = "threat-pulse" + ) +} + +/** Solid pulsing disc — the idle / waiting-for-fix face of the circle. */ +@Composable +internal fun PulsingDisc( + color: Color, + animating: Boolean, + label: String?, + labelColor: Color, + modifier: Modifier = Modifier +) { + val pulse by rememberPulse(animating) + val alpha = if (animating) pulse else 1.0f + Box( + modifier = modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf(color.copy(alpha = alpha), color.copy(alpha = alpha * 0.6f)) + ) + ), + contentAlignment = Alignment.Center + ) { + if (label != null) { + Text( + text = label, + color = labelColor, + fontSize = 22.sp, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace + ) + } + } +} + +/** Threat-tier scrim laid over the map so the tier reads at a glance. */ +@Composable +internal fun TierScrim(color: Color, animating: Boolean, modifier: Modifier = Modifier) { + val pulse by rememberPulse(animating) + val scrimAlpha = (0.55f * pulse).coerceIn(0.40f, 0.65f) + Box(modifier.fillMaxSize().background(color.copy(alpha = scrimAlpha))) +} + +/** + * Remembers the last camera position applied to a [org.osmdroid.views.MapView] + * so the bounding-box zoom is only issued when the center or radius actually + * moved, rather than on every pass through an `AndroidView` update block. + */ +internal class MapCamera { + private var lat = Double.NaN + private var lon = Double.NaN + private var radius = Float.NaN + + /** True (and records the new values) when this camera differs from the last applied one. */ + fun needsMove(newLat: Double, newLon: Double, newRadius: Float): Boolean { + // Sub-metre jitter on a stationary fix shouldn't re-zoom the map. + val moved = lat.isNaN() || + kotlin.math.abs(newLat - lat) > 1e-5 || + kotlin.math.abs(newLon - lon) > 1e-5 || + radius != newRadius + if (moved) { lat = newLat; lon = newLon; radius = newRadius } + return moved + } +} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt index e3f9361..d532928 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt @@ -59,12 +59,10 @@ fun SettingsScreen( val ble by settings.bleEnabled.collectAsState() val wifi by settings.wifiEnabled.collectAsState() val deflock by settings.deflockEnabled.collectAsState() - val citizen by settings.citizenEnabled.collectAsState() val waze by settings.wazeEnabled.collectAsState() + val aircraft by settings.aircraftEnabled.collectAsState() val mic by settings.micEnabled.collectAsState() - val deflockProx by settings.deflockProximityM.collectAsState() - val citizenProx by settings.citizenProximityM.collectAsState() - val wazeProx by settings.wazeProximityM.collectAsState() + val detectionRadius by settings.detectionRadiusM.collectAsState() val wazeApiKey by settings.wazeApiKey.collectAsState() val theme by settings.themeMode.collectAsState() val vibrate by settings.vibrateOnAlert.collectAsState() @@ -98,8 +96,8 @@ fun SettingsScreen( SourceToggle("BLE • Bluetooth Low Energy", ble) { settings.setBleEnabled(it) } SourceToggle("WIFI • WiFi BSSID + SSID", wifi) { settings.setWifiEnabled(it) } SourceToggle("DEFLOCK • ALPR map (Overpass)", deflock) { settings.setDeflockEnabled(it) } - SourceToggle("CITIZEN • Real-time incident feed", citizen) { settings.setCitizenEnabled(it) } SourceToggle("WAZE • Live police reports", waze) { settings.setWazeEnabled(it) } + SourceToggle("AIRCRAFT • Police / surveillance planes", aircraft) { settings.setAircraftEnabled(it) } SourceToggle("COMMERCIAL • Nest, Ring, Echo, glasses", mic) { settings.setMicEnabled(it) } Spacer(Modifier.height(8.dp)) if (isRunning) { @@ -127,27 +125,21 @@ fun SettingsScreen( } Spacer(Modifier.height(16.dp)) - SectionLabel("Proximity thresholds") - SliderRow( - label = "DeFlock alert distance", - persistedValue = deflockProx, - range = 50f..1600f, - steps = 30, - onCommit = { settings.setDeflockProximityM(it) } + SectionLabel("Detection radius") + Text( + "How close a Flock/DeFlock camera or a Waze police report has to be " + + "to count. Also the area drawn on the map.", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontFamily = FontFamily.Monospace, + modifier = Modifier.padding(vertical = 4.dp) ) SliderRow( - label = "Citizen alert distance", - persistedValue = citizenProx, - range = 100f..5000f, + label = "Detection radius", + persistedValue = detectionRadius, + range = Settings.RADIUS_MIN.toFloat()..Settings.RADIUS_MAX.toFloat(), steps = 48, - onCommit = { settings.setCitizenProximityM(it) } - ) - SliderRow( - label = "Waze alert distance", - persistedValue = wazeProx, - range = 100f..5000f, - steps = 48, - onCommit = { settings.setWazeProximityM(it) } + onCommit = { settings.setDetectionRadiusM(it) } ) Spacer(Modifier.height(16.dp)) diff --git a/app/src/main/res/raw/le_aircraft.csv b/app/src/main/res/raw/le_aircraft.csv new file mode 100644 index 0000000..cd4feef --- /dev/null +++ b/app/src/main/res/raw/le_aircraft.csv @@ -0,0 +1,1972 @@ +# hex,owner,ladd — generated by scripts/gen-le-aircraft.py, do not edit +20c462,US CUSTOMS & BORDER PROTECTION OFFICE OF AIR AN,0 +25ea33,US CUSTOMS & BORDER PROTECTION NATIONAL AIR SEC,0 +29f7db,UNITED STATES CUSTOMS AND BORDER PROTECTION OFF,0 +486868,Dutch Police,0 +70c12d,Royal Oman Police,0 +70c12e,Royal Oman Police,0 +70c12f,Royal Oman Police,0 +71d870,Korean National Police,0 +71d871,Korean National Police,0 +71d872,Korean National Police,0 +71d873,Korean National Police,0 +71d91a,Korean National Police,0 +71d91b,Korean National Police,0 +71d91c,Korean National Police,0 +71d91d,Korean National Police,0 +7c0ff9,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c1185,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c1239,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c1563,AUSTRALIAN FEDERAL POLICE,1 +7c1cdb,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,0 +7c4ced,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c4cf8,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c4d02,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c4d05,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c4e49,STATE OF NEW SOUTH WALES REPRESENTED BY NSW POLI,1 +7c4ee8,SKYTRADERS PTY LTD/Victoria Police,1 +7c6bdc,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,1 +7c6ca7,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,1 +7c6ca8,WESTERN AUSTRALIA POLICE,1 +7c7180,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,1 +7c7181,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,0 +7c718c,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,0 +7c7194,STATE OF WESTERN AUSTRALIA - REPRESENTED BY COMM,0 +a00137,STATE OF NEW JERSEY C/O DEPT OF LAW AND PUBLIC,0 +a00196,PINELLAS COUNTY SHERIFFS OFFICE,0 +a001a0,STATE OF DELAWARE DIV OF SAFETY AND HOMELAND SE,0 +a002a3,ALAMEDA COUNTY SHERIFFS OFFICE,1 +a003f9,STATE OF KENTUCKY JUSTICE CABINET DEPARTMENT O,0 +a00504,MARSHALL COUNTY SHERIFF DEPT,0 +a00534,FLORENCE COUNTY SHERIFFS OFFICE,0 +a00650,MISSOURI STATE HIGHWAY PATROL,0 +a00692,VICTORIA COUNTY SHERIFF,0 +a00808,DEPARTMENT OF HOMELAND SECURITY,0 +a00928,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +a009b4,FLOWOOD POLICE DEPARTMENT,0 +a00a21,HILLSBOROUGH COUNTY SHERIFFS OFFICE,0 +a00a25,HILLSBOROUGH COUNTY SHERIFFS OFFICE,0 +a00cdf,CALIFORNIA HIGHWAY PATROL,0 +a00d2a,WASHINGTON STATE PATROL,0 +a00fa3,ST JOHNS COUNTY SHERIFFS OFFICE,1 +a00ff1,SALT RIVER POLICE DEPT,0 +a010af,CITY OF JACKSONVILLE SHERIFFS OFFICE,0 +a0127d,WASHOE COUNTY SHERIFFS OFFICE,0 +a014b1,MAITLAND POLICE DEPARTMENT,0 +a0175f,SALT RIVER POLICE DEPARTMENT,0 +a0178a,CADDO PARISH SHERIFF'S DEPT,0 +a01868,MAITLAND POLICE DEPARTMENT,0 +a018e5,DEPARTMENT OF EMERGENCY SVCS & PUBLIC PROTECTION,1 +a018e7,SALT RIVER POLICE DEPARTMENT,0 +a01d92,SHELBYVILLE POLICE DEP,0 +a02053,DEPARTMENT OF EMERGENCY SVC & PUBLIC PROTECTION,1 +a022ab,PASCO SHERIFF'S OFFICE,0 +a022b0,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a022e9,DEPT OF HOMELAND SECURITY,0 +a02400,RIVERSIDE COUNTY SHERIFFS,0 +a0240a,DALE COUNTY SHERIFFS OFFICE,0 +a02566,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a02598,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a0263f,BROWARD COUNTY SHERIFFS OFFICE,0 +a026e0,FLORIDA HIGHWAY PATROL,0 +a028c4,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a02b6e,HARRIS COUNTY SHERIFFS OFFICE,0 +a02b78,STATE OF NEW YORK DIVISION OF STATE POLICE,0 +a02cb3,BREVARD COUNTY SHERIFFS OFFICE,0 +a02dc7,PINAL COUNTY SHERIFF DEPT,0 +a030c3,LIVINGSTON PARISH SHERIFF DEPT,0 +a0314b,DELAWARE STATE POLICE,0 +a03188,STATE OF MICHIGAN DEPARTMENT OF STATE POLICE A,0 +a0340c,ALACHUA COUNTY SHERIFFS OFFICE,0 +a034c2,MESA POLICE DEPT,0 +a0353f,MINNESOTA DEPARTMENT OF PUBLIC SAFETY,0 +a03815,STATE OF OHIO DEPARTMENT OF PUBLIC SAFETY OHIO,0 +a038a1,BREVARD COUNTY SHERIFF OFFICE,1 +a039f7,ALACHUA COUNTY SHERIFF,0 +a03a07,ALACHUA COUNTY SHERIFF,0 +a03a8c,SCOTTSDALE POLICE DEPT,0 +a03b59,MINNESOTA DEPARTMENT OF PUBLIC SAFETY STATE PAT,0 +a03c30,MESA POLICE DEPT,0 +a03c58,OMAHA POLICE DEPARTMENT,0 +a03cad,MINNESOTA STATE PATROL AVIATION,0 +a03e97,OSCEOLA COUNTY SHERIFFS OFFICE,1 +a03f75,DEPARTMENT OF HOMELAND SECURITY,0 +a03fe7,MESA POLICE DEPT,0 +a0400f,SALT RIVER POLICE DEPARTMENT,0 +a04064,MINNESOTA STATE PATROL,0 +a0439e,MISSOURI STATE HIGHWAY PATROL,0 +a043a9,DEPARTMENT OF HOMELAND SECURITY,0 +a043c6,SALT RIVER POLICE DEPARTMENT,0 +a0441b,DELAWARE STATE POLICE,0 +a046c3,LA PAZ CO SHERIFFS DEPT,0 +a04742,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +a04755,MESA POLICE DEPT,0 +a049fc,ADAMS COUNTY SHERIFFS OFFICE,0 +a04a1e,POLICE SCIENCE INSTITUTE INC,0 +a04b0c,MESA POLICE DEPT,0 +a04b7f,PINAL COUNTY SHERIFFS OFFICE,0 +a04b89,MINNESOTA STATE PATROL,0 +a04db3,ADAMS COUNTY SHERIFFS OFFICE,0 +a04ec3,MESA POLICE DEPT,0 +a04ee5,CITY OF NEW YORK DEPARTMENT OF ENVIRONMENTAL PR,0 +a05216,STATE OF OHIO DEPT OF PUBLIC SAFETY OHIO STATE,0 +a05293,CITY OF NEWARK N J POLICE DEPT,0 +a052f7,NEW YORK STATE POLICE AVIATION UNIT,1 +a053e1,BAXTER COUNTY SHERIFFS DEPARTMENT,0 +a054f5,CITY OF NEW YORK DEPARTMENT OF ENVIRONMENTAL PR,0 +a05545,ST CHARLES PARISH SHERIFFS OFFICE,0 +a0584c,TOMBALL POLICE DEPARTMENT,0 +a05868,OSCEOLA CO SHERIFF OFFICE,0 +a05f83,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a06006,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a0642c,MINNESOTA STATE PATROL,0 +a0644d,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a06653,AUSTIN POLICE DEPT,0 +a0669e,DALLAS POLICE DEPT.,0 +a0677d,ALBUQUERQUE POLICE DEPARTMENT,0 +a067e3,INDIANA STATE POLICE,0 +a06aae,HENRY COUNTY SHERIFF'S OFFICE,0 +a07232,DEPARTMENT OF HOMELAND SECURITY,0 +a07642,MESA POLICE DEPT,0 +a076bf,NEW YORK STATE POLICE,1 +a07834,ST JOHNS COUNTY SHERIFFS OFFICE,0 +a07a6c,SNOHOMISH COUNTY SHERIFFS OFFICE,0 +a07a76,NEW YORK STATE POLICE,1 +a07c86,PUERTO RICO POLICE DEPARTMENT,1 +a07da8,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +a0807c,BENTON COUNTY SHERIFFS DEPARTMENT,0 +a083c0,MESA POLICE DEPT,0 +a0843d,INDIANA STATE POLICE,0 +a08577,STATE OF ALASKA DEPT OF PUBLIC SAFETY,1 +a08ba1,BENTON COUNTY SHERIFFS OFFICE,0 +a095ef,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a0967b,PUERTO RICO POLICE DEPARTMENT,1 +a09a32,PUERTO RICO POLICE DEPARTMENT,1 +a09c14,STATE OF ALASKA DEPT OF PUBLIC SAFETY ALASKA WI,1 +a09cfc,VOLUSIA SHERIFFS OFFICE,0 +a09d5d,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a09de9,PUERTO RICO POLICE DEPT,1 +a0a1eb,LAKE COUNTY SHERIFFS OFFICE,0 +a0a36d,CALIFORNIA HIGHWAY PATROL,0 +a0a7b0,CITY OF ATLANTA POLICE,0 +a0aa61,US CUSTOMS & BORDER PROTECTION OFFICE OF AIR &,0 +a0abb1,SPARTANBURG COUNTY SHERIFFS OFFICE,0 +a0abbc,GEORGIA STATE PATROL,0 +a0acf4,MARICOPA COUNTY SHERIFFS OFFICE ATTN AVIATION D,0 +a0ae18,US CUSTOMS & BORDER PROTECTION OFFICE OF AIR &,0 +a0af01,DEPARTMENT OF HOMELAND SECURITY,0 +a0b1cf,US CUSTOMS & BORDER PROTECTION OFFICE OF AIR &,0 +a0b32a,GEORGIA STATE PATROL,0 +a0b59f,VOLUSIA SHERIFFS OFFICE,0 +a0b5a2,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +a0b651,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +a0b693,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AC,1 +a0b702,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a0b721,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a0b93d,UNITED STATES CUSTOMS AND BORDER PROTECTION,0 +a0bcf4,UNITED STATES CUSTOMS AND BORDER PROTECTION OFF,0 +a0bdfa,PUERTO RICO POLICE DEPT,0 +a0bf9a,SUFFOLK COUNTY POLICE PILOTS ASSOC,0 +a0c0ab,UNITED STATES CUSTOMS AND BORDER PROTECTION,0 +a0c1b1,CITY OF SAN JOSE-POLICE DEPARTMENT,0 +a0c462,US CUSTOMS & BORDER PROTECTION OFFICE OF AIR &,0 +a0cb78,ANNE ARUNDEL COUNTY POLICE DEPT,0 +a0cea3,MONTANA HIGHWAY PATROL 303 N ROBERTS,0 +a0cf2f,ANNE ARUNDEL COUNTRY POLICE,0 +a0cf3b,CITRUS COUNTY SHERIFFS OFFICE,0 +a0cf44,DEPARTMENT OF HOMELAND SECURITY,0 +a0d0d5,CITY OF DALLAS POLICE DEPARTMENT,0 +a0d0f8,CITY OF DALLAS POLICE DEPARTMENT,0 +a0d2e6,SALT RIVER POLICE DEPARTMENT,0 +a0d611,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a0d69d,ANNE ARUNDEL COUNTY POLICE DEPARTMENT,0 +a0d6e8,JACKSON COUNTY SHERIFFS DEPT,0 +a0da53,PUTNAM COUNTY SHERIFF'S OFFICE,0 +a0da54,ANNE ARUNDEL COUNTY POLICE DEPARTMENT,0 +a0dcda,COCONINO COUNTY SHERIFFS OFFICE,0 +a0de0b,ANNE ARUNDEL COUNTY POLICE DEPT,0 +a0de56,JACKSON COUNTY SHERIFFS DEPT,0 +a0e07c,AK-CHIN POLICE DEPARTMENT,0 +a0e136,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a0e1c2,ANNE ARUNDEL COUNTY POLICE DEPT,0 +a0e579,PUERTO RICO POLICE DEPT,0 +a0e5ce,LOUISIANA STATE POLICE,0 +a0e760,CITY OF HOUSTON POLICE DEPT,0 +a0e834,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a0e899,HENRY COUNTY SHERIFF'S OFFICE,0 +a0e8e6,COLLIER COUNTY SHERIFFS OFFICE,0 +a0e93d,GREENVILLE COUNTY SHERIFFS OFFICE,0 +a0e985,LOUISIANA STATE POLICE,0 +a0ec5b,CALIFORNIA HIGHWAY PATROL,0 +a0f0f3,STATE OF NEW YORK DIVISION OF STATE POLICE,1 +a0f740,DEPARTMENT OF HOMELAND SECURITY,0 +a0f9b2,DEPARTMENT OF HOMELAND SECURITY,0 +a0f9d9,CALIFORNIA HIGHWAY PATROL,0 +a0fabe,STATE OF MICHIGAN DEPARTMENT OF STATE POLICE,0 +a1016b,MEMPHIS POLICE DEPARTMENT AVIATION UNIT,0 +a103ac,CUSTOMS AND BORDER PROTECTION,0 +a10aec,STATE OF MONTANA DEPT OF JUSTICE MONTANA HIGHW,0 +a10bb2,ALACHUA COUNTY SHERIFFS OFFICE,0 +a10be1,DEPARTMENT OF HOMELAND SECURITY,0 +a10c6c,CALIFORNIA HIGHWAY PATROL,0 +a10cf8,PUERTO RICO POLICE DEPT,0 +a11466,SALT RIVER POLICE DEPARTMENT,0 +a11791,STATE OF OHIO DEPT OF PUBLIC SAFETY OHIO STATE,0 +a11872,NEW YORK STATE POLICE AVIATION UNIT,1 +a11b2f,HENNEPIN COUNTY SHERIFFS OFFICE,0 +a11de3,MONTGOMERY COUNTY SHERIFFS OFFICE,0 +a11e2d,SUFFOLK COUNTY POLICE DEPT,0 +a11e82,STATE OF IOWA - IOWA STATE PATROL,0 +a120a0,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a12152,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a121a0,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a1222f,SACRAMENTO COUNTY SHERIFF DEPARTMENT,0 +a1228a,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a122f2,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a123f3,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a1246b,DEPT OF HOMELAND SECURITY,0 +a1250f,CALIFORNIA HIGHWAY PATROL,0 +a127e1,OREGON STATE POLICE,0 +a1299c,SEMINOLE COUNTY SHERIFFS OFFICE,0 +a129a7,NEW YORK STATE POLICE,1 +a129ab,MICHIGAN STATE POLICE AVIATION UNIT,0 +a12b3e,NEBRASKA STATE PATROL,0 +a12c41,SEMINOLE COUNTY SHERIFFS OFFICE,0 +a12c7d,CALIFORNIA HIGHWAY PATROL,0 +a12c8b,SACRAMENTO COUNTY SHERIFFS OFFICE,0 +a12d53,SEMINOLE COUNTY SHERIFFS OFFICE,0 +a12d54,SACRAMENTO COUNTY SHERIFF DEPARTMENT,0 +a1310a,SEMINOLE COUNTY SHERIFFS OFFICE,1 +a13115,WEST VIRGINIA STATE POLICE,0 +a13477,PUERTO RICO POLICE DEPT,0 +a134cc,DIVISION OF PUBLIC SAFETY/W VIRGINIA STATE POLIC,0 +a1382e,SALT RIVER POLICE DEPARTMENT,0 +a13883,DIVISION OF PUBLIC SAFETY/W VIRGINIA STATE POLIC,0 +a13be5,SALT RIVER POLICE DEPARTMENT,0 +a13ed3,FLORENCE COUNTY SHERIFF DEPARTMENT,0 +a13f10,STATE OF OHIO DEPARTMENT OF PUBLIC SAFETY OHIO,0 +a13f88,TENNESSEE DEPT OF SAFTEY & HOMELAND SECURITY,0 +a13ff1,STATE OF NEW YORK DIVISION OF STATE POLICE,1 +a1420a,DEPARTMENT OF HOMELAND SECURITY,0 +a1423f,ST CLAIR COUNTY SHERIFFS DEPT,0 +a1441c,GWINNETT COUNTY POLICE DEPT,0 +a145f7,ORANGE COUNTY SHERIFFS DEPARTMENT,0 +a14962,PICKENS COUNTY SHERIFFS OFFICE,0 +a14b70,JACKSON COUNTY SHERIFFS OFFICE,0 +a14bd6,U S DEPT OF HOMELAND SECURITY CUSTOMS & BORDER,1 +a14d65,ORANGE COUNTY SHERIFF'S DEPARTMENT C/O CORONER,0 +a14dc8,WASHINGTON COUNTY SHERIFF'S OFFIICE,0 +a150ff,DEPARTMENT OF HOMELAND SECURITY,0 +a154d3,ORANGE COUNTY SHERIFF'S DEPARTMENT,0 +a155ac,SANTA BARBARA COUNTY SHERIFFS DEPT,0 +a155bb,PLACER COUNTY SHERIFFS DEPARTMENT,0 +a155c1,PLACER COUNTY SHERIFF,0 +a156fb,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a157b3,CALIFORNIA HIGHWAY PATROL,0 +a1588a,ORANGE COUNTY SHERIFFS DEPT,0 +a15a0e,PIMA COUNTY BOARD OF SUPERVISORS SHERIFFS DEPT,0 +a15ab2,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a15bf6,SALT RIVER POLICE DEPARTMENT,0 +a15c88,DEPARTMENT OF HOMELAND SECURITY,0 +a15d4b,PUTNAM COUNTY SHERIFF'S OFICE,1 +a15f21,CALIFORNIA HIGHWAY PATROL,0 +a1611a,CONSTABLE WILLIAM H,0 +a162e6,TULARE COUNTY SHERIFF,0 +a1668f,STATE OF OHIO DEPT OF PUBLIC SAFETY OHIO STATE,0 +a16770,NEW YORK STATE POLICE,1 +a168e8,OKLA DEPT OF PUBLIC SAFETY OKLA HWY PATROL,0 +a16aed,DALE COUNTY SHERIFFS OFFICE,0 +a16c52,NORTH CAROLINA DEPARTMENT OF PUBLIC SAFETY/EMG M,0 +a16d1e,MARYLAND NATURAL RESOURCES POLICE,0 +a16da1,TEXAS DEPT OF PUBLIC SAFETY,1 +a16e5d,WASHOE COUNTY SHERIFFS OFFICE,0 +a16f99,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a17009,NORTH CAROLINA DEPARTMENT OF PUBLIC SAFETY/EMG M,0 +a17016,DEPARTMENT OF HOMELAND SECURITY,0 +a17056,CALIFORNIA HIGHWAY PATROL,0 +a17385,CANADIAN COUNTY SHERIFFS OFFICE,0 +a173c0,NORTH CAROLINA DEPARTMENT OF PUBLIC SAFETY/EMG M,0 +a17499,SALT RIVER POLICE DEPARTMENT,0 +a174ee,INDIANA STATE POLICE,1 +a17777,NORTH CAROLINA DEPARTMENT OF PUBLIC SAFETY/EMG M,0 +a17850,SALT RIVER POLICE DEPARTMENT,0 +a17e66,WASHINGTON STATE PATROL AVIATION,0 +a17e78,AK-CHIN POLICE DEPARTMENT,0 +a17e8e,DEPARTMENT OF HOMELAND SECURITY,0 +a180e6,CORONA POLICE AERO SQUADRON INC,0 +a18164,DEPARTMENT OF HOMELAND SECURITY,0 +a182ab,CITY OF PHOENIX POLICE DEPT,0 +a18375,CITY OF COLORADO SPRINGS POLICE DEPARTMENT,0 +a18580,DEPARTMENT OF HOMELAND SECURITY,0 +a18618,COFFEE COUNTY SHERIFFS OFFICE,0 +a186a0,CALIFORNIA HIGHWAY PATROL,0 +a189cf,COFFEE COUNTY SHERIFF OFFICE,0 +a18e86,NEW JERSEY STATE POLICE HELICOPTER BUREAU,0 +a18ee5,PINELLAS COUNTY SHERIFF'S OFFICE,0 +a19148,NEW YORK STATE POLICE AVIATION UNIT,1 +a192c0,OKLAHOMA DEPARTMENT OF PUBLIC SAFETY,1 +a19346,NEW YORK STATE POLICE AVIATION UNIT,1 +a19758,COLORADO STATE PATROL,0 +a19a2e,CALIFORNIA HIGHWAY PATROL,0 +a19aba,NEW YORK CITY POLICE AVN UNIT HGNR 4,0 +a19af4,MADISONVILLE POLICE DEPARTMENT,0 +a19b0f,COLORADO STATE PATROL,0 +a19b20,TWIN FALLS COUNTY SHERIFFS OFFICE,0 +a19de5,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +a19e49,CITY OF MESA POLICE DEPT AVIATION DEPT,0 +a19e7e,ALABAMA DEPT OF PUBLIC SAFETY,0 +a19ec6,COLORADO STATE PATROL,0 +a19f67,DEPARTMENT OF HOMELAND SECURITY,0 +a1a200,CITY OF MESA POLICE DEPT/AVIATION DEPT,0 +a1a27d,COLORADO STATE PATROL,0 +a1a528,PASCO SHERIFFS OFFICE,1 +a1a595,COLLIER COUNTY SHERIFFS OFFICE,0 +a1a5df,NEW YORK CITY POLICE AVN UNIT HGNR 4,0 +a1a62a,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a1a674,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a1a88f,METRO-DADE POLICE DEPT/AVIATION UNIT,0 +a1a91a,METRO-DADE POLICE DEPT/AVIATION UNIT,0 +a1a95c,STANISLAUS COUNTY SHERIFFS DEPARTMENT,0 +a1aa2b,VENTURA COUNTY SHERIFF'S DEPT,0 +a1acb5,STATE OF WASHINGTON WASHINGTON STATE PATROL,0 +a1ae80,BENTON COUNTY SHERIFFS OFFICE,0 +a1af93,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY A/C,1 +a1b0ba,COLLIER COUNTY SHERIFFS OFFICE,0 +a1b4bb,MODESTO POLICE DEPARTMENT,0 +a1b5fd,CITY OF EL PASO POLICE DEPARTMENT,0 +a1bb5e,COMMONWEALTH OF VIRGINIA DEPARTMENT OF STATE PO,0 +a1bcfb,DEPARMENT OF HOMELAND SECURITY,0 +a1bed4,POLK COUNTY SHERIFFS OFFICE,0 +a1c0aa,SOUTH DAKOTA HIGHWAY PATROL,1 +a1c178,OSCEOLA COUNTY SHERIFFS OFFICE,1 +a1c246,ALABAMA DEPT OF PUBLIC SAFETY,0 +a1c5a4,LAKE COUNTY SHERIFFS OFFICE,0 +a1c5c0,LOS ANGELES POLICE DEPARTMENT AIR SUPPORT DIVISI,0 +a1c95b,LAKE COUNTY SHERIFFS OFFICE,0 +a1cbc9,AUSTIN POLICE DEPT,0 +a1cd12,LAKE COUNTY SHERIFFS OFFICE,0 +a1cda8,SPOKANE COUNTY SHERIFFS OFFICE,0 +a1cdd4,TEXAS DEPT OF PUBLIC SAFETY,0 +a1cdf3,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a1d296,PORTLAND POLICE BUREAU,1 +a1d75b,OSCEOLA COUNTY SHERIFFS OFFICE,1 +a1d7b9,FORT BEND COUNTY SHERIFFS OFFICE,0 +a1d898,DEPARTMENT OF HOMELAND SECURITY,0 +a1d9c2,STATE OF OREGON DEPARTMENT OF STATE POLICE,0 +a1da3b,LOS ANGELES POLICE DEPT AIR SUPPORT DIVISION,0 +a1db12,OSCEOLA COUNTY SHERIFFS OFFICE,1 +a1dbec,LOS ANGELES POLICE DEPARTMENT,0 +a1dc12,CITY OF MESA POLICE DEPT/AVIATION DEPT,0 +a1dcb0,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a1e24a,CITY OF OAKLAND POLICE DEPARTMENT HELICOPTER U,0 +a1e29f,KENTUCKY STATE POLICE,0 +a1ecaf,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a1ece3,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +a1ef70,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +a1ef84,OREGON STATE POLICE,0 +a1f0c0,PASADENA POLICE DEPARTMENT,0 +a1f17b,NEW YORK STATE POLICE AVIATION UNIT,1 +a1f3bd,EAST BATON ROUGE SHERIFF,0 +a1f4b5,CITY OF MESA POLICE DEPT/PUBLIC INFORMATION OFFI,0 +a1f4dd,BATON ROUGE POLICE DEPARTMENT,0 +a1f77f,U S BORDER PATROL,0 +a1f894,ANAHEIM POLICE DEPT,0 +a1f8fb,TOM GREEN COUNTY SHERIFF'S OFFICE,0 +a1f908,DEPARTMENT OF HOMELAND SECURITY,0 +a1fc32,HILLSBOROUGH COUNTY SHERIFF,0 +a1fca0,IOWA STATE PATROL,0 +a1fcc1,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a1fd9e,DEPARTMENT OF HOMELAND SECURITY,0 +a20129,TOWN OF WEST SENECA POLICE DEPT,0 +a2036f,COLLIER COUNTY SHERIFFS OFFICE,0 +a20403,ST CHARLES PARISH SHERIFFS OFFICE,0 +a205fb,DEPARTMENT OF HOMELAND SECURITY,0 +a208d5,GEORGIA DEPARTMENT OF PUBLIC SAFETY,0 +a20981,DEPARTMENT OF HOMELAND SECURITY,0 +a2099b,LIVINGSTON PARISH SHERIFF'S OFFICE,1 +a20b14,DEPARTMENT OF HOMELAND SECURITY,0 +a20dd5,IOWA STATE PATROL,0 +a21095,GREENVILLE COUNTY SHERIFFS OFFICE,0 +a2144c,GREENVILLE COUNTY SHERIFFS OFFICE,0 +a2147b,WASHOE COUNTY SHERIFF DEPT,0 +a2153f,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +a2165c,DEPARTMENT OF HOMELAND SECURITY,0 +a2165d,DEPARTMENT OF HOMELAND SECURITY,0 +a217dc,FAULKNER COUNTY SHERIFFS DEPARTMENT/,0 +a21819,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +a21a16,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +a21aa4,DEPARTMENT OF HOMELAND SECURITY,0 +a21b1b,ERIE COUNTY SHERIFFS OFFICE,0 +a21bba,GREENVILLE COUNTY SHERIFFS OFFICE,0 +a21f4a,FAULKNER COUNTY SHERIFFS DEPARTMENT/,0 +a21fc5,LOS ANGLELES POLICE DEPT,0 +a2237c,LOS ANGLELES POLICE DEPT,0 +a22569,HOUSTON POLICE AIR SUPPORT DIVISION,0 +a2256b,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a2266c,U S BORDER PATROL,0 +a22733,LOS ANGLELES POLICE DEPT,0 +a22904,NEBRASKA STATE PATROL,0 +a22a23,U S BORDER PATROL,0 +a22b10,MESA POLICE DEPT AVIATION DEPT,0 +a22b45,PASADENA POLICE DEPARTMENT AIR OPERATIONS SECTI,0 +a22e63,YOLO COUNTY/YOLO COUNTY SHERIFFS OFFICE,0 +a23148,CITY OF OMAHA OMAHA POLICE DEPARTMENT,0 +a23268,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a2334f,COLLIER COUNTY SHERIFFS OFFICE,0 +a2376d,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a23c21,LIMESTONE COUNTY SHERIFF DEPARTMENT,0 +a23c45,MESA POLICE DEPARTMENT,0 +a23ce3,TEXAS DEPARTMENT OF PUBLIC SAFETY AIRCRAFT SECT,0 +a23fd8,LIMESTONE COUNTY SHERIFF DEPT,0 +a2421b,WASHINGTON STATE PATROL,0 +a24344,HILLSBOROUGH COUNTY SHERIFFS OFFICE,0 +a24597,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +a2467e,ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a2474a,DEPARTMENT OF HOMELAND SECURITY,0 +a24947,DEPARTMENT OF HOMELAND SECURITY,0 +a24a7f,FORT BEND COUNTY SHERIFFS DEPARTMENT,0 +a24abd,CALIFORNIA HIGHWAY PATROL,0 +a24b01,DEPARTMENT OF HOMELAND SECURITY,0 +a255e2,FLORIDA HIGHWAY PATROL,0 +a257be,CULLMAN POLICE DEPARTMENT,0 +a25bfd,DEPARTMENT OF HOMELAND SECURITY,0 +a263a4,DEPARTMENT OF HOMELAND SECURITY,0 +a265c3,DEPARTMENT OF HOMELAND SECURITY,0 +a2668e,U S BORDER PATROL,0 +a2675b,DEPARTMENT OF HOMELAND SECURITY,0 +a26a38,VICTORIA COUNTY SHERIFF,0 +a26ba5,SACRAMENTO COUNTY SHERIFFS OFFICE,0 +a2719b,BROWARD COUNTY SHERIFFS OFFICE,0 +a27635,COLLIER COUNTY SHERIFFS OFFICE,0 +a276b0,MARTIN COUNTY SHERIFFS OFFICE,0 +a2791b,PINAL COUNTY SHERIFFS OFFICE,0 +a2796c,FORT BEND COUNTY SHERIFFS OFFICE,0 +a27e80,VIRGINIA STATE POLICE,0 +a27ffc,DALE COUNTY SHERIFF OFFICE,0 +a28012,CITY OF MYRTLE BEACH POLICE DEPARTMENT,0 +a283b3,DALE COUNTY SHERIFFS OFFICE,0 +a283e0,DEPARTMENT OF HOMELAND SECURITY,0 +a28676,DALE COUNTY SHERIFFS OFFICE,0 +a2876a,DALE CO SHERIFFS DEPT,0 +a287ff,CULLMAN POLICE DEPARTMENT,0 +a2898d,BRISTOL VA POLICE DEPT,0 +a28a56,U S BORDER PATROL,0 +a28adf,CALIFORNIA HIGHWAY PATROL,0 +a28b21,DALE COUNTY SHERIFF'S OFFICE DALE COUNTY ALABAM,0 +a28b9c,RICHLAND COUNTY SHERIFFS DEPARTMENT OPERATIONS,0 +a28bb6,DALE COUNTY SHERIFF OFFICE,0 +a28ed8,DALE COUNTY SHERIFFS OFFICE,0 +a28f6c,ST CLAIR COUNTY SHERIFFS DEPARTMENT,0 +a291ac,BROWARD COUNTY SHERIFFS OFFICE,0 +a2928f,DALE COUNTY SHERIFFS OFFICE,0 +a292ea,SAN BERNARDINO COUNTY SHERIFF'S DEPARTMENT,0 +a29324,DALE COUNTY SHERIFF OFFICE,0 +a2945a,CALCASIEU PARISH POLICE JURY,0 +a29500,DEPARTMENT OF HOMELAND SECURITY,0 +a2957b,U S BORDER PATROL,0 +a296db,SACRAMENTO COUNTY SHERIFFS DEPARTMENT,0 +a298fe,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a299bb,FLORIDA HIGHWAY PATROL,0 +a29cd1,BROWARD COUNTY SHERIFFS OFFICE,0 +a29ce9,U S BORDER PATROL,0 +a29d03,DALE COUNTY SHERIFFS OFFICE,0 +a29d72,FLORIDA HIGHWAY PATROL,0 +a29e49,DALE COUNTY SHERIFF OFFICE,0 +a2a17a,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +a2a20a,DEPARTMENT OF PUBLIC SAFETY OFFICE OF STATE POL,1 +a2a40e,NEW CASTLE COUNTY POLICE DEPARTMENT,0 +a2a739,OKLA DEPT OF PUBLIC SAFETY OKLA HWY PATROL,0 +a2a791,CITY OF MYRTLE BEACH POLICE DEPARTMENT,0 +a2aa67,U S BORDER PATROL,0 +a2aacc,GALLIA COUNTY SHERIFF'S OFFICE,0 +a2b19d,DEPARTMENT OF HOMELAND SECURITY,0 +a2b331,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a2b58c,U S BORDER PATROL,0 +a2b98c,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a2c211,SACRAMENTO COUNTY SHERIFFS OFFICE,0 +a2c381,DEPARTMENT OF HOMELAND SECURITY,0 +a2c518,CULLMAN POLICE DEPT,0 +a2c81f,U S BORDER PATROL,0 +a2cd7e,VIRGINIA STATE POLICE,0 +a2cef1,TENNESSEE DEPARTMENT OF SAFETY & HOMELAND SECURI,0 +a2d1bf,NEW CASTLE COUNTY POLICE DEPT,0 +a2d56c,AK-CHIN POLICE DEPARTMENT,0 +a2d68a,PASCO COUNTY SHERIFFS OFFICE,1 +a2d6fc,ST CHALRES PARISH SHERIFFS OFFICE,0 +a2d707,KENTUCKY STATE POLICE,0 +a2d91e,DEPARTMENT OF HOMELAND SECURITY,0 +a2dabe,KENTUCKY STATE POLICE,0 +a2dcda,ALACHUA COUNTY SHERIFFS OFFICE,0 +a2dd7f,GRAND TRAVERSE COUNTY SHERIFFS OFFICE,0 +a2ddf8,PASCO COUNTY SHERIFFS OFFICE,1 +a2de75,KENTUCKY STATE POLICE,0 +a2e0aa,BROWARD COUNTY SHERIFF'S OFFICE,0 +a2e0c2,U S BORDER PATROL,0 +a2e136,GRAND TRAVERSE COUNTY SHERIFFS OFFICE,0 +a2e1af,MISSOURI STATE HIGHWAY PATROL,0 +a2e22c,KENTUCKY STATE POLICE,0 +a2e461,BROWARD COUNTY SHERIFF'S OFFICE,0 +a2e479,U S BORDER PATROL,0 +a2e4ed,GRAND TRAVERSE COUNTY SHERIFFS OFFICE,0 +a2e522,CITRUS COUNTY SHERIFFS OFFICE,0 +a2e8a4,GRAND TRAVERSE COUNTY SHERIFFS OFFICE,0 +a2ec85,COUNTY OF SACRAMENTO SACRAMENTO CO SHERIFF'S DE,0 +a2ed47,WASHINGTON COUNTY SHERIFF DEPARTMENT,0 +a2f08b,MISSOURI STATE HIGHWAY PATROL,0 +a2f1e5,SARASOTA COUNTY SHERIFFS OFFICE,0 +a2f592,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a2f637,OKLA DEPT OF PUBLIC SAFETY OKLA HWY PATROL,0 +a2f8c8,DEPARTMENT OF HOMELAND SECURITY,0 +a2f949,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a2f9ca,GWINNETT COUNTY POLICE DEPT,0 +a2fd00,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a300b7,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a300d3,U S BORDER PATROL,0 +a301a7,SOUTH KINGSTOWN POLICE DEPARTMENT,0 +a30825,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a30842,HENRY COUNTY SHERIFFS OFFICE,1 +a308cd,BREVARD COUNTY SHERIFFS OFFICE,1 +a30956,CLAYTON COUNTY POLICE DEPARTMENT,0 +a30bdc,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a30bf9,CLAYTON COUNTY POLICE DEPARTMENT,0 +a30d0d,CITY OF PHILADELPHIA C/O PHILADELPHIA POLICE AV,1 +a30d54,DEPARTMENT OF HOMELAND SECURITY,0 +a30f93,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a31021,WALKER COUNTY SHERIFF DEPT,0 +a310c4,CITY OF PHILADELPHIA C/O PHILADELPHIA POLICE AV,1 +a3134a,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a31367,CLAYTON COUNTY POLICE DEPT,0 +a31369,CITY OF EDINA MN POLICE DEPARTMENT,0 +a313d8,WALKER COUNTY SHERIFF DEPT,0 +a31701,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a3171e,CLAYTON COUNTY POLICE DEPT,0 +a31744,KINGS COUNTY SHERIFFS OFFICE,0 +a3178f,WALKER COUNTY SHERIFF DEPT,0 +a317cd,KINGS COUNTY SHERIFF'S OFFICE,0 +a31832,CLAYTON COUNTY POLICE DEPARTMENT,0 +a31887,NORTH DAKOTA HIGHWAY PATROL,1 +a31a6a,DALE COUNTY SHERIFFS OFFICE,0 +a31bd5,STATE OF NEW JERSEY DEPARTMENT OF LAW AND PUBLI,0 +a31be9,SUFFOLK COUNTY POLICE DEPT,0 +a31e37,CITY OF ONTARIO POLICE DEPARTMENT AIR SUPPORT U,1 +a31ed5,COMMONWEALTH OF VIRGINIA DEPARTMENT OF STATE PO,0 +a3209a,PUTNAM COUNTY SHERIFF'S DEPARTMENT,0 +a320ab,VILLAGE OF PLOVER POLICE DEPARTMENT,0 +a3249d,HARRISON COUNTY SHERIFFS DEPARTMENT,0 +a326c3,AK-CHIN POLICE DEPARTMENT,0 +a32aab,U S BORDER PATROL,0 +a32b34,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a32c0b,HARRIS COUNTY SHERIFF'S OFFICE,0 +a32eeb,NC HIGHWAY PATROL,0 +a32f4f,MIAMI-DADE SHERIFF'S OFFICE,0 +a32f77,CITY OF TUCSON POLICE DEPARTMENT,0 +a32fc2,HARRISON COUNTY SHERIFFS DEPARTMENT,1 +a3323a,WASHINGTON STATE PATROL,0 +a33306,MIAMI-DADE SHERIFF'S OFFICE,0 +a3335f,WASHINGTON STATE PATROL AVIATION SECTION,0 +a33377,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +a3364d,WAYNE COUNTY SHERIFFS OFFICE,0 +a336bd,MIAMI-DADE SHERIFFS OFFICE,0 +a336e5,CITY OF TUCSON POLICE DEPARTMENT,0 +a3372e,SAN BERNARDIINO COUNTY SHERIFFS DEPT,0 +a33a74,MIAMI-DADE SHERIFFS OFFICE,0 +a33ae5,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +a33dbb,ROBESON COUNTY SHERIFFS OFFICE,0 +a33dc7,CALIFORNIA HIGHWAY PATROL,0 +a33e07,LEON COUNTY SHERIFFS OFFICE,0 +a33e2b,MIAMI-DADE SHERIFFS OFFICE,0 +a33e9c,SAN BERNARDINO COUNTY SHERIFFS DEPT,0 +a341e2,MIAMI-DADE SHERIFFS OFFICE,0 +a34253,SAN BERNARDINO COUNTY SHERIFF DEPT,0 +a344e5,DEPARTMENT OF HOMELAND SECURITY,0 +a34a8b,AK-CHIN POLICE DEPARTMENT,0 +a34b45,OHIO DEPARTMENT OF PUBLIC SAFETY OHIO STATE HIG,0 +a34bac,US MARSHALS SERVICE JUSTICE PRISONER & ALIEN TR,0 +a34c19,ALABAMA DEPT OF PUBLIC SAFETY,0 +a34cb1,YELLOWSTONE COUNTY SHERIFF OFFICE,0 +a34efc,NC HIGHWAY PATROL,0 +a34fd3,HARRIS COUNTY SHERIFFS OFFICE,0 +a34fe1,STATE OF MICHIGAN DEPARTMENT OF STATE POLICE,0 +a350c6,MONTPELIER POLICE,0 +a3524f,SAN DIEGO COUNTY SHERIFF DEPT,0 +a355c9,BRUNSWICK COUNTY SHERIFFS OFFICE,0 +a3566a,CALIFORNIA HIGHWAY PATROL,0 +a35e18,PASCO COUNTY SHERIFFS OFFICE,1 +a36111,PASCO SHERIFFS OFFICE,0 +a3618f,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a36546,NORTH CAROLINA HIGHWAY PATROL,0 +a36811,FULTON COUNTY POLICE HEADQUARTERS,0 +a368fd,NORTH CAROLINA HIGHWAY PATROL,0 +a3693d,PASCO COUNTY SHERIFFS OFFICE,1 +a36968,SAN DIEGO COUNTY SHERIFF DEPARTMENT,0 +a369b9,PALM BEACH COUNTY SHERIFFS DEPARTMENT,0 +a36f0d,NORTH CAROLINA HIGHWAY PATROL,0 +a37285,WILMINGTON POLICE DEPARTMENT,0 +a3767b,NORTH CAROLINA HIGHWAY PATROL,0 +a37799,DEPARTMENT OF HOMELAND SECURITY,0 +a378bb,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a37a32,NORTH CAROLINA HIGHWAY PATROL,0 +a37de9,NORTH CAROLINA HIGHWAY PATROL,0 +a384ce,U S BORDER PATROL,0 +a384d7,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a38744,U S DEPARTMENT OF HOMELAND SECURITY U S CUSTOMS,0 +a389a2,SAN ANTONIO POLICE DEPARTMENT,0 +a38c0b,AK-CHIN POLICE DEPARTMENT,0 +a38ff3,U S BORDER PATROL,0 +a3917e,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a39552,COMMONWEALTH OF VIRGINIA DEPARTMENT OF STATE PO,0 +a3961d,COWETA COUNTY SHERIFF'S OFFICE,0 +a39718,CITY OF OAKLAND POLICE DEPARTMENT HELICOPTER UN,0 +a39cc5,POLK COUNTY SHERIFF OFFICE,0 +a39f1b,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a39f96,DEPARTMENT OF HOMELAND SECURITY,0 +a3a255,DELAWARE STATE POLICE,0 +a3a3f5,DEPARTMENT OF HOMELAND SECURITY,0 +a3a9f4,PARK COUNTY SHERIFFS OFFICE,0 +a3ac1c,AK-CHIN POLICE DEPARTMENT,0 +a3af7f,DEPARTMENT OF HOMELAND SECURITY,0 +a3b51b,ERIE COUNTY SHERIFFS OFFICE,0 +a3b741,AK-CHIN POLICE DEPARTMENT,0 +a3b772,U S BORDER PATROL,0 +a3b8d2,ERIE COUNTY SHERIFFS OFFICE,0 +a3bcd1,COMMONWEALTH OF VIRGINIA DEPARTMENT OF STATE PO,0 +a3be97,CITY OF OMAHA OMAHA POLICE DEPARTMENT,0 +a3c1c2,CALIFORNIA HIGHWAY PATROL,0 +a3c633,DEPARTMENT OF HOMELAND SECURITY,0 +a3c6e5,YELLOWSTONE COUNTY SHERIFFS OFFFICE,0 +a3d12a,CITY OF BETHLEHEM POLICE DEPT,0 +a3def1,U S BORDER PATROL,0 +a3e372,MISSISSIPPI COUNTY SHERIFFS DEPARTMENT,0 +a3e4d0,ALACHUA COUNTY SHERIFF,0 +a3e54c,PHOENIX POLICE,0 +a3e5ee,CITY OF MEMPHIS POLICE DEPARTMENT,0 +a3e5f1,MISSISSIPPI DEPARTMENT OF PUBLIC SAFETY,0 +a3e758,STANISLAUS COUNTY SHERIFFS DEPARTMENT,0 +a3e89c,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +a3e9a8,MISSISSIPPI DEPARTMENT OF PUBLIC SAFETY,0 +a3ec6f,U S BORDER PATROL,0 +a3ed84,SUFFOLK COUNTY POLICE DEPARTMENT,0 +a3f29a,WASHINGTON STATE PATROL AVIATION SECTION,0 +a3f372,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a3f4f2,SUFFOLK COUNTY POLICE DEPARTMENT,0 +a3f547,LOUISIANA STATE POLICE,1 +a3f66c,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a3f68b,KERN COUNTY SHERIFFS DEPARTMENT,0 +a3f7bc,DESOTO PARISH SHERIFF OFFICE,0 +a3f8fe,LOUISIANA STATE POLICE,1 +a3fa65,DEPARTMENT OF HOMELAND SECURITY,0 +a3fa88,DEPARTMENT OF HOMELAND SECURITY,0 +a3fb1a,AK-CHIN POLICE DEPARTMENT,0 +a3fc14,LAKE COUNTY INDIANA C/O LAKE COUNTY SHERIFF'S D,1 +a3fcb5,LOUISIANA DEPARTMENT OF PUBLIC SAFETY,1 +a4006c,LOUISIANA DEPARTMENT OF PUBLIC SAFETY STATE POL,1 +a40384,COLLIER COUNTY SHERIFFS OFFICE,0 +a403fe,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a406bb,PHOENIX POLICE DEPARTMENT AIR SUPPORT UNIT,0 +a40b91,KENTUCKY STATE POLICE,0 +a40bcf,COMMONWEALTH OF VIRGINIA DEPT OF STATE POLICE,0 +a41146,STATE OF NEW YORK DIVISION OF STATE POLICE,0 +a411c2,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a414fd,STATE OF NEW YORK DIVISION OF STATE POLICE,0 +a417a5,U S BORDER PATROL,0 +a41e9b,CITY OF COVINGTON COVINGTON POLICE DEPARTMENT,0 +a423ca,STATE OF NEW HAMPSHIRE DEPT OF SAFETY DIVISION,0 +a42434,NEW HAMPSHIRE STATE POLICE,0 +a427c6,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a42eba,CITY OF OXFORD ALABAMA POLICE DEPARTMENT,0 +a43488,OKLAHOMA DEPARTMENT OF PUBLIC SAFETY,1 +a4356b,DEPARTMENT OF HOMELAND SECURITY,0 +a437d9,CITY OF BAKERSFIELD CALIFORNIA POLICE DEPT,1 +a43834,HENRY COUNTY SHERIFF'S OFFICE,0 +a43dab,U S DEPARTMENT OF HOMELAND SECURITY U S CUSTOMS,0 +a43f25,CANADIAN COUNTY SHERIFFS OFFICE,0 +a43fa2,HENRY COUNTY SHERIFFS OFFICE,0 +a44021,NEBRASKA STATE PATROL,0 +a442dc,CANADIAN COUNTY SHERIFFS OFFICE,0 +a44359,HENRY COUNTY SHERIFF'S OFFICE,0 +a44466,TEXAS DEPT. OF PUBLIC SAFETY,0 +a44710,HENRY COUNTY SHERIFF'S OFFICE,0 +a449d6,HENRY COUNTY SHERIFF'S OFFICE,0 +a45c07,OKLAHOMA DEPARTMENT OF PUBLIC SAFETY,0 +a45c61,MARYLAND STATE POLICE AVIATION COMMAND,0 +a46018,MARYLAND STATE POLICE AVIATION COMMAND,0 +a46095,PINAL COUNTY SHERIFF'S OFFICE,0 +a4612a,YELLOWSTONE COUNTY SHERIFF OFFICE,0 +a46140,DEPARTMENT OF HOMELAND SECURITY,1 +a462bb,AK-CHIN POLICE DEPARTMENT,0 +a463cf,MARYLAND STATE POLICE AVIATION COMMAND,0 +a4644c,PINAL COUNTY SHERIFF'S OFFICE,0 +a46786,MARYLAND STATE POLICE AVIATION COMMAND,0 +a46803,PINAL COUNTY SHERIFF'S OFFICE,0 +a46811,STATE OF MICHIGAN DEPARTMENT OF STATE POLICE DE,0 +a46b3d,MARYLAND STATE POLICE AVIATION COMMAND,0 +a46ef4,MARYLAND STATE POLICE AVIATION COMMAND,0 +a472ab,MARYLAND STATE POLICE AVIATION COMMAND,0 +a47662,MARYLAND STATE POLICE AVIATION COMMAND,0 +a47a19,MARYLAND STATE POLICE AVIATION COMMAND,0 +a47a95,ST CHARLES PARISH SHERIFFS OFFICE,0 +a47dd0,MARYLAND STATE POLICE AVIATION COMMAND,0 +a47e4c,ST CHARLES PARISH SHERIFFS OFFICE,0 +a482e1,SALT RIVER POLICE DEPARTMENT,0 +a4833b,ARKANSAS STATE POLICE,0 +a483e0,MARYLAND STATE POLICE AVIATION COMMAND,0 +a48467,STATE OF ARKANSAS ARKANSAS STATE POLICE,0 +a48797,MARYLAND STATE POLICE AVIATION COMMAND,0 +a4906a,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a490b0,DEPARTMENT OF HOMELAND SECURITY CUSTOMS & BORDE,0 +a490d3,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,1 +a490f6,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,1 +a4913c,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a4915f,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a49182,DEPT OF HOMELAND SECURITY CUSTOMS & BORDER PROT,0 +a4933d,SALINA COUNTY SHERIFFS DEPT,0 +a493fe,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a4948a,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a494d0,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a494f3,DEPARTMENT OF HOMELAND SECURITY CUSTOMS & BORDE,0 +a49539,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a497d8,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a4981e,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a49b6c,DEPARTMENT OF HOMELAND SECURITY,0 +a49f5c,LIMESTONE COUNTY SHERIFF DEPT,0 +a49f69,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a49f6f,COUNTY OF MERCED SHERIFF'S DEPT,0 +a4a2da,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a4a343,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a4a691,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a4a924,STATE OF NEW JERSEY NEW JERSEY STATE POLICE AVI,0 +a4abda,SAN BERNADINO COUNTY SHERIFFS OFFICE,0 +a4ac24,COMMONWEALTH OF VIRGINA DEPT OF STATE POLICE,0 +a4b107,ST JOHNS COUNTY SHERIFF OFFICE,1 +a4b179,MIAMI-DADE SHERIFFS OFFICE,0 +a4b187,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4b1f6,CONNECTICUT STATE POLICE AVIATION SECTION,1 +a4b4da,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a4b53e,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4b558,CITY OF OMAHA OMAHA POLICE DEPARTMENT,0 +a4b5ad,CONNECTICUT STATE POLICE AVIATION SECTION,0 +a4b8e7,MIAMI-DADE POLICE DEPT,0 +a4b8f5,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4b960,UNITED STATES CUSTOMS AND BORDER PROTECTION NAT,0 +a4bae2,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a4bcac,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4bef8,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a4c063,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4c07b,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a4c0f3,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a4c246,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a4c32b,CALCASIEU PARISH POLICE JURY MOSQUITO CONTROL,0 +a4c41a,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4c434,CHARLOTTE-MECKLENBURG POLICE DEPT,1 +a4c670,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a4c6d7,COUNTY OF CONTRA COSTA OFFICE OF THE SHERIFF,1 +a4c6f0,SAN JOSE POLICE DEPARTMENT,0 +a4c6f1,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a4c72a,SARASOTA COUNTY SHERIFFS OFFICE,0 +a4c76e,ORANGE COUNTY SHERIFF,0 +a4c785,ORANGE COUNTY SHERIFFS OFFICE,0 +a4c794,KING COUNTY SHERIFFS OFFICE,0 +a4c7a8,ORANGE COUNTY SHERIFFS OFFICE,0 +a4c7d1,NC HIGHWAY PATROL,0 +a4c7d9,ORANGE COUNTY SHERIFF'S OFFICE,0 +a4c7e0,ALACHUA COUNTY SHERIFF,0 +a4c7e5,NEW YORK CITY POLICE AVIATION UNIT,0 +a4c7f5,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a4c81b,ALACHUA COUNTY SHERIFF,0 +a4c834,SANTA BARBARA SHERIFFS DEPT,0 +a4c840,DELAWARE STATE POLICE,0 +a4c843,STATE OF INDIANA INDIANA STATE POLICE,1 +a4c877,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a4cb16,CALIFORNIA HIGHWAY PATROL,0 +a4cb7a,MIAMI-DADE SHERIFF'S OFFICE,0 +a4cb88,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4cbed,COUNTY OF SANTA CLARA OFFICE OF THE SHERIFF AI,0 +a4cf31,MIAMI-DADE SHERIFF'S OFFICE,0 +a4cf3f,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4d240,MEMPHIS POLICE DEPARTMENT,0 +a4d452,DEPARTMENT OF HOMELAND SECURITY,0 +a4d4d2,HENNEPIN COUNTY SHERIFFS OFFICE,0 +a4d54f,NORTH CAROLINA STATE HIGHWAY PATROL,0 +a4d98e,CITY OF TAMPA POLICE DEPARTMENT,1 +a4db92,GEORGIA DEPT OF PUBLIC SAFETY,0 +a4dc0f,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a4dc24,DEPARTMENT OF HOMELAND SECURITY,0 +a4dd4d,BERNAILILLO COUNTY SHERIFFS OFFICE,0 +a4dd6c,COUNTY OF VENTURA SHERIFFS OFFICE,0 +a4e019,HILLSBOROUGH COUNTY SHERIFFS OFFICE,0 +a4e940,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a4eb1c,HENNEPIN COUNTY SHERIFFS OFFICE,0 +a4f149,KEARNEY POLICE DEPARTMENT,0 +a4f5e8,DUNWOODY POLICE DEPARTMENT,0 +a4f670,DEPARTMENT OF HOMELAND SECURITY,0 +a4f7ef,MONTPELIER POLICE,0 +a4f8af,MONTPELIER POLICE DEPARTMENT,0 +a4fe7c,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a4ff6f,DEPARTMENT OF HOMELAND SECURITY,0 +a500f4,KENTUCKY STATE POLICE,0 +a50351,KING COUNTY SHERIFFS OFFICE,0 +a5085e,UNITED STATES CUSTOMS AND BORDER PROTECTION OFF,0 +a50e9f,CITY OF PORTLAND PORTLAND POLICE BUREAU,0 +a50efd,JACKSON COUNTY SHERIFFS OFFICE,0 +a50f7b,JACKSON POLICE DEPARTMENT,1 +a515ac,STATE OF OREGON OREGON STATE POLICE,0 +a5173e,STATE OF NEW YORK DIVISION OF STATE POLICE,0 +a51e51,NEW YORK CITY POLICE AVIATION UNIT,0 +a51e57,NEW YORK CITY POLICE DEPARTMENT,0 +a51eac,ARKANSAS STATE POLICE,0 +a51eec,VENTURA COUNTY SHERIFFS DEPARTMENT,0 +a52259,PINELLAS COUNTY SHERIFFS OFFICE,0 +a52353,CHARLESTON COUNTY SHERIFFS OFFICE,0 +a52431,STATE OF LOUISIANA DEPARTMENT OF PUBLIC SAFETY,0 +a524dd,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a5286f,UNITED STATES CUSTOMS AND BORDER PROTECTION OFF,0 +a52bcf,STATE OF NEW YORK DIVISION OF STATE POLICE,1 +a534b4,DEPARTMENT OF HOMELAND SECURITY,0 +a534d3,HOOD RIVER COUNTY SHERIFF'S OFFICE STATE OF ORE,0 +a539b4,MICHIGAN DEPARTMENT OF STATE POLICE,0 +a53eb0,SUFFOLK COUNTY SHERIFFS OFFICE,0 +a54be6,CITY OF OAKLAND POLICE DEPARTMENT HELICOPTER UNI,0 +a54dfb,COLLIER COUNTY SHERIFFS OFFICE,0 +a54f9d,BATON ROUGE POLICE DEPARTMENT,0 +a5520e,ALAMEDA COUNTY SHERIFFS OFFICE,0 +a55354,DESOTO PARISH SHERIFF OFFICE,0 +a5567f,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a55b17,DELAWARE STATE POLICE,0 +a55d3d,IOWA STATE PATROL,0 +a55f4a,NEW CASTLE COUNTY POLICE DEPARTMENT (DE),0 +a57013,MARSHALL COUNTY SHERIFF DEPT,0 +a57161,KENTUCKY STATE POLICE,0 +a58178,FULTON COUNTY SHERIFFS OFFICE,0 +a58241,COUNTY OF SUFFOLK POLICE DEPARTMENT,0 +a582a3,DEPARTMENT OF PUBLIC SAFETY OF THE STATE OF TEXA,0 +a5852f,FULTON COUNTY SHERIFFS OFFICE,0 +a585f8,CHARLOTTE MECKLENBURG POLICE DEPARTMENT,1 +a58923,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +a590d3,COLLIER COUNTY SHERIFFS OFFICE,0 +a598e0,KENTUCKY STATE POLICE,0 +a599b3,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a59d6a,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a5a0a4,STATE OF MICHIGAN DEPARTMENT OF STATE POLICE A,0 +a5a121,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5a4d8,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5a88f,STATE OF ARIZONA ARIZONA DEPARTMENTOF PUBLIC SA,0 +a5ac46,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5affd,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5b045,GEORGIA DEPT OF PUBLIC SAFETY,0 +a5b0fb,MONROE COUNTY SHERIFFS OFFICE,0 +a5b114,NEW CASTLE COUNTY POLICE DEPT,0 +a5b3b4,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5b76b,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +a5b869,MONROE COUNTY SHERIFFS OFFICE,0 +a5b882,NEW CASTLE COUNTY POLICE DEPT,0 +a5b948,WINDWARD AVIATION INC MAUI POLICE DEPARTMENT,0 +a5bb22,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5bc20,MONROE COUNTY SHERIFFS OFFICE,0 +a5bc39,NEW CASTLE COUNTY POLICE DEPT,0 +a5c249,NEW CASTLE COUNTY POLICE DEPT,0 +a5c550,FORT BEND COUNTY SHERIFFS OFFICE,0 +a5c600,NEW CASTLE COUNTY POLICE DEPT,0 +a5c6a4,DEPARTMENT OF HOMELAND SECURITY,1 +a5c8bd,CITRUS COUNTY SHERIFF'S OFFICE,0 +a5c9b7,NEW CASTLE COUNTY POLICE DEPT,0 +a5ca5b,DEPARTMENT OF HOMELAND SECURITY,1 +a5ccbe,FORT BEND COUNTY SHERIFFS OFFICE,0 +a5cd6e,NEW CASTLE COUNTY POLICE DEPT,0 +a5ce12,DEPARTMENT OF HOMELAND SECURITY,1 +a5d125,NEW CASTLE COUNTY POLICE DEPT,0 +a5d1c9,DEPARTMENT OF HOMELAND SECURITY,1 +a5d4dc,NEW CASTLE COUNTY POLICE DEPT,0 +a5d580,DEPARTMENT OF HOMELAND SECURITY,1 +a5d893,NEW CASTLE COUNTY POLICE DEPT,0 +a5d937,DEPARTMENT OF HOMELAND SECURITY,1 +a5dc4a,NEW CASTLE COUNTY POLICE DEPT,0 +a5dcee,DEPARTMENT OF HOMELAND SECURITY,1 +a5df8f,UTAH HIGHWAY PATROL,0 +a5e001,NEW CASTLE COUNTY POLICE DEPT,0 +a5e0a5,DEPARTMENT OF HOMELAND SECURITY,1 +a5e2bb,DEPARTMENT OF HOMELAND SECURITY,0 +a5e3b8,NEW CASTLE COUNTY POLICE DEPT,0 +a5e45c,DEPARTMENT OF HOMELAND SECURITY,0 +a5e8b1,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5e9ed,PLAQUEMINES PARISH SHERIFF'S OFFICE,0 +a5ea33,U S CUSTOMS & BORDER PROTECTION NATIONAL AIR SE,0 +a5ea6c,DEPARTMENT OF HOMELAND SECURITY,0 +a5ec68,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a5ed98,ORANGE COUNTY SHERIFF,0 +a5edca,ORANGE COUNTY SHERIFF'S OFFICE,0 +a5edd2,ORANGE COUNTY SHERIFF,0 +a5f01f,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5f3d6,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5f78d,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5f791,BROWARD COUNTY SHERIFF'S OFFICE,0 +a5fb44,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a5fc75,MONTPELIER POLICE DEPARTMENT,0 +a602b2,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6034b,COUNTY OF BUTTE A POLITICAL SUBDIVISION OF THE,0 +a60669,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a6066d,BUTTE COUNTY SHERIFFS OFFICE,0 +a60a20,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a60ba6,MN STATE PATROL,0 +a6165b,SANTA BARBARA COUNTY SHERIFFS DEPARTMENT,0 +a62a7d,KERN COUNTY SHERIFFS DEPARTMENT,0 +a62b61,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a63208,NEW CASTLE COUNTY POLICE DEPT (DE),0 +a6323a,NEW CASTLE COUNTY POLICE DEPT (DE),0 +a6326c,NEW CASTLE COUNTY POLICE DEPT (DE),0 +a63673,STATE OF NEW JERSEY DEPT OF LAW AND PUBLIC SAFE,0 +a63a68,SNOHOMISH COUNTY SHERIFF,0 +a63b9f,PINE BLUFF POLICE DEPARTMENT JEFFERSON COUNTY S,0 +a63e64,HONOLULU POLICE DEPARTMENT CITY & COUNTY OF HON,0 +a63e80,SHELBY COUNTY SHERIFFS OFFICE,0 +a63f2a,CITY OF PASADENA POLICE DEPARTMENT-AVIATION SEC,0 +a63f65,ANDERSON COUNTY SHERIFFS OFFICE,0 +a6421b,HONOLULU POLICE DEPARTMENT,0 +a6427f,CITY OF MESA POLICE DEPT/AVIATION DEPT,0 +a64662,CITY OF MIAMI POLICE DEPARTMENT,0 +a64989,FLORIDA HIGHWAY PATROL,0 +a64c91,ALBUQUERQUE POLICE DEPARTMENT,0 +a64ce2,SPOKANE COUNTY SHERIFF'S OFFICE,0 +a64d34,OFFICE OF THE SHERIFF COUNTY OF BUTTE CALIFORNI,0 +a64da4,CITY OF MESA POLICE DEPT AVIATION UNIT,1 +a64e41,ANDERSON COUNTY SHERIFFS OFFICE,0 +a64e48,UTAH COUNTY SHERIFF,0 +a65512,MESA POLICE DEPT,1 +a6558f,STATE OF INDIANA INDIANA STATE POLICE,1 +a655af,ANDERSON COUNTY SHERIFFS OFFICE,0 +a65865,CALIFORNIA HIGHWAY PATROL,0 +a658c9,MESA POLICE DEPT/AVIATION UNIT,0 +a65946,LOUISIANA STATE POLICE,1 +a65bbe,SPOKANE COUNTY SHERIFFS OFFICE,0 +a65c38,SPOKANE COUNTY SHERIFFS OFFICE,0 +a65c80,MESA POLICE DEPT/AVIATION UNIT,0 +a65cf2,SPOKANE COUNTY SHERIFFS OFFICE,0 +a65fcb,SAN MATEO COUNTY SHERIFF'S OFFICE,1 +a660b4,DEPARTMENT OF PUBLIC SAFETY DIVISION OF STATE P,0 +a6628f,DEKALB COUNTY SHERIFFS OFFICE,0 +a665e3,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a66625,SALT RIVER POLICE DEPT,0 +a669da,LAKE COUNTY SHERIFF DEPARTMENT,0 +a66a26,DETROIT POLICE AIR SUPPORT,0 +a66b77,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a66d51,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +a66d91,LAKE COUNTY SHERIFFS DEPT,0 +a66ddd,CITY OF DETROIT POLICE DEPARTMENT,0 +a67108,STATE OF OHIO / DEPARTMENT OF PUBLIC SAFETY OHI,0 +a67117,CITY OF FRESNO POLICE DEPARTMENT SKYWATCH OPERA,0 +a67148,LAKE COUNTY SHERIFFS DEPARTMENT,0 +a67194,CITY OF DETROIT DETROIT PUBLIC SAFETY HEADQUART,0 +a674ff,LAKE COUNTY SHERIFF DEPARTMENT,0 +a67c51,DEPARTMENT OF HOMELAND SECURITY,0 +a67c6d,LAKE COUNTY SHERIFF,0 +a67f83,VOLUSIA SHERIFFS OFFICE,0 +a68024,LAKE COUNTY SHERIFF,0 +a681e2,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a68235,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +a683db,LAKE COUNTY SHERIFF,0 +a689eb,LAKE COUNTY SHERIFF,0 +a68a37,CITY OF GLENDALE POLICE DEPARTMENT CITY OF BURB,1 +a68a85,LOUISVILL POLICE,0 +a68a8c,ARKANSAS STATE POLICE,0 +a68ba3,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a68bb5,DALE COUNTY SHERIFFS OFFICE,0 +a68bd9,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a68be0,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a68cd6,BAXTER COUNTY SHERIFFS OFFICE,0 +a68df1,PRINCE GEORGES COUNTY POLICE AVIATION SECTION,0 +a68e43,DEPARTMENT OF ARKANSAS STATE POLICE,0 +a6903d,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a69164,GUERNSEY COUNTY SHERIFFS OFFICE,0 +a691fa,ARKANSAS DEPARTMENT OF CORRECTION,0 +a693ea,CALCASIEU PARISH SHERIFFS OFFICE,1 +a6943c,EAST BATON ROUGE SHERIFF,0 +a695a7,EAST BATON ROUGE SHERIFF,0 +a695b1,ARKANSAS STATE POLICE,0 +a697c2,RIVERSIDE COUNTY SHERIFFS DEPARTMENT,0 +a698f2,CITY OF FRESNO POLICE DEPT SKYWATCH OPERATIONS,0 +a69968,ARKANSAS STATE POLICE,0 +a69c4c,JACKSON COUNTY SHERIFFS DEPARTMENT,0 +a69cca,CITY OF GLENDALE POLICE DEPT,0 +a69d1f,ARKANSAS STATE POLICE,0 +a6a0b1,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a6a4e5,ALACHUA COUNTY SHERIFF,0 +a6a56c,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a6ab5c,COLLIER COUNTY SHERIFFS OFFICE,0 +a6aefc,KENTUCKY STATE POLICE,0 +a6aff0,COMMONWEALTH OF VIRGINIA DEPT OF STATE POLICE,0 +a6b07b,JEFFERSON COUNTY POLICE DEPT,0 +a6b089,BUTTE COUNTY SHERIFFS OFFICE,0 +a6b0d4,EL DORADO COUNTY SHERIFFS OFFICE,0 +a6b140,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +a6b1b6,ALBUQUERQUE POLICE DEPARTMENT,0 +a6b1b9,PRINCE GEORGES COUNTY POLICE AVIATION SECTION,0 +a6b201,EAST BATON ROUGE PARISH SHERIFF,0 +a6b20b,LOUISIANA HIGHWAY PATROL C/O FATC,0 +a6b27a,OXFORD POLICE DEPARTMENT,0 +a6b4e1,FLORIDA HIGHWAY PATROL DEPARTMENT OF HIGHWAY SA,0 +a6b56d,ALBUQUERQUE POLICE DEPARTMENT,0 +a6bcf3,CALHOUN COUNTY SHERIFFS OFFICE,0 +a6bd25,CALHOUN COUNTY SHERIFFS OFFICE,0 +a6bd88,BAY COUNTY SHERIFF'S DEPT,0 +a6bf32,MAINE STATE POLICE,0 +a6c08e,DEPARTMENT OF HOMELAND SECURITY,0 +a6c092,FRESNO POLICE DEPARTMENT,0 +a6c50d,OXFORD POLICE DEPARTMENT,0 +a6c703,MICHIGAN DEPARTMENT OF STATE POLICE,0 +a6c800,WESTCHESTER COUNTY DEPARTMENT OF PUBLIC SAFETY,0 +a6cbb3,PUTNAM COUNTY SHERIFFS OFFICE,0 +a6d692,WILMINGTON POLICE DEPARTMENT,0 +a6d731,INDIANA STATE POLICE,1 +a6d7fa,JEFFERSON COUNTY POLICE DEPT,0 +a6d804,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6dbbb,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6dc39,DEPARTMENT OF HOMELAND SECURITY,0 +a6dcea,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a6df72,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6e329,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6e6e0,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6ea97,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6ebc8,EUCLID POLICE DEPARTMENT C/O SGT ADAM BEESE,0 +a6ebde,ADAMS COUNTY SHERIFFS OFFICE,0 +a6ec39,OCONEE COUNTY SHERIFF DEPT,0 +a6ee4e,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6f0c7,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a6f205,STATE OF ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +a6f4eb,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a6f5bc,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a6f5c0,BUTTE COUNTY SHERIFFS OFFICE,0 +a6f6fe,SAN JOAQUIN COUNTY SHERIFFS OFFICE,0 +a6f973,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a6faf3,SALINA COUNTY SHERIFFS DEPT,0 +a6fb56,CANYON COUNTY SHERIFF'S OFFICE,0 +a70028,KANSAS HIGHWAY PATROL,0 +a7005d,KANSAS HIGHWAY PATROL,1 +a7033a,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +a70370,SAN JOSE POLICE DEPARTMENT,0 +a7044f,CA STATE UNIVERSITY MONTEREY BAY POLICE DEPT,0 +a704c0,ILLINOIS STATE POLICE,0 +a70519,CALCASIEU PARISH POLICE JURY,0 +a70713,ORANGE COUNTY SHERIFF'S OFFICE,0 +a7086d,EAST BATON ROUGE SHERIFF,0 +a709f3,SHERIFFS DEPARTMENT COUNTY OF ALAMEDA,1 +a70ad0,CITY OF COLUMBUS DIVISION OF POLICE,0 +a70b4d,CALIFORNIA HIGHWAY PATROL,0 +a70f83,MD DNR POLICE DEPT,0 +a71392,EAST BATON ROUGE PARISH SHERIFF,0 +a715f5,CITY OF COLUMBUS DIVISION OF POLICE,0 +a71749,HARRIS COUNTY SHERIFFS OFFICE,0 +a71774,TEXAS DEPT OF PUBLIC SAFETY,0 +a719ac,CITY OF COLUMBUS DIVISION OF POLICE,0 +a71fc4,ALASKA DEPARTMENT OF PUBLIC SAFETY AIRCRAFT SEC,1 +a724a9,STATE OF ARIZONA DEPARTMENT OF PUBLIC SAFETY,1 +a72a1e,GRANT PARISH SHERIFFS OFFICE,0 +a72aa1,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a72ab9,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +a72b4d,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a72c3f,ILLINOIS STATE POLICE AIR OPERATIONS BUREAU,0 +a72ff1,ST JAMES PARISH SHERIFFS OFFICE,0 +a741a8,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +a74523,HIGHLAND COUNTY SHERIFFS OFFICE,0 +a749fa,BREVARD COUNTY SHERIFFS OFFICE,0 +a74e77,JEFFERSON COUNTY POLICE DEPT,0 +a75028,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a7591f,LOUISIANA STATE POLICE,1 +a75920,DEPARTMENT OF PUBLIC SAFETY OFFICE OF STATE POL,1 +a75ede,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +a7622d,NEBRASKA STATE PATROL,0 +a762f3,VILLAGE OF WINTHROP HARBOR POLICE DEPARTMENT,0 +a76c6f,VILLAGE OF LAKE VILLA POLICE DEPARTMENT,0 +a773a7,STATE OF ARIZONA DEPARTMENT OF PUBLIC SAFETY A,1 +a7740f,FLORENCE COUNTY SHERIFF DEPT,0 +a775f3,SHELBY COUNTY SHERIFFS OFFICE,0 +a775f6,JEFFERSON COUNTY POLICE DEPT,0 +a779aa,SHELBY COUNTY SHERIFFS OFFICE,0 +a77b32,SHELBY COUNTY SHERIFFS OFFICE,0 +a785df,MOBILE POLICE DEPARTMENT,0 +a787c8,DESOTO CTY SHERIFF DEPT,0 +a7892d,HENRY COUNTY SHERIFF'S OFFICE,0 +a78aed,PINELLAS COUNTY SHERIFFS OFFICE,0 +a79d75,JEFFERSON COUNTY POLICE DEPT,0 +a79ee0,PALM BEACH COUNTY SHERIFFS OFFICE AIR OPERATION,0 +a7a50b,CUSTOMS AND BORDER PROTECTION,0 +a7a61e,WESTCHESTER COUNTY DEPT OF PUBLIC SAFETY TRNSPN,0 +a7a64e,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a7a9d4,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a7aa05,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a7adbc,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a7b173,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a7b52a,PALM BEACH COUNTY SHERIFFS OFFICE,0 +a7c01f,MONTPELIER POLICE DEPARTMENT,0 +a7c06a,PIMA COUNTY SHERIFFS DEPARTMENT,0 +a7c136,MONTPELIER POLICE DEPARTMENT,0 +a7c34a,STATE OF OHIO DEPARTMENT OF PUBLIC SAFETY OHIO,0 +a7c3c2,NEW JERSEY STATE POLICE,0 +a7c624,CITY OF ONTARIO POLICE DEPARTMENT AIR SUPPORT U,0 +a7c6a5,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a7c7fc,KANSAS HIGHWAY PATROL,0 +a7c8d3,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a7ca94,POLICE FACILITY DESIGN GROUP PA,0 +a7cbb3,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +a7cc3f,NASSAU COUNTY POLICE DEPARTMENT,0 +a7cc8a,JEFFERSON COUNTY SHERIFFS OFFICE,1 +a7cec5,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a7cff6,NASSAU COUNTY POLICE DEPARTMENT,0 +a7d041,JEFFERSON COUNTY SHERIFF'S OFFICE,0 +a7d06a,DEPARTMENT OF HOMELAND SECURITY,0 +a7d3ad,NASSAU CNTY POLICE DEPT,0 +a7d3f9,NEW MEXICO STATE POLICE,0 +a7d764,NASSAU COUNTY POLICE DEPARTMENT AIR BUREAU,0 +a7d925,ORANGE COUNTY SHERIFFS DEPARTMENT RNSP,0 +a7d9d1,NEW YORK STATE POLICE,1 +a7dae8,MANATEE COUNTY SHERIFFS OFFICE,0 +a7db1b,NASSAU COUNTY POLICE DEPT,0 +a7ded2,NASSAU COUNTY POLICE DEPARTMENT AIR BUREAU,0 +a7e2de,NEW MEXICO STATE POLICE AIRCRAFT SECTION,0 +a7e2ec,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a7e68b,SNOHOMISH COUNTY SHERIFFS OFFICE,0 +a7e695,NEW MEXICO STATE POLICE,0 +a7e7f0,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a7e847,U S DEPT OF HOMELAND SECURITY U S CUSTOMS & BOR,0 +a7e869,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a7ea4c,NEW MEXICO STATE POLICE,0 +a7eb79,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a7ee03,NEW YORK STATE POLICE,1 +a7eebe,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a7f007,NASSAU COUNTY POLICE DEPARTMENT,0 +a7f075,CITY OF TAMPA POLICE DEPT,1 +a7f195,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a7f2f5,FLAGLER COUNTY SHERIFFS OFFICE,0 +a7f332,FLORIDA HIGHWAY PATROL,0 +a7fa03,PIMA COUNTY SHERIFF DEPT,0 +a7fa17,PIMA COUNTY SHERIFF DEPT,0 +a7faae,PIMA COUNTY SHERIFF DEPT,0 +a7ff7a,OFFICE OF HOMELAND SECURITY COLUMBUS GA CONSOLI,0 +a802ec,CUSTOMS AND BORDER PROTECTION,0 +a80348,WILLIAMSON COUNTY SHERIFF OFFICE,0 +a8054b,UTICA POLICE DEPARTMENT,0 +a8097c,CALIFORNIA HIGHWAY PATROL,0 +a80a08,CULLMAN POLICE DEPARTMENT,0 +a80ab6,WILLIAMSON COUNTY SHERIFF'S OFFICE,0 +a80cd2,VOLUSIA SHERIFFS OFFICE,0 +a816fa,DEPARTMENT OF PUBLIC SAFETY,1 +a81709,WAUKESHA POLICE DEPT,0 +a8184d,CHEROKEE SHERIFFS OFFICE,0 +a81a73,CITY OF PHOENIX POLICE AIR SUPPORT UNIT,0 +a81c6f,DEPARTMENT OF HOMELAND SECURITY,0 +a81e2a,CITY OF PHOENIX POLICE AIR SUPPORT UNIT,0 +a820f8,AUSTIN POLICE DEPT,0 +a82214,HERNANDO COUNTY SHERIFFS OFFICE,0 +a82598,CITY OF PHOENIX POLICE AIR SUPPORT UNIT,0 +a8294f,CITY OF PHOENIX POLICE AIR SUPPORT UNIT,0 +a82a62,SAN BERNARDINO COUNTY SHERIFFS DEPARTMENT,0 +a82f24,BRUNSWICK COUNTY SHERIFFS OFFICE,0 +a83dd4,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a83e79,OKLAHOMA DEPARTMENT OF PUBLIC SAFETY,0 +a83e96,DALE COUNTY SHERIFFS OFFICE,0 +a8416f,HERNANDO COUNTY SHERIFF,0 +a8418b,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a8426d,DEPARTMENT OF HOMELAND SECURITY,0 +a84305,SAN BERNARDINO COUNTY SHERIFFS DEPT,0 +a84311,ILLINOIS STATE POLICE,0 +a8431f,TUSCALOOSA POLICE DEPARTMENT,0 +a8447d,JAY CONSTABLE,0 +a846d6,ETOWAH COUNTY SHERIFFS OFFICE,0 +a846dd,KENTUCKY STATE POLICE,0 +a848f9,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a84a75,OAKLAND COUNTY SHERIFF'S DEPARTMENT,0 +a84b3c,LOS ANGELES COUNTY SHERIFFS DEPARTMENT,0 +a84cb0,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a84e44,TUSCALOOSA COUNTY SHERIFFS OFFICE,0 +a84ef6,COMMONWEALTH OF MASSACHUSETTS MASSACHUSETTS STA,0 +a85067,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,0 +a851e3,OAKLAND COUNTY SHERIFFS OFFICE,0 +a85230,HIGHLAND COUNTY SHERIFFS OFFICE,0 +a85452,MICHIGAN STATE POLICE,0 +a8594f,SAN BERNARDINO COUNTY SHERIFF'S DEPT,0 +a85d06,SAN BERNARDINO COUNTY SHERIFF'S DEPT,0 +a860bd,SAN BERNARDINO COUNTY SHERIFF'S DEPT,0 +a865f8,KANSAS HIGHWAY PATROL,0 +a867cc,CITY OF MEMPHIS POLICE DEPT,0 +a86819,STATE OF LOUISIANA DEPARTMENT OF PUBLIC SAFETY,0 +a874fb,KANSAS CITY POLICE DEPT,0 +a87b5d,CHARLOTTE COUNTY SHERIFFS OFFICE,0 +a87c5e,WASHINGTON STATE PATROL,0 +a8815e,WILLIAMSON COUNTY SHERIFFS OFFICE,0 +a88b1e,OHIO DEPARTMENT OF PUBLIC SAFETY,0 +a88cd2,STATE OF ARIZONA ARIZONA DEPT OF PUBLIC SAFETY,0 +a88d77,KANSAS HIGHWAY PATROL,0 +a890a3,DEPARTMENT OF HOMELAND SECURITY,0 +a8914a,COLORADO STATE PATROL,0 +a8920f,NEW YORK STATE POLICE,1 +a89372,CANTON POLICE DEPARTMENT,0 +a8a06e,METRA POLICE DEPT,0 +a8a425,METRA POLICE DEPARTMENT,0 +a8ab88,MARTIN COUNTY SHERIFFS OFFICE,0 +a8b4f6,KANSAS HIGHWAY PATROL,0 +a8b98e,ILLINOIS STATE POLICE,0 +a8ca85,MIDDLETOWN POLICE DEPARTMENT,1 +a8cb06,MIDDLETOWN POLICE DEPARTMENT,0 +a8cc2b,MIDDLETOWN POLICE DEPARTMENT,0 +a8cf83,LOS ANGELES POLICE DEPARTMENT,0 +a8d33a,LOS ANGELES POLICE DEPT AIR SUPPORT DIVISION,0 +a8da5e,CITY OF WILMINGTON POLICE DEPARTMENT,0 +a8dc75,KANSAS HIGHWAY PATROL 3600 NE SARDOU AVE BLDG 2,0 +a8dd00,SAN DIEGO COUNTY SHERIFFS DEPARTMENT,0 +a8dd4c,MARSHALL COUNTY SHERIFFS OFFICE,0 +a8dd56,DEPARTMENT OF PUBLIC SAFETY OFFICE OF STATE POL,1 +a8de3e,LAUDERDALE COUNTRY SHERIFFS OFFICE,0 +a8deb6,DESOTO COUNTY SHERIFF DEPT,0 +a8df6d,SPARTANBURG COUNTY SHERIFFS OFFICE,0 +a8df87,PIMA COUNTY BOARD OF SUPERVISORS PIMA COUNTY SH,0 +a8e10d,ILLINOIS STATE POLICE,0 +a8e2fb,STATE OF LOUISIANA DEPARTMENT OF PUBLIC SAFETY,0 +a8e33e,PIMA COUNTY BOARD OF SUPERVISORS PIMA COUNTY SH,0 +a8e6e8,CITRUS COUNTY SHERIFF'S OFFICE,0 +a8ea23,GEORGIA STATE PATROL AVIATION DIVISION,0 +a8ea97,ALAMEDA COUNTY SHERIFFS OFFICE,0 +a8ec40,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a8ec53,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +a8f25e,CUSTOMS AND BORDER PROTECTION,0 +a8f34b,CULLMAN POLICE DEPARTMENT,0 +a8f4e6,OKLAHOMA DEPARTMENT OF CORRECTIONS,0 +a8f547,AUSTIN POLICE DEPARTMENT,0 +a8f852,GEORGIA STATE PATROL AVIATION DIV,1 +a8fa16,GWINNETT COUNTY POLICE DEPT,0 +a8fc0b,KING COUNTY SHERIFF'S OFFICE,0 +a8fcf9,KING COUNTY SHERIFFS OFFICE,0 +a8febb,BREVARD COUNTY SHERIFF OFFICE,0 +a8ffbc,CHAUTAUQUA COUNTY SHERIFFS OFFICE,0 +a90097,DEPARTMENT OF HOMELAND SECURITY,0 +a903f4,KANSAS HIGHWAY PATROL,0 +a90882,JEFFERSON COUNTY SHERIFFS OFFICE,0 +a92bff,BOARD OF POLICE COMMISSIONERS,0 +a92fb6,BOARD OF POLICE COMMISSIONERS,0 +a9336d,BOARD OF POLICE COMMISSIONERS,0 +a935de,ALAMEDA COUNTY SHERIFFS OFFICE,0 +a942a2,STATE OF MICHIGAN DEPARTMENT OF STATE POLICE,0 +a9479c,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a94805,DEPARTMENT OF HOMELAND SECURITY CUSTOMS & BORDE,0 +a949b7,BOARD OF POLICE COMMISSIONERS,0 +a94aea,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +a94c41,BUTLER COUNTY SHERIFF,1 +a94d6e,BOARD OF POLICE COMMISSIONERS,0 +a94d9f,BRUNSWICK COUNTY SHERIFFS OFFICE,0 +a95111,STATE OF NEW JERSEY DEPT OF LAW AND PUBLIC SAFE,0 +a95902,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +a95a2b,U S DEPARTMENT OF HOMELAND SECURITY,0 +a96089,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a96199,U S DEPARTMENT OF HOMELAND SECURITY,1 +a961ae,SAN DIEGO COUNTY SHERIFF,0 +a96440,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a96694,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a966fd,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +a967fe,TULARE COUNTY SHERIFF,0 +a96907,U S DEPARTMENT OF HOMELAND SECURITY,1 +a9699c,STATE OF ALASKA AIRCRAFT SECTION DEPARTMENT OF,1 +a96a05,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a96a28,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a96a4b,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a96ab4,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a96b7e,GWINNETT COUNTY POLICE DEPARTMENT,0 +a96b95,FLORIDA HIGHWAY PATROL DEPT HIGHWAY SAFETY & MO,0 +a97075,U S DEPARTMENT OF HOMELAND SECURITY,0 +a97126,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a97127,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a97128,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a9738f,EAST BAY REGIONAL PARK DISTRICT POLICE AIR SUPP,0 +a9754d,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +a9775e,BILLINGS COUNTY SHERIFFS DEPARTMENT,0 +a977a8,BILLINGS COUNTY SHERIFFS DEPARTMENT,0 +a977f8,SAN DIEGO CO SHERIFF DEPT,0 +a97a71,OHIO DEPARTMENT OF PUBLIC SAFETY,0 +a97aa3,KING COUNTY DEPARTMENT OF PUBLIC SAFETY,0 +a97ab1,LEE COUNTY SHERIFFS OFFICE,1 +a97b90,VIRGINIA STATE POLICE,0 +a97d56,CHINO POLICE DEPARTMENT,0 +a98687,TRINITY COUNTY SHERIFFS DEPARTMENT,0 +a98752,ADAMS COUNTY SHERIFFS DEPT,0 +a9877e,EAST BATON ROUGE PARISH SHERIFFS OFFICE,0 +a987b1,EAST BATON ROUGE PARISH SHERIFFS OFFICE,0 +a98c1f,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a98f5d,OHIO DEPARTMENT OF PUBLIC SAFETY OHIO STATE HIG,0 +a99257,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +a99378,METRO NASHVILLE POLICE AVIATION UNIT,0 +a99389,NIAGARA COUNTY SHERIFF'S OFFICE,0 +a99425,MIAMI DADE SHERIFF'S OFFICE CRIME SCENE INVESTI,0 +a99644,HILLSBOROUGH COUNTY SHERIFFS OFFICE,1 +a996a5,GEORGIA STATE PATROL AVN,0 +a996cb,OHIO DEPARTMENT OF PUBLIC SAFETY,0 +a997ac,MINNESOTA STATE PATROL,0 +a99a21,VOLUSIA SHERIFFS OFFICE,0 +a99c89,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +a99e13,GEORGIA STATE PATROL AVN,0 +a9a0ae,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +a9a1f0,OHIO DEPARTMENT OF PUBLIC SAFETY,0 +a9a230,LEE COUNTY SHERIFFS OFFICE,1 +a9a449,TENNESSEE HIGHWAY PATROL DEPARTMENT OF SAFETY,0 +a9a52c,INDIAN RIVER COUNTY SHERIFFS OFFICE,0 +a9ae61,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +a9b5be,COLLIER COUNTY SHERIFFS OFFICE,0 +a9b5de,CALHOUN COUNTY SHERIFFS OFFICE,0 +a9b711,KANSAS HIGHWAY PATROL,0 +a9b8ca,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9b933,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9b956,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9c015,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9c038,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9c05b,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9c07e,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +a9c0d0,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a9c0d1,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a9c0d3,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a9c0d4,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +a9c12d,US CUSTOMS AND BORDER PROTECTION,0 +a9c3a9,US CUSTOMS AND BORDER PROTECTION,0 +a9c3cc,US CUSTOMS AND BORDER PROTECTION,0 +a9c412,US CUSTOMS AND BORDER PROTECTION,0 +a9c5fa,COLLIER COUNTY SHERIFFS OFFICE,0 +a9c96f,OHIO STATE HIGHWAY PATROL,0 +a9c9af,LEE COUNTY SHERIFFS OFFICE,1 +a9cbc8,TENNESSEE HIGHWAY PATROL DEPARTMENT OF SAFETY,0 +a9d056,HARRIS COUNTY SHERIFFS OFFICE,0 +a9d47c,LOS ANGELES COUNTY SHERIFFS DEPARTMENT,0 +a9d5b4,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +a9dc03,SUMTER COUNTY SHERIFFS OFFICE,1 +a9df5b,EL PASO POLICE DEPARTMENT,0 +a9e1a1,MICHIGAN STATE POLICE,0 +a9e6be,STANISLAUS COUNTY SHERIFFS DEPARTMENT,0 +a9e74c,COUNTY OF SANTA CRUZ SHERIFFS OFFICE,1 +a9e898,SHERIFF OF CHARLOTTE COUNTY,0 +a9e8c3,U S MARSHALS SERVICE JPATS - AIR OPERATIONS,0 +a9ea1e,MACOMB COUNTY SHERIFFS AVIATION DIVISION,0 +a9ec57,AUGLAIZE COUNTY SHERIFFS OFFICE,0 +a9f046,MONROE COUNTY SHERIFF'S OFFICE,0 +a9f12e,LEE COUNTY SHERIFFS OFFICE,1 +a9f380,WILLIAMSON COUNTY SHERIFFS OFFICE,0 +a9f3b2,WILLIAMSON COUNTY SHERIFFS OFFICE,0 +a9f673,DEPARTMENT OF HOMELAND SECURITY,0 +a9f7db,UNITED STATES CUSTOMS AND BORDER PROTECTION OFF,0 +a9f7dd,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +aa1597,NEW YORK STATE POLICE,1 +aa169d,HOUSTON POLICE DEPT AIR SUPPORT DIV,0 +aa16c0,HOUSTON POLICE DEPT AIR SUPPORT DIV,0 +aa1dcc,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aa20ae,AL ETOWAH CO SHERIFF DEPT,0 +aa2183,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aa21c5,MISSISSIPPI COUNTY SHERIFF DEPARTMENT,0 +aa24cb,MERCED COUNTY SHERIFFS OFFICE,0 +aa253a,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aa2592,DEPARTMENT OF HOMELAND SECURITY,0 +aa28f1,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aa2ca8,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aa3111,MARICOPA COUNTY SHERIFFS OFFICE,0 +aa32db,PIERCE COUNTY SHERIFFS DEPARTMENT,0 +aa3ba4,MARIN COUNTY SHERIFFS OFFICE,0 +aa4245,TENNESSEE DEPARTMENT OF SAFETY & HOMELAND SECURI,0 +aa4682,NEW YORK STATE POLICE,0 +aa49f3,LEON COUNTY SHERIFFS OFFICE,0 +aa4a39,NEW YORK STATE POLICE,0 +aa5812,CITY OF CLEVELAND DIVISION OF POLICE,0 +aa5fcb,LEHIGH UNIVERSITY POLICE DEPARTMENT,0 +aa60e2,MI STATE POLICE FIELD SUPP AVIATION,0 +aa6491,US CUSTOMS & BORDER PROTECTION OFFICE OF AIR &,0 +aa6e5c,ILLINOIS STATE POLICE,0 +aa7114,POLK COUNTY SHERIFF'S OFFICE,0 +aa74e9,FLORIDA HIGHWAY PATROL DEPARTMENT OF HIGHWAY SA,0 +aa7641,HENDERSONVILLE POLICE NC,0 +aa786b,SACRAMENTO COUNTY SHERIFF'S OFFICE,0 +aa7d46,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aa80ef,CARTER COUNTY SHERIFFS DEPARTMENT,0 +aa8695,SALT RIVER POLICE DEPARTMENT,0 +aa86d4,WAYNE COUNTY SHERIFFS OFFICE,0 +aa8c44,MIAMI-DADE SHERIFF'S OFFICE CRIME SCENE INVESTI,0 +aa8d7e,DEPARTMENT OF HOMELAND SECURITY,0 +aa8da1,DEPARTMENT OF HOMELAND SECURITY,0 +aa8dc4,DEPARTMENT OF HOMELAND SECURITY,0 +aa8de7,DEPARTMENT OF HOMELAND SECURITY,0 +aa8e0a,DEPARTMENT OF HOMELAND SECURITY,0 +aa90d3,PALM BEACH COUNTY SHERIFFS OFFICE,0 +aa9143,TENNESSEE DEPARTMENT OF SAFETY & HOMELAND SECURI,0 +aa9459,BROWARD COUNTY SHERIFFS OFFICE,0 +aa95cf,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +aa9810,BROWARD COUNTY SHERIFFS OFFICE,0 +aa9986,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,1 +aa9b21,ANAHEIM POLICE DEPT,0 +aa9bc3,STATE OF ARIZONA DEPARTMENT OF PUBLIC SAFETY,1 +aa9bc7,BROWARD COUNTY SHERIFF'S OFFICE,0 +aa9d3d,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +aaa0f4,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +aaa4ab,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +aaa862,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +aaa888,BEAVER COUNTY SHERIFFS OFFICE,0 +aaac19,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,1 +aaafd0,SAN BERNARDINO COUNTY SHERIFFS AVIATION DIVISION,0 +aab319,US MARSHALS SERVICE,0 +aab46c,KING COUNTY SHERIFFS OFFICE,0 +aab6cd,MISSOURI STATE HIGHWAY PATROL,0 +aab6f5,CLARK COUNTY SHERIFFS OFFICE,0 +aab811,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aab8c2,TENNESSEE DEPARTMENT OF SAFETY & HOMELAND SECURI,0 +aab985,KING COUNTY SHERIFFS OFFICE,0 +aaba6a,WARREN COUNTY SHERIFFS DEPT,0 +aabbc8,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aabf7f,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aac234,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +aac24f,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +aac336,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aac6ed,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aacc11,PALM BEACH COUNTY SHERIFFS OFFICE,0 +aace5b,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,1 +aade5d,CITY OF PASADENA POLICE DEPARTMENT,1 +aae0c2,LOS ANGELES INTERAGENCY METROPOLITAN POLICE APP,0 +aae2f3,MARTIN COUNTY SHERIFFS DEPARTMENT,0 +aae5f0,PASCO SHERIFFS OFFICE,0 +aae651,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +aae6ab,MIAMI-DADE SHERIFF'S OFFICE CRIME SCENE INVESTI,0 +aae6b7,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aae6dd,SAN DIEGO POLICE DEPARTMENT,0 +aae7b1,CUSTOMS AND BORDER PROTECTION/PERATON,0 +aaea6e,DEPARTMENT OF HOMELAND SECURITY CUSTOMS & BORDE,0 +aaea94,SAN DIEGO POLICE DEPARTMENT,0 +aaeb68,CUSTOMS AND BORDER PROTECTION/PERATON,0 +aaed5e,PASCO SHERIFFS OFFICE,0 +aaee25,DEPARTMENT OF HOMELAND SECURITY CUSTOMS AND BOR,0 +aaee4b,SAN DIEGO POLICE DEPARTMENT,0 +aaee64,DOVER DELAWARE POLICE DEPARTMENT,0 +aaef1f,CUSTOMS AND BORDER PROTECTION/PERATON,0 +aaf1da,MIAMI DADE POLICE DEPT,0 +aaf1dc,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aaf202,SAN DIEGO POLICE DEPARTMENT,0 +aaf278,TEXAS DEPT. OF PUBLIC SAFETY,0 +aaf287,MIAMI DADE POLICE DEPT CRIME SCENE INVESTIGATIV,0 +aaf2d6,CUSTOMS AND BORDER PROTECTION/PERATON,0 +aaf3f7,ST BERNARD PARISH POLICE JURY MOSQUITO CONTROL,0 +aaf591,MIAMI DADE POLICE DEPT,0 +aaf593,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aaf5b9,SAN DIEGO POLICE DEPARTMENT,0 +aaf60b,COUNTY OF SAN LUIS OBISPO SHERIFFS OFFICE,0 +aaf68d,CUSTOMS AND BORDER PROTECTION/PERATON,0 +aaf94a,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aaf970,SAN DIEGO POLICE DEPARTMENT,0 +aaf9e6,LUBBOCK POLICE DEPT,0 +aaf9ed,DOVER DELAWARE POLICE DEPARTMENT,0 +aafa44,CUSTOMS AND BORDER PROTECTION/PERATON,0 +aafbea,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aafbfc,DOVER DELAWARE POLICE DEPT,0 +aafd01,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aafd27,SAN DIEGO POLICE DEPARTMENT,0 +aafdfb,CUSTOMS AND BORDER PROTECTION/PERATON,0 +ab00b8,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab0569,CUSTOMS AND BORDER PROTECTION/PERATON,0 +ab0824,STATE OF MISSOURI DEPARTMENT OF PUBLIC SAFETY,0 +ab0a7f,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab0dc2,DEPARTMENT OF HOMELAND SECURITY,0 +ab0ed2,QUEEN ANNES COUNTY SHERIFFS OFFICE,0 +ab1289,TEXAS DEPT OF PUBLIC SAFETY,0 +ab1564,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab1608,INDIAN RIVER COUNTY SHERIFFS OFFICE,0 +ab1838,DEPARTMENT OF HOMELAND SECURITY,0 +ab191b,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab194e,MARTIN COUNTY SHERIFFS OFFICE,0 +ab1a19,ALLEGHENY COUNTY SHERIFF'S OFFICE,0 +ab1cd2,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab1d82,SNOHOMISH COUNTY SHERIFFS OFFICE,0 +ab2055,DEPARTMENT OF HOMELAND SECURITY,0 +ab2089,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab2410,DELAWARE DEPARTMENT OF CORRECTION,0 +ab2440,DEPT OF HOMELAND SECURITY US CUSTOMS AND BORDER,0 +ab2698,BLADEN COUNTY SHERIFF DEPARTMENT NORTH CAROLINA,0 +ab27f7,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab2bae,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab2c8a,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ab2f2d,GWINNETT COUNTY POLICE DEPT,0 +ab2f81,COLLIER COUNTY SHERIFFS OFFICE,0 +ab3198,TENNESSEE DEPT OF SAFETY AND HOMELAND SECURITY,0 +ab31be,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab3651,TEXAS DEPT OF PUBLIC SAFETY,0 +ab3670,VANDERBURGH COUNTY SHERIFFS OFFICE,0 +ab3861,ARIZONA DEPARTMENT OF PUBLIC SAFETY,1 +ab399c,MASSACHUSETTS STATE POLICE,0 +ab3c6b,WASHINGTON STATE PATROL,0 +ab3cd4,MASSACHUSETTS STATE POLICE,0 +ab3ce3,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab3dde,VANDERBURGH COUNTY SHERIFFS OFFICE,0 +ab3e10,CLARK COUNTY SHERIFF'S OFFICE,0 +ab3fbf,MASSACHUSETTS STATE POLICE,0 +ab4176,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ab4195,VANDERBURGH COUNTY SHERIFFS OFFICE,0 +ab448d,MASSACHUSETTS STATE POLICE,0 +ab454c,VANDERBURGH COUNTY SHERIFFS OFFICE,0 +ab4731,US CUSTOMS AND BORDER PROTECTION,0 +ab4ae8,US CUSTOMS AND BORDER PROTECTION,0 +ab4c7e,MI STATE POLICE FIELD SUPP AVIATION,0 +ab4c9b,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ab4db8,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +ab4dbb,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +ab4dbc,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY AI,1 +ab4e4a,BAY COUNTY SHERIFFS OFFICE,0 +ab4e91,SHERIFF CHRISTOPHER J,0 +ab4e9f,US CUSTOMS AND BORDER PROTECTION,0 +ab50bc,DELAWARE DEPARTMENT OF CORRECTION,0 +ab50d5,DELAWARE DEPARTMENT OF CORRECTION,0 +ab5112,DELAWARE DEPARTMENT OF CORRECTION,0 +ab5256,US CUSTOMS AND BORDER PROTECTION,0 +ab532d,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab5349,COLLIER COUNTY SHERIFFS OFFICE,0 +ab5722,MISSOURI STATE HIGHWAY PATROL ATTN AIRCRAFT DIV,0 +ab5866,US CUSTOMS AND BORDER PROTECTION,0 +ab5917,TENNESSEE DEPARTMENT OF SAFETY & HOMELAND SECURI,0 +ab593d,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab5a43,PULASKI COUNTY SHERIFFS OFFICE,0 +ab5a58,PULASKI COUNTY SHERIFFS OFFICE,0 +ab5da3,SAN BERNARDINO COUNTY SHERIFFS DEPT,0 +ab5da4,SANTA CRUZ COUNTY SHERIFF'S OFFICE,0 +ab615a,SAN BERNARDINO COUNTY SHERIFF'S DEPT,0 +ab6462,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab6511,SAN BERNARDINO COUNTY SHERIFFS DEPT,0 +ab68c8,SAN BERNARDINO COUNTY SHERIFF'S DEPT,0 +ab6c1c,NEW CASTLE COUNTY POLICE,0 +ab6c7f,SAN BERNARDINO COUNTY SHERIFFS DEPT,0 +ab6c80,ST. CHARLES PARISH SHERIFF'S OFFICE ROBERT RIDDI,0 +ab6ef0,MICHIGAN STATE POLICE,0 +ab7036,SAN BERNARDINO COUNTY SHERIFFS DEPT,0 +ab7037,ST. CHARLES PARISH SHERIFF'S OFFICE ROBERT RIDDI,0 +ab7294,COLUMBUS OFFICE OF HOMELAND SECURITY COLUMBUS C,0 +ab74ff,CITY OF HOUSTON POLICE DEPARTMENT,0 +ab76f5,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS AND,0 +ab7b5a,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab8096,TENNESSEE DEPARTMENT OF SAFETY & HOMELAND SECURI,0 +ab816d,MARSHALL COUNTY SHERIFFS OFFICE,0 +ab876e,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab87e1,HABERSHAM COUNTY SHERIFFS OFFICE,0 +ab8b25,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab8edc,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,1 +ab9074,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ab9293,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab964a,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab9a01,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +ab9b6d,ST CHARLES PARISH SHERIFFS OFFICE,0 +ab9db8,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aba16f,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +aba282,METROPOLITAN NASHVILLE POLICE AVIATION SECTION,0 +aba2af,JOHNSTON COUNTY SHERIFFS OFFICE,0 +aba2db,ST. CHARLES PARISH SHERIFF'S OFFICE ROBERT RIDDI,0 +aba77f,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +abab7f,LOS ANGELES POLICE DEPARTMENT AIR SUPPORT DIVIS,0 +abac7a,NEW CASTLE COUNTY POLICE,0 +abb13a,SALT RIVER POLICE DEPARTMENT,0 +abb1ec,SALT RIVER POLICE DEPARTMENT,0 +abb2a4,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,1 +abb410,ST. CHARLES PARISH SHERIFF'S OFFICE ROBERT RIDDI,0 +abb44b,MIAMI-DADE SHERIFF'S OFFICE CRIME SCENE INVESTI,0 +abb5a6,BLADEN COUNTY SHERIFFS OFFICE,0 +abb65b,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +abb70c,PASADENA POLICE DEPARTMENT,0 +abb7b4,PASADENA POLICE DEPARTMENT,0 +abbdc9,US DEPARTMENT OF HOMELAND SECURITY US CUSTOMS &,0 +abcc93,MOHAVE COUNTY SHERIFF'S OFFICE,1 +abcd9f,MISSOURI STATE HIGHWAY PATROL,0 +abd6de,CANYON COUNTY SHERIFFS OFFICE,0 +abd7ea,LONG BEACH POLICE DEPARTMENT,0 +abd947,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +abdba1,LONG BEACH POLICE DEPARTMENT,0 +abde18,JONES COUNTY SHERIFFS OFFICE,0 +abdf58,LONG BEACH POLICE DEPARTMENT,0 +abe24e,KNOX COUNTY SHERIFFS OFFICE,0 +abe30f,LONG BEACH POLICE DEPARTMENT,0 +abe578,ORANGE COUNTY SHERIFFS OFFICE,0 +abf713,KANSAS HIGHWAY PATROL,0 +abf7f8,PENNSYLVANIA STATE POLICE COMMONWEALTH OF PENNS,1 +abf8b4,HOUSTON POLICE DEPARTMENT AIR SUPPORT DIVISION,0 +abfa5c,WAUKESHA POLICE DEPARTMENT,0 +abfad6,BUFFALO COUNTY SHERIFFS OFFICE,0 +abfbaf,PENNSYLVANIA STATE POLICE BUREAU OF EMERGENCY &,1 +abfc04,WARRICK COUNTY SHERIFF,0 +abfdde,DELAWARE DEPARTMENT OF CORRECTION,0 +abff66,PENNSYLVANIA STATE POLICE BUREAU OF EMERGENCY &,1 +abffbb,WARRICK COUNTY SHERIFF,0 +ac00c7,DEPARTMENT OF HOMELAND SECURITY US CUSTOMS & BO,0 +ac029c,MISSOURI STATE HIGHWAY PATROL,0 +ac031d,PENNSYLVANIA STATE POLICE BUREAU OF EMERGENCY &,1 +ac0372,WARRICK COUNTY SHERIFF,0 +ac06d4,PENNSYLVANIA STATE POLICE BUREAU OF EMERGENCY &,1 +ac0729,WARRICK COUNTY SHERIFF,0 +ac0876,MARION COUNTY SHERIFFS OFFICE,0 +ac0a8b,PENNSYLVANIA STATE POLICE BUREAU OF EMERGENCY &,1 +ac0e42,PENNSYLVANIA STATE POLICE BUREAU OF EMERGENCY &,1 +ac0e7c,COMMONWEALTH OF VIRGINIA DEPARTMENT OF STATE PO,0 +ac1085,WILLIAMSON COUNTY SHERIFF OFFICE,0 +ac1267,CRAIG CO SHERIFFS OFFICE,0 +ac153d,CAVUGA NATION POLICE DEPT,0 +ac15b0,PENNSYLVANIA STATE POLICE,0 +ac1967,PENNSYLVANIA STATE POLICE COMMONWEALTH OF PENNS,1 +ac1b86,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +ac1d10,CITY OF CHICAGO DEPARTMENT OF POLICE,1 +ac2198,US CUSTOMS AND BORDER PROTECTION,0 +ac22d6,CITY OF EDINA MN POLICE DEPT,0 +ac232a,ILLINOIS STATE POLICE AIR OPERATIONS BUREAU,0 +ac2431,GWINNETT COUNTY POLICE DEPARTMENT,0 +ac249a,GWINNETT COUNTY POLICE DEPARTMENT,0 +ac254f,US CUSTOMS AND BORDER PROTECTION,0 +ac2672,NIAGARA COUNTY SHERIFFS DEPARTMENT,0 +ac2753,SAVANNAH POLICE DEPARTMENT,0 +ac2906,US CUSTOMS AND BORDER PROTECTION,0 +ac2a8e,CITY OF SAN DIEGO POLICE AIR SUPPORT UNIT,0 +ac2e45,CITY OF SAN DIEGO POLICE AIR SUPPORT UNIT,0 +ac2e4f,SAVANNAH POLICE DEPT,0 +ac455f,ALAMOSA COUNTY SHERIFFS OFFICE,0 +ac46f2,WEST VIRGINIA STATE POLICE,0 +ac4795,GWINNETT COUNTY POLICE DEPARTMENT,0 +ac4ab8,MASSACHUSETTS STATE POLICE,0 +ac4e64,PENNSYLVANIA STATE POLICE,0 +ac4e8b,KEARNEY POLICE DEPARTMENT,0 +ac51a5,DEPARTMENT OF HOMELAND SECURITY,0 +ac51f3,RIVERSIDE COUNTY SHERIFFS OFFICE CALIFORNIA,0 +ac521b,PENNSYLVANIA STATE POLICE,0 +ac55d2,PENNSYLVANIA STATE POLICE,0 +ac5985,WEST VIRGINIA STATE POLICE,0 +ac5989,PENNSYLVANIA STATE POLICE,0 +ac5c54,GWINNETT COUNTY POLICE DEPARTMENT,0 +ac5d40,PENNSYLVANIA STATE POLICE,0 +ac60f7,PENNSYLVANIA STATE POLICE,0 +ac6365,GWINNETT COUNTY POLICE DEPT,0 +ac64ae,PENNSYLVANIA STATE POLICE,0 +ac6d28,DEKALB COUNTY POLICE,0 +ac6d90,OKLAHOMA DEPARTMENT OF PUBLIC SAFETY,1 +ac6df4,STATE OF MISSOURI MISSOURI HIGHWAY PATROL,0 +ac6e82,MONTEREY COUNTY SHERIFFS OFFICE,0 +ac6e92,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ac6fe9,KANSAS HIGHWAY PATROL AIR SUPPORT UNIT,0 +ac715d,GWINNETT COUNTY POLICE DEPARTMENT,0 +ac72fb,ARIZONA DEPT OF PUBLIC SAFETY,0 +ac73ad,MARTIN COUNTY SHERIFFS OFFICE,0 +ac73eb,LIVINGSTON PARISH SHERIFFS OFFICE,1 +ac7499,TN DEPT OF PUBLIC SAFETY,0 +ac74ac,GWINNETT COUNTY POLICE DEPARTMENT,0 +ac74c5,GWINNWTT COUNTY POLICE DEPARTMENT,0 +ac77a2,LIVINGSTON PARISH SHERIFFS OFFICE,1 +ac7b0e,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +ac7d0a,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,1 +ac8265,GILA RIVER POLICE DEPARTMENT,0 +ac827c,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +ac835d,GEORGIA DEPARTMENT OF PUBLIC SAFETY,1 +ac861c,GILA RIVER POLICE DEPARTMENT,0 +ac8633,DEPARTMENT OF CALIFORNIA HIGHWAY PATROL,0 +ac8714,GEORGIA DEPARTMENT OF PUBLIC SAFETY,0 +ac8a42,STATE OF ALASKA DEPT OF PUBLIC SAFETY AIRCRAFT,0 +ac8ac6,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +ac8acf,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +ac8e82,GEORGIA DEPARTMENT OF PUBLIC SAFETY,1 +ac9141,GILA RIVER POLICE DEPARTMENT,0 +ac917c,JEFFERSON COUNTY SHERIFF DEPT,0 +ac92b4,MIAMI-DADE POLICE DEPT CRIME SCENE INVESTIGATIV,0 +ac94a7,DEKALB POLICE DEPARTMENT,0 +ac94b9,PUTNAM COUNTY SHERIFFS OFFICE,0 +ac9524,WASHOE COUNTY SHERIFF DEPT,0 +ac9582,STRATFORD POLICE DEPARTMENT,0 +ac96c3,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +ac9768,STATE OF OHIO DEPARTMENT OF PUBLIC SAFETY,0 +ac978f,KNOX COUNTY SHERIFFS OFFICE,0 +ac97f7,PRINCE GEORGES COUNTY POLICE DEPARTMENT,0 +ac9869,MULTNOMAH COUNTY SHERIFFS OFFICE,0 +ac98b0,MARTIN COUNTY SHERIFFS OFFICE,0 +ac9a65,ALAMEDA CO SHERIFFS OFFICE,1 +ac9a68,MARION COUNTY SHERIFFS OFFICE,0 +ac9a6a,ST LUCIE COUNTY SHERIFFS OFFICE,0 +ac9a71,ALBUQUERQUE POLICE DEPT AIR SUPPORT UNIT,0 +ac9a73,WASHINGTON DC METROPOLITAN POLICE,0 +ac9a75,KNOX COUNTY SHERIFFS DEPARTMENT,0 +ac9a7a,STATE OF ARIZONA ARIZONA DEPARTMENT OF PUBLIC S,1 +ac9a7c,MARION COUNTY SHERIFFS OFFICE,0 +ac9a7f,SUMTER COUNTY SHERIFFS OFFICE,0 +ac9a95,CALIFORNIA HIGHWAY PATROL,0 +ac9aa2,ST LOUIS COUNTY POLICE DEPARTMENT,1 +ac9ab4,HILLSBOROUGH COUNTY SHERIFFS OFFICE,0 +ac9ab6,BREVARD COUNTY SHERIFFS OFFICE,1 +ac9abb,OAKLAND COUNTY POLICE DEPT,0 +ac9acd,ALACHUA COUNTY SHERIFF,0 +ac9ad4,ST LOUIS COUNTY POLICE DEPARTMENT,0 +ac9add,MARION CO SHERIFFS OFFICE,0 +ac9ae7,POLK COUNTY SHERIFFS OFFICE,0 +ac9afb,SACRAMENTO COUNTY SHERIFFS DEPARTMENT,0 +ac9afc,MARTIN COUNTY SHERIFFS OFFICE,0 +ac9b01,POLK COUNTY SHERIFFS OFFICE,0 +ac9b03,POLK COUNTY SHERIFFS OFFICE,0 +ac9b1c,JEFFERSON PARISH SHERIFFS OFFICE PURCHASING DIV,0 +ac9b1d,MARION CO SHERIFFS OFFICE,0 +ac9b1e,SANTA BARBARA COUNTY SHERIFFS DEPARTMENT,0 +ac9b20,ANNE ARUNDEL COUNTY POLICE,0 +ac9b35,JEFFERSON PARISH SHERIFFS OFFICE,1 +ac9b38,POLK COUNTY SHERIFFS OFFICE,0 +ac9b3d,BREVARD COUNTY SHERIFFS DEPARTMENT,1 +ac9b40,ST LOUIS COUNTY POLICE,1 +ac9b51,SANTA BARBARA SHERIFFS DEPT,0 +ac9b57,MARION CO SHERIFF OFFICE,0 +ac9b7c,MARION COUNTY SHERIFFS DEPARTMENT,0 +ac9b83,MODESTO POLICE DEPARTMENT,0 +ac9b88,ST LOUIS COUNTY POLICE DEPT ATTN: CAPT JON BELM,1 +ac9b9b,TEHAMA COUNTY SHERIFF'S OFFICE,0 +ac9b9d,CHARLOTTE COUNTY SHERIFFS OFFICE,0 +ac9ba2,WASHOE COUNTY SHERIFFS OFFICE,0 +ac9ba5,ST LOUIS COUNTY (MO) POLICE DEPT,1 +ac9bab,VILLAGE OF WINTHROP HARBOR POLICE DEPARTMENT,0 +ac9bae,PRINCE GEORGES COUNTY POLICE DEPARTMENT,0 +ac9baf,ST LOUIS COUNTY POLICE DEPARTMENT,1 +ac9bbc,BREVARD COUNTY SHERIFFS OFFICE,0 +ac9be0,POLK COUNTY SHERIFFS OFFICE,0 +ac9be7,CITY OF RIVERSIDE POLICE - AVIATION UNIT,0 +ac9bee,POLK COUNTY SHERIFFS OFFICE,0 +ac9bf0,SACRAMENTO COUNTY SHERIFFS DEPARTMENT,0 +ac9bfd,ST LUCIE COUNTY SHERIFFS DEPT,0 +ac9c03,MERCED COUNTY SHERIFFS OFFICE,0 +ac9c09,ST LUCIE COUNTY SHERIFFS OFFICE,0 +ac9c0a,BERNALILLO COUNTY SHERIFFS DEPARTMENT,0 +ac9c1b,SAINT LUCIE COUNTY SHERIFFS OFFICE,0 +ac9c28,INDIAN RIVER COUNTY SHERIFF'S OFFICE,0 +ac9c37,CITY OF CHICAGO CHICAGO POLICE DEPT,1 +ac9c45,COOK COUNTY SHERIFF'S OFFICE,1 +ac9c59,MONTPELIER POLICE DEPT,0 +ac9c5f,POLK COUNTY SHERIFFS OFFICE,0 +ac9c67,MARTIN COUNTY SHERIFFS OFFICE,0 +ac9c6c,ANNE ARUNDEL COUNTY POLICE,0 +ac9c6d,LAS VEGAS METROPOLITAN POLICE DEPARTMENT,0 +ac9c8b,CHELAN COUNTY SHERIFFS OFFICE,0 +ac9c9f,CITY OF CHICAGO DEPARTMENT OF POLICE,1 +ac9ca2,VILLAGE OF WINTHROP HARBOR POLICE DEPARTMENT,0 +ac9caf,CITY OF CHICAGO DEPARTMENT OF POLICE,1 +ac9cb0,CITY OF CHICAGO DEPARTMENT OF POLICE,1 +ac9cb1,CITY OF CHICAGO DEPARTMENT OF POLICE,1 +ac9cb2,BERNALALILLO COUNTY SHERIFFS OFFICE,0 +ac9cb9,BERNALILLO COUNTY SHERIFF'S DEPT,1 +ac9ce4,MARYLAND STATE POLICE,0 +ac9e2c,KNOX COUNTY SHERIFFS OFFICE,0 +ac9e2e,KNOX COUNTY SHERIFFS DEPARTMENT,0 +ac9efd,KERN COUNTY SHERIFFS OFFICE,0 +ac9f21,LEXINGTON FAYETTE URBAN COUNTY DIVISION OF POLI,0 +ac9f65,PRINCE GEORGES COUNTY POLICE DEPARTMENT,0 +aca042,CHELAN COUNTY SHERIFFS OFFICE,0 +aca1e8,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +aca1ef,MIAMI-DADE SHERIFF'S OFFICE CRIME SCENE INVESTI,0 +aca240,ORANGE COUNTY SHERIFF'S OFFICE,0 +aca2a6,JEFFERSON PARISH SHERIFF'S OFFICE,0 +aca2b4,KERN COUNTY SHERIFFS OFFICE,0 +aca31c,PRINCE GEORGES COUNTY POLICE DEPARTMENT,0 +aca3f9,CHELAN COUNTY SHERIFF,0 +aca5f7,ORANGE COUNTY SHERIFFS OFFICE,0 +aca659,JEFFERSON PARISH SHERIFF'S OFFICE,0 +aca66b,KERN COUNTY SHERIFF DEPARTMENT,0 +aca9ae,ORANGE COUNTY SHERIFFS OFFICE,0 +acaa14,JEFFERSON PARISH SHERIFF'S OFFICE,0 +acaa22,KERN COUNTY SHERIFF DEPARTMENT,0 +acaad2,HARRIS COUNTY SHERIFF'S OFFICE,0 +acadcb,JEFFERSON PARISH SHERIFF'S OFFICE,0 +acadd9,KERN COUNTY SHERIFF DEPARTMENT,0 +acae89,SACRAMENTO SHERIFFS OFFICE,0 +acaeb4,LUBBOCK POLICE DEPT,0 +acb03d,SEWARD COUNTY SHERIFF'S OFFICE,0 +acb076,DEPARTMENT OF HOMELAND SECURITY,0 +acb0a9,SEWARD COUNTY SHERIFF'S OFFICE,0 +acb0ac,DEPT OF HOMELAND SECURITY,0 +acb0cb,DEPARTMENT OF HOMELAND SECURITY,0 +acb152,GILA RIVER POLICE DEPARTMENT,0 +acb15c,DEPT OF HOMELAND SECURITY,0 +acb175,DEPARTMENT OF HOMELAND SECURITY,0 +acb1a0,DEPARTMENT OF HOMELAND SECURITY,0 +acb1ad,DEPARTMENT OF HOMELAND SECURITY,0 +acb1f5,NEW YORK CITY POLICE DEPARTMENT,0 +acb202,DEPARTMENT OF HOMELAND SECURITY,0 +acb234,DEPARTMENT OF HOMELAND SECURITY,0 +acb245,SAN JUAN COUNTY SHERIFF DEPT,0 +acb47b,SALT RIVER POLICE DEPARTMENT,0 +acb496,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +acb4d3,ORANGE COUNTY SHERIFFS OFFICE,0 +acb509,GILA RIVER POLICE DEPARTMENT,0 +acb5ac,NEW YORK CITY POLICE DEPARTMENT,0 +acb603,SALT RIVER POLICE DEPARTMENT,0 +acb82d,KNOX COUNTY SHERIFFS OFFICE,0 +acb82f,KNOX COUNTY SHERIFFS DEPART,0 +acb963,NEW YORK CITY POLICE DEPARTMENT,0 +acbc06,CANADIAN CO SHERIFFS OFFICE,0 +acbca7,CITY OF JACKSONVILLE SHERIFFS OFFICE,0 +acbd1a,CLARK COUNTY SHERIFFS OFFICE,0 +acbe42,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +acbeaa,FRESNO COUNTY SHERIFF DEPT,0 +acbee7,KANSAS HIGHWAY PATROL,0 +acbf73,NEW YORK CITY POLICE DEPARTMENT,0 +acc1f9,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +acc29e,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +acc32a,CITY OF NEW YORK POLICE DEPARTMENT,0 +acc6e1,CITY OF NEW YORK POLICE DEPT,0 +acc736,GEORGIA DEPARTMENT OF PUBLIC SAFETY,1 +acc9bf,ORANGE COUNTY SHERIFFS OFFICE,0 +acca0c,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +accae3,FRESNO COUNTY SHERIFFS DEPARTMENT AIR SUPPORT U,0 +accaed,GEORGIA STATE PATROL,0 +accd76,ORANGE COUNTY SHERIFFS OFFICE,0 +accea4,GEORGIA DEPT OF PUBLIC SAFETY GEORGIA STATE PAT,1 +accf62,MCLENNAN COUNTY SHERIFFS OFFICE,1 +accf6b,MC LENNAN COUNTY SHERIFFS DEPT,1 +accf6c,MCLENNAN COUNTY SHERIFF'S DEPARTMENT,1 +acd0d5,PINAL COUNTY SHERIFFS OFFICE,0 +acd163,GILA RIVER POLICE DEPARTMENT,0 +acd25b,GEORGIA DEPARTMENT OF PUBLIC SAFETY,0 +acd51a,GILA RIVER POLICE DEPARTMENT,0 +acd89b,ORANGE COUNTY SHERIFFS OFFICE,0 +acd93e,DEPARTMENT OF HOMELAND SECURITY,0 +acd9c9,GEORGIA DEPARTMENT OF PUBLIC SAFETY,1 +acdc52,ORANGE COUNTY SHERIFFS OFFICE,0 +acdd80,GEORGIA STATE PATROL,0 +acdffd,DEPARTMENT OF HOMELAND SECURITY,1 +ace098,COLLIER COUNTY SHERIFFS OFFICE,0 +ace0d3,METRO NASHVILLE POLICE,0 +ace158,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ace471,MISSOURI STATE HIGHWAY PATROL,0 +ace480,STRATFORD POLICE DEPT,0 +ace5c1,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +ace5c5,BEAUFORT COUNTY SHERIFFS OFFICE,0 +ace619,ORANGE COUNTY SHERIFFS OFFICE,0 +ace666,KANSAS HIGHWAY PATROL,0 +ace6f2,CULLMAN POLICE DEPARTMENT,0 +ace747,GEORGIA DEPARTMENT OF PUBLIC SAFETY,1 +ace978,ARIZONA DEPT OF PUBLIC SAFETY,0 +ace9d0,CLACKAMAS COUNTY SHERIFFS OFFICE,0 +acea1d,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +acea4d,CLACKAMAS COUNTY SHERIFFS OFFICE,0 +aceafe,ILLINOIS STATE POLICE,0 +aceb15,CLACKAMAS COUNTY SHERIFFS OFFICE,0 +acf3f2,UTAH COUNTY SHERIFF,0 +acf8f9,DEPT OF CALIFORNIA HIGHWAY PATROL,0 +acfeef,OCEAN VIEW POLICE DEPARTMENT,0 +ad001a,ORANGE COUNTY SHERIFFS OFFICE,0 +ad03d1,ORANGE COUNTY SHERIFFS OFFICE,0 +ad05c6,ALASKA DEPARTMENT OF PUBLIC SAFETY AIRCRAFT SEC,1 +ad0bf0,MISSOURI STATE HIGHWAY PATROL,0 +ad0d40,ARIZONA DEPARTMENT OF PUBLIC SAFETY,0 +ad0de5,KANSAS HIGHWAY PATROL AIR SUPPORT UNIT,0 +ad0ec6,GEORGIA DEPT OF PUBLIC SAFETY GEORGIA STATE PATR,0 +ad10f7,ARIZONA DEPT OF PUBLIC SAFETY,0 +ad121a,CHARLOTTE COUNTY SHERIFFS OFFICE,0 +ad18bd,ORANGE COUNTY SHERIFFS OFFICE,0 +ad19e0,SPALDING COUNTY SHERIFFS OFFICE,0 +ad1d97,SPALDING COUNTY SHERIFFS OFFICE,0 +ad1f3c,PIERCE COUNTY SHERIFFS DEPARTMENT AIR SUPPORT U,0 +ad202b,ORANGE COUNTY SHERIFFS OFFICE,0 +ad2266,ALASKA DEPARTMENT OF PUBLIC SAFETY AIRCRAFT SEC,1 +ad22db,OCONEE COUNTY SHERIFF'S DEPT,0 +ad23e2,ORANGE COUNTY SHERIFFS OFFICE,0 +ad2505,SPALDING COUNTY SHERIFFS OFFICE,0 +ad2799,ORANGE COUNTY SHERIFFS OFFICE,0 +ad2c7e,STATE OF NEW YORK DIVISION OF STATE POLICE,0 +ad336f,MISSOURI STATE HIGHWAY PATROL AIRCRAFT DIVISION,0 +ad337e,NEW MILFORD POLICE DEPARTMENT,0 +ad34bf,ARIZONA DEPT OF PUBLIC SAFETY,0 +ad3564,KANSAS HIGHWAY PATROL,0 +ad3574,COUNTY OF LOS ANGELES SHERIFFS DEPT,0 +ad35bc,LOS ANGELES COUNTY SHERIFFS DEPARTMENT AERO BUR,0 +ad3645,NEW YORK STATE POLICE,0 +ad3860,LOS ANGELES COUNTY SHERIFFS DEPARTMENT,0 +ad3876,ARIZONE DEPT OF PUBLIC SAFETY,0 +ad389e,CITY OF CLEVELAND CLEVELAND DVISION OF POLICE,1 +ad395a,COUNTY OF LOS ANGELES C/O SHERIFFS DEPT,0 +ad39e3,CITY OF RIVERSIDE POLICE AVIATION UNIT,0 +ad3c55,CITY OF CLEVELAND DEPARTMENT OF PUBLIC SAFETY,1 +ad3ce5,COUNTY OF LOS ANGELES C/O SHERIFFS DEPT,0 +ad416a,NEW YORK STATE POLICE,0 +ad43c3,CITY OF CLEVELAND POLICE DEPARTMENT,0 +ad447e,COUNTY OF LOS ANGELES C/O SHERIFFS DEPT,0 +ad4cb0,TEXAS DEPARTMENT OF PUBLIC SAFETY,1 +ad535a,COUNTY OF LOS ANGELES C/O SHERIFFS DEPT,0 +ad53f3,CHAUTAUQUA COUNTY SHERIFF OFFICE,0 +ad57ae,SALINA COUNTY SHERIFFS DEPT,0 +ad57b4,STATE OF NEW YORK DIVISION OF STATE POLICE,0 +ad5ce3,KANSAS HIGHWAY PATROL,0 +ad5daa,ORANGE COUNTY SHERIFFS DEPARTMENT RNSP,0 +ad5ff5,ARIZONA DEPT OF PUBLIC SAFETY,0 +ad6083,GILA RIVER POLICE DEPARTMENT,0 +ad6171,COUNTY OF LOS ANGELES SHERIFFS DEPARTMENT AERO,0 +ad643a,GILA RIVER POLICE DEPARTMENT,0 +ad648f,COUNTY OF LOS ANGELES C/O SHERIFFS DEPARTMENT,0 +ad6532,NEW YORK STATE POLICE,0 +ad67f1,GILA RIVER POLICE DEPARTMENT,0 +ad68df,COUNTY OF LOS ANGELES SHERIFFS AERO BUREAU,0 +ad7057,LOUISIANA STATE POLICE,0 +ad723f,KINGS COUNTY SHERIFFS DEPT,0 +ad740e,LOUISIANA STATE POLICE,0 +ad7ad4,SERGEANT JEFF GIRGLE C/O EL MONTE POLICE DEPT,0 +ad7b9d,TEXAS DEPARTMENT OF PUBLIC SAFETY,0 +ad7f28,ST CHARLES PARISH SHERIFFS OFFICE,0 +ad81b0,KERN COUNTY SHERIFFS DEPARTMENT,0 +ad8462,KANSAS HIGHWAY PATROL,0 +ad84c6,MISSOURI STATE HIGHWAY PATROL,0 +ad8774,ARIZONA DEPT OF PUBLIC SAFETY,0 +ad88fa,ILLINOIS STATE POLICE STATE OF ILLINOIS AIR OPE,0 +ad8bb2,POLK COUNTY SHERIFFS OFFICE,0 +ad933e,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +ad96f5,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +ad989b,LOUISIANA DEPARTMENT OF PUBLIC SAFETY STATE POL,0 +ad9aac,CALIFORNIA HIGHWAY PATROL,0 +ad9e9c,SERGEANT JEFF GIRGLE C/O EL MONTE POLICE DEPT,0 +ad9f44,ILLINOIS STATE POLICE,0 +ada21a,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +ada5d1,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATIO,0 +adacb8,MARSHALL COUNTY SHERIFF OFFICE,0 +adaee5,STATE OF ALASKA DEPARTMENT OF PUBLIC SAFETY,0 +adaef3,ARIZONA DEPT OF PUBLIC SAFETY,0 +adaf98,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +adb34f,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +adb44c,MONROE COUNTY SHERIFFS OFFICE,0 +adb64c,ALAMEDA COUNTY SHERIFFS OFFICE,0 +adb706,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +adbabd,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +adbe74,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +adc22b,CALIFORNIA HIGHWAY PATROL OFFICE OF AIR OPERATI,0 +adc309,POLK COUNTY SHERIFF'S OFFICE,0 +adc79e,MADERA COUNTY SHERIFFS OFFICE,0 +adc999,CALIFORNIA HIGHWAY PATROL,0 +adca7a,STATE OF NEW YORK DIVISION OF STATE POLICE,1 +adce96,DEPARTMENT OF HOMELAND SECURITY CUSTOMS & BORDE,0 +add2bb,SALT RIVER POLICE DEPARTMENT,0 +add30a,ETOWAH COUNTY SHERIFF'S OFFICE,0 +add3ec,SALT RIVER POLICE DEPARTMENT,0 +add41d,RIVERSIDE COUNTY SHERIFFS,0 +add437,RIVERSIDE COUNTY SHERIFFS DEPARTMENT,0 +add476,SALT RIVER POLICE DEPT,0 +add672,ARIZONA DEPT OF PUBLIC SAFETY,0 +add6c1,ETOWAH COUNTY SHERIFFS OFFICE,0 +add775,GEORGIA DEPARTMENT OF PUBLIC SAFETY,1 +add7a3,OAKLAND COUNTY POLICE DEPT,0 +add7f8,ILLINOIS STATE POLICE,0 +adda78,ETOWAH COUNTY SHERIFFS OFFICE,0 +addb8b,RIVERSIDE COUNTY SHERIFFS,0 +addba5,COUNTY OF RIVERSIDE RIVERSIDE SHERIFFS AVIATION,0 +addf42,RIVERSIDE COUNTY SHERIFFS,0 +ade2c8,SUFFOLK COUNTY POLICE DEPARTMENT,0 +ade2d5,MISSISSIPPI DEPT OF PUBLIC SAFETY,0 +ade2f9,RIVERSIDE COUNTY SHERIFFS,0 +ade6be,RIVERSIDE COUNTY SHERIFFS,0 +ade8ae,CITY OF ALBUQUERQUE POLICE DEPARTMENT AIR SUPP,0 +ade930,IOWA STATE PATROL,0 +adea67,RIVERSIDE COUNTY SHERIFF'S,0 +adea75,RIVERSIDE COUNTY SHERIFFS DEPARTMENT,0 +adf1a4,CITY OF MARICOPA POLICE DEPARTMENT,0 +adf5a6,RIVERSIDE COUNTY SHERIFFS DEPARTMENT,0 +ae0ce1,US CUSTOMS AND BORDER PROTECTION,0 +ae0d7c,DEPARTMENT OF HOMELAND SECURITY,0 +ae2d1e,US CUSTOMS & BORDER PROTECTION,0 +ae2fd0,US CUSTOMS AND BORDER PROTECTION,0 +ae4a89,DEPARTMENT OF HOMELAND SECURITY,0 +ae4bd8,US CUSTOMS AND BORDER PROTECTION,0 +ae4dde,US CUSTOMS & BORDER PROTECTION,0 +ae4ddf,US CUSTOMS AND BORDER PROTECTION,0 +ae4de0,US CUSTOMS AND BORDER PROTECTION,0 +ae4ef0,US CUSTOMS AND BORDER PROTECTION,0 +ae6987,DEPARTMENT OF HOMELAND SECURITY,0 +af3bc5,US CUSTOMS & BORDER PROTECTION,0 +af8421,US CUSTOMS & BORDER PROTECTION,0 +c00714,PEEL REGIONAL POLICE,0 +c02698,Province Of Ontario Ministry of the Solicitor G,0 +c06bdb,SASKATCHEWAN PUBLIC SAFETY AGENCY (AIR OPERATION,0 +c06c0f,PROVINCE OF ONTARIO MINISTRY OF THE SOLICITOR G,1 +c07318,THE BOARD OF POLICE COMMISSIONERS IN AND FOR THE,1 +c085d2,REGIONAL MUNICIPALITY OF YORK POLICE SERVICES BO,1 +c4b960,US CUSTOMS & BORDER PROTECTION NATIONAL AIR SEC,0 +f2f7db,UNITED STATES CUSTOMS AND BORDER PROTECTION OFF,0 diff --git a/docs/index.html b/docs/index.html index 08ca1ba..d01fd4b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,9 +4,9 @@ OVERWATCH — passive surveillance detection for Android - + - + @@ -17,7 +17,6 @@ - @@ -54,7 +53,7 @@
- v0.5.6 · Android · passive + v0.5.8 · Android · passive

OVERWATCH @@ -76,7 +75,7 @@
0fused sources
0threat tiers
0packets sent
-
0releases
+
0releases

@@ -102,8 +101,8 @@
  • Bluetooth-LE + WiFi radio
  • DeFlock ALPR map
  • -
  • Citizen incidents
  • Waze police alerts
  • +
  • ADS-B aircraft
  • OpenStreetMap · Overpass
@@ -129,7 +128,7 @@
Detection-sources drill-down: BLE, WiFi, DeFlock, Citizen, Waze and Commercial rows, each with a color tier dot and the observations that fired it. + alt="Detection-sources drill-down: BLE, WiFi, DeFlock, Waze and Commercial rows, each with a color tier dot and the observations that fired it." loading="lazy">
@@ -147,7 +146,8 @@ Source-color dots

Flock / DeFlock cameras red · Waze police blue · - Citizen incidents purple. + aircraft violet · + user position as a ⌖ crosshair. Tap any row for coordinates + a Maps deep-link.

@@ -202,6 +202,21 @@

+
+
✈️
+

Aircraft — eyes above

+

+ Police and surveillance aircraft overhead, from the free community + ADS-B networks — used specifically + because the commercial trackers filter law-enforcement flights at + government request. Contacts are matched by ICAO address against a + bundled registry of 1,971 US law-enforcement airframes, + and scored by distance, altitude and orbit behaviour — because + surveillance aircraft circle, and airliners don't. An unlisted aircraft + loitering low and slow overhead still registers. +

+
+
📡

WiFi

@@ -224,20 +239,6 @@

-
-
🚨
-

Citizen — retired upstream

-

- Real-time public-safety incidents — police-relevant only, - fire/medical filtered out — within range and under 30 min old. - Citizen ended the police-dispatch partnership behind its public feed - in June 2026 and stubbed the endpoints, so this - source now reports the shutdown plainly and backs off instead of - polling a dead API. Police presence is still covered by Waze and - DeFlock. -

-
-
🚓

Waze — police alerts

@@ -319,7 +320,7 @@
Detection-sources drill-down: BLE, WiFi, DeFlock, Citizen, Waze and Commercial rows, each with a color tier dot and the observations that fired it. + alt="Detection-sources drill-down: BLE, WiFi, DeFlock, Waze and Commercial rows, each with a color tier dot and the observations that fired it." loading="lazy">
Source drill-downTap the ring for every source, its live tier, and the exact observation that fired it.
@@ -423,7 +424,7 @@
Settings screen: per-source toggles (BLE, WiFi, DeFlock, Citizen, Waze, Commercial), proximity distance sliders, and the encrypted Waze API key field. + alt="Settings screen: per-source toggles (BLE, WiFi, DeFlock, Waze, Commercial), proximity distance sliders, and the encrypted Waze API key field." loading="lazy">
@@ -431,7 +432,7 @@
  • Grab the latest debug-signed APK from Releases - (currently v0.5.6).
  • + (currently v0.5.8).
@@ -482,7 +483,6 @@

Signal from

@@ -501,7 +501,7 @@ transmit, probe, jam, or interfere with any device or network.

Also on Tor diff --git a/scripts/emu-gps.sh b/scripts/emu-gps.sh new file mode 100755 index 0000000..a3a71d7 --- /dev/null +++ b/scripts/emu-gps.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Feed a location to an Android emulator, for testing OVERWATCH's +# location-driven sources (DeFlock, Waze). +# +# scripts/emu-gps.sh [serial] [seconds] +# scripts/emu-gps.sh 38.8324 -77.1062 # Springfield VA, 120s +# scripts/emu-gps.sh 40.7128 -74.0060 emulator-5558 300 +# +# Why not `adb emu geo fix`: it returns OK and frequently changes nothing. +# Verified 2026-09-16 on emulator 37.1.11.0 — a fresh AVD and a 2-day-old one +# both stayed pinned at a stale Northern California fix through dozens of +# `geo fix` calls and raw `geo nmea` sentences, every one answered OK. The +# same trap is recorded in the DUCAT project's notes. Android's own test +# provider bypasses the emulator console entirely and lands immediately. +# +# Two things have to be true or the app still sees nothing: +# 1. `appops set --uid 0` (NOT `--uid shell`, which fails with +# "uid 2000 not allowed to perform MOCK_LOCATION"). +# 2. Something must actually engage the GPS provider. OVERWATCH requests +# PRIORITY_HIGH_ACCURACY, so starting a scan is enough; with a BALANCED +# request Play services parks the fused provider at ProviderRequest[OFF] +# and no fix is ever delivered. +# +# Note the argument orders differ between tools and it is easy to transpose: +# this script and `set-test-provider-location` take LAT,LON; `geo fix` takes +# LON first. Verify with: +# adb -s shell dumpsys location | grep -m1 "last location" +set -e +LAT=${1:?usage: emu-gps.sh [serial] [seconds]} +LON=${2:?usage: emu-gps.sh [serial] [seconds]} +SERIAL=${3:-emulator-5558} +SECONDS_TOTAL=${4:-120} +export PATH=$PATH:${ANDROID_HOME:-$HOME/Android/Sdk}/platform-tools + +adb -s "$SERIAL" shell settings put secure location_mode 3 >/dev/null 2>&1 || true +adb -s "$SERIAL" shell appops set --uid 0 android:mock_location allow >/dev/null 2>&1 || true +adb -s "$SERIAL" shell cmd location providers add-test-provider gps >/dev/null 2>&1 || true +adb -s "$SERIAL" shell cmd location providers set-test-provider-enabled gps true >/dev/null 2>&1 || true + +echo "feeding $LAT,$LON to $SERIAL for ${SECONDS_TOTAL}s (every 3s)…" +END=$((SECONDS + SECONDS_TOTAL)) +while [ $SECONDS -lt $END ]; do + # A test-provider location is one-shot; re-send so it never ages out from + # under a scanner that only polls every few minutes. + adb -s "$SERIAL" shell cmd location providers set-test-provider-location gps \ + --location "$LAT,$LON" >/dev/null 2>&1 || true + sleep 3 +done +echo "done. verify: adb -s $SERIAL shell dumpsys location | grep -m1 'last location'" diff --git a/scripts/gen-le-aircraft.py b/scripts/gen-le-aircraft.py new file mode 100755 index 0000000..b6b09f1 --- /dev/null +++ b/scripts/gen-le-aircraft.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Regenerate app/src/main/res/raw/le_aircraft.csv — the bundled list of +law-enforcement aircraft, keyed by the ICAO hex that ADS-B actually broadcasts. + + python3 scripts/gen-le-aircraft.py + +Source: ADSBexchange's `basic-ac-db.json.gz` (~14.5 MB, ~618k aircraft), which +is derived from the FAA registry and already keyed by ICAO hex. That matters: +registry.faa.gov itself is behind Akamai bot mitigation and answers 403, and +the raw FAA registry is keyed by N-number, which would need a separate +N-number -> hex conversion step. + +Why not the community plane-alert-db lists: measured 2026-09-17, their +plane-alert-pol.csv holds 996 aircraft but only 255 US-registered, and matching +all 2,760 of their police+government hexes against 394 live aircraft over a +250 nm sweep of Washington DC produced zero hits. Filtering this database by +owner name yields ~1,950 US law-enforcement aircraft — roughly 7.7x the US +coverage. + +This runs at development time and commits its output; the app never downloads +it. Re-run occasionally to pick up registry changes. +""" +import gzip, io, json, re, sys, urllib.request +from pathlib import Path + +SRC = "https://downloads.adsbexchange.com/downloads/basic-ac-db.json.gz" +OUT = Path(__file__).resolve().parent.parent / "app/src/main/res/raw/le_aircraft.csv" + +# Owner-name patterns. Word boundaries matter: a bare "MARSHAL" matches +# "MARSHALL SPACE FLIGHT CENTER", and a bare "POLICE" would be fine but +# "SHERIFF" needs to catch both "SHERIFF" and "SHERIFFS". +LE = re.compile(r"(\bSHERIFF|\bPOLICE\b|\bSTATE PATROL\b|\bHIGHWAY PATROL\b|" + r"\bSTATE TROOPER|\bPUBLIC SAFETY\b|\bUS MARSHAL\b|" + r"\bMARSHALS SERVICE\b|\bCONSTABLE\b|\bDEPT OF CORRECTION|" + r"\bDEPARTMENT OF CORRECTION|\bBORDER PATROL\b|" + r"\bCUSTOMS AND BORDER|\bCUSTOMS & BOR|\bHOMELAND SECURITY\b|" + r"\bDRUG ENFORCEMENT\b|\bFEDERAL BUREAU OF INVESTIGATION\b)", re.I) + +# Air-ambulance and firefighting outfits share a lot of vocabulary with police +# aviation ("PUBLIC SAFETY"), and a medevac helicopter overhead is not the +# thing this app exists to warn about. +EXCLUDE = re.compile(r"MARSHALL SPACE|\bFIRE\b|AMBULANCE|MEDICAL|\bEMS\b|" + r"AIR METHODS|LIFE FLIGHT|MEDEVAC|HOSPITAL", re.I) + + +def main() -> int: + print(f"fetching {SRC} …") + req = urllib.request.Request(SRC, headers={"User-Agent": "OVERWATCH-build/1.0"}) + with urllib.request.urlopen(req, timeout=180) as r: + raw = gzip.decompress(r.read()) + + rows, ladd, seen = [], 0, 0 + for line in io.BytesIO(raw): + line = line.decode("utf-8", "replace").strip().rstrip(",") + if not line or line in "[]": + continue + try: + rec = json.loads(line) + except ValueError: + continue + seen += 1 + own = (rec.get("ownop") or "").strip() + if not own or not LE.search(own) or EXCLUDE.search(own): + continue + hexid = (rec.get("icao") or "").strip().lower() + if len(hexid) != 6: + continue + # Commas would break the one-line-per-record format; the owner is + # display text only, so squashing punctuation is harmless. + owner = re.sub(r"[,\r\n]+", " ", own)[:48].strip() + if rec.get("faa_ladd"): + ladd += 1 + rows.append((hexid, owner, "1" if rec.get("faa_ladd") else "0")) + + rows.sort() + OUT.parent.mkdir(parents=True, exist_ok=True) + with OUT.open("w", encoding="utf-8") as f: + f.write("# hex,owner,ladd — generated by scripts/gen-le-aircraft.py, do not edit\n") + for hexid, owner, l in rows: + f.write(f"{hexid},{owner},{l}\n") + + print(f"scanned {seen} aircraft -> {len(rows)} law-enforcement ({ladd} LADD-flagged)") + print(f"wrote {OUT} ({OUT.stat().st_size // 1024} KB)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())