package.src.util.mapbox.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mapbox-gl Show documentation
Show all versions of mapbox-gl Show documentation
A WebGL interactive maps library
The newest version!
// @flow
/***** START WARNING - IF YOU USE THIS CODE WITH MAPBOX MAPPING APIS, REMOVAL OR
* MODIFICATION OF THE FOLLOWING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******
* The following code is used to access Mapbox's Mapping APIs. Removal or modification
* of this code when used with Mapbox's Mapping APIs can result in higher fees and/or
* termination of your account with Mapbox.
*
* Under the Mapbox Terms of Service, you may not use this code to access Mapbox
* Mapping APIs other than through Mapbox SDKs.
*
* The Mapping APIs documentation is available at https://docs.mapbox.com/api/maps/#maps
* and the Mapbox Terms of Service are available at https://www.mapbox.com/tos/
******************************************************************************/
import config from './config';
import browser from './browser';
import window from './window';
import webpSupported from './webp_supported';
import {createSkuToken, SKU_ID} from './sku_token';
import {version as sdkVersion} from '../../package.json';
import {uuid, validateUuid, storageAvailable, b64DecodeUnicode, b64EncodeUnicode, warnOnce, extend} from './util';
import {postData, ResourceType} from './ajax';
import type {RequestParameters} from './ajax';
import type {Cancelable} from '../types/cancelable';
import type {TileJSON} from '../types/tilejson';
type ResourceTypeEnum = $Keys;
export type RequestTransformFunction = (url: string, resourceType?: ResourceTypeEnum) => RequestParameters;
type UrlObject = {|
protocol: string,
authority: string,
path: string,
params: Array
|};
export class RequestManager {
_skuToken: string;
_skuTokenExpiresAt: number;
_transformRequestFn: ?RequestTransformFunction;
_customAccessToken: ?string;
constructor(transformRequestFn?: RequestTransformFunction, customAccessToken?: string) {
this._transformRequestFn = transformRequestFn;
this._customAccessToken = customAccessToken;
this._createSkuToken();
}
_createSkuToken() {
const skuToken = createSkuToken();
this._skuToken = skuToken.token;
this._skuTokenExpiresAt = skuToken.tokenExpiresAt;
}
_isSkuTokenExpired(): boolean {
return Date.now() > this._skuTokenExpiresAt;
}
transformRequest(url: string, type: ResourceTypeEnum) {
if (this._transformRequestFn) {
return this._transformRequestFn(url, type) || {url};
}
return {url};
}
normalizeStyleURL(url: string, accessToken?: string): string {
if (!isMapboxURL(url)) return url;
const urlObject = parseUrl(url);
urlObject.path = `/styles/v1${urlObject.path}`;
return this._makeAPIURL(urlObject, this._customAccessToken || accessToken);
}
normalizeGlyphsURL(url: string, accessToken?: string): string {
if (!isMapboxURL(url)) return url;
const urlObject = parseUrl(url);
urlObject.path = `/fonts/v1${urlObject.path}`;
return this._makeAPIURL(urlObject, this._customAccessToken || accessToken);
}
normalizeSourceURL(url: string, accessToken?: string): string {
if (!isMapboxURL(url)) return url;
const urlObject = parseUrl(url);
urlObject.path = `/v4/${urlObject.authority}.json`;
// TileJSON requests need a secure flag appended to their URLs so
// that the server knows to send SSL-ified resource references.
urlObject.params.push('secure');
return this._makeAPIURL(urlObject, this._customAccessToken || accessToken);
}
normalizeSpriteURL(url: string, format: string, extension: string, accessToken?: string): string {
const urlObject = parseUrl(url);
if (!isMapboxURL(url)) {
urlObject.path += `${format}${extension}`;
return formatUrl(urlObject);
}
urlObject.path = `/styles/v1${urlObject.path}/sprite${format}${extension}`;
return this._makeAPIURL(urlObject, this._customAccessToken || accessToken);
}
normalizeTileURL(tileURL: string, tileSize?: ?number): string {
if (this._isSkuTokenExpired()) {
this._createSkuToken();
}
if (tileURL && !isMapboxURL(tileURL)) return tileURL;
const urlObject = parseUrl(tileURL);
const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/;
const tileURLAPIPrefixRe = /^.+\/v4\//;
// The v4 mapbox tile API supports 512x512 image tiles only when @2x
// is appended to the tile URL. If `tileSize: 512` is specified for
// a Mapbox raster source force the @2x suffix even if a non hidpi device.
const suffix = browser.devicePixelRatio >= 2 || tileSize === 512 ? '@2x' : '';
const extension = webpSupported.supported ? '.webp' : '$1';
urlObject.path = urlObject.path.replace(imageExtensionRe, `${suffix}${extension}`);
urlObject.path = urlObject.path.replace(tileURLAPIPrefixRe, '/');
urlObject.path = `/v4${urlObject.path}`;
const accessToken = this._customAccessToken || getAccessToken(urlObject.params) || config.ACCESS_TOKEN;
if (config.REQUIRE_ACCESS_TOKEN && accessToken && this._skuToken) {
urlObject.params.push(`sku=${this._skuToken}`);
}
return this._makeAPIURL(urlObject, accessToken);
}
canonicalizeTileURL(url: string, removeAccessToken: boolean) {
const version = "/v4/";
// matches any file extension specified by a dot and one or more alphanumeric characters
const extensionRe = /\.[\w]+$/;
const urlObject = parseUrl(url);
// Make sure that we are dealing with a valid Mapbox tile URL.
// Has to begin with /v4/, with a valid filename + extension
if (!urlObject.path.match(/(^\/v4\/)/) || !urlObject.path.match(extensionRe)) {
// Not a proper Mapbox tile URL.
return url;
}
// Reassemble the canonical URL from the parts we've parsed before.
let result = "mapbox://tiles/";
result += urlObject.path.replace(version, '');
// Append the query string, minus the access token parameter.
let params = urlObject.params;
if (removeAccessToken) {
params = params.filter(p => !p.match(/^access_token=/));
}
if (params.length) result += `?${params.join('&')}`;
return result;
}
canonicalizeTileset(tileJSON: TileJSON, sourceURL?: string) {
const removeAccessToken = sourceURL ? isMapboxURL(sourceURL) : false;
const canonical = [];
for (const url of tileJSON.tiles || []) {
if (isMapboxHTTPURL(url)) {
canonical.push(this.canonicalizeTileURL(url, removeAccessToken));
} else {
canonical.push(url);
}
}
return canonical;
}
_makeAPIURL(urlObject: UrlObject, accessToken: string | null | void): string {
const help = 'See https://www.mapbox.com/api-documentation/#access-tokens-and-token-scopes';
const apiUrlObject = parseUrl(config.API_URL);
urlObject.protocol = apiUrlObject.protocol;
urlObject.authority = apiUrlObject.authority;
if (urlObject.protocol === 'http') {
const i = urlObject.params.indexOf('secure');
if (i >= 0) urlObject.params.splice(i, 1);
}
if (apiUrlObject.path !== '/') {
urlObject.path = `${apiUrlObject.path}${urlObject.path}`;
}
if (!config.REQUIRE_ACCESS_TOKEN) return formatUrl(urlObject);
accessToken = accessToken || config.ACCESS_TOKEN;
if (!accessToken)
throw new Error(`An API access token is required to use Mapbox GL. ${help}`);
if (accessToken[0] === 's')
throw new Error(`Use a public access token (pk.*) with Mapbox GL, not a secret access token (sk.*). ${help}`);
urlObject.params = urlObject.params.filter((d) => d.indexOf('access_token') === -1);
urlObject.params.push(`access_token=${accessToken}`);
return formatUrl(urlObject);
}
}
function isMapboxURL(url: string) {
return url.indexOf('mapbox:') === 0;
}
const mapboxHTTPURLRe = /^((https?:)?\/\/)?([^\/]+\.)?mapbox\.c(n|om)(\/|\?|$)/i;
function isMapboxHTTPURL(url: string): boolean {
return mapboxHTTPURLRe.test(url);
}
function hasCacheDefeatingSku(url: string) {
return url.indexOf('sku=') > 0 && isMapboxHTTPURL(url);
}
function getAccessToken(params: Array): string | null {
for (const param of params) {
const match = param.match(/^access_token=(.*)$/);
if (match) {
return match[1];
}
}
return null;
}
const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/;
function parseUrl(url: string): UrlObject {
const parts = url.match(urlRe);
if (!parts) {
throw new Error('Unable to parse URL object');
}
return {
protocol: parts[1],
authority: parts[2],
path: parts[3] || '/',
params: parts[4] ? parts[4].split('&') : []
};
}
function formatUrl(obj: UrlObject): string {
const params = obj.params.length ? `?${obj.params.join('&')}` : '';
return `${obj.protocol}://${obj.authority}${obj.path}${params}`;
}
export {isMapboxURL, isMapboxHTTPURL, hasCacheDefeatingSku};
const telemEventKey = 'mapbox.eventData';
function parseAccessToken(accessToken: ?string) {
if (!accessToken) {
return null;
}
const parts = accessToken.split('.');
if (!parts || parts.length !== 3) {
return null;
}
try {
const jsonData = JSON.parse(b64DecodeUnicode(parts[1]));
return jsonData;
} catch (e) {
return null;
}
}
type TelemetryEventType = 'appUserTurnstile' | 'map.load';
class TelemetryEvent {
eventData: any;
anonId: ?string;
queue: Array;
type: TelemetryEventType;
pendingRequest: ?Cancelable;
_customAccessToken: ?string;
constructor(type: TelemetryEventType) {
this.type = type;
this.anonId = null;
this.eventData = {};
this.queue = [];
this.pendingRequest = null;
}
getStorageKey(domain: ?string) {
const tokenData = parseAccessToken(config.ACCESS_TOKEN);
let u = '';
if (tokenData && tokenData['u']) {
u = b64EncodeUnicode(tokenData['u']);
} else {
u = config.ACCESS_TOKEN || '';
}
return domain ?
`${telemEventKey}.${domain}:${u}` :
`${telemEventKey}:${u}`;
}
fetchEventData() {
const isLocalStorageAvailable = storageAvailable('localStorage');
const storageKey = this.getStorageKey();
const uuidKey = this.getStorageKey('uuid');
if (isLocalStorageAvailable) {
//Retrieve cached data
try {
const data = window.localStorage.getItem(storageKey);
if (data) {
this.eventData = JSON.parse(data);
}
const uuid = window.localStorage.getItem(uuidKey);
if (uuid) this.anonId = uuid;
} catch (e) {
warnOnce('Unable to read from LocalStorage');
}
}
}
saveEventData() {
const isLocalStorageAvailable = storageAvailable('localStorage');
const storageKey = this.getStorageKey();
const uuidKey = this.getStorageKey('uuid');
if (isLocalStorageAvailable) {
try {
window.localStorage.setItem(uuidKey, this.anonId);
if (Object.keys(this.eventData).length >= 1) {
window.localStorage.setItem(storageKey, JSON.stringify(this.eventData));
}
} catch (e) {
warnOnce('Unable to write to LocalStorage');
}
}
}
processRequests(_: ?string) {}
/*
* If any event data should be persisted after the POST request, the callback should modify eventData`
* to the values that should be saved. For this reason, the callback should be invoked prior to the call
* to TelemetryEvent#saveData
*/
postEvent(timestamp: number, additionalPayload: {[_: string]: any}, callback: (err: ?Error) => void, customAccessToken?: ?string) {
if (!config.EVENTS_URL) return;
const eventsUrlObject: UrlObject = parseUrl(config.EVENTS_URL);
eventsUrlObject.params.push(`access_token=${customAccessToken || config.ACCESS_TOKEN || ''}`);
const payload: Object = {
event: this.type,
created: new Date(timestamp).toISOString(),
sdkIdentifier: 'mapbox-gl-js',
sdkVersion,
skuId: SKU_ID,
userId: this.anonId
};
const finalPayload = additionalPayload ? extend(payload, additionalPayload) : payload;
const request: RequestParameters = {
url: formatUrl(eventsUrlObject),
headers: {
'Content-Type': 'text/plain' //Skip the pre-flight OPTIONS request
},
body: JSON.stringify([finalPayload])
};
this.pendingRequest = postData(request, (error) => {
this.pendingRequest = null;
callback(error);
this.saveEventData();
this.processRequests(customAccessToken);
});
}
queueRequest(event: number | {id: number, timestamp: number}, customAccessToken?: ?string) {
this.queue.push(event);
this.processRequests(customAccessToken);
}
}
export class MapLoadEvent extends TelemetryEvent {
+success: {[_: number]: boolean};
skuToken: string;
constructor() {
super('map.load');
this.success = {};
this.skuToken = '';
}
postMapLoadEvent(tileUrls: Array, mapId: number, skuToken: string, customAccessToken: string) {
this.skuToken = skuToken;
const accessTokenIsSet = !!(customAccessToken || config.ACCESS_TOKEN);
const usesMapboxTiles = Array.isArray(tileUrls) && tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url));
// Enabled only when Mapbox Access Token is set and a source uses mapbox tiles.
if (config.EVENTS_URL && accessTokenIsSet && usesMapboxTiles) {
this.queueRequest({id: mapId, timestamp: Date.now()}, customAccessToken);
}
}
processRequests(customAccessToken?: ?string) {
if (this.pendingRequest || this.queue.length === 0) return;
const {id, timestamp} = this.queue.shift();
// Only one load event should fire per map
if (id && this.success[id]) return;
if (!this.anonId) {
this.fetchEventData();
}
if (!validateUuid(this.anonId)) {
this.anonId = uuid();
}
this.postEvent(timestamp, {skuToken: this.skuToken}, (err) => {
if (!err) {
if (id) this.success[id] = true;
}
}, customAccessToken);
}
}
export class TurnstileEvent extends TelemetryEvent {
constructor(customAccessToken?: ?string) {
super('appUserTurnstile');
this._customAccessToken = customAccessToken;
}
postTurnstileEvent(tileUrls: Array, customAccessToken?: ?string) {
//Enabled only when Mapbox Access Token is set and a source uses
// mapbox tiles.
if (config.EVENTS_URL &&
config.ACCESS_TOKEN &&
Array.isArray(tileUrls) &&
tileUrls.some(url => isMapboxURL(url) || isMapboxHTTPURL(url))) {
this.queueRequest(Date.now(), customAccessToken);
}
}
processRequests(customAccessToken?: ?string) {
if (this.pendingRequest || this.queue.length === 0) {
return;
}
if (!this.anonId || !this.eventData.lastSuccess || !this.eventData.tokenU) {
//Retrieve cached data
this.fetchEventData();
}
const tokenData = parseAccessToken(config.ACCESS_TOKEN);
const tokenU = tokenData ? tokenData['u'] : config.ACCESS_TOKEN;
//Reset event data cache if the access token owner changed.
let dueForEvent = tokenU !== this.eventData.tokenU;
if (!validateUuid(this.anonId)) {
this.anonId = uuid();
dueForEvent = true;
}
const nextUpdate = this.queue.shift();
// Record turnstile event once per calendar day.
if (this.eventData.lastSuccess) {
const lastUpdate = new Date(this.eventData.lastSuccess);
const nextDate = new Date(nextUpdate);
const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000);
dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate();
} else {
dueForEvent = true;
}
if (!dueForEvent) {
return this.processRequests();
}
this.postEvent(nextUpdate, {"enabled.telemetry": false}, (err) => {
if (!err) {
this.eventData.lastSuccess = nextUpdate;
this.eventData.tokenU = tokenU;
}
}, customAccessToken);
}
}
const turnstileEvent_ = new TurnstileEvent();
export const postTurnstileEvent = turnstileEvent_.postTurnstileEvent.bind(turnstileEvent_);
const mapLoadEvent_ = new MapLoadEvent();
export const postMapLoadEvent = mapLoadEvent_.postMapLoadEvent.bind(mapLoadEvent_);
/***** END WARNING - REMOVAL OR MODIFICATION OF THE
PRECEDING CODE VIOLATES THE MAPBOX TERMS OF SERVICE ******/