Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Geo I] Typing functionality #5179

Merged
merged 21 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
madsnedergaard marked this conversation as resolved.
Show resolved Hide resolved
}
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;

Check warning

Code scanning / ESLint

Disallow the `any` type

Unexpected any. Specify a different type.
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';
madsnedergaard marked this conversation as resolved.
Show resolved Hide resolved

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[]) {
madsnedergaard marked this conversation as resolved.
Show resolved Hide resolved
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;
Fixed Show fixed Hide fixed

Check warning

Code scanning / ESLint

Disallow the `any` type

Unexpected any. Specify a different type.
}

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

export interface ExchangeConfig {
capacity?: [number, number];
lonlat: [number, number];
rotation: number;
[key: string]: any;

Check warning

Code scanning / ESLint

Disallow the `any` type

Unexpected any. Specify a different type.
// 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