diff --git a/.gitignore b/.gitignore index 5952f55..2afc654 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ baselineEToData/*.png baselineEToData/*.tif baselineEToData/dataPreparer[.exe] observations.json +geocoderCache.json diff --git a/routes/geocoders/Geocoder.ts b/routes/geocoders/Geocoder.ts new file mode 100644 index 0000000..80e5c6d --- /dev/null +++ b/routes/geocoders/Geocoder.ts @@ -0,0 +1,49 @@ +import fs = require("fs"); + +import { GeoCoordinates } from "../../types"; + +export abstract class Geocoder { + + private static cacheFile: string = __dirname + "/../../../geocoderCache.json"; + + private cache: Map; + + public constructor() { + // Load the cache from disk. + if ( fs.existsSync( Geocoder.cacheFile ) ) { + this.cache = new Map( JSON.parse( fs.readFileSync( Geocoder.cacheFile, "utf-8" ) ) ); + } else { + this.cache = new Map(); + } + + // Write the cache to disk every 5 minutes. + setInterval( () => { + this.saveCache(); + }, 5 * 60 * 1000 ); + } + + private saveCache(): void { + fs.writeFileSync( Geocoder.cacheFile, JSON.stringify( Array.from( this.cache.entries() ) ) ); + } + + /** + * Converts a location name to geographic coordinates. + * @param location A location name. + * @return A Promise that will be resolved with the GeoCoordinates of the specified location, or rejected with a + * CodedError. + */ + protected abstract geocodeLocation( location: string ): Promise; + + /** + * Converts a location name to geographic coordinates, first checking the cache and updating it if necessary. + */ + public async getLocation( location: string ): Promise { + if ( this.cache.has( location ) ) { + return this.cache.get( location ); + } + + const coords: GeoCoordinates = await this.geocodeLocation( location ); + this.cache.set( location, coords ); + return coords; + } +} diff --git a/routes/geocoders/GoogleMaps.ts b/routes/geocoders/GoogleMaps.ts new file mode 100644 index 0000000..237ee86 --- /dev/null +++ b/routes/geocoders/GoogleMaps.ts @@ -0,0 +1,35 @@ +import { GeoCoordinates } from "../../types"; +import { CodedError, ErrorCode } from "../../errors"; +import { httpJSONRequest } from "../weather"; +import { Geocoder } from "./Geocoder"; + +export default class GoogleMaps extends Geocoder { + private readonly API_KEY: string; + + public constructor() { + super(); + this.API_KEY = process.env.GOOGLE_MAPS_API_KEY; + if ( !this.API_KEY ) { + throw "GOOGLE_MAPS_API_KEY environment variable is not defined."; + } + } + + public async geocodeLocation( location: string ): Promise { + // Generate URL for Google Maps geocoding request + const url = `https://maps.googleapis.com/maps/api/geocode/json?key=${ this.API_KEY }&address=${ encodeURIComponent( location ) }`; + + let data; + try { + data = await httpJSONRequest( url ); + } catch ( err ) { + // If the request fails, indicate no data was found. + throw new CodedError( ErrorCode.LocationServiceApiError ); + } + + if ( !data.results.length ) { + throw new CodedError( ErrorCode.NoLocationFound ); + } + + return [ data.results[ 0 ].geometry.location.lat, data.results[ 0 ].geometry.location.lng ]; + } +} diff --git a/routes/geocoders/WUnderground.ts b/routes/geocoders/WUnderground.ts new file mode 100644 index 0000000..48b3861 --- /dev/null +++ b/routes/geocoders/WUnderground.ts @@ -0,0 +1,31 @@ +import { GeoCoordinates } from "../../types"; +import { CodedError, ErrorCode } from "../../errors"; +import { httpJSONRequest } from "../weather"; +import { Geocoder } from "./Geocoder"; + +export default class WUnderground extends Geocoder { + public async geocodeLocation( location: string ): Promise { + // Generate URL for autocomplete request + const url = "http://autocomplete.wunderground.com/aq?h=0&query=" + + encodeURIComponent( location ); + + let data; + try { + data = await httpJSONRequest( url ); + } catch ( err ) { + // If the request fails, indicate no data was found. + throw new CodedError( ErrorCode.LocationServiceApiError ); + } + + // Check if the data is valid + if ( typeof data.RESULTS === "object" && data.RESULTS.length && data.RESULTS[ 0 ].tz !== "MISSING" ) { + + // If it is, reply with an array containing the GPS coordinates + return [ parseFloat( data.RESULTS[ 0 ].lat ), parseFloat( data.RESULTS[ 0 ].lon ) ]; + } else { + + // Otherwise, indicate no data was found + throw new CodedError( ErrorCode.NoLocationFound ); + } + } +} \ No newline at end of file diff --git a/routes/weather.ts b/routes/weather.ts index 95a8d12..0afe94e 100644 --- a/routes/weather.ts +++ b/routes/weather.ts @@ -14,9 +14,11 @@ import ZimmermanAdjustmentMethod from "./adjustmentMethods/ZimmermanAdjustmentMe import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod"; import EToAdjustmentMethod from "./adjustmentMethods/EToAdjustmentMethod"; import { CodedError, ErrorCode, makeCodedError } from "../errors"; +import { Geocoder } from "./geocoders/Geocoder"; const WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.WEATHER_PROVIDER || "OWM" ) ).default )(); const PWS_WEATHER_PROVIDER: WeatherProvider = new ( require("./weatherProviders/" + ( process.env.PWS_WEATHER_PROVIDER || "WUnderground" ) ).default )(); +const GEOCODER: Geocoder = new ( require("./geocoders/" + ( process.env.GEOCODER || "WUnderground" ) ).default )(); // Define regex filters to match against location const filters = { @@ -55,28 +57,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi const split: string[] = location.split( "," ); return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ]; } else { - // Generate URL for autocomplete request - const url = "http://autocomplete.wunderground.com/aq?h=0&query=" + - encodeURIComponent( location ); - - let data; - try { - data = await httpJSONRequest( url ); - } catch (err) { - // If the request fails, indicate no data was found. - throw new CodedError( ErrorCode.LocationServiceApiError ); - } - - // Check if the data is valid - if ( typeof data.RESULTS === "object" && data.RESULTS.length && data.RESULTS[ 0 ].tz !== "MISSING" ) { - - // If it is, reply with an array containing the GPS coordinates - return [ parseFloat( data.RESULTS[ 0 ].lat ), parseFloat( data.RESULTS[ 0 ].lon ) ]; - } else { - - // Otherwise, indicate no data was found - throw new CodedError( ErrorCode.NoLocationFound ); - } + return GEOCODER.getLocation( location ); } }