Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 194 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ store and query points on the Earth's surface.

- Insert points into the geospatial key value store along with their geographic
coordinates.
- Efficiently query for all points within a given rectangle on the sphere.
- Efficiently query for all points within a rectangle or polygon on the sphere.
- Control the sort order for the results with a custom sorting key.
- Filter query results with equality and `IN` clauses.
- Store polygons and polylines as first-class indexed geometries.
- Query for geometries that contain a point, intersect a shape, or are within a
distance.
- Calculate area, perimeter, length, and centroids of geometries.
- And since it's built on Convex, everything is automatically consistent,
reactive, and cached!

Expand Down Expand Up @@ -143,6 +147,60 @@ New integrations should prefer `nearest`.
This query will find all points that lie within the query rectangle, sort them
in ascending `sortKey` order, and return at most 16 results.

You can also query within arbitrary polygon shapes:

```ts
// convex/index.ts

const example = query({
handler: async (ctx) => {
const polygon = {
exterior: [
{ latitude: 40.7831, longitude: -73.9712 },
{ latitude: 40.7931, longitude: -73.9612 },
{ latitude: 40.7731, longitude: -73.9512 },
],
};
const result = await geospatial.query(ctx, {
shape: { type: "polygon", polygon },
limit: 16,
});
return result;
},
});
```

Polygons are defined by an `exterior` ring of points. The winding order doesn't
matter—the library will automatically normalize it.

You can also query for points within a buffer distance of a polyline (useful for
route-based searches):

```ts
// convex/index.ts

const example = query({
handler: async (ctx) => {
const route = [
{ latitude: 40.7128, longitude: -74.006 }, // NYC
{ latitude: 40.7589, longitude: -73.9851 }, // Midtown
{ latitude: 40.7831, longitude: -73.9712 }, // Upper East
];
const result = await geospatial.query(ctx, {
shape: {
type: "polyline",
polyline: route,
bufferMeters: 500, // 500m corridor
},
limit: 16,
});
return result;
},
});
```

This finds all points within 500 meters of the route path.

You can optionally add filter conditions to queries.

The first type of filter condition is an `in()` filter, which requires that a
Expand Down Expand Up @@ -289,6 +347,141 @@ the query avoids reading unrelated points. Pairing that with a sensible
`maxDistance` further constrains the search space and can greatly speed up
searching the index.

## Storing geometries (polygons and polylines)

In addition to indexing points, you can store polygons and polylines as
first-class indexed geometries. This enables queries like "which delivery zones
contain this address?" or "which transit routes pass near this location?"

```ts
// convex/index.ts

const example = mutation({
handler: async (ctx) => {
// Store a polygon (e.g., a delivery zone)
await geospatial.insertPolygon(
ctx,
"zone-manhattan",
{
exterior: [
{ latitude: 40.7, longitude: -74.02 },
{ latitude: 40.7, longitude: -73.97 },
{ latitude: 40.82, longitude: -73.97 },
{ latitude: 40.82, longitude: -74.02 },
],
},
{ type: "delivery-zone", region: "nyc" },
);

// Store a polyline (e.g., a transit route)
await geospatial.insertPolyline(
ctx,
"route-a-train",
[
{ latitude: 40.7128, longitude: -74.006 },
{ latitude: 40.7589, longitude: -73.9851 },
{ latitude: 40.8448, longitude: -73.8648 },
],
{ type: "subway", line: "A" },
);

// Update or remove geometries
await geospatial.updateGeometry(ctx, "zone-manhattan", undefined, {
type: "delivery-zone",
active: true,
});
await geospatial.removeGeometry(ctx, "route-a-train");
},
});
```

### Querying geometries

Find all polygons that contain a given point:

```ts
const example = query({
handler: async (ctx) => {
const { results } = await geospatial.containsPoint(
ctx,
{ latitude: 40.7580, longitude: -73.9855 }, // Times Square
{ type: "delivery-zone" }, // Optional filter
);
return results; // All delivery zones containing this point
},
});
```

Find geometries that intersect a given shape:

```ts
const example = query({
handler: async (ctx) => {
const { results } = await geospatial.intersects(ctx, {
type: "rectangle",
rectangle: { south: 40.7, north: 40.8, west: -74.0, east: -73.9 },
});
return results; // All geometries overlapping this rectangle
},
});
```

Find geometries within a distance of a point:

```ts
const example = query({
handler: async (ctx) => {
const { results } = await geospatial.geometriesNear(
ctx,
{ latitude: 40.7580, longitude: -73.9855 },
5000, // 5km radius
);
// Results include distance in meters, sorted by proximity
return results;
},
});
```

## Geometry measurements

Calculate area, perimeter, length, and centroids of geometries:

```ts
const example = query({
handler: async (ctx) => {
const polygon = {
exterior: [
{ latitude: 40.7, longitude: -74.0 },
{ latitude: 40.7, longitude: -73.9 },
{ latitude: 40.8, longitude: -73.9 },
{ latitude: 40.8, longitude: -74.0 },
],
};

// Polygon measurements
const area = await geospatial.polygonArea(ctx, polygon); // square meters
const perimeter = await geospatial.polygonPerimeter(ctx, polygon); // meters
const centroid = await geospatial.polygonCentroid(ctx, polygon); // { latitude, longitude }

// Polyline measurements
const route = [
{ latitude: 40.7128, longitude: -74.006 },
{ latitude: 40.7589, longitude: -73.9851 },
];
const length = await geospatial.polylineLength(ctx, route); // meters
const routeCentroid = await geospatial.polylineCentroid(ctx, route);

return { area, perimeter, centroid, length, routeCentroid };
},
});
```

All measurements use spherical great-circle calculations on the Earth's surface
(approximating Earth as a sphere for performance and robustness). For most
applications, this provides sufficient accuracy. If you need exact WGS84
ellipsoidal geodesics, consider using a dedicated geodesy library like
[GeographicLib](https://geographiclib.sourceforge.io/).

## Example

See [`example/`](./example/) for a full example with a
Expand Down
2 changes: 1 addition & 1 deletion example/convex/_generated/dataModel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
* Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
*
* Documents can be loaded using `db.get(id)` in query and mutation functions.
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
*
* IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking.
Expand Down
Loading