package.src.source.tile.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
import {uniqueId, parseCacheControl} from '../util/util';
import {deserialize as deserializeBucket} from '../data/bucket';
import FeatureIndex from '../data/feature_index';
import GeoJSONFeature from '../util/vectortile_to_geojson';
import featureFilter from '../style-spec/feature_filter';
import SymbolBucket from '../data/bucket/symbol_bucket';
import {CollisionBoxArray} from '../data/array_types';
import Texture from '../render/texture';
import browser from '../util/browser';
import toEvaluationFeature from '../data/evaluation_feature';
import EvaluationParameters from '../style/evaluation_parameters';
import SourceFeatureState from '../source/source_state';
import {lazyLoadRTLTextPlugin} from './rtl_text_plugin';
const CLOCK_SKEW_RETRY_TIMEOUT = 30000;
import type {Bucket} from '../data/bucket';
import type StyleLayer from '../style/style_layer';
import type {WorkerTileResult} from './worker_source';
import type Actor from '../util/actor';
import type DEMData from '../data/dem_data';
import type {AlphaImage} from '../util/image';
import type ImageAtlas from '../render/image_atlas';
import type ImageManager from '../render/image_manager';
import type Context from '../gl/context';
import type {OverscaledTileID} from './tile_id';
import type Framebuffer from '../gl/framebuffer';
import type Transform from '../geo/transform';
import type {LayerFeatureStates} from './source_state';
import type {Cancelable} from '../types/cancelable';
import type {FilterSpecification} from '../style-spec/types';
export type TileState =
| 'loading' // Tile data is in the process of loading.
| 'loaded' // Tile data has been loaded. Tile can be rendered.
| 'reloading' // Tile data has been loaded and is being updated. Tile can be rendered.
| 'unloaded' // Tile data has been deleted.
| 'errored' // Tile data was not loaded because of an error.
| 'expired'; /* Tile data was previously loaded, but has expired per its
* HTTP headers and is in the process of refreshing. */
/**
* A tile object is the combination of a Coordinate, which defines
* its place, as well as a unique ID and data tracking for its content
*
* @private
*/
class Tile {
tileID: OverscaledTileID;
uid: number;
uses: number;
tileSize: number;
buckets: {[_: string]: Bucket};
latestFeatureIndex: ?FeatureIndex;
latestRawTileData: ?ArrayBuffer;
imageAtlas: ?ImageAtlas;
imageAtlasTexture: Texture;
glyphAtlasImage: ?AlphaImage;
glyphAtlasTexture: Texture;
expirationTime: any;
expiredRequestCount: number;
state: TileState;
timeAdded: any;
fadeEndTime: any;
collisionBoxArray: ?CollisionBoxArray;
redoWhenDone: boolean;
showCollisionBoxes: boolean;
placementSource: any;
actor: ?Actor;
vtLayers: {[_: string]: VectorTileLayer};
neighboringTiles: ?Object;
dem: ?DEMData;
aborted: ?boolean;
needsHillshadePrepare: ?boolean;
request: ?Cancelable;
texture: any;
fbo: ?Framebuffer;
demTexture: ?Texture;
refreshedUponExpiration: boolean;
reloadCallback: any;
resourceTiming: ?Array;
queryPadding: number;
symbolFadeHoldUntil: ?number;
hasSymbolBuckets: boolean;
hasRTLText: boolean;
dependencies: Object;
/**
* @param {OverscaledTileID} tileID
* @param size
* @private
*/
constructor(tileID: OverscaledTileID, size: number) {
this.tileID = tileID;
this.uid = uniqueId();
this.uses = 0;
this.tileSize = size;
this.buckets = {};
this.expirationTime = null;
this.queryPadding = 0;
this.hasSymbolBuckets = false;
this.hasRTLText = false;
this.dependencies = {};
// Counts the number of times a response was already expired when
// received. We're using this to add a delay when making a new request
// so we don't have to keep retrying immediately in case of a server
// serving expired tiles.
this.expiredRequestCount = 0;
this.state = 'loading';
}
registerFadeDuration(duration: number) {
const fadeEndTime = duration + this.timeAdded;
if (fadeEndTime < browser.now()) return;
if (this.fadeEndTime && fadeEndTime < this.fadeEndTime) return;
this.fadeEndTime = fadeEndTime;
}
wasRequested() {
return this.state === 'errored' || this.state === 'loaded' || this.state === 'reloading';
}
/**
* Given a data object with a 'buffers' property, load it into
* this tile's elementGroups and buffers properties and set loaded
* to true. If the data is null, like in the case of an empty
* GeoJSON tile, no-op but still set loaded to true.
* @param {Object} data
* @param painter
* @returns {undefined}
* @private
*/
loadVectorData(data: WorkerTileResult, painter: any, justReloaded: ?boolean) {
if (this.hasData()) {
this.unloadVectorData();
}
this.state = 'loaded';
// empty GeoJSON tile
if (!data) {
this.collisionBoxArray = new CollisionBoxArray();
return;
}
if (data.featureIndex) {
this.latestFeatureIndex = data.featureIndex;
if (data.rawTileData) {
// Only vector tiles have rawTileData, and they won't update it for
// 'reloadTile'
this.latestRawTileData = data.rawTileData;
this.latestFeatureIndex.rawTileData = data.rawTileData;
} else if (this.latestRawTileData) {
// If rawTileData hasn't updated, hold onto a pointer to the last
// one we received
this.latestFeatureIndex.rawTileData = this.latestRawTileData;
}
}
this.collisionBoxArray = data.collisionBoxArray;
this.buckets = deserializeBucket(data.buckets, painter.style);
this.hasSymbolBuckets = false;
for (const id in this.buckets) {
const bucket = this.buckets[id];
if (bucket instanceof SymbolBucket) {
this.hasSymbolBuckets = true;
if (justReloaded) {
bucket.justReloaded = true;
} else {
break;
}
}
}
this.hasRTLText = false;
if (this.hasSymbolBuckets) {
for (const id in this.buckets) {
const bucket = this.buckets[id];
if (bucket instanceof SymbolBucket) {
if (bucket.hasRTLText) {
this.hasRTLText = true;
lazyLoadRTLTextPlugin();
break;
}
}
}
}
this.queryPadding = 0;
for (const id in this.buckets) {
const bucket = this.buckets[id];
this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(id).queryRadius(bucket));
}
if (data.imageAtlas) {
this.imageAtlas = data.imageAtlas;
}
if (data.glyphAtlasImage) {
this.glyphAtlasImage = data.glyphAtlasImage;
}
}
/**
* Release any data or WebGL resources referenced by this tile.
* @returns {undefined}
* @private
*/
unloadVectorData() {
for (const id in this.buckets) {
this.buckets[id].destroy();
}
this.buckets = {};
if (this.imageAtlasTexture) {
this.imageAtlasTexture.destroy();
}
if (this.imageAtlas) {
this.imageAtlas = null;
}
if (this.glyphAtlasTexture) {
this.glyphAtlasTexture.destroy();
}
this.latestFeatureIndex = null;
this.state = 'unloaded';
}
getBucket(layer: StyleLayer) {
return this.buckets[layer.id];
}
upload(context: Context) {
for (const id in this.buckets) {
const bucket = this.buckets[id];
if (bucket.uploadPending()) {
bucket.upload(context);
}
}
const gl = context.gl;
if (this.imageAtlas && !this.imageAtlas.uploaded) {
this.imageAtlasTexture = new Texture(context, this.imageAtlas.image, gl.RGBA);
this.imageAtlas.uploaded = true;
}
if (this.glyphAtlasImage) {
this.glyphAtlasTexture = new Texture(context, this.glyphAtlasImage, gl.ALPHA);
this.glyphAtlasImage = null;
}
}
prepare(imageManager: ImageManager) {
if (this.imageAtlas) {
this.imageAtlas.patchUpdatedImages(imageManager, this.imageAtlasTexture);
}
}
// Queries non-symbol features rendered for this tile.
// Symbol features are queried globally
queryRenderedFeatures(layers: {[_: string]: StyleLayer},
serializedLayers: {[string]: Object},
sourceFeatureState: SourceFeatureState,
queryGeometry: Array,
cameraQueryGeometry: Array,
scale: number,
params: { filter: FilterSpecification, layers: Array, availableImages: Array },
transform: Transform,
maxPitchScaleFactor: number,
pixelPosMatrix: Float32Array): {[_: string]: Array<{ featureIndex: number, feature: GeoJSONFeature }>} {
if (!this.latestFeatureIndex || !this.latestFeatureIndex.rawTileData)
return {};
return this.latestFeatureIndex.query({
queryGeometry,
cameraQueryGeometry,
scale,
tileSize: this.tileSize,
pixelPosMatrix,
transform,
params,
queryPadding: this.queryPadding * maxPitchScaleFactor
}, layers, serializedLayers, sourceFeatureState);
}
querySourceFeatures(result: Array, params: any) {
const featureIndex = this.latestFeatureIndex;
if (!featureIndex || !featureIndex.rawTileData) return;
const vtLayers = featureIndex.loadVTLayers();
const sourceLayer = params ? params.sourceLayer : '';
const layer = vtLayers._geojsonTileLayer || vtLayers[sourceLayer];
if (!layer) return;
const filter = featureFilter(params && params.filter);
const {z, x, y} = this.tileID.canonical;
const coord = {z, x, y};
for (let i = 0; i < layer.length; i++) {
const feature = layer.feature(i);
if (filter.needGeometry) {
const evaluationFeature = toEvaluationFeature(feature, true);
if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), evaluationFeature, this.tileID.canonical)) continue;
} else if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) {
continue;
}
const id = featureIndex.getId(feature, sourceLayer);
const geojsonFeature = new GeoJSONFeature(feature, z, x, y, id);
(geojsonFeature: any).tile = coord;
result.push(geojsonFeature);
}
}
hasData() {
return this.state === 'loaded' || this.state === 'reloading' || this.state === 'expired';
}
patternsLoaded() {
return this.imageAtlas && !!Object.keys(this.imageAtlas.patternPositions).length;
}
setExpiryData(data: any) {
const prior = this.expirationTime;
if (data.cacheControl) {
const parsedCC = parseCacheControl(data.cacheControl);
if (parsedCC['max-age']) this.expirationTime = Date.now() + parsedCC['max-age'] * 1000;
} else if (data.expires) {
this.expirationTime = new Date(data.expires).getTime();
}
if (this.expirationTime) {
const now = Date.now();
let isExpired = false;
if (this.expirationTime > now) {
isExpired = false;
} else if (!prior) {
isExpired = true;
} else if (this.expirationTime < prior) {
// Expiring date is going backwards:
// fall back to exponential backoff
isExpired = true;
} else {
const delta = this.expirationTime - prior;
if (!delta) {
// Server is serving the same expired resource over and over: fall
// back to exponential backoff.
isExpired = true;
} else {
// Assume that either the client or the server clock is wrong and
// try to interpolate a valid expiration date (from the client POV)
// observing a minimum timeout.
this.expirationTime = now + Math.max(delta, CLOCK_SKEW_RETRY_TIMEOUT);
}
}
if (isExpired) {
this.expiredRequestCount++;
this.state = 'expired';
} else {
this.expiredRequestCount = 0;
}
}
}
getExpiryTimeout() {
if (this.expirationTime) {
if (this.expiredRequestCount) {
return 1000 * (1 << Math.min(this.expiredRequestCount - 1, 31));
} else {
// Max value for `setTimeout` implementations is a 32 bit integer; cap this accordingly
return Math.min(this.expirationTime - new Date().getTime(), Math.pow(2, 31) - 1);
}
}
}
setFeatureState(states: LayerFeatureStates, painter: any) {
if (!this.latestFeatureIndex ||
!this.latestFeatureIndex.rawTileData ||
Object.keys(states).length === 0) {
return;
}
const vtLayers = this.latestFeatureIndex.loadVTLayers();
for (const id in this.buckets) {
if (!painter.style.hasLayer(id)) continue;
const bucket = this.buckets[id];
// Buckets are grouped by common source-layer
const sourceLayerId = bucket.layers[0]['sourceLayer'] || '_geojsonTileLayer';
const sourceLayer = vtLayers[sourceLayerId];
const sourceLayerStates = states[sourceLayerId];
if (!sourceLayer || !sourceLayerStates || Object.keys(sourceLayerStates).length === 0) continue;
bucket.update(sourceLayerStates, sourceLayer, this.imageAtlas && this.imageAtlas.patternPositions || {});
const layer = painter && painter.style && painter.style.getLayer(id);
if (layer) {
this.queryPadding = Math.max(this.queryPadding, layer.queryRadius(bucket));
}
}
}
holdingForFade(): boolean {
return this.symbolFadeHoldUntil !== undefined;
}
symbolFadeFinished(): boolean {
return !this.symbolFadeHoldUntil || this.symbolFadeHoldUntil < browser.now();
}
clearFadeHold() {
this.symbolFadeHoldUntil = undefined;
}
setHoldDuration(duration: number) {
this.symbolFadeHoldUntil = browser.now() + duration;
}
setDependencies(namespace: string, dependencies: Array) {
const index = {};
for (const dep of dependencies) {
index[dep] = true;
}
this.dependencies[namespace] = index;
}
hasDependency(namespaces: Array, keys: Array) {
for (const namespace of namespaces) {
const dependencies = this.dependencies[namespace];
if (dependencies) {
for (const key of keys) {
if (dependencies[key]) {
return true;
}
}
}
}
return false;
}
}
export default Tile;