UNPKG

@vtmap/vtmap-sdk-js

Version:

JS SDK for accessing Viettelmaps APIs

1,770 lines (1,549 loc) 160 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.vtmapSdk = factory()); }(this, (function () { 'use strict'; // Like https://github.com/thlorenz/lib/parse-link-header but without any // additional dependencies. function parseParam(param) { var parts = param.match(/\s*(.+)\s*=\s*"?([^"]+)"?/); if (!parts) return null; return { key: parts[1], value: parts[2] }; } function parseLink(link) { var parts = link.match(/<?([^>]*)>(.*)/); if (!parts) return null; var linkUrl = parts[1]; var linkParams = parts[2].split(';'); var rel = null; var parsedLinkParams = linkParams.reduce(function(result, param) { var parsed = parseParam(param); if (!parsed) return result; if (parsed.key === 'rel') { if (!rel) { rel = parsed.value; } return result; } result[parsed.key] = parsed.value; return result; }, {}); if (!rel) return null; return { url: linkUrl, rel: rel, params: parsedLinkParams }; } /** * Parse a Link header. * * @param {string} linkHeader * @returns {{ * [string]: { * url: string, * params: { [string]: string } * } * }} */ function parseLinkHeader(linkHeader) { if (!linkHeader) return {}; return linkHeader.split(/,\s*</).reduce(function(result, link) { var parsed = parseLink(link); if (!parsed) return result; // rel value can be multiple whitespace-separated rels. var splitRel = parsed.rel.split(/\s+/); splitRel.forEach(function(rel) { if (!result[rel]) { result[rel] = { url: parsed.url, params: parsed.params }; } }); return result; }, {}); } var parseLinkHeader_1 = parseLinkHeader; /** * A Mapbox API response. * * @class MapiResponse * @property {Object} body - The response body, parsed as JSON. * @property {string} rawBody - The raw response body. * @property {number} statusCode - The response's status code. * @property {Object} headers - The parsed response headers. * @property {Object} links - The parsed response links. * @property {MapiRequest} request - The response's originating `MapiRequest`. */ /** * @ignore * @param {MapiRequest} request * @param {Object} responseData * @param {Object} responseData.headers * @param {string} responseData.body * @param {number} responseData.statusCode */ function MapiResponse(request, responseData) { this.request = request; this.headers = responseData.headers; this.rawBody = responseData.body; this.statusCode = responseData.statusCode; try { this.body = JSON.parse(responseData.body || '{}'); } catch (parseError) { this.body = responseData.body; } this.links = parseLinkHeader_1(this.headers.link); } /** * Check if there is a next page that you can fetch. * * @returns {boolean} */ MapiResponse.prototype.hasNextPage = function hasNextPage() { return !!this.links.next; }; /** * Create a request for the next page, if there is one. * If there is no next page, returns `null`. * * @returns {MapiRequest | null} */ MapiResponse.prototype.nextPage = function nextPage() { if (!this.hasNextPage()) return null; return this.request._extend({ path: this.links.next.url }); }; var mapiResponse = MapiResponse; var constants = { API_ORIGIN: 'https://api.mapbox.com', EVENT_PROGRESS_DOWNLOAD: 'downloadProgress', EVENT_PROGRESS_UPLOAD: 'uploadProgress', EVENT_ERROR: 'error', EVENT_RESPONSE: 'response', ERROR_HTTP: 'HttpError', ERROR_REQUEST_ABORTED: 'RequestAbortedError' }; /** * A Mapbox API error. * * If there's an error during the API transaction, * the Promise returned by `MapiRequest`'s [`send`](#send) * method should reject with a `MapiError`. * * @class MapiError * @hideconstructor * @property {MapiRequest} request - The errored request. * @property {string} type - The type of error. Usually this is `'HttpError'`. * If the request was aborted, so the error was * not sent from the server, the type will be * `'RequestAbortedError'`. * @property {number} [statusCode] - The numeric status code of * the HTTP response. * @property {Object | string} [body] - If the server sent a response body, * this property exposes that response, parsed as JSON if possible. * @property {string} [message] - Whatever message could be derived from the * call site and HTTP response. * * @param {MapiRequest} options.request * @param {number} [options.statusCode] * @param {string} [options.body] * @param {string} [options.message] * @param {string} [options.type] */ function MapiError(options) { var errorType = options.type || constants.ERROR_HTTP; var body; if (options.body) { try { body = JSON.parse(options.body); } catch (e) { body = options.body; } } else { body = null; } var message = options.message || null; if (!message) { if (typeof body === 'string') { message = body; } else if (body && typeof body.message === 'string') { message = body.message; } else if (errorType === constants.ERROR_REQUEST_ABORTED) { message = 'Request aborted'; } } this.message = message; this.type = errorType; this.statusCode = options.statusCode || null; this.request = options.request; this.body = body; } var mapiError = MapiError; function parseSingleHeader(raw) { var boundary = raw.indexOf(':'); var name = raw .substring(0, boundary) .trim() .toLowerCase(); var value = raw.substring(boundary + 1).trim(); return { name: name, value: value }; } /** * Parse raw headers into an object with lowercase properties. * Does not fully parse headings into more complete data structure, * as larger libraries might do. Also does not deal with duplicate * headers because Node doesn't seem to deal with those well, so * we shouldn't let the browser either, for consistency. * * @param {string} raw * @returns {Object} */ function parseHeaders(raw) { var headers = {}; if (!raw) { return headers; } raw .trim() .split(/[\r|\n]+/) .forEach(function(rawHeader) { var parsed = parseSingleHeader(rawHeader); headers[parsed.name] = parsed.value; }); return headers; } var parseHeaders_1 = parseHeaders; // Keys are request IDs, values are XHRs. var requestsUnderway = {}; function browserAbort(request) { var xhr = requestsUnderway[request.id]; if (!xhr) return; xhr.abort(); delete requestsUnderway[request.id]; } function createResponse(request, xhr) { return new mapiResponse(request, { body: xhr.response, headers: parseHeaders_1(xhr.getAllResponseHeaders()), statusCode: xhr.status }); } function normalizeBrowserProgressEvent(event) { var total = event.total; var transferred = event.loaded; var percent = (100 * transferred) / total; return { total: total, transferred: transferred, percent: percent }; } function sendRequestXhr(request, xhr) { return new Promise(function(resolve, reject) { xhr.onprogress = function(event) { request.emitter.emit( constants.EVENT_PROGRESS_DOWNLOAD, normalizeBrowserProgressEvent(event) ); }; var file = request.file; if (file) { xhr.upload.onprogress = function(event) { request.emitter.emit( constants.EVENT_PROGRESS_UPLOAD, normalizeBrowserProgressEvent(event) ); }; } xhr.onerror = function(error) { reject(error); }; xhr.onabort = function() { var mapiError$$1 = new mapiError({ request: request, type: constants.ERROR_REQUEST_ABORTED }); reject(mapiError$$1); }; xhr.onload = function() { delete requestsUnderway[request.id]; if (xhr.status < 200 || xhr.status >= 400) { var mapiError$$1 = new mapiError({ request: request, body: xhr.response, statusCode: xhr.status }); reject(mapiError$$1); return; } resolve(xhr); }; var body = request.body; // matching service needs to send a www-form-urlencoded request if (typeof body === 'string') { xhr.send(body); } else if (body) { xhr.send(JSON.stringify(body)); } else if (file) { xhr.send(file); } else { xhr.send(); } requestsUnderway[request.id] = xhr; }).then(function(xhr) { return createResponse(request, xhr); }); } // The accessToken argument gives this function flexibility // for Mapbox's internal client. function createRequestXhr(request, accessToken) { var url = request.url(accessToken); var xhr = new window.XMLHttpRequest(); xhr.open(request.method, url); Object.keys(request.headers).forEach(function(key) { xhr.setRequestHeader(key, request.headers[key]); }); return xhr; } function browserSend(request) { return Promise.resolve().then(function() { var xhr = createRequestXhr(request, request.client.accessToken); return sendRequestXhr(request, xhr); }); } var browserLayer = { browserAbort: browserAbort, sendRequestXhr: sendRequestXhr, browserSend: browserSend, createRequestXhr: createRequestXhr }; var immutable = extend; var hasOwnProperty = Object.prototype.hasOwnProperty; function extend() { var target = {}; for (var i = 0; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target } var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var eventemitter3 = createCommonjsModule(function (module) { var has = Object.prototype.hasOwnProperty , prefix = '~'; /** * Constructor to create a storage for our `EE` objects. * An `Events` instance is a plain object whose properties are event names. * * @constructor * @private */ function Events() {} // // We try to not inherit from `Object.prototype`. In some engines creating an // instance in this way is faster than calling `Object.create(null)` directly. // If `Object.create(null)` is not supported we prefix the event names with a // character to make sure that the built-in object properties are not // overridden or used as an attack vector. // if (Object.create) { Events.prototype = Object.create(null); // // This hack is needed because the `__proto__` property is still inherited in // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. // if (!new Events().__proto__) prefix = false; } /** * Representation of a single event listener. * * @param {Function} fn The listener function. * @param {*} context The context to invoke the listener with. * @param {Boolean} [once=false] Specify if the listener is a one-time listener. * @constructor * @private */ function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } /** * Add a listener for a given event. * * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} context The context to invoke the listener with. * @param {Boolean} once Specify if the listener is a one-time listener. * @returns {EventEmitter} * @private */ function addListener(emitter, event, fn, context, once) { if (typeof fn !== 'function') { throw new TypeError('The listener must be a function'); } var listener = new EE(fn, context || emitter, once) , evt = prefix ? prefix + event : event; if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); else emitter._events[evt] = [emitter._events[evt], listener]; return emitter; } /** * Clear event by name. * * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. * @param {(String|Symbol)} evt The Event name. * @private */ function clearEvent(emitter, evt) { if (--emitter._eventsCount === 0) emitter._events = new Events(); else delete emitter._events[evt]; } /** * Minimal `EventEmitter` interface that is molded against the Node.js * `EventEmitter` interface. * * @constructor * @public */ function EventEmitter() { this._events = new Events(); this._eventsCount = 0; } /** * Return an array listing the events for which the emitter has registered * listeners. * * @returns {Array} * @public */ EventEmitter.prototype.eventNames = function eventNames() { var names = [] , events , name; if (this._eventsCount === 0) return names; for (name in (events = this._events)) { if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); } if (Object.getOwnPropertySymbols) { return names.concat(Object.getOwnPropertySymbols(events)); } return names; }; /** * Return the listeners registered for a given event. * * @param {(String|Symbol)} event The event name. * @returns {Array} The registered listeners. * @public */ EventEmitter.prototype.listeners = function listeners(event) { var evt = prefix ? prefix + event : event , handlers = this._events[evt]; if (!handlers) return []; if (handlers.fn) return [handlers.fn]; for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { ee[i] = handlers[i].fn; } return ee; }; /** * Return the number of listeners listening to a given event. * * @param {(String|Symbol)} event The event name. * @returns {Number} The number of listeners. * @public */ EventEmitter.prototype.listenerCount = function listenerCount(event) { var evt = prefix ? prefix + event : event , listeners = this._events[evt]; if (!listeners) return 0; if (listeners.fn) return 1; return listeners.length; }; /** * Calls each of the listeners registered for a given event. * * @param {(String|Symbol)} event The event name. * @returns {Boolean} `true` if the event had listeners, else `false`. * @public */ EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return false; var listeners = this._events[evt] , len = arguments.length , args , i; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length , j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; default: if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); } } } return true; }; /** * Add a listener for a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.on = function on(event, fn, context) { return addListener(this, event, fn, context, false); }; /** * Add a one-time listener for a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.once = function once(event, fn, context) { return addListener(this, event, fn, context, true); }; /** * Remove the listeners of a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn Only remove the listeners that match this function. * @param {*} context Only remove the listeners that have this context. * @param {Boolean} once Only remove one-time listeners. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return this; if (!fn) { clearEvent(this, evt); return this; } var listeners = this._events[evt]; if (listeners.fn) { if ( listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context) ) { clearEvent(this, evt); } } else { for (var i = 0, events = [], length = listeners.length; i < length; i++) { if ( listeners[i].fn !== fn || (once && !listeners[i].once) || (context && listeners[i].context !== context) ) { events.push(listeners[i]); } } // // Reset the array, or remove it completely if we have no more listeners. // if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; else clearEvent(this, evt); } return this; }; /** * Remove all listeners, or those of the specified event. * * @param {(String|Symbol)} [event] The event name. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { var evt; if (event) { evt = prefix ? prefix + event : event; if (this._events[evt]) clearEvent(this, evt); } else { this._events = new Events(); this._eventsCount = 0; } return this; }; // // Alias methods names because people roll like that. // EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.addListener = EventEmitter.prototype.on; // // Expose the prefix. // EventEmitter.prefixed = prefix; // // Allow `EventEmitter` to be imported as module namespace. // EventEmitter.EventEmitter = EventEmitter; // // Expose the module. // { module.exports = EventEmitter; } }); // Encode each item of an array individually. The comma // delimiters should not themselves be encoded. function encodeArray(arrayValue) { return arrayValue.map(encodeURIComponent).join(','); } function encodeValue(value) { if (Array.isArray(value)) { return encodeArray(value); } return encodeURIComponent(String(value)); } /** * Append a query parameter to a URL. * * @param {string} url * @param {string} key * @param {string|number|boolean|Array<*>>} [value] - Provide an array * if the value is a list and commas between values need to be * preserved, unencoded. * @returns {string} - Modified URL. */ function appendQueryParam(url, key, value) { if (value === false || value === null) { return url; } var punctuation = /\?/.test(url) ? '&' : '?'; var query = encodeURIComponent(key); if (value !== undefined && value !== '' && value !== true) { query += '=' + encodeValue(value); } return '' + url + punctuation + query; } /** * Derive a query string from an object and append it * to a URL. * * @param {string} url * @param {Object} [queryObject] - Values should be primitives. * @returns {string} - Modified URL. */ function appendQueryObject(url, queryObject) { if (!queryObject) { return url; } var result = url; Object.keys(queryObject).forEach(function(key) { var value = queryObject[key]; if (value === undefined) { return; } if (Array.isArray(value)) { value = value .filter(function(v) { return v !== null && v !== undefined; }) .join(','); } result = appendQueryParam(result, key, value); }); return result; } /** * Prepend an origin to a URL. If the URL already has an * origin, do nothing. * * @param {string} url * @param {string} origin * @returns {string} - Modified URL. */ function prependOrigin(url, origin) { if (!origin) { return url; } if (url.slice(0, 4) === 'http') { return url; } var delimiter = url[0] === '/' ? '' : '/'; return '' + origin.replace(/\/$/, '') + delimiter + url; } /** * Interpolate values into a route with express-style, * colon-prefixed route parameters. * * @param {string} route * @param {Object} [params] - Values should be primitives * or arrays of primitives. Provide an array if the value * is a list and commas between values need to be * preserved, unencoded. * @returns {string} - Modified URL. */ function interpolateRouteParams(route, params) { if (!params) { return route; } return route.replace(/\/:([a-zA-Z0-9]+)/g, function(_, paramId) { var value = params[paramId]; if (value === undefined) { throw new Error('Unspecified route parameter ' + paramId); } var preppedValue = encodeValue(value); return '/' + preppedValue; }); } var urlUtils = { appendQueryObject: appendQueryObject, appendQueryParam: appendQueryParam, prependOrigin: prependOrigin, interpolateRouteParams: interpolateRouteParams }; var requestId = 1; /** * A Mapbox API request. * * Note that creating a `MapiRequest` does *not* send the request automatically. * Use the request's `send` method to send it off and get a `Promise`. * * The `emitter` property is an `EventEmitter` that emits the following events: * * - `'response'` - Listeners will be called with a `MapiResponse`. * - `'error'` - Listeners will be called with a `MapiError`. * - `'downloadProgress'` - Listeners will be called with `ProgressEvents`. * - `'uploadProgress'` - Listeners will be called with `ProgressEvents`. * Upload events are only available when the request includes a file. * * @class MapiRequest * @property {EventEmitter} emitter - An event emitter. See above. * @property {MapiClient} client - This request's `MapiClient`. * @property {MapiResponse|null} response - If this request has been sent and received * a response, the response is available on this property. * @property {MapiError|Error|null} error - If this request has been sent and * received an error in response, the error is available on this property. * @property {boolean} aborted - If the request has been aborted * (via [`abort`](#abort)), this property will be `true`. * @property {boolean} sent - If the request has been sent, this property will * be `true`. You cannot send the same request twice, so if you need to create * a new request that is the equivalent of an existing one, use * [`clone`](#clone). * @property {string} path - The request's path, including colon-prefixed route * parameters. * @property {string} origin - The request's origin. * @property {string} method - The request's HTTP method. * @property {Object} query - A query object, which will be transformed into * a URL query string. * @property {Object} params - A route parameters object, whose values will * be interpolated the path. * @property {Object} headers - The request's headers. * @property {Object|string|null} body - Data to send with the request. * If the request has a body, it will also be sent with the header * `'Content-Type: application/json'`. * @property {Blob|ArrayBuffer|string|ReadStream} file - A file to * send with the request. The browser client accepts Blobs and ArrayBuffers; * the Node client accepts strings (filepaths) and ReadStreams. * @property {string} encoding - The encoding of the response. * @property {string} sendFileAs - The method to send the `file`. Options are * `data` (x-www-form-urlencoded) or `form` (multipart/form-data). */ /** * @ignore * @param {MapiClient} client * @param {Object} options * @param {string} options.method * @param {string} options.path * @param {Object} [options.query={}] * @param {Object} [options.params={}] * @param {string} [options.origin] * @param {Object} [options.headers] * @param {Object} [options.body=null] * @param {Blob|ArrayBuffer|string|ReadStream} [options.file=null] * @param {string} [options.encoding=utf8] */ function MapiRequest(client, options) { if (!client) { throw new Error('MapiRequest requires a client'); } if (!options || !options.path || !options.method) { throw new Error( 'MapiRequest requires an options object with path and method properties' ); } var defaultHeaders = {}; if (options.body) { defaultHeaders['content-type'] = 'application/json'; } var headersWithDefaults = immutable(defaultHeaders, options.headers); // Disallows duplicate header names of mixed case, // e.g. Content-Type and content-type. var headers = Object.keys(headersWithDefaults).reduce(function(memo, name) { memo[name.toLowerCase()] = headersWithDefaults[name]; return memo; }, {}); this.id = requestId++; this._options = options; this.emitter = new eventemitter3(); this.client = client; this.response = null; this.error = null; this.sent = false; this.aborted = false; this.path = options.path; this.method = options.method; this.origin = options.origin || client.origin; this.query = options.query || {}; this.params = options.params || {}; this.body = options.body || null; this.file = options.file || null; this.encoding = options.encoding || 'utf8'; this.sendFileAs = options.sendFileAs || null; this.headers = headers; } /** * Get the URL of the request. * * @param {string} [accessToken] - By default, the access token of the request's * client is used. * @return {string} */ MapiRequest.prototype.url = function url(accessToken) { var url = urlUtils.prependOrigin(this.path, this.origin); url = urlUtils.appendQueryObject(url, this.query); var routeParams = this.params; var actualAccessToken = accessToken == null ? this.client.accessToken : accessToken; if (actualAccessToken) { url = urlUtils.appendQueryParam(url, 'access_token', actualAccessToken); var accessTokenOwnerId = actualAccessToken; routeParams = immutable({ ownerId: accessTokenOwnerId }, routeParams); } url = urlUtils.interpolateRouteParams(url, routeParams); return url; }; /** * Send the request. Returns a Promise that resolves with a `MapiResponse`. * You probably want to use `response.body`. * * `send` only retrieves the first page of paginated results. You can get * the next page by using the `MapiResponse`'s [`nextPage`](#nextpage) * function, or iterate through all pages using [`eachPage`](#eachpage) * instead of `send`. * * @returns {Promise<MapiResponse>} */ MapiRequest.prototype.send = function send() { var self = this; if (self.sent) { throw new Error( 'This request has already been sent. Check the response and error properties. Create a new request with clone().' ); } self.sent = true; return self.client.sendRequest(self).then( function(response) { self.response = response; self.emitter.emit(constants.EVENT_RESPONSE, response); return response; }, function(error) { self.error = error; self.emitter.emit(constants.EVENT_ERROR, error); throw error; } ); }; /** * Abort the request. * * Any pending `Promise` returned by [`send`](#send) will be rejected with * an error with `type: 'RequestAbortedError'`. If you've created a request * that might be aborted, you need to catch and handle such errors. * * This method will also abort any requests created while fetching subsequent * pages via [`eachPage`](#eachpage). * * If the request has not been sent or has already been aborted, nothing * will happen. */ MapiRequest.prototype.abort = function abort() { if (this._nextPageRequest) { this._nextPageRequest.abort(); delete this._nextPageRequest; } if (this.response || this.error || this.aborted) return; this.aborted = true; this.client.abortRequest(this); }; /** * Invoke a callback for each page of a paginated API response. * * The callback should have the following signature: * * ```js * ( * error: MapiError, * response: MapiResponse, * next: () => void * ) => void * ``` * * **The next page will not be fetched until you've invoked the * `next` callback**, indicating that you're ready for it. * * @param {Function} callback */ MapiRequest.prototype.eachPage = function eachPage(callback) { var self = this; function handleResponse(response) { function getNextPage() { delete self._nextPageRequest; var nextPageRequest = response.nextPage(); if (nextPageRequest) { self._nextPageRequest = nextPageRequest; getPage(nextPageRequest); } } callback(null, response, getNextPage); } function handleError(error) { callback(error, null, function() {}); } function getPage(request) { request.send().then(handleResponse, handleError); } getPage(this); }; /** * Clone this request. * * Each request can only be sent *once*. So if you'd like to send the * same request again, clone it and send away. * * @returns {MapiRequest} - A new `MapiRequest` configured just like this one. */ MapiRequest.prototype.clone = function clone() { return this._extend(); }; /** * @ignore */ MapiRequest.prototype._extend = function _extend(options) { var extendedOptions = immutable(this._options, options); return new MapiRequest(this.client, extendedOptions); }; var mapiRequest = MapiRequest; /** * A low-level Mapbox API client. Use it to create service clients * that share the same configuration. * * Services and `MapiRequest`s use the underlying `MapiClient` to * determine how to create, send, and abort requests in a way * that is appropriate to the configuration and environment * (Node or the browser). * * @class MapiClient * @property {string} accessToken - The Mapbox access token assigned * to this client. * @property {string} [origin] - The origin * to use for API requests. Defaults to https://api.mapbox.com. */ function MapiClient(options) { if (!options || !options.accessToken) { throw new Error('Cannot create a client without an access token'); } this.accessToken = options.accessToken; this.origin = options.origin || constants.API_ORIGIN; } MapiClient.prototype.createRequest = function createRequest(requestOptions) { return new mapiRequest(this, requestOptions); }; var mapiClient = MapiClient; function BrowserClient(options) { mapiClient.call(this, options); } BrowserClient.prototype = Object.create(mapiClient.prototype); BrowserClient.prototype.constructor = BrowserClient; BrowserClient.prototype.sendRequest = browserLayer.browserSend; BrowserClient.prototype.abortRequest = browserLayer.browserAbort; /** * Create a client for the browser. * * @param {Object} options * @param {string} options.accessToken * @param {string} [options.origin] * @returns {MapiClient} */ function createBrowserClient(options) { return new BrowserClient(options); } var browserClient = createBrowserClient; var toString = Object.prototype.toString; var isPlainObj = function (x) { var prototype; return toString.call(x) === '[object Object]' && (prototype = Object.getPrototypeOf(x), prototype === null || prototype === Object.getPrototypeOf({})); }; /** * Validators are functions which assert certain type. * They can return a string which can then be used * to display a helpful error message. * They can also return a function for a custom error message. */ var DEFAULT_ERROR_PATH = 'value'; var NEWLINE_INDENT = '\n '; var v = {}; /** * Runners * * Take root validators and run assertion */ v.assert = function(rootValidator, options) { options = options || {}; return function(value) { var message = validate(rootValidator, value); // all good if (!message) { return; } var errorMessage = processMessage(message, options); if (options.apiName) { errorMessage = options.apiName + ': ' + errorMessage; } throw new Error(errorMessage); }; }; /** * Higher Order Validators * * validators which take other validators as input * and output a new validator */ v.shape = function shape(validatorObj) { var validators = objectEntries(validatorObj); return function shapeValidator(value) { var validationResult = validate(v.plainObject, value); if (validationResult) { return validationResult; } var key, validator; var errorMessages = []; for (var i = 0; i < validators.length; i++) { key = validators[i].key; validator = validators[i].value; validationResult = validate(validator, value[key]); if (validationResult) { // return [key].concat(validationResult); errorMessages.push([key].concat(validationResult)); } } if (errorMessages.length < 2) { return errorMessages[0]; } // enumerate all the error messages return function(options) { errorMessages = errorMessages.map(function(message) { var key = message[0]; var renderedMessage = processMessage(message, options) .split('\n') .join(NEWLINE_INDENT); // indents any inner nesting return '- ' + key + ': ' + renderedMessage; }); var objectId = options.path.join('.'); var ofPhrase = objectId === DEFAULT_ERROR_PATH ? '' : ' of ' + objectId; return ( 'The following properties' + ofPhrase + ' have invalid values:' + NEWLINE_INDENT + errorMessages.join(NEWLINE_INDENT) ); }; }; }; v.strictShape = function strictShape(validatorObj) { var shapeValidator = v.shape(validatorObj); return function strictShapeValidator(value) { var shapeResult = shapeValidator(value); if (shapeResult) { return shapeResult; } var invalidKeys = Object.keys(value).reduce(function(memo, valueKey) { if (validatorObj[valueKey] === undefined) { memo.push(valueKey); } return memo; }, []); if (invalidKeys.length !== 0) { return function() { return 'The following keys are invalid: ' + invalidKeys.join(', '); }; } }; }; v.arrayOf = function arrayOf(validator) { return createArrayValidator(validator); }; v.tuple = function tuple() { var validators = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments); return createArrayValidator(validators); }; // Currently array validation fails when the first invalid item is found. function createArrayValidator(validators) { var validatingTuple = Array.isArray(validators); var getValidator = function(index) { if (validatingTuple) { return validators[index]; } return validators; }; return function arrayValidator(value) { var validationResult = validate(v.plainArray, value); if (validationResult) { return validationResult; } if (validatingTuple && value.length !== validators.length) { return 'an array with ' + validators.length + ' items'; } for (var i = 0; i < value.length; i++) { validationResult = validate(getValidator(i), value[i]); if (validationResult) { return [i].concat(validationResult); } } }; } v.required = function required(validator) { function requiredValidator(value) { if (value == null) { return function(options) { return formatErrorMessage( options, isArrayCulprit(options.path) ? 'cannot be undefined/null.' : 'is required.' ); }; } return validator.apply(this, arguments); } requiredValidator.__required = true; return requiredValidator; }; v.oneOfType = function oneOfType() { var validators = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments); return function oneOfTypeValidator(value) { var messages = validators .map(function(validator) { return validate(validator, value); }) .filter(Boolean); // If we don't have as many messages as no. of validators, // then at least one validator was ok with the value. if (messages.length !== validators.length) { return; } // check primitive type if ( messages.every(function(message) { return message.length === 1 && typeof message[0] === 'string'; }) ) { return orList( messages.map(function(m) { return m[0]; }) ); } // Complex oneOfTypes like // `v.oneOftypes(v.shape({name: v.string})`, `v.shape({name: v.number}))` // are complex ¯\_(ツ)_/¯. For the current scope only returning the longest message. return messages.reduce(function(max, arr) { return arr.length > max.length ? arr : max; }); }; }; /** * Meta Validators * which take options as argument (not validators) * and return a new primitive validator */ v.equal = function equal(compareWith) { return function equalValidator(value) { if (value !== compareWith) { return JSON.stringify(compareWith); } }; }; v.oneOf = function oneOf() { var options = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments); var validators = options.map(function(value) { return v.equal(value); }); return v.oneOfType.apply(this, validators); }; v.range = function range(compareWith) { var min = compareWith[0]; var max = compareWith[1]; return function rangeValidator(value) { var validationResult = validate(v.number, value); if (validationResult || value < min || value > max) { return 'number between ' + min + ' & ' + max + ' (inclusive)'; } }; }; /** * Primitive validators * * simple validators which return a string or undefined */ v.any = function any() { return; }; v.boolean = function boolean(value) { if (typeof value !== 'boolean') { return 'boolean'; } }; v.number = function number(value) { if (typeof value !== 'number') { return 'number'; } }; v.plainArray = function plainArray(value) { if (!Array.isArray(value)) { return 'array'; } }; v.plainObject = function plainObject(value) { if (!isPlainObj(value)) { return 'object'; } }; v.string = function string(value) { if (typeof value !== 'string') { return 'string'; } }; v.func = function func(value) { if (typeof value !== 'function') { return 'function'; } }; function validate(validator, value) { // assertions are optional by default unless wrapped in v.require if (value == null && !validator.hasOwnProperty('__required')) { return; } var result = validator(value); if (result) { return Array.isArray(result) ? result : [result]; } } function processMessage(message, options) { // message array follows the convention // [...path, result] // path is an array of object keys / array indices // result is output of the validator var len = message.length; var result = message[len - 1]; var path = message.slice(0, len - 1); if (path.length === 0) { path = [DEFAULT_ERROR_PATH]; } options = immutable(options, { path: path }); return typeof result === 'function' ? result(options) // allows customization of result : formatErrorMessage(options, prettifyResult(result)); } function orList(list) { if (list.length < 2) { return list[0]; } if (list.length === 2) { return list.join(' or '); } return list.slice(0, -1).join(', ') + ', or ' + list.slice(-1); } function prettifyResult(result) { return 'must be ' + addArticle(result) + '.'; } function addArticle(nounPhrase) { if (/^an? /.test(nounPhrase)) { return nounPhrase; } if (/^[aeiou]/i.test(nounPhrase)) { return 'an ' + nounPhrase; } if (/^[a-z]/i.test(nounPhrase)) { return 'a ' + nounPhrase; } return nounPhrase; } function formatErrorMessage(options, prettyResult) { var arrayCulprit = isArrayCulprit(options.path); var output = options.path.join('.') + ' ' + prettyResult; var prepend = arrayCulprit ? 'Item at position ' : ''; return prepend + output; } function isArrayCulprit(path) { return typeof path[path.length - 1] == 'number' || typeof path[0] == 'number'; } function objectEntries(obj) { return Object.keys(obj || {}).map(function(key) { return { key: key, value: obj[key] }; }); } v.validate = validate; v.processMessage = processMessage; var lib = v; function file(value) { // If we're in a browser so Blob is available, the file must be that. // In Node, however, it could be a filepath or a pipeable (Readable) stream. if (typeof window !== 'undefined') { if (value instanceof commonjsGlobal.Blob || value instanceof commonjsGlobal.ArrayBuffer) { return; } return 'Blob or ArrayBuffer'; } if (typeof value === 'string' || value.pipe !== undefined) { return; } return 'Filename or Readable stream'; } function assertShape(validatorObj, apiName) { return lib.assert(lib.strictShape(validatorObj), apiName); } function date(value) { var msg = 'date'; if (typeof value === 'boolean') { return msg; } try { var date = new Date(value); if (date.getTime && isNaN(date.getTime())) { return msg; } } catch (e) { return msg; } } function coordinates(value) { return lib.tuple(lib.number, lib.number)(value); } var validator = immutable(lib, { file: file, date: date, coordinates: coordinates, assertShape: assertShape }); /** * Create a new object by picking properties off an existing object. * The second param can be overloaded as a callback for * more fine grained picking of properties. * @param {Object} source * @param {Array<string>|function(string, Object):boolean} keys * @returns {Object} */ function pick(source, keys) { var filter = function(key, val) { return keys.indexOf(key) !== -1 && val !== undefined; }; if (typeof keys === 'function') { filter = keys; } return Object.keys(source) .filter(function(key) { return filter(key, source[key]); }) .reduce(function(result, key) { result[key] = source[key]; return result; }, {}); } var pick_1 = pick; // This will create the environment-appropriate client. function createServiceFactory(ServicePrototype) { return function(clientOrConfig) { var client; if (mapiClient.prototype.isPrototypeOf(clientOrConfig)) { client = clientOrConfig; } else { client = browserClient(clientOrConfig); } var service = Object.create(ServicePrototype); service.client = client; return service; }; } var createServiceFactory_1 = createServiceFactory; /** * Datasets API service. * * Learn more about this service and its responses in * [the HTTP service documentation](https://docs.mapbox.com/api/maps/#datasets). */ var Datasets = {}; /** * List datasets in your account. * * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#list-datasets). * * @param {Object} [config] * @param {string} [config.sortby=created] - Sort by either `modified` or `created` (default) dates. * @return {MapiRequest} * * @example * datasetsClient.listDatasets() * .send() * .then(response => { * const datasets = response.body; * }); * * @example * datasetsClient.listDatasets() * .eachPage((error, response, next) => { * // Handle error or response and call next. * }); */ Datasets.listDatasets = function(config) { validator.assertShape({ sortby: validator.oneOf('created', 'modified') })(config); return this.client.createRequest({ method: 'GET', path: '/datasets/v1/:ownerId', query: config ? pick_1(config, ['sortby']) : {} }); }; /** * Create a new, empty dataset. * * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#create-a-dataset). * * @param {Object} config * @param {string} [config.name] * @param {string} [config.description] * @return {MapiRequest} * * @example * datasetsClient.createDataset({ * name: 'example', * description: 'An example dataset' * }) * .send() * .then(response => { * const datasetMetadata = response.body; * }); */ Datasets.createDataset = function(config) { validator.assertShape({ name: validator.string, description: validator.string })(config); return this.client.createRequest({ method: 'POST', path: '/datasets/v1/:ownerId', body: config }); }; /** * Get metadata about a dataset. * * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#retrieve-a-dataset). * * @param {Object} config * @param {string} config.datasetId * @return {MapiRequest} * * @example * datasetsClient.getMetadata({ * datasetId: 'dataset-id' * }) * .send() * .then(response => { * const datasetMetadata = response.body; * }) */ Datasets.getMetadata = function(config) { validator.assertShape({ datasetId: validator.required(validator.string), description: validator.string })(config); return this.client.createRequest({ method: 'GET', path: '/datasets/v1/:ownerId/:datasetId', params: config }); }; /** * Update user-defined properties of a dataset's metadata. * * See the [corresponding HTTP service documentation](https://docs.mapbox.com/api/maps/#update-a-dataset). * * @param {Object} config * @param {string} config.datasetId * @param {string} [config.name] * @param {string} [config.description] * @return {MapiRequest} * * @example * datasetsClient.updateMetadata({ * datasetId: 'dataset-id', * name: 'foo' * }) * .send() * .then(response => { * const datasetMetadata = response.body; * }); */ Datasets.updateMetadata = function(config) { validator.assertShape({ datasetId: validator.required