Skip to content

Commit

Permalink
feat(picking): Add Picking Model API (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
donmccurdy authored Nov 14, 2024
1 parent 69d138c commit 00c7bf7
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 6 deletions.
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

0 comments on commit 00c7bf7

Please sign in to comment.