Skip to content

Commit c795432

Browse files
committed
Add ruview-geo: geospatial satellite integration (11 modules, 8/8 tests)
New crate with free satellite imagery, terrain, OSM, weather, and brain integration. Modules: types, coord, locate, cache, tiles, terrain, osm, register, fuse, brain, temporal Tests: 8 passed (haversine, ENU roundtrip, tiles, HGT parse, registration) Validation: real data — 43.49N 79.71W, 4 Sentinel-2 tiles, 2°C weather, brain stored Data sources (all free, no API keys): - EOX Sentinel-2 cloudless (10m satellite tiles) - SRTM GL1 (30m elevation) - Overpass API (OSM buildings/roads) - ip-api.com (geolocation) - Open Meteo (weather) ADR-044 documents architecture decisions. README.md in crate subdirectory. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 4ab6935 commit c795432

File tree

19 files changed

+1153
-0
lines changed

19 files changed

+1153
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# ADR-044: Geospatial Satellite Integration
2+
3+
## Status
4+
Accepted
5+
6+
## Context
7+
RuView generates real-time 3D point clouds from camera + WiFi CSI, but these exist in a local coordinate frame with no geographic reference. Integrating free satellite imagery, terrain elevation, and map data provides environmental context that enables the ruOS brain to reason about the physical world beyond the room.
8+
9+
## Decision
10+
11+
### Data Sources (all free, no API keys)
12+
| Source | Data | Resolution | Update | Format |
13+
|--------|------|-----------|--------|--------|
14+
| EOX Sentinel-2 Cloudless | Satellite tiles | 10m | Static mosaic | XYZ/JPEG |
15+
| SRTM GL1 (NASA) | Elevation/DEM | 30m (1-arcsec) | Static | Binary HGT |
16+
| Overpass API (OSM) | Buildings, roads | Vector | Real-time | JSON |
17+
| ip-api.com | IP geolocation | ~1km | Per-request | JSON |
18+
| Sentinel-2 STAC | Temporal satellite | 10m | Every 5 days | COG/STAC |
19+
| Open Meteo | Weather | Point | Hourly | JSON |
20+
21+
### Architecture
22+
Pure Rust implementation in `wifi-densepose-geo` crate. No GDAL/PROJ/GEOS — coordinate transforms implemented directly (~250 LOC). Tile caching on disk at `~/.local/share/ruview/geo-cache/`.
23+
24+
### Coordinate System
25+
- WGS84 for geographic coordinates
26+
- ENU (East-North-Up) as the bridge between local sensor frame and world
27+
- Local sensor frame: camera origin, +Z forward, +Y up
28+
29+
### Temporal Awareness
30+
Nightly scheduled fetch of Sentinel-2 latest imagery + OSM diffs + weather.
31+
Changes detected via image comparison and stored as brain memories for
32+
contrastive learning.
33+
34+
### Brain Integration
35+
Geospatial context stored as brain memories:
36+
- `spatial-geo`: location, elevation, nearby landmarks
37+
- `spatial-change`: detected changes in satellite/OSM data
38+
- `spatial-weather`: current conditions + forecast
39+
- `spatial-season`: vegetation index, snow cover, seasonal patterns
40+
41+
## Consequences
42+
### Positive
43+
- Agent gains environmental awareness beyond the room
44+
- Temporal data enables seasonal calibration of CSI sensing
45+
- Change detection finds construction, vegetation, weather effects
46+
- All data sources are genuinely free with no API keys
47+
48+
### Negative
49+
- Initial data fetch requires internet (~2MB tiles + ~25MB DEM)
50+
- Cached data becomes stale (mitigated by nightly refresh)
51+
- IP geolocation has ~1km accuracy (mitigated by manual override)

