Skip to content

Commit 8a42b63

Browse files
authored
Merge pull request #300 from commonknowledge/feat/countries
feat: add UK country boundaries
2 parents aa53a36 + 1068632 commit 8a42b63

File tree

7 files changed

+145
-5
lines changed

7 files changed

+145
-5
lines changed

bin/cmd.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Command } from "commander";
22
import { SignJWT } from "jose";
33
import ensureOrganisationMap from "@/server/commands/ensureOrganisationMap";
44
import importConstituencies from "@/server/commands/importConstituencies";
5+
import importCountries from "@/server/commands/importCountries";
56
import importLADs from "@/server/commands/importLADs";
67
import importMSOAs from "@/server/commands/importMSOAs";
78
import importOutputAreas from "@/server/commands/importOutputAreas";
@@ -88,6 +89,13 @@ program
8889
await importRegions();
8990
});
9091

92+
program
93+
.command("importCountries")
94+
.description("Import UK Countries")
95+
.action(async () => {
96+
await importCountries();
97+
});
98+
9199
program
92100
.command("importDataSource <dataSourceId>")
93101
.description("Import a data source by its ID")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { sql } from "kysely";
3+
import type { Kysely } from "kysely";
4+
5+
export async function up(db: Kysely<any>): Promise<void> {
6+
// Set data_record.geocode_result->'areas'->'UKC24' to:
7+
// case data_record.geocode_result->'areas'->'UKR18' starts with 'E': E92000001
8+
// case data_record.geocode_result->'areas'->'UKR18' starts with 'W': W92000004
9+
// case data_record.geocode_result->'areas'->'UKR18' starts with 'S': S92000003
10+
// case data_record.geocode_result->'areas'->'UKR18' starts with 'N': N92000002
11+
await db
12+
.updateTable("data_record")
13+
.set({
14+
geocode_result: sql`jsonb_set(
15+
geocode_result,
16+
'{areas,UKC24}',
17+
to_jsonb(
18+
CASE
19+
WHEN geocode_result->'areas'->>'UKR18' LIKE 'E%' THEN 'E92000001'
20+
WHEN geocode_result->'areas'->>'UKR18' LIKE 'W%' THEN 'W92000004'
21+
WHEN geocode_result->'areas'->>'UKR18' LIKE 'S%' THEN 'S92000003'
22+
WHEN geocode_result->'areas'->>'UKR18' LIKE 'N%' THEN 'N92000002'
23+
END
24+
)
25+
)`,
26+
})
27+
.where(sql<boolean>`geocode_result->'areas' ? 'UKR18'`)
28+
.where(sql<boolean>`geocode_result->'areas'->>'UKR18' ~ '^[EWSN]'`)
29+
.execute();
30+
}
31+
32+
export async function down(db: Kysely<any>): Promise<void> {
33+
// remove "UKC24" key from data_record.geocode_result->'areas'
34+
await db
35+
.updateTable("data_record")
36+
.set({
37+
geocode_result: sql`geocode_result #- '{areas,UKC24}'`,
38+
})
39+
.where(sql<boolean>`geocode_result->'areas' ? 'UKC24'`)
40+
.execute();
41+
}

