kepler.gl.geoiq
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
344 lines (294 loc) • 9.33 kB
JavaScript
// Copyright (c) 2023 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 uniq from 'lodash.uniq';
import Layer from '../base-layer';
import {TripsLayer as DeckGLTripsLayer} from 'deck.gl';
import {GEOJSON_FIELDS} from 'constants/default-settings';
import TripLayerIcon from './trip-layer-icon';
import {
getGeojsonDataMaps,
getGeojsonBounds,
getGeojsonFeatureTypes
} from 'layers/geojson-layer/geojson-utils';
import {
isTripGeoJsonField,
parseTripGeoJsonTimestamp
} from './trip-utils';
import {hexToRgb} from 'utils/color-utils';
import TripInfoModalFactory from './trip-info-modal';
export const tripVisConfigs = {
opacity: 'opacity',
thickness: {
type: 'number',
defaultValue: 0.5,
label: 'Stroke Width',
isRanged: false,
range: [0, 100],
step: 0.1,
group: 'stroke',
property: 'thickness'
},
colorRange: 'colorRange',
trailLength: 'trailLength',
sizeRange: 'strokeWidthRange'
};
export const geoJsonRequiredColumns = ['geojson'];
export const featureAccessor = ({geojson}) => d => d[geojson.fieldIdx];
export const featureResolver = ({geojson}) => geojson.fieldIdx;
export default class TripLayer extends Layer {
constructor(props) {
super(props);
this.dataToFeature = [];
this.dataToTimeStamp = [];
this.registerVisConfig(tripVisConfigs);
this.getFeature = memoize(featureAccessor, featureResolver);
this._layerInfoModal = TripInfoModalFactory();
}
get type() {
return 'trip';
}
get name() {
return 'Trip';
}
get layerIcon() {
return TripLayerIcon;
}
get requiredLayerColumns() {
return geoJsonRequiredColumns;
}
get visualChannels() {
return {
...super.visualChannels,
size: {
...super.visualChannels.size,
property: 'stroke',
condition: config => config.visConfig.stroked
}
};
}
get animationDomain() {
return this.config.animation.domain;
}
get layerInfoModal() {
return {
id: 'iconInfo',
template: this._layerInfoModal,
modalProps: {
title: 'How to enable trip animation'
}
};
}
getPositionAccessor() {
return this.getFeature(this.config.columns);
}
static findDefaultLayerProps({label, fields = [], allData = [], id}, foundLayers) {
const geojsonColumns = fields
.filter(f => f.type === 'geojson')
.map(f => f.name);
const defaultColumns = {
geojson: uniq([...GEOJSON_FIELDS.geojson, ...geojsonColumns])
};
const geoJsonColumns = this.findDefaultColumnField(defaultColumns, fields);
const tripColumns = (geoJsonColumns || []).filter(col =>
isTripGeoJsonField(allData, fields[col.geojson.fieldIdx])
);
if (!tripColumns.length) {
return {props: []};
}
return {
props: tripColumns.map(columns => ({
label:
(typeof label === 'string' && label.replace(/\.[^/.]+$/, '')) ||
this.type,
columns,
isVisible: true
})),
// if a geojson layer is created from this column, delete it
foundLayers: foundLayers.filter(
prop =>
prop.type !== 'geojson' ||
prop.dataId !== id ||
!tripColumns.find(c => prop.columns.geojson.name === c.geojson.name)
)
};
}
getDefaultLayerConfig(props) {
return {
...super.getDefaultLayerConfig(props),
animation: {
enabled: true,
domain: null
}
};
}
getHoverData(object, allData) {
// index of allData is saved to feature.properties
return allData[object.properties.index];
}
// TODO: fix complexity
/* eslint-disable complexity */
formatLayerData(_, allData, filteredIndex, oldLayerData, opt = {}) {
// to-do: parse segment from allData
const {
colorScale,
colorField,
colorDomain,
color,
sizeScale,
sizeDomain,
sizeField,
visConfig
} = this.config;
const {stroked, colorRange, sizeRange} = visConfig;
const getFeature = this.getPositionAccessor(this.config.column);
// geojson feature are object, if doesn't exists
// create it and save to layer
if (!oldLayerData || oldLayerData.getFeature !== getFeature) {
this.updateLayerMeta(allData, getFeature);
}
let geojsonData;
if (
oldLayerData &&
oldLayerData.data &&
opt.sameData &&
oldLayerData.getFeature === getFeature
) {
// no need to create a new array of data
// use updateTriggers to selectively re-calculate attributes
geojsonData = oldLayerData.data;
} else {
// filteredIndex is a reference of index in allData which can map to feature
geojsonData = filteredIndex
.map(i => this.dataToFeature[i])
.filter(d => d && d.geometry.type === 'LineString');
}
// color
const cScale =
colorField &&
this.getVisChannelScale(
colorScale,
colorDomain,
colorRange.colors.map(hexToRgb)
);
// calculate stroke scale - if stroked = true
const sScale =
sizeField &&
stroked &&
this.getVisChannelScale(sizeScale, sizeDomain, sizeRange);
return {
data: geojsonData,
getPath: d => d.geometry.coordinates,
getTimestamps: d => this.dataToTimeStamp[d.properties.index],
getColor: d =>
cScale
? this.getEncodedChannelValue(
cScale,
allData[d.properties.index],
colorField
)
: d.properties.fillColor || color,
getWidth: d =>
sScale
? this.getEncodedChannelValue(
sScale,
allData[d.properties.index],
sizeField,
0
)
: d.properties.lineWidth || 1
};
}
/* eslint-enable complexity */
updateAnimationDomain(domain) {
this.updateLayerConfig({
animation: {
...this.config.animation,
domain
}
});
}
updateLayerMeta(allData) {
const getFeature = this.getPositionAccessor();
if (getFeature === this.meta.getFeature) {
// TODO: revisit this after gpu filtering
return;
}
this.dataToFeature = getGeojsonDataMaps(allData, getFeature);
const {dataToTimeStamp, animationDomain} =
parseTripGeoJsonTimestamp(this.dataToFeature);
this.dataToTimeStamp = dataToTimeStamp;
this.updateAnimationDomain(animationDomain);
// get bounds from features
const bounds = getGeojsonBounds(this.dataToFeature);
// keep a record of what type of geometry the collection has
const featureTypes = getGeojsonFeatureTypes(this.dataToFeature);
this.updateMeta({bounds, featureTypes, getFeature});
}
setInitialLayerConfig(allData) {
this.updateLayerMeta(allData);
return this;
}
renderLayer({data, idx, mapState, animationConfig}) {
const {visConfig} = this.config;
const zoomFactor = this.getZoomFactor(mapState);
const updateTriggers = {
getColor: {
color: this.config.color,
colorField: this.config.colorField,
colorRange: visConfig.colorRange,
colorScale: this.config.colorScale
},
getWidth: {
sizeField: this.config.sizeField,
sizeRange: visConfig.sizeRange
},
getTimestamps: {
columns: this.config.columns,
domain0: animationConfig.domain[0]
}
};
return [
new DeckGLTripsLayer({
id: this.id,
idx,
data: data.data,
getPath: data.getPath,
getColor: data.getColor,
getTimestamps: d =>
data.getTimestamps(d).map(ts => ts - animationConfig.domain[0]),
opacity: this.config.visConfig.opacity,
widthScale: this.config.visConfig.thickness * zoomFactor * 8,
highlightColor: this.config.highlightColor,
getWidth: data.getWidth,
rounded: true,
pickable: true,
autoHighlight: true,
parameters: {
depthTest: mapState.dragRotate,
depthMask: false
},
trailLength: visConfig.trailLength * 1000,
currentTime: animationConfig.currentTime - animationConfig.domain[0],
updateTriggers
})
];
}
}