Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] improvements for layer type change logic #2995

Merged
merged 7 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions src/layers/src/base-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React from 'react';
import * as arrow from 'apache-arrow';
import DefaultLayerIcon from './default-layer-icon';
import {diffUpdateTriggers} from './layer-update';
import {getSatisfiedColumnMode, FindDefaultLayerPropsReturnValue} from './layer-utils';

import {
CHANNEL_SCALES,
Expand Down Expand Up @@ -145,7 +146,7 @@ export type UpdateTrigger = {
[key: string]: any;
};
export type LayerBounds = [number, number, number, number];
export type FindDefaultLayerPropsReturnValue = {props: any[]; foundLayers?: any[]};

/**
* Approx. number of points to sample in a large data set
*/
Expand Down Expand Up @@ -386,6 +387,7 @@ class Layer implements KeplerLayer {
get supportedDatasetTypes(): string[] | null {
return null;
}

/*
* Given a dataset, automatically find props to create layer based on it
* and return the props and previous found layers.
Expand Down Expand Up @@ -660,8 +662,15 @@ class Layer implements KeplerLayer {
* When change layer type, try to copy over layer configs as much as possible
* @param configToCopy - config to copy over
* @param visConfigSettings - visConfig settings of config to copy
* @param datasets - current datasets.
* @param defaultLayerProps - default layer creation configurations for current layer and datasets.
*/
assignConfigToLayer(configToCopy, visConfigSettings) {
assignConfigToLayer(
configToCopy: LayerBaseConfig & Partial<LayerColorConfig & LayerSizeConfig>,
visConfigSettings: {[key: string]: ValueOf<LayerVisConfigSettings>},
datasets?: Datasets,
defaultLayerProps?: FindDefaultLayerPropsReturnValue | null
) {
// don't deep merge visualChannel field
// don't deep merge color range, reversed: is not a key by default
const shallowCopy = ['colorRange', 'strokeColorRange'].concat(
Expand Down Expand Up @@ -690,10 +699,40 @@ class Layer implements KeplerLayer {

// update columNode based on new columns
if (this.config.columnMode && this.supportedColumnModes) {
// find a mode with all requied columns
const satisfiedColumnMode = this.supportedColumnModes?.find(mode => {
return mode.requiredColumns?.every(requriedCol => copied.columns?.[requriedCol]?.value);
});
const dataset = datasets?.[this.config.dataId];
// try to find a mode with all requied columns from the source config
let satisfiedColumnMode = getSatisfiedColumnMode(
this.supportedColumnModes,
copied.columns,
dataset?.fields
);

// if no suitable column mode found or no such columMode exists for the layer
// then try use one of the automatically detected layer configs
if (!satisfiedColumnMode) {
const options = [
...(defaultLayerProps?.props || []),
...(defaultLayerProps?.altProps || [])
];
if (options.length) {
// Use the first of the default configurations
const defaultColumnConfig = options[0].columns;

satisfiedColumnMode = getSatisfiedColumnMode(
this.supportedColumnModes,
defaultColumnConfig,
dataset?.fields
);

if (satisfiedColumnMode) {
copied.columns = {
...copied.columns,
...defaultColumnConfig
};
}
}
}

copied.columnMode = satisfiedColumnMode?.key || copied.columnMode;
}

Expand Down
2 changes: 1 addition & 1 deletion src/layers/src/geojson-layer/geojson-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ export default class GeoJsonLayer extends Layer {
this.updateMeta({bounds, fixedRadius, featureTypes});
this.dataToFeature = [...this.dataToFeature, ...dataToFeature];
}
} else if (this.dataToFeature.length === 0) {
} else if (this.dataToFeature.length === 0 || this.config.columnMode === COLUMN_MODE_TABLE) {
const getFeature = this.getPositionAccessor(dataContainer);

const {dataToFeature, bounds, fixedRadius, featureTypes, centroids} = getGeojsonLayerMeta({
Expand Down
11 changes: 11 additions & 0 deletions src/layers/src/heatmap-layer/heatmap-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import {hexToRgb, DataContainerInterface} from '@kepler.gl/utils';
import {KeplerTable} from '@kepler.gl/table';

import {getGeoArrowPointLayerProps, FindDefaultLayerPropsReturnValue} from '../layer-utils';

export type HeatmapLayerVisConfigSettings = {
opacity: VisConfigNumber;
colorRange: VisConfigColorRange;
Expand Down Expand Up @@ -157,6 +159,15 @@ class HeatmapLayer extends MapboxGLLayer {
return super.hasAllColumns();
}

static findDefaultLayerProps(dataset: KeplerTable): FindDefaultLayerPropsReturnValue {
const altProps = getGeoArrowPointLayerProps(dataset);

return {
props: [],
altProps
};
}

get visualChannels(): VisualChannels {
return {
// @ts-expect-error
Expand Down
6 changes: 3 additions & 3 deletions src/layers/src/icon-layer/icon-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import IconLayerIcon from './icon-layer-icon';
import {ICON_FIELDS, KEPLER_UNFOLDED_BUCKET} from '@kepler.gl/constants';
import IconInfoModalFactory from './icon-info-modal';
import Layer, {LayerBaseConfig, LayerBaseConfigPartial} from '../base-layer';
import {assignPointPairToLayerColumn} from '../layer-utils';
import {assignPointPairToLayerColumn, FindDefaultLayerPropsReturnValue} from '../layer-utils';
import {isTest} from '@kepler.gl/utils';
import {getTextOffsetByRadius, formatTextLabelData} from '../layer-text-label';
import {default as KeplerTable} from '@kepler.gl/table';
Expand Down Expand Up @@ -225,8 +225,8 @@ export default class IconLayer extends Layer {
this._layerInfoModal = IconInfoModalFactory(svgIcons);
}

static findDefaultLayerProps({fieldPairs = [], fields = []}: KeplerTable) {
const notFound = {props: []};
static findDefaultLayerProps({fieldPairs = [], fields = []}: KeplerTable): FindDefaultLayerPropsReturnValue {
const notFound: FindDefaultLayerPropsReturnValue = {props: []};
if (!fieldPairs.length || !fields.length) {
return notFound;
}
Expand Down
3 changes: 1 addition & 2 deletions src/layers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ export type {
LayerBaseConfig,
VisualChannelDomain,
VisualChannel,
VisualChannelDescription,
FindDefaultLayerPropsReturnValue
VisualChannelDescription
} from './base-layer';
export * from './base-layer';

Expand Down
81 changes: 79 additions & 2 deletions src/layers/src/layer-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import * as arrow from 'apache-arrow';
import {Feature, BBox} from 'geojson';
import {getGeoMetadata} from '@loaders.gl/gis';

import {KeplerTable} from '@kepler.gl/table';
import {
Field,
ProtoDatasetField,
FieldPair,
SupportedColumnMode,
LayerColumn
LayerColumn,
LayerColumns,
RGBColor
} from '@kepler.gl/types';
import {DataContainerInterface, ArrowDataContainer} from '@kepler.gl/utils';
import {
Expand All @@ -33,7 +36,23 @@ import {

import {DeckGlGeoTypes, GeojsonDataMaps} from './geojson-layer/geojson-utils';

export function assignPointPairToLayerColumn(pair: FieldPair, hasAlt: boolean) {
export type FindDefaultLayerProps = {
label: string;
color?: RGBColor;
isVisible?: boolean;
columns?: Record<string, LayerColumn>;
};

export type FindDefaultLayerPropsReturnValue = {
/** Layer props to create layers by default when a dataset is added */
props: FindDefaultLayerProps[];
/** layer props of possible alternative layer configurations, not created by default */
altProps?: FindDefaultLayerProps[];
/** Already found layer configurations */
foundLayers?: FindDefaultLayerProps[];
};

export function assignPointPairToLayerColumn(pair: FieldPair, hasAlt: boolean): Record<string, LayerColumn> {
const {lat, lng, altitude} = pair.pair;
if (!hasAlt) {
return {lat, lng};
Expand Down Expand Up @@ -411,3 +430,61 @@ export function getBoundsFromArrowMetadata(

return false;
}

/**
* Finds and returns the first satisfied column mode based on the provided columns and fields.
* @param supportedColumnModes - An array of supported column modes to check.
* @param columns - The available columns.
* @param fields - Optional table fields to be used for extra verification.
* @returns The first column mode that satisfies the required conditions, or undefined if none match.
*/
export function getSatisfiedColumnMode(
columnModes: SupportedColumnMode[] | null,
columns: LayerColumns | undefined,
fields?: KeplerTable['fields']
): SupportedColumnMode | undefined {
return columnModes?.find(mode => {
return mode.requiredColumns?.every(requriedCol => {
const column = columns?.[requriedCol];
if (column?.value) {
if (mode.verifyField && fields?.[column.fieldIdx]) {
const field = fields[column.fieldIdx];
return mode.verifyField(field);
}
return true;
}
return false;
});
});
}

/**
* Returns true if the field is of geoarrow point format.
* @param field A field.
* @returns Returns true if the field is of geoarrow point format.
*/
export function isGeoArrowPointField(field: Field) {
return (
field.type === 'geoarrow' &&
field.metadata?.get('ARROW:extension:name') === EXTENSION_NAME.POINT
);
}

/**
* Create default geoarrow column props based on the dataset.
* @param dataset A dataset to create layer props from.
* @returns geoarrow column props.
*/
export function getGeoArrowPointLayerProps(dataset: KeplerTable) {
const {label} = dataset;
const altProps: FindDefaultLayerProps[] = [];
dataset.fields.forEach(field => {
if (isGeoArrowPointField(field)) {
altProps.push({
label: (typeof label === 'string' && label.replace(/\.[^/.]+$/, '')) || field.name,
columns: {geoarrow: {value: field.name, fieldIdx: field.fieldIdx}}
});
}
});
return altProps;
}
26 changes: 15 additions & 11 deletions src/layers/src/point-layer/point-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ import {
assignPointPairToLayerColumn,
isLayerHoveredFromArrow,
getBoundsFromArrowMetadata,
getGeoArrowPointLayerProps,
isGeoArrowPointField,
createGeoArrowPointVector,
getFilteredIndex,
getNeighbors
getNeighbors,
FindDefaultLayerProps
} from '../layer-utils';
import {getGeojsonPointDataMaps, GeojsonPointDataMaps} from '../geojson-layer/geojson-utils';
import {
Expand Down Expand Up @@ -150,12 +153,14 @@ const SUPPORTED_COLUMN_MODES = [
{
key: COLUMN_MODE_GEOJSON,
label: 'GeoJSON Feature',
requiredColumns: geojsonRequiredColumns
requiredColumns: geojsonRequiredColumns,
verifyField: f => !isGeoArrowPointField(f)
},
{
key: COLUMN_MODE_GEOARROW,
label: 'Geoarrow Points',
requiredColumns: geoarrowRequiredColumns
requiredColumns: geoarrowRequiredColumns,
verifyField: f => isGeoArrowPointField(f)
}
];
const DEFAULT_COLUMN_MODE = COLUMN_MODE_POINTS;
Expand Down Expand Up @@ -322,13 +327,10 @@ export default class PointLayer extends Layer {
return this;
}

static findDefaultLayerProps({fieldPairs = [], type}: KeplerTable) {
const props: {
label: string;
color?: RGBColor;
isVisible?: boolean;
columns?: PointLayerColumnsConfig;
}[] = [];
static findDefaultLayerProps(dataset: KeplerTable) {
const {fieldPairs = [], type} = dataset;

const props: FindDefaultLayerProps[] = [];

if (type === DatasetType.VECTOR_TILE) {
return {props};
Expand Down Expand Up @@ -362,7 +364,9 @@ export default class PointLayer extends Layer {
props.push(prop);
});

return {props};
const altProps = getGeoArrowPointLayerProps(dataset);

return {props, altProps};
}

getDefaultLayerConfig(props: LayerBaseConfigPartial) {
Expand Down
5 changes: 2 additions & 3 deletions src/layers/src/vector-tile/abstract-tile-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from '../base-layer';
import TileDataset from './common-tile/tile-dataset';
import {isIndexedField, isDomainQuantiles} from './common-tile/tile-utils';
import {FindDefaultLayerPropsReturnValue} from '../layer-utils';

const DEFAULT_ELEVATION = 500;
export const DEFAULT_RADIUS = 1;
Expand Down Expand Up @@ -163,9 +164,7 @@ export default abstract class AbstractTileLayer<

protected abstract initTileDataset(): TileDataset<T, I>;

static findDefaultLayerProps(dataset: KeplerDataset): {
props: {dataId: string; label?: string; isVisible: boolean}[];
} {
static findDefaultLayerProps(dataset: KeplerDataset): FindDefaultLayerPropsReturnValue {
if (!isTileDataset(dataset)) {
return {props: []};
}
Expand Down
5 changes: 2 additions & 3 deletions src/layers/src/vector-tile/vector-tile-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
VisualChannelDomain,
VisualChannelField
} from '../base-layer';
import {FindDefaultLayerPropsReturnValue} from '../layer-utils';

import AbstractTileLayer, {
LayerData as CommonLayerData,
Expand Down Expand Up @@ -186,9 +187,7 @@ export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Featu

meta = {};

static findDefaultLayerProps(dataset: KeplerDataset): {
props: {dataId: string; label?: string; isVisible: boolean}[];
} {
static findDefaultLayerProps(dataset: KeplerDataset): FindDefaultLayerPropsReturnValue {
if (dataset.type !== DatasetType.VECTOR_TILE) {
return {props: []};
}
Expand Down
13 changes: 12 additions & 1 deletion src/reducers/src/vis-state-updaters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,18 @@ export function layerTypeChangeUpdater(
// get a mint layer, with new id and type
// because deck.gl uses id to match between new and old layer.
// If type has changed but id is the same, it will break
newLayer.assignConfigToLayer(oldLayer.config, oldLayer.visConfigSettings);

const defaultLayerProps =
typeof state.layerClasses[newType].findDefaultLayerProps === 'function'
? state.layerClasses[newType].findDefaultLayerProps(state.datasets[newLayer.config.dataId])
: null;

newLayer.assignConfigToLayer(
oldLayer.config,
oldLayer.visConfigSettings,
state.datasets,
defaultLayerProps
);
newLayer.updateLayerDomain(state.datasets);
}

Expand Down
1 change: 1 addition & 0 deletions src/types/layers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type SupportedColumnMode = {
requiredColumns: string[];
optionalColumns?: string[];
hasHelp?: boolean;
verifyField?: (field: Field) => boolean;
};

export type VisualChannelField = Field | null;
Expand Down
Loading