StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
17 KiB
487 lines
17 KiB
// @flow
|
|
|
|
import { uniqueId, deepEqual, 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 { RasterBoundsArray, CollisionBoxArray } from '../data/array_types';
|
|
import rasterBoundsAttributes from '../data/raster_bounds_attributes';
|
|
import EXTENT from '../data/extent';
|
|
import Point from '@mapbox/point-geometry';
|
|
import Texture from '../render/texture';
|
|
import SegmentVector from '../data/segment';
|
|
import { TriangleIndexArray } from '../data/index_array_type';
|
|
import browser from '../util/browser';
|
|
import EvaluationParameters from '../style/evaluation_parameters';
|
|
import SourceFeatureState from '../source/source_state';
|
|
|
|
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 {Mask} from '../render/tile_mask';
|
|
import type Context from '../gl/context';
|
|
import type IndexBuffer from '../gl/index_buffer';
|
|
import type VertexBuffer from '../gl/vertex_buffer';
|
|
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};
|
|
mask: Mask;
|
|
|
|
neighboringTiles: ?Object;
|
|
dem: ?DEMData;
|
|
aborted: ?boolean;
|
|
maskedBoundsBuffer: ?VertexBuffer;
|
|
maskedIndexBuffer: ?IndexBuffer;
|
|
segments: ?SegmentVector;
|
|
needsHillshadePrepare: ?boolean;
|
|
request: ?Cancelable;
|
|
texture: any;
|
|
fbo: ?Framebuffer;
|
|
demTexture: ?Texture;
|
|
refreshedUponExpiration: boolean;
|
|
reloadCallback: any;
|
|
resourceTiming: ?Array<PerformanceResourceTiming>;
|
|
queryPadding: number;
|
|
|
|
symbolFadeHoldUntil: ?number;
|
|
hasSymbolBuckets: boolean;
|
|
|
|
/**
|
|
* @param {OverscaledTileID} tileID
|
|
* @param size
|
|
*/
|
|
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;
|
|
|
|
// 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.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';
|
|
}
|
|
|
|
unloadDEMData() {
|
|
this.dem = null;
|
|
this.neighboringTiles = 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},
|
|
sourceFeatureState: SourceFeatureState,
|
|
queryGeometry: Array<Point>,
|
|
cameraQueryGeometry: Array<Point>,
|
|
scale: number,
|
|
params: { filter: FilterSpecification, layers: Array<string> },
|
|
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, sourceFeatureState);
|
|
}
|
|
|
|
querySourceFeatures(result: Array<GeoJSONFeature>, params: any) {
|
|
if (!this.latestFeatureIndex || !this.latestFeatureIndex.rawTileData) return;
|
|
|
|
const vtLayers = this.latestFeatureIndex.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(new EvaluationParameters(this.tileID.overscaledZ), feature)) {
|
|
const geojsonFeature = new GeoJSONFeature(feature, z, x, y);
|
|
(geojsonFeature: any).tile = coord;
|
|
result.push(geojsonFeature);
|
|
}
|
|
}
|
|
}
|
|
|
|
clearMask() {
|
|
if (this.segments) {
|
|
this.segments.destroy();
|
|
delete this.segments;
|
|
}
|
|
if (this.maskedBoundsBuffer) {
|
|
this.maskedBoundsBuffer.destroy();
|
|
delete this.maskedBoundsBuffer;
|
|
}
|
|
if (this.maskedIndexBuffer) {
|
|
this.maskedIndexBuffer.destroy();
|
|
delete this.maskedIndexBuffer;
|
|
}
|
|
}
|
|
|
|
setMask(mask: Mask, context: Context) {
|
|
|
|
// don't redo buffer work if the mask is the same;
|
|
if (deepEqual(this.mask, mask)) return;
|
|
|
|
this.mask = mask;
|
|
this.clearMask();
|
|
|
|
// We want to render the full tile, and keeping the segments/vertices/indices empty means
|
|
// using the global shared buffers for covering the entire tile.
|
|
if (deepEqual(mask, {'0': true})) return;
|
|
|
|
const maskedBoundsArray = new RasterBoundsArray();
|
|
const indexArray = new TriangleIndexArray();
|
|
|
|
this.segments = new SegmentVector();
|
|
// Create a new segment so that we will upload (empty) buffers even when there is nothing to
|
|
// draw for this tile.
|
|
this.segments.prepareSegment(0, maskedBoundsArray, indexArray);
|
|
|
|
const maskArray = Object.keys(mask);
|
|
for (let i = 0; i < maskArray.length; i++) {
|
|
const maskCoord = mask[+maskArray[i]];
|
|
const vertexExtent = EXTENT >> maskCoord.z;
|
|
const tlVertex = new Point(maskCoord.x * vertexExtent, maskCoord.y * vertexExtent);
|
|
const brVertex = new Point(tlVertex.x + vertexExtent, tlVertex.y + vertexExtent);
|
|
|
|
// not sure why flow is complaining here because it doesn't complain at L401
|
|
const segment = (this.segments: any).prepareSegment(4, maskedBoundsArray, indexArray);
|
|
|
|
maskedBoundsArray.emplaceBack(tlVertex.x, tlVertex.y, tlVertex.x, tlVertex.y);
|
|
maskedBoundsArray.emplaceBack(brVertex.x, tlVertex.y, brVertex.x, tlVertex.y);
|
|
maskedBoundsArray.emplaceBack(tlVertex.x, brVertex.y, tlVertex.x, brVertex.y);
|
|
maskedBoundsArray.emplaceBack(brVertex.x, brVertex.y, brVertex.x, brVertex.y);
|
|
|
|
const offset = segment.vertexLength;
|
|
// 0, 1, 2
|
|
// 1, 2, 3
|
|
indexArray.emplaceBack(offset, offset + 1, offset + 2);
|
|
indexArray.emplaceBack(offset + 1, offset + 2, offset + 3);
|
|
|
|
segment.vertexLength += 4;
|
|
segment.primitiveLength += 2;
|
|
}
|
|
|
|
this.maskedBoundsBuffer = context.createVertexBuffer(maskedBoundsArray, rasterBoundsAttributes.members);
|
|
this.maskedIndexBuffer = context.createIndexBuffer(indexArray);
|
|
}
|
|
|
|
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) {
|
|
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 || {});
|
|
if (painter && painter.style) {
|
|
this.queryPadding = Math.max(this.queryPadding, painter.style.getLayer(id).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;
|
|
}
|
|
}
|
|
|
|
export default Tile;
|
|
|