Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] add cluster mapreduce feature to style spec #7004

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/generate-flow-typed-style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ ${flowObjectDeclaration('StyleSpecification', spec.$root)}

${flowObjectDeclaration('LightSpecification', spec.light)}

${flowObjectDeclaration('ClusterMapReduceSpecification', spec.clusterMapReduce)}

${spec.source.map(key => flowObjectDeclaration(flowSourceTypeName(key), spec[key])).join('\n\n')}

declare type SourceSpecification =
Expand Down
8 changes: 8 additions & 0 deletions flow-typed/style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,18 @@ declare type LightSpecification = {|
"intensity"?: PropertyValueSpecification<number>
|}

declare type ClusterMapReduceSpecification = {|
"initial"?: mixed,
"map"?: mixed,
"reduce"?: mixed
|}

declare type VectorSourceSpecification = {
"type": "vector",
"url"?: string,
"tiles"?: Array<string>,
"bounds"?: [number, number, number, number],
"scheme"?: "xyz" | "tms",
"minzoom"?: number,
"maxzoom"?: number,
"attribution"?: string
Expand Down Expand Up @@ -116,6 +123,7 @@ declare type GeojsonSourceSpecification = {|
"cluster"?: boolean,
"clusterRadius"?: number,
"clusterMaxZoom"?: number,
"clusterMapReduce"?: ClusterMapReduceSpecification,
"lineMetrics"?: boolean
|}

Expand Down
1 change: 1 addition & 0 deletions src/source/geojson_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class GeoJSONSource extends Evented implements Source {
(this.maxzoom - 1),
extent: EXTENT,
radius: (options.clusterRadius || 50) * scale,
mapreduce: options.clusterMapReduce,
log: false
}
}, options.workerOptions);
Expand Down
74 changes: 73 additions & 1 deletion src/source/geojson_worker_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import { getJSON } from '../util/ajax';

import performance from '../util/performance';
import styleSpec from '../style-spec/reference/latest';
import extend from '../style-spec/util/extend';
import rewind from '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, createPropertyExpression } from '../style-spec/expression';