src/app/map/[id]/components/Choropleth/areas.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export const getValidAreaSetGroupCodes = (
1818
: null;
1919

2020
if (areaSetCode) {
21-
// Get a list of area sets that are smaller or equal in size
22-
// to the data source area set
21+
// Get a list of area sets that are larger than the data source
22+
// area set. Also always include the data source area set itself.
2323
const dataSourceAreaSize = AreaSetSizes[areaSetCode];
2424
const validAreaSets = Object.keys(AreaSetSizes).filter(
25-
(code) => AreaSetSizes[code as AreaSetCode] >= dataSourceAreaSize,
25+
(code) =>
26+
code === areaSetCode ||
27+
AreaSetSizes[code as AreaSetCode] > dataSourceAreaSize,
2628
);
2729

2830
// Get the associated group for each valid area set

src/app/map/[id]/components/Choropleth/configs.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ export const CHOROPLETH_LAYER_CONFIGS: Record<
1919
AreaSetGroupCode,
2020
ChoroplethLayerConfig[]
2121
> = {
22+
UKC24: [
23+
{
24+
areaSetCode: AreaSetCode.UKC24,
25+
minZoom: 0,
26+
requiresBoundingBox: false,
27+
mapbox: {
28+
featureCodeProperty: "CTRY24CD",
29+
featureNameProperty: "CTRY24NM",
30+
layerId: "countries",
31+
sourceId: "commonknowledge.4rj0pr4g",
32+
},
33+
},
34+
],
2235
UKR18: [
2336
{
2437
areaSetCode: AreaSetCode.UKR18,

src/labels.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ export const AreaSetCodeLabels: Record<AreaSetCode, string> = {
2222
MSOA21: "Middle Super Output Area (2021)",
2323
OA21: "Census Output Area (2021)",
2424
UKR18: "UK Region (2018)",
25+
UKC24: "UK Country (2024)",
2526
};
2627

2728
export const AreaSetGroupCodeLabels: Record<AreaSetGroupCode, string> = {
2829
WMC24: "Westminster Constituency (2024)",
2930
LAD25: "Local Authority District (2025)",
30-
W25: "Ward (2025)",
31-
OA21: "Census Output Area (2021)",
31+
W25: "Local Authority District -> Ward (2025)",
32+
OA21: "MSOA -> Census Output Area (2021)",
3233
UKR18: "UK Region (2018)",
34+
UKC24: "UK Country (2024)",
3335
};
3436

3537
type DataSourceConfigKey =
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import fs from "fs";
2+
import { join } from "path";
3+
import { sql } from "kysely";
4+
import { AreaSetCode } from "@/server/models/AreaSet";
5+
import {
6+
createAreaSet,
7+
findAreaSetByCode,
8+
} from "@/server/repositories/AreaSet";
9+
import { db } from "@/server/services/database";
10+
import logger from "@/server/services/logger";
11+
import { getBaseDir } from "@/server/utils";
12+
13+
const AREA_SET_CODE = AreaSetCode.UKC24;
14+
15+
const importCountries = async () => {
16+
const countriesGeojsonPath = join(
17+
getBaseDir(),
18+
"resources",
19+
"areaSets",
20+
"countries.geojson",
21+
);
22+
if (!fs.existsSync(countriesGeojsonPath)) {
23+
logger.error(
24+
`File not found: ${countriesGeojsonPath}. Download from https://www.data.gov.uk/dataset/c0ebe11c-0c81-4eed-81b3-a0394d4116a9/countries-december-2024-boundaries-uk-bgc1`,
25+
);
26+
return;
27+
}
28+
let areaSet = await findAreaSetByCode(AREA_SET_CODE);
29+
if (!areaSet) {
30+
areaSet = await createAreaSet({
31+
name: "UK Countries",
32+
code: AREA_SET_CODE,
33+
});
34+
logger.info(`Inserted area set ${AREA_SET_CODE}`);
35+
} else {
36+
logger.info(`Using area set ${AREA_SET_CODE}`);
37+
}
38+
const geojson = fs.readFileSync(countriesGeojsonPath, "utf8");
39+
const areas = JSON.parse(geojson) as {
40+
features: {
41+
properties: { CTRY24NM: string; CTRY24CD: string };
42+
geometry: unknown;
43+
}[];
44+
};
45+
const count = areas.features.length;
46+
for (let i = 0; i < count; i++) {
47+
const feature = areas.features[i];
48+
const code = feature.properties.CTRY24CD;
49+
const name = feature.properties.CTRY24NM;
50+
await sql`INSERT INTO area (name, code, geography, area_set_id)
51+
VALUES (
52+
${name},
53+
${code},
54+
ST_Transform(
55+
ST_SetSRID(
56+
ST_GeomFromGeoJSON(${JSON.stringify(feature.geometry)}),
57+
27700 -- Set the original EPSG:27700 (British National Grid)
58+
),
59+
4326 -- Convert to WGS 84
60+
)::geography,
61+
${areaSet.id}
62+
)
63+
ON CONFLICT (code, area_set_id) DO UPDATE SET geography = EXCLUDED.geography;
64+
`.execute(db);
65+
66+
const percentComplete = Math.floor((i * 100) / count);
67+
logger.info(`Inserted area ${code}. ${percentComplete}% complete`);
68+
}
69+
};
70+
71+
export default importCountries;

src/server/models/AreaSet.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export enum AreaSetCode {
99
MSOA21 = "MSOA21",
1010
OA21 = "OA21",
1111
UKR18 = "UKR18",
12+
UKC24 = "UKC24",
1213
}
1314
export const areaSetCodes = Object.values(AreaSetCode);
1415

@@ -20,6 +21,7 @@ export enum AreaSetGroupCode {
2021
W25 = "W25",
2122
OA21 = "OA21",
2223
UKR18 = "UKR18",
24+
UKC24 = "UKC24",
2325
}
2426
export const areaSetGroupCodes = Object.values(AreaSetGroupCode);
2527

@@ -33,6 +35,7 @@ export const AreaSetSizes: Record<AreaSetCode, number> = {
3335
[AreaSetCode.LAD25]: 4,
3436
[AreaSetCode.WMC24]: 4,
3537
[AreaSetCode.UKR18]: 8,
38+
[AreaSetCode.UKC24]: 12,
3639
};
3740

3841
export const areaSetSchema = z.object({

0 commit comments

Comments
 (0)