Merge pull request #98 from Derpthemeus/google-maps-geocoding

Use Google Maps for geocoding
This commit is contained in:
Ray
2020-06-06 17:44:31 -04:00
committed by GitHub
5 changed files with 119 additions and 22 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ baselineEToData/*.png
baselineEToData/*.tif
baselineEToData/dataPreparer[.exe]
observations.json
geocoderCache.json

View File

@@ -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<string, GeoCoordinates>;
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<GeoCoordinates>;
/**
* Converts a location name to geographic coordinates, first checking the cache and updating it if necessary.
*/
public async getLocation( location: string ): Promise<GeoCoordinates> {
if ( this.cache.has( location ) ) {
return this.cache.get( location );
}
const coords: GeoCoordinates = await this.geocodeLocation( location );
this.cache.set( location, coords );
return coords;
}
}

View File

@@ -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<GeoCoordinates> {
// 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 ];
}
}

View File

@@ -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<GeoCoordinates> {
// 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 );
}
}
}

View File

@@ -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 );
}
}