@nspot/geo-engine 0.4.0 → 0.5.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 +56 -1
- package/dist/cli.js +2 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/localities.d.ts +71 -0
- package/dist/localities.js +87 -0
- package/dist/packs.d.ts +14 -2
- package/dist/packs.js +10 -3
- package/dist/types.d.ts +503 -26
- package/dist/types.js +52 -1
- package/package.json +1 -1
- package/sql/009_localities.sql +665 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 009 — localities: named points beside regions
|
|
3
|
+
--
|
|
4
|
+
-- A locality is a place with a name and a code but no boundary: Romania's
|
|
5
|
+
-- villages and the component localities of its towns. It has a label point
|
|
6
|
+
-- (where its name goes on a map) or, when its source has none, a reason in
|
|
7
|
+
-- point_gap. Its identity is (country, code), the target of host foreign keys,
|
|
8
|
+
-- so a host's rows can name a locality the same way in every database.
|
|
9
|
+
-- Localities have no membership: a host row declares its locality.
|
|
10
|
+
-- ============================================================================
|
|
11
|
+
|
|
12
|
+
-- Lower case ASCII for search: the Latin diacritics of the region folded, in
|
|
13
|
+
-- both cases (lower() leaves them alone under lc_ctype C), ß as ss, runs of
|
|
14
|
+
-- whitespace and hyphens as one space, trimmed.
|
|
15
|
+
CREATE OR REPLACE FUNCTION geo.fold(p text) RETURNS text
|
|
16
|
+
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT AS $$
|
|
17
|
+
SELECT btrim(regexp_replace(lower(translate(replace(replace(p, 'ß', 'ss'), 'ẞ', 'ss'),
|
|
18
|
+
'șşțţăâîáàäãåéèëêíìïóòöőôõúùüűûýÿçčćñňšśžźżłđďťř'
|
|
19
|
+
|| 'ȘŞȚŢĂÂÎÁÀÄÃÅÉÈËÊÍÌÏÓÒÖŐÔÕÚÙÜŰÛÝŸÇČĆÑŇŠŚŽŹŻŁĐĎŤŘ',
|
|
20
|
+
'ssttaaiaaaaaeeeeiiioooooouuuuuyycccnnsszzzlddtr'
|
|
21
|
+
|| 'ssttaaiaaaaaeeeeiiioooooouuuuuyycccnnsszzzlddtr')), '[\s-]+', ' ', 'g'))
|
|
22
|
+
$$;
|
|
23
|
+
|
|
24
|
+
CREATE TABLE geo.localities (
|
|
25
|
+
id serial PRIMARY KEY,
|
|
26
|
+
country char(2) NOT NULL REFERENCES geo.countries(code) ON DELETE CASCADE,
|
|
27
|
+
code text NOT NULL,
|
|
28
|
+
name text NOT NULL,
|
|
29
|
+
name_key text GENERATED ALWAYS AS (geo.fold(name)) STORED,
|
|
30
|
+
parent_id int REFERENCES geo.regions(id) ON DELETE SET NULL,
|
|
31
|
+
declared_parent text,
|
|
32
|
+
declared_parent_country char(2),
|
|
33
|
+
point geometry(Point, 4326),
|
|
34
|
+
point_gap text,
|
|
35
|
+
tags text[] NOT NULL DEFAULT '{}',
|
|
36
|
+
properties jsonb NOT NULL DEFAULT '{}',
|
|
37
|
+
pack_id int REFERENCES geo.packs(id) ON DELETE SET NULL,
|
|
38
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
39
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
40
|
+
UNIQUE (country, code),
|
|
41
|
+
CHECK (code <> '' AND name <> ''),
|
|
42
|
+
CHECK ((point IS NULL) = (point_gap IS NOT NULL))
|
|
43
|
+
);
|
|
44
|
+
CREATE INDEX localities_point_gix ON geo.localities USING GIST (point);
|
|
45
|
+
CREATE INDEX localities_point_geog_gix ON geo.localities USING GIST ((point::geography));
|
|
46
|
+
CREATE INDEX localities_parent_idx ON geo.localities (parent_id);
|
|
47
|
+
CREATE INDEX localities_pack_idx ON geo.localities (pack_id);
|
|
48
|
+
CREATE TRIGGER localities_updated_at_trg BEFORE UPDATE ON geo.localities
|
|
49
|
+
FOR EACH ROW EXECUTE FUNCTION geo.set_updated_at();
|
|
50
|
+
|
|
51
|
+
COMMENT ON TABLE geo.localities IS
|
|
52
|
+
'Named points: a code, a name, a parent region, a label point or a gap reason. Hosts reference (country, code).';
|
|
53
|
+
|
|
54
|
+
-- A GeoJSON Point as a 4326 point, NULL for NULL; p_where names the locality in errors.
|
|
55
|
+
CREATE OR REPLACE FUNCTION geo.locality_point(p_geojson jsonb, p_where text) RETURNS geometry
|
|
56
|
+
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
57
|
+
DECLARE
|
|
58
|
+
g geometry;
|
|
59
|
+
BEGIN
|
|
60
|
+
IF p_geojson IS NULL OR jsonb_typeof(p_geojson) = 'null' THEN
|
|
61
|
+
RETURN NULL;
|
|
62
|
+
END IF;
|
|
63
|
+
IF jsonb_typeof(p_geojson) <> 'object' OR p_geojson->>'type' IS DISTINCT FROM 'Point' THEN
|
|
64
|
+
RAISE EXCEPTION '%: a locality''s point must be a GeoJSON Point or null', p_where
|
|
65
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
66
|
+
END IF;
|
|
67
|
+
IF jsonb_typeof(p_geojson->'coordinates') IS DISTINCT FROM 'array'
|
|
68
|
+
OR jsonb_array_length(p_geojson->'coordinates') NOT IN (2, 3)
|
|
69
|
+
OR EXISTS (SELECT 1 FROM jsonb_array_elements(p_geojson->'coordinates') c
|
|
70
|
+
WHERE jsonb_typeof(c) <> 'number') THEN
|
|
71
|
+
RAISE EXCEPTION '%: a Point''s coordinates must be [lng, lat], got %', p_where, p_geojson->'coordinates'
|
|
72
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
73
|
+
END IF;
|
|
74
|
+
g := ST_SetSRID(ST_GeomFromGeoJSON(p_geojson), 4326);
|
|
75
|
+
IF ST_X(g) NOT BETWEEN -180 AND 180 OR ST_Y(g) NOT BETWEEN -90 AND 90 THEN
|
|
76
|
+
RAISE EXCEPTION '%: point [%, %] is outside -180..180, -90..90', p_where, ST_X(g), ST_Y(g)
|
|
77
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
78
|
+
END IF;
|
|
79
|
+
RETURN ST_Force2D(g);
|
|
80
|
+
END $$;
|
|
81
|
+
|
|
82
|
+
-- What every read returns. distance_m, rounded to 0.1 m, is added last when given.
|
|
83
|
+
CREATE OR REPLACE FUNCTION geo.locality_summary(l geo.localities, p_distance_m float8 DEFAULT NULL) RETURNS json
|
|
84
|
+
LANGUAGE sql STABLE AS $$
|
|
85
|
+
-- Both branches build the same object and must stay in step; the second only
|
|
86
|
+
-- appends distance_m last, which one json_build_object cannot leave out.
|
|
87
|
+
SELECT CASE WHEN p_distance_m IS NULL THEN
|
|
88
|
+
json_build_object(
|
|
89
|
+
'country', l.country, 'code', l.code, 'name', l.name,
|
|
90
|
+
'point', CASE WHEN l.point IS NULL THEN NULL ELSE json_build_array(ST_X(l.point), ST_Y(l.point)) END,
|
|
91
|
+
'point_gap', l.point_gap,
|
|
92
|
+
'parent', (SELECT json_build_object('slug', r.slug, 'name', r.name) FROM geo.regions r WHERE r.id = l.parent_id),
|
|
93
|
+
'tags', to_json(l.tags), 'properties', l.properties,
|
|
94
|
+
'pack', (SELECT p.slug FROM geo.packs p WHERE p.id = l.pack_id))
|
|
95
|
+
ELSE
|
|
96
|
+
json_build_object(
|
|
97
|
+
'country', l.country, 'code', l.code, 'name', l.name,
|
|
98
|
+
'point', CASE WHEN l.point IS NULL THEN NULL ELSE json_build_array(ST_X(l.point), ST_Y(l.point)) END,
|
|
99
|
+
'point_gap', l.point_gap,
|
|
100
|
+
'parent', (SELECT json_build_object('slug', r.slug, 'name', r.name) FROM geo.regions r WHERE r.id = l.parent_id),
|
|
101
|
+
'tags', to_json(l.tags), 'properties', l.properties,
|
|
102
|
+
'pack', (SELECT p.slug FROM geo.packs p WHERE p.id = l.pack_id),
|
|
103
|
+
'distance_m', round(p_distance_m::numeric, 1))
|
|
104
|
+
END
|
|
105
|
+
$$;
|
|
106
|
+
|
|
107
|
+
-- Link every pack locality to the pack region its pack declared, in any pack,
|
|
108
|
+
-- never a local region (as geo.link_pack_parents does for regions). Returns how
|
|
109
|
+
-- many links changed.
|
|
110
|
+
CREATE OR REPLACE FUNCTION geo.link_pack_locality_parents() RETURNS int
|
|
111
|
+
LANGUAGE sql AS $$
|
|
112
|
+
WITH target AS (
|
|
113
|
+
SELECT l.id, (
|
|
114
|
+
SELECT r.id FROM geo.regions r JOIN geo.countries c ON c.id = r.country_id
|
|
115
|
+
WHERE r.pack_id IS NOT NULL AND r.slug = l.declared_parent AND c.code = l.declared_parent_country) AS parent
|
|
116
|
+
FROM geo.localities l
|
|
117
|
+
WHERE l.pack_id IS NOT NULL
|
|
118
|
+
), changed AS (
|
|
119
|
+
UPDATE geo.localities l SET parent_id = t.parent
|
|
120
|
+
FROM target t
|
|
121
|
+
WHERE l.id = t.id AND l.parent_id IS DISTINCT FROM t.parent
|
|
122
|
+
RETURNING 1
|
|
123
|
+
)
|
|
124
|
+
SELECT count(*)::int FROM changed
|
|
125
|
+
$$;
|
|
126
|
+
|
|
127
|
+
-- "XA 1, XA 2, … and 5 more": the first 20 of a list of codes.
|
|
128
|
+
CREATE OR REPLACE FUNCTION geo.locality_list(p_codes text[]) RETURNS text
|
|
129
|
+
LANGUAGE sql IMMUTABLE AS $$
|
|
130
|
+
SELECT array_to_string(p_codes[1:20], ', ')
|
|
131
|
+
|| CASE WHEN cardinality(p_codes) > 20 THEN format(' and %s more', cardinality(p_codes) - 20) ELSE '' END
|
|
132
|
+
$$;
|
|
133
|
+
|
|
134
|
+
-- Delete the given pack's localities that p_keep does not name (NULL keeps none).
|
|
135
|
+
-- A host foreign key that still references one aborts with 23503, naming the
|
|
136
|
+
-- pack (p_label) and the localities leaving, Postgres's own DETAIL kept. Set-based
|
|
137
|
+
-- (an anti-join and a join on ids), since a pack may hold 13,749 of them.
|
|
138
|
+
CREATE OR REPLACE FUNCTION geo.delete_pack_localities(p_pack_id int, p_keep jsonb, p_label text) RETURNS int
|
|
139
|
+
LANGUAGE plpgsql AS $$
|
|
140
|
+
DECLARE
|
|
141
|
+
ids int[];
|
|
142
|
+
leaving text[];
|
|
143
|
+
n int;
|
|
144
|
+
d text;
|
|
145
|
+
BEGIN
|
|
146
|
+
SELECT array_agg(l.id), array_agg(l.country || ' ' || l.code ORDER BY l.country, l.code) INTO ids, leaving
|
|
147
|
+
FROM geo.localities l
|
|
148
|
+
LEFT JOIN (
|
|
149
|
+
SELECT upper(f->'properties'->>'country') AS country, f->'properties'->>'code' AS code
|
|
150
|
+
FROM jsonb_array_elements(coalesce(p_keep, '[]'::jsonb)) f
|
|
151
|
+
) k ON k.country = l.country::text AND k.code = l.code
|
|
152
|
+
WHERE l.pack_id = p_pack_id AND k.code IS NULL;
|
|
153
|
+
IF ids IS NULL THEN
|
|
154
|
+
RETURN 0;
|
|
155
|
+
END IF;
|
|
156
|
+
BEGIN
|
|
157
|
+
DELETE FROM geo.localities l USING unnest(ids) AS gone(id) WHERE l.id = gone.id;
|
|
158
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
159
|
+
EXCEPTION WHEN foreign_key_violation THEN
|
|
160
|
+
GET STACKED DIAGNOSTICS d = PG_EXCEPTION_DETAIL;
|
|
161
|
+
RAISE EXCEPTION '% would remove localities a host still references; leaving: %', p_label, geo.locality_list(leaving)
|
|
162
|
+
USING ERRCODE = 'foreign_key_violation', DETAIL = d;
|
|
163
|
+
END;
|
|
164
|
+
RETURN n;
|
|
165
|
+
END $$;
|
|
166
|
+
|
|
167
|
+
-- The localities section of a pack, after its regions: check every feature,
|
|
168
|
+
-- refuse codes another owner holds, upsert, delete the ones that left, record
|
|
169
|
+
-- declared parents. Returns how many the pack has.
|
|
170
|
+
CREATE OR REPLACE FUNCTION geo.install_pack_localities(p_pack_id int, p_feats jsonb, p_label text) RETURNS int
|
|
171
|
+
LANGUAGE plpgsql AS $$
|
|
172
|
+
DECLARE
|
|
173
|
+
v jsonb;
|
|
174
|
+
i int := 0;
|
|
175
|
+
where_ text;
|
|
176
|
+
held text[];
|
|
177
|
+
written int;
|
|
178
|
+
BEGIN
|
|
179
|
+
FOR v IN SELECT * FROM jsonb_array_elements(p_feats) LOOP
|
|
180
|
+
where_ := format('locality %s (%s %s)', i, upper(v->'properties'->>'country'), v->'properties'->>'code');
|
|
181
|
+
IF coalesce(v->'properties'->>'country', '') = '' OR coalesce(v->'properties'->>'code', '') = ''
|
|
182
|
+
OR coalesce(v->'properties'->>'name', '') = '' THEN
|
|
183
|
+
RAISE EXCEPTION 'locality %: country, code and name are required', i USING ERRCODE = 'invalid_parameter_value';
|
|
184
|
+
END IF;
|
|
185
|
+
IF NOT EXISTS (SELECT 1 FROM geo.countries c WHERE c.code = upper(v->'properties'->>'country')) THEN
|
|
186
|
+
RAISE EXCEPTION '%: unknown country %', where_, upper(v->'properties'->>'country')
|
|
187
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
188
|
+
END IF;
|
|
189
|
+
IF (coalesce(jsonb_typeof(v->'geometry'), 'null') = 'object') = (coalesce(v->'properties'->>'point_gap', '') <> '') THEN
|
|
190
|
+
RAISE EXCEPTION '%: give a point or a point_gap, not both and not neither', where_
|
|
191
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
192
|
+
END IF;
|
|
193
|
+
PERFORM geo.locality_point(v->'geometry', where_);
|
|
194
|
+
i := i + 1;
|
|
195
|
+
END LOOP;
|
|
196
|
+
|
|
197
|
+
-- a code twice in one country: the second occurrence, set-based
|
|
198
|
+
SELECT format('locality %s (%s %s): code given twice', g.n - 1, g.country, g.code) INTO where_
|
|
199
|
+
FROM (
|
|
200
|
+
SELECT t.n, upper(t.feat->'properties'->>'country') AS country, t.feat->'properties'->>'code' AS code,
|
|
201
|
+
row_number() OVER (PARTITION BY upper(t.feat->'properties'->>'country'), t.feat->'properties'->>'code'
|
|
202
|
+
ORDER BY t.n) AS k
|
|
203
|
+
FROM jsonb_array_elements(p_feats) WITH ORDINALITY AS t(feat, n)
|
|
204
|
+
) g
|
|
205
|
+
WHERE g.k = 2
|
|
206
|
+
ORDER BY g.n
|
|
207
|
+
LIMIT 1;
|
|
208
|
+
IF FOUND THEN
|
|
209
|
+
RAISE EXCEPTION '%', where_ USING ERRCODE = 'invalid_parameter_value';
|
|
210
|
+
END IF;
|
|
211
|
+
|
|
212
|
+
SELECT array_agg(l.country || ' ' || l.code ORDER BY l.country, l.code) INTO held
|
|
213
|
+
FROM jsonb_array_elements(p_feats) f
|
|
214
|
+
JOIN geo.localities l ON l.country = upper(f->'properties'->>'country') AND l.code = f->'properties'->>'code'
|
|
215
|
+
WHERE l.pack_id IS DISTINCT FROM p_pack_id;
|
|
216
|
+
IF held IS NOT NULL THEN
|
|
217
|
+
RAISE EXCEPTION '% has localities another owner holds: %', p_label, geo.locality_list(held)
|
|
218
|
+
USING ERRCODE = 'unique_violation';
|
|
219
|
+
END IF;
|
|
220
|
+
|
|
221
|
+
INSERT INTO geo.localities AS l
|
|
222
|
+
(country, code, name, declared_parent, declared_parent_country, point, point_gap, tags, properties, pack_id)
|
|
223
|
+
SELECT upper(f->'properties'->>'country'), f->'properties'->>'code', f->'properties'->>'name',
|
|
224
|
+
nullif(f->'properties'->>'parent', ''),
|
|
225
|
+
CASE WHEN nullif(f->'properties'->>'parent', '') IS NULL THEN NULL
|
|
226
|
+
ELSE upper(coalesce(f->'properties'->>'parent_country', f->'properties'->>'country')) END,
|
|
227
|
+
geo.locality_point(f->'geometry', 'locality'),
|
|
228
|
+
nullif(f->'properties'->>'point_gap', ''),
|
|
229
|
+
ARRAY(SELECT jsonb_array_elements_text(coalesce(f->'properties'->'tags', '[]'::jsonb))),
|
|
230
|
+
coalesce(f->'properties'->'properties', '{}'::jsonb),
|
|
231
|
+
p_pack_id
|
|
232
|
+
FROM jsonb_array_elements(p_feats) f
|
|
233
|
+
ON CONFLICT (country, code) DO UPDATE SET
|
|
234
|
+
name = EXCLUDED.name, declared_parent = EXCLUDED.declared_parent,
|
|
235
|
+
declared_parent_country = EXCLUDED.declared_parent_country, point = EXCLUDED.point,
|
|
236
|
+
point_gap = EXCLUDED.point_gap, tags = EXCLUDED.tags, properties = EXCLUDED.properties
|
|
237
|
+
WHERE l.pack_id = p_pack_id;
|
|
238
|
+
-- a code another owner took since the check above is skipped by the WHERE: refuse it
|
|
239
|
+
GET DIAGNOSTICS written = ROW_COUNT;
|
|
240
|
+
IF written < i THEN
|
|
241
|
+
RAISE EXCEPTION '% has localities another owner holds', p_label USING ERRCODE = 'unique_violation';
|
|
242
|
+
END IF;
|
|
243
|
+
|
|
244
|
+
PERFORM geo.delete_pack_localities(p_pack_id, p_feats, p_label);
|
|
245
|
+
RETURN i;
|
|
246
|
+
END $$;
|
|
247
|
+
|
|
248
|
+
CREATE OR REPLACE FUNCTION geo.install_pack(p_pack jsonb) RETURNS jsonb
|
|
249
|
+
LANGUAGE plpgsql AS $$
|
|
250
|
+
DECLARE
|
|
251
|
+
hdr jsonb := p_pack->'pack';
|
|
252
|
+
feats jsonb := coalesce(p_pack->'regions'->'features', '[]'::jsonb);
|
|
253
|
+
pid int;
|
|
254
|
+
f jsonb;
|
|
255
|
+
n_countries int := 0;
|
|
256
|
+
n_regions int := 0;
|
|
257
|
+
n_localities int;
|
|
258
|
+
n_members bigint;
|
|
259
|
+
code text;
|
|
260
|
+
BEGIN
|
|
261
|
+
IF hdr IS NULL OR coalesce(hdr->>'slug', '') = '' OR coalesce(hdr->>'version', '') = '' THEN
|
|
262
|
+
RAISE EXCEPTION 'Pack header must have slug and version' USING ERRCODE = 'invalid_parameter_value';
|
|
263
|
+
END IF;
|
|
264
|
+
IF coalesce((hdr->>'schema_version')::int, 0) > geo.schema_version() THEN
|
|
265
|
+
RAISE EXCEPTION 'Pack "%" needs schema version % but this database has %',
|
|
266
|
+
hdr->>'slug', hdr->>'schema_version', geo.schema_version()
|
|
267
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
268
|
+
END IF;
|
|
269
|
+
-- no key leaves the pack's localities alone; a key must be a usable section,
|
|
270
|
+
-- since an empty one removes them all
|
|
271
|
+
IF p_pack ? 'localities' AND (jsonb_typeof(p_pack->'localities') IS DISTINCT FROM 'object'
|
|
272
|
+
OR jsonb_typeof(p_pack->'localities'->'features') IS DISTINCT FROM 'array') THEN
|
|
273
|
+
RAISE EXCEPTION 'localities must be a FeatureCollection with a features array'
|
|
274
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
275
|
+
END IF;
|
|
276
|
+
|
|
277
|
+
INSERT INTO geo.packs (slug, version, schema_version)
|
|
278
|
+
VALUES (hdr->>'slug', hdr->>'version', coalesce((hdr->>'schema_version')::int, geo.schema_version()))
|
|
279
|
+
ON CONFLICT (slug) DO UPDATE
|
|
280
|
+
SET version = EXCLUDED.version, schema_version = EXCLUDED.schema_version, installed_at = now()
|
|
281
|
+
RETURNING id INTO pid;
|
|
282
|
+
|
|
283
|
+
PERFORM set_config('geo.skip_membership_triggers', 'on', true);
|
|
284
|
+
|
|
285
|
+
FOR f IN SELECT * FROM jsonb_array_elements(coalesce(p_pack->'countries', '[]'::jsonb)) LOOP
|
|
286
|
+
PERFORM geo.upsert_country(f->>'code', f->>'name',
|
|
287
|
+
CASE WHEN jsonb_typeof(f->'geometry') = 'object' THEN f->'geometry' END);
|
|
288
|
+
n_countries := n_countries + 1;
|
|
289
|
+
END LOOP;
|
|
290
|
+
|
|
291
|
+
FOR f IN SELECT * FROM jsonb_array_elements(feats) LOOP
|
|
292
|
+
PERFORM geo.upsert_region(
|
|
293
|
+
f->'properties'->>'country',
|
|
294
|
+
f->'properties'->>'slug',
|
|
295
|
+
f->'properties'->>'name',
|
|
296
|
+
(f->'properties'->>'type')::geo.region_type,
|
|
297
|
+
(f->'properties'->>'level')::int,
|
|
298
|
+
NULL,
|
|
299
|
+
f->'properties'->>'code',
|
|
300
|
+
ARRAY(SELECT jsonb_array_elements_text(coalesce(f->'properties'->'tags', '[]'::jsonb))),
|
|
301
|
+
coalesce(f->'properties'->'properties', '{}'::jsonb),
|
|
302
|
+
f->'geometry',
|
|
303
|
+
true,
|
|
304
|
+
pid);
|
|
305
|
+
n_regions := n_regions + 1;
|
|
306
|
+
END LOOP;
|
|
307
|
+
|
|
308
|
+
-- regions that left the pack
|
|
309
|
+
DELETE FROM geo.regions r
|
|
310
|
+
USING geo.countries c
|
|
311
|
+
WHERE r.pack_id = pid AND c.id = r.country_id
|
|
312
|
+
AND NOT EXISTS (
|
|
313
|
+
SELECT 1 FROM jsonb_array_elements(feats) ff
|
|
314
|
+
WHERE upper(ff->'properties'->>'country') = c.code AND ff->'properties'->>'slug' = r.slug);
|
|
315
|
+
|
|
316
|
+
-- the parent each region declares (a slug, in its parent_country or its own)
|
|
317
|
+
UPDATE geo.regions r
|
|
318
|
+
SET declared_parent = nullif(ff->'properties'->>'parent', ''),
|
|
319
|
+
declared_parent_country = CASE WHEN nullif(ff->'properties'->>'parent', '') IS NULL THEN NULL
|
|
320
|
+
ELSE upper(coalesce(ff->'properties'->>'parent_country', ff->'properties'->>'country')) END
|
|
321
|
+
FROM jsonb_array_elements(feats) ff
|
|
322
|
+
JOIN geo.countries c ON c.code = upper(ff->'properties'->>'country')
|
|
323
|
+
WHERE r.pack_id = pid AND r.country_id = c.id AND r.slug = ff->'properties'->>'slug';
|
|
324
|
+
|
|
325
|
+
-- parents among pack regions, this pack's and every other's
|
|
326
|
+
PERFORM geo.link_pack_parents();
|
|
327
|
+
|
|
328
|
+
IF p_pack ? 'localities' THEN
|
|
329
|
+
n_localities := geo.install_pack_localities(pid, p_pack->'localities'->'features',
|
|
330
|
+
format('Pack "%s" %s', hdr->>'slug', hdr->>'version'));
|
|
331
|
+
END IF;
|
|
332
|
+
PERFORM geo.link_pack_locality_parents();
|
|
333
|
+
|
|
334
|
+
FOR code IN SELECT c.code FROM geo.countries c WHERE c.geom IS NULL LOOP
|
|
335
|
+
PERFORM geo.derive_country_geom(code);
|
|
336
|
+
END LOOP;
|
|
337
|
+
|
|
338
|
+
n_members := geo.rebuild_all_memberships();
|
|
339
|
+
PERFORM set_config('geo.skip_membership_triggers', 'off', true);
|
|
340
|
+
RETURN jsonb_build_object(
|
|
341
|
+
'pack', hdr->>'slug', 'version', hdr->>'version',
|
|
342
|
+
'countries', n_countries, 'regions', n_regions, 'memberships', n_members)
|
|
343
|
+
|| CASE WHEN n_localities IS NULL THEN '{}'::jsonb ELSE jsonb_build_object('localities', n_localities) END;
|
|
344
|
+
END $$;
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
CREATE OR REPLACE FUNCTION geo.uninstall_pack(p_slug text) RETURNS int
|
|
348
|
+
LANGUAGE plpgsql AS $$
|
|
349
|
+
DECLARE
|
|
350
|
+
pid int;
|
|
351
|
+
n int;
|
|
352
|
+
BEGIN
|
|
353
|
+
SELECT id INTO pid FROM geo.packs WHERE slug = p_slug;
|
|
354
|
+
IF pid IS NULL THEN
|
|
355
|
+
RAISE EXCEPTION 'Unknown pack "%"', p_slug USING ERRCODE = 'invalid_parameter_value';
|
|
356
|
+
END IF;
|
|
357
|
+
PERFORM geo.delete_pack_localities(pid, NULL,
|
|
358
|
+
format('Pack "%s" %s', p_slug, (SELECT version FROM geo.packs WHERE id = pid)));
|
|
359
|
+
DELETE FROM geo.regions WHERE pack_id = pid; -- memberships cascade
|
|
360
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
361
|
+
DELETE FROM geo.packs WHERE id = pid;
|
|
362
|
+
RETURN n;
|
|
363
|
+
END $$;
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
CREATE OR REPLACE FUNCTION geo.localities_by_code(p_codes text[], p_country text DEFAULT NULL) RETURNS SETOF json
|
|
367
|
+
LANGUAGE sql STABLE AS $$
|
|
368
|
+
-- a code given twice answers once, at its first position
|
|
369
|
+
SELECT geo.locality_summary(l)
|
|
370
|
+
FROM (SELECT u.code, min(u.ord) AS ord FROM unnest(p_codes) WITH ORDINALITY AS u(code, ord) GROUP BY u.code) c
|
|
371
|
+
JOIN geo.localities l ON l.code = c.code AND (p_country IS NULL OR l.country = upper(p_country))
|
|
372
|
+
ORDER BY c.ord, l.country
|
|
373
|
+
$$;
|
|
374
|
+
|
|
375
|
+
-- The region ids and every region below them.
|
|
376
|
+
CREATE OR REPLACE FUNCTION geo.region_tree(p_ids int[]) RETURNS SETOF int
|
|
377
|
+
LANGUAGE sql STABLE AS $$
|
|
378
|
+
WITH RECURSIVE tree AS (
|
|
379
|
+
SELECT unnest(p_ids) AS id
|
|
380
|
+
UNION
|
|
381
|
+
SELECT r.id FROM geo.regions r JOIN tree t ON r.parent_id = t.id
|
|
382
|
+
)
|
|
383
|
+
SELECT id FROM tree
|
|
384
|
+
$$;
|
|
385
|
+
|
|
386
|
+
CREATE OR REPLACE FUNCTION geo.localities_in(p_regions text[], p_via text DEFAULT 'parent') RETURNS SETOF json
|
|
387
|
+
LANGUAGE plpgsql STABLE AS $$
|
|
388
|
+
DECLARE
|
|
389
|
+
ids int[];
|
|
390
|
+
BEGIN
|
|
391
|
+
IF p_via IS NULL OR p_via NOT IN ('parent', 'point') THEN
|
|
392
|
+
RAISE EXCEPTION 'via must be parent or point, got %', p_via USING ERRCODE = 'invalid_parameter_value';
|
|
393
|
+
END IF;
|
|
394
|
+
IF p_regions IS NULL OR cardinality(p_regions) = 0 THEN
|
|
395
|
+
RAISE EXCEPTION 'name at least one region' USING ERRCODE = 'invalid_parameter_value';
|
|
396
|
+
END IF;
|
|
397
|
+
ids := geo.resolve_regions(p_regions);
|
|
398
|
+
IF p_via = 'parent' THEN
|
|
399
|
+
RETURN QUERY
|
|
400
|
+
SELECT geo.locality_summary(l) FROM geo.localities l
|
|
401
|
+
WHERE l.parent_id IN (SELECT geo.region_tree(ids))
|
|
402
|
+
ORDER BY l.name_key, l.name, l.country, l.code;
|
|
403
|
+
ELSE
|
|
404
|
+
RETURN QUERY
|
|
405
|
+
SELECT geo.locality_summary(l) FROM geo.localities l
|
|
406
|
+
WHERE l.point IS NOT NULL
|
|
407
|
+
AND EXISTS (SELECT 1 FROM geo.regions r WHERE r.id = ANY (ids) AND ST_Intersects(r.geom, l.point))
|
|
408
|
+
ORDER BY l.name_key, l.name, l.country, l.code;
|
|
409
|
+
END IF;
|
|
410
|
+
END $$;
|
|
411
|
+
|
|
412
|
+
CREATE OR REPLACE FUNCTION geo.check_limit(p_limit int) RETURNS void
|
|
413
|
+
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
414
|
+
BEGIN
|
|
415
|
+
IF p_limit IS NULL OR p_limit < 1 OR p_limit > 1000 THEN
|
|
416
|
+
RAISE EXCEPTION 'limit must be 1 to 1000, got %', coalesce(p_limit::text, 'null')
|
|
417
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
418
|
+
END IF;
|
|
419
|
+
END $$;
|
|
420
|
+
|
|
421
|
+
CREATE OR REPLACE FUNCTION geo.search_localities(p_query text, p_within text[] DEFAULT NULL, p_limit int DEFAULT 20)
|
|
422
|
+
RETURNS SETOF json
|
|
423
|
+
LANGUAGE plpgsql STABLE AS $$
|
|
424
|
+
DECLARE
|
|
425
|
+
q text := coalesce(geo.fold(p_query), '');
|
|
426
|
+
within int[];
|
|
427
|
+
BEGIN
|
|
428
|
+
IF q = '' THEN
|
|
429
|
+
RAISE EXCEPTION 'query must not be empty' USING ERRCODE = 'invalid_parameter_value';
|
|
430
|
+
END IF;
|
|
431
|
+
IF p_within IS NOT NULL AND cardinality(p_within) = 0 THEN
|
|
432
|
+
RAISE EXCEPTION 'within must name at least one region, or be null' USING ERRCODE = 'invalid_parameter_value';
|
|
433
|
+
END IF;
|
|
434
|
+
PERFORM geo.check_limit(p_limit);
|
|
435
|
+
IF p_within IS NOT NULL THEN
|
|
436
|
+
within := ARRAY(SELECT geo.region_tree(geo.resolve_regions(p_within)));
|
|
437
|
+
END IF;
|
|
438
|
+
RETURN QUERY
|
|
439
|
+
SELECT geo.locality_summary(l) FROM geo.localities l
|
|
440
|
+
WHERE strpos(l.name_key, q) > 0
|
|
441
|
+
AND (within IS NULL OR l.parent_id = ANY (within))
|
|
442
|
+
ORDER BY CASE WHEN l.name_key = q THEN 0 WHEN starts_with(l.name_key, q) THEN 1 ELSE 2 END,
|
|
443
|
+
l.name_key, l.name, l.country, l.code
|
|
444
|
+
LIMIT p_limit;
|
|
445
|
+
END $$;
|
|
446
|
+
|
|
447
|
+
CREATE OR REPLACE FUNCTION geo.localities_near(p_lng float8, p_lat float8, p_limit int DEFAULT 10, p_max_m float8 DEFAULT NULL)
|
|
448
|
+
RETURNS SETOF json
|
|
449
|
+
LANGUAGE plpgsql STABLE AS $$
|
|
450
|
+
DECLARE
|
|
451
|
+
here geography;
|
|
452
|
+
BEGIN
|
|
453
|
+
IF p_lng IS NULL OR p_lat IS NULL OR p_lng NOT BETWEEN -180 AND 180 OR p_lat NOT BETWEEN -90 AND 90 THEN
|
|
454
|
+
RAISE EXCEPTION 'lng must be -180..180 and lat -90..90, got %, %', p_lng, p_lat
|
|
455
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
456
|
+
END IF;
|
|
457
|
+
PERFORM geo.check_limit(p_limit);
|
|
458
|
+
IF p_max_m IS NOT NULL AND p_max_m < 0 THEN
|
|
459
|
+
RAISE EXCEPTION 'max_m must be 0 or more, got %', p_max_m USING ERRCODE = 'invalid_parameter_value';
|
|
460
|
+
END IF;
|
|
461
|
+
here := ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography;
|
|
462
|
+
RETURN QUERY
|
|
463
|
+
SELECT geo.locality_summary(n.l, n.m)
|
|
464
|
+
FROM (
|
|
465
|
+
SELECT l, ST_Distance(l.point::geography, here) AS m
|
|
466
|
+
FROM geo.localities l
|
|
467
|
+
WHERE l.point IS NOT NULL
|
|
468
|
+
AND (p_max_m IS NULL OR ST_DWithin(l.point::geography, here, p_max_m))
|
|
469
|
+
ORDER BY l.point::geography <-> here
|
|
470
|
+
LIMIT p_limit
|
|
471
|
+
) n
|
|
472
|
+
ORDER BY n.m, (n.l).country, (n.l).code;
|
|
473
|
+
END $$;
|
|
474
|
+
|
|
475
|
+
CREATE OR REPLACE FUNCTION geo.upsert_locality(
|
|
476
|
+
p_country text,
|
|
477
|
+
p_code text,
|
|
478
|
+
p_name text,
|
|
479
|
+
p_parent text DEFAULT NULL,
|
|
480
|
+
p_parent_country text DEFAULT NULL,
|
|
481
|
+
p_point jsonb DEFAULT NULL,
|
|
482
|
+
p_point_gap text DEFAULT NULL,
|
|
483
|
+
p_tags text[] DEFAULT '{}',
|
|
484
|
+
p_properties jsonb DEFAULT '{}'
|
|
485
|
+
) RETURNS json
|
|
486
|
+
LANGUAGE plpgsql AS $$
|
|
487
|
+
DECLARE
|
|
488
|
+
cc text := upper(p_country);
|
|
489
|
+
owner_ text;
|
|
490
|
+
parent int;
|
|
491
|
+
saved geo.localities;
|
|
492
|
+
BEGIN
|
|
493
|
+
IF coalesce(p_code, '') = '' OR coalesce(p_name, '') = '' THEN
|
|
494
|
+
RAISE EXCEPTION 'A locality needs a code and a name' USING ERRCODE = 'invalid_parameter_value';
|
|
495
|
+
END IF;
|
|
496
|
+
SELECT p.slug INTO owner_ FROM geo.localities l JOIN geo.packs p ON p.id = l.pack_id
|
|
497
|
+
WHERE l.country = cc AND l.code = p_code;
|
|
498
|
+
IF owner_ IS NOT NULL THEN
|
|
499
|
+
RAISE EXCEPTION 'Locality % % belongs to pack "%"; update or uninstall the pack instead', cc, p_code, owner_
|
|
500
|
+
USING ERRCODE = 'unique_violation';
|
|
501
|
+
END IF;
|
|
502
|
+
IF NOT EXISTS (SELECT 1 FROM geo.countries WHERE code = cc) THEN
|
|
503
|
+
RAISE EXCEPTION 'Unknown country "%"', cc USING ERRCODE = 'invalid_parameter_value';
|
|
504
|
+
END IF;
|
|
505
|
+
IF (p_point IS NOT NULL AND jsonb_typeof(p_point) <> 'null') = (coalesce(p_point_gap, '') <> '') THEN
|
|
506
|
+
RAISE EXCEPTION 'locality % %: give a point or a point_gap, not both and not neither', cc, p_code
|
|
507
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
508
|
+
END IF;
|
|
509
|
+
p_parent := nullif(p_parent, ''); -- '' is no parent
|
|
510
|
+
IF p_parent IS NOT NULL THEN
|
|
511
|
+
SELECT r.id INTO parent FROM geo.regions r JOIN geo.countries c ON c.id = r.country_id
|
|
512
|
+
WHERE r.slug = p_parent AND c.code = upper(coalesce(p_parent_country, cc));
|
|
513
|
+
IF parent IS NULL THEN
|
|
514
|
+
RAISE EXCEPTION 'Unknown parent region "%" in %', p_parent, upper(coalesce(p_parent_country, cc))
|
|
515
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
516
|
+
END IF;
|
|
517
|
+
END IF;
|
|
518
|
+
INSERT INTO geo.localities AS l (country, code, name, parent_id, point, point_gap, tags, properties)
|
|
519
|
+
VALUES (cc, p_code, p_name, parent, geo.locality_point(p_point, format('locality %s %s', cc, p_code)),
|
|
520
|
+
nullif(p_point_gap, ''), coalesce(p_tags, '{}'), coalesce(p_properties, '{}'))
|
|
521
|
+
ON CONFLICT (country, code) DO UPDATE SET
|
|
522
|
+
name = EXCLUDED.name, parent_id = EXCLUDED.parent_id, point = EXCLUDED.point,
|
|
523
|
+
point_gap = EXCLUDED.point_gap, tags = EXCLUDED.tags, properties = EXCLUDED.properties
|
|
524
|
+
WHERE l.pack_id IS NULL
|
|
525
|
+
RETURNING * INTO saved;
|
|
526
|
+
-- a pack installed it since the check above: the WHERE skipped the update
|
|
527
|
+
IF NOT FOUND THEN
|
|
528
|
+
RAISE EXCEPTION 'Locality % % belongs to a pack; update or uninstall the pack instead', cc, p_code
|
|
529
|
+
USING ERRCODE = 'unique_violation';
|
|
530
|
+
END IF;
|
|
531
|
+
RETURN geo.locality_summary(saved);
|
|
532
|
+
END $$;
|
|
533
|
+
|
|
534
|
+
CREATE OR REPLACE FUNCTION geo.delete_locality(p_country text, p_code text) RETURNS boolean
|
|
535
|
+
LANGUAGE plpgsql AS $$
|
|
536
|
+
DECLARE
|
|
537
|
+
owner_ text;
|
|
538
|
+
n int;
|
|
539
|
+
d text;
|
|
540
|
+
BEGIN
|
|
541
|
+
SELECT p.slug INTO owner_ FROM geo.localities l JOIN geo.packs p ON p.id = l.pack_id
|
|
542
|
+
WHERE l.country = upper(p_country) AND l.code = p_code;
|
|
543
|
+
IF owner_ IS NOT NULL THEN
|
|
544
|
+
RAISE EXCEPTION 'Locality % % belongs to pack "%"; update or uninstall the pack instead', upper(p_country), p_code, owner_
|
|
545
|
+
USING ERRCODE = 'unique_violation';
|
|
546
|
+
END IF;
|
|
547
|
+
BEGIN
|
|
548
|
+
DELETE FROM geo.localities WHERE country = upper(p_country) AND code = p_code AND pack_id IS NULL;
|
|
549
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
550
|
+
EXCEPTION WHEN foreign_key_violation THEN
|
|
551
|
+
GET STACKED DIAGNOSTICS d = PG_EXCEPTION_DETAIL;
|
|
552
|
+
RAISE EXCEPTION 'Locality % % is still referenced: a host still references it', upper(p_country), p_code
|
|
553
|
+
USING ERRCODE = 'foreign_key_violation', DETAIL = d;
|
|
554
|
+
END;
|
|
555
|
+
RETURN n > 0;
|
|
556
|
+
END $$;
|
|
557
|
+
|
|
558
|
+
-- export_pack as in 007, plus p_include: 'all' (the default), 'regions' or
|
|
559
|
+
-- 'localities'. Under 'all' a localities section is written only when there are
|
|
560
|
+
-- localities to write, so a pack without them exports exactly as before; under
|
|
561
|
+
-- 'localities' it is always written, so installing an empty one empties the pack.
|
|
562
|
+
-- A slug prefix also selects the localities whose parent region it selects.
|
|
563
|
+
DROP FUNCTION geo.export_pack(text, text, text, text);
|
|
564
|
+
CREATE OR REPLACE FUNCTION geo.export_pack(
|
|
565
|
+
p_slug text,
|
|
566
|
+
p_version text,
|
|
567
|
+
p_owner text DEFAULT NULL,
|
|
568
|
+
p_slug_prefix text DEFAULT NULL,
|
|
569
|
+
p_include text DEFAULT 'all'
|
|
570
|
+
) RETURNS jsonb
|
|
571
|
+
LANGUAGE plpgsql STABLE AS $$
|
|
572
|
+
DECLARE
|
|
573
|
+
owner_id int;
|
|
574
|
+
scoped boolean := p_owner IS NOT NULL OR p_slug_prefix IS NOT NULL OR p_include <> 'all';
|
|
575
|
+
BEGIN
|
|
576
|
+
IF p_include IS NULL OR p_include NOT IN ('all', 'regions', 'localities') THEN
|
|
577
|
+
RAISE EXCEPTION 'include must be all, regions or localities, got %', p_include
|
|
578
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
579
|
+
END IF;
|
|
580
|
+
IF p_owner IS NOT NULL AND p_owner <> 'local' THEN
|
|
581
|
+
SELECT id INTO owner_id FROM geo.packs WHERE slug = p_owner;
|
|
582
|
+
IF owner_id IS NULL THEN
|
|
583
|
+
RAISE EXCEPTION 'Unknown pack "%"', p_owner USING ERRCODE = 'invalid_parameter_value';
|
|
584
|
+
END IF;
|
|
585
|
+
END IF;
|
|
586
|
+
|
|
587
|
+
RETURN (
|
|
588
|
+
WITH sel AS (
|
|
589
|
+
SELECT r.*
|
|
590
|
+
FROM geo.regions r
|
|
591
|
+
WHERE (p_owner IS NULL
|
|
592
|
+
OR (p_owner = 'local' AND r.pack_id IS NULL)
|
|
593
|
+
OR r.pack_id = owner_id)
|
|
594
|
+
AND (p_slug_prefix IS NULL OR starts_with(r.slug, p_slug_prefix))
|
|
595
|
+
AND p_include <> 'localities'
|
|
596
|
+
),
|
|
597
|
+
feats AS (
|
|
598
|
+
SELECT c.code AS country, r.slug, jsonb_build_object(
|
|
599
|
+
'type', 'Feature',
|
|
600
|
+
'geometry', ST_AsGeoJSON(r.geom, 6)::jsonb,
|
|
601
|
+
'properties', jsonb_build_object(
|
|
602
|
+
'country', c.code, 'slug', r.slug, 'name', r.name, 'type', r.type, 'level', r.level,
|
|
603
|
+
'parent', coalesce(p.slug, r.declared_parent),
|
|
604
|
+
'parent_country', CASE WHEN p.id IS NOT NULL THEN pc.code ELSE r.declared_parent_country END,
|
|
605
|
+
'code', r.code, 'tags', to_jsonb(r.tags), 'properties', r.properties)) AS feature,
|
|
606
|
+
coalesce(pc.code, r.declared_parent_country) AS parent_country
|
|
607
|
+
FROM sel r
|
|
608
|
+
JOIN geo.countries c ON c.id = r.country_id
|
|
609
|
+
LEFT JOIN geo.regions p ON p.id = r.parent_id
|
|
610
|
+
LEFT JOIN geo.countries pc ON pc.id = p.country_id
|
|
611
|
+
),
|
|
612
|
+
-- with a slug prefix, only the localities whose parent region the prefix selects
|
|
613
|
+
lsel AS (
|
|
614
|
+
SELECT l.* FROM geo.localities l
|
|
615
|
+
WHERE p_include <> 'regions'
|
|
616
|
+
AND (p_owner IS NULL OR (p_owner = 'local' AND l.pack_id IS NULL) OR l.pack_id = owner_id)
|
|
617
|
+
AND (p_slug_prefix IS NULL OR l.parent_id IN (
|
|
618
|
+
SELECT r.id FROM geo.regions r WHERE starts_with(r.slug, p_slug_prefix)))
|
|
619
|
+
),
|
|
620
|
+
lfeats AS (
|
|
621
|
+
SELECT l.country, l.code, jsonb_build_object(
|
|
622
|
+
'type', 'Feature',
|
|
623
|
+
'geometry', CASE WHEN l.point IS NULL THEN NULL ELSE ST_AsGeoJSON(l.point, 6)::jsonb END,
|
|
624
|
+
'properties', jsonb_build_object(
|
|
625
|
+
'country', l.country, 'code', l.code, 'name', l.name,
|
|
626
|
+
'parent', coalesce(p.slug, l.declared_parent),
|
|
627
|
+
'parent_country', CASE WHEN p.id IS NOT NULL THEN pc.code ELSE l.declared_parent_country END,
|
|
628
|
+
'point_gap', l.point_gap, 'tags', to_jsonb(l.tags), 'properties', l.properties)) AS feature,
|
|
629
|
+
coalesce(pc.code, l.declared_parent_country) AS parent_country
|
|
630
|
+
FROM lsel l
|
|
631
|
+
LEFT JOIN geo.regions p ON p.id = l.parent_id
|
|
632
|
+
LEFT JOIN geo.countries pc ON pc.id = p.country_id
|
|
633
|
+
),
|
|
634
|
+
needed AS (
|
|
635
|
+
SELECT country AS code FROM feats
|
|
636
|
+
UNION
|
|
637
|
+
SELECT parent_country FROM feats WHERE parent_country IS NOT NULL
|
|
638
|
+
UNION
|
|
639
|
+
SELECT country FROM lfeats
|
|
640
|
+
UNION
|
|
641
|
+
SELECT parent_country FROM lfeats WHERE parent_country IS NOT NULL
|
|
642
|
+
)
|
|
643
|
+
SELECT jsonb_build_object(
|
|
644
|
+
'pack', jsonb_build_object(
|
|
645
|
+
'slug', p_slug, 'version', p_version, 'schema_version', geo.schema_version(),
|
|
646
|
+
'published_at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')),
|
|
647
|
+
'countries', coalesce((
|
|
648
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
649
|
+
'code', c.code, 'name', c.name,
|
|
650
|
+
'geometry', CASE WHEN scoped OR c.geom IS NULL THEN NULL ELSE ST_AsGeoJSON(c.geom, 6)::jsonb END
|
|
651
|
+
) ORDER BY c.code)
|
|
652
|
+
FROM geo.countries c
|
|
653
|
+
WHERE NOT scoped OR c.code IN (SELECT code FROM needed)), '[]'::jsonb),
|
|
654
|
+
'regions', jsonb_build_object(
|
|
655
|
+
'type', 'FeatureCollection',
|
|
656
|
+
'features', coalesce((SELECT jsonb_agg(feature ORDER BY country, slug) FROM feats), '[]'::jsonb)))
|
|
657
|
+
|| CASE WHEN p_include = 'localities' OR EXISTS (SELECT 1 FROM lfeats) THEN
|
|
658
|
+
jsonb_build_object('localities', jsonb_build_object(
|
|
659
|
+
'type', 'FeatureCollection',
|
|
660
|
+
'features', coalesce((SELECT jsonb_agg(feature ORDER BY country, code) FROM lfeats), '[]'::jsonb)))
|
|
661
|
+
ELSE '{}'::jsonb END
|
|
662
|
+
);
|
|
663
|
+
END $$;
|
|
664
|
+
|
|
665
|
+
SELECT geo.pin_search_path();
|