@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 +18 -0
- package/dist/query.d.ts +13 -1
- package/dist/query.js +15 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/sql/008_regions_bbox.sql +24 -0
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
|
@@ -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();
|