Spaces:
Sleeping
Sleeping
| // @flow | |
| import {Event, ErrorEvent, Evented} from '../util/evented'; | |
| import {extend} from '../util/util'; | |
| import EXTENT from '../data/extent'; | |
| import {ResourceType} from '../util/ajax'; | |
| import browser from '../util/browser'; | |
| import type {Source} from './source'; | |
| import type Map from '../ui/map'; | |
| import type Dispatcher from '../util/dispatcher'; | |
| import type Tile from './tile'; | |
| import type Actor from '../util/actor'; | |
| import type {Callback} from '../types/callback'; | |
| import type {GeoJSON, GeoJSONFeature} from '@mapbox/geojson-types'; | |
| import type {GeoJSONSourceSpecification, PromoteIdSpecification} from '../style-spec/types'; | |
| /** | |
| * A source containing GeoJSON. | |
| * (See the [Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#sources-geojson) for detailed documentation of options.) | |
| * | |
| * @example | |
| * map.addSource('some id', { | |
| * type: 'geojson', | |
| * data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_ports.geojson' | |
| * }); | |
| * | |
| * @example | |
| * map.addSource('some id', { | |
| * type: 'geojson', | |
| * data: { | |
| * "type": "FeatureCollection", | |
| * "features": [{ | |
| * "type": "Feature", | |
| * "properties": {}, | |
| * "geometry": { | |
| * "type": "Point", | |
| * "coordinates": [ | |
| * -76.53063297271729, | |
| * 39.18174077994108 | |
| * ] | |
| * } | |
| * }] | |
| * } | |
| * }); | |
| * | |
| * @example | |
| * map.getSource('some id').setData({ | |
| * "type": "FeatureCollection", | |
| * "features": [{ | |
| * "type": "Feature", | |
| * "properties": { "name": "Null Island" }, | |
| * "geometry": { | |
| * "type": "Point", | |
| * "coordinates": [ 0, 0 ] | |
| * } | |
| * }] | |
| * }); | |
| * @see [Draw GeoJSON points](https://www.mapbox.com/mapbox-gl-js/example/geojson-markers/) | |
| * @see [Add a GeoJSON line](https://www.mapbox.com/mapbox-gl-js/example/geojson-line/) | |
| * @see [Create a heatmap from points](https://www.mapbox.com/mapbox-gl-js/example/heatmap/) | |
| * @see [Create and style clusters](https://www.mapbox.com/mapbox-gl-js/example/cluster/) | |
| */ | |
| class GeoJSONSource extends Evented implements Source { | |
| type: 'geojson'; | |
| id: string; | |
| minzoom: number; | |
| maxzoom: number; | |
| tileSize: number; | |
| attribution: string; | |
| promoteId: ?PromoteIdSpecification; | |
| isTileClipped: boolean; | |
| reparseOverscaled: boolean; | |
| _data: GeoJSON | string; | |
| _options: any; | |
| workerOptions: any; | |
| map: Map; | |
| actor: Actor; | |
| _loaded: boolean; | |
| _collectResourceTiming: boolean; | |
| _resourceTiming: Array<PerformanceResourceTiming>; | |
| _removed: boolean; | |
| /** | |
| * @private | |
| */ | |
| constructor(id: string, options: GeoJSONSourceSpecification & {workerOptions?: any, collectResourceTiming: boolean}, dispatcher: Dispatcher, eventedParent: Evented) { | |
| super(); | |
| this.id = id; | |
| // `type` is a property rather than a constant to make it easy for 3rd | |
| // parties to use GeoJSONSource to build their own source types. | |
| this.type = 'geojson'; | |
| this.minzoom = 0; | |
| this.maxzoom = 18; | |
| this.tileSize = 512; | |
| this.isTileClipped = true; | |
| this.reparseOverscaled = true; | |
| this._removed = false; | |
| this._loaded = false; | |
| this.actor = dispatcher.getActor(); | |
| this.setEventedParent(eventedParent); | |
| this._data = (options.data: any); | |
| this._options = extend({}, options); | |
| this._collectResourceTiming = options.collectResourceTiming; | |
| this._resourceTiming = []; | |
| if (options.maxzoom !== undefined) this.maxzoom = options.maxzoom; | |
| if (options.type) this.type = options.type; | |
| if (options.attribution) this.attribution = options.attribution; | |
| this.promoteId = options.promoteId; | |
| const scale = EXTENT / this.tileSize; | |
| // sent to the worker, along with `url: ...` or `data: literal geojson`, | |
| // so that it can load/parse/index the geojson data | |
| // extending with `options.workerOptions` helps to make it easy for | |
| // third-party sources to hack/reuse GeoJSONSource. | |
| this.workerOptions = extend({ | |
| source: this.id, | |
| cluster: options.cluster || false, | |
| geojsonVtOptions: { | |
| buffer: (options.buffer !== undefined ? options.buffer : 128) * scale, | |
| tolerance: (options.tolerance !== undefined ? options.tolerance : 0.375) * scale, | |
| extent: EXTENT, | |
| maxZoom: this.maxzoom, | |
| lineMetrics: options.lineMetrics || false, | |
| generateId: options.generateId || false | |
| }, | |
| superclusterOptions: { | |
| maxZoom: options.clusterMaxZoom !== undefined ? | |
| Math.min(options.clusterMaxZoom, this.maxzoom - 1) : | |
| (this.maxzoom - 1), | |
| minPoints: Math.max(2, options.clusterMinPoints || 2), | |
| extent: EXTENT, | |
| radius: (options.clusterRadius || 50) * scale, | |
| log: false, | |
| generateId: options.generateId || false | |
| }, | |
| clusterProperties: options.clusterProperties, | |
| filter: options.filter | |
| }, options.workerOptions); | |
| } | |
| load() { | |
| this.fire(new Event('dataloading', {dataType: 'source'})); | |
| this._updateWorkerData((err) => { | |
| if (err) { | |
| this.fire(new ErrorEvent(err)); | |
| return; | |
| } | |
| const data: Object = {dataType: 'source', sourceDataType: 'metadata'}; | |
| if (this._collectResourceTiming && this._resourceTiming && (this._resourceTiming.length > 0)) { | |
| data.resourceTiming = this._resourceTiming; | |
| this._resourceTiming = []; | |
| } | |
| // although GeoJSON sources contain no metadata, we fire this event to let the SourceCache | |
| // know its ok to start requesting tiles. | |
| this.fire(new Event('data', data)); | |
| }); | |
| } | |
| onAdd(map: Map) { | |
| this.map = map; | |
| this.load(); | |
| } | |
| /** | |
| * Sets the GeoJSON data and re-renders the map. | |
| * | |
| * @param {Object|string} data A GeoJSON data object or a URL to one. The latter is preferable in the case of large GeoJSON files. | |
| * @returns {GeoJSONSource} this | |
| */ | |
| setData(data: GeoJSON | string) { | |
| this._data = data; | |
| this.fire(new Event('dataloading', {dataType: 'source'})); | |
| this._updateWorkerData((err) => { | |
| if (err) { | |
| this.fire(new ErrorEvent(err)); | |
| return; | |
| } | |
| const data: Object = {dataType: 'source', sourceDataType: 'content'}; | |
| if (this._collectResourceTiming && this._resourceTiming && (this._resourceTiming.length > 0)) { | |
| data.resourceTiming = this._resourceTiming; | |
| this._resourceTiming = []; | |
| } | |
| this.fire(new Event('data', data)); | |
| }); | |
| return this; | |
| } | |
| /** | |
| * For clustered sources, fetches the zoom at which the given cluster expands. | |
| * | |
| * @param clusterId The value of the cluster's `cluster_id` property. | |
| * @param callback A callback to be called when the zoom value is retrieved (`(error, zoom) => { ... }`). | |
| * @returns {GeoJSONSource} this | |
| */ | |
| getClusterExpansionZoom(clusterId: number, callback: Callback<number>) { | |
| this.actor.send('geojson.getClusterExpansionZoom', {clusterId, source: this.id}, callback); | |
| return this; | |
| } | |
| /** | |
| * For clustered sources, fetches the children of the given cluster on the next zoom level (as an array of GeoJSON features). | |
| * | |
| * @param clusterId The value of the cluster's `cluster_id` property. | |
| * @param callback A callback to be called when the features are retrieved (`(error, features) => { ... }`). | |
| * @returns {GeoJSONSource} this | |
| */ | |
| getClusterChildren(clusterId: number, callback: Callback<Array<GeoJSONFeature>>) { | |
| this.actor.send('geojson.getClusterChildren', {clusterId, source: this.id}, callback); | |
| return this; | |
| } | |
| /** | |
| * For clustered sources, fetches the original points that belong to the cluster (as an array of GeoJSON features). | |
| * | |
| * @param clusterId The value of the cluster's `cluster_id` property. | |
| * @param limit The maximum number of features to return. | |
| * @param offset The number of features to skip (e.g. for pagination). | |
| * @param callback A callback to be called when the features are retrieved (`(error, features) => { ... }`). | |
| * @returns {GeoJSONSource} this | |
| * @example | |
| * // Retrieve cluster leaves on click | |
| * map.on('click', 'clusters', function(e) { | |
| * var features = map.queryRenderedFeatures(e.point, { | |
| * layers: ['clusters'] | |
| * }); | |
| * | |
| * var clusterId = features[0].properties.cluster_id; | |
| * var pointCount = features[0].properties.point_count; | |
| * var clusterSource = map.getSource('clusters'); | |
| * | |
| * clusterSource.getClusterLeaves(clusterId, pointCount, 0, function(error, features) { | |
| * // Print cluster leaves in the console | |
| * console.log('Cluster leaves:', error, features); | |
| * }) | |
| * }); | |
| */ | |
| getClusterLeaves(clusterId: number, limit: number, offset: number, callback: Callback<Array<GeoJSONFeature>>) { | |
| this.actor.send('geojson.getClusterLeaves', { | |
| source: this.id, | |
| clusterId, | |
| limit, | |
| offset | |
| }, callback); | |
| return this; | |
| } | |
| /* | |
| * Responsible for invoking WorkerSource's geojson.loadData target, which | |
| * handles loading the geojson data and preparing to serve it up as tiles, | |
| * using geojson-vt or supercluster as appropriate. | |
| */ | |
| _updateWorkerData(callback: Callback<void>) { | |
| this._loaded = false; | |
| const options = extend({}, this.workerOptions); | |
| const data = this._data; | |
| if (typeof data === 'string') { | |
| options.request = this.map._requestManager.transformRequest(browser.resolveURL(data), ResourceType.Source); | |
| options.request.collectResourceTiming = this._collectResourceTiming; | |
| } else { | |
| options.data = JSON.stringify(data); | |
| } | |
| // target {this.type}.loadData rather than literally geojson.loadData, | |
| // so that other geojson-like source types can easily reuse this | |
| // implementation | |
| this.actor.send(`${this.type}.loadData`, options, (err, result) => { | |
| if (this._removed || (result && result.abandoned)) { | |
| return; | |
| } | |
| this._loaded = true; | |
| if (result && result.resourceTiming && result.resourceTiming[this.id]) | |
| this._resourceTiming = result.resourceTiming[this.id].slice(0); | |
| // Any `loadData` calls that piled up while we were processing | |
| // this one will get coalesced into a single call when this | |
| // 'coalesce' message is processed. | |
| // We would self-send from the worker if we had access to its | |
| // message queue. Waiting instead for the 'coalesce' to round-trip | |
| // through the foreground just means we're throttling the worker | |
| // to run at a little less than full-throttle. | |
| this.actor.send(`${this.type}.coalesce`, {source: options.source}, null); | |
| callback(err); | |
| }); | |
| } | |
| loaded(): boolean { | |
| return this._loaded; | |
| } | |
| loadTile(tile: Tile, callback: Callback<void>) { | |
| const message = !tile.actor ? 'loadTile' : 'reloadTile'; | |
| tile.actor = this.actor; | |
| const params = { | |
| type: this.type, | |
| uid: tile.uid, | |
| tileID: tile.tileID, | |
| zoom: tile.tileID.overscaledZ, | |
| maxZoom: this.maxzoom, | |
| tileSize: this.tileSize, | |
| source: this.id, | |
| pixelRatio: browser.devicePixelRatio, | |
| showCollisionBoxes: this.map.showCollisionBoxes, | |
| promoteId: this.promoteId | |
| }; | |
| tile.request = this.actor.send(message, params, (err, data) => { | |
| delete tile.request; | |
| tile.unloadVectorData(); | |
| if (tile.aborted) { | |
| return callback(null); | |
| } | |
| if (err) { | |
| return callback(err); | |
| } | |
| tile.loadVectorData(data, this.map.painter, message === 'reloadTile'); | |
| return callback(null); | |
| }); | |
| } | |
| abortTile(tile: Tile) { | |
| if (tile.request) { | |
| tile.request.cancel(); | |
| delete tile.request; | |
| } | |
| tile.aborted = true; | |
| } | |
| unloadTile(tile: Tile) { | |
| tile.unloadVectorData(); | |
| this.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id}); | |
| } | |
| onRemove() { | |
| this._removed = true; | |
| this.actor.send('removeSource', {type: this.type, source: this.id}); | |
| } | |
| serialize() { | |
| return extend({}, this._options, { | |
| type: this.type, | |
| data: this._data | |
| }); | |
| } | |
| hasTransition() { | |
| return false; | |
| } | |
| } | |
| export default GeoJSONSource; | |