@deck.gl/carto
Version:
CARTO official integration with Deck.gl. Build geospatial applications using CARTO and Deck.gl.
385 lines • 13.9 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { deviation, extent, groupSort, median, variance } from 'd3-array';
import { rgb } from 'd3-color';
import { scaleLinear, scaleOrdinal, scaleLog, scalePoint, scaleQuantile, scaleQuantize, scaleSqrt, scaleThreshold } from 'd3-scale';
import { format as d3Format } from 'd3-format';
import moment from 'moment-timezone';
import { GridLayer, HeatmapLayer, HexagonLayer } from '@deck.gl/aggregation-layers';
import { GeoJsonLayer } from '@deck.gl/layers';
import { H3HexagonLayer } from '@deck.gl/geo-layers';
import ClusterTileLayer from "../layers/cluster-tile-layer.js";
import H3TileLayer from "../layers/h3-tile-layer.js";
import QuadbinTileLayer from "../layers/quadbin-tile-layer.js";
import RasterTileLayer from "../layers/raster-tile-layer.js";
import VectorTileLayer from "../layers/vector-tile-layer.js";
import { assert, createBinaryProxy, scaleIdentity } from "../utils.js";
import HeatmapTileLayer from "../layers/heatmap-tile-layer.js";
const SCALE_FUNCS = {
linear: scaleLinear,
ordinal: scaleOrdinal,
log: scaleLog,
point: scalePoint,
quantile: scaleQuantile,
quantize: scaleQuantize,
sqrt: scaleSqrt,
custom: scaleThreshold,
identity: scaleIdentity
};
function identity(v) {
return v;
}
const UNKNOWN_COLOR = '#868d91';
export const AGGREGATION = {
average: 'MEAN',
maximum: 'MAX',
minimum: 'MIN',
sum: 'SUM'
};
export const OPACITY_MAP = {
getFillColor: 'opacity',
getLineColor: 'strokeOpacity',
getTextColor: 'opacity'
};
const AGGREGATION_FUNC = {
'count unique': (values, accessor) => groupSort(values, v => v.length, accessor).length,
median,
// Unfortunately mode() is only available in d3-array@3+ which is ESM only
mode: (values, accessor) => groupSort(values, v => v.length, accessor).pop(),
stddev: deviation,
variance
};
const TILE_LAYER_TYPE_TO_LAYER = {
clusterTile: ClusterTileLayer,
h3: H3TileLayer,
heatmapTile: HeatmapTileLayer,
mvt: VectorTileLayer,
quadbin: QuadbinTileLayer,
raster: RasterTileLayer,
tileset: VectorTileLayer
};
const hexToRGBA = c => {
const { r, g, b, opacity } = rgb(c);
return [r, g, b, 255 * opacity];
};
// Kepler prop value -> Deck.gl prop value
// Supports nested definitions, and function transforms:
// {keplerProp: 'deckProp'} is equivalent to:
// {keplerProp: x => ({deckProp: x})}
const sharedPropMap = {
// Apply the value of Kepler `color` prop to the deck `getFillColor` prop
color: 'getFillColor',
isVisible: 'visible',
label: 'cartoLabel',
textLabel: {
alignment: 'getTextAlignmentBaseline',
anchor: 'getTextAnchor',
// Apply the value of Kepler `textLabel.color` prop to the deck `getTextColor` prop
color: 'getTextColor',
size: 'getTextSize'
},
visConfig: {
enable3d: 'extruded',
elevationScale: 'elevationScale',
filled: 'filled',
strokeColor: 'getLineColor',
stroked: 'stroked',
thickness: 'getLineWidth',
radius: 'getPointRadius',
wireframe: 'wireframe'
}
};
const customMarkersPropsMap = {
color: 'getIconColor',
visConfig: {
radius: 'getIconSize'
}
};
const heatmapTilePropsMap = {
visConfig: {
colorRange: x => ({ colorRange: x.colors.map(hexToRGBA) }),
radius: 'radiusPixels'
}
};
const aggregationVisConfig = {
colorAggregation: x => ({ colorAggregation: AGGREGATION[x] || AGGREGATION.sum }),
colorRange: x => ({ colorRange: x.colors.map(hexToRGBA) }),
coverage: 'coverage',
elevationPercentile: ['elevationLowerPercentile', 'elevationUpperPercentile'],
percentile: ['lowerPercentile', 'upperPercentile']
};
const defaultProps = {
lineMiterLimit: 2,
lineWidthUnits: 'pixels',
pointRadiusUnits: 'pixels',
rounded: true,
wrapLongitude: false
};
function mergePropMaps(a = {}, b = {}) {
return { ...a, ...b, visConfig: { ...a.visConfig, ...b.visConfig } };
}
export function getLayer(type, config, dataset) {
let basePropMap = sharedPropMap;
if (config.visConfig?.customMarkers) {
basePropMap = mergePropMaps(basePropMap, customMarkersPropsMap);
}
if (type === 'heatmapTile') {
basePropMap = mergePropMaps(basePropMap, heatmapTilePropsMap);
}
if (TILE_LAYER_TYPE_TO_LAYER[type]) {
return getTileLayer(dataset, basePropMap, type);
}
const geoColumn = dataset?.geoColumn;
const getPosition = d => d[geoColumn].coordinates;
const hexagonId = config.columns?.hex_id;
const layerTypeDefs = {
point: {
Layer: GeoJsonLayer,
propMap: {
columns: {
altitude: x => ({ parameters: { depthWriteEnabled: Boolean(x) } })
},
visConfig: { outline: 'stroked' }
}
},
geojson: {
Layer: GeoJsonLayer
},
grid: {
Layer: GridLayer,
propMap: { visConfig: { ...aggregationVisConfig, worldUnitSize: x => ({ cellSize: 1000 * x }) } },
defaultProps: { getPosition }
},
heatmap: {
Layer: HeatmapLayer,
propMap: { visConfig: { ...aggregationVisConfig, radius: 'radiusPixels' } },
defaultProps: { getPosition }
},
hexagon: {
Layer: HexagonLayer,
propMap: { visConfig: { ...aggregationVisConfig, worldUnitSize: x => ({ radius: 1000 * x }) } },
defaultProps: { getPosition }
},
hexagonId: {
Layer: H3HexagonLayer,
propMap: { visConfig: { coverage: 'coverage' } },
defaultProps: { getHexagon: d => d[hexagonId], stroked: false }
}
};
const layer = layerTypeDefs[type];
assert(layer, `Unsupported layer type: ${type}`);
return {
...layer,
propMap: mergePropMaps(basePropMap, layer.propMap),
defaultProps: { ...defaultProps, ...layer.defaultProps }
};
}
function getTileLayer(dataset, basePropMap, type) {
const { aggregationExp, aggregationResLevel } = dataset;
return {
Layer: TILE_LAYER_TYPE_TO_LAYER[type] || VectorTileLayer,
propMap: basePropMap,
defaultProps: {
...defaultProps,
...(aggregationExp && { aggregationExp }),
...(aggregationResLevel && { aggregationResLevel }),
uniqueIdProperty: 'geoid'
}
};
}
function domainFromAttribute(attribute, scaleType, scaleLength) {
if (scaleType === 'ordinal' || scaleType === 'point') {
return attribute.categories.map(c => c.category).filter(c => c !== undefined && c !== null);
}
if (scaleType === 'quantile' && attribute.quantiles) {
return attribute.quantiles.global
? attribute.quantiles.global[scaleLength]
: attribute.quantiles[scaleLength];
}
let { min } = attribute;
if (scaleType === 'log' && min === 0) {
min = 1e-5;
}
return [min, attribute.max];
}
function domainFromValues(values, scaleType) {
if (scaleType === 'ordinal' || scaleType === 'point') {
return groupSort(values, g => -g.length, d => d);
}
else if (scaleType === 'quantile') {
return values.sort((a, b) => a - b);
}
else if (scaleType === 'log') {
const [d0, d1] = extent(values);
return [d0 === 0 ? 1e-5 : d0, d1];
}
return extent(values);
}
function calculateDomain(data, name, scaleType, scaleLength) {
if (data.tilestats) {
// Tileset data type
const { attributes } = data.tilestats.layers[0];
const attribute = attributes.find(a => a.attribute === name);
return domainFromAttribute(attribute, scaleType, scaleLength);
}
else if (data.features) {
// GeoJSON data type
const values = data.features.map(({ properties }) => properties[name]);
return domainFromValues(values, scaleType);
}
else if (Array.isArray(data) && data[0][name] !== undefined) {
// JSON data type
const values = data.map(properties => properties[name]);
return domainFromValues(values, scaleType);
}
return [0, 1];
}
function normalizeAccessor(accessor, data) {
if (data.features || data.tilestats) {
return (object, info) => {
if (object) {
return accessor(object.properties || object.__source.object.properties);
}
const { data, index } = info;
const proxy = createBinaryProxy(data, index);
return accessor(proxy);
};
}
return accessor;
}
export function opacityToAlpha(opacity) {
return opacity !== undefined ? Math.round(255 * Math.pow(opacity, 1 / 2.2)) : 255;
}
function getAccessorKeys(name, aggregation) {
let keys = [name];
if (aggregation) {
// Snowflake will capitalized the keys, need to check lower and upper case version
keys = keys.concat([aggregation, aggregation.toUpperCase()].map(a => `${name}_${a}`));
}
return keys;
}
function findAccessorKey(keys, properties) {
for (const key of keys) {
if (key in properties) {
return [key];
}
}
throw new Error(`Could not find property for any accessor key: ${keys}`);
}
export function getColorValueAccessor({ name }, colorAggregation, data) {
const aggregator = AGGREGATION_FUNC[colorAggregation];
const accessor = values => aggregator(values, p => p[name]);
return normalizeAccessor(accessor, data);
}
export function getColorAccessor({ name, colorColumn }, scaleType, { aggregation, range }, opacity, data) {
const scale = calculateLayerScale(colorColumn || name, scaleType, range, data);
const alpha = opacityToAlpha(opacity);
let accessorKeys = getAccessorKeys(name, aggregation);
const accessor = properties => {
if (!(accessorKeys[0] in properties)) {
accessorKeys = findAccessorKey(accessorKeys, properties);
}
const propertyValue = properties[accessorKeys[0]];
const { r, g, b } = rgb(scale(propertyValue));
return [r, g, b, propertyValue === null ? 0 : alpha];
};
return normalizeAccessor(accessor, data);
}
function calculateLayerScale(name, scaleType, range, data) {
const scale = SCALE_FUNCS[scaleType]();
let domain = [];
let scaleColor = [];
if (scaleType !== 'identity') {
const { colorMap, colors } = range;
if (Array.isArray(colorMap)) {
colorMap.forEach(([value, color]) => {
domain.push(value);
scaleColor.push(color);
});
}
else {
domain = calculateDomain(data, name, scaleType, colors.length);
scaleColor = colors;
}
if (scaleType === 'ordinal') {
domain = domain.slice(0, scaleColor.length);
}
}
scale.domain(domain);
scale.range(scaleColor);
scale.unknown(UNKNOWN_COLOR);
return scale;
}
const FALLBACK_ICON = 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiLz4NCjwvc3ZnPg==';
export function getIconUrlAccessor(field, range, { fallbackUrl, maxIconSize, useMaskedIcons }, data) {
const urlToUnpackedIcon = (url) => ({
id: `${url}@@${maxIconSize}`,
url,
width: maxIconSize,
height: maxIconSize,
mask: useMaskedIcons
});
let unknownValue = fallbackUrl || FALLBACK_ICON;
if (range?.othersMarker) {
unknownValue = range.othersMarker;
}
const unknownIcon = urlToUnpackedIcon(unknownValue);
if (!range || !field) {
return () => unknownIcon;
}
const mapping = {};
for (const { value, markerUrl } of range.markerMap) {
if (markerUrl) {
mapping[value] = urlToUnpackedIcon(markerUrl);
}
}
const accessor = properties => {
const propertyValue = properties[field.name];
return mapping[propertyValue] || unknownIcon;
};
return normalizeAccessor(accessor, data);
}
export function getMaxMarkerSize(visConfig, visualChannels) {
const { radiusRange, radius } = visConfig;
const { radiusField, sizeField } = visualChannels;
const field = radiusField || sizeField;
return Math.ceil(radiusRange && field ? radiusRange[1] : radius);
}
export function negateAccessor(accessor) {
return typeof accessor === 'function' ? (d, i) => -accessor(d, i) : -accessor;
}
export function getSizeAccessor({ name }, scaleType, aggregation, range, data) {
const scale = scaleType ? SCALE_FUNCS[scaleType]() : identity;
if (scaleType) {
if (aggregation !== 'count') {
scale.domain(calculateDomain(data, name, scaleType));
}
scale.range(range);
}
let accessorKeys = getAccessorKeys(name, aggregation);
const accessor = properties => {
if (!(accessorKeys[0] in properties)) {
accessorKeys = findAccessorKey(accessorKeys, properties);
}
const propertyValue = properties[accessorKeys[0]];
return scale(propertyValue);
};
return normalizeAccessor(accessor, data);
}
const FORMATS = {
date: s => moment.utc(s).format('MM/DD/YY HH:mm:ssa'),
integer: d3Format('i'),
float: d3Format('.5f'),
timestamp: s => moment.utc(s).format('X'),
default: String
};
export function getTextAccessor({ name, type }, data) {
const format = FORMATS[type] || FORMATS.default;
const accessor = properties => {
return format(properties[name]);
};
return normalizeAccessor(accessor, data);
}
export { domainFromValues as _domainFromValues };
//# sourceMappingURL=layer-map.js.map