# geo-tool

Terminal-native GIS tool for vector data. No QGIS, no GUI, no bloat.

Inspect, convert, reproject, filter, and visualize geospatial datasets entirely from the command line. Renders maps as Unicode braille characters directly in your terminal.

## Supported Formats

| Format      | Read | Write |
|-------------|------|-------|
| Shapefile   | Yes  | Yes   |
| GeoPackage  | Yes  | Yes   |
| GeoJSON     | Yes  | Yes   |
| FlatGeobuf  | Yes  | Yes   |
| TopoJSON    | Yes  | No    |

## Installation

**Recommended:** Use [uv](https://docs.astral.sh/uv/) for the smoothest experience.

```bash
# Add to your project
uv add gis-terminal

# Run the tool
gis-tool --help
```

> ⚠️ **Note:** This package is in active development (beta). Requires Python 3.12+.  
> **Do not use `pip install`** — use `uv` for reliable dependency resolution.

### Development Setup

To contribute or modify the source:

```bash
git clone <repo-url>
cd "GIS Terminal"
uv sync
```

Run without activating:

```bash
# Windows:
.venv\Scripts\gis-tool.exe --help
# Linux/macOS:
.venv/bin/gis-tool --help
```

### Dependencies

Four runtime dependencies, all pulling from GDAL/OGR, PROJ, and GEOS under the hood:

- **click** -- CLI framework
- **fiona** -- vector I/O (GDAL/OGR bindings with streaming support)
- **shapely** -- geometry operations (GEOS bindings)
- **pyproj** -- CRS transformations (PROJ bindings)

## Commands

### `geo-tool info` -- Inspect metadata

```bash
geo-tool info countries.shp
```

```
File:      countries.shp
Driver:    ESRI Shapefile
CRS:       EPSG:4326
Geometry:  Polygon
Features:  177
Extent:    -180.000000, -90.000000, 180.000000, 83.645130

Fields:
  NAME        str
  POP_EST     int64
  GDP_MD      float
  ISO_A3      str
```

Machine-readable output for scripting:

```bash
geo-tool --json info countries.shp
```

```json
{
  "path": "countries.shp",
  "driver": "ESRI Shapefile",
  "crs": "EPSG:4326",
  "geometry_type": "Polygon",
  "feature_count": 177,
  "bounds": [-180.0, -90.0, 180.0, 83.64513],
  "fields": [
    {"name": "NAME", "type": "str"},
    {"name": "POP_EST", "type": "int64"}
  ]
}
```

For multi-layer formats (GeoPackage):

```bash
geo-tool info data.gpkg --layer boundaries
```

### `geo-tool convert` -- Format conversion

Convert between any supported writable formats. The output format is detected from the file extension.

```bash
# Shapefile to GeoPackage
geo-tool convert input.shp output.gpkg

# GeoJSON to FlatGeobuf
geo-tool convert input.geojson output.fgb

# Convert and reproject in one step
geo-tool convert input.shp output.geojson --crs EPSG:4326
```

### `geo-tool reproject` -- Change coordinate reference system

```bash
# WGS84 to Web Mercator
geo-tool reproject input.geojson output.geojson --crs EPSG:3857

# To a UTM zone
geo-tool reproject input.shp output.gpkg --crs EPSG:32633
```

The `--crs` flag accepts any string that [pyproj](https://pyproj4.github.io/pyproj/) understands: `EPSG:XXXX`, WKT, or PROJ strings.

### `geo-tool filter` -- Spatial and attribute filtering

Filter by bounding box (west, south, east, north):

```bash
geo-tool filter countries.gpkg --bbox -10,35,30,60
```

Filter by attribute expression (OGR SQL syntax):

```bash
geo-tool filter countries.gpkg --where "POP_EST > 50000000"
```

Combine both:

```bash
geo-tool filter countries.gpkg --bbox -10,35,30,60 --where "POP_EST > 10000000"
```

Write results to a file instead of stdout:

```bash
geo-tool filter countries.gpkg --bbox 0,0,10,10 -o subset.geojson
```

When no `-o` is given, GeoJSON is written to stdout -- pipe it wherever you want:

```bash
geo-tool filter data.gpkg --where "type = 'highway'" | jq '.features | length'
```

### `geo-tool view` -- Terminal map preview

Renders geometries as Unicode braille characters directly in your terminal.

```bash
geo-tool view countries.shp
```

**Interactive controls:**

| Key       | Action          |
|-----------|-----------------|
| `+`       | Zoom in         |
| `-`       | Zoom out        |
| Arrows    | Pan             |
| `r`       | Reset view      |
| `q`       | Quit            |

For non-interactive (pipe-friendly) output:

```bash
geo-tool view countries.shp --static
```

Restrict the initial view to a bounding box:

```bash
geo-tool view countries.shp --bbox -20,30,45,72
```

**How it works:** Each terminal character cell is a 2x4 braille dot pattern (Unicode block U+2800). This gives 8x the pixel resolution of regular characters. An 80-column terminal renders at 160x84 effective pixels. A 200-column terminal reaches 400x192 pixels -- enough to recognize coastlines, boundaries, and road networks.

Geometries are rasterized using Bresenham's line algorithm for lines and polygon outlines. Points are drawn as small crosses. For large datasets (>10,000 features), the renderer samples features and displays a notice.

### `geo-tool serve` -- Browser map

Opens the dataset on a Leaflet map in your browser. No external server or build step required.

```bash
geo-tool serve countries.gpkg
```

```
Serving countries.gpkg at http://localhost:8080
Press Ctrl+C to stop.
```

Options:

```bash
# Custom port
geo-tool serve data.geojson --port 3000

# Don't auto-open the browser
geo-tool serve data.geojson --no-open
```

The server uses Python's built-in `http.server` with Leaflet loaded from CDN. Features are spatially filtered on each map pan/zoom -- only the visible extent is sent to the browser (max 5,000 features per request). Click any feature to see its attributes in a popup.

## Global Options

These go **before** the subcommand:

```bash
geo-tool --json info data.shp        # JSON output
geo-tool -q convert in.shp out.gpkg  # Suppress status messages
geo-tool --version                   # Show version
```

| Flag        | Effect                                |
|-------------|---------------------------------------|
| `--json`    | Machine-readable JSON output          |
| `-q/--quiet`| Suppress non-essential stderr output  |
| `-v/--verbose`| Debug-level logging                 |
| `--version` | Print version and exit                |

## Piping and Scripting

`geo-tool` is designed to be Unix-pipe friendly. Combine commands:

```bash
# Get the CRS of a file
geo-tool --json info data.shp | python -m json.tool

# Filter then inspect the result
geo-tool filter big.gpkg --bbox 0,0,10,10 -o subset.geojson && geo-tool info subset.geojson

# Count features matching a query
geo-tool --json filter data.gpkg --where "pop > 1000000" | python -c "
import json, sys
fc = json.load(sys.stdin)
print(len(fc['features']))
"
```

When stdout is not a TTY (i.e., output is piped), `geo-tool view` automatically uses static mode.

## Project Structure

```
src/geo/
  cli.py                  Entry point, Click command group
  formats.py              File extension to OGR driver mapping
  output.py               Human-readable and JSON formatters
  errors.py               Exception hierarchy
  commands/
    info.py               geo-tool info
    convert.py            geo-tool convert
    reproject.py          geo-tool reproject
    filter.py             geo-tool filter
    view.py               geo-tool view
    serve.py              geo-tool serve
  core/
    reader.py             Dataset opening via Fiona
    writer.py             Dataset writing via Fiona
    inspector.py          Metadata extraction
    transform.py          CRS reprojection via pyproj
    query.py              Spatial/attribute filtering via OGR
  render/
    canvas.py             Boolean pixel grid
    braille.py            Pixel-to-braille encoding
    viewport.py           Geographic-to-pixel coordinate transform
    rasterizer.py         Bresenham lines, geometry rasterization
    display.py            Interactive terminal loop
  server/
    app.py                HTTP server startup
    handler.py            Request routing and feature API
    templates.py          Leaflet HTML template
```

## Design Decisions

- **Click over Typer** -- explicit argument handling, no type-hint magic
- **Fiona over pyogrio** -- streaming iteration instead of bulk memory load
- **Braille over block characters** -- 8x pixel resolution per character cell
- **Leaflet over MapLibre** -- 40KB from CDN, zero build step, native GeoJSON support
- **No rich/tabulate** -- the tool has ~3 tables total; `str.ljust()` is enough
- **`always_xy=True` in pyproj** -- prevents silent lon/lat axis swap that corrupts data
- **`src/` package layout** -- PyPA standard, prevents accidental local imports

## License

MIT
