UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

295 lines (247 loc) 8.69 kB
// Copyright (c) 2021 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/geo-layers'; 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 TripInfoModalFactory from './trip-info-modal'; const zoomFactorValue = 8; export const defaultThickness = 0.5; export const defaultLineWidth = 1; export const tripVisConfigs = { opacity: 'opacity', thickness: { type: 'number', defaultValue: defaultThickness, 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}) => dc => d => dc.valueAt(d.index, 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() { const visualChannels = super.visualChannels; return { ...visualChannels, color: { ...visualChannels.color, accessor: 'getColor', nullValue: visualChannels.color.nullValue, getAttributeValue: config => d => d.properties.lineColor || config.color, // used this to get updateTriggers defaultValue: config => config.color }, size: { ...visualChannels.size, property: 'stroke', accessor: 'getWidth', condition: config => config.visConfig.stroked, nullValue: 0, getAttributeValue: () => d => d.properties.lineWidth || defaultLineWidth } }; } get animationDomain() { return this.config.animation.domain; } get layerInfoModal() { return { id: 'iconInfo', template: this._layerInfoModal, modalProps: { title: 'modal.tripInfo.title' } }; } getPositionAccessor(dataContainer) { return this.getFeature(this.config.columns)(dataContainer); } static findDefaultLayerProps({label, fields = [], dataContainer, 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(dataContainer, 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, dataContainer) { // index for dataContainer is saved to feature.properties return dataContainer.row(object.properties.index); } calculateDataAttribute({dataContainer, filteredIndex}, getPosition) { return filteredIndex .map(i => this.dataToFeature[i]) .filter(d => d && d.geometry.type === 'LineString'); } formatLayerData(datasets, oldLayerData) { // to-do: parse segment from dataContainer const {dataContainer, gpuFilter} = datasets[this.config.dataId]; const {data} = this.updateData(datasets, oldLayerData); const customFilterValueAccessor = (dc, f, fieldIndex) => { return dc.valueAt(f.properties.index, fieldIndex); }; const indexAccessor = f => f.properties.index; const dataAccessor = dc => d => ({index: d.properties.index}); const accessors = this.getAttributeAccessors({dataAccessor, dataContainer}); return { data, getFilterValue: gpuFilter.filterValueAccessor(dataContainer)( indexAccessor, customFilterValueAccessor ), getPath: d => d.geometry.coordinates, getTimestamps: d => this.dataToTimeStamp[d.properties.index], ...accessors }; } updateAnimationDomain(domain) { this.updateLayerConfig({ animation: { ...this.config.animation, domain } }); } updateLayerMeta(dataContainer) { const getFeature = this.getPositionAccessor(dataContainer); if (getFeature === this.meta.getFeature) { // TODO: revisit this after gpu filtering return; } this.dataToFeature = getGeojsonDataMaps(dataContainer, 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({dataContainer}) { this.updateLayerMeta(dataContainer); return this; } renderLayer(opts) { const {data, gpuFilter, mapState, animationConfig} = opts; const {visConfig} = this.config; const zoomFactor = this.getZoomFactor(mapState); const isValidTime = animationConfig && Array.isArray(animationConfig.domain) && animationConfig.domain.every(Number.isFinite) && Number.isFinite(animationConfig.currentTime); if (!isValidTime) { return []; } const domain0 = animationConfig.domain?.[0]; const updateTriggers = { ...this.getVisualChannelUpdateTriggers(), getTimestamps: { columns: this.config.columns, domain0 }, getFilterValue: gpuFilter.filterValueUpdateTriggers }; const defaultLayerProps = this.getDefaultDeckLayerProps(opts); return [ new DeckGLTripsLayer({ ...defaultLayerProps, ...data, getTimestamps: d => data.getTimestamps(d).map(ts => ts - domain0), widthScale: this.config.visConfig.thickness * zoomFactor * zoomFactorValue, rounded: true, wrapLongitude: false, parameters: { depthTest: mapState.dragRotate, depthMask: false }, trailLength: visConfig.trailLength * 1000, currentTime: animationConfig.currentTime - domain0, updateTriggers }) ]; } }