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

Spatial index widgets #34

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
608d5f2
feat(source): extend source options in widget constructor with spatia…
juandjara Nov 29, 2024
e317a9b
feat(widgets): read spatialDataColumn and spatialDataType from props …
juandjara Nov 29, 2024
6932b37
feat(widgets): allow passing spatialFiltersResolution to all model calls
juandjara Nov 29, 2024
51622c2
feat(widgets): allow passing spatialFiltersMode to all model calls
juandjara Nov 29, 2024
341e691
fix(source): fix source input types
juandjara Nov 29, 2024
76fde7a
feat(model): add dataResolution prop
juandjara Nov 29, 2024
aceef33
feat(utils): add getSpatialFiltersResolution util
juandjara Nov 29, 2024
c4d7332
fix vector source types
juandjara Dec 2, 2024
7c74ae2
export spatial index utils
juandjara Dec 2, 2024
25ae33d
prettier format
juandjara Dec 2, 2024
d50d904
Spatial index widgets second proposal (#35)
juandjara Dec 4, 2024
cbe99fc
chore: run prettier
juandjara Dec 4, 2024
f85f818
dont export functions from spatial-index util file
juandjara Dec 4, 2024
a186251
pass view state to model call in widgets
juandjara Dec 4, 2024
05ea3bd
use new params in inner model call
juandjara Dec 4, 2024
c684a72
example with h3 widgets
juandjara Dec 4, 2024
6628b01
chore: run prettier
juandjara Dec 4, 2024
046a077
add .env to .gitignore
juandjara Dec 5, 2024
8251cea
simplify viewstate passing
juandjara Dec 5, 2024
c41dbe9
simplify source prop type in getSpatialFiltersResolution
juandjara Dec 5, 2024
2f98cbc
simplify viewState prop type in getSpatialFiltersResolution
juandjara Dec 5, 2024
adb0bf7
chore: remove geoColumn
juandjara Dec 5, 2024
62c890e
chore: run prettier
juandjara Dec 5, 2024
5703d12
Refactor model call query params (#41)
juandjara Dec 10, 2024
cbea41b
run prettier
juandjara Dec 10, 2024
b0af864
query parameters default
juandjara Dec 10, 2024
e3bbde6
chore(release): v0.4.1-alpha.0
juandjara Dec 10, 2024
aeb8df5
Merge branch 'main' of https://github.com/CartoDB/carto-api-client in…
juandjara Dec 10, 2024
125385b
chore(release): v0.4.1-alpha.1
juandjara Dec 10, 2024
1d47f7b
chore(release): v0.4.1-alpha.0
juandjara Dec 10, 2024
26b8948
pass dataResolution from source to widgets
juandjara Dec 12, 2024
898740a
Merge branch 'main' of https://github.com/CartoDB/carto-api-client in…
juandjara Dec 12, 2024
f7220a0
run prettier
juandjara Dec 12, 2024
cb8711f
chore(release): v0.4.2-alpha.0
juandjara Dec 12, 2024
2bb52df
Update src/models/model.ts
juandjara Dec 18, 2024
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
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './filters.js';
export * from './geo.js';
export * from './widget-sources/index.js';
export * from './types.js';
export * from './spatial-index.js';

export {
APIErrorContext,
Expand Down
7 changes: 7 additions & 0 deletions src/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {$TODO} from '../types-internal.js';
import {assert} from '../utils.js';
import {ModelRequestOptions, makeCall} from './common.js';
import {ApiVersion} from '../constants.js';
import {SpatialDataType, SpatialFilterPolyfillMode} from '../sources/types.js';

/** @internalRemarks Source: @carto/react-api */
const AVAILABLE_MODELS = [
Expand Down Expand Up @@ -38,6 +39,12 @@ export interface ModelSource {
geoColumn?: string;
spatialFilter?: SpatialFilter;
queryParameters?: QueryParameters;
spatialDataColumn?: string;
spatialDataType?: SpatialDataType;
spatialFiltersResolution?: number;
spatialFiltersMode?: SpatialFilterPolyfillMode;
/** original resolution of the spatial index data as stored in the DW */
dataResolution?: number;
}

const {V3} = ApiVersion;
Expand Down
14 changes: 11 additions & 3 deletions src/sources/h3-query-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import type {
export type H3QuerySourceOptions = SourceOptions &
QuerySourceOptions &
AggregationOptions &
FilterOptions;
FilterOptions & {
spatialDataType: 'h3';
};

type UrlParameters = {
aggregationExp: string;
aggregationResLevel?: string;
Expand All @@ -30,7 +33,7 @@ type UrlParameters = {
};

export const h3QuerySource = async function (
options: H3QuerySourceOptions
options: Omit<H3QuerySourceOptions, 'spatialDataType'>
juandjara marked this conversation as resolved.
Show resolved Hide resolved
): Promise<TilejsonResult & WidgetQuerySourceResult> {
const {
aggregationExp,
Expand Down Expand Up @@ -59,7 +62,12 @@ export const h3QuerySource = async function (
return baseSource<UrlParameters>('query', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetQuerySource(options),
widgetSource: new WidgetQuerySource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'h3'
spatialDataColumn,
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved
spatialDataType: 'h3',
}),
})
);
};
13 changes: 10 additions & 3 deletions src/sources/h3-table-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import type {
export type H3TableSourceOptions = SourceOptions &
TableSourceOptions &
AggregationOptions &
FilterOptions;
FilterOptions & {
spatialDataType: 'h3';
};

type UrlParameters = {
aggregationExp: string;
Expand All @@ -30,7 +32,7 @@ type UrlParameters = {
};

export const h3TableSource = async function (
options: H3TableSourceOptions
options: Omit<H3TableSourceOptions, 'spatialDataType'>
): Promise<TilejsonResult & WidgetTableSourceResult> {
const {
aggregationExp,
Expand All @@ -55,7 +57,12 @@ export const h3TableSource = async function (
return baseSource<UrlParameters>('table', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetTableSource(options),
widgetSource: new WidgetTableSource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'h3'
spatialDataColumn,
spatialDataType: 'h3',
}),
})
);
};
13 changes: 10 additions & 3 deletions src/sources/quadbin-query-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import type {
export type QuadbinQuerySourceOptions = SourceOptions &
QuerySourceOptions &
AggregationOptions &
FilterOptions;
FilterOptions & {
spatialDataType: 'quadbin';
};

type UrlParameters = {
aggregationExp: string;
Expand All @@ -31,7 +33,7 @@ type UrlParameters = {
};

export const quadbinQuerySource = async function (
options: QuadbinQuerySourceOptions
options: Omit<QuadbinQuerySourceOptions, 'spatialDataType'>
): Promise<TilejsonResult & WidgetQuerySourceResult> {
const {
aggregationExp,
Expand Down Expand Up @@ -60,7 +62,12 @@ export const quadbinQuerySource = async function (
return baseSource<UrlParameters>('query', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetQuerySource(options),
widgetSource: new WidgetQuerySource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'quadbin'
spatialDataColumn,
spatialDataType: 'quadbin',
}),
})
);
};
13 changes: 10 additions & 3 deletions src/sources/quadbin-table-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import type {
export type QuadbinTableSourceOptions = SourceOptions &
TableSourceOptions &
AggregationOptions &
FilterOptions;
FilterOptions & {
spatialDataType: 'quadbin';
};

type UrlParameters = {
aggregationExp: string;
Expand All @@ -30,7 +32,7 @@ type UrlParameters = {
};

export const quadbinTableSource = async function (
options: QuadbinTableSourceOptions
options: Omit<QuadbinTableSourceOptions, 'spatialDataType'>
): Promise<TilejsonResult & WidgetTableSourceResult> {
const {
aggregationExp,
Expand All @@ -56,7 +58,12 @@ export const quadbinTableSource = async function (
return baseSource<UrlParameters>('table', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetTableSource(options),
widgetSource: new WidgetTableSource({
...options,
// NOTE: passing redundant spatialDataColumn here to apply the default value 'quadbin'
spatialDataColumn,
spatialDataType: 'quadbin',
}),
})
);
};
8 changes: 8 additions & 0 deletions src/sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ export type ColumnsOption = {

export type SpatialDataType = 'geo' | 'h3' | 'quadbin';

/**
* Strategy used for covering spatial filter geometry with spatial indexes.
* See https://docs.carto.com/data-and-analysis/analytics-toolbox-for-bigquery/sql-reference/quadbin#quadbin_polyfill_mode
* or https://docs.carto.com/data-and-analysis/analytics-toolbox-for-bigquery/sql-reference/h3#h3_polyfill_mode for more information.
* @internalRemarks Source: cloud-native maps-api
* */
export type SpatialFilterPolyfillMode = 'center' | 'intersects' | 'contains';

export type TilejsonMapInstantiation = MapInstantiation & {
tilejson: {url: string[]};
};
Expand Down
11 changes: 8 additions & 3 deletions src/sources/vector-query-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import type {
export type VectorQuerySourceOptions = SourceOptions &
QuerySourceOptions &
FilterOptions &
ColumnsOption;
ColumnsOption & {
spatialDataType: 'geo';
};

type UrlParameters = {
columns?: string;
Expand All @@ -34,7 +36,7 @@ type UrlParameters = {
};

export const vectorQuerySource = async function (
options: VectorQuerySourceOptions
options: Omit<VectorQuerySourceOptions, 'spatialDataType'>
): Promise<TilejsonResult & WidgetQuerySourceResult> {
const {
columns,
Expand Down Expand Up @@ -64,7 +66,10 @@ export const vectorQuerySource = async function (
return baseSource<UrlParameters>('query', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetQuerySource(options),
widgetSource: new WidgetQuerySource({
...options,
spatialDataType: 'geo',
}),
})
);
};
12 changes: 9 additions & 3 deletions src/sources/vector-table-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import type {
export type VectorTableSourceOptions = SourceOptions &
TableSourceOptions &
FilterOptions &
ColumnsOption;
ColumnsOption & {
spatialDataType: 'geo';
};

type UrlParameters = {
columns?: string;
filters?: Record<string, unknown>;
Expand All @@ -32,7 +35,7 @@ type UrlParameters = {
};

export const vectorTableSource = async function (
options: VectorTableSourceOptions
options: Omit<VectorTableSourceOptions, 'spatialDataType'>
): Promise<TilejsonResult & WidgetTableSourceResult> {
const {
columns,
Expand All @@ -58,7 +61,10 @@ export const vectorTableSource = async function (
return baseSource<UrlParameters>('table', options, urlParameters).then(
(result) => ({
...(result as TilejsonResult),
widgetSource: new WidgetTableSource(options),
widgetSource: new WidgetTableSource({
...options,
spatialDataType: 'geo',
}),
})
);
};
125 changes: 125 additions & 0 deletions src/spatial-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
DEFAULT_AGGREGATION_RES_LEVEL_H3,
DEFAULT_AGGREGATION_RES_LEVEL_QUADBIN,
} from './constants-internal';
import {ModelSource} from './models/model';
import {AggregationOptions} from './sources/types';

const DEFAULT_TILE_SIZE = 512;
const QUADBIN_ZOOM_MAX_OFFSET = 4;

export function getSpatialFiltersResolution({
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved
source,
viewState,
}: {
source: ModelSource & AggregationOptions;
viewState: {
zoom: number;
latitude: number;
longitude: number;
};
}) {
if (source.spatialDataType === 'geo') {
return undefined;
}

const currentZoom = viewState.zoom ?? 1;
juandjara marked this conversation as resolved.
Show resolved Hide resolved

const dataResolution = source.dataResolution ?? Number.MAX_VALUE;

const aggregationResLevel =
source.aggregationResLevel ??
(source.spatialDataType === 'h3'
? DEFAULT_AGGREGATION_RES_LEVEL_H3
: DEFAULT_AGGREGATION_RES_LEVEL_QUADBIN);
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved

const aggregationResLevelOffset = Math.max(
0,
Math.floor(aggregationResLevel)
);

const currentZoomInt = Math.ceil(currentZoom);
if (source.spatialDataType === 'h3') {
const tileSize = DEFAULT_TILE_SIZE;
const maxResolutionForZoom =
maxH3SpatialFiltersResolutions.find(
([zoom]) => zoom === currentZoomInt
)?.[1] ?? Math.max(0, currentZoomInt - 3);

const maxSpatialFiltersResolution = maxResolutionForZoom
? Math.min(dataResolution, maxResolutionForZoom)
: dataResolution;

const hexagonResolution =
getHexagonResolution(
{zoom: currentZoom, latitude: viewState.latitude},
tileSize
) + aggregationResLevelOffset;

return Math.min(hexagonResolution, maxSpatialFiltersResolution);
}

if (source.spatialDataType === 'quadbin') {
const maxResolutionForZoom = currentZoomInt + QUADBIN_ZOOM_MAX_OFFSET;
const maxSpatialFiltersResolution = Math.min(
dataResolution,
maxResolutionForZoom
);

const quadsResolution =
Math.floor(viewState.zoom) + aggregationResLevelOffset;
return Math.min(quadsResolution, maxSpatialFiltersResolution);
}

return undefined;
}

const maxH3SpatialFiltersResolutions = [
[20, 14],
[19, 13],
[18, 12],
[17, 11],
[16, 10],
[15, 9],
[14, 8],
[13, 7],
[12, 7],
[11, 7],
[10, 6],
[9, 6],
[8, 5],
[7, 4],
[6, 4],
[5, 3],
[4, 2],
[3, 1],
[2, 1],
[1, 0],
];

// stolen from https://github.com/visgl/deck.gl/blob/master/modules/carto/src/layers/h3-tileset-2d.ts

// Relative scale factor (0 = no biasing, 2 = a few hexagons cover view)
const BIAS = 2;

// Resolution conversion function. Takes a WebMercatorViewport and returns
// a H3 resolution such that the screen space size of the hexagons is
// similar
export function getHexagonResolution(
viewport: {zoom: number; latitude: number},
tileSize: number
): number {
// Difference in given tile size compared to deck's internal 512px tile size,
// expressed as an offset to the viewport zoom.
const zoomOffset = Math.log2(tileSize / DEFAULT_TILE_SIZE);
const hexagonScaleFactor = (2 / 3) * (viewport.zoom - zoomOffset);
const latitudeScaleFactor = Math.log(
1 / Math.cos((Math.PI * viewport.latitude) / 180)
);

// Clip and bias
return Math.max(
0,
Math.floor(hexagonScaleFactor + latitudeScaleFactor - BIAS)
);
}
4 changes: 3 additions & 1 deletion src/widget-sources/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {TileResolution} from '../sources/types';
import {SpatialFilterPolyfillMode, TileResolution} from '../sources/types';
import {
GroupDateType,
SortColumnType,
Expand All @@ -13,6 +13,8 @@ import {
/** Common options for {@link WidgetBaseSource} requests. */
interface BaseRequestOptions {
spatialFilter?: SpatialFilter;
spatialFiltersResolution?: number;
spatialFiltersMode?: SpatialFilterPolyfillMode;
abortController?: AbortController;
filterOwner?: string;
}
Expand Down
Loading