diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d725fbe..e67926c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Not released +- Spatial Index Sources use remote widgets calculation [#898](https://github.com/CartoDB/carto-react/pull/898) + ## 3.0.0 ### 3.0.0-alpha.17 (2024-07-29) diff --git a/packages/react-api/__tests__/api/model.test.js b/packages/react-api/__tests__/api/model.test.js index a0bc77a12..2a8606d9a 100644 --- a/packages/react-api/__tests__/api/model.test.js +++ b/packages/react-api/__tests__/api/model.test.js @@ -123,7 +123,7 @@ describe('model', () => { expect(mockedMakeCall).toHaveBeenCalledWith({ credentials: TABLE_SOURCE.credentials, opts: { method: 'GET' }, - url: 'https://gcp-us-east1.api.carto.com/v3/sql/carto-ps-bq-developers/model/formula?type=query&client=c4react&source=SELECT+*+FROM+%60cartobq.public_account.seattle_collisions%60+WHERE+time_column+%3E+%40start+AND+time_column+%3C+%40end¶ms=%7B%22column%22%3A%22__test__%22%2C%22operation%22%3A%22avg%22%7D&queryParameters=%7B%22start%22%3A%222019-01-01%22%2C%22end%22%3A%222019-01-02%22%7D&filters=%7B%7D&filtersLogicalOperator=AND&spatialFilters=%7B%22geom%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B-84.40640911186557%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C31.358634554371573%5D%5D%5D%7D%7D' + url: 'https://gcp-us-east1.api.carto.com/v3/sql/carto-ps-bq-developers/model/formula?type=query&client=c4react&source=SELECT+*+FROM+%60cartobq.public_account.seattle_collisions%60+WHERE+time_column+%3E+%40start+AND+time_column+%3C+%40end¶ms=%7B%22column%22%3A%22__test__%22%2C%22operation%22%3A%22avg%22%7D&queryParameters=%7B%22start%22%3A%222019-01-01%22%2C%22end%22%3A%222019-01-02%22%7D&filters=%7B%7D&filtersLogicalOperator=AND&spatialFilters=%7B%22geom%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B-84.40640911186557%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C31.358634554371573%5D%5D%5D%7D%7D&spatialDataType=geo' }); }); @@ -138,7 +138,7 @@ describe('model', () => { expect(mockedMakeCall).toHaveBeenCalledWith({ credentials: TABLE_SOURCE.credentials, opts: { method: 'GET' }, - url: 'https://gcp-us-east1.api.carto.com/v3/sql/carto-ps-bq-developers/model/formula?type=query&client=c4react&source=SELECT+*+FROM+%60cartobq.public_account.seattle_collisions%60+WHERE+time_column+%3E+%40start+AND+time_column+%3C+%40end¶ms=%7B%22column%22%3A%22__test__%22%2C%22operation%22%3A%22avg%22%7D&queryParameters=%7B%22start%22%3A%222019-01-01%22%2C%22end%22%3A%222019-01-02%22%7D&filters=%7B%7D&filtersLogicalOperator=AND&spatialFilters=%7B%22abc%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B-84.40640911186557%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C31.358634554371573%5D%5D%5D%7D%7D' + url: 'https://gcp-us-east1.api.carto.com/v3/sql/carto-ps-bq-developers/model/formula?type=query&client=c4react&source=SELECT+*+FROM+%60cartobq.public_account.seattle_collisions%60+WHERE+time_column+%3E+%40start+AND+time_column+%3C+%40end¶ms=%7B%22column%22%3A%22__test__%22%2C%22operation%22%3A%22avg%22%7D&queryParameters=%7B%22start%22%3A%222019-01-01%22%2C%22end%22%3A%222019-01-02%22%7D&filters=%7B%7D&filtersLogicalOperator=AND&spatialFilters=%7B%22abc%22%3A%7B%22type%22%3A%22Polygon%22%2C%22coordinates%22%3A%5B%5B%5B-84.40640911186557%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C23.809680634191537%5D%2C%5B-78.72111096471372%2C31.358634554371573%5D%2C%5B-84.40640911186557%2C31.358634554371573%5D%5D%5D%7D%7D&spatialDataType=geo' }); }); }); diff --git a/packages/react-api/src/api/model.js b/packages/react-api/src/api/model.js index ba2f29016..9078e9545 100644 --- a/packages/react-api/src/api/model.js +++ b/packages/react-api/src/api/model.js @@ -66,15 +66,46 @@ export function executeModel(props) { filtersLogicalOperator }; - // API supports multiple filters, we apply it only to geoColumn - const spatialFilters = spatialFilter - ? { - [source.geoColumn ? source.geoColumn : DEFAULT_GEO_COLUMN]: spatialFilter + let spatialFilters; + if (spatialFilter) { + let spatialDataType = source.spatialDataType; + let spatialDataColumn = source.spatialDataColumn; + + if (!spatialDataType || !spatialDataColumn) { + if (source.geoColumn) { + const parsedGeoColumn = source.geoColumn.split(':'); + if (parsedGeoColumn.length === 2) { + spatialDataType = parsedGeoColumn[0]; + spatialDataColumn = parsedGeoColumn[1]; + } else if (parsedGeoColumn.length === 1) { + spatialDataColumn = parsedGeoColumn[0] || DEFAULT_GEO_COLUMN; + spatialDataType = 'geo'; + } + if (spatialDataType === 'geom') { + // fallback if for some reason someone provided old `geom:$column` + spatialDataType = 'geo'; + } + } else { + spatialDataType = 'geo'; + spatialDataColumn = DEFAULT_GEO_COLUMN; } - : undefined; + } + + // API supports multiple filters, we apply it only to geometry column or spatialDataColumn + spatialFilters = spatialFilter + ? { + [spatialDataColumn]: spatialFilter + } + : undefined; - if (spatialFilters) { queryParams.spatialFilters = JSON.stringify(spatialFilters); + queryParams.spatialDataType = spatialDataType; + if (spatialDataType !== 'geo') { + if (source.spatialFiltersResolution !== undefined) { + queryParams.spatialFiltersResolution = source.spatialFiltersResolution; + } + queryParams.spatialFiltersMode = source.spatialFiltersMode || 'intersects'; + } } const urlWithSearchParams = url + '?' + new URLSearchParams(queryParams).toString(); diff --git a/packages/react-api/src/types.d.ts b/packages/react-api/src/types.d.ts index 415197526..ccb84c4b4 100644 --- a/packages/react-api/src/types.d.ts +++ b/packages/react-api/src/types.d.ts @@ -43,6 +43,10 @@ export type SourceProps = { type: MapTypesType['QUERY'] | MapTypesType['TABLE'] | MapTypesType['TILESET']; connection: string; geoColumn?: string; + dataResolution?: number; + spatialDataType?: string; + spatialDataColumn?: string; + spatialFiltersResolution?: number; aggregationExp?: string; credentials?: Credentials; queryParameters?: QueryParameters; @@ -73,4 +77,3 @@ export type UseCartoLayerFilterProps = { }; export type ExecuteSQLResponse = Promise; - diff --git a/packages/react-redux/src/slices/cartoSlice.js b/packages/react-redux/src/slices/cartoSlice.js index ceaea8f9c..3efc33e90 100644 --- a/packages/react-redux/src/slices/cartoSlice.js +++ b/packages/react-redux/src/slices/cartoSlice.js @@ -204,6 +204,8 @@ export const createCartoSlice = (initialState) => { * @param {FiltersLogicalOperators=} data.filtersLogicalOperator - logical operator that defines how filters for different columns are joined together. * @param {import('@deck.gl/carto').QueryParameters} data.queryParameters - SQL query parameters. * @param {string=} data.geoColumn - (optional) name of column containing geometries or spatial index data. + * @param {number=} data.dataResolution - data resolution for spatial index data. + * @param {number=} data.spatialFiltersResolution - spatial filters resolution for spatial index data. * @param {string=} data.aggregationExp - (optional) for spatial index data. * @param {string=} data.provider - (optional) type of the data warehouse. */ @@ -217,6 +219,10 @@ export const addSource = ({ filtersLogicalOperator = FiltersLogicalOperators.AND, queryParameters = [], geoColumn, + spatialDataType, + spatialDataColumn, + dataResolution, + spatialFiltersResolution, aggregationExp, provider }) => ({ @@ -231,6 +237,10 @@ export const addSource = ({ filtersLogicalOperator, queryParameters, geoColumn, + dataResolution, + spatialDataType, + spatialDataColumn, + spatialFiltersResolution, aggregationExp, provider } diff --git a/packages/react-widgets/src/hooks/useWidgetFetch.js b/packages/react-widgets/src/hooks/useWidgetFetch.js index 92287814a..fb31931e1 100644 --- a/packages/react-widgets/src/hooks/useWidgetFetch.js +++ b/packages/react-widgets/src/hooks/useWidgetFetch.js @@ -57,6 +57,19 @@ export function selectGeometryToIntersect(global, viewport, spatialFilter) { } } +// stolen from deck.gl/modules/carto/src/layers/h3-tileset-2d.ts +const BIAS = 2; +export function getHexagonResolution(viewport, tileSize) { + // 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 / 512); + 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)); +} + export default function useWidgetFetch( modelFn, { @@ -85,6 +98,7 @@ export default function useWidgetFetch( ); const viewport = useSelector(selectViewport); + const viewState = useSelector((state) => state.carto.viewState); const spatialFilter = useSelector((state) => selectValidSpatialFilter(state, dataSource) ); @@ -93,6 +107,37 @@ export default function useWidgetFetch( [global, viewport, spatialFilter] ); + const enrichedSource = useMemo(() => { + if ( + !source || + !geometryToIntersect || + source.spatialDataType === 'geo' || + source.spatialFiltersResolution !== undefined || + !source.dataResolution + ) { + return source; + } + + if (source.spatialDataType === 'h3') { + const hexagonResolution = getHexagonResolution( + { zoom: viewState.zoom, latitude: viewState.latitude }, + source.dataResolution + ); + return { + ...source, + spatialFiltersResolution: Math.min(source.dataResolution, hexagonResolution) + }; + } + if (source.spatialDataType === 'quadbin') { + const quadsResolution = Math.floor(viewState.zoom); + return { + ...source, + spatialFiltersResolution: Math.min(source.dataResolution, quadsResolution) + }; + } + return source; + }, [geometryToIntersect, source, viewState.zoom, viewState.latitude]); + useCustomCompareEffect( () => { let outdated = false; @@ -106,7 +151,7 @@ export default function useWidgetFetch( onStateChange?.({ state: WidgetStateType.Loading }); modelFn({ - source, + source: enrichedSource, ...params, global, remoteCalculation, @@ -141,7 +186,7 @@ export default function useWidgetFetch( }, [ params, - source, + enrichedSource, onError, isSourceReady, global, diff --git a/packages/react-widgets/src/models/utils.js b/packages/react-widgets/src/models/utils.js index 7a34358f3..140a451df 100644 --- a/packages/react-widgets/src/models/utils.js +++ b/packages/react-widgets/src/models/utils.js @@ -1,9 +1,4 @@ -import { - AggregationTypes, - getSpatialIndexFromGeoColumn, - _filtersToSQL, - Provider -} from '@carto/react-core'; +import { AggregationTypes, _filtersToSQL, Provider } from '@carto/react-core'; import { FullyQualifiedName } from './fqn'; import { MAP_TYPES, API_VERSIONS } from '@carto/react-api'; @@ -14,7 +9,6 @@ export function isRemoteCalculationSupported(props) { source && source.type !== MAP_TYPES.TILESET && source.credentials.apiVersion !== API_VERSIONS.V2 && - !(source.geoColumn && getSpatialIndexFromGeoColumn(source.geoColumn)) && source.provider !== 'databricks' ); }