UNPKG

mapwrap

Version:

Simple wrapper around Google Maps API

386 lines (322 loc) 14.5 kB
"use strict"; const axios = require("axios"); const LRUCache = require("./cache/LRUCache"); const ReverseGeoWrapper = require("./wrappers/ReverseGeoWrapper"); const GeoWrapper = require("./wrappers/GeoWrapper"); const DirectionsWrapper = require("./wrappers/DirectionsWrapper"); const PlaceSearchWrapper = require("./wrappers/PlaceSearchWrapper"); const Joi = require("joi"); const nearbySearchModel = require("./models/nearbySearchPlaces"); const directionsModel = require("./models/directions"); const mapwrapConfigModel = require("./models/mapwrapConfig"); /** * Wrapper for several Google API services including Directions, Geocoding, and Places with built in LRU caching. */ class MapWrap { /** * constructor to configure MapWrap * @constructor * @param {Object} config - Object to configure MapWrap instance. * @param {String} config.DEFAULT_API_KEY - Google API Key to be used. If a key was restricted to a certain service, you must use `useRestrictedKeys`. * * @param {Object} [config.useRestrictedKeys] - Alternative option to DEFAULT_API_KEY. * @param {String} [config.useRestrictedKeys.GEOCODING_API_KEY] - API key for Geocoding API services. * @param {String} [config.useRestrictedKeys.DIRECTIONS_API_KEY] - API key for Directions API services. * @param {String} [config.useRestrictedKeys.PLACES_API_KEY] - API key for Places API services. * * @param {Boolean} [config.logCache] - enables logging of messages when an item is retrieved from each LRU cache. `false` by default. * @param {Function} [config.logger] - Specify a custom logger function to render any messages the instance will produce. `console.log` bu default. * * @param {Object} [config.cacheMaxItemAges] - object containing configuration to set a custom max item age for each cache in ms. Cannot be under 1 minute or over 30 days. * @param {Number} [config.cacheMaxItemAges.reverseGeoCache] - specify max item age of reverse geocoding cache in ms. 300000 by default. * @param {Number} [config.cacheMaxItemAges.geoCache] - specify max item age of geocoding cache in ms. 300000 by default. * @param {Number} [config.cacheMaxItemAges.directionsCache] - specify max item age of directions cache in ms. 300000 by default. * @param {Number} [config.cacheMaxItemAges.nearbySearchCache] - specify max item age of nearby search cache in ms. 300000 by default. * @param {Number} [config.cacheMaxItemAges.placeDetailsCache] - specify max item age of place details cache in ms. 300000 by default. * * @param {Object} [config.cacheMaxSizes] - object containing configuration to set a custom size for each cache. * @param {Number} [config.cacheMaxSizes.reverseGeoCache] - specify max size of reverse geocoding cache. 10 by default. * @param {Number} [config.cacheMaxSizes.geoCache] - specify max size of geocoding cache. 10 by default. * @param {Number} [config.cacheMaxSizes.directionsCache] - specify max size of directions cache. 10 by default. * @param {Number} [config.cacheMaxSizes.nearbySearchCache] - specify max size of nearby search cache. 10 by default. * @param {Number} [config.cacheMaxSizes.placeDetailsCache] - specify max size of place details cache. 10 by default. */ constructor(config) { const { value, error } = Joi.validate(config, mapwrapConfigModel); if (error) { // console.log(error); throw new Error(error.message); } const { DEFAULT_API_KEY, logCache, logger, useRestrictedKeys, cacheMaxItemAges, cacheMaxSizes } = value; this._keys = { GEOCODING_API_KEY: useRestrictedKeys.GEOCODING_API_KEY || DEFAULT_API_KEY, DIRECTIONS_API_KEY: useRestrictedKeys.DIRECTIONS_API_KEY || DEFAULT_API_KEY, PLACES_API_KEY: useRestrictedKeys.PLACES_API_KEY || DEFAULT_API_KEY }; this._googleMaps = axios.create({ baseURL: "https://maps.googleapis.com/maps/api" }); this._reverseGeoCache = new LRUCache(cacheMaxSizes.reverseGeoCache, cacheMaxItemAges.reverseGeoCache); this._geoCache = new LRUCache(cacheMaxSizes.geoCache, cacheMaxItemAges.geoCache); this._directionsCache = new LRUCache(cacheMaxSizes.directionsCache, cacheMaxItemAges.directionsCache); this._nearbySearchCache = new LRUCache(cacheMaxSizes.nearbySearchCache, cacheMaxItemAges.nearbySearchCache); this._placeDetailsCache = new LRUCache(cacheMaxSizes.placeDetailsCache, cacheMaxItemAges.placeDetailsCache); this._logCache = logCache; this._logger = logger; } /** * Returns a GeoWrapper object encapsulating the Google API response. * https://developers.google.com/maps/documentation/geocoding/intro * @param {String} address - Address to search * @returns {Promise<GeoWrapper>} - returns a GeoWrapper instance. */ async geocode(address) { if (typeof address !== "string") throw new TypeError("`address` must be a string"); const data = this._geoCache.get(address); if (!data) { try { const response = await this._googleMaps.get(`/geocode/json?address=${address.replace(/#/g, '')}&key=${this._keys.GEOCODING_API_KEY}`); const initData = new GeoWrapper(response.data); this._geoCache.set(address, initData); return initData; } catch (e) { throw new Error(e.message); } } else { if (this._logCache) { this._logger("MapWrap.geocode: Cache hit."); } return data; } } /** * Returns a ReverseGeoWrapper object encapsulating the Google API response. * https://developers.google.com/maps/documentation/geocoding/intro * @param {Number} lat - latitude * @param {Number} lng - longitude * @returns {Promise<ReverseGeoWrapper>} - returns a ReverseGeoWrapper instance. */ async reverseGeocode(lat, lng) { if (typeof lat !== "number" || typeof lng !== "number") throw new TypeError("lat and lng must be a Number"); let data = this._reverseGeoCache.get(`${lat},${lng}`); if (!data) { try { const response = await this._googleMaps.get(`/geocode/json?latlng=${lat},${lng}&key=${this._keys.GEOCODING_API_KEY}`); const initData = new ReverseGeoWrapper(response.data); this._reverseGeoCache.set(`${lat},${lng}`, initData); return initData; } catch (e) { throw new Error(e.message); } } else { if (this._logCache) { this._logger("MapWrap.reverseGeocode: Cache hit."); } return data; } } /** * Returns a DirectionsWrapper object encapsulating the Google API response. * @param {Object} params - object literal of options. * @param {String} params.origin - Address of origin * @param {String} params.destination - Address of destination * @param {String} [params.mode] - Optional. Refer to Google Direction API docs for valid modes of transport. `"driving"` by default. * https://developers.google.com/maps/documentation/directions/intro#TravelModes * @param {String} [params.units] - Optional. Can be `"imperial"` or `"metric"`. `"imperial"` by default. * @param {Boolean} [params.altRoutes] - Optional. If `true`, search for alternatives routes. `false` by default. * @param {Boolean} [params.avoidFerries] - Optional. If `true`, avoid ferries. `false` by default. * @param {Boolean} [params.avoidHighways] - Optional. If `true`, avoid highways. `false` by default. * @param {Boolean} [params.avoidIndoor] - Optional. If `true`, avoid indoor. `false` by default. * @param {Boolean} [params.avoidTolls] - Optional. If `true`, avoid tolls. `false` by default. * @returns {Promise<DirectionsWrapper>} - returns a DirectionWrapper instance. */ async directions(params) { const { value, error } = Joi.validate(params, directionsModel); if (error) { // error.details.map(e => console.log(e.message)); throw new Error(error.message); } if (value.avoidIndoor && value.mode !== "walking") throw new Error("`avoidIndoor` only allowed if mode is `walking`"); let { origin, destination, altRoutes, avoidFerries, avoidHighways, avoidIndoor, avoidTolls, mode, units } = value; let avoidArr = []; if (avoidTolls) avoidArr.push("tolls"); if (avoidHighways) avoidArr.push("highways"); if (avoidFerries) avoidArr.push("ferries"); if (avoidIndoor) avoidArr.push("indoor"); const avoidStr = avoidArr.join("|"); const mapsParams = { params: { origin: origin.replace(/#/g, ''), // regex eliminates invalid characters when making http request destination: destination.replace(/#/g, ''), mode: mode || "driving", alternatives: altRoutes || false, units: units || "imperial", ...(avoidStr && { avoid: avoidStr }), key: this._keys.DIRECTIONS_API_KEY } }; const buildCacheKey = `${origin}&${destination}&${mode || "driving"}&${altRoutes || false}&${units || "imperial"}&${avoidStr || "null"}`; let data = this._directionsCache.get(buildCacheKey); if (!data) { try { const response = await this._googleMaps.get("/directions/json", mapsParams); // wrap data // cache it with buildCacheKey const initData = new DirectionsWrapper(response.data); this._directionsCache.set(buildCacheKey, initData); // return wrapped data return initData; } catch (e) { throw new Error(e.message); } } else { if (this._logCache) { this._logger("MapWrap.directions: Cache hit."); } return data; } } /** * Returns a PlaceSearchWrapper object encapsulating the Google API response. * @param {Object} params - object literal of options. * @param {Object} params.location - Specify coordinate to serve as a center to look from in the format of `{ lat: Number, lng: Number }` * @param {Number} params.radius - Radius in meters. Must be no less than 1 and no more than 50000. * @param {String} [params.keyword] - Optional. Specify a phrase and look for * @param {String} [params.type] - Optional. Filter only for locations matching the specified type string. * String type from this list: https://developers.google.com/places/web-service/supported_types * @param {Number} [params.minprice] - Optional. Number from -1 to 4 specifying the min price range of the location. `-1` or `undefined` to disable. Must be less than `maxprice`. * @param {Number} [params.maxprice] - Optional. Number from -1 to 4 specifying the max price range of the location. `-1` or `undefined` to disable. * Info on price ranges: https://developers.google.com/places/web-service/search * @returns {Promise<PlaceSearchWrapper>} - returns a PlaceSearchWrapper instance. */ async nearbySearchPlaces(params) { const { value, error } = Joi.validate(params, nearbySearchModel); if (value.minprice > value.maxprice) { throw new Error("minprice excees maxprice"); } if (error) { // error.details.map(e => console.log(e.message)); throw new Error(error.message); } let { location, radius, keyword, type, minprice, maxprice } = value; const placesParams = { params: { location: `${location.lat},${location.lng}`, radius, ...(keyword && { keyword }), ...(type && { type }), ...(minprice !== -1 && minprice !== undefined && { minprice }), ...(maxprice !== -1 && maxprice !== undefined && { maxprice }), key: this._keys.PLACES_API_KEY } }; const buildCacheKey = `${location.lat},${location.lng}&${radius}&${keyword || ""}&${type || "null"}&${minprice || -1}&${maxprice || -1}`; let data = this._nearbySearchCache.get(buildCacheKey); if (!data) { try { const response = await this._googleMaps.get(`/place/nearbysearch/json`, placesParams); // wrap data // cache it with buildCacheKey const initData = new PlaceSearchWrapper(response.data); this._nearbySearchCache.set(buildCacheKey, initData); // return wrapped data return initData; } catch (e) { throw new Error(e.message); } } else { if (this._logCache) { this._logger("MapWrap.nearbySearchPlaces: Cache hit."); } return data; } } /** * Returns a PlaceSearchWrapper object encapsulating the Google API response given a page token. Does not cache results. * @param {String} nextPageToken - Page token acquired from Google API response. https://developers.google.com/places/web-service/search#PlaceSearchPaging * @returns {Promise<PlaceSearchWrapper>} - returns a PlaceSearchWrapper instance. */ async additionalPlaces(nextPageToken = null) { if (!nextPageToken || typeof nextPageToken !== "string") { throw new TypeError("`nextPageToken` must be of type `string`"); } const placesParams = { params: { pagetoken: nextPageToken, key: this._keys.PLACES_API_KEY } }; try { const response = await this._googleMaps.get(`/place/nearbysearch/json`, placesParams); return new PlaceSearchWrapper(response.data); } catch (e) { throw new Error(e.message); } } /** * Returns an object containing place details. * @param {String} placeId - Place ID acquired from Google API response. https://developers.google.com/places/web-service/details * @returns {Promise<Object>} - returns an object containing the resulting object with the place details. */ async placeDetails(placeId) { if (!placeId || typeof placeId !== "string") { throw new TypeError("`placeId` must be of type `string`"); } let data = this._placeDetailsCache.get(placeId); if (!data) { try { const response = await this._googleMaps.get(`/place/details/json?placeid=${placeId}&key=${this._keys.PLACES_API_KEY}`); this._placeDetailsCache.set(placeId, response.data); return response.data; } catch (e) { throw new Error(e.message); } } else { if (this._logCache) { this._logger("MapWrap.placeDetails: Cache hit."); } return data; } } } module.exports = MapWrap;