npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@convex-dev/geospatial

v0.1.5

Published

A geospatial index for Convex

Downloads

287

Readme

Convex Geospatial Index (Beta)

npm version

image

This component adds a geospatial index to Convex, allowing you to efficiently 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.
  • Control the sort order for the results with a custom sorting key.
  • Filter query results with equality and IN clauses.
  • And since it's built on Convex, everything is automatically consistent, reactive, and cached!

This component is currently in beta. It's missing some functionality, but what's there should work. We've tested the example app up to about 1,000,000 points, so reach out if you're using a much larger dataset. If you find a bug or have a feature request, you can file it here.

Pre-requisite: Convex

You'll need an existing Convex project to use the component. Convex is a hosted backend platform, including a database, serverless functions, and a ton more you can learn about here.

Run npm create convex or follow any of the quickstarts to set one up.

Installation

First, add @convex-dev/geospatial to your Convex project:

npm install @convex-dev/geospatial

Then, install the component into your Convex project within the convex/convex.config.ts file:

// convex/convex.config.ts
import geospatial from "@convex-dev/geospatial/convex.config";
import { defineApp } from "convex/server";

const app = defineApp();
app.use(geospatial);

export default app;

Finally, create a new GeospatialIndex within your convex/ folder, and point it to the installed component:

// convex/index.ts
import { GeospatialIndex } from "@convex-dev/geospatial";
import { components } from "./_generated/api";

const geospatial = new GeospatialIndex(components.geospatial);

Inserting points

After installing the component, you can insert, get, and remove points from the index. You can specify a filterKeys record for filtering at query time and optionally a sortKey for the query result order. We currently only support ascending order on the sortKey.

// convex/index.ts

const example = mutation({
  handler: async (ctx) => {
    const cityId = await ctx.db.insert("cities", {...});
    await geospatial.insert(
      ctx,
      "American Museum of Natural History",
      {
        latitude: 40.7813,
        longitude: -73.9737,
      },
      { category: "museum" },
      28.0, // Price used as the sort key
    );
    const result = await geospatial.get(ctx, cityId);
    await geospatial.remove(ctx, cityId);
  },
});

If you would like some more typesafety, you can specify a type argument for the GeospatialIndex class. This will also provide you with auto-complete for the filterKeys and sortKey parameters. Above the key was "American Museum of Natural History" but most commonly the key will be an ID in another table of yours.

// convex/index.ts
import { GeospatialIndex, Point } from "@convex-dev/geospatial";
import { components } from "./_generated/api";
import { Id } from "./_generated/dataModel";

export const geospatial = new GeospatialIndex<
  Id<"museums">,
  { category: string; anotherFilter?: number }
>(components.geospatial);

Querying points

After inserting some points, you can query them with the query API.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      limit: 16,
    });
    return result;
  },
});

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 optionally add filter conditions to queries.

The first type of filter condition is an in() filter, which requires that a matching document have a filter field with a value in a specified set.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      filter: (q) => q.in("category", ["museum", "restaurant"]),
    });
    return result;
  },
});

The second type of filter condition is an eq() filter, which requires that a matching document have a filter field with a value equal to a specified value.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      filter: (q) => q.eq("category", "museum"),
    });
    return result;
  },
});

The final type of filter condition allows you to specify ranges over the sortKey. We currently only support (optional) inclusive lower bounds and exclusive upper bounds.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      filter: (q) => q.gte("sortKey", 10).lt("sortKey", 30),
    });
    return result;
  },
});

Queries take in a limit, which bounds the maximum number of rows returned. If this limit is hit, the query will return a nextCursor for continuation. The query may also return a nextCursor with fewer than limit results if it runs out of its IO budget while executing.

In either case, you can continue the stream by passing nextCursor to the next call's cursor parameter.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const startCursor = undefined;
    const result = await geospatial.query(
      ctx,
      {
        shape: { type: "rectangle", rectangle },
        limit: 16,
      },
      startCursor,
    );
    if (result.nextCursor) {
      // Continue the query, starting from the first query's cursor.
      const nextResult = await geospatial.query(
        ctx,
        {
          shape: { type: "rectangle", rectangle },
          limit: 16,
        },
        result.nextCursor,
      );
      return [...result.results, ...nextResult.results];
    }
    return result.results; // { key, coordinates }[]
  },
});

Note: you typically pass the nextCursor in from a client that is paginating through results, to avoid loading too much data in a single query.

Example

See example/ for a full example with a Leaflet-based frontend.

Development

Install dependencies and fire up the example app to get started.

npm install
cd example
npm install
npm run dev

The component definition is in src/ and reflects what users of the component will install. The example app, which is entirely independent, lives in example/.