|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- import {
- readFile,
- writeFile,
- mkdir,
- stat,
- } from 'fs/promises';
- import { Stats } from 'fs';
- import { resolve } from 'path';
- import { read } from './json';
-
- type RequiredFeatureProperties = {
- NAME: string,
- ISO_A2_EH: string,
- }
-
- type FeaturePropertyExtensions = {
- properties: RequiredFeatureProperties
- }
-
- type BoundingBoxData = {
- name: string,
- countryCode: string,
- geometry: GeoJSON.Polygon | GeoJSON.MultiPolygon,
- bbox: GeoJSON.Position[] | GeoJSON.Position[][],
- }
-
- type GetCountryGeometryOptions = {
- mergeBoundingBoxes?: boolean;
- }
-
- type GeoOptions = GetCountryGeometryOptions
-
- const getPolygonBoundingBox = (
- theBoundingBox: GeoJSON.Position[],
- polygon: GeoJSON.Position[],
- ) => polygon.reduce<GeoJSON.Position[]>(
- (polygonBoundingBox, [lng, lat]) => [
- [
- Math.min(lng, polygonBoundingBox[0][0]),
- Math.max(lat, polygonBoundingBox[0][1]),
- ],
- [
- Math.max(lng, polygonBoundingBox[1][0]),
- Math.min(lat, polygonBoundingBox[1][1]),
- ],
- ],
- theBoundingBox,
- );
-
- // returns [nw, se] bounds
- const getBoundingBox = (feature: Partial<GeoJSON.Feature>, mergeBoundingBoxes = false) => {
- const { geometry } = feature;
- const { type, coordinates } = geometry as (GeoJSON.Polygon | GeoJSON.MultiPolygon);
-
- const initialBounds: GeoJSON.Position[] = [
- [180, -90],
- [-180, 90],
- ];
-
- if (type === 'Polygon') {
- const polygonBoundingBox = coordinates.reduce(
- (theBoundingBox, polygon) => getPolygonBoundingBox(
- theBoundingBox,
- polygon,
- ),
- initialBounds,
- );
-
- return (mergeBoundingBoxes ? [polygonBoundingBox] : polygonBoundingBox);
- }
-
- if (type === 'MultiPolygon') {
- if (mergeBoundingBoxes) {
- return coordinates.map((polygons) => polygons.reduce(
- getPolygonBoundingBox,
- initialBounds,
- ));
- }
-
- return coordinates.reduce(
- (theCoordinatesBoundingBox, polygons) => polygons.reduce(
- getPolygonBoundingBox,
- theCoordinatesBoundingBox,
- ),
- initialBounds,
- );
- }
-
- throw new Error(`Invalid feature type: ${type as string}. Only Polygon and MultiPolygon are supported.`);
- };
-
- const getRecognizedCountries = async () => {
- const json = await read<GeoJSON.FeatureCollection>('assets/ne_10m_admin_0_countries.json');
- const recognizedCountries = json.features.filter((f) => (
- f.properties
- && f.properties.ISO_A2_EH
- && f.properties.ISO_A2_EH !== '-99'
- && f.properties.NAME
- )) as (typeof json.features[number] & FeaturePropertyExtensions)[];
- return recognizedCountries.map((f) => ({
- name: f.properties.NAME,
- countryCode: f.properties.ISO_A2_EH,
- geometry: f.geometry as (GeoJSON.Polygon | GeoJSON.MultiPolygon),
- }))
- .sort((a, b) => a.name.localeCompare(b.name));
- };
-
- const getCountryData = async (options: GeoOptions): Promise<BoundingBoxData[]> => {
- const outputPath = resolve('assets/countries.json');
- let outputStat: Stats | undefined;
- try {
- outputStat = await stat(outputPath);
- } catch (errRaw) {
- const err = errRaw as { code: string };
- if (err.code !== 'ENOENT') {
- throw err;
- }
- }
-
- if (outputStat) {
- if (outputStat.isDirectory()) {
- throw new Error('Invalid country data. File expected, but directory found.');
- }
-
- const countryBuffer = await readFile(outputPath);
- const countryString = countryBuffer.toString('utf-8');
- return JSON.parse(countryString) as BoundingBoxData[];
- }
-
- const countries = await getRecognizedCountries();
- const countriesWithBoundingBox = countries.map(({ geometry, ...c }) => ({
- ...c,
- geometry,
- bbox: getBoundingBox({ ...c, geometry }, options.mergeBoundingBoxes),
- }));
-
- try {
- await mkdir('assets');
- } catch (errRaw) {
- const err = errRaw as { code: string };
- if (err.code !== 'EEXIST') {
- throw err;
- }
-
- // noop
- }
-
- await writeFile(outputPath, JSON.stringify(countriesWithBoundingBox));
- return countriesWithBoundingBox;
- };
-
- export const getCountryGeometry = async (countryCode: string, options = {} as GeoOptions) => {
- const countryData = await getCountryData(options);
- const country = countryData.find((c) => (
- c.countryCode.toLowerCase() === countryCode.toLowerCase()
- ));
-
- if (country) {
- return country;
- }
-
- throw new Error(`Invalid country code: ${countryCode}`);
- };
|