kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
315 lines (269 loc) • 8.58 kB
JavaScript
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import memoize from 'lodash.memoize';
import Layer from '../base-layer';
import {GeoJsonLayer} from 'deck.gl';
import H3HexagonCellLayer from 'deckgl-layers/h3-hexagon-cell-layer/h3-hexagon-cell-layer';
import {getVertices, getCentroid, idToPolygonGeo} from './h3-utils';
import H3HexagonLayerIcon from './h3-hexagon-layer-icon';
import {CHANNEL_SCALES} from 'constants/default-settings';
export const HEXAGON_ID_FIELDS = {
hex_id: ['hex_id', 'hexagon_id', 'h3_id']
};
export const hexIdRequiredColumns = ['hex_id'];
export const hexIdAccessor = ({hex_id}) => d => d[hex_id.fieldIdx];
export const hexIdResolver = ({hex_id}) => hex_id.fieldIdx;
export const HexagonIdVisConfigs = {
opacity: 'opacity',
colorRange: 'colorRange',
coverage: 'coverage',
sizeRange: 'elevationRange',
coverageRange: 'coverageRange',
elevationScale: 'elevationScale',
'hi-precision': 'hi-precision'
};
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return [r, g, b];
}
export default class HexagonIdLayer extends Layer {
constructor(props) {
super(props);
this.registerVisConfig(HexagonIdVisConfigs);
this.getHexId = memoize(hexIdAccessor, hexIdResolver);
}
get type() {
return 'hexagonId';
}
get name() {
return 'H3';
}
get requiredLayerColumns() {
return hexIdRequiredColumns;
}
get layerIcon() {
// use hexagon layer icon for now
return H3HexagonLayerIcon;
}
get visualChannels() {
return {
...super.visualChannels,
size: {
...super.visualChannels.size,
property: 'height'
},
coverage: {
property: 'coverage',
field: 'coverageField',
scale: 'coverageScale',
domain: 'coverageDomain',
range: 'coverageRange',
key: 'coverage',
channelScaleType: CHANNEL_SCALES.radius
}
};
}
static findDefaultLayerProps({fields}) {
const foundColumns = this.findDefaultColumnField(HEXAGON_ID_FIELDS, fields);
if (!foundColumns || !foundColumns.length) {
return null;
}
return foundColumns.map(columns => ({
isVisible: true,
label: 'H3 Hexagon',
columns
}));
}
getDefaultLayerConfig(props = {}) {
return {
...super.getDefaultLayerConfig(props),
// add height visual channel
coverageField: null,
coverageDomain: [0, 1],
coverageScale: 'linear'
};
}
formatLayerData(_, allData, filteredIndex, oldLayerData, opt = {}) {
const {
colorScale,
colorDomain,
colorField,
color,
columns,
sizeField,
sizeScale,
sizeDomain,
coverageField,
coverageScale,
coverageDomain,
visConfig: {sizeRange, colorRange, coverageRange}
} = this.config;
// color
const cScale =
colorField &&
this.getVisChannelScale(
colorScale,
colorDomain,
colorRange.colors.map(c => hexToRgb(c))
);
// height
const sScale =
sizeField && this.getVisChannelScale(sizeScale, sizeDomain, sizeRange);
// coverage
const coScale =
coverageField && this.getVisChannelScale(coverageScale, coverageDomain, coverageRange);
const getHexId = this.getHexId(columns);
if (!oldLayerData || oldLayerData.getHexId !== getHexId) {
this.updateLayerMeta(allData, getHexId);
}
let data;
if (
oldLayerData &&
oldLayerData.data &&
opt.sameData &&
oldLayerData.getHexId === getHexId
) {
data = oldLayerData.data;
} else {
data = filteredIndex.reduce((accu, index, i) => {
const id = getHexId(allData[index]);
const centroid = this.dataToFeature.centroids[index];
if (centroid) {
accu.push({
// keep a reference to the original data index
index: i,
data: allData[index],
id,
centroid
});
}
return accu;
}, []);
}
const getElevation = sScale ? d =>
this.getEncodedChannelValue(sScale, d.data, sizeField, 0) : 0;
const getColor = cScale ? d =>
this.getEncodedChannelValue(cScale, d.data, colorField) : color;
const getCoverage = coScale ? d =>
this.getEncodedChannelValue(coScale, d.data, coverageField, 0) : 1;
// const layerData = {
return {
data,
getElevation,
getColor,
getHexId,
getCoverage,
hexagonVertices: this.dataToFeature.hexagonVertices,
hexagonCenter: this.dataToFeature.hexagonCenter
};
}
updateLayerMeta(allData, getHexId) {
let hexagonVertices;
let hexagonCenter;
const centroids = {};
allData.forEach((d, index) => {
const id = getHexId(d);
if (typeof id !== 'string' || !id.length) {
return;
}
// find hexagonVertices
// only need 1 instance of hexagonVertices
if (!hexagonVertices) {
hexagonVertices = id && getVertices({id});
hexagonCenter = id && getCentroid({id})
}
// save a reference of centroids to dataToFeature
// so we don't have to re calculate it again
centroids[index] = getCentroid({id});
});
const bounds = this.getPointsBounds(Object.values(centroids), d => d);
const lightSettings = this.getLightSettingsFromBounds(bounds);
this.dataToFeature = {hexagonVertices, hexagonCenter, centroids};
this.updateMeta({bounds, lightSettings});
}
renderLayer({
data,
idx,
layerInteraction,
objectHovered,
mapState,
interactionConfig
}) {
const zoomFactor = this.getZoomFactor(mapState);
const eleZoomFactor = this.getElevationZoomFactor(mapState);
const {config, meta} = this;
const {visConfig} = config;
const updateTriggers = {
getColor: {
color: config.color,
colorField: config.colorField,
colorRange: config.visConfig.colorRange,
colorScale: config.colorScale
},
getElevation: {
sizeField: config.sizeField,
sizeRange: config.visConfig.sizeRange
},
getCoverage: {
coverageField: config.coverageField,
coverageRange: config.visConfig.coverageRange
}
};
return [
new H3HexagonCellLayer({
...layerInteraction,
...data,
id: this.id,
idx,
pickable: true,
// coverage
coverage: config.coverageField ? 1 : visConfig.coverage,
// parameters
parameters: {depthTest: Boolean(config.sizeField || mapState.dragRotate)},
// highlight
autoHighlight: Boolean(config.sizeField),
// elevation
extruded: Boolean(config.sizeField),
elevationScale: visConfig.elevationScale * eleZoomFactor,
// color
opacity: visConfig.opacity,
// render
lightSettings: meta.lightSettings,
updateTriggers
}),
...(this.isLayerHovered(objectHovered) && !config.sizeField
? [
new GeoJsonLayer({
id: `${this.id}-hovered`,
data: [
idToPolygonGeo(objectHovered, {
lineColor: config.highlightColor
})
],
lineWidthScale: 8 * zoomFactor
})
]
: [])
];
}
}