| name | shiny-spatial |
| description | Build geospatial applications with Shiny.Spatial - a dependency-free, AOT-compatible .NET spatial database using SQLite R*Tree indexing with custom C# geometry algorithms for two-pass spatial queries |
| auto_invoke | true |
| triggers | ["spatial","geospatial","spatial database","rtree","shiny.spatial","shiny spatial","geofence","geofencing","region change"] |
Shiny.Spatial Skill
You are an expert in Shiny.Spatial, a dependency-free, cross-platform .NET geospatial database library that uses SQLite R*Tree for spatial indexing with custom C# geometry algorithms for query refinement. No SpatiaLite, no NetTopologySuite — only SQLite via Microsoft.Data.Sqlite.
When to Use This Skill
Invoke this skill when the user wants to:
- Create or query a spatial database with geospatial data
- Store and retrieve geometry objects (Points, LineStrings, Polygons, Multi*, GeometryCollection)
- Perform spatial queries: distance, intersection, containment, bounding box
- Use the fluent query builder with property filters and distance ordering
- Work with WKB (Well-Known Binary) serialization
- Use pre-built databases (US/Canadian states, provinces, cities)
- Build location-aware applications on iOS, Android, or any .NET platform
- Set up GPS-driven geofence monitoring with enter/exit events
- Implement an
ISpatialGeofenceDelegate for region change handling
- Configure
ISpatialGeofenceManager to start/stop geofence detection
Library Overview
Repository: https://github.com/shinyorg/geospatialdb
Namespace: Shiny.Spatial
AOT compatible and trimmable.
Target Frameworks
| Package | Framework | Notes |
|---|
Shiny.Spatial | net10.0 | AOT compatible and trimmable |
Shiny.Spatial.Geofencing | net10.0-ios, net10.0-android | iOS/Android GPS geofencing |
Dependencies
Microsoft.Data.Sqlite — brings SQLitePCLRaw.bundle_e_sqlite3 with R*Tree enabled
Shiny.Locations — geofencing package only (background GPS)
Setup
Install NuGet Package
dotnet add package Shiny.Spatial
Architecture
Two-Pass Query Pipeline
- R*Tree bounding box filter (SQL, O(log n)) — eliminates most candidates using the SQLite R*Tree index
- C# geometry refinement — exact Contains/Intersects/WithinDistance checks on survivors
SQLite Schema
Each spatial table creates a single R*Tree virtual table with auxiliary columns:
CREATE VIRTUAL TABLE {name}_rtree USING rtree(
id, min_x, max_x, min_y, max_y,
+geometry BLOB,
+prop_{name} {type}, ...
);
Metadata is tracked in __spatial_meta and __spatial_columns tables.
Geometry Types
All geometry classes are immutable and sealed, extending the abstract Geometry base class. Namespace: Shiny.Spatial.Geometry.
| Type | Description |
|---|
Coordinate | Readonly struct with X/Y (aliased as Longitude/Latitude); implicitly converts to Point |
Envelope | Readonly struct — bounding box with MinX, MaxX, MinY, MaxY |
Point | Single coordinate |
LineString | Ordered sequence of coordinates (minimum 2) |
Polygon | Exterior ring + optional interior rings (holes) |
MultiPoint | Collection of Points |
MultiLineString | Collection of LineStrings |
MultiPolygon | Collection of Polygons |
GeometryCollection | Collection of mixed Geometry types |
Creating Geometries
using Shiny.Spatial.Geometry;
var point = new Point(-104.99, 39.74);
var line = new LineString(new[]
{
new Coordinate(-104.99, 39.74),
new Coordinate(-104.82, 38.83)
});
var polygon = new Polygon(new[]
{
new Coordinate(-109.05, 37.0), new Coordinate(-102.05, 37.0),
new Coordinate(-102.05, 41.0), new Coordinate(-109.05, 41.0),
new Coordinate(-109.05, 37.0)
});
var polygonWithHole = new Polygon(
exteriorRing: new[] { },
interiorRings: new[] { new[] { } }
);
var multiPoint = new MultiPoint(new[] { point1, point2 });
var multiLine = new MultiLineString(new[] { line1, line2 });
var multiPoly = new MultiPolygon(new[] { polygon1, polygon2 });
var collection = new GeometryCollection(new Geometry[] { point, line, polygon });
Database API
SpatialDatabase (IDisposable)
using Shiny.Spatial.Database;
var db = new SpatialDatabase("path.db");
var db = new SpatialDatabase(":memory:");
SpatialTable table = db.CreateTable(name, coordinateSystem, properties...);
SpatialTable table = db.GetTable(name);
bool exists = db.TableExists(name);
db.DropTable(name);
db.Dispose();
Creating Tables with Properties
var table = db.CreateTable("cities", CoordinateSystem.Wgs84,
new PropertyDefinition("name", PropertyType.Text),
new PropertyDefinition("population", PropertyType.Integer),
new PropertyDefinition("area", PropertyType.Real));
| PropertyType | Description |
|---|
Text | String values |
Integer | Long integer values |
Real | Double floating point values |
Blob | Binary data |
SpatialTable CRUD
| Method | Returns | Description |
|---|
Insert(feature) | long | Insert a feature, returns its ID |
BulkInsert(features) | void | Insert many features in a single transaction |
Update(feature) | void | Update a feature by ID |
Delete(id) | bool | Delete a feature by ID |
GetById(id) | SpatialFeature? | Retrieve a single feature |
Count() | long | Total feature count |
SpatialFeature
var feature = new SpatialFeature(new Point(-104.99, 39.74))
{
Properties = { ["name"] = "Denver", ["population"] = 715000L }
};
long id = feature.Id;
Geometry geom = feature.Geometry;
Dictionary<string, object?> props = feature.Properties;
Spatial Queries
| Method | Description |
|---|
FindInEnvelope(envelope) | R*Tree bounding box query |
FindIntersecting(geometry) | Two-pass intersection query |
FindContainedBy(geometry) | Two-pass containment query |
FindWithinDistance(center, meters) | Two-pass distance query |
Query() | Returns a fluent SpatialQuery builder |
var nearby = table.FindWithinDistance(
new Coordinate(-104.99, 39.74),
distanceMeters: 150_000
);
var inState = table.FindIntersecting(coloradoPolygon);
var envelope = new Envelope(-110, -100, 35, 42);
var inBox = table.FindInEnvelope(envelope);
Fluent Query Builder
| Method | Type | Description |
|---|
InEnvelope(envelope) | Filter | Bounding box filter |
Intersecting(geometry) | Filter | Geometry intersection |
ContainedBy(geometry) | Filter | Geometry containment |
WithinDistance(center, meters) | Filter | Distance radius |
WhereProperty(name, op, value) | Filter | Property comparison (=, !=, <, <=, >, >=, LIKE) |
OrderByDistance(center) | Sort | Order by distance from coordinate |
Limit(count) | Paging | Limit result count |
Offset(count) | Paging | Skip first N results |
ToList() | Terminal | Execute and return results |
Count() | Terminal | Execute and return count |
FirstOrDefault() | Terminal | Execute and return first or null |
var center = new Coordinate(-104.99, 39.74);
var results = table.Query()
.WithinDistance(center, 150_000)
.WhereProperty("population", ">", 200000L)
.OrderByDistance(center)
.Limit(10)
.ToList();
int count = table.Query().InEnvelope(envelope).Count();
var first = table.Query().WithinDistance(center, 1000).FirstOrDefault();
Coordinate vs geometry parameters: WithinDistance/OrderByDistance (and FindWithinDistance) take a Coordinate; the geometry methods Intersecting/ContainedBy (and FindIntersecting/FindContainedBy) take a Geometry. A Coordinate implicitly converts to a Point, so you can pass a bare lon/lat to the geometry methods directly — e.g. query.Intersecting(new Coordinate(lon, lat)) — without writing new Point(...).
Algorithms
Namespace: Shiny.Spatial.Algorithms
| Class | Method | Description |
|---|
DistanceCalculator | Haversine(a, b) | Great-circle distance in meters (WGS84) |
DistanceCalculator | Euclidean(a, b) | Cartesian distance |
DistanceCalculator | DistanceToSegment(p, a, b) | Perpendicular distance from point to segment |
PointInPolygon | Contains(polygon, point) | Ray-casting with hole support |
SegmentIntersection | Intersects(a1, a2, b1, b2) | Cross-product segment intersection test |
SpatialPredicates | Intersects(a, b) | Dispatch for all geometry type combinations |
SpatialPredicates | Contains(container, contained) | Dispatch for all geometry type combinations |
EnvelopeExpander | ExpandByDistance(env, meters, cs) | Expand envelope by distance (WGS84 or Cartesian) |
WKB Serialization
using Shiny.Spatial.Serialization;
byte[] wkb = WkbWriter.Write(geometry);
Geometry restored = WkbReader.Read(wkb);
Full roundtrip support for all geometry types using the WKB (Well-Known Binary) format.
Pre-Built Databases
Located in databases/:
| Database | Table | Geometry | Records | Properties |
|---|
us-states.db | states | Polygon | 51 (50 states + DC) | name, abbreviation, population |
us-cities.db | cities | Point | 100 (top 100 by pop.) | name, state, population |
ca-provinces.db | provinces | Polygon | 13 (all provinces/territories) | name, abbreviation, population |
ca-cities.db | cities | Point | 50 (top 50 by pop.) | name, province, population |
All use CoordinateSystem.Wgs84 (longitude/latitude).
using var db = new SpatialDatabase("databases/us-states.db");
var states = db.GetTable("states");
var denver = new Point(-104.99, 39.74);
var results = states.FindIntersecting(denver);
Coordinate Systems
| System | Enum | Distance Algorithm | Use Case |
|---|
| WGS84 | CoordinateSystem.Wgs84 | Haversine (great-circle) | Real-world GPS coordinates |
| Cartesian | CoordinateSystem.Cartesian | Euclidean | Flat/projected coordinate systems |
Code Generation Instructions
When generating code with Shiny.Spatial:
1. Database Lifecycle
- Always wrap
SpatialDatabase in a using statement or IDisposable pattern
- Use
:memory: for tests, file paths for production
- Call
CreateTable for new tables, GetTable for existing tables
2. Coordinate System Selection
- Use
CoordinateSystem.Wgs84 for real-world GPS latitude/longitude data
- Use
CoordinateSystem.Cartesian for projected or flat coordinate systems
- WGS84 uses Haversine distance (meters), Cartesian uses Euclidean distance
3. Property Types
- Use
PropertyType.Text for strings
- Use
PropertyType.Integer for long values — always use L suffix (e.g., 715000L)
- Use
PropertyType.Real for doubles
- Use
PropertyType.Blob for binary data
4. Query Strategy
- For simple spatial queries, use the direct methods (
FindWithinDistance, FindIntersecting, etc.)
- For combined spatial + property filtering, use the fluent
Query() builder
- Always apply spatial filters first (they use the R*Tree index) before property filters
- Use
Limit() and Offset() for pagination
- Use
OrderByDistance() for nearest-neighbor results
5. Bulk Operations
- Use
BulkInsert() for inserting multiple features — it wraps in a transaction for performance
- Single
Insert() is fine for one-off inserts
6. Geometry Construction
- Polygons must be closed (first coordinate == last coordinate)
- LineStrings require minimum 2 coordinates
- Use
Coordinate struct for X/Y pairs (aliases: Longitude/Latitude)