Merge pull request #98 from Derpthemeus/google-maps-geocoding
Use Google Maps for geocoding
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ baselineEToData/*.png
|
|||||||
baselineEToData/*.tif
|
baselineEToData/*.tif
|
||||||
baselineEToData/dataPreparer[.exe]
|
baselineEToData/dataPreparer[.exe]
|
||||||
observations.json
|
observations.json
|
||||||
|
geocoderCache.json
|
||||||
|
|||||||
49
routes/geocoders/Geocoder.ts
Normal file
49
routes/geocoders/Geocoder.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
routes/geocoders/GoogleMaps.ts
Normal file
35
routes/geocoders/GoogleMaps.ts
Normal 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 ];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
routes/geocoders/WUnderground.ts
Normal file
31
routes/geocoders/WUnderground.ts
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,9 +14,11 @@ import ZimmermanAdjustmentMethod from "./adjustmentMethods/ZimmermanAdjustmentMe
|
|||||||
import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod";
|
import RainDelayAdjustmentMethod from "./adjustmentMethods/RainDelayAdjustmentMethod";
|
||||||
import EToAdjustmentMethod from "./adjustmentMethods/EToAdjustmentMethod";
|
import EToAdjustmentMethod from "./adjustmentMethods/EToAdjustmentMethod";
|
||||||
import { CodedError, ErrorCode, makeCodedError } from "../errors";
|
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 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 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
|
// Define regex filters to match against location
|
||||||
const filters = {
|
const filters = {
|
||||||
@@ -55,28 +57,7 @@ export async function resolveCoordinates( location: string ): Promise< GeoCoordi
|
|||||||
const split: string[] = location.split( "," );
|
const split: string[] = location.split( "," );
|
||||||
return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ];
|
return [ parseFloat( split[ 0 ] ), parseFloat( split[ 1 ] ) ];
|
||||||
} else {
|
} else {
|
||||||
// Generate URL for autocomplete request
|
return GEOCODER.getLocation( location );
|
||||||
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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user