rust-port/wifi-densepose-rs/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-port/wifi-densepose-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"crates/wifi-densepose-ruvector",
1919
"crates/wifi-densepose-desktop",
2020
"crates/wifi-densepose-pointcloud",
21+
"crates/wifi-densepose-geo",
2122
]
2223
# ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std),
2324
# excluded from workspace to avoid breaking `cargo test --workspace`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "ruview-geo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Geospatial satellite integration — free satellite tiles, DEM, OSM, temporal tracking"
6+
7+
[dependencies]
8+
serde = { workspace = true }
9+
serde_json = { workspace = true }
10+
tokio = { workspace = true }
11+
anyhow = { workspace = true }
12+
reqwest = { version = "0.12", features = ["json", "native-tls"], default-features = false }
13+
chrono = "0.4"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# ruview-geo — Geospatial Satellite Integration
2+
3+
Free satellite imagery, terrain elevation, and map data for RuView spatial sensing. No API keys required.
4+
5+
## What It Does
6+
7+
Integrates your local sensor data (camera + WiFi CSI point cloud) with geographic context:
8+
9+
- **Satellite tiles** — 10m Sentinel-2 cloudless imagery for your location
10+
- **Elevation** — SRTM 30m DEM for terrain modeling
11+
- **Buildings + roads** — OpenStreetMap data via Overpass API
12+
- **Weather** — Open Meteo current conditions + forecast
13+
- **Geo-registration** — maps local sensor coordinates to WGS84
14+
- **Temporal tracking** — detects changes over time (construction, vegetation, weather)
15+
- **Brain integration** — stores geospatial context as ruOS brain memories
16+
17+
## Data Sources (all free, no API keys)
18+
19+
| Source | Data | Resolution | License |
20+
|--------|------|-----------|---------|
21+
| [EOX S2 Cloudless](https://s2maps.eu/) | Satellite tiles | 10m | CC-BY-4.0 |
22+
| [SRTM GL1](https://portal.opentopography.org/) | Elevation/DEM | 30m | Public domain |
23+
| [Overpass API](https://overpass-api.de/) | OSM buildings/roads | Vector | ODbL |
24+
| [ip-api.com](http://ip-api.com/) | IP geolocation | ~1km | Free |
25+
| [Open Meteo](https://open-meteo.com/) | Weather | Point | CC-BY-4.0 |
26+
27+
## Modules
28+
29+
| Module | LOC | Purpose |
30+
|--------|-----|---------|
31+
| `types.rs` | 140 | GeoPoint, GeoBBox, TileCoord, ElevationGrid, OsmFeature |
32+
| `coord.rs` | 80 | WGS84/ENU transforms, tile math, haversine distance |
33+
| `locate.rs` | 45 | IP geolocation with caching |
34+
| `cache.rs` | 55 | Disk cache (`~/.local/share/ruview/geo-cache/`) |
35+
| `tiles.rs` | 80 | Sentinel-2/ESRI/OSM tile fetcher |
36+
| `terrain.rs` | 100 | SRTM HGT parser, elevation lookup |
37+
| `osm.rs` | 150 | Overpass API client, building/road extraction |
38+
| `register.rs` | 50 | Local-to-WGS84 coordinate registration |
39+
| `fuse.rs` | 70 | Multi-source scene builder + summary |
40+
| `brain.rs` | 30 | Store geo context in ruOS brain |
41+
| `temporal.rs` | 100 | Weather, OSM change detection |
42+
43+
## Usage
44+
45+
```rust
46+
use ruview_geo::{fuse, brain, temporal};
47+
48+
// Build geo scene for current location
49+
let scene = fuse::build_scene(500.0).await?; // 500m radius
50+
println!("{}", fuse::summarize(&scene));
51+
// "Location: 43.6532N, 79.3832W, elevation 76m ASL.
52+
// 23 buildings within view. 8 roads nearby (King St, Queen St).
53+
// 12 satellite tiles at zoom 16."
54+
55+
// Store in brain
56+
brain::store_geo_context(&scene).await?;
57+
58+
// Fetch weather
59+
let weather = temporal::fetch_weather(&scene.location).await?;
60+
// temperature: 12°C, partly cloudy, humidity 65%
61+
```
62+
63+
## Brain Integration
64+
65+
Geospatial context is stored as brain memories:
66+
67+
| Category | Content | Frequency |
68+
|----------|---------|-----------|
69+
| `spatial-geo` | Location, elevation, buildings, roads | On startup + daily |
70+
| `spatial-weather` | Temperature, conditions, humidity, wind | Nightly |
71+
| `spatial-change` | New/removed buildings, road changes | Nightly diff |
72+
73+
The ruOS agent can search: "what buildings are near me?" or "what's the weather?" and get geospatial context from the brain.
74+
75+
## Security
76+
77+
- No API keys stored or transmitted
78+
- IP geolocation uses HTTP (not HTTPS) — location is approximate (~1km)
79+
- All tile fetches use HTTPS except ip-api.com
80+
- Path traversal protection in cache key sanitization
81+
- No user data sent to external services
82+
- All data cached locally after first fetch
83+
84+
## Architecture
85+
86+
```
87+
IP Geolocation ──→ (lat, lon)
88+
89+
┌─────────────┼─────────────┐
90+
▼ ▼ ▼
91+
Sentinel-2 SRTM DEM Overpass API
92+
(tiles) (elevation) (buildings/roads)
93+
│ │ │
94+
└─────────────┼─────────────┘
95+
96+
GeoScene (fused)
97+
98+
┌───────┴───────┐
99+
▼ ▼
100+
Brain Memory Three.js Viewer
101+
```
102+
103+
## License
104+
105+
MIT (same as RuView)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use ruview_geo::*;
2+
3+
#[tokio::main]
4+
async fn main() -> anyhow::Result<()> {
5+
println!("╔══════════════════════════════════════════════╗");
6+
println!("║ ruview-geo — Real Data Validation ║");
7+
println!("╚══════════════════════════════════════════════╝\n");
8+
9+
let t0 = std::time::Instant::now();
10+
let cache = cache::TileCache::new("/tmp/ruview-geo-validate");
11+
12+
let loc = locate::get_location(&format!("{}/location.json", cache.base_dir.display())).await?;
13+
println!(" Location: {:.4}N, {:.4}W", loc.lat, loc.lon);
14+
15+
let bbox = GeoBBox::from_center(&loc, 300.0);
16+
let tiles_list = tiles::fetch_area(&tiles::TileProvider::Sentinel2Cloudless, &bbox, 16, &cache).await?;
17+
println!(" Tiles: {} ({:.0}KB)", tiles_list.len(),
18+
tiles_list.iter().map(|t| t.data.len()).sum::<usize>() as f64 / 1024.0);
19+
20+
let dem = terrain::fetch_elevation(&loc, &cache).await?;
21+
println!(" Elevation: {:.0}m (grid {}x{})", terrain::elevation_at(&dem, &loc), dem.cols, dem.rows);
22+
23+
let buildings = osm::fetch_buildings(&loc, 300.0).await.unwrap_or_default();
24+
let roads = osm::fetch_roads(&loc, 300.0).await.unwrap_or_default();
25+
println!(" OSM: {} buildings, {} roads", buildings.len(), roads.len());
26+
27+
let weather = temporal::fetch_weather(&loc).await?;
28+
println!(" Weather: {:.0}°C humidity={:.0}% wind={:.1}m/s",
29+
weather.temperature_c, weather.humidity_pct, weather.wind_speed_ms);
30+
31+
let scene = GeoScene {
32+
location: loc.clone(), bbox, elevation_m: terrain::elevation_at(&dem, &loc),
33+
buildings, roads, tile_count: tiles_list.len(),
34+
registration: register::auto_register(&loc),
35+
last_updated: chrono::Utc::now().to_rfc3339(),
36+
};
37+
println!("\n {}", fuse::summarize(&scene));
38+
39+
match brain::store_geo_context(&scene).await {
40+
Ok(n) => println!(" Brain: {} memories stored", n),
41+
Err(e) => println!(" Brain: {e}"),
42+
}
43+
44+
println!("\n Total: {}ms | Cache: {:.0}KB",
45+
t0.elapsed().as_millis(), cache.size_bytes() as f64 / 1024.0);
46+
Ok(())
47+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Brain integration — store geospatial context in ruOS brain.
2+
3+
use crate::fuse;
4+
use crate::types::GeoScene;
5+
use anyhow::Result;
6+
7+
const BRAIN_URL: &str = "http://127.0.0.1:9876";
8+
9+
/// Store geospatial context in the brain.
10+
pub async fn store_geo_context(scene: &GeoScene) -> Result<u32> {
11+
let client = reqwest::Client::builder()
12+
.timeout(std::time::Duration::from_secs(5))
13+
.build()?;
14+
15+
let mut stored = 0u32;
16+
17+
// Store location summary
18+
let summary = fuse::summarize(scene);
19+
let body = serde_json::json!({
20+
"category": "spatial-geo",
21+
"content": summary,
22+
});
23+
if client.post(format!("{BRAIN_URL}/memories")).json(&body).send().await.is_ok() {
24+
stored += 1;
25+
}
26+
27+
Ok(stored)
28+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Disk cache for tiles, DEM, and OSM data.
2+
3+
use anyhow::Result;
4+
use std::path::{Path, PathBuf};
5+
6+
pub struct TileCache {
7+
pub base_dir: PathBuf,
8+
}
9+
10+
impl TileCache {
11+
pub fn new(base_dir: &str) -> Self {
12+
let expanded = base_dir.replace('~', &std::env::var("HOME").unwrap_or_default());
13+
let path = PathBuf::from(expanded);
14+
let _ = std::fs::create_dir_all(&path);
15+
Self { base_dir: path }
16+
}
17+
18+
pub fn default_cache() -> Self {
19+
Self::new("~/.local/share/ruview/geo-cache")
20+
}
21+
22+
pub fn get(&self, key: &str) -> Option<Vec<u8>> {
23+
let path = self.key_path(key);
24+
std::fs::read(&path).ok()
25+
}
26+
27+
pub fn put(&self, key: &str, data: &[u8]) -> Result<()> {
28+
let path = self.key_path(key);
29+
if let Some(parent) = path.parent() {
30+
std::fs::create_dir_all(parent)?;
31+
}
32+
std::fs::write(&path, data)?;
33+
Ok(())
34+
}
35+
36+
pub fn has(&self, key: &str) -> bool {
37+
self.key_path(key).exists()
38+
}
39+
40+
pub fn size_bytes(&self) -> u64 {
41+
walkdir(self.base_dir.as_path())
42+
}
43+
44+
fn key_path(&self, key: &str) -> PathBuf {
45+
// Sanitize key to prevent path traversal
46+
let safe_key = key.replace("..", "_").replace('/', "_");
47+
self.base_dir.join(safe_key)
48+
}
49+
}
50+
51+
fn walkdir(path: &Path) -> u64 {
52+
std::fs::read_dir(path)
53+
.into_iter()
54+
.flatten()
55+
.filter_map(|e| e.ok())
56+
.map(|e| {
57+
if e.path().is_dir() { walkdir(&e.path()) }
58+
else { e.metadata().map(|m| m.len()).unwrap_or(0) }
59+
})
60+
.sum()
61+
}

0 commit comments

Comments
 (0)