Skip to content

Commit

Permalink
Add methods for inspecting GeoJSON clusters (#6829)
Browse files Browse the repository at this point in the history
* upgrade supercluster to v4.0.1

* add GeoJSONSource getClusterExpansionZoom method

* add getClusterChildren and getClusterLeaves methods

* improve flow typings
  • Loading branch information
mourner authored Jun 21, 2018
1 parent 20c0d13 commit bf88cd7
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 69 deletions.
152 changes: 95 additions & 57 deletions debug/cluster.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,67 +18,105 @@
<script src='/debug/access_token_generated.js'></script>
<script>

var style = {
"version": 8,
"metadata": {
"test": {
"native": false,
"width": 512,
"height": 512
var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 0,
center: [0, 0],
style: 'mapbox://styles/mapbox/cjf4m44iw0uza2spb3q0a7s41',
hash: true
});

map.on('load', () => {
map.addSource('geojson', {
"type": "geojson",
"data": "/test/integration/data/places.geojson",
"cluster": true,
"clusterRadius": 50
});
map.addLayer({
"id": "cluster",
"type": "circle",
"source": "geojson",
"filter": ["==", "cluster", true],
"paint": {
"circle-color": "rgba(0, 200, 0, 1)",
"circle-radius": 20
}
},
"center": [0, 0],
"zoom": 0,
"sources": {
"geojson": {
"type": "geojson",
"data": "/test/integration/data/places.geojson",
"cluster": true,
"clusterRadius": 25
});
map.addLayer({
"id": "cluster_label",
"type": "symbol",
"source": "geojson",
"filter": ["==", "cluster", true],
"layout": {
"text-field": "{point_count}",
"text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
"text-size": 12,
"text-allow-overlap": true,
"text-ignore-placement": true
}
},
"glyphs": "/test/integration/glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "cluster",
"type": "circle",
"source": "geojson",
"filter": ["==", "cluster", true],
"paint": {
"circle-color": "rgba(0, 200, 0, 1)",
"circle-radius": 20
}
},
{
"id": "cluster_label",
"type": "symbol",
"source": "geojson",
"filter": ["==", "cluster", true],
"layout": {
"text-field": "{point_count}",
"text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
"text-size": 12,
"text-allow-overlap": true,
"text-ignore-placement": true
}
},
{
"id": "unclustered_point",
"type": "circle",
"source": "geojson",
"filter": ["!=", "cluster", true],
"paint": {
"circle-color": "rgba(0, 0, 200, 1)",
"circle-radius": 10
}
});
map.addLayer({
"id": "unclustered_point",
"type": "circle",
"source": "geojson",
"filter": ["!=", "cluster", true],
"paint": {
"circle-color": "rgb(0, 0, 200)",
"circle-radius": 10
}
]
};
});

var map = window.map = new mapboxgl.Map({
container: 'map',
style: style,
hash: true
var hoverData = {
"type": "FeatureCollection",
"features": []
};

map.addSource('cluster_children', {
"type": "geojson",
"data": hoverData
});

map.addLayer({
"id": "cluster_children",
"type": "circle",
"source": "cluster_children",
"paint": {
"circle-color": "rgb(0, 150, 0)",
"circle-radius": 7
}
});

function updateHover(features) {
hoverData.features = features;
map.getSource('cluster_children').setData(hoverData);
}

map.on('click', 'cluster', (e) => {
var feature = e.features[0];
map.getSource('geojson').getClusterExpansionZoom(feature.properties.cluster_id, (err, zoom) => {
if (err) throw err;
updateHover([]);
map.easeTo({
center: feature.geometry.coordinates,
zoom: zoom
});
});
});

map.on('mouseenter', 'cluster', (e) => {
map.getCanvas().style.cursor = 'pointer';

map.getSource('geojson').getClusterLeaves(e.features[0].properties.cluster_id, Infinity, 0, (err, features) => {
if (err) throw err;
updateHover(features);
});
});

map.on('mouseleave', 'cluster', (e) => {
map.getCanvas().style.cursor = '';
updateHover([]);
});
});

map.addControl(new mapboxgl.NavigationControl());
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"rw": "^1.3.3",
"shuffle-seed": "^1.1.6",
"sort-object": "^0.3.2",
"supercluster": "^2.3.0",
"supercluster": "^4.0.1",
"through2": "^2.0.3",
"tinyqueue": "^1.1.0",
"vt-pbf": "^3.0.1"
Expand Down
48 changes: 46 additions & 2 deletions src/source/geojson_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ class GeoJSONSource extends Evented implements Source {
this.fire(new Event('dataloading', {dataType: 'source'}));
this._updateWorkerData((err) => {
if (err) {
return this.fire(new ErrorEvent(err));
this.fire(new ErrorEvent(err));
return;
}

const data: Object = { dataType: 'source', sourceDataType: 'content' };
Expand All @@ -189,12 +190,55 @@ class GeoJSONSource extends Evented implements Source {
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.dispatcher.send('geojson.getClusterExpansionZoom', { clusterId, source: this.id }, callback, this.workerID);
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.dispatcher.send('geojson.getClusterChildren', { clusterId, source: this.id }, callback, this.workerID);
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
*/
getClusterLeaves(clusterId: number, limit: number, offset: number, callback: Callback<Array<GeoJSONFeature>>) {
this.dispatcher.send('geojson.getClusterLeaves', {
source: this.id,
clusterId,
limit,
offset
}, callback, this.workerID);
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: Function) {
_updateWorkerData(callback: Callback<void>) {
const options = extend({}, this.workerOptions);
const data = this._data;
if (typeof data === 'string') {
Expand Down
20 changes: 18 additions & 2 deletions src/source/geojson_worker_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import type {LoadVectorDataCallback} from './vector_tile_worker_source';
import type {RequestParameters} from '../util/ajax';
import type { Callback } from '../types/callback';

export type GeoJSON = Object;

export type LoadGeoJSONParameters = {
request?: RequestParameters,
data?: string,
Expand All @@ -37,6 +35,12 @@ export type LoadGeoJSONParameters = {
export type LoadGeoJSON = (params: LoadGeoJSONParameters, callback: Callback<mixed>) => void;

export interface GeoJSONIndex {
getTile(z: number, x: number, y: number): Object;

// supercluster methods
getClusterExpansionZoom(clusterId: number): number;
getChildren(clusterId: number): Array<GeoJSONFeature>;
getLeaves(clusterId: number, limit: number, offset: number): Array<GeoJSONFeature>;
}

function loadGeoJSONTile(params: WorkerTileParameters, callback: LoadVectorDataCallback) {
Expand Down Expand Up @@ -274,6 +278,18 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
}
callback();
}

getClusterExpansionZoom(params: {clusterId: number}, callback: Callback<number>) {
callback(null, this._geoJSONIndex.getClusterExpansionZoom(params.clusterId));
}

getClusterChildren(params: {clusterId: number}, callback: Callback<Array<GeoJSONFeature>>) {
callback(null, this._geoJSONIndex.getChildren(params.clusterId));
}

getClusterLeaves(params: {clusterId: number, limit: number, offset: number}, callback: Callback<Array<GeoJSONFeature>>) {
callback(null, this._geoJSONIndex.getLeaves(params.clusterId, params.limit, params.offset));
}
}

export default GeoJSONWorkerSource;
14 changes: 7 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5911,9 +5911,9 @@ just-extend@^1.1.27:
version "1.1.27"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905"

kdbush@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-1.0.1.tgz#3cbd03e9dead9c0f6f66ccdb96450e5cecc640e0"
kdbush@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-2.0.1.tgz#90c6128e3001ac68c550d7c9e2f222c0269666f1"

kebab-case@^1.0.0:
version "1.0.0"
Expand Down Expand Up @@ -9695,11 +9695,11 @@ sugarss@^1.0.0:
dependencies:
postcss "^6.0.14"

supercluster@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-2.3.0.tgz#87ab56081bbea9a1d724df5351ee9e8c3af2f48b"
supercluster@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-4.0.1.tgz#eead7ab49f50322b265e0087859ebcabdc5c2ed8"
dependencies:
kdbush "^1.0.1"
kdbush "^2.0.1"

supports-color@^2.0.0:
version "2.0.0"
Expand Down

0 comments on commit bf88cd7

Please sign in to comment.