@nspot/geo-engine 0.2.1 → 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 +27 -0
- package/dist/packs.d.ts +19 -1
- package/dist/packs.js +16 -2
- 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/007_pack_scope.sql +199 -0
- package/sql/008_regions_bbox.sql +24 -0
package/README.md
CHANGED
|
@@ -67,6 +67,15 @@ path. Pass `{ resolveFrom }` to resolve the package name from a directory other
|
|
|
67
67
|
`process.cwd()`. A pack package must expose `pack.json`; if its `package.json` declares
|
|
68
68
|
an `exports` map, that map must include `"./pack.json"`.
|
|
69
69
|
|
|
70
|
+
A pack may build on another: a region's `parent` is a slug, found first among the same
|
|
71
|
+
pack's regions and then among any other installed pack's (never among your local regions).
|
|
72
|
+
Links are remade after every install, so packs can be installed in any order, and
|
|
73
|
+
reinstalling a parent pack restores the links its uninstall cut. `exportPack(pool, slug,
|
|
74
|
+
version, { owner, slugPrefix })` exports every region, or only one owner's (`"local"` for
|
|
75
|
+
the regions no pack owns, or a pack's slug) and/or those under a slug prefix; a scoped
|
|
76
|
+
export names only the countries it needs, with no outline, so installing it never
|
|
77
|
+
overwrites one. Both need schema 7 (`@nspot/geo-engine` 0.3.0).
|
|
78
|
+
|
|
70
79
|
**4. Filter your own query by region.**
|
|
71
80
|
|
|
72
81
|
```ts
|
|
@@ -111,6 +120,23 @@ from request input — use a literal, or pick from a fixed list in your own code
|
|
|
111
120
|
additionally checked against a plain-identifier pattern and rejected with
|
|
112
121
|
`GeoEngineError("invalid_input", …)` when it is anything else.
|
|
113
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
|
+
|
|
114
140
|
## CLI
|
|
115
141
|
|
|
116
142
|
The package ships a `geo-engine` bin (run it with `npx geo-engine …`, `pnpm dlx geo-engine …`,
|
|
@@ -146,6 +172,7 @@ database, and `cause` when it wraps something else (a zod `ZodError`, a fetch fa
|
|
|
146
172
|
| `invalid_input` | Postgres rejected a value (`22023`) — e.g. a malformed geometry or invalid enum value |
|
|
147
173
|
| `invalid_input` | `idType` is not a plain SQL type name (`inRegions` / `notInRegions`), raised before any SQL is built |
|
|
148
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 |
|
|
149
176
|
| `invalid_input` | `installPack` could not download, read, resolve or JSON-parse the pack (`cause` is the underlying error) |
|
|
150
177
|
| `invalid_input` | the pack document failed validation — `Invalid pack document: <path>: <issue>; …`, `cause` is the `ZodError` |
|
|
151
178
|
| `invalid_input` | `migrate` found a database whose schema is newer than the one this package ships |
|
package/dist/packs.d.ts
CHANGED
|
@@ -8,7 +8,25 @@ export interface InstallPackOptions {
|
|
|
8
8
|
/** Directory to resolve a bare package name from; defaults to process.cwd(). */
|
|
9
9
|
resolveFrom?: string;
|
|
10
10
|
}
|
|
11
|
-
export
|
|
11
|
+
export interface ExportPackOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whose regions to export: `"local"` for the regions no pack owns, or an installed pack's
|
|
14
|
+
* slug for that pack's regions. Omitted, every region is exported.
|
|
15
|
+
*/
|
|
16
|
+
owner?: string;
|
|
17
|
+
/** Export only regions whose slug starts with this. */
|
|
18
|
+
slugPrefix?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The database's regions as one pack document, named `slug`@`version`.
|
|
22
|
+
*
|
|
23
|
+
* Unscoped, it holds every region and every country with its outline. Scoped (`owner` or
|
|
24
|
+
* `slugPrefix`), it holds the chosen regions and only the countries they and their parents
|
|
25
|
+
* sit in, with no outline, so installing it never overwrites one. A region's `parent` is its
|
|
26
|
+
* parent's slug wherever that parent lives; a pack region whose parent is in another pack
|
|
27
|
+
* gets linked to it when both are installed (schema 7).
|
|
28
|
+
*/
|
|
29
|
+
export declare function exportPack(db: Queryable, slug: string, version: string, opts?: ExportPackOptions): Promise<Pack>;
|
|
12
30
|
/**
|
|
13
31
|
* Install a pack into the database.
|
|
14
32
|
*
|
package/dist/packs.js
CHANGED
|
@@ -6,8 +6,22 @@ import { GeoEngineError, run } from "./errors.js";
|
|
|
6
6
|
import { packSchema } from "./types.js";
|
|
7
7
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
8
|
const PACKAGE_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The database's regions as one pack document, named `slug`@`version`.
|
|
11
|
+
*
|
|
12
|
+
* Unscoped, it holds every region and every country with its outline. Scoped (`owner` or
|
|
13
|
+
* `slugPrefix`), it holds the chosen regions and only the countries they and their parents
|
|
14
|
+
* sit in, with no outline, so installing it never overwrites one. A region's `parent` is its
|
|
15
|
+
* parent's slug wherever that parent lives; a pack region whose parent is in another pack
|
|
16
|
+
* gets linked to it when both are installed (schema 7).
|
|
17
|
+
*/
|
|
18
|
+
export async function exportPack(db, slug, version, opts = {}) {
|
|
19
|
+
const rows = await run(db, "SELECT geo.export_pack($1, $2, $3, $4) AS pack", [
|
|
20
|
+
slug,
|
|
21
|
+
version,
|
|
22
|
+
opts.owner ?? null,
|
|
23
|
+
opts.slugPrefix ?? null,
|
|
24
|
+
]);
|
|
11
25
|
return rows[0].pack;
|
|
12
26
|
}
|
|
13
27
|
/**
|
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,199 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 007 — packs that build on other packs
|
|
3
|
+
--
|
|
4
|
+
-- A pack may name a parent that lives in another pack (a commune pack whose
|
|
5
|
+
-- communes sit in the Romania pack's județ). Every pack region keeps the parent
|
|
6
|
+
-- its pack declared, and the link is made to whichever pack region carries that
|
|
7
|
+
-- slug: the pack's own first (a slug is unique per country, so there is only
|
|
8
|
+
-- one), else another pack's. A local region is never adopted, since a pack
|
|
9
|
+
-- cannot know about it. Links are remade after every install, so the order in
|
|
10
|
+
-- which packs are installed does not matter, and reinstalling a parent pack
|
|
11
|
+
-- restores the links its uninstall cut (ON DELETE SET NULL).
|
|
12
|
+
--
|
|
13
|
+
-- export_pack gains a scope: the regions of one owner (a pack slug, or 'local'
|
|
14
|
+
-- for regions no pack owns) and/or a slug prefix. A scoped pack names only the
|
|
15
|
+
-- countries it needs, with no outline, so installing it never overwrites the
|
|
16
|
+
-- outline another pack or the host set.
|
|
17
|
+
-- ============================================================================
|
|
18
|
+
|
|
19
|
+
ALTER TABLE geo.regions
|
|
20
|
+
ADD COLUMN declared_parent text,
|
|
21
|
+
ADD COLUMN declared_parent_country text;
|
|
22
|
+
|
|
23
|
+
COMMENT ON COLUMN geo.regions.declared_parent IS
|
|
24
|
+
'For a pack region: the parent slug its pack declared, resolved by geo.link_pack_parents()';
|
|
25
|
+
COMMENT ON COLUMN geo.regions.declared_parent_country IS
|
|
26
|
+
'For a pack region: the country code of its declared parent';
|
|
27
|
+
|
|
28
|
+
-- Link every pack region to its declared parent among pack regions. Returns how
|
|
29
|
+
-- many links changed.
|
|
30
|
+
CREATE OR REPLACE FUNCTION geo.link_pack_parents() RETURNS int
|
|
31
|
+
LANGUAGE plpgsql AS $$
|
|
32
|
+
DECLARE
|
|
33
|
+
n int;
|
|
34
|
+
BEGIN
|
|
35
|
+
UPDATE geo.regions r
|
|
36
|
+
SET parent_id = par.id
|
|
37
|
+
FROM geo.countries pc
|
|
38
|
+
JOIN geo.regions par ON par.country_id = pc.id AND par.pack_id IS NOT NULL
|
|
39
|
+
WHERE r.pack_id IS NOT NULL
|
|
40
|
+
AND r.declared_parent IS NOT NULL
|
|
41
|
+
AND pc.code = r.declared_parent_country
|
|
42
|
+
AND par.slug = r.declared_parent
|
|
43
|
+
AND par.id <> r.id
|
|
44
|
+
AND r.parent_id IS DISTINCT FROM par.id;
|
|
45
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
46
|
+
RETURN n;
|
|
47
|
+
END $$;
|
|
48
|
+
|
|
49
|
+
DROP FUNCTION geo.export_pack(text, text);
|
|
50
|
+
CREATE OR REPLACE FUNCTION geo.export_pack(
|
|
51
|
+
p_slug text,
|
|
52
|
+
p_version text,
|
|
53
|
+
p_owner text DEFAULT NULL,
|
|
54
|
+
p_slug_prefix text DEFAULT NULL
|
|
55
|
+
) RETURNS jsonb
|
|
56
|
+
LANGUAGE plpgsql STABLE AS $$
|
|
57
|
+
DECLARE
|
|
58
|
+
owner_id int;
|
|
59
|
+
scoped boolean := p_owner IS NOT NULL OR p_slug_prefix IS NOT NULL;
|
|
60
|
+
BEGIN
|
|
61
|
+
IF p_owner IS NOT NULL AND p_owner <> 'local' THEN
|
|
62
|
+
SELECT id INTO owner_id FROM geo.packs WHERE slug = p_owner;
|
|
63
|
+
IF owner_id IS NULL THEN
|
|
64
|
+
RAISE EXCEPTION 'Unknown pack "%"', p_owner USING ERRCODE = 'invalid_parameter_value';
|
|
65
|
+
END IF;
|
|
66
|
+
END IF;
|
|
67
|
+
|
|
68
|
+
RETURN (
|
|
69
|
+
WITH sel AS (
|
|
70
|
+
SELECT r.*
|
|
71
|
+
FROM geo.regions r
|
|
72
|
+
WHERE (p_owner IS NULL
|
|
73
|
+
OR (p_owner = 'local' AND r.pack_id IS NULL)
|
|
74
|
+
OR r.pack_id = owner_id)
|
|
75
|
+
AND (p_slug_prefix IS NULL OR starts_with(r.slug, p_slug_prefix))
|
|
76
|
+
),
|
|
77
|
+
feats AS (
|
|
78
|
+
SELECT c.code AS country, r.slug, jsonb_build_object(
|
|
79
|
+
'type', 'Feature',
|
|
80
|
+
'geometry', ST_AsGeoJSON(r.geom, 6)::jsonb,
|
|
81
|
+
'properties', jsonb_build_object(
|
|
82
|
+
'country', c.code, 'slug', r.slug, 'name', r.name, 'type', r.type, 'level', r.level,
|
|
83
|
+
'parent', coalesce(p.slug, r.declared_parent),
|
|
84
|
+
'parent_country', CASE WHEN p.id IS NOT NULL THEN pc.code ELSE r.declared_parent_country END,
|
|
85
|
+
'code', r.code, 'tags', to_jsonb(r.tags), 'properties', r.properties)) AS feature,
|
|
86
|
+
coalesce(pc.code, r.declared_parent_country) AS parent_country
|
|
87
|
+
FROM sel r
|
|
88
|
+
JOIN geo.countries c ON c.id = r.country_id
|
|
89
|
+
LEFT JOIN geo.regions p ON p.id = r.parent_id
|
|
90
|
+
LEFT JOIN geo.countries pc ON pc.id = p.country_id
|
|
91
|
+
),
|
|
92
|
+
needed AS (
|
|
93
|
+
SELECT country AS code FROM feats
|
|
94
|
+
UNION
|
|
95
|
+
SELECT parent_country FROM feats WHERE parent_country IS NOT NULL
|
|
96
|
+
)
|
|
97
|
+
SELECT jsonb_build_object(
|
|
98
|
+
'pack', jsonb_build_object(
|
|
99
|
+
'slug', p_slug, 'version', p_version, 'schema_version', geo.schema_version(),
|
|
100
|
+
'published_at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')),
|
|
101
|
+
'countries', coalesce((
|
|
102
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
103
|
+
'code', c.code, 'name', c.name,
|
|
104
|
+
'geometry', CASE WHEN scoped OR c.geom IS NULL THEN NULL ELSE ST_AsGeoJSON(c.geom, 6)::jsonb END
|
|
105
|
+
) ORDER BY c.code)
|
|
106
|
+
FROM geo.countries c
|
|
107
|
+
WHERE NOT scoped OR c.code IN (SELECT code FROM needed)), '[]'::jsonb),
|
|
108
|
+
'regions', jsonb_build_object(
|
|
109
|
+
'type', 'FeatureCollection',
|
|
110
|
+
'features', coalesce((SELECT jsonb_agg(feature ORDER BY country, slug) FROM feats), '[]'::jsonb)))
|
|
111
|
+
);
|
|
112
|
+
END $$;
|
|
113
|
+
|
|
114
|
+
-- install_pack as in 005, except that it records each region's declared parent
|
|
115
|
+
-- and links parents across packs.
|
|
116
|
+
CREATE OR REPLACE FUNCTION geo.install_pack(p_pack jsonb) RETURNS jsonb
|
|
117
|
+
LANGUAGE plpgsql AS $$
|
|
118
|
+
DECLARE
|
|
119
|
+
hdr jsonb := p_pack->'pack';
|
|
120
|
+
feats jsonb := coalesce(p_pack->'regions'->'features', '[]'::jsonb);
|
|
121
|
+
pid int;
|
|
122
|
+
f jsonb;
|
|
123
|
+
n_countries int := 0;
|
|
124
|
+
n_regions int := 0;
|
|
125
|
+
n_members bigint;
|
|
126
|
+
code text;
|
|
127
|
+
BEGIN
|
|
128
|
+
IF hdr IS NULL OR coalesce(hdr->>'slug', '') = '' OR coalesce(hdr->>'version', '') = '' THEN
|
|
129
|
+
RAISE EXCEPTION 'Pack header must have slug and version' USING ERRCODE = 'invalid_parameter_value';
|
|
130
|
+
END IF;
|
|
131
|
+
IF coalesce((hdr->>'schema_version')::int, 0) > geo.schema_version() THEN
|
|
132
|
+
RAISE EXCEPTION 'Pack "%" needs schema version % but this database has %',
|
|
133
|
+
hdr->>'slug', hdr->>'schema_version', geo.schema_version()
|
|
134
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
135
|
+
END IF;
|
|
136
|
+
|
|
137
|
+
INSERT INTO geo.packs (slug, version, schema_version)
|
|
138
|
+
VALUES (hdr->>'slug', hdr->>'version', coalesce((hdr->>'schema_version')::int, geo.schema_version()))
|
|
139
|
+
ON CONFLICT (slug) DO UPDATE
|
|
140
|
+
SET version = EXCLUDED.version, schema_version = EXCLUDED.schema_version, installed_at = now()
|
|
141
|
+
RETURNING id INTO pid;
|
|
142
|
+
|
|
143
|
+
PERFORM set_config('geo.skip_membership_triggers', 'on', true);
|
|
144
|
+
|
|
145
|
+
FOR f IN SELECT * FROM jsonb_array_elements(coalesce(p_pack->'countries', '[]'::jsonb)) LOOP
|
|
146
|
+
PERFORM geo.upsert_country(f->>'code', f->>'name',
|
|
147
|
+
CASE WHEN jsonb_typeof(f->'geometry') = 'object' THEN f->'geometry' END);
|
|
148
|
+
n_countries := n_countries + 1;
|
|
149
|
+
END LOOP;
|
|
150
|
+
|
|
151
|
+
FOR f IN SELECT * FROM jsonb_array_elements(feats) LOOP
|
|
152
|
+
PERFORM geo.upsert_region(
|
|
153
|
+
f->'properties'->>'country',
|
|
154
|
+
f->'properties'->>'slug',
|
|
155
|
+
f->'properties'->>'name',
|
|
156
|
+
(f->'properties'->>'type')::geo.region_type,
|
|
157
|
+
(f->'properties'->>'level')::int,
|
|
158
|
+
NULL,
|
|
159
|
+
f->'properties'->>'code',
|
|
160
|
+
ARRAY(SELECT jsonb_array_elements_text(coalesce(f->'properties'->'tags', '[]'::jsonb))),
|
|
161
|
+
coalesce(f->'properties'->'properties', '{}'::jsonb),
|
|
162
|
+
f->'geometry',
|
|
163
|
+
true,
|
|
164
|
+
pid);
|
|
165
|
+
n_regions := n_regions + 1;
|
|
166
|
+
END LOOP;
|
|
167
|
+
|
|
168
|
+
-- regions that left the pack
|
|
169
|
+
DELETE FROM geo.regions r
|
|
170
|
+
USING geo.countries c
|
|
171
|
+
WHERE r.pack_id = pid AND c.id = r.country_id
|
|
172
|
+
AND NOT EXISTS (
|
|
173
|
+
SELECT 1 FROM jsonb_array_elements(feats) ff
|
|
174
|
+
WHERE upper(ff->'properties'->>'country') = c.code AND ff->'properties'->>'slug' = r.slug);
|
|
175
|
+
|
|
176
|
+
-- the parent each region declares (a slug, in its parent_country or its own)
|
|
177
|
+
UPDATE geo.regions r
|
|
178
|
+
SET declared_parent = nullif(ff->'properties'->>'parent', ''),
|
|
179
|
+
declared_parent_country = CASE WHEN nullif(ff->'properties'->>'parent', '') IS NULL THEN NULL
|
|
180
|
+
ELSE upper(coalesce(ff->'properties'->>'parent_country', ff->'properties'->>'country')) END
|
|
181
|
+
FROM jsonb_array_elements(feats) ff
|
|
182
|
+
JOIN geo.countries c ON c.code = upper(ff->'properties'->>'country')
|
|
183
|
+
WHERE r.pack_id = pid AND r.country_id = c.id AND r.slug = ff->'properties'->>'slug';
|
|
184
|
+
|
|
185
|
+
-- parents among pack regions, this pack's and every other's
|
|
186
|
+
PERFORM geo.link_pack_parents();
|
|
187
|
+
|
|
188
|
+
FOR code IN SELECT c.code FROM geo.countries c WHERE c.geom IS NULL LOOP
|
|
189
|
+
PERFORM geo.derive_country_geom(code);
|
|
190
|
+
END LOOP;
|
|
191
|
+
|
|
192
|
+
n_members := geo.rebuild_all_memberships();
|
|
193
|
+
PERFORM set_config('geo.skip_membership_triggers', 'off', true);
|
|
194
|
+
RETURN jsonb_build_object(
|
|
195
|
+
'pack', hdr->>'slug', 'version', hdr->>'version',
|
|
196
|
+
'countries', n_countries, 'regions', n_regions, 'memberships', n_members);
|
|
197
|
+
END $$;
|
|
198
|
+
|
|
199
|
+
SELECT geo.pin_search_path();
|
|
@@ -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();
|