UNPKG

kepler.gl.geoiq

Version:

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

687 lines (623 loc) 19.1 kB
// 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, {colorMaker} from '../base-layer'; import {GeoJsonLayer as DeckGLGeoJsonLayer} from 'deck.gl'; import {hexToRgb} from 'utils/color-utils'; import GeojsonLayerIcon from './geojson-layer-icon'; import { GEOJSON_FIELDS, HIGHLIGH_COLOR_3D, CHANNEL_SCALES, NO_VALUE_COLOR, ALL_FIELD_TYPES } from 'constants/default-settings'; import axios from 'axios'; import {ON_PREMESIS_URL} from 'constants/default-settings'; // import {featureCollection} from '@turf/helpers'; // import {saveText} from '../boundary-layer/boundary-layer'; 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' }, strokeColor: 'strokeColor', colorRange: 'colorRange', strokeColorRange: 'strokeColorRange', radius: 'radius', sizeRange: 'strokeWidthRange', radiusRange: 'radiusRange', heightRange: 'elevationRange', elevationScale: 'elevationScale', 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 'backendGeojson'; } get name() { return 'Polygon'; } get showLoader() { return true; } get layerIcon() { return GeojsonLayerIcon; } get requiredLayerColumns() { return geoJsonRequiredColumns; } get visualChannels() { return { ...super.visualChannels, // strokeColor: { // property: 'strokeColor', // field: 'strokeColorField', // scale: 'strokeColorScale', // domain: 'strokeColorDomain', // range: 'strokeColorRange', // key: 'strokeColor', // channelScaleType: CHANNEL_SCALES.color, // condition: config => config.visConfig.stroked // }, 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' } }; } shouldRenderColumnConfig() { return false; } updateLayerConfig(newConfig) { if ( Object.keys(newConfig).includes('colorField') || Object.keys(newConfig).includes('colorUI') || Object.keys(newConfig).includes('colorScale') ) { newConfig = {...newConfig, ...{legendApiCallRequest: true}}; if (!newConfig.colorField && this.config.colorField) { newConfig = {...newConfig, ...{apiCallRequest: true}}; } } this.config = {...this.config, ...newConfig}; return this; } axiosLegendAPICall(datasets, filters, auth, project) { const {dataId, colorField, colorScale, visConfig} = this.config; const {colorRange} = visConfig; const {indexName} = datasets[dataId]; const {uid} = auth; const {isEdit} = project; const noOfBreaks = colorRange.colors.length; filters = filters.filter(f => f.dataId.includes(dataId)); // const colorScale = 'quantile'; const legendAPIData = { colorField: colorField.name, userId: uid, colorFieldType: colorField.type, colorScale, permissionType: isEdit, noOfBreaks, indexName, filters }; const legendConfig = { headers: { 'Content-type': 'application/json' } }; const response = axios .post( `${ON_PREMESIS_URL}/geoiqutilities/legend/v1.0/fetch`, legendAPIData, legendConfig ) .then(result => { let breakPoints = result.data.data.Breakpoints; breakPoints = breakPoints.reduce((accu, breakpoint, index) => { if (accu.length < noOfBreaks) { accu.push({x1: breakpoint, x2: breakPoints[index + 1]}); } return accu; }, []); return breakPoints; // this.updateLayerConfig({ // legend: breakPoints, // apiCallRequest: true, // legendApiCallRequest: false // }); }); // this.updateLayerConfig({ // apiCallRequest: true, // legendApiCallRequest: false // }); return response; } axiosApiCall(datasets, viewport, zoom, filters, auth, project) { const {colorField, dataId, visConfig} = this.config; const {colorAggregation} = visConfig; const {indexName} = datasets[dataId]; const {isEdit} = project; const {uid} = auth; filters = filters.filter(f => f.dataId.includes(dataId)); const config = { headers: { 'Content-Type': 'application/json' } }; // aggregation type // filters // viewport // zoom const data = { colorField: colorField ? colorField.name : null, colorAggregation: colorField ? colorAggregation : 'count', colorFieldType: colorField ? colorField.type : null, filters: JSON.stringify(filters), indexName, // indexName, userId: uid, viewport: JSON.stringify(viewport), zoom, permissionType: isEdit }; // const url = 'https://app.geoiq.io/boundary/v1.0/boundary_query'; const response = axios .post( `${ON_PREMESIS_URL}/geoiqlayers/polygonlayer/v1.0/fetch`, data, config ) .then(function(response) { return response.data.data; }) .catch(); // const response = geoHashData; // var response = new Promise((resolve, reject) => { // let name = 'Dave' // if (name === 'Dave') { // resolve(testGeojsonData); // } // else { // reject(Error("Promise rejected")); // } // }); return response; } getPositionAccessor() { return this.getFeature(this.config.columns); } // 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 {props: []}; // } // return { // props: foundColumns.map(columns => ({ // label: // (typeof label === 'string' && label.replace(/\.[^/.]+$/, '')) || // this.type, // columns, // isVisible: true // })) // }; // } getDefaultLayerConfig(props = {}) { return { ...super.getDefaultLayerConfig(props), apiCallRequest: true, // add height visual channel heightField: null, heightDomain: [0, 1], heightScale: 'linear', // add radius visual channel radiusField: null, radiusDomain: [0, 1], radiusScale: 'linear', // add stroke color visual channel strokeColorField: null, strokeColorDomain: [0, 1], strokeColorScale: 'quantile' }; } getHoverData(object) { // index of allData is saved to feature.properties return object; } getEncodedChannelValue(scale, data, field, defaultValue = NO_VALUE_COLOR) { const {type} = field; const value = data; let attributeValue; if (type === ALL_FIELD_TYPES.timestamp) { // shouldn't need to convert here // scale Function should take care of it attributeValue = scale(new Date(value)); } else { attributeValue = scale(value); } if (!attributeValue) { attributeValue = defaultValue; } return attributeValue; } getGeoJsonEncodedChannelValue( legend, colorRange, colorField, data, defaultValue = NO_VALUE_COLOR ) { const {type} = colorField; const value = data; let attributeValue; if (type === ALL_FIELD_TYPES.string) { attributeValue = defaultValue; } else { attributeValue = colorRange[ legend.reduce((acc, breakpoint, index) => { if (value >= breakpoint.x1 && value <= breakpoint.x2) { acc = index; } return acc; }, null) ]; } return attributeValue; } // TODO: fix complexity /* eslint-disable complexity */ formatLayerData(_, allData, filteredIndex, oldLayerData, response, opt = {}) { const { colorScale, colorField, strokeColorField, strokeColorScale, strokeColorDomain, color, sizeScale, sizeDomain, sizeField, heightField, heightScale, radiusField, radiusDomain, radiusScale, visConfig, legend } = this.config; var {heightDomain, colorDomain} = this.config; const { enable3d, stroked, colorRange, heightRange, sizeRange, radiusRange, strokeColorRange, strokeColor } = visConfig; if (!oldLayerData) { this.updateLayerMeta(); } if (oldLayerData && oldLayerData.collected) { var collected = oldLayerData.collected; } colorDomain = []; if (response.polygons) { collected = response.polygons; // if (collected) { // saveText(JSON.stringify(featureCollection(collected)), 'filename.json'); // } } // 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); // } Object.values(this.visualChannels).forEach(channel => { const {scale, domain} = channel; // ordinal dommain is based on allData, if only filter changed // no need to update ordinal domain if (collected) { const updateDomain = this.calculateLayerDomain( {}, {collected}, channel ); this.updateLayerConfig({[domain]: updateDomain}); } }); // fill color const cScale = colorField && this.getVisChannelScale( colorScale, colorDomain, colorRange.colors.map(hexToRgb) ); // stroke color const scScale = strokeColorField && this.getVisChannelScale( strokeColorScale, strokeColorDomain, strokeColorRange.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 { collected, // getFeature, getFillColor: d => { return cScale && legend ? this.getGeoJsonEncodedChannelValue( legend, colorRange.colors.map(hexToRgb), colorField, d.properties[colorField.name] ) : d.properties.fillColor || color; }, getLineColor: d => cScale && legend ? this.getGeoJsonEncodedChannelValue( legend, colorRange.colors.map(hexToRgb), colorField, d.properties[colorField.name] ) : d.properties.fillColor || 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 }; } /* eslint-enable complexity */ updateLayerMeta(allData) { // const getFeature = this.getPositionAccessor(); // this.dataToFeature = getGeojsonDataMaps(allData, getFeature); // get bounds from features // const bounds = getGeojsonBounds(this.dataToFeature); // if any of the feature has properties.radius set to be true // const fixedRadius = Boolean( // this.dataToFeature.find(d => d && d.properties && d.properties.radius) // ); // keep a record of what type of geometry the collection has // const featureTypes = getGeojsonFeatureTypes(this.dataToFeature); const bounds = {}; const fixedRadius = {}; const featureTypes = {polygon: true}; this.updateMeta({bounds, fixedRadius, featureTypes}); } setInitialLayerConfig(allData) { this.updateLayerMeta(allData); const {featureTypes} = this.meta; // default settings is stroke: true, filled: false if (featureTypes && featureTypes.polygon) { // set both fill and stroke to true return this.updateLayerVisConfig({ filled: true, stroked: true, strokeColor: colorMaker.next().value }); } else if (featureTypes && featureTypes.point) { // set fill to true if detect point return this.updateLayerVisConfig({filled: true, stroked: false}); } return this; } renderLayer({data, idx, objectHovered, mapState, interactionConfig}) { const {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, elevationScale: visConfig.elevationScale, pointRadiusScale: radiusScale, 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.strokeColorField, colorRange: visConfig.colorRange // colorScale: this.config.strokeColorScale, // legend: this.config.legend }, 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.collected, getFillColor: data.getFillColor, getLineColor: data.getLineColor, getLineWidth: data.getLineWidth, getRadius: data.getRadius, getElevation: data.getElevation, // highlight pickable: true, highlightColor: HIGHLIGH_COLOR_3D, 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, lineMiterLimit: 2, rounded: true, updateTriggers }), ...(this.isLayerHovered(objectHovered) && !visConfig.enable3d ? [ new DeckGLGeoJsonLayer({ ...layerProps, id: `${this.id}-hovered`, data: [objectHovered.object], getLineWidth: data.getLineWidth, getRadius: data.getRadius, getElevation: data.getElevation, getLineColor: this.config.highlightColor, getFillColor: this.config.highlightColor, updateTriggers, stroked: true, pickable: false, filled: false }) ] : []) ]; } } // legend API // { // colorField:colorField(fieldName) // userId:UserID // colorScale:'quantile', // isEdit:true, // noOfBreaks:6, // } // filter API // { // fieldName:'', // userId:'', // isEdit:'', // fieldType:'', // tableName:'string' // }