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
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, {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'
// }