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

feat(picking): Add Picking model #31

Merged
merged 6 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 14 additions & 5 deletions src/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const AVAILABLE_MODELS = [
'category',
'histogram',
'formula',
'pick',
'timeseries',
'range',
'scatterplot',
Expand Down Expand Up @@ -74,7 +75,13 @@ export function executeModel(props: {

let url = `${apiBaseUrl}/v3/sql/${connectionName}/model/${model}`;

const {filters, filtersLogicalOperator = 'and', data} = source;
const {
data,
filters,
filtersLogicalOperator = 'and',
geoColumn = DEFAULT_GEO_COLUMN,
} = source;

const queryParameters = source.queryParameters
? JSON.stringify(source.queryParameters)
: '';
Expand All @@ -89,12 +96,14 @@ export function executeModel(props: {
filtersLogicalOperator,
};

// Picking Model API requires 'spatialDataColumn'.
if (model === 'pick') {
queryParams.spatialDataColumn = geoColumn;
}

// API supports multiple filters, we apply it only to geoColumn
const spatialFilters = source.spatialFilter
? {
[source.geoColumn ? source.geoColumn : DEFAULT_GEO_COLUMN]:
source.spatialFilter,
}
? {[geoColumn]: source.spatialFilter}
: undefined;

if (spatialFilters) {
Expand Down
51 changes: 51 additions & 0 deletions src/widget-sources/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {TileResolution} from '../sources/types';
import {
GroupDateType,
SortColumnType,
Expand All @@ -23,6 +24,49 @@ export interface CategoryRequestOptions extends BaseRequestOptions {
operationColumn?: string;
}

/**
* Options for {@link WidgetBaseSource#getFeatures}.
* @experimental
* @internal
*/
export interface FeaturesRequestOptions extends BaseRequestOptions {
/**
* Feature IDs, as found in `_carto_feature_id`. Feature IDs are a hash
* of geometry, and features with identical geometry will have the same
* feature ID. Order is important; features in the result set will be
* sorted according to the order of IDs in the request.
*/
featureIds: string[];

/**
* Columns to be returned for each picked object. Note that for datasets
* containing features with identical geometry, more than one result per
* requested feature ID may be returned. To match results back to the
* requested feature ID, include `_carto_feature_id` in the columns list.
*/
columns: string[];

/** Topology of objects to be picked. */
dataType: 'points' | 'lines' | 'polygons';

/** Zoom level, required if using 'points' data type. */
z?: number;

/**
* Maximum number of objects to return in the result set. For datasets
* containing features with identical geometry, those features will have
* the same feature IDs, and so more results may be returned than feature IDs
* given in the request.
*/
limit?: number;

/**
* Must match `tileResolution` used when obtaining the `_carto_feature_id`
* column, typically in a layer's tile requests.
*/
tileResolution?: TileResolution;
}

/** Options for {@link WidgetBaseSource#getFormula}. */
export interface FormulaRequestOptions extends BaseRequestOptions {
column: string;
Expand Down Expand Up @@ -77,6 +121,13 @@ export interface TimeSeriesRequestOptions extends BaseRequestOptions {
* WIDGET API RESPONSES
*/

/**
* Response from {@link WidgetBaseSource#getFeatures}.
* @experimental
* @internal
*/
export type FeaturesResponse = {rows: Record<string, unknown>[]};

/** Response from {@link WidgetBaseSource#getFormula}. */
export type FormulaResponse = {value: number};

Expand Down
44 changes: 43 additions & 1 deletion src/widget-sources/widget-base-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {executeModel} from '../models/index.js';
import {
CategoryRequestOptions,
CategoryResponse,
FeaturesRequestOptions,
FeaturesResponse,
FormulaRequestOptions,
FormulaResponse,
HistogramRequestOptions,
Expand All @@ -21,7 +23,10 @@ import {getClient} from '../client.js';
import {ModelSource} from '../models/model.js';
import {SourceOptions} from '../sources/index.js';
import {ApiVersion, DEFAULT_API_BASE_URL} from '../constants.js';
import {DEFAULT_GEO_COLUMN} from '../constants-internal.js';
import {
DEFAULT_GEO_COLUMN,
DEFAULT_TILE_RESOLUTION,
} from '../constants-internal.js';

export interface WidgetBaseSourceProps extends Omit<SourceOptions, 'filters'> {
apiVersion?: ApiVersion;
Expand Down Expand Up @@ -105,6 +110,43 @@ export abstract class WidgetBaseSource<Props extends WidgetBaseSourceProps> {
}).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows));
}

/****************************************************************************
* FEATURES
*/

/**
* Given a list of feature IDs (as found in `_carto_feature_id`) returns all
* matching features. In datasets containing features with duplicate geometries,
* feature IDs may be duplicated (IDs are a hash of geometry) and so more
* results may be returned than IDs in the request.
* @internal
* @experimental
*/
async getFeatures(
options: FeaturesRequestOptions
): Promise<FeaturesResponse> {
const {filterOwner, spatialFilter, abortController, ...params} = options;
const {columns, dataType, featureIds, z, limit, tileResolution} = params;

type FeaturesModelResponse = {rows: Record<string, unknown>[]};

return executeModel({
model: 'pick',
source: {...this.getModelSource(filterOwner), spatialFilter},
params: {
columns,
dataType,
featureIds,
z,
limit: limit || 1000,
tileResolution: tileResolution || DEFAULT_TILE_RESOLUTION,
},
opts: {abortController},
}).then((res: FeaturesModelResponse) => ({
rows: normalizeObjectKeys(res.rows),
}));
}

/****************************************************************************
* FORMULA
*/
Expand Down
49 changes: 49 additions & 0 deletions test/widget-sources/widget-base-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,55 @@ test('filters - owner', async () => {
}
});

/******************************************************************************
* getFeatures
*/

test('getFeatures', async () => {
const widgetSource = new WidgetTestSource({
accessToken: '<token>',
connectionName: 'carto_dw',
});

const expectedRows = [
{_carto_feature_id: 'a', name: 'Veggie Mart', revenue: 1200},
{_carto_feature_id: 'b', name: 'EZ Drive Thru', revenue: 400},
{_carto_feature_id: 'c', name: "Buddy's Convenience", revenue: 800},
];

const mockFetch = vi
.fn()
.mockResolvedValueOnce(
createMockResponse({rows: expectedRows, meta: {foo: 'bar'}})
);
vi.stubGlobal('fetch', mockFetch);

const actualFeatures = await widgetSource.getFeatures({
columns: ['_carto_feature_id', 'name', 'revenue'],
featureIds: ['a', 'b', 'c'],
dataType: 'points',
});

expect(mockFetch).toHaveBeenCalledOnce();
expect(actualFeatures).toEqual({rows: expectedRows});

const params = new URL(mockFetch.mock.lastCall[0]).searchParams.entries();
expect(Object.fromEntries(params)).toMatchObject({
type: 'test',
source: 'test-data',
params: JSON.stringify({
columns: ['_carto_feature_id', 'name', 'revenue'],
dataType: 'points',
featureIds: ['a', 'b', 'c'],
limit: 1000,
tileResolution: 0.5,
}),
queryParameters: '',
filters: JSON.stringify({}),
filtersLogicalOperator: 'and',
});
});

/******************************************************************************
* getFormula
*/
Expand Down