# Tools CLI scripts for fetching, rendering, and reprojecting geospatial imagery. All tools read the Google Maps API key from `.env` automatically. Output files go to `output/` by default (gitignored). ## Prerequisites - Node.js (via `mise`) - `sharp` and `puppeteer` (devDependencies — `npm install`) - Google Maps API key in `.env` as `GOOGLE_MAPS_API_KEY` - APIs enabled on your Google Cloud project: **Map Tiles API**, **Street View Static API** --- ## sat-ortho.mjs Stitches satellite tiles from the Map Tiles API into a single ortho image centered on a lat/lon. ```sh # 2K ortho at zoom 21 (~6.4 cm/pixel, ~132m coverage) node tools/sat-ortho.mjs --lat 30.266476 --lon -97.73719 # Zoom 20 for wider coverage (~264m), or zoom 22 for max detail (~3.7 cm/pixel) node tools/sat-ortho.mjs --lat 30.266476 --lon -97.73719 --zoom 20 --size 2048 node tools/sat-ortho.mjs --lat 30.266476 --lon -97.73719 --zoom 22 --size 2048 ``` | Option | Default | Description | |--------|---------|-------------| | `--lat` | — | Center latitude (required) | | `--lon` | — | Center longitude (required) | | `--zoom` | 21 | Tile zoom level (max typically 22) | | `--size` | 2048 | Output square size in pixels | Reports GSD, ground coverage, and NW/SE corner coordinates for georeferencing. --- ## streetview-headings.mjs Fetches 8 Street View static images (one per compass heading) at a location, with optional first-order neighbor traversal. ```sh # 8 images at the intersection node tools/streetview-headings.mjs --lat 30.266476 --lon -97.73719 # Include all first-order neighbor locations (auto-deduped) node tools/streetview-headings.mjs --lat 30.266476 --lon -97.73719 --neighbors # Custom FOV and pitch node tools/streetview-headings.mjs --lat 30.266476 --lon -97.73719 --fov 120 --pitch -10 ``` | Option | Default | Description | |--------|---------|-------------| | `--lat` | — | Latitude (required) | | `--lon` | — | Longitude (required) | | `--fov` | 90 | Field of view in degrees (10-120) | | `--pitch` | 0 | Camera pitch (-90 to 90) | | `--size` | 640x640 | Image size (max 640 per axis) | | `--step` | 45 | Heading step in degrees | | `--neighbors` | off | Also fetch images from nearby Street View locations | The `--neighbors` flag queries Google's panorama metadata for linked locations, deduplicates them by pano ID via the metadata API, and fetches 8 heading images from each unique neighbor within 15m. --- ## streetview-panorama.mjs Fetches and stitches a full equirectangular panorama from the Map Tiles API (Street View tiles endpoint). ```sh # Default zoom 3 → 4096x2048 panorama node tools/streetview-panorama.mjs --lat 30.266476 --lon -97.73719 # Max resolution (zoom 5 → up to 16384x8192) node tools/streetview-panorama.mjs --lat 30.266476 --lon -97.73719 --zoom 5 ``` | Option | Default | Description | |--------|---------|-------------| | `--lat` | — | Latitude (required) | | `--lon` | — | Longitude (required) | | `--zoom` | 3 | Tile zoom 0-5 (higher = larger pano) | | `--radius` | 50 | Search radius in meters for nearest panorama | Output is a standard equirectangular JPEG suitable as input for `pano-pinhole.mjs`. --- ## pano-pinhole.mjs Reprojects an equirectangular panorama into pinhole (perspective) camera views. Uses inverse mapping with bilinear interpolation — every output pixel is sampled from the panorama, so there are no holes. ```sh # Single view: looking west, 90 deg FOV, 1920x1080 node tools/pano-pinhole.mjs --input output/panorama_30.266476_-97.73719.jpg --heading 270 --hfov 90 # All 8 compass headings from one panorama node tools/pano-pinhole.mjs --input output/panorama_30.266476_-97.73719.jpg --all --hfov 90 # 2K output with narrow FOV node tools/pano-pinhole.mjs --input output/panorama_30.266476_-97.73719.jpg --heading 180 --hfov 60 --width 2560 --height 1440 # Specify focal length instead of FOV node tools/pano-pinhole.mjs --input output/panorama_30.266476_-97.73719.jpg --heading 90 --focal 2000 # 12 views at 30-degree steps node tools/pano-pinhole.mjs --input output/panorama_30.266476_-97.73719.jpg --all --step 30 --hfov 90 ``` | Option | Default | Description | |--------|---------|-------------| | `--input` | — | Equirectangular panorama JPEG (required) | | `--heading` | 0 | Compass heading (0=N, 90=E, 180=S, 270=W) | | `--pitch` | 0 | Pitch in degrees (positive=up) | | `--roll` | 0 | Roll in degrees (positive=clockwise) | | `--hfov` | 90 | Horizontal field of view in degrees | | `--focal` | — | Focal length in pixels (overrides `--hfov`) | | `--width` | 1920 | Output width | | `--height` | 1080 | Output height | | `--all` | off | Render all compass headings (ignores `--heading`) | | `--step` | 45 | Heading step for `--all` mode | Reports horizontal, vertical, and diagonal FOV plus equivalent focal length. --- ## cesium-render.mjs Renders a CesiumJS 3D view to JPEG via headless Chromium with Google Photorealistic 3D tiles. Requires SwiftShader (no GPU). ```sh # Look-at mode: camera looks at a target point from above node tools/cesium-render.mjs --lookat-lat 30.266476 --lookat-lon -97.73719 --heading 180 --pitch -30 --height 25 # Top-down view at 80m node tools/cesium-render.mjs --lookat-lat 30.266476 --lookat-lon -97.73719 --pitch -90 --height 80 --fov 90 # 2K output node tools/cesium-render.mjs --lookat-lat 30.266476 --lookat-lon -97.73719 --heading 180 --pitch -30 --height 25 --width 2560 --height-px 1440 # Direct mode: camera placed at coordinates node tools/cesium-render.mjs --lat 30.266476 --lon -97.73719 --heading 270 --pitch -15 --height 8 ``` | Option | Default | Description | |--------|---------|-------------| | `--lat/--lon` | — | Camera position (direct mode) | | `--lookat-lat/--lookat-lon` | — | Target to look at (lookat mode) | | `--heading` | 0 | Compass heading | | `--pitch` | -10 | Camera pitch (must be negative in lookat mode) | | `--height` | 8 | Meters above ground | | `--fov` | 60 | Vertical field of view | | `--width` | 1280 | Image width | | `--height-px` | 720 | Image height | | `--sse` | 2 | Screen-space error (lower = sharper, slower) | | `--timeout` | 30 | Max wait in seconds | Uses progressive SSE refinement and automatic ground-height sampling. Street-level views are limited by Google's photogrammetry tile resolution. --- ## Typical Workflow ```sh # 1. Get a satellite ortho of an intersection node tools/sat-ortho.mjs --lat 30.266476 --lon -97.73719 --zoom 21 # 2. Fetch a 4K panorama at the same location node tools/streetview-panorama.mjs --lat 30.266476 --lon -97.73719 # 3. Extract 8 pinhole views from the panorama node tools/pano-pinhole.mjs --input output/panorama_30.266476_-97.73719.jpg --all --hfov 90 # 4. Also grab the Street View static API versions for comparison node tools/streetview-headings.mjs --lat 30.266476 --lon -97.73719 # 5. Render a 3D view of the same location from above node tools/cesium-render.mjs --lookat-lat 30.266476 --lookat-lon -97.73719 --pitch -90 --height 50 --fov 90 --width 2560 --height-px 1440 ```