import type {
WorkerTileParameters,
Expand All @@ -23,6 +26,8 @@ import type {LoadVectorDataCallback} from './vector_tile_worker_source';
import type {RequestParameters} from '../util/ajax';
import type { Callback } from '../types/callback';

type ExpressionFunction = typeof createExpression | typeof createPropertyExpression;

export type LoadGeoJSONParameters = {
request?: RequestParameters,
data?: string,
Expand Down Expand Up @@ -170,7 +175,7 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {

try {
this._geoJSONIndex = params.cluster ?
supercluster(params.superclusterOptions).load(data.features) :
supercluster(this._getSuperclusterOptions(params)).load(data.features) :
geojsonvt(data, params.geojsonVtOptions);
} catch (err) {
return callback(err);
Expand All @@ -193,6 +198,73 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource {
});
}


_getExpressions(values: any, expressionFunction: ExpressionFunction, spec: any) {
const expressions = {};

Object.keys(values).forEach(key => {
const expression = expressionFunction(values[key], spec[key] || spec['*']);
if (expression.result === 'success') {
expressions[key] = expression.value;
} else {
const messages = expression.value.map(({ message }) => message).join(', ');
const value = JSON.stringify(values[key]);

console.log(`Error evaluating property "${key}" with value (${value}): ${messages}`);
}
});

return expressions;
}

_getSuperclusterOptions(params: LoadGeoJSONParameters) {
const options = extend({}, params.superclusterOptions);
const { mapreduce } = options;

if (mapreduce) {
delete options.mapreduce;
const { initial, map, reduce } = mapreduce;
const globals = {};
const spec = styleSpec.clusterMapReduce;

if (initial) {
const expressions = this._getExpressions(initial, createExpression, spec.initial);

options.initial = () => Object.keys(expressions).reduce(
(_, key) => extend(_, ({ [key]: expressions[key].evaluate(globals) })),
{}
);
}

if (map) {
const expressions = this._getExpressions(map, createPropertyExpression, spec.map);

options.map = (properties) => Object.keys(expressions).reduce(
(_, key) => extend(_, ({ [key]: expressions[key].evaluate(globals, { properties }) })),
{}
);
}

if (reduce) {
const expressions = this._getExpressions(reduce, createPropertyExpression, spec.reduce);

options.reduce = (cluster, point) => {
const properties = { cluster, point };
const newValues = Object.keys(expressions).reduce(
(_, key) => extend(_, ({ [key]: expressions[key].evaluate(globals, { properties }) })),
{}
);

Object.keys(newValues).forEach(key => {
cluster[key] = newValues[key];
});
};
}
}

return options;
}

/**
* While processing `loadData`, we coalesce all further
* `loadData` messages into a single call to _loadData
Expand Down
50 changes: 50 additions & 0 deletions src/style-spec/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,30 @@
"type": "number",
"doc": "Max zoom on which to cluster points if clustering is enabled. Defaults to one zoom less than maxzoom (so that last zoom features are not clustered)."
},
"clusterMapReduce": {
"type": "clusterMapReduce",
"doc": "Execute mapreduce functions on cluster properties to get more meaningful data.",
"example": {
"initial": {
"max": 1,
"sum": 0
},
"map": {
"max": ["get", "scalerank"],
"sum": ["get", "scalerank"]
},
"reduce": {
"max": ["max",
["get", "max", ["get", "cluster"]],
["get", "max", ["get", "point"]]
],
"sum": ["+",
["get", "sum", ["get", "cluster"]],
["get", "sum", ["get", "point"]]
]
}
}
},
"lineMetrics": {
"type": "boolean",
"default": false,
Expand Down Expand Up @@ -3229,6 +3253,32 @@
}
}
},
"clusterMapReduce": {
"initial": {
"type": "*",
"*": {
"type": "*",
"property-type": "data-constant"
},
"doc": "Initial properties of a cluster (before runnning the reducer)."
},
"map": {
"type": "*",
"*": {
"type": "*",
"property-type": "data-driven"
},
"doc": "Properties to use for individual points when running the reducer. Point properties can be accessed directly."
},
"reduce": {
"type": "*",
"*": {
"type": "*",
"property-type": "data-driven"
},
"doc": "Properties to add to the cluster. The cluster properties can be accessed from the `cluster` property (e.g. `[\"get\", \"total\", [\"get\", \"cluster\"]]`), and the properties mapped in `map` can be accessed from `point` (e.g. `[\"get\", \"total\", [\"get\", \"point\"]]`)."
}
},
"paint": [
"paint_fill",
"paint_line",
Expand Down
Binary file modified test/integration/render-tests/geojson/clustered/expected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 33 additions & 3 deletions test/integration/render-tests/geojson/clustered/style.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,27 @@
"type": "geojson",
"data": "local://data/places.geojson",
"cluster": true,
"clusterRadius": 25
"clusterRadius": 25,
"clusterMapReduce": {
"initial": {
"max": 1,
"sum": 0
},
"map": {
"max": ["get", "scalerank"],
"sum": ["get", "scalerank"]
},
"reduce": {
"max": ["max",
["get", "max", ["get", "cluster"]],
["get", "max", ["get", "point"]]
],
"sum": ["+",
["get", "sum", ["get", "cluster"]],
["get", "sum", ["get", "point"]]
]
}
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
Expand All @@ -32,7 +52,7 @@
],
"paint": {
"circle-color": "rgba(0, 200, 0, 1)",
"circle-radius": 20
"circle-radius": ["*", ["get", "max"], 5]
}
},
{
Expand All @@ -45,7 +65,17 @@
true
],
"layout": {
"text-field": "{point_count}",
"text-field": ["to-string",
["/",
["round",
["*",
["/", ["get", "sum"], ["get", "point_count"]],
100
]
],
100
]
],
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
Expand Down