diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 83% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index 0bb3096..f04effd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Run Tests +name: Run Checks and Tests on: push: @@ -24,5 +24,8 @@ jobs: - name: Install dependencies run: npm ci + - name: Run checks + run: npm run lint + - name: Run tests run: npm test diff --git a/.prettierignore b/.prettierignore index d521d14..df969d4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ node_modules .env .env.* !.env.example +CHANGELOG.md # Package Managers package-lock.json diff --git a/eslint.config.ts b/eslint.config.ts index cf4f3ed..abad146 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -17,7 +17,7 @@ export default ts.config( globals: { ...globals.browser, ...globals.node }, parserOptions: { projectService: { - allowDefaultProject: ['*.js', '*.ts'] + allowDefaultProject: ['*.js', '*.ts', 'scripts/*.ts'] }, ecmaVersion: 'latest', sourceType: 'module' diff --git a/package-lock.json b/package-lock.json index 27216d1..81234d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "globals": "^16.4.0", + "jiti": "^2.6.1", "prettier": "^3.6.2", "tsx": "^4.20.6", "typescript": "5.9.3", @@ -1800,6 +1801,7 @@ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1859,6 +1861,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2314,6 +2317,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2702,6 +2706,7 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3354,6 +3359,17 @@ "dev": true, "license": "MIT" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -3943,6 +3959,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4090,6 +4107,7 @@ "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -4383,6 +4401,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -4416,6 +4435,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4488,6 +4508,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index e19fc0e..a3d4900 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "globals": "^16.4.0", + "jiti": "^2.6.1", "prettier": "^3.6.2", "tsx": "^4.20.6", "typescript": "5.9.3", diff --git a/src/grids/factory.ts b/src/grids/factory.ts index 5e4fa87..7fa3368 100644 --- a/src/grids/factory.ts +++ b/src/grids/factory.ts @@ -16,10 +16,11 @@ export class GridFactory { return new ProjectionGrid(data, ranges); case 'regular': return new RegularGrid(data, ranges); - default: + default: { // This ensures exhaustiveness checking const _exhaustive: never = data; throw new Error(`Unknown grid type: ${_exhaustive}`); + } } } } diff --git a/src/grids/projected.ts b/src/grids/projected.ts index 7a6fd76..3aa2959 100644 --- a/src/grids/projected.ts +++ b/src/grids/projected.ts @@ -46,30 +46,34 @@ export class ProjectionGrid implements GridInterface { this.ny = ranges[0].end - ranges[0].start; switch (data.type) { - case 'projectedFromBounds': + case 'projectedFromBounds': { this.projection = createProjection(data.projection); - let sw = this.projection.forward(data.latitudeBounds[0], data.longitudeBounds[0]); - let ne = this.projection.forward(data.latitudeBounds[1], data.longitudeBounds[1]); + const sw = this.projection.forward(data.latitudeBounds[0], data.longitudeBounds[0]); + const ne = this.projection.forward(data.latitudeBounds[1], data.longitudeBounds[1]); this.origin = sw; this.dx = (ne[0] - sw[0]) / (data.nx - 1); this.dy = (ne[1] - sw[1]) / (data.ny - 1); break; - case 'projectedFromGeographicOrigin': + } + case 'projectedFromGeographicOrigin': { this.projection = createProjection(data.projection); this.origin = this.projection.forward(data.latitude, data.longitude); this.dx = data.dx; this.dy = data.dy; break; - case 'projectedFromProjectedOrigin': + } + case 'projectedFromProjectedOrigin': { this.projection = createProjection(data.projection); this.origin = [data.projectedLongitudeOrigin, data.projectedLatitudeOrigin]; this.dx = data.dx; this.dy = data.dy; break; - default: + } + default: { // This ensures exhaustiveness checking const _exhaustive: never = data; throw new Error(`Unknown projection: ${_exhaustive}`); + } } this.minX = this.origin[0] + this.dx * ranges[1].start; diff --git a/src/grids/projections.ts b/src/grids/projections.ts index 24d1c47..5c9b791 100644 --- a/src/grids/projections.ts +++ b/src/grids/projections.ts @@ -25,10 +25,11 @@ export function createProjection(opts: ProjectionData): Projection { return new LambertConformalConicProjection(opts); case 'LambertAzimuthalEqualAreaProjection': return new LambertAzimuthalEqualAreaProjection(opts); - default: + default: { // This ensures exhaustiveness checking const _exhaustive: never = opts; throw new Error(`Unknown projection: ${_exhaustive}`); + } } } diff --git a/src/grids/regular.ts b/src/grids/regular.ts index 139f6a8..572db50 100644 --- a/src/grids/regular.ts +++ b/src/grids/regular.ts @@ -72,21 +72,6 @@ export class RegularGrid implements GridInterface { return interpolateLinear(values, index, xFraction, yFraction, this.nx); } - getIndex(lat:number, lon:number) { - if ( - lat < this.bounds[1] || - lat >= this.bounds[3] || - lon < this.bounds[0] || - lon >= this.bounds[2] - ) { - return NaN; - } - const x = Math.floor((lon - this.bounds[0]) / this.dx); - const y = Math.floor((lat - this.bounds[1]) / this.dy); - - return y * this.nx + x; - } - getBounds(): Bounds { return this.bounds; } diff --git a/src/utils/arrows.ts b/src/utils/arrows.ts index b5d94c4..c45fcb0 100644 --- a/src/utils/arrows.ts +++ b/src/utils/arrows.ts @@ -33,31 +33,24 @@ export const generateArrows = ( const grid = GridFactory.create(domain.grid); for (let tileY = 0; tileY < extent + 1; tileY += size) { - let lat = tile2lat(y + tileY / extent, z); + const lat = tile2lat(y + tileY / extent, z); for (let tileX = 0; tileX < extent + 1; tileX += size) { - let lon = tile2lon(x + tileX / extent, z); + const lon = tile2lon(x + tileX / extent, z); - let center = [tileX - size / 2, tileY - size / 2]; + const center = [tileX - size / 2, tileY - size / 2]; const geom = []; - // make scale to zoomlevel - let index = grid.getIndex(lat, lon) - if (index % domain.grid.nx < 20) { - continue - } - if (index / domain.grid.ny > (domain.grid.nx -20)) { - continue - } - - let speed = grid.getLinearInterpolatedValue(values, lat, lon); - let direction = degreesToRadians(grid.getLinearInterpolatedValue(directions, lat, lon) + 180); + const speed = grid.getLinearInterpolatedValue(values, lat, lon); + const direction = degreesToRadians( + grid.getLinearInterpolatedValue(directions, lat, lon) + 180 + ); const properties: { value?: number; direction?: number } = { value: speed, direction: direction }; - let rotation = direction; + const rotation = direction; let length = 0.95; if (speed < 30) { length = 0.9; @@ -85,7 +78,7 @@ export const generateArrows = ( } // left arrow head - let [xt0, yt0] = rotatePoint( + const [xt0, yt0] = rotatePoint( center[0], center[1], rotation, diff --git a/src/utils/contours.ts b/src/utils/contours.ts index 080caa0..52d8b1a 100644 --- a/src/utils/contours.ts +++ b/src/utils/contours.ts @@ -107,8 +107,7 @@ export const generateContours = ( y: number, z: number, interval: number = 2, - extent: number = 4096, - threshold = undefined + extent: number = 4096 ) => { const features = []; let cursor: [number, number] = [0, 0]; diff --git a/src/utils/index.ts b/src/utils/index.ts index 73ea453..4073908 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,6 @@ import * as maplibregl from 'maplibre-gl'; -import type { Domain, DomainMetaData, Variable } from '../types'; +import type { Domain, Variable } from '../types'; const now = new Date(); now.setHours(now.getHours() + 1, 0, 0, 0); @@ -24,7 +24,8 @@ export const closestDomainInterval = (time: Date, domain: Domain) => { return newTime; }; -export const closestModelRun = (domain: Domain, selectedTime: Date, latest?: DomainMetaData) => { +// TODO: Is this used/needed? +export const closestModelRun = (domain: Domain, selectedTime: Date) => { const year = selectedTime.getUTCFullYear(); const month = selectedTime.getUTCMonth(); const date = selectedTime.getUTCDate(); diff --git a/src/utils/pbf.ts b/src/utils/pbf.ts index e9ea9fd..51c1a1e 100644 --- a/src/utils/pbf.ts +++ b/src/utils/pbf.ts @@ -3,20 +3,37 @@ import Pbf from 'pbf'; interface Feature { id: number; type: number; - properties: {}; + properties: Record; geom: number[]; } interface Context { - feature: Feature | undefined; + feature?: Feature; keys: string[]; - values: any[]; - keycache: {}; - valuecache: {}; + values: Array; + keycache: Record; + valuecache: Record; +} + +interface VectorTileLayer { + version?: number; + name: string; + extent: number; + features: Feature[]; +} + +interface GeometryPoint { + x: number; + y: number; +} + +interface GeometryFeature { + loadGeometry(): GeometryPoint[][]; + type: number; } // writer for VectorTileLayer -export const writeLayer = (layer: any, pbf: Pbf) => { +export const writeLayer = (layer: VectorTileLayer, pbf: Pbf) => { pbf.writeVarintField(15, layer.version || 2); pbf.writeStringField(1, layer.name); pbf.writeVarintField(5, layer.extent); @@ -65,7 +82,7 @@ export const zigzag = (n: number) => { return (n << 1) ^ (n >> 31); }; -export const writeGeometry = (feature: any, pbf: Pbf) => { +export const writeGeometry = (feature: GeometryFeature, pbf: Pbf) => { const geometry = feature.loadGeometry(); const type = feature.type; let x = 0; @@ -97,18 +114,18 @@ export const writeGeometry = (feature: any, pbf: Pbf) => { } }; -export const writeProperties = (context: any, pbf: Pbf) => { - const feature = context.feature; +export const writeProperties = (context: Context, pbf: Pbf) => { + const feature = context.feature!; const keys = context.keys; const values = context.values; const keycache = context.keycache; const valuecache = context.valuecache; for (const key in feature.properties) { - let value = feature.properties[key]; + const raw = feature.properties[key]; + if (raw === null) continue; // don't encode null value properties let keyIndex = keycache[key]; - if (value === null) continue; // don't encode null value properties if (typeof keyIndex === 'undefined') { keys.push(key); @@ -117,14 +134,18 @@ export const writeProperties = (context: any, pbf: Pbf) => { } pbf.writeVarint(keyIndex); - const type = typeof value; - if (type !== 'string' && type !== 'boolean' && type !== 'number') { - value = JSON.stringify(value); + // normalize value to a primitive for caching/encoding + let storedValue: string | number | boolean; + if (typeof raw === 'string' || typeof raw === 'boolean' || typeof raw === 'number') { + storedValue = raw; + } else { + storedValue = JSON.stringify(raw); } - const valueKey = type + ':' + value; + + const valueKey = typeof raw + ':' + storedValue; let valueIndex = valuecache[valueKey]; if (typeof valueIndex === 'undefined') { - values.push(value); + values.push(storedValue); valueIndex = values.length - 1; valuecache[valueKey] = valueIndex; } @@ -132,19 +153,20 @@ export const writeProperties = (context: any, pbf: Pbf) => { } }; -export const writeValue = (value: any, pbf: Pbf) => { +export const writeValue = (value: string | number | boolean, pbf: Pbf) => { const type = typeof value; if (type === 'string') { - pbf.writeStringField(1, value); + pbf.writeStringField(1, value as string); } else if (type === 'boolean') { - pbf.writeBooleanField(7, value); + pbf.writeBooleanField(7, value as boolean); } else if (type === 'number') { - if (value % 1 !== 0) { - pbf.writeDoubleField(3, value); - } else if (value < 0) { - pbf.writeSVarintField(6, value); + const num = value as number; + if (num % 1 !== 0) { + pbf.writeDoubleField(3, num); + } else if (num < 0) { + pbf.writeSVarintField(6, num); } else { - pbf.writeVarintField(5, value); + pbf.writeVarintField(5, num); } } }; diff --git a/src/worker.ts b/src/worker.ts index 2fc1b86..d7eb8ef 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -11,8 +11,6 @@ import { hideZero } from './utils/variables'; import { GridFactory } from './grids/index'; import { TileRequest } from './worker-pool'; -const OPACITY = 75; - self.onmessage = async (message: MessageEvent): Promise => { if (message.data.type == 'getImage') { const key = message.data.key;