mapbox-gl
Version:
A WebGL interactive maps library
346 lines (287 loc) • 13.6 kB
JavaScript
// @flow
import Actor from '../util/actor.js';
import StyleLayerIndex from '../style/style_layer_index.js';
import VectorTileWorkerSource from './vector_tile_worker_source.js';
import RasterDEMTileWorkerSource from './raster_dem_tile_worker_source.js';
import GeoJSONWorkerSource from './geojson_worker_source.js';
import Tiled3dModelWorkerSource from '../../3d-style/source/tiled_3d_model_worker_source.js';
import assert from 'assert';
import {plugin as globalRTLTextPlugin} from './rtl_text_plugin.js';
import {enforceCacheSizeLimit} from '../util/tile_request_cache.js';
import {PerformanceUtils} from '../util/performance.js';
import {Event} from '../util/evented.js';
import {getProjection} from '../geo/projection/index.js';
import type {
WorkerSource,
WorkerTileParameters,
WorkerDEMTileParameters,
WorkerTileCallback,
WorkerDEMTileCallback,
TileParameters
} from '../source/worker_source.js';
import type {WorkerGlobalScopeInterface} from '../util/web_worker.js';
import type {Callback} from '../types/callback.js';
import type {LayerSpecification, ProjectionSpecification} from '../style-spec/types.js';
import type {ConfigOptions} from '../style/properties.js';
import type {PluginState} from './rtl_text_plugin.js';
import type Projection from '../geo/projection/projection.js';
/**
* @private
*/
export default class Worker {
self: WorkerGlobalScopeInterface;
actor: Actor;
layerIndexes: {[mapId: string]: {[scope: string]: StyleLayerIndex }};
availableImages: {[mapId: string]: {[scope: string]: Array<string>}};
workerSourceTypes: {[_: string]: Class<WorkerSource> };
workerSources: {[mapId: string]: {[scope: string]: {[sourceType: string]: {[sourceId: string]: WorkerSource}}}};
demWorkerSources: {[mapId: string]: {[scope: string]: {[sourceId: string]: RasterDEMTileWorkerSource }}};
projections: {[_: string]: Projection };
defaultProjection: Projection;
isSpriteLoaded: {[mapId: string]: {[scope: string]: boolean}};
referrer: ?string;
dracoUrl: ?string
brightness: ?number;
constructor(self: WorkerGlobalScopeInterface) {
PerformanceUtils.measure('workerEvaluateScript');
this.self = self;
this.actor = new Actor(self, this);
this.layerIndexes = {};
this.availableImages = {};
this.isSpriteLoaded = {};
this.projections = {};
this.defaultProjection = getProjection({name: 'mercator'});
this.workerSourceTypes = {
vector: VectorTileWorkerSource,
geojson: GeoJSONWorkerSource,
'batched-model': Tiled3dModelWorkerSource
};
// [mapId][scope][sourceType][sourceName] => worker source instance
this.workerSources = {};
this.demWorkerSources = {};
this.self.registerWorkerSource = (name: string, WorkerSource: Class<WorkerSource>) => {
if (this.workerSourceTypes[name]) {
throw new Error(`Worker source with name "${name}" already registered.`);
}
this.workerSourceTypes[name] = WorkerSource;
};
// This is invoked by the RTL text plugin when the download via the `importScripts` call has finished, and the code has been parsed.
this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => {
if (globalRTLTextPlugin.isParsed()) {
throw new Error('RTL text plugin already registered.');
}
globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping;
globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText;
globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText;
};
}
clearCaches(mapId: string, unused: mixed, callback: WorkerTileCallback) {
delete this.layerIndexes[mapId];
delete this.availableImages[mapId];
delete this.workerSources[mapId];
delete this.demWorkerSources[mapId];
callback();
}
checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) {
// noop, used to check if a worker is fully set up and ready to receive messages
callback();
}
setReferrer(mapID: string, referrer: string) {
this.referrer = referrer;
}
spriteLoaded(mapId: string, {scope, isLoaded}: {scope: string, isLoaded: boolean}) {
if (!this.isSpriteLoaded[mapId])
this.isSpriteLoaded[mapId] = {};
this.isSpriteLoaded[mapId][scope] = isLoaded;
if (!this.workerSources[mapId] || !this.workerSources[mapId][scope]) {
return;
}
for (const workerSource in this.workerSources[mapId][scope]) {
const ws = this.workerSources[mapId][scope][workerSource];
for (const source in ws) {
if (ws[source] instanceof VectorTileWorkerSource) {
ws[source].isSpriteLoaded = isLoaded;
ws[source].fire(new Event('isSpriteLoaded'));
}
}
}
}
setImages(mapId: string, {scope, images}: {scope: string, images: Array<string>}, callback: WorkerTileCallback) {
if (!this.availableImages[mapId]) {
this.availableImages[mapId] = {};
}
this.availableImages[mapId][scope] = images;
if (!this.workerSources[mapId] || !this.workerSources[mapId][scope]) {
callback();
return;
}
for (const workerSource in this.workerSources[mapId][scope]) {
const ws = this.workerSources[mapId][scope][workerSource];
for (const source in ws) {
ws[source].availableImages = images;
}
}
callback();
}
setProjection(mapId: string, config: ProjectionSpecification) {
this.projections[mapId] = getProjection(config);
}
setBrightness(mapId: string, brightness: ?number, callback: WorkerTileCallback) {
this.brightness = brightness;
callback();
}
setLayers(mapId: string, params: {layers: Array<LayerSpecification>, scope: string, options: ConfigOptions}, callback: WorkerTileCallback) {
this.getLayerIndex(mapId, params.scope).replace(params.layers, params.options);
callback();
}
updateLayers(mapId: string, params: {layers: Array<LayerSpecification>, scope: string, removedIds: Array<string>, options: ConfigOptions}, callback: WorkerTileCallback) {
this.getLayerIndex(mapId, params.scope).update(params.layers, params.removedIds, params.options);
callback();
}
loadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) {
assert(params.type);
params.projection = this.projections[mapId] || this.defaultProjection;
this.getWorkerSource(mapId, params.type, params.source, params.scope).loadTile(params, callback);
}
loadDEMTile(mapId: string, params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) {
this.getDEMWorkerSource(mapId, params.source, params.scope).loadTile(params, callback);
}
reloadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) {
assert(params.type);
params.projection = this.projections[mapId] || this.defaultProjection;
this.getWorkerSource(mapId, params.type, params.source, params.scope).reloadTile(params, callback);
}
abortTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) {
assert(params.type);
this.getWorkerSource(mapId, params.type, params.source, params.scope).abortTile(params, callback);
}
removeTile(mapId: string, params: TileParameters & {type: string}, callback: WorkerTileCallback) {
assert(params.type);
this.getWorkerSource(mapId, params.type, params.source, params.scope).removeTile(params, callback);
}
removeSource(mapId: string, params: {source: string, scope: string, type: string}, callback: WorkerTileCallback) {
assert(params.type);
assert(params.scope);
assert(params.source);
if (!this.workerSources[mapId] ||
!this.workerSources[mapId][params.scope] ||
!this.workerSources[mapId][params.scope][params.type] ||
!this.workerSources[mapId][params.scope][params.type][params.source]) {
return;
}
const worker = this.workerSources[mapId][params.scope][params.type][params.source];
delete this.workerSources[mapId][params.scope][params.type][params.source];
if (worker.removeSource !== undefined) {
worker.removeSource(params, callback);
} else {
callback();
}
}
/**
* Load a {@link WorkerSource} script at params.url. The script is run
* (using importScripts) with `registerWorkerSource` in scope, which is a
* function taking `(name, workerSourceObject)`.
* @private
*/
loadWorkerSource(map: string, params: { url: string }, callback: Callback<void>) {
try {
this.self.importScripts(params.url);
callback();
} catch (e) {
callback(e.toString());
}
}
syncRTLPluginState(map: string, state: PluginState, callback: Callback<boolean>) {
try {
globalRTLTextPlugin.setState(state);
const pluginURL = globalRTLTextPlugin.getPluginURL();
if (
globalRTLTextPlugin.isLoaded() &&
!globalRTLTextPlugin.isParsed() &&
pluginURL != null // Not possible when `isLoaded` is true, but keeps flow happy
) {
this.self.importScripts(pluginURL);
const complete = globalRTLTextPlugin.isParsed();
const error = complete ? undefined : new Error(`RTL Text Plugin failed to import scripts from ${pluginURL}`);
callback(error, complete);
}
} catch (e) {
callback(e.toString());
}
}
setDracoUrl(map: string, dracoUrl: string) {
this.dracoUrl = dracoUrl;
}
getAvailableImages(mapId: string, scope: string): Array<string> {
if (!this.availableImages[mapId]) {
this.availableImages[mapId] = {};
}
let availableImages = this.availableImages[mapId][scope];
if (!availableImages) {
availableImages = [];
}
return availableImages;
}
getLayerIndex(mapId: string, scope: string): StyleLayerIndex {
if (!this.layerIndexes[mapId]) {
this.layerIndexes[mapId] = {};
}
let layerIndex = this.layerIndexes[mapId][scope];
if (!layerIndex) {
layerIndex = this.layerIndexes[mapId][scope] = new StyleLayerIndex();
layerIndex.scope = scope;
}
return layerIndex;
}
getWorkerSource(mapId: string, type: string, source: string, scope: string): WorkerSource {
if (!this.workerSources[mapId])
this.workerSources[mapId] = {};
if (!this.workerSources[mapId][scope])
this.workerSources[mapId][scope] = {};
if (!this.workerSources[mapId][scope][type])
this.workerSources[mapId][scope][type] = {};
if (!this.isSpriteLoaded[mapId])
this.isSpriteLoaded[mapId] = {};
if (!this.workerSources[mapId][scope][type][source]) {
// use a wrapped actor so that we can attach a target mapId param
// to any messages invoked by the WorkerSource
const actor = {
send: (type: string, data: mixed, callback: any, _: any, mustQueue: boolean, metadata: any) => {
this.actor.send(type, data, callback, mapId, mustQueue, metadata);
},
scheduler: this.actor.scheduler
};
this.workerSources[mapId][scope][type][source] = new (this.workerSourceTypes[type]: any)(
(actor: any),
this.getLayerIndex(mapId, scope),
this.getAvailableImages(mapId, scope),
this.isSpriteLoaded[mapId][scope],
undefined,
this.brightness);
}
return this.workerSources[mapId][scope][type][source];
}
getDEMWorkerSource(mapId: string, source: string, scope: string): RasterDEMTileWorkerSource {
if (!this.demWorkerSources[mapId])
this.demWorkerSources[mapId] = {};
if (!this.demWorkerSources[mapId][scope])
this.demWorkerSources[mapId][scope] = {};
if (!this.demWorkerSources[mapId][scope][source]) {
this.demWorkerSources[mapId][scope][source] = new RasterDEMTileWorkerSource();
}
return this.demWorkerSources[mapId][scope][source];
}
enforceCacheSizeLimit(mapId: string, limit: number) {
enforceCacheSizeLimit(limit);
}
getWorkerPerformanceMetrics(mapId: string, params: any, callback: (error: ?Error, result: ?Object) => void) {
callback(undefined, PerformanceUtils.getWorkerPerformanceMetrics());
}
}
/* global WorkerGlobalScope */
if (typeof WorkerGlobalScope !== 'undefined' &&
typeof self !== 'undefined' &&
self instanceof WorkerGlobalScope) {
// $FlowFixMe[prop-missing]
self.worker = new Worker(self);
}