From fc74e933d6334576096c2c49152dff819577c749 Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Mon, 30 Mar 2026 21:53:01 +0100 Subject: [PATCH] Add StationDataModule, implementing getNearestStations & a Geohash generator method --- package-lock.json | 23 +++++++++++++++++++---- package.json | 8 ++++++-- src/lib/validation.ts | 20 +++++++++++++++++--- src/modules/pis.ts | 2 +- src/modules/stationData.ts | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 src/modules/stationData.ts diff --git a/package-lock.json b/package-lock.json index c8d3495..c012257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "3.0.0", "license": "GPL-3.0", "dependencies": { - "@owlboard/api-schema-types": "^3.0.2-alpha2" + "@owlboard/api-schema-types": "^3.0.2-alpha3", + "latlon-geohash": "^2.0.0" }, "devDependencies": { + "@types/latlon-geohash": "^2.0.4", "@types/node": "^25.3.0", "ts-node": "^10.9.2", "tsx": "^4.21.0", @@ -502,9 +504,9 @@ } }, "node_modules/@owlboard/api-schema-types": { - "version": "3.0.2-alpha2", - "resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.2-alpha2/api-schema-types-3.0.2-alpha2.tgz", - "integrity": "sha512-KyX4QcOCzVqYpiXY+WfhM1soXduMt2ldG6JSBK2WBxXWokS+keZshOHWHGTZvPLoZEWsuPznMAdzytI03/D3Ag==", + "version": "3.0.2-alpha3", + "resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.2-alpha3/api-schema-types-3.0.2-alpha3.tgz", + "integrity": "sha512-3NFP21QdSfjziwlGQixlNnUWC55HlKZGyWANIOimWu0FZejWQWExJiaAVfb6m3Sbv+zvQMu3B8mzcMCcGadZCQ==", "license": "MIT" }, "node_modules/@tsconfig/node10": { @@ -535,6 +537,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/latlon-geohash": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/latlon-geohash/-/latlon-geohash-2.0.4.tgz", + "integrity": "sha512-R/wb/V8lhhI0hyRGDTkT6F4C3lynkF/D29iMSN5B4bYswaa4R+wajgcTv9z73BotGArf4Q8BnDNtmLeI+WiKkQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", @@ -665,6 +674,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/latlon-geohash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/latlon-geohash/-/latlon-geohash-2.0.0.tgz", + "integrity": "sha512-OKBswTwrvTdtenV+9C9euBmvgGuqyjJNAzpQCarRz1m8/pYD2nz9fKkXmLs2S3jeXaLi3Ry76twQplKKUlgS/g==", + "license": "MIT" + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", diff --git a/package.json b/package.json index d7d2069..28e0681 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "type": "module", "sideEffects": false, "types": "./dist/index.d.ts", - "files": ["dist"], + "files": [ + "dist" + ], "exports": { ".": { "types": "./dist/index.d.ts", @@ -28,9 +30,11 @@ "author": "Frederick Boniface", "license": "GPL-3.0", "dependencies": { - "@owlboard/api-schema-types": "^3.0.2-alpha2" + "@owlboard/api-schema-types": "^3.0.2-alpha3", + "latlon-geohash": "^2.0.0" }, "devDependencies": { + "@types/latlon-geohash": "^2.0.4", "@types/node": "^25.3.0", "ts-node": "^10.9.2", "tsx": "^4.21.0", diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 76188fd..a57c2e4 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -1,6 +1,7 @@ /** -* Checks if a string is a valid CRS +* Checks if a string is a valid CRS (Syntactically Only) * using byte level checking for max performance +* @param CRS The CRS Code to be validated */ export const IsValidCrs = (CRS: string): boolean => { if (CRS.length !== 3) return false; @@ -15,8 +16,9 @@ export const IsValidCrs = (CRS: string): boolean => { } /** - * Checks if a string is a valid TIPLOC + * Checks if a string is a valid TIPLOC (Syntactically Only) * using byte level checking for max performance + * @param TIPLOC The TIPLOC to be validated */ export const IsValidTiploc = (TIPLOC: string): boolean => { const l = TIPLOC.length; @@ -33,9 +35,10 @@ export const IsValidTiploc = (TIPLOC: string): boolean => { } /** - * Checks if a string is a valid PIS Code + * Checks if a string is a valid PIS Code (Syntactically Only) * using byte level checking for max performance * ONLY GWR (4-digit) CODES SUPPORTED + * @param PIS The PIS Code to be validated */ export const IsValidPis = (PIS: string): boolean => { if (PIS.length !== 4) return false; @@ -44,4 +47,15 @@ export const IsValidPis = (PIS: string): boolean => { if (!(char >= 48 && char <= 57)) return false; } return true; +} + +/** + * Validates Geohash string against standard b32 alphabet + * (Syntactically Only validation) + * @param hash The geohash to be validated + * @param maxLen Defaults to 6 - which is enforced by the server, should not need changing + */ +export const IsValidGeoHash = (hash: string, maxLen: number = 6): boolean => { + const geoHashRegex = new RegExp(`^[0-9bcdefghjkmnpqrstuvwxyz]{1,${maxLen}}$`, 'i'); + return geoHashRegex.test(hash); } \ No newline at end of file diff --git a/src/modules/pis.ts b/src/modules/pis.ts index 6939211..a62df71 100644 --- a/src/modules/pis.ts +++ b/src/modules/pis.ts @@ -1,6 +1,6 @@ import { ApiPisObject } from '@owlboard/api-schema-types'; import type { BaseClient, ApiResult } from '../lib/base.js'; -import { IsValidCrs, IsValidTiploc, IsValidPis } from '../lib/validation.js'; +import { IsValidCrs, IsValidPis } from '../lib/validation.js'; import { ValidationError } from '../lib/errors.js'; export class PisModule { diff --git a/src/modules/stationData.ts b/src/modules/stationData.ts new file mode 100644 index 0000000..b658c24 --- /dev/null +++ b/src/modules/stationData.ts @@ -0,0 +1,37 @@ +import { ApiStationsNearestStations } from '@owlboard/api-schema-types'; +import type { BaseClient, ApiResult } from '../lib/base.js'; +import { IsValidGeoHash } from '../lib/validation.js'; +import { ValidationError } from '../lib/errors.js'; +import Geohash from 'latlon-geohash'; + +export class StationDataModule { + constructor(private client: BaseClient) {} + + /** + * Generates a 6-char Geohash for the given co-ordinates + * @param lt Latitude + * @param ln Longitude + * @returns Geohash as string + */ + static generateGeohash(lt: number, ln: number): string { + return Geohash.encode(lt, ln, 6); + } + + /** + * + * @param geohash Geohash as string (up to six characters), generate using this.generateGeohash() + * @returns Nearest Stations API Response (CRS, Name) + */ + async getNearestStations(geohash: string): Promise> { + if (!IsValidGeoHash(geohash)) { + throw new ValidationError("hash", "Invalid Geohash requested"); + } + + const path = `/stationData/nearest/${geohash}`; + return this.client.request(path, { + method: 'GET', + }) + } + + // getStationSate(crs: string){} +} \ No newline at end of file