From b8b26a84ff5eb94fce6e35fc7e8c9c7dc5210d33 Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Sun, 24 Mar 2024 00:33:29 +0200 Subject: [PATCH] [Feat] support create geojson path from point csv in polygon layer Signed-off-by: Ihor Dykhta --- package.json | 2 +- .../layer-panel/layer-column-config.tsx | 2 +- .../layer-panel/layer-column-mode-config.tsx | 8 +- src/deckgl-layers/package.json | 2 +- src/layers/package.json | 2 +- src/layers/src/base-layer.ts | 46 ++++-- .../src/geojson-layer/geojson-info-modal.tsx | 96 +++++++++++ src/layers/src/geojson-layer/geojson-layer.ts | 133 +++++++++++++-- src/layers/src/geojson-layer/geojson-utils.ts | 155 ++++++++++++++++-- src/layers/src/index.ts | 2 +- src/layers/src/layer-utils.ts | 5 +- src/layers/src/trip-layer/trip-info-modal.tsx | 146 ++++++++++++----- src/layers/src/trip-layer/trip-layer.ts | 86 +++++----- src/layers/src/trip-layer/trip-utils.ts | 50 +----- src/localization/src/translations/en.ts | 92 ++++++++++- src/reducers/src/vis-state-merger.ts | 1 - src/reducers/src/vis-state-updaters.ts | 9 +- src/table/src/kepler-table.ts | 2 +- src/types/layers.d.ts | 14 +- .../side-panel/layer-configurator-test.js | 7 +- test/browser/layer-tests/point-layer-specs.js | 5 +- test/fixtures/state-saved-v0.js | 11 +- test/fixtures/state-saved-v1-1.js | 19 ++- test/fixtures/test-trip-csv-data.js | 98 +++++++++++ test/helpers/layer-utils.js | 31 +++- test/helpers/mock-state.js | 47 +++++- test/node/reducers/vis-state-test.js | 102 +++++++++++- test/node/utils/kepler-table-test.js | 4 +- yarn.lock | 6 +- 29 files changed, 941 insertions(+), 242 deletions(-) create mode 100644 src/layers/src/geojson-layer/geojson-info-modal.tsx create mode 100644 test/fixtures/test-trip-csv-data.js diff --git a/package.json b/package.json index 977f31019b..33ac8e97c0 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "@testing-library/user-event": "^14.4.3", "@types/d3-array": "^2.0.0", "@types/d3-scale": "^3.2.2", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.8", "@types/jsdom": "^21.1.1", "@types/redux-actions": "^2.6.2", "@types/supercluster": "^7.1.0", diff --git a/src/components/src/side-panel/layer-panel/layer-column-config.tsx b/src/components/src/side-panel/layer-panel/layer-column-config.tsx index 54c876e825..7040114302 100644 --- a/src/components/src/side-panel/layer-panel/layer-column-config.tsx +++ b/src/components/src/side-panel/layer-panel/layer-column-config.tsx @@ -53,7 +53,7 @@ function getValidFieldPairsSuggestionsForColumn( LayerColumnConfigFactory.deps = [ColumnSelectorFactory]; function LayerColumnConfigFactory(ColumnSelector: ReturnType) { - const LayerColumnConfig: React.FC> = ({ + const LayerColumnConfig: React.FC> = ({ columnPairs, fieldPairs, columns, diff --git a/src/components/src/side-panel/layer-panel/layer-column-mode-config.tsx b/src/components/src/side-panel/layer-panel/layer-column-mode-config.tsx index 910234dda7..1ebc0294a2 100644 --- a/src/components/src/side-panel/layer-panel/layer-column-mode-config.tsx +++ b/src/components/src/side-panel/layer-panel/layer-column-mode-config.tsx @@ -76,7 +76,9 @@ const ConfigPanesContainer = styled.div` } `; -interface FieldOption extends MinimalField {} +interface FieldOption extends MinimalField { + fieldIdx: number; +} export type ColumnModeConfigProps = { supportedColumnModes: SupportedColumnMode[] | null; @@ -237,9 +239,7 @@ function LayerColumnModeConfigFactory( columnPairs={layer.columnPairs} columns={cols} assignColumnPairs={layer.assignColumnPairs.bind(layer)} - assignColumn={ - layer.assignColumn.bind(layer) as (key: string, field: FieldOption) => LayerColumns - } + assignColumn={layer.assignColumn.bind(layer)} columnLabels={layer.columnLabels} fields={fields} fieldPairs={fieldPairs} diff --git a/src/deckgl-layers/package.json b/src/deckgl-layers/package.json index fd8a031340..87c2068f81 100644 --- a/src/deckgl-layers/package.json +++ b/src/deckgl-layers/package.json @@ -43,7 +43,7 @@ "@mapbox/geo-viewport": "^0.4.1", "@mapbox/vector-tile": "^1.3.1", "@types/d3-array": "^2.0.0", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.8", "@types/lodash.memoize": "^4.1.7", "@types/supercluster": "^7.1.0", "d3-array": "^2.8.0", diff --git a/src/layers/package.json b/src/layers/package.json index 10ca93362a..7e00d6732d 100644 --- a/src/layers/package.json +++ b/src/layers/package.json @@ -56,7 +56,7 @@ "@turf/boolean-within": "^6.0.1", "@turf/center": "^6.0.1", "@turf/helpers": "^6.1.4", - "@types/geojson": "^7946.0.7", + "@types/geojson": "^7946.0.8", "@types/keymirror": "^0.1.1", "@types/lodash.memoize": "^4.1.7", "@types/lodash.uniq": "^4.5.7", diff --git a/src/layers/src/base-layer.ts b/src/layers/src/base-layer.ts index ac1248b002..d4e2716526 100644 --- a/src/layers/src/base-layer.ts +++ b/src/layers/src/base-layer.ts @@ -41,6 +41,7 @@ import { hexToRgb, getLatLngBounds, isPlainObject, + toArray, notNullorUndefined, DataContainerInterface, getSampleContainerData @@ -355,8 +356,9 @@ class Layer { */ get defaultPointColumnPairs(): ColumnPairs { return { - lat: {pair: 'lng', fieldPairKey: 'lat'}, - lng: {pair: 'lat', fieldPairKey: 'lng'} + lat: {pair: ['lng', 'altitude'], fieldPairKey: 'lat'}, + lng: {pair: ['lat', 'altitude'], fieldPairKey: 'lng'}, + altitude: {pair: ['lng', 'lat'], fieldPairKey: 'altitude'} }; } @@ -546,11 +548,8 @@ class Layer { /** * Assign a field to layer column, return column config - * @param key - Column Key - * @param field - Selected field - * @returns {{}} - Column config */ - assignColumn(key: string, field: Field): LayerColumns { + assignColumn(key: string, field: {name: string; fieldIdx: number}): LayerColumns { // field value could be null for optional columns const update = field ? { @@ -570,30 +569,41 @@ class Layer { /** * Assign a field pair to column config, return column config - * @param key - Column Key - * @param pair - field Pair - * @returns Column config */ - assignColumnPairs(key: string, pair: FieldPair): LayerColumns { + assignColumnPairs(key: string, fieldPairs: FieldPair): LayerColumns { if (!this.columnPairs || !this.columnPairs?.[key]) { // should not end in this state return this.config.columns; } + // key = 'lat' + const {pair, fieldPairKey} = this.columnPairs?.[key]; - const {pair: partnerKey, fieldPairKey} = this.columnPairs?.[key] || {}; - - if (!pair[fieldPairKey]) { + if (typeof fieldPairKey === 'string' && !pair[fieldPairKey]) { // do not allow `key: undefined` to creep into the `updatedColumn` object return this.config.columns; } - const {fieldPairKey: partnerFieldPairKey} = this.columnPairs?.[partnerKey] || {}; - - return { + // pair = ['lng', 'alt] | 'lng' + const updatedColumn = { ...this.config.columns, - [key]: pair[fieldPairKey], - [partnerKey]: pair[partnerFieldPairKey] + // @ts-expect-error fieldPairKey can be string[] here? + [key]: fieldPairs[fieldPairKey] }; + + const partnerKeys = toArray(pair); + for (const partnerKey of partnerKeys) { + if ( + this.config.columns[partnerKey] && + this.columnPairs?.[partnerKey] && + // @ts-ignore + fieldPairs[this.columnPairs?.[partnerKey].fieldPairKey] + ) { + // @ts-ignore + updatedColumn[partnerKey] = fieldPairs[this.columnPairs?.[partnerKey].fieldPairKey]; + } + } + + return updatedColumn; } /** diff --git a/src/layers/src/geojson-layer/geojson-info-modal.tsx b/src/layers/src/geojson-layer/geojson-info-modal.tsx new file mode 100644 index 0000000000..d8fc09ac0f --- /dev/null +++ b/src/layers/src/geojson-layer/geojson-info-modal.tsx @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +import React from 'react'; +import styled from 'styled-components'; +import ReactMarkdown from 'react-markdown'; +import {useIntl} from 'react-intl'; + +import {FormattedMessage} from '@kepler.gl/localization'; + +import Table from '../table'; + +const InfoModal = styled.div` + font-size: 13px; + color: ${props => props.theme.titleColorLT}; + + pre { + padding: 12px; + background-color: #f8f8f9; + } +`; + +const StyledTitle = styled.div` + font-size: 20px; + letter-spacing: 1.25px; + margin: 18px 0 14px 0; + color: ${props => props.theme.titleColorLT}; +`; + +const StyledCode = styled.code` + color: ${props => props.theme.titleColorLT}; +`; + +const exampleTableHeader = ['id', 'latitude', 'longitude', 'sort by']; +const exampleTabbleRows = [ + ['A', '40.81773', '-74.20986', '0'], + ['A', '40.81765', '-74.20987', '1'], + ['A', '40.81746', '-74.20998', '2'], + ['B', '40.64375', '-74.33242', '0'], + ['B', '40.64353', '-74.20987', '1'], + ['B', '40.64222', '-74.33001', '2'] +]; + +const ExampleTable = () => ( + + + + {exampleTableHeader.map(v => ( + + ))} + + + + {exampleTabbleRows.map((row, i) => ( + + {row.map((v, j) => ( + + ))} + + ))} + +
{v}
+ {v} +
+); + +const GeojsonInfoModalFactory = columnMode => { + const GeojsonInfoModal = () => { + const intl = useIntl(); + return ( + +
+ +
+ {columnMode === 'table' ? ( +
+ + + + +
+ ) : null} +
+ ); + }; + return GeojsonInfoModal; +}; + +export default GeojsonInfoModalFactory; diff --git a/src/layers/src/geojson-layer/geojson-layer.ts b/src/layers/src/geojson-layer/geojson-layer.ts index d5b75c930d..0ad0eefcf8 100644 --- a/src/layers/src/geojson-layer/geojson-layer.ts +++ b/src/layers/src/geojson-layer/geojson-layer.ts @@ -9,6 +9,7 @@ import uniq from 'lodash.uniq'; import {DATA_TYPES} from 'type-analyzer'; import Layer, { colorMaker, + defaultGetFieldValue, LayerBaseConfig, LayerBaseConfigPartial, LayerColorConfig, @@ -18,11 +19,18 @@ import Layer, { LayerStrokeColorConfig } from '../base-layer'; import {GeoJsonLayer as DeckGLGeoJsonLayer} from '@deck.gl/layers'; -import {getGeojsonLayerMeta, GeojsonDataMaps, DeckGlGeoTypes} from './geojson-utils'; +import { + getGeojsonLayerMeta, + GeojsonDataMaps, + DeckGlGeoTypes, + detectTableColumns, + COLUMN_MODE_GEOJSON +} from './geojson-utils'; import { getGeojsonLayerMetaFromArrow, isLayerHoveredFromArrow, - getHoveredObjectFromArrow + getHoveredObjectFromArrow, + assignColumnsByColumnMode } from '../layer-utils'; import GeojsonLayerIcon from './geojson-layer-icon'; import { @@ -46,6 +54,7 @@ import { import {KeplerTable} from '@kepler.gl/table'; import {DataContainerInterface, ArrowDataContainer} from '@kepler.gl/utils'; import {FilterArrowExtension} from '@kepler.gl/deckgl-layers'; +import GeojsonInfoModalFactory from './geojson-info-modal'; const SUPPORTED_ANALYZER_TYPES = { [DATA_TYPES.GEOMETRY]: true, @@ -179,6 +188,21 @@ const geoColumnAccessor = (dc: DataContainerInterface): arrow.Vector | null => dc.getColumn?.(geojson.fieldIdx) as arrow.Vector; +const getTableModeValueAccessor = feature => { + // Called from gpu-filter-utils.getFilterValueAccessor() + return field => feature.properties.values.map(v => field.valueAccessor(v)); +}; + +const getTableModeFieldValue = (field, data) => { + let rv; + if (typeof data === 'function') { + rv = data(field); + } else { + rv = defaultGetFieldValue(field, data); + } + return rv; +}; + const geoFieldAccessor = ({geojson}: GeoJsonLayerColumnsConfig) => (dc: DataContainerInterface): Field | null => @@ -189,6 +213,22 @@ export const defaultElevation = 500; export const defaultLineWidth = 1; export const defaultRadius = 1; +export const COLUMN_MODE_TABLE = 'table'; +const SUPPORTED_COLUMN_MODES = [ + { + key: COLUMN_MODE_GEOJSON, + label: 'GeoJSON', + requiredColumns: ['geojson'] + }, + { + key: COLUMN_MODE_TABLE, + label: 'Table columns', + requiredColumns: ['id', 'lat', 'lng'], + optionalColumns: ['altitude', 'sortBy'] + } +]; +const DEFAULT_COLUMN_MODE = COLUMN_MODE_GEOJSON; + export default class GeoJsonLayer extends Layer { declare config: GeoJsonLayerConfig; declare visConfigSettings: GeoJsonVisConfigSettings; @@ -200,12 +240,21 @@ export default class GeoJsonLayer extends Layer { filteredIndexTrigger: number[] | null = null; centroids: Array = []; + _layerInfoModal: { + [COLUMN_MODE_TABLE]: () => React.JSX.Element; + [COLUMN_MODE_GEOJSON]: () => React.JSX.Element; + }; + constructor(props) { super(props); this.registerVisConfig(geojsonVisConfigs); this.getPositionAccessor = (dataContainer: DataContainerInterface) => featureAccessor(this.config.columns)(dataContainer); + this._layerInfoModal = { + [COLUMN_MODE_TABLE]: GeojsonInfoModalFactory(COLUMN_MODE_TABLE), + [COLUMN_MODE_GEOJSON]: GeojsonInfoModalFactory(COLUMN_MODE_GEOJSON) + }; } get type() { @@ -223,8 +272,38 @@ export default class GeoJsonLayer extends Layer { return GeojsonLayerIcon; } - get requiredLayerColumns() { - return geoJsonRequiredColumns; + get columnPairs() { + return this.defaultPointColumnPairs; + } + + get supportedColumnModes() { + return SUPPORTED_COLUMN_MODES; + } + + get layerInfoModal() { + return { + [COLUMN_MODE_GEOJSON]: { + id: 'iconInfo', + template: this._layerInfoModal[COLUMN_MODE_GEOJSON], + modalProps: { + title: 'modal.polygonInfo.title' + } + }, + [COLUMN_MODE_TABLE]: { + id: 'iconInfo', + template: this._layerInfoModal[COLUMN_MODE_TABLE], + modalProps: { + title: 'modal.polygonInfo.titleTable' + } + } + }; + } + + accessVSFieldValue(field, indexKey) { + if (this.config.columnMode === COLUMN_MODE_GEOJSON) { + return defaultGetFieldValue; + } + return getTableModeFieldValue; } get visualChannels() { @@ -322,7 +401,7 @@ export default class GeoJsonLayer extends Layer { getDefaultLayerConfig(props: LayerBaseConfigPartial) { return { ...super.getDefaultLayerConfig(props), - + columnMode: props?.columnMode ?? DEFAULT_COLUMN_MODE, // add height visual channel heightField: null, heightDomain: [0, 1], @@ -385,12 +464,17 @@ export default class GeoJsonLayer extends Layer { const {gpuFilter, dataContainer} = datasets[this.config.dataId]; const {data} = this.updateData(datasets, oldLayerData); - const customFilterValueAccessor = (dc, d, fieldIndex) => { - return dc.valueAt(d.properties.index, fieldIndex); - }; - const indexAccessor = f => f.properties.index; + let filterValueAccessor; + let dataAccessor; + if (this.config.columnMode === COLUMN_MODE_GEOJSON) { + filterValueAccessor = (dc, d, fieldIndex) => dc.valueAt(d.properties.index, fieldIndex); + dataAccessor = dc => d => ({index: d.properties.index}); + } else { + filterValueAccessor = getTableModeValueAccessor; + dataAccessor = dc => d => ({index: d.properties.index}); + } - const dataAccessor = () => d => ({index: d.properties.index}); + const indexAccessor = f => f.properties.index; const accessors = this.getAttributeAccessors({dataAccessor, dataContainer}); const isFilteredAccessor = d => { @@ -401,7 +485,7 @@ export default class GeoJsonLayer extends Layer { data, getFilterValue: gpuFilter.filterValueAccessor(dataContainer)( indexAccessor, - customFilterValueAccessor + filterValueAccessor ), getFiltered: isFilteredAccessor, ...accessors @@ -437,6 +521,7 @@ export default class GeoJsonLayer extends Layer { if (this.dataToFeature.length < dataContainer.numChunks()) { // for incrementally loading data, we only load and render the latest batch; otherwise, we will load and render all batches const isIncrementalLoad = dataContainer.numChunks() - this.dataToFeature.length === 1; + // TODO: add support for COLUMN_MODE_TABLE in getGeojsonLayerMetaFromArrow const {dataToFeature, bounds, fixedRadius, featureTypes, centroids} = getGeojsonLayerMetaFromArrow({ dataContainer, @@ -451,7 +536,8 @@ export default class GeoJsonLayer extends Layer { } else if (this.dataToFeature.length === 0) { const {dataToFeature, bounds, fixedRadius, featureTypes, centroids} = getGeojsonLayerMeta({ dataContainer, - getFeature + getFeature, + config: this.config }); if (centroids) this.centroids = centroids; this.dataToFeature = dataToFeature; @@ -459,10 +545,31 @@ export default class GeoJsonLayer extends Layer { } } - setInitialLayerConfig({dataContainer}) { + setInitialLayerConfig(dataset) { + const {dataContainer} = dataset; if (!dataContainer.numRows()) { return this; } + + // defefaultLayerProps will automatically find geojson column + // if not found, we try to set it to id / lat /lng /ts + if (!this.config.columns.geojson.value) { + // find columns from lat, lng, id, and ts + const columnConfig = detectTableColumns(dataset, this.config.columns, 'sortBy'); + if (columnConfig) { + const columns = assignColumnsByColumnMode({ + columns: columnConfig.columns, + supportedColumnModes: this.supportedColumnModes, + columnMode: COLUMN_MODE_TABLE + }); + this.updateLayerConfig({ + ...columnConfig, + columns, + columnMode: COLUMN_MODE_TABLE + }); + } + } + this.updateLayerMeta(dataContainer); const {featureTypes} = this.meta; diff --git a/src/layers/src/geojson-layer/geojson-utils.ts b/src/layers/src/geojson-layer/geojson-utils.ts index ce41d041c3..c3d6d92904 100644 --- a/src/layers/src/geojson-layer/geojson-utils.ts +++ b/src/layers/src/geojson-layer/geojson-utils.ts @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import {Feature, BBox} from 'geojson'; +import {Feature, Position, BBox} from 'geojson'; import normalize from '@mapbox/geojson-normalize'; import bbox from '@turf/bbox'; +import {ascending} from 'd3-array'; import center from '@turf/center'; import {AllGeoJSON} from '@turf/helpers'; import {parseSync} from '@loaders.gl/core'; @@ -11,12 +12,17 @@ import {WKBLoader, WKTLoader} from '@loaders.gl/wkt'; import {binaryToGeometry} from '@loaders.gl/gis'; import {BinaryFeatureCollection} from '@loaders.gl/schema'; import {DataContainerInterface, getSampleData} from '@kepler.gl/utils'; +import {ALL_FIELD_TYPES} from '@kepler.gl/constants'; +import {LayerColumns} from '@kepler.gl/types'; +import {KeplerTable} from '@kepler.gl/table'; -import {GeojsonLayerMetaProps} from '../layer-utils'; +import {GeojsonLayerMetaProps, assignPointPairToLayerColumn} from '../layer-utils'; export type GetFeature = (d: any) => Feature; export type GeojsonDataMaps = (Feature | BinaryFeatureCollection | null)[]; +export const COLUMN_MODE_GEOJSON = 'geojson'; + /* eslint-disable */ // TODO: Re-enable eslint when we upgrade to handle enums and type maps export enum FeatureTypes { @@ -28,6 +34,11 @@ export enum FeatureTypes { MultiPolygon = 'MultiPolygon' } +// @ts-expect-error return type of getGeojsonFeatureTypes ? +type FeatureTypeMap = { + [key in FeatureTypes]: boolean; +}; + /* eslint-enable */ export function parseGeoJsonRawFeature(rawFeature: unknown): Feature | null { @@ -61,12 +72,22 @@ export function parseGeoJsonRawFeature(rawFeature: unknown): Feature | null { export function getGeojsonLayerMeta({ dataContainer, - getFeature + getFeature, + config }: { dataContainer: DataContainerInterface; getFeature: GetFeature; + config: { + columnMode: string | undefined; + columns: LayerColumns; + }; }): GeojsonLayerMetaProps { - const dataToFeature = getGeojsonDataMaps(dataContainer, getFeature); + const dataToFeature = + config.columnMode === COLUMN_MODE_GEOJSON + ? getGeojsonDataMaps(dataContainer, getFeature) + : // COLUMN_MODE_TABLE + groupColumnsAsGeoJson(dataContainer, config.columns, 'sortBy'); + // get bounds from features const bounds = getGeojsonBounds(dataToFeature); // if any of the feature has properties.radius set to be true @@ -102,11 +123,11 @@ export function getGeojsonLayerMeta({ /** * Parse raw data to GeoJson feature - * @param dataContainer - * @param getFeature - * @returns {{}} */ -export function getGeojsonDataMaps(dataContainer: any, getFeature: GetFeature): GeojsonDataMaps { +export function getGeojsonDataMaps( + dataContainer: DataContainerInterface, + getFeature: GetFeature +): GeojsonDataMaps { const acceptableTypes = [ 'Point', 'MultiPoint', @@ -146,7 +167,7 @@ export function getGeojsonDataMaps(dataContainer: any, getFeature: GetFeature): * @param {String} geoString * @returns {null | Object} geojson object or null if failed */ -export function parseGeometryFromString(geoString: string): Feature | null { +function parseGeometryFromString(geoString: string): Feature | null { let parsedGeo; // try parse as geojson string @@ -192,6 +213,9 @@ export function parseGeometryFromString(geoString: string): Feature | null { return normalized.features[0]; } +/** + * Get geojson bounds + */ export function getGeojsonBounds(features: GeojsonDataMaps = []): BBox | null { // 70 ms for 10,000 polygons // here we only pick couple @@ -229,8 +253,6 @@ export type DeckGlGeoTypes = { /** * Parse geojson from string - * @param {Array} allFeatures - * @returns {Object} mapping of feature type existence */ export function getGeojsonFeatureTypes(allFeatures: GeojsonDataMaps): DeckGlGeoTypes { // @ts-expect-error some test cases only have 1 geotype @@ -247,3 +269,114 @@ export function getGeojsonFeatureTypes(allFeatures: GeojsonDataMaps): DeckGlGeoT return featureTypes; } + +type CoordsType = [number, number, number, number | null] & { + datumIndex: number; + datum: number[]; +}; + +export function groupColumnsAsGeoJson( + dataContainer: DataContainerInterface, + columns: LayerColumns, + sortByColumn = 'timestamp' +): Feature[] { + const groupedById: {[key: string]: CoordsType[]} = {}; + const sortByFieldIdx = columns[sortByColumn].fieldIdx; + const sortByRequired = !columns[sortByColumn].optional; + for (let index = 0; index < dataContainer.numRows(); index++) { + // note: can materialize the row + const datum = dataContainer.rowAsArray(index); + const id = datum[columns.id.fieldIdx]; + const lat = datum[columns.lat.fieldIdx]; + const lon = datum[columns.lng.fieldIdx]; + const altitude = columns.altitude ? datum[columns.altitude.fieldIdx] : 0; + const sortBy = sortByFieldIdx > -1 ? datum[sortByFieldIdx] : null; + // @ts-expect-error + const coords: CoordsType = [lon, lat, altitude, sortBy]; + // Adding references to the original data to the coordinates array + coords.datumIndex = index; + coords.datum = datum; + if (!groupedById[id]) groupedById[id] = []; + + if ( + Number.isFinite(lon) && + Number.isFinite(lat) && + (!sortByRequired || (sortByRequired && sortBy)) + ) { + // only push points if lat,lng,and sortby exist + groupedById[id].push(coords); + } + } + + const result: Feature[] = Object.entries(groupedById).map( + ([id, items]: [string, CoordsType[]], index) => ({ + type: 'Feature' as 'Feature', + geometry: { + type: 'LineString' as 'LineString', + // Sort by columns if has sortByField + // TODO: items are expected in Position[] format? + coordinates: (sortByFieldIdx > -1 + ? items.sort((a, b) => ascending(a[3] as any, b[3] as any)) + : items) as Position[] + }, + properties: { + index, + // values are used for valueAccessor in TripLayer.formatLayerData() + // Note: this can cause row materialization in case of non-row based containers + values: items.map(item => dataContainer.rowAsArray(item.datumIndex)) + } + }) + ); + return result; +} + +/** + * Find id / ts / lat / lng columns from a table and assign it to layer columns + */ +export function detectTableColumns( + dataset: KeplerTable, + layerColumns: LayerColumns, + sortBy: string = 'timestamp' +) { + const {fields, fieldPairs} = dataset; + if (!fieldPairs.length || !fields.length) { + return null; + } + // find sort by field + const sortByFieldIdx = fields.findIndex(f => f.type === ALL_FIELD_TYPES.timestamp); + // find id column + const idFieldIdx = fields.findIndex(f => f.name?.toLowerCase().match(/^(id|uuid)$/g)); + + if (sortByFieldIdx > -1 && idFieldIdx > -1) { + const pointColumns = assignPointPairToLayerColumn(fieldPairs[0], true); + return { + columns: { + ...Object.keys(layerColumns).reduce( + (accu, col) => ({ + ...accu, + [col]: pointColumns[col] ?? layerColumns[col] + }), + {} + ), + geojson: { + value: null, + fieldIdx: -1 + // optional: true + }, + id: { + value: fields[idFieldIdx].name, + fieldIdx: idFieldIdx + // optional: false + }, + [sortBy]: { + value: fields[sortByFieldIdx].name, + fieldIdx: sortByFieldIdx + // optional: false + } + }, + label: fieldPairs[0].defaultName + }; + } + + return null; +} diff --git a/src/layers/src/index.ts b/src/layers/src/index.ts index b0e53d0f1a..47aeb3e2fb 100644 --- a/src/layers/src/index.ts +++ b/src/layers/src/index.ts @@ -15,7 +15,7 @@ import {default as GridLayer} from './grid-layer/grid-layer'; export {pointToPolygonGeo} from './grid-layer/grid-utils'; import {default as HexagonLayer} from './hexagon-layer/hexagon-layer'; import {default as GeojsonLayer} from './geojson-layer/geojson-layer'; -export {defaultElevation, defaultLineWidth, defaultRadius} from './geojson-layer/geojson-layer'; +export {defaultElevation, defaultLineWidth, defaultRadius, COLUMN_MODE_TABLE} from './geojson-layer/geojson-layer'; import {default as ClusterLayer} from './cluster-layer/cluster-layer'; import {default as IconLayer} from './icon-layer/icon-layer'; import {default as HeatmapLayer} from './heatmap-layer/heatmap-layer'; diff --git a/src/layers/src/layer-utils.ts b/src/layers/src/layer-utils.ts index 9ca9a09120..380ee758e7 100644 --- a/src/layers/src/layer-utils.ts +++ b/src/layers/src/layer-utils.ts @@ -14,7 +14,7 @@ import { import {DeckGlGeoTypes, GeojsonDataMaps} from './geojson-layer/geojson-utils'; export function assignPointPairToLayerColumn(pair: FieldPair, hasAlt: boolean) { - const {lat, lng, alt} = pair.pair; + const {lat, lng, altitude} = pair.pair; if (!hasAlt) { return {lat, lng}; } @@ -24,7 +24,7 @@ export function assignPointPairToLayerColumn(pair: FieldPair, hasAlt: boolean) { return { lat, lng, - altitude: alt ? {...defaultAltColumn, ...alt} : defaultAltColumn + altitude: altitude ? {...defaultAltColumn, ...altitude} : defaultAltColumn }; } @@ -154,6 +154,7 @@ export function assignColumnsByColumnMode({ columnMode: string | undefined; }): LayerColumns { const requiredColumns = getColumnModeRequiredColumns(supportedColumnModes, columnMode); + if (requiredColumns) { return Object.entries(columns).reduce( (acc, [columnKey, column]) => ({ diff --git a/src/layers/src/trip-layer/trip-info-modal.tsx b/src/layers/src/trip-layer/trip-info-modal.tsx index 1314684423..f63380431f 100644 --- a/src/layers/src/trip-layer/trip-info-modal.tsx +++ b/src/layers/src/trip-layer/trip-info-modal.tsx @@ -3,10 +3,20 @@ import React from 'react'; import styled from 'styled-components'; +import ReactMarkdown from 'react-markdown'; +import {useIntl} from 'react-intl'; + import {FormattedMessage} from '@kepler.gl/localization'; +import {Table} from '@kepler.gl/layers'; -const StyledCode = styled.code` +const InfoModal = styled.div` + font-size: 13px; color: ${props => props.theme.titleColorLT}; + + pre { + padding: 12px; + background-color: #f8f8f9; + } `; const StyledTitle = styled.div` @@ -15,50 +25,100 @@ const StyledTitle = styled.div` margin: 18px 0 14px 0; color: ${props => props.theme.titleColorLT}; `; +const StyledCode = styled.code` + color: ${props => props.theme.titleColorLT}; +`; + +const codeExampleGeojson = ` +${'```json'} +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "vendor": "A", + "vol":20 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [-74.20986, 40.81773, 0, 1564184363], + [-74.20987, 40.81765, 0, 1564184396], + [-74.20998, 40.81746, 0, 1564184409] + ] + } + } + ] +} +${'```'} +`; + +const exampleTableHeader = ['id', 'latitude', 'longitude', 'timestamp']; +const exampleTabbleRows = [ + ['A', '40.81773', '-74.20986', '1564184363'], + ['A', '40.81765', '-74.20987', '1564184396'], + ['A', '40.81746', '-74.20998', '1564184409'], + ['B', '40.64375', '-74.33242', '1565578213'], + ['B', '40.64353', '-74.20987', '1565578217'], + ['B', '40.64222', '-74.33001', '1565578243'] +]; + +const ExampleTable = () => ( + + + + {exampleTableHeader.map(v => ( + + ))} + + + + {exampleTabbleRows.map((row, i) => ( + + {row.map((v, j) => ( + + ))} + + ))} + +
{v}
+ {v} +
+); -const TripInfoModalFactory = () => { - const TripInfoModal = () => ( -
-
-

- - - - - -

-
-
- - - -
-          
-            {`
-              {
-                "type": "FeatureCollection",
-                "features": [
-                  {
-                    "type": "Feature",
-                    "properties": { "vendor":  "A",
-                    "vol":20},
-                    "geometry": {
-                      "type": "LineString",
-                      "coordinates": [
-                        [-74.20986, 40.81773, 0, 1564184363],
-                        [-74.20987, 40.81765, 0, 1564184396],
-                        [-74.20998, 40.81746, 0, 1564184409]
-                      ]
-                    }
-                  }
-                ]
+const TripInfoModalFactory = columnMode => {
+  const TripInfoModal = () => {
+    const intl = useIntl();
+    return (
+      
+        
+ +
+
+ + -
-
-
- ); + /> + + {columnMode === 'geojson' ? ( + + ) : ( + + )} + + + ); + }; return TripInfoModal; }; diff --git a/src/layers/src/trip-layer/trip-layer.ts b/src/layers/src/trip-layer/trip-layer.ts index cf3a6287d2..0a6fc30b38 100644 --- a/src/layers/src/trip-layer/trip-layer.ts +++ b/src/layers/src/trip-layer/trip-layer.ts @@ -7,18 +7,20 @@ import uniq from 'lodash.uniq'; import Layer, {LayerBaseConfig, defaultGetFieldValue} from '../base-layer'; import {TripsLayer as DeckGLTripsLayer} from '@deck.gl/geo-layers'; -import {GEOJSON_FIELDS, ColorRange, ALL_FIELD_TYPES} from '@kepler.gl/constants'; +import {GEOJSON_FIELDS, ColorRange} from '@kepler.gl/constants'; import TripLayerIcon from './trip-layer-icon'; import { getGeojsonDataMaps, getGeojsonBounds, getGeojsonFeatureTypes, - GeojsonDataMaps + GeojsonDataMaps, + detectTableColumns, + groupColumnsAsGeoJson } from '../geojson-layer/geojson-utils'; -import {groupColumnsAsGeoJson, isTripGeoJsonField, parseTripGeoJsonTimestamp} from './trip-utils'; -import {assignPointPairToLayerColumn} from '../layer-utils'; +import {isTripGeoJsonField, parseTripGeoJsonTimestamp} from './trip-utils'; +import {assignColumnsByColumnMode} from '../layer-utils'; import TripInfoModalFactory from './trip-info-modal'; import {bisectRight} from 'd3-array'; import { @@ -108,8 +110,8 @@ const getTableModeFieldValue = (field, data) => { return rv; }; -const COLUMN_MODE_GEOJSON = 'geojson'; -const COLUMN_MODE_TABLE = 'table'; +export const COLUMN_MODE_GEOJSON = 'geojson'; +export const COLUMN_MODE_TABLE = 'table'; const SUPPORTED_COLUMN_MODES = [ { key: COLUMN_MODE_GEOJSON, @@ -134,7 +136,7 @@ export default class TripLayer extends Layer { dataToFeature: GeojsonDataMaps; dataToTimeStamp: any[]; getFeature: (columns: TripLayerColumnsConfig) => (dataContainer: DataContainerInterface) => any; - _layerInfoModal: () => JSX.Element; + _layerInfoModal: Record JSX.Element>; constructor(props) { super(props); @@ -144,7 +146,10 @@ export default class TripLayer extends Layer { this.dataContainer = null; this.registerVisConfig(tripVisConfigs); this.getFeature = memoize(featureAccessor, featureResolver); - this._layerInfoModal = TripInfoModalFactory(); + this._layerInfoModal = { + [COLUMN_MODE_TABLE]: TripInfoModalFactory(COLUMN_MODE_TABLE), + [COLUMN_MODE_GEOJSON]: TripInfoModalFactory(COLUMN_MODE_GEOJSON) + }; } get supportedColumnModes() { @@ -209,10 +214,17 @@ export default class TripLayer extends Layer { return { [COLUMN_MODE_GEOJSON]: { id: 'iconInfo', - template: this._layerInfoModal, + template: this._layerInfoModal[COLUMN_MODE_GEOJSON], modalProps: { title: 'modal.tripInfo.title' } + }, + [COLUMN_MODE_TABLE]: { + id: 'iconInfo', + template: this._layerInfoModal[COLUMN_MODE_TABLE], + modalProps: { + title: 'modal.tripInfo.titleTable' + } } }; } @@ -259,36 +271,6 @@ export default class TripLayer extends Layer { }; } - // find columns from lat, lng, id, and ts - if (fieldPairs.length) { - // find time column - const timeFieldIdx = fields.findIndex(f => f.type === ALL_FIELD_TYPES.timestamp); - // find id column - const idFieldIdx = fields.findIndex(f => f.name?.toLowerCase().match(/^(id|uuid)$/g)); - - if (timeFieldIdx > -1 && idFieldIdx > -1) { - const layerProp = { - columns: { - ...assignPointPairToLayerColumn(fieldPairs[0], true), - id: { - value: fields[idFieldIdx], - fieldIdx: idFieldIdx - }, - timestamp: { - value: fields[timeFieldIdx], - fieldIdx: timeFieldIdx - } - }, - label: fieldPairs[0].defaultName, - columnMode: COLUMN_MODE_TABLE - }; - - return { - props: [layerProp] - }; - } - } - return {props: []}; } @@ -358,6 +340,7 @@ export default class TripLayer extends Layer { return {}; } // to-do: parse segment from dataContainer + const {dataContainer, gpuFilter} = datasets[this.config.dataId]; const {data} = this.updateData(datasets, oldLayerData); @@ -406,7 +389,7 @@ export default class TripLayer extends Layer { this.dataToFeature = getGeojsonDataMaps(dataContainer, getFeature); } else { this.dataContainer = dataContainer; - this.dataToFeature = groupColumnsAsGeoJson(dataContainer, this.config.columns); + this.dataToFeature = groupColumnsAsGeoJson(dataContainer, this.config.columns, 'timestamp'); } const {dataToTimeStamp, animationDomain} = parseTripGeoJsonTimestamp(this.dataToFeature); @@ -423,10 +406,31 @@ export default class TripLayer extends Layer { this.updateMeta({bounds, featureTypes, getFeature}); } - setInitialLayerConfig({dataContainer}) { + setInitialLayerConfig(dataset) { + const {dataContainer} = dataset; if (!dataContainer.numRows()) { return this; } + + // defefaultLayerProps will automatically find geojson column + // if not found, we try to set it to id / lat /lng /ts + if (!this.config.columns.geojson.value) { + // find columns from lat, lng, id, and ts + const columnConfig = detectTableColumns(dataset, this.config.columns); + if (columnConfig) { + const columns = assignColumnsByColumnMode({ + columns: columnConfig.columns, + supportedColumnModes: this.supportedColumnModes, + columnMode: COLUMN_MODE_TABLE + }); + this.updateLayerConfig({ + ...columnConfig, + columns, + columnMode: COLUMN_MODE_TABLE + }); + } + } + this.updateLayerMeta(dataContainer); return this; } diff --git a/src/layers/src/trip-layer/trip-utils.ts b/src/layers/src/trip-layer/trip-utils.ts index 2116cb2915..201eaedbce 100644 --- a/src/layers/src/trip-layer/trip-utils.ts +++ b/src/layers/src/trip-layer/trip-utils.ts @@ -2,9 +2,8 @@ // Copyright contributors to the kepler.gl project import {Analyzer, DATA_TYPES} from 'type-analyzer'; -import {ascending} from 'd3-array'; -import {Field, LayerColumns} from '@kepler.gl/types'; +import {Field} from '@kepler.gl/types'; import {parseGeoJsonRawFeature, getGeojsonFeatureTypes} from '../geojson-layer/geojson-utils'; import { @@ -167,50 +166,3 @@ export function getAnimationDomainFromTimestamps(dataToTimeStamp: number[][] = [ [Infinity, -Infinity] ); } - -type GeoJsonFeature = any; -type CoordsType = number[] & { - datumIndex: number; - datum: number[]; -}; - -export function groupColumnsAsGeoJson( - dataContainer: DataContainerInterface, - columns: LayerColumns -): GeoJsonFeature[] { - const groupedById: {[key: string]: CoordsType[]} = {}; - for (let index = 0; index < dataContainer.numRows(); index++) { - // Note: this can cause row materialization in case of non-row based containers - const datum = dataContainer.rowAsArray(index) as number[]; - const id = datum[columns.id.fieldIdx]; - const lat = datum[columns.lat.fieldIdx]; - const lon = datum[columns.lng.fieldIdx]; - const altitude = columns.altitude ? datum[columns.altitude.fieldIdx] : 0; - const time = datum[columns.timestamp.fieldIdx]; - // @ts-expect-error - const coords: CoordsType = [lon, lat, altitude, time]; - // Adding references to the original data to the coordinates array - coords.datumIndex = index; - coords.datum = datum; - if (!groupedById[id]) groupedById[id] = []; - if (Number.isFinite(lon) && Number.isFinite(lat) && time) { - groupedById[id].push(coords); - } - } - const result = Object.entries(groupedById).map(([id, items]: [string, CoordsType[]], index) => ({ - type: 'Feature', - geometry: { - type: 'LineString', - coordinates: - // Sort by time - items.sort((a, b) => ascending(a[3], b[3])) - }, - properties: { - index, - // values are used for valueAccessor in TripLayer.formatLayerData() - // Note: this can cause row materialization in case of non-row based containers - values: items.map(item => dataContainer.rowAsArray(item.datumIndex)) - } - })); - return result; -} diff --git a/src/localization/src/translations/en.ts b/src/localization/src/translations/en.ts index 04dc90f33e..64b1e69493 100644 --- a/src/localization/src/translations/en.ts +++ b/src/localization/src/translations/en.ts @@ -381,13 +381,90 @@ export default { storage: 'Load from Storage' }, tripInfo: { - title: 'How to enable trip animation', - description1: - 'In order to animate the path, the geoJSON data needs to contain `LineString` in its feature geometry, and the coordinates in the LineString need to have 4 elements in the formats of', - code: ' [longitude, latitude, altitude, timestamp] ', - description2: - 'with the last element being a timestamp. Valid timestamp formats include unix in seconds such as `1564184363` or in milliseconds such as `1564184363000`.', - example: 'Example:' + title: 'Create trips from GeoJson', + titleTable: 'Create trips from a list of points', + description1: `To animate the path, the GeoJSON data needs to contain \`LineString\` in its feature geometry, and the coordinates in the LineString need to have 4 elements in the formats of +${'```json'} +[longitude, latitude, altitude, timestamp] +${'```'} +The 3rd element is a timestamp. Valid timestamp formats include unix in seconds such as \`1564184363\` or in milliseconds such as \`1564184363000\`.`, + descriptionTable1: + 'Trips can be created by joining a list of points from latitude and longitude, sort by timestamps and group by uniq ids.', + example: 'Example GeoJSON', + exampleTable: 'Example Csv' + }, + polygonInfo: { + title: 'Create polygon layer from GeoJSON feature', + titleTable: 'Create path from points', + description: `Polygon can be created from +__1 .A GeoJSON Feature Collection__ +__2. A Csv contains geometry column__ + +### 1. Create polygon from GeoJSON file + +When upload a GeoJSON file contains FeatureCollection, a polygon layer will be auto-created + +Example GeoJSON +${'```json'} +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [102.0, 0.5] + }, + "properties": { + "prop0": "value0" + } + }, { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + [105.0, 1.0] + ] + }, + "properties": { + "prop0": "value0" + } + }] +} +${'```'} + +### 2. Create polygon from a Geometry column in Csv table +Geometries (Polygons, Points, LindStrings etc) can be embedded into CSV as a \`GeoJSON\` or \`WKT\` formatted string. + +#### 2.1 \`GeoJSON\` string +Example data.csv with \`GeoJSON\` string +${'```txt'} +id,_geojson +1,"{""type"":""Polygon"",""coordinates"":[[[-74.158491,40.835947],[-74.157914,40.83902]]]}" +${'```'} + +#### 2.2 \`WKT\` string +Example data.csv with \`WKT\` string +[The Well-Known Text (WKT)](https://dev.mysql.com/doc/refman/5.7/en/gis-data-formats.html#gis-wkt-format) representation of geometry values is designed for exchanging geometry data in ASCII form. + +Example data.csv with WKT +${'```txt'} +id,_geojson +1,"POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7, 5 5))" +${'```'} +`, + descriptionTable: `Paths can be created by joining a list of points from latitude and longitude, sort by an index field (e.g. timestamp) and group by uniq ids. + + ### Layer columns: + - **id**: - *required* - A \`id\` column is used to group by points. Points with the same id will be joined into a single path. + - **lat**: - *required* - The latitude of the point + - **lon**: - *required* - The longitude of the point + - **alt**: - *optional* - The altitude of the point + - **sort by**: - *optional* - A \`sort by\` column is used to sort the points, if not specified, points will be sorted by row index. +`, + exampleTable: 'Example CSV' }, iconInfo: { title: 'How to draw icons', @@ -446,6 +523,7 @@ export default { icon: 'icon', geojson: 'geojson', token: 'token', + sortBy: 'sort by', arc: { lat0: 'source lat', lng0: 'source lng', diff --git a/src/reducers/src/vis-state-merger.ts b/src/reducers/src/vis-state-merger.ts index 4c724d5918..beab0b7c1e 100644 --- a/src/reducers/src/vis-state-merger.ts +++ b/src/reducers/src/vis-state-merger.ts @@ -800,7 +800,6 @@ export function validateLayerWithData( color: savedLayer.config.color, isVisible: savedLayer.config.isVisible, hidden: savedLayer.config.hidden, - // columns: savedLayer.config.columns, columnMode: savedLayer.config.columnMode, highlightColor: savedLayer.config.highlightColor }); diff --git a/src/reducers/src/vis-state-updaters.ts b/src/reducers/src/vis-state-updaters.ts index 9342cc5c14..105e8bd7d2 100644 --- a/src/reducers/src/vis-state-updaters.ts +++ b/src/reducers/src/vis-state-updaters.ts @@ -710,13 +710,10 @@ export function setInitialLayerConfig(layer, datasets, layerClasses): Layer { dataId: newLayer.config.dataId, isConfigActive: newLayer.config.isConfigActive }); - - return typeof newLayer.setInitialLayerConfig === 'function' - ? newLayer.setInitialLayerConfig(dataset) - : newLayer; } - - return newLayer; + return typeof newLayer.setInitialLayerConfig === 'function' + ? newLayer.setInitialLayerConfig(dataset) + : newLayer; } /** * Update layer type. Previews layer config will be copied if applicable. diff --git a/src/table/src/kepler-table.ts b/src/table/src/kepler-table.ts index f4253427d5..09171fd945 100644 --- a/src/table/src/kepler-table.ts +++ b/src/table/src/kepler-table.ts @@ -541,7 +541,7 @@ export function findPointFieldPairs(fields: Field[]): FieldPair[] { }, ...(altIdx > -1 ? { - alt: { + altitude: { fieldIdx: altIdx, value: fields[altIdx].name } diff --git a/src/types/layers.d.ts b/src/types/layers.d.ts index 9211279263..c2c3f47dc9 100644 --- a/src/types/layers.d.ts +++ b/src/types/layers.d.ts @@ -38,8 +38,8 @@ export type LayerColumns = { }; export type ColumnPair = { - pair: string; - fieldPairKey: string; + pair: string | string[]; + fieldPairKey: string | string[]; }; export type ColumnPairs = {[key: string]: ColumnPair}; @@ -114,7 +114,15 @@ export type Field = { export type FieldPair = { defaultName: string; pair: { - [key: string]: { + lat: { + fieldIdx: number; + value: string; + }; + lng: { + fieldIdx: number; + value: string; + }; + altitude?: { fieldIdx: number; value: string; }; diff --git a/test/browser/components/side-panel/layer-configurator-test.js b/test/browser/components/side-panel/layer-configurator-test.js index 5421c1c692..3242e88cd9 100644 --- a/test/browser/components/side-panel/layer-configurator-test.js +++ b/test/browser/components/side-panel/layer-configurator-test.js @@ -138,7 +138,7 @@ test('Components -> LayerConfigurator.mount -> default prop 1', t => { t.end(); }); -test('Components -> LayerConfigurator.mount -> defaut prop 2', t => { +test('Components -> LayerConfigurator.mount -> LayerColumnConfig', t => { // mount const updateLayerConfigSpy = sinon.spy(); @@ -298,11 +298,6 @@ test('Components -> LayerConfigurator.mount -> collapsed / expand config group ' 'LayerColumnModeConfig should be expanded' ); - // t.equal( - // wrapper.find(LayerColumnModeConfig).length, - // 1, - // 'LayerColumnModeConfig should be expanded' - // ); t.end(); }); diff --git a/test/browser/layer-tests/point-layer-specs.js b/test/browser/layer-tests/point-layer-specs.js index c49bd07311..56ad5d8763 100644 --- a/test/browser/layer-tests/point-layer-specs.js +++ b/test/browser/layer-tests/point-layer-specs.js @@ -40,8 +40,9 @@ test('#PointLayer -> constructor', t => { t.deepEqual( layer.columnPairs, { - lat: {pair: 'lng', fieldPairKey: 'lat'}, - lng: {pair: 'lat', fieldPairKey: 'lng'} + lat: {pair: ['lng', 'altitude'], fieldPairKey: 'lat'}, + lng: {pair: ['lat', 'altitude'], fieldPairKey: 'lng'}, + altitude: {pair: ['lng', 'lat'], fieldPairKey: 'altitude'} }, 'columnPairs should be correct' ); diff --git a/test/fixtures/state-saved-v0.js b/test/fixtures/state-saved-v0.js index b1d418c674..a1c7fb6948 100644 --- a/test/fixtures/state-saved-v0.js +++ b/test/fixtures/state-saved-v0.js @@ -1277,9 +1277,16 @@ mergedLayer4.config = { columns: { geojson: { value: '_geojson', - fieldIdx: 0 - } + fieldIdx: 0, + optional: false + }, + id: {value: null, fieldIdx: -1, optional: true}, + lat: {value: null, fieldIdx: -1, optional: true}, + lng: {value: null, fieldIdx: -1, optional: true}, + altitude: {value: null, fieldIdx: -1, optional: true}, + sortBy: {value: null, fieldIdx: -1, optional: true} }, + columnMode: 'geojson', highlightColor: [252, 242, 26, 255], isConfigActive: false, hidden: false, diff --git a/test/fixtures/state-saved-v1-1.js b/test/fixtures/state-saved-v1-1.js index 5c69f2fd17..e8002ab240 100644 --- a/test/fixtures/state-saved-v1-1.js +++ b/test/fixtures/state-saved-v1-1.js @@ -857,9 +857,16 @@ mergedLayer0.config = { columns: { geojson: { fieldIdx: 1, - value: 'simplified_shape_v2' - } + value: 'simplified_shape_v2', + optional: false + }, + id: {value: null, fieldIdx: -1, optional: true}, + lat: {value: null, fieldIdx: -1, optional: true}, + lng: {value: null, fieldIdx: -1, optional: true}, + altitude: {value: null, fieldIdx: -1, optional: true}, + sortBy: {value: null, fieldIdx: -1, optional: true} }, + columnMode: 'geojson', hidden: false, isVisible: true, isConfigActive: false, @@ -2685,8 +2692,14 @@ mergedLayer1.config = { label: 'stroke by pop', color: [221, 178, 124], columns: { - geojson: {value: 'simplified_shape', fieldIdx: 2} + geojson: {value: 'simplified_shape', fieldIdx: 2, optional: false}, + id: {value: null, fieldIdx: -1, optional: true}, + lat: {value: null, fieldIdx: -1, optional: true}, + lng: {value: null, fieldIdx: -1, optional: true}, + altitude: {value: null, fieldIdx: -1, optional: true}, + sortBy: {value: null, fieldIdx: -1, optional: true} }, + columnMode: 'geojson', isVisible: true, isConfigActive: false, highlightColor: [252, 242, 26, 255], diff --git a/test/fixtures/test-trip-csv-data.js b/test/fixtures/test-trip-csv-data.js new file mode 100644 index 0000000000..70a0947904 --- /dev/null +++ b/test/fixtures/test-trip-csv-data.js @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +import {DEFAULT_TEXT_LABEL, DEFAULT_COLOR_RANGE, DEFAULT_LAYER_OPACITY} from '@kepler.gl/constants'; + +const gps = `timestamp,location-lng,location-lat,ground-speed,heading,name,location-alt +2014-08-01 00:00:23.000,90.2266981,27.6162803,0.22,0.0,Thuub,3217.0 +2014-08-01 00:10:07.000,,,0.27,140.9,Thuub,3212.3 +2014-08-01 00:20:07.000,90.2267115,27.6162102,0.27,110.1,Thuub,3209.1 +2014-08-01 00:30:07.000,90.2267409,27.6162514,0.32,0.0,Thuub,3206.2 +2014-08-01 00:40:07.000,90.2265545,27.6163481,0.46,0.0,Thuub,3231.3 +2014-08-01 00:50:07.000,90.2269491,27.6161884,0.51,0.0,Thuub,3178.8 +2014-08-01 01:00:07.000,90.2265336,27.6163122,0.3,99.61,Thuub,3257.2 +2014-08-01 01:10:07.000,90.2267568,27.6162647,0.33,0.0,Thuub,3192.5 +2014-08-01 01:20:07.000,90.2267296,27.6163003,0.43,0.0,Thuub,3199.2 +2014-08-01 06:42:41.000,82.1138274,27.4378974,15.95,64.23,Ngang Ka,966.3 +,82.1139649,27.4379582,14.9,62.91,Ngang Ka,969.9 +2014-08-01 06:42:43.000,82.1140881,27.4380211,13.67,58.33,Ngang Ka,972.8 +2014-08-01 06:42:44.000,82.1141907,27.4380927,12.54,48.5,Ngang Ka,974.6 +2014-08-01 06:42:45.000,82.114263,27.4381765,11.44,32.11,Ngang Ka,975.2 +2014-08-01 06:42:46.000,82.1143013,27.438272,11.35,13.76,Ngang Ka,975.8 +2014-08-01 06:42:47.000,82.11431,27.4383692,10.8,0.66,Ngang Ka,977.7 +2014-08-01 06:42:48.000,82.1142962,27.4384518,8.76,346.69,Ngang Ka,980.6 +2014-08-01 06:42:49.000,82.1142637,27.4385091,6.63,325.71,Ngang Ka,983.6 +2014-08-01 06:42:50.000,82.1142176,27.4385368,5.34,292.95,Ngang Ka,985.9`; + +export const tripCsvDataInfo = { + id: 'trip_csv_data', + label: 'Trip Csv Data', + color: [100, 100, 100] +}; + +// test first 2 +export const expectedCoordinates = [ + [90.2266981, 27.6162803, 3217, '2014-08-01 00:00:23.000'], + [90.2267115, 27.6162102, 3209.1, '2014-08-01 00:20:07.000'] +]; +expectedCoordinates[0].datumIndex = 0; +expectedCoordinates[0].datum = [ + '2014-08-01 00:00:23.000', + 90.2266981, + 27.6162803, + 0.22, + 0, + 'Thuub', + 3217 +]; + +expectedCoordinates[1].datumIndex = 2; +expectedCoordinates[1].datum = [ + '2014-08-01 00:20:07.000', + 90.2267115, + 27.6162102, + 0.27, + 110.1, + 'Thuub', + 3209.1 +]; + +export const expectedTripLayerConfig = { + id: 'dont_test_me', + type: 'trip', + config: { + dataId: 'trip_csv_data', + columnMode: 'table', + label: 'location', + color: [130, 154, 227], + columns: { + id: 'name', + lat: 'location-lat', + lng: 'location-lng', + timestamp: 'timestamp', + altitude: 'location-alt' + }, + isVisible: true, + visConfig: { + opacity: DEFAULT_LAYER_OPACITY, + thickness: 2, + colorRange: DEFAULT_COLOR_RANGE, + trailLength: 180, + fadeTrail: true, + billboard: false, + sizeRange: [0, 10] + }, + hidden: false, + textLabel: [DEFAULT_TEXT_LABEL] + }, + visualChannels: { + colorField: { + name: 'ground-speed', + type: 'real' + }, + colorScale: 'quantile', + sizeField: null, + sizeScale: 'linear' + } +}; +export default gps; diff --git a/test/helpers/layer-utils.js b/test/helpers/layer-utils.js index 57c3c7fce2..ead7075c1e 100644 --- a/test/helpers/layer-utils.js +++ b/test/helpers/layer-utils.js @@ -30,6 +30,7 @@ import csvData, {wktCsv} from '../fixtures/test-csv-data'; import testLayerData, {bounds, fieldDomain, iconGeometry} from '../fixtures/test-layer-data'; import {geojsonData} from '../fixtures/geojson'; import tripGeoJson from '../fixtures/trip-geojson'; +import {IntlWrapper} from './component-utils'; import {logStep} from '../../scripts/log'; @@ -85,17 +86,29 @@ export function testCreateCases(t, LayerClass, testCases) { mount(); }, 'layer icon should be mountable'); - /* if (layer.layerInfoModal) { - t.doesNotThrow(() => { - mount( - - - - ); - }, 'layer info modal should be mountable'); + if (layer.layerInfoModal.template) { + t.doesNotThrow(() => { + mount( + + + + ); + }, 'layer info modal should be mountable'); + } else if (Object.keys(layer.layerInfoModal).length) { + // layerInfoModal is based on columnMode + Object.keys(layer.layerInfoModal).forEach(mode => { + const Template = layer.layerInfoModal[mode].template; + t.doesNotThrow(() => { + mount( + +