@nspot/geo-engine 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,6 +120,23 @@ from request input — use a literal, or pick from a fixed list in your own code
120
120
  additionally checked against a plain-identifier pattern and rejected with
121
121
  `GeoEngineError("invalid_input", …)` when it is anything else.
122
122
 
123
+ **6. Fit a map to regions.**
124
+
125
+ ```ts
126
+ import { regionsBbox } from "@nspot/geo-engine";
127
+
128
+ const bbox = await regionsBbox(pool, ["bran-bv", "moieciu-bv"], { country: "RO" });
129
+ // [west, south, east, north], or null when no slug names a region
130
+ ```
131
+
132
+ `regionsBbox` gives the box of the union of the named regions, exact to the coordinate, ready
133
+ for a map's `fitBounds`. Unknown slugs among known ones are ignored. Slugs are unique per
134
+ country, not globally, so without `country` a slug two countries share gives the box of both.
135
+ An empty list, or a string in place of the list, is refused with
136
+ `GeoEngineError("invalid_input", …)` before the database is reached. The `Bbox` type also
137
+ comes from `@nspot/geo-engine/types`, which is safe to import in a browser bundle.
138
+ `regionsBbox` needs schema 8 (`@nspot/geo-engine` 0.4.0).
139
+
123
140
  ## CLI
124
141
 
125
142
  The package ships a `geo-engine` bin (run it with `npx geo-engine …`, `pnpm dlx geo-engine …`,
@@ -155,6 +172,7 @@ database, and `cause` when it wraps something else (a zod `ZodError`, a fetch fa
155
172
  | `invalid_input` | Postgres rejected a value (`22023`) — e.g. a malformed geometry or invalid enum value |
156
173
  | `invalid_input` | `idType` is not a plain SQL type name (`inRegions` / `notInRegions`), raised before any SQL is built |
157
174
  | `invalid_input` | `filter.regions` is empty in `inRegions` / `ids`, raised before the database is touched |
175
+ | `invalid_input` | `slugs` is empty, or not an array, in `regionsBbox`, raised before the database is touched |
158
176
  | `invalid_input` | `installPack` could not download, read, resolve or JSON-parse the pack (`cause` is the underlying error) |
159
177
  | `invalid_input` | the pack document failed validation — `Invalid pack document: <path>: <issue>; …`, `cause` is the `ZodError` |
160
178
  | `invalid_input` | `migrate` found a database whose schema is newer than the one this package ships |
package/dist/query.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Queryable } from "./migrate.js";
2
- import type { RegionSummary } from "./types.js";
2
+ import type { Bbox, RegionSummary } from "./types.js";
3
3
  export interface RegionFilter {
4
4
  /** Region slugs or numeric ids; at least one. */
5
5
  regions: string[];
@@ -41,3 +41,15 @@ export declare function notInRegions(source: string, regions: string[], opts: Fr
41
41
  export declare function ids(db: Queryable, source: string, filter: RegionFilter): Promise<string[]>;
42
42
  export declare function regionsOf(db: Queryable, source: string, externalId: string | number): Promise<RegionSummary[]>;
43
43
  export declare function regionsAt(db: Queryable, lng: number, lat: number): Promise<RegionSummary[]>;
44
+ export interface RegionsBboxOptions {
45
+ /** A country's ISO code, in either case: match the slugs in that country only. */
46
+ country?: string;
47
+ }
48
+ /**
49
+ * The box holding the named regions, for a map that fits its camera to them: [west, south,
50
+ * east, north] of the union of their envelopes, exact to the coordinate, or null when no slug
51
+ * names a region. Unknown slugs among known ones are ignored. Slugs are unique per country,
52
+ * not globally, so without `country` a slug two countries share gives the box of both.
53
+ * Needs schema 8 (`geo.regions_bbox`).
54
+ */
55
+ export declare function regionsBbox(db: Queryable, slugs: readonly string[], opts?: RegionsBboxOptions): Promise<Bbox | null>;
package/dist/query.js CHANGED
@@ -50,3 +50,18 @@ export async function regionsOf(db, source, externalId) {
50
50
  export async function regionsAt(db, lng, lat) {
51
51
  return run(db, "SELECT id, slug, name, type, level, parent_id, code FROM geo.regions_at_point($1, $2)", [lng, lat]);
52
52
  }
53
+ /**
54
+ * The box holding the named regions, for a map that fits its camera to them: [west, south,
55
+ * east, north] of the union of their envelopes, exact to the coordinate, or null when no slug
56
+ * names a region. Unknown slugs among known ones are ignored. Slugs are unique per country,
57
+ * not globally, so without `country` a slug two countries share gives the box of both.
58
+ * Needs schema 8 (`geo.regions_bbox`).
59
+ */
60
+ export async function regionsBbox(db, slugs, opts = {}) {
61
+ // a string is not a list: iterated, it would name one-letter slugs and answer null
62
+ if (!Array.isArray(slugs) || slugs.length === 0) {
63
+ throw new GeoEngineError("invalid_input", "slugs must name at least one region");
64
+ }
65
+ const rows = await run(db, "SELECT geo.regions_bbox($1::text[], $2::text) AS b", [slugs, opts.country ?? null]);
66
+ return rows[0].b;
67
+ }
package/dist/types.d.ts CHANGED
@@ -22,9 +22,12 @@ export type MultiPolygonGeometry = {
22
22
  coordinates: Position[][][];
23
23
  };
24
24
  export type AreaGeometry = PolygonGeometry | MultiPolygonGeometry;
25
+ /** [west, south, east, north] in degrees: a GeoJSON `bbox` member, and what a map's fitBounds takes. */
26
+ export type Bbox = [west: number, south: number, east: number, north: number];
25
27
  export type Feature<G = AreaGeometry, P = Record<string, unknown>> = {
26
28
  type: "Feature";
27
29
  id?: string | number;
30
+ bbox?: Bbox;
28
31
  geometry: G;
29
32
  properties: P;
30
33
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nspot/geo-engine",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Region membership for your own PostGIS tables: install the geo schema, register tables, install region packs, query by region.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,24 @@
1
+ -- ============================================================================
2
+ -- 008 — region bounding boxes
3
+ --
4
+ -- geo.regions_bbox(slugs, country) answers "what box holds these regions?" for a
5
+ -- map that fits its camera to them: [west, south, east, north] of the union of
6
+ -- the named regions' envelopes, as jsonb, exact to the coordinate, or NULL when
7
+ -- no slug names a region. Unknown slugs among known ones are ignored. Slugs are
8
+ -- unique per country, not globally: without p_country a slug two countries share
9
+ -- gives the box of both; with it (an ISO code, either case) that country's only.
10
+ -- ============================================================================
11
+
12
+ CREATE OR REPLACE FUNCTION geo.regions_bbox(p_slugs text[], p_country text DEFAULT NULL)
13
+ RETURNS jsonb
14
+ LANGUAGE sql STABLE PARALLEL SAFE AS $$
15
+ SELECT CASE WHEN count(*) = 0 THEN NULL
16
+ ELSE jsonb_build_array(min(ST_XMin(r.geom)), min(ST_YMin(r.geom)), max(ST_XMax(r.geom)), max(ST_YMax(r.geom)))
17
+ END
18
+ FROM geo.regions r
19
+ JOIN geo.countries c ON c.id = r.country_id
20
+ WHERE r.slug = ANY (p_slugs)
21
+ AND (p_country IS NULL OR c.code = upper(p_country));
22
+ $$;
23
+
24
+ SELECT geo.pin_search_path();