Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
package.src.source.geojson_worker_source.js Maven / Gradle / Ivy
// @flow
import {getJSON} from '../util/ajax';
import {RequestPerformance} from '../util/performance';
import rewind from '@mapbox/geojson-rewind';
import GeoJSONWrapper from './geojson_wrapper';
import vtpbf from 'vt-pbf';
import Supercluster from 'supercluster';
import geojsonvt from 'geojson-vt';
import assert from 'assert';
import VectorTileWorkerSource from './vector_tile_worker_source';
import {createExpression} from '../style-spec/expression';
import type {
WorkerTileParameters,
WorkerTileCallback,
} from '../source/worker_source';
import type Actor from '../util/actor';
import type StyleLayerIndex from '../style/style_layer_index';
import type {LoadVectorDataCallback} from './vector_tile_worker_source';
import type {RequestParameters, ResponseCallback} from '../util/ajax';
import type {Callback} from '../types/callback';
import type {GeoJSONFeature} from '@mapbox/geojson-types';
export type LoadGeoJSONParameters = {
request?: RequestParameters,
data?: string,
source: string,
cluster: boolean,
superclusterOptions?: Object,
geojsonVtOptions?: Object,
clusterProperties?: Object,
filter?: Array
};
export type LoadGeoJSON = (params: LoadGeoJSONParameters, callback: ResponseCallback) => void;
export interface GeoJSONIndex {
getTile(z: number, x: number, y: number): Object;
// supercluster methods
getClusterExpansionZoom(clusterId: number): number;
getChildren(clusterId: number): Array;
getLeaves(clusterId: number, limit: number, offset: number): Array;
}
function loadGeoJSONTile(params: WorkerTileParameters, callback: LoadVectorDataCallback) {
const canonical = params.tileID.canonical;
if (!this._geoJSONIndex) {
return callback(null, null); // we couldn't load the file
}
const geoJSONTile = this._geoJSONIndex.getTile(canonical.z, canonical.x, canonical.y);
if (!geoJSONTile) {
return callback(null, null); // nothing in the given tile
}
const geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features);
// Encode the geojson-vt tile into binary vector tile form. This
// is a convenience that allows `FeatureIndex` to operate the same way
// across `VectorTileSource` and `GeoJSONSource` data.
let pbf = vtpbf(geojsonWrapper);
if (pbf.byteOffset !== 0 || pbf.byteLength !== pbf.buffer.byteLength) {
// Compatibility with node Buffer (https://github.com/mapbox/pbf/issues/35)
pbf = new Uint8Array(pbf);
}
callback(null, {
vectorTile: geojsonWrapper,
rawData: pbf.buffer
});
}
export type SourceState =
| 'Idle' // Source empty or data loaded
| 'Coalescing' // Data finished loading, but discard 'loadData' messages until receiving 'coalesced'
| 'NeedsLoadData'; // 'loadData' received while coalescing, trigger one more 'loadData' on receiving 'coalesced'
/**
* The {@link WorkerSource} implementation that supports {@link GeoJSONSource}.
* This class is designed to be easily reused to support custom source types
* for data formats that can be parsed/converted into an in-memory GeoJSON
* representation. To do so, create it with
* `new GeoJSONWorkerSource(actor, layerIndex, customLoadGeoJSONFunction)`.
* For a full example, see [mapbox-gl-topojson](https://github.com/developmentseed/mapbox-gl-topojson).
*
* @private
*/
class GeoJSONWorkerSource extends VectorTileWorkerSource {
loadGeoJSON: LoadGeoJSON;
_state: SourceState;
_pendingCallback: Callback<{
resourceTiming?: {[_: string]: Array},
abandoned?: boolean }>;
_pendingLoadDataParams: LoadGeoJSONParameters;
_geoJSONIndex: GeoJSONIndex
/**
* @param [loadGeoJSON] Optional method for custom loading/parsing of
* GeoJSON based on parameters passed from the main-thread Source.
* See {@link GeoJSONWorkerSource#loadGeoJSON}.
* @private
*/
constructor(actor: Actor, layerIndex: StyleLayerIndex, availableImages: Array, loadGeoJSON: ?LoadGeoJSON) {
super(actor, layerIndex, availableImages, loadGeoJSONTile);
if (loadGeoJSON) {
this.loadGeoJSON = loadGeoJSON;
}
}
/**
* Fetches (if appropriate), parses, and index geojson data into tiles. This
* preparatory method must be called before {@link GeoJSONWorkerSource#loadTile}
* can correctly serve up tiles.
*
* Defers to {@link GeoJSONWorkerSource#loadGeoJSON} for the fetching/parsing,
* expecting `callback(error, data)` to be called with either an error or a
* parsed GeoJSON object.
*
* When `loadData` requests come in faster than they can be processed,
* they are coalesced into a single request using the latest data.
* See {@link GeoJSONWorkerSource#coalesce}
*
* @param params
* @param callback
* @private
*/
loadData(params: LoadGeoJSONParameters, callback: Callback<{
resourceTiming?: {[_: string]: Array},
abandoned?: boolean }>) {
if (this._pendingCallback) {
// Tell the foreground the previous call has been abandoned
this._pendingCallback(null, {abandoned: true});
}
this._pendingCallback = callback;
this._pendingLoadDataParams = params;
if (this._state &&
this._state !== 'Idle') {
this._state = 'NeedsLoadData';
} else {
this._state = 'Coalescing';
this._loadData();
}
}
/**
* Internal implementation: called directly by `loadData`
* or by `coalesce` using stored parameters.
*/
_loadData() {
if (!this._pendingCallback || !this._pendingLoadDataParams) {
assert(false);
return;
}
const callback = this._pendingCallback;
const params = this._pendingLoadDataParams;
delete this._pendingCallback;
delete this._pendingLoadDataParams;
const perf = (params && params.request && params.request.collectResourceTiming) ?
new RequestPerformance(params.request) : false;
this.loadGeoJSON(params, (err: ?Error, data: ?Object) => {
if (err || !data) {
return callback(err);
} else if (typeof data !== 'object') {
return callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
} else {
rewind(data, true);
try {
if (params.filter) {
const compiled = createExpression(params.filter, {type: 'boolean', 'property-type': 'data-driven', overridable: false, transition: false});
if (compiled.result === 'error')
throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', '));
const features = data.features.filter(feature => compiled.value.evaluate({zoom: 0}, feature));
data = {type: 'FeatureCollection', features};
}
this._geoJSONIndex = params.cluster ?
new Supercluster(getSuperclusterOptions(params)).load(data.features) :
geojsonvt(data, params.geojsonVtOptions);
} catch (err) {
return callback(err);
}
this.loaded = {};
const result = {};
if (perf) {
const resourceTimingData = perf.finish();
// it's necessary to eval the result of getEntriesByName() here via parse/stringify
// late evaluation in the main thread causes TypeError: illegal invocation
if (resourceTimingData) {
result.resourceTiming = {};
result.resourceTiming[params.source] = JSON.parse(JSON.stringify(resourceTimingData));
}
}
callback(null, result);
}
});
}
/**
* While processing `loadData`, we coalesce all further
* `loadData` messages into a single call to _loadData
* that will happen once we've finished processing the
* first message. {@link GeoJSONSource#_updateWorkerData}
* is responsible for sending us the `coalesce` message
* at the time it receives a response from `loadData`
*
* State: Idle
* ↑ |
* 'coalesce' 'loadData'
* | (triggers load)
* | ↓
* State: Coalescing
* ↑ |
* (triggers load) |
* 'coalesce' 'loadData'
* | ↓
* State: NeedsLoadData
*/
coalesce() {
if (this._state === 'Coalescing') {
this._state = 'Idle';
} else if (this._state === 'NeedsLoadData') {
this._state = 'Coalescing';
this._loadData();
}
}
/**
* Implements {@link WorkerSource#reloadTile}.
*
* If the tile is loaded, uses the implementation in VectorTileWorkerSource.
* Otherwise, such as after a setData() call, we load the tile fresh.
*
* @param params
* @param params.uid The UID for this tile.
* @private
*/
reloadTile(params: WorkerTileParameters, callback: WorkerTileCallback) {
const loaded = this.loaded,
uid = params.uid;
if (loaded && loaded[uid]) {
return super.reloadTile(params, callback);
} else {
return this.loadTile(params, callback);
}
}
/**
* Fetch and parse GeoJSON according to the given params. Calls `callback`
* with `(err, data)`, where `data` is a parsed GeoJSON object.
*
* GeoJSON is loaded and parsed from `params.url` if it exists, or else
* expected as a literal (string or object) `params.data`.
*
* @param params
* @param [params.url] A URL to the remote GeoJSON data.
* @param [params.data] Literal GeoJSON data. Must be provided if `params.url` is not.
* @private
*/
loadGeoJSON(params: LoadGeoJSONParameters, callback: ResponseCallback) {
// Because of same origin issues, urls must either include an explicit
// origin or absolute path.
// ie: /foo/bar.json or http://example.com/bar.json
// but not ../foo/bar.json
if (params.request) {
getJSON(params.request, callback);
} else if (typeof params.data === 'string') {
try {
return callback(null, JSON.parse(params.data));
} catch (e) {
return callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
}
} else {
return callback(new Error(`Input data given to '${params.source}' is not a valid GeoJSON object.`));
}
}
removeSource(params: {source: string}, callback: Callback) {
if (this._pendingCallback) {
// Don't leak callbacks
this._pendingCallback(null, {abandoned: true});
}
callback();
}
getClusterExpansionZoom(params: {clusterId: number}, callback: Callback) {
try {
callback(null, this._geoJSONIndex.getClusterExpansionZoom(params.clusterId));
} catch (e) {
callback(e);
}
}
getClusterChildren(params: {clusterId: number}, callback: Callback>) {
try {
callback(null, this._geoJSONIndex.getChildren(params.clusterId));
} catch (e) {
callback(e);
}
}
getClusterLeaves(params: {clusterId: number, limit: number, offset: number}, callback: Callback>) {
try {
callback(null, this._geoJSONIndex.getLeaves(params.clusterId, params.limit, params.offset));
} catch (e) {
callback(e);
}
}
}
function getSuperclusterOptions({superclusterOptions, clusterProperties}) {
if (!clusterProperties || !superclusterOptions) return superclusterOptions;
const mapExpressions = {};
const reduceExpressions = {};
const globals = {accumulated: null, zoom: 0};
const feature = {properties: null};
const propertyNames = Object.keys(clusterProperties);
for (const key of propertyNames) {
const [operator, mapExpression] = clusterProperties[key];
const mapExpressionParsed = createExpression(mapExpression);
const reduceExpressionParsed = createExpression(
typeof operator === 'string' ? [operator, ['accumulated'], ['get', key]] : operator);
assert(mapExpressionParsed.result === 'success');
assert(reduceExpressionParsed.result === 'success');
mapExpressions[key] = mapExpressionParsed.value;
reduceExpressions[key] = reduceExpressionParsed.value;
}
superclusterOptions.map = (pointProperties) => {
feature.properties = pointProperties;
const properties = {};
for (const key of propertyNames) {
properties[key] = mapExpressions[key].evaluate(globals, feature);
}
return properties;
};
superclusterOptions.reduce = (accumulated, clusterProperties) => {
feature.properties = clusterProperties;
for (const key of propertyNames) {
globals.accumulated = accumulated[key];
accumulated[key] = reduceExpressions[key].evaluate(globals, feature);
}
};
return superclusterOptions;
}
export default GeoJSONWorkerSource;