UNPKG

kepler.gl

Version:

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

436 lines (389 loc) 12.2 kB
// 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 uniq from 'lodash.uniq'; import Layer from '../base-layer'; import HighlightPolygonLayer from 'deckgl-layers/geojson-layer/solid-polygon-layer'; import {GeoJsonLayer as DeckGLGeoJsonLayer} from 'deck.gl'; import {hexToRgb} from 'utils/color-utils'; import { getGeojsonDataMaps, getGeojsonBounds, featureToDeckGlGeoType } from './geojson-utils'; import GeojsonLayerIcon from './geojson-layer-icon'; import {GEOJSON_FIELDS} from 'constants/default-settings'; export const geojsonVisConfigs = { 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', radius: 'radius', sizeRange: 'strokeWidthRange', radiusRange: 'radiusRange', heightRange: 'elevationRange', elevationScale: 'elevationScale', 'hi-precision': 'hi-precision', stroked: 'stroked', filled: 'filled', enable3d: 'enable3d', wireframe: 'wireframe' }; export const geoJsonRequiredColumns = ['geojson']; export const featureAccessor = ({geojson}) => d => d[geojson.fieldIdx]; export const featureResolver = ({geojson}) => geojson.fieldIdx; export default class GeoJsonLayer extends Layer { constructor(props) { super(props); this.dataToFeature = {}; this.registerVisConfig(geojsonVisConfigs); this.getFeature = memoize(featureAccessor, featureResolver); } get type() { return 'geojson'; } get name() { return 'Polygon'; } get layerIcon() { return GeojsonLayerIcon; } get requiredLayerColumns() { return geoJsonRequiredColumns; } get visualChannels() { return { ...super.visualChannels, size: { ...super.visualChannels.size, property: 'stroke', condition: config => config.visConfig.stroked }, height: { property: 'height', field: 'heightField', scale: 'heightScale', domain: 'heightDomain', range: 'heightRange', key: 'height', channelScaleType: 'size', condition: config => config.visConfig.enable3d }, radius: { property: 'radius', field: 'radiusField', scale: 'radiusScale', domain: 'radiusDomain', range: 'radiusRange', key: 'radius', channelScaleType: 'radius' } }; } static findDefaultLayerProps({label, fields}) { const geojsonColumns = fields .filter(f => f.type === 'geojson') .map(f => f.name); const defaultColumns = { geojson: uniq([...GEOJSON_FIELDS.geojson, ...geojsonColumns]) }; const foundColumns = this.findDefaultColumnField(defaultColumns, fields); if (!foundColumns || !foundColumns.length) { return []; } return foundColumns.map(columns => ({ label: typeof label === 'string' && label.replace(/\.[^/.]+$/, '') || this.type, columns, isVisible: true })); } getDefaultLayerConfig(props = {}) { return { ...super.getDefaultLayerConfig(props), // add height visual channel heightField: null, heightDomain: [0, 1], heightScale: 'linear', // add radius visual channel radiusField: null, radiusDomain: [0, 1], radiusScale: 'linear' }; } getHoverData(object, allData) { // index of allData is saved to feature.properties return allData[object.properties.index]; } formatLayerData(_, allData, filteredIndex, oldLayerData, opt = {}) { const { colorScale, colorField, colorDomain, color, sizeScale, sizeDomain, sizeField, heightField, heightDomain, heightScale, radiusField, radiusDomain, radiusScale, visConfig, columns } = this.config; const { enable3d, stroked, colorRange, heightRange, sizeRange, radiusRange } = visConfig; const getFeature = this.getFeature(columns); // 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); } 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); // calculate elevation scale - if extruded = true const eScale = heightField && enable3d && this.getVisChannelScale(heightScale, heightDomain, heightRange); // point radius const rScale = radiusField && this.getVisChannelScale(radiusScale, radiusDomain, radiusRange); return { data: geojsonData, getFeature, getFillColor: d => cScale ? this.getEncodedChannelValue( cScale, allData[d.properties.index], colorField ) : d.properties.fillColor || color, getLineColor: d => cScale ? this.getEncodedChannelValue( cScale, allData[d.properties.index], colorField ) : d.properties.lineColor || color, getLineWidth: d => sScale ? this.getEncodedChannelValue( sScale, allData[d.properties.index], sizeField, 0 ) : d.properties.lineWidth || 1, getElevation: d => eScale ? this.getEncodedChannelValue( eScale, allData[d.properties.index], heightField, 0 ) : d.properties.elevation || 500, getRadius: d => rScale ? this.getEncodedChannelValue( rScale, allData[d.properties.index], radiusField, 0 ) : d.properties.radius || 1 }; } updateLayerMeta(allData, getFeature) { this.dataToFeature = getGeojsonDataMaps(allData, getFeature); // calculate layer meta const allFeatures = Object.values(this.dataToFeature); // get bounds from features const bounds = getGeojsonBounds(allFeatures); // get lightSettings from points const lightSettings = this.getLightSettingsFromBounds(bounds); // if any of the feature has properties.hi-precision set to be true const fp64 = Boolean( allFeatures.find(d => d && d.properties && d.properties['hi-precision']) ); const fixedRadius = Boolean( allFeatures.find(d => d && d.properties && d.properties.radius) ); // keep a record of what type of geometry the collection has const featureTypes = allFeatures.reduce((accu, f) => { const geoType = featureToDeckGlGeoType( f && f.geometry && f.geometry.type ); if (geoType) { accu[geoType] = true; } return accu; }, {}); this.updateMeta({bounds, lightSettings, fp64, fixedRadius, featureTypes}); } renderLayer({ data, idx, objectHovered, mapState, interactionConfig }) { const {fp64, lightSettings, fixedRadius} = this.meta; const radiusScale = this.getRadiusScaleByZoom(mapState, fixedRadius); const zoomFactor = this.getZoomFactor(mapState); const {visConfig} = this.config; const layerProps = { // multiplier applied just so it being consistent with previously saved maps lineWidthScale: visConfig.thickness * zoomFactor * 8, lineWidthMinPixels: 1, elevationScale: visConfig.elevationScale, pointRadiusScale: radiusScale, fp64: fp64 || visConfig['hi-precision'], lineMiterLimit: 4 }; const updateTriggers = { getElevation: { heightField: this.config.heightField, heightScale: this.config.heightScale, heightRange: visConfig.heightRange }, getFillColor: { color: this.config.color, colorField: this.config.colorField, colorRange: visConfig.colorRange, colorScale: this.config.colorScale }, getLineColor: { color: this.config.color, colorField: this.config.colorField, colorRange: visConfig.colorRange, colorScale: this.config.colorScale }, getLineWidth: { sizeField: this.config.sizeField, sizeRange: visConfig.sizeRange }, getRadius: { radiusField: this.config.radiusField, radiusRange: visConfig.radiusRange } }; return [ new DeckGLGeoJsonLayer({ ...layerProps, id: this.id, idx, data: data.data, getFillColor: data.getFillColor, getLineColor: data.getLineColor, getLineWidth: data.getLineWidth, getRadius: data.getRadius, getElevation: data.getElevation, // highlight pickable: true, // highlightColor: this.config.highlightColor, autoHighlight: visConfig.enable3d, // parameters parameters: {depthTest: Boolean(visConfig.enable3d || mapState.dragRotate)}, opacity: visConfig.opacity, stroked: visConfig.stroked, filled: visConfig.filled, extruded: visConfig.enable3d, wireframe: visConfig.wireframe, lightSettings, updateTriggers, subLayers: { ...DeckGLGeoJsonLayer.defaultProps.subLayers, PolygonLayer: HighlightPolygonLayer } }), ...(this.isLayerHovered(objectHovered) && !visConfig.enable3d ? [ new DeckGLGeoJsonLayer({ ...layerProps, id: `${this.id}-hovered`, data: [ { ...objectHovered.object, properties: { ...objectHovered.object.properties, lineColor: this.config.highlightColor, fillColor: this.config.highlightColor }, getLineWidth: data.getLineWidth, getRadius: data.getRadius, getElevation: data.getElevation } ], updateTriggers, stroked: true, pickable: false, filled: false }) ] : []) ]; } }