Skip to content

Commit

Permalink
[Geo I] Typing functionality (#5179)
Browse files Browse the repository at this point in the history
Co-authored-by: Tony <tonyvanswet@gmail.com>
  • Loading branch information
madsnedergaard and tonypls authored Mar 17, 2023
1 parent 030e919 commit 5073fff
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 89 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Generate config files
run: pnpm create-generated-files
- name: Run TypeScript typechecking
- name: Run TypeScript typechecking for app
run: pnpm run run-tsc
- name: Run TypeScript typechecking for scripts
run: pnpm run run-tsc --project tsconfig.node.json
4 changes: 3 additions & 1 deletion web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ coverage
.nyc_output
cypress/videos
cypress/screenshots
config
config

tsconfig.node.tsbuildinfo
8 changes: 5 additions & 3 deletions web/geo/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const sortObjectByKey = (object: any) =>
import { ZoneConfig } from './types';

const sortObjectByKey = (object: ZoneConfig) =>
Object.keys(object)
.sort()
.reduce((result, key) => {
result[key] = object[key];
return result;
}, {});
}, {} as { [key: string]: ZoneConfig });

const saveZoneYaml = (zoneKey: string, zone: any) => {
const saveZoneYaml = (zoneKey: string, zone: ZoneConfig) => {
const zonePath = path.resolve(
fileURLToPath(new URL(`../../config/zones/${zoneKey}.yaml`, import.meta.url))
);
Expand Down
55 changes: 35 additions & 20 deletions web/geo/generateAggregates.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { Feature, MultiPolygon, Polygon, Properties, union } from '@turf/turf';
import type { ZoneConfig } from './types';
import { Feature, MultiPolygon, union } from '@turf/turf';
import { ZonesConfig, WorldFeatureCollection, FeatureProperties } from './types';

const generateAggregates = (geojson, zones: ZoneConfig) => {
const emptyFeature: Feature<MultiPolygon, FeatureProperties> = {
type: 'Feature',
properties: {
zoneName: '',
countryKey: '',
countryName: '',
isHighestGranularity: false,
isAggregatedView: true,
isCombined: true,
},
geometry: { type: 'MultiPolygon', coordinates: [] },
};

const generateAggregates = (fc: WorldFeatureCollection, zones: ZonesConfig) => {
const skippedZones: string[] = []; // Holds skipped subZones that are not in the geojson
const { features } = geojson;
const { features } = fc;

const countryZonesToCombine = Object.values(zones)
.filter((zone) => zone.subZoneNames && zone.subZoneNames.length > 0)
Expand All @@ -28,32 +41,34 @@ const generateAggregates = (geojson, zones: ZoneConfig) => {

const combinedZones = countryZonesToCombine
.map((country) => {
// TODO: Consider if should remove this check and just fail if country is undefined.
// This was done to avoid having null-checks everywhere, but maybe it can be done it a
// better way. See discussion here: https://github.com/electricitymaps/electricitymaps-contrib/pull/5179#discussion_r1131568845
if (country === undefined) {
return null;
return emptyFeature;
}
const combinedCountry: Feature<MultiPolygon | Polygon, Properties> = {
type: 'Feature',
properties: {
isHighestGranularity: false,
isAggregatedView: true,
isCombined: true,
},
geometry: { type: 'MultiPolygon', coordinates: [] },
};
const multiZoneCountry = unCombinedZones.find(
(feature) => feature.properties.zoneName === country[0]
);
const combinedCountry: Feature<MultiPolygon, FeatureProperties> = {
...emptyFeature,
properties: {
...emptyFeature.properties,
countryKey: multiZoneCountry?.properties.countryKey || '',
zoneName: multiZoneCountry?.properties.countryKey || '',
countryName: multiZoneCountry?.properties.countryName || '',
},
};

for (const subZone of country) {
const zoneToAdd = unCombinedZones.find(
(feature) => feature.properties.zoneName === subZone
);

const combinedCountryPolygon = combinedCountry.geometry as MultiPolygon;
const combinedCountryPolygon = combinedCountry.geometry;
if (zoneToAdd) {
const unionGeometry = union(
combinedCountryPolygon,
zoneToAdd.geometry
)?.geometry;
const unionGeometry = union(combinedCountryPolygon, zoneToAdd.geometry)
?.geometry as MultiPolygon;
if (unionGeometry) {
combinedCountry.geometry = unionGeometry;
}
Expand All @@ -62,7 +77,7 @@ const generateAggregates = (geojson, zones: ZoneConfig) => {
}
}

if (combinedCountry.properties) {
if (combinedCountry.properties && multiZoneCountry) {
combinedCountry.properties['countryKey'] = multiZoneCountry.properties.countryKey;
combinedCountry.properties['zoneName'] = multiZoneCountry.properties.countryKey;
}
Expand Down
5 changes: 3 additions & 2 deletions web/geo/generateExchangesToExclude.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { mergeExchanges } from '../scripts/generateZonesConfig.js';
import { ZonesConfig } from './types.js';
import { fileExists, getJSON, writeJSON } from './utilities.js';

const exchangeConfig = mergeExchanges();

const generateExchangesToIgnore = (OUT_PATH, zonesConfig) => {
const generateExchangesToIgnore = (OUT_PATH: string, zonesConfig: ZonesConfig) => {
console.info(`Generating new excludedAggregatedExchanges.json...`);
const countryKeysToExclude = new Set(
Object.keys(zonesConfig).filter((key) => {
if (zonesConfig[key].subZoneNames?.length > 0) {
if ((zonesConfig[key].subZoneNames ?? []).length > 0) {
return key;
}
})
Expand Down
12 changes: 8 additions & 4 deletions web/geo/generateTopojson.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as turf from '@turf/turf';
import { topology } from 'topojson-server';
import { fileExists, getJSON, round, writeJSON } from './utilities.js';
import { WorldFeatureCollection } from './types.js';

function getCenter(geojson, zoneName) {
function getCenter(geojson: WorldFeatureCollection, zoneName: string) {
switch (zoneName) {
case 'US-AK': {
return [-151.77, 65.32];
Expand Down Expand Up @@ -49,16 +50,19 @@ function getCenter(geojson, zoneName) {
];
}

function generateTopojson(fc, { OUT_PATH, verifyNoUpdates }) {
function generateTopojson(
fc: WorldFeatureCollection,
{ OUT_PATH, verifyNoUpdates }: { OUT_PATH: string; verifyNoUpdates: boolean }
) {
const output = OUT_PATH.split('/').pop();
console.info(`Generating new ${output}`);
const topo = topology({
objects: fc,
});

// We do the following to match the specific format needed for visualization
const objects: any = topo.objects.objects;
const newObjects = {};
const objects = topo.objects.objects as any;
const newObjects = {} as typeof topo.objects;
for (const geo of objects.geometries) {
// Precompute center for enable centering on the zone
geo.properties.center = getCenter(fc, geo.properties.zoneName);
Expand Down
7 changes: 4 additions & 3 deletions web/geo/generateWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { generateExchangesToIgnore } from './generateExchangesToExclude.js';
import { generateTopojson } from './generateTopojson.js';
import { getJSON, round } from './utilities.js';
import { validateGeometry } from './validate.js';
import { WorldFeatureCollection } from './types.js';

const config = {
export const config = {
WORLD_PATH: path.resolve(fileURLToPath(new URL('world.geojson', import.meta.url))),
OUT_PATH: path.resolve(fileURLToPath(new URL('../config/world.json', import.meta.url))),
ERROR_PATH: path.resolve(fileURLToPath(new URL('.', import.meta.url))),
Expand All @@ -17,13 +18,13 @@ const config = {
MIN_AREA_INTERSECTION: 6_000_000,
SLIVER_RATIO: 0.0001, // ratio of length and area to determine if the polygon is a sliver and should be ignored
verifyNoUpdates: process.env.VERIFY_NO_UPDATES !== undefined,
};
} as const;

const EXCHANGE_OUT_PATH = path.resolve(
fileURLToPath(new URL('../config/excludedAggregatedExchanges.json', import.meta.url))
);

const fc = getJSON(config.WORLD_PATH);
const fc: WorldFeatureCollection = getJSON(config.WORLD_PATH);
const zoneConfig = mergeZones();
const aggregates = generateAggregates(fc, zoneConfig);

Expand Down
21 changes: 11 additions & 10 deletions web/geo/generateZoneBoundingBoxes.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const arguments_ = process.argv.slice(2);

import { mergeZones } from '../scripts/generateZonesConfig.js';
import { saveZoneYaml } from './files.js';
import { getJSON } from './utilities.js';
import { WorldFeatureCollection } from './types.js';
import { Position } from '@turf/turf';

const inputArguments = process.argv.slice(2);

const zonesGeo = getJSON(
const zonesGeo: WorldFeatureCollection = getJSON(
path.resolve(fileURLToPath(new URL('world.geojson', import.meta.url)))
);
const zones = mergeZones();

if (arguments_.length <= 0) {
if (inputArguments.length <= 0) {
console.error(
'ERROR: Please add a zoneName parameter ("ts-node generateZoneBoundingBoxes.ts DE")'
);
process.exit(1);
}

const zoneKey = arguments_[0];
const zoneKey = inputArguments[0];

if (!(zoneKey in zones)) {
console.error(`ERROR: Zone ${zoneKey} does not exist in configuration`);
Expand All @@ -28,7 +29,7 @@ if (!(zoneKey in zones)) {

zonesGeo.features = zonesGeo.features.filter((d) => d.properties.zoneName === zoneKey);

let allCoords: number[] = [];
let allCoords = [];
const boundingBoxes: { [key: string]: any } = {};

for (const zone of zonesGeo.features) {
Expand All @@ -46,7 +47,7 @@ for (const zone of zonesGeo.features) {
let maxLon = -200;

if (geometryType == 'MultiPolygon') {
for (const coord of allCoords) {
for (const coord of allCoords as Position[]) {
const lon = coord[0];
const lat = coord[1];

Expand All @@ -56,8 +57,8 @@ for (const zone of zonesGeo.features) {
maxLat = Math.max(maxLat, lat);
}
} else {
const lon = allCoords[0];
const lat = allCoords[1];
const lon = (allCoords as Position)[0];
const lat = (allCoords as Position)[1];

minLon = Math.min(minLon, lon);
maxLon = Math.max(maxLon, lon);
Expand Down
48 changes: 44 additions & 4 deletions web/geo/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
import { FeatureCollection, Feature, MultiPolygon, Polygon } from '@turf/turf';
import { config } from './generateWorld';

export type GeoConfig = typeof config;

export interface ZoneConfig {
[key: string]: {
subZoneNames?: string[];
bounding_box: number[];
};
subZoneNames?: string[];
bounding_box: number[][];
timezone: string;
[key: string]: any;
}

export interface ZonesConfig {
[key: string]: ZoneConfig;
}

export interface ExchangeConfig {
capacity?: [number, number];
lonlat: [number, number];
rotation: number;
[key: string]: any;
// The following properties are removed from the generated exchange config
// comment?: string;
// _comment?: string;
// parsers?: {
// exchange?: string;
// exchangeForecast?: string;
// };
}

export interface ExchangesConfig {
[key: string]: ExchangeConfig;
}

export declare type FeatureProperties = {
zoneName: string;
countryKey: string;
countryName: string;
isAggregatedView?: boolean;
isHighestGranularity?: boolean;
isCombined?: boolean;
};

export interface WorldFeatureCollection extends FeatureCollection {
features: Feature<MultiPolygon | Polygon, FeatureProperties>[];
}
Loading

0 comments on commit 5073fff

Please sign in to comment.