@carto/api-client 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  ## 0.4
6
6
 
7
+ ### 0.4.4
8
+
9
+ - feat: Add support for spatial index types (H3, quadbin) in Widget APIs
10
+
7
11
  ### 0.4.3
8
12
 
9
13
  - feat: Add support for`aggregationExp` parameter to `vectorQuerySource` and `vectorTableSource`
@@ -1,3 +1,3 @@
1
1
  import type { SourceOptions, QuerySourceOptions, QueryResult } from '../sources/types';
2
- export type QueryOptions = SourceOptions & Omit<QuerySourceOptions, 'spatialDataColumn'>;
2
+ export type QueryOptions = SourceOptions & QuerySourceOptions;
3
3
  export declare const query: (options: QueryOptions) => Promise<QueryResult>;
@@ -893,41 +893,43 @@ function executeModel(props) {
893
893
  data,
894
894
  filters,
895
895
  filtersLogicalOperator = 'and',
896
- geoColumn = DEFAULT_GEO_COLUMN
896
+ spatialDataType = 'geo',
897
+ spatialFiltersMode = 'intersects',
898
+ spatialFiltersResolution = 0
897
899
  } = source;
898
- const queryParameters = source.queryParameters ? JSON.stringify(source.queryParameters) : '';
899
900
  const queryParams = {
900
901
  type,
901
902
  client: clientId,
902
903
  source: data,
903
- params: JSON.stringify(params),
904
- queryParameters,
905
- filters: JSON.stringify(filters),
904
+ params,
905
+ queryParameters: source.queryParameters || '',
906
+ filters,
906
907
  filtersLogicalOperator
907
908
  };
909
+ const spatialDataColumn = source.spatialDataColumn || DEFAULT_GEO_COLUMN;
908
910
  // Picking Model API requires 'spatialDataColumn'.
909
911
  if (model === 'pick') {
910
- queryParams.spatialDataColumn = geoColumn;
912
+ queryParams.spatialDataColumn = spatialDataColumn;
911
913
  }
912
- // API supports multiple filters, we apply it only to geoColumn
914
+ // API supports multiple filters, we apply it only to spatialDataColumn
913
915
  const spatialFilters = source.spatialFilter ? {
914
- [geoColumn]: source.spatialFilter
916
+ [spatialDataColumn]: source.spatialFilter
915
917
  } : undefined;
916
918
  if (spatialFilters) {
917
- queryParams.spatialFilters = JSON.stringify(spatialFilters);
919
+ queryParams.spatialFilters = spatialFilters;
920
+ queryParams.spatialDataColumn = spatialDataColumn;
921
+ queryParams.spatialDataType = spatialDataType;
922
+ }
923
+ if (spatialDataType !== 'geo') {
924
+ if (spatialFiltersResolution > 0) {
925
+ queryParams.spatialFiltersResolution = spatialFiltersResolution;
926
+ }
927
+ queryParams.spatialFiltersMode = spatialFiltersMode;
918
928
  }
919
- const urlWithSearchParams = url + '?' + new URLSearchParams(queryParams).toString();
929
+ const urlWithSearchParams = url + '?' + objectToURLSearchParams(queryParams).toString();
920
930
  const isGet = urlWithSearchParams.length <= REQUEST_GET_MAX_URL_LENGTH;
921
931
  if (isGet) {
922
932
  url = urlWithSearchParams;
923
- } else {
924
- // undo the JSON.stringify, @TODO find a better pattern
925
- queryParams.params = params;
926
- queryParams.filters = filters;
927
- queryParams.queryParameters = source.queryParameters;
928
- if (spatialFilters) {
929
- queryParams.spatialFilters = spatialFilters;
930
- }
931
933
  }
932
934
  return makeCall({
933
935
  url,
@@ -941,6 +943,63 @@ function executeModel(props) {
941
943
  }
942
944
  });
943
945
  }
946
+ function objectToURLSearchParams(object) {
947
+ const params = new URLSearchParams();
948
+ for (const key in object) {
949
+ if (isPureObject(object[key])) {
950
+ params.append(key, JSON.stringify(object[key]));
951
+ } else if (Array.isArray(object[key])) {
952
+ params.append(key, JSON.stringify(object[key]));
953
+ } else if (object[key] === null) {
954
+ params.append(key, 'null');
955
+ } else if (object[key] !== undefined) {
956
+ params.append(key, String(object[key]));
957
+ }
958
+ }
959
+ return params;
960
+ }
961
+
962
+ const DEFAULT_TILE_SIZE = 512;
963
+ const QUADBIN_ZOOM_MAX_OFFSET = 4;
964
+ function getSpatialFiltersResolution(source, viewState) {
965
+ const dataResolution = source.dataResolution ?? Number.MAX_VALUE;
966
+ const aggregationResLevel = source.aggregationResLevel ?? (source.spatialDataType === 'h3' ? DEFAULT_AGGREGATION_RES_LEVEL_H3 : DEFAULT_AGGREGATION_RES_LEVEL_QUADBIN);
967
+ const aggregationResLevelOffset = Math.max(0, Math.floor(aggregationResLevel));
968
+ const currentZoomInt = Math.ceil(viewState.zoom);
969
+ if (source.spatialDataType === 'h3') {
970
+ const tileSize = DEFAULT_TILE_SIZE;
971
+ const maxResolutionForZoom = maxH3SpatialFiltersResolutions.find(_ref => {
972
+ let [zoom] = _ref;
973
+ return zoom === currentZoomInt;
974
+ })?.[1] ?? Math.max(0, currentZoomInt - 3);
975
+ const maxSpatialFiltersResolution = maxResolutionForZoom ? Math.min(dataResolution, maxResolutionForZoom) : dataResolution;
976
+ const hexagonResolution = getHexagonResolution(viewState, tileSize) + aggregationResLevelOffset;
977
+ return Math.min(hexagonResolution, maxSpatialFiltersResolution);
978
+ }
979
+ if (source.spatialDataType === 'quadbin') {
980
+ const maxResolutionForZoom = currentZoomInt + QUADBIN_ZOOM_MAX_OFFSET;
981
+ const maxSpatialFiltersResolution = Math.min(dataResolution, maxResolutionForZoom);
982
+ const quadsResolution = Math.floor(viewState.zoom) + aggregationResLevelOffset;
983
+ return Math.min(quadsResolution, maxSpatialFiltersResolution);
984
+ }
985
+ return undefined;
986
+ }
987
+ 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]];
988
+ // stolen from https://github.com/visgl/deck.gl/blob/master/modules/carto/src/layers/h3-tileset-2d.ts
989
+ // Relative scale factor (0 = no biasing, 2 = a few hexagons cover view)
990
+ const BIAS = 2;
991
+ // Resolution conversion function. Takes a WebMercatorViewport and returns
992
+ // a H3 resolution such that the screen space size of the hexagons is
993
+ // similar
994
+ function getHexagonResolution(viewport, tileSize) {
995
+ // Difference in given tile size compared to deck's internal 512px tile size,
996
+ // expressed as an offset to the viewport zoom.
997
+ const zoomOffset = Math.log2(tileSize / DEFAULT_TILE_SIZE);
998
+ const hexagonScaleFactor = 2 / 3 * (viewport.zoom - zoomOffset);
999
+ const latitudeScaleFactor = Math.log(1 / Math.cos(Math.PI * viewport.latitude / 180));
1000
+ // Clip and bias
1001
+ return Math.max(0, Math.floor(hexagonScaleFactor + latitudeScaleFactor - BIAS));
1002
+ }
944
1003
 
945
1004
  /**
946
1005
  * Source for Widget API requests on a data source defined by a SQL query.
@@ -965,9 +1024,21 @@ class WidgetBaseSource {
965
1024
  connectionName: props.connectionName,
966
1025
  filters: getApplicableFilters(owner, props.filters),
967
1026
  filtersLogicalOperator: props.filtersLogicalOperator,
968
- geoColumn: props.geoColumn
1027
+ spatialDataType: props.spatialDataType,
1028
+ spatialDataColumn: props.spatialDataColumn,
1029
+ dataResolution: props.dataResolution
969
1030
  };
970
1031
  }
1032
+ _getSpatialFiltersResolution(source, spatialFilter, referenceViewState) {
1033
+ // spatialFiltersResolution applies only to spatial index sources.
1034
+ if (!spatialFilter || source.spatialDataType === 'geo') {
1035
+ return;
1036
+ }
1037
+ if (!referenceViewState) {
1038
+ throw new Error('Missing required option, "spatialIndexReferenceViewState".');
1039
+ }
1040
+ return getSpatialFiltersResolution(source, referenceViewState);
1041
+ }
971
1042
  /****************************************************************************
972
1043
  * CATEGORIES
973
1044
  */
@@ -981,6 +1052,8 @@ class WidgetBaseSource {
981
1052
  const {
982
1053
  filterOwner,
983
1054
  spatialFilter,
1055
+ spatialFiltersMode,
1056
+ spatialIndexReferenceViewState,
984
1057
  abortController,
985
1058
  ...params
986
1059
  } = options;
@@ -989,10 +1062,14 @@ class WidgetBaseSource {
989
1062
  operation,
990
1063
  operationColumn
991
1064
  } = params;
1065
+ const source = _this.getModelSource(filterOwner);
1066
+ const spatialFiltersResolution = _this._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
992
1067
  return Promise.resolve(executeModel({
993
1068
  model: 'category',
994
1069
  source: {
995
- ..._this.getModelSource(filterOwner),
1070
+ ...source,
1071
+ spatialFiltersResolution,
1072
+ spatialFiltersMode,
996
1073
  spatialFilter
997
1074
  },
998
1075
  params: {
@@ -1025,6 +1102,8 @@ class WidgetBaseSource {
1025
1102
  const {
1026
1103
  filterOwner,
1027
1104
  spatialFilter,
1105
+ spatialFiltersMode,
1106
+ spatialIndexReferenceViewState,
1028
1107
  abortController,
1029
1108
  ...params
1030
1109
  } = options;
@@ -1036,10 +1115,14 @@ class WidgetBaseSource {
1036
1115
  limit,
1037
1116
  tileResolution
1038
1117
  } = params;
1118
+ const source = _this2.getModelSource(filterOwner);
1119
+ const spatialFiltersResolution = _this2._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1039
1120
  return Promise.resolve(executeModel({
1040
1121
  model: 'pick',
1041
1122
  source: {
1042
- ..._this2.getModelSource(filterOwner),
1123
+ ...source,
1124
+ spatialFiltersResolution,
1125
+ spatialFiltersMode,
1043
1126
  spatialFilter
1044
1127
  },
1045
1128
  params: {
@@ -1079,6 +1162,8 @@ class WidgetBaseSource {
1079
1162
  const {
1080
1163
  filterOwner,
1081
1164
  spatialFilter,
1165
+ spatialFiltersMode,
1166
+ spatialIndexReferenceViewState,
1082
1167
  abortController,
1083
1168
  operationExp,
1084
1169
  ...params
@@ -1087,10 +1172,14 @@ class WidgetBaseSource {
1087
1172
  column,
1088
1173
  operation
1089
1174
  } = params;
1175
+ const source = _this3.getModelSource(filterOwner);
1176
+ const spatialFiltersResolution = _this3._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1090
1177
  return Promise.resolve(executeModel({
1091
1178
  model: 'formula',
1092
1179
  source: {
1093
- ..._this3.getModelSource(filterOwner),
1180
+ ...source,
1181
+ spatialFiltersResolution,
1182
+ spatialFiltersMode,
1094
1183
  spatialFilter
1095
1184
  },
1096
1185
  params: {
@@ -1119,6 +1208,8 @@ class WidgetBaseSource {
1119
1208
  const {
1120
1209
  filterOwner,
1121
1210
  spatialFilter,
1211
+ spatialFiltersMode,
1212
+ spatialIndexReferenceViewState,
1122
1213
  abortController,
1123
1214
  ...params
1124
1215
  } = options;
@@ -1127,10 +1218,14 @@ class WidgetBaseSource {
1127
1218
  operation,
1128
1219
  ticks
1129
1220
  } = params;
1221
+ const source = _this4.getModelSource(filterOwner);
1222
+ const spatialFiltersResolution = _this4._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1130
1223
  return Promise.resolve(executeModel({
1131
1224
  model: 'histogram',
1132
1225
  source: {
1133
- ..._this4.getModelSource(filterOwner),
1226
+ ...source,
1227
+ spatialFiltersResolution,
1228
+ spatialFiltersMode,
1134
1229
  spatialFilter
1135
1230
  },
1136
1231
  params: {
@@ -1175,16 +1270,22 @@ class WidgetBaseSource {
1175
1270
  const {
1176
1271
  filterOwner,
1177
1272
  spatialFilter,
1273
+ spatialFiltersMode,
1274
+ spatialIndexReferenceViewState,
1178
1275
  abortController,
1179
1276
  ...params
1180
1277
  } = options;
1181
1278
  const {
1182
1279
  column
1183
1280
  } = params;
1281
+ const source = _this5.getModelSource(filterOwner);
1282
+ const spatialFiltersResolution = _this5._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1184
1283
  return Promise.resolve(executeModel({
1185
1284
  model: 'range',
1186
1285
  source: {
1187
- ..._this5.getModelSource(filterOwner),
1286
+ ...source,
1287
+ spatialFiltersResolution,
1288
+ spatialFiltersMode,
1188
1289
  spatialFilter
1189
1290
  },
1190
1291
  params: {
@@ -1211,6 +1312,8 @@ class WidgetBaseSource {
1211
1312
  const {
1212
1313
  filterOwner,
1213
1314
  spatialFilter,
1315
+ spatialFiltersMode,
1316
+ spatialIndexReferenceViewState,
1214
1317
  abortController,
1215
1318
  ...params
1216
1319
  } = options;
@@ -1220,12 +1323,16 @@ class WidgetBaseSource {
1220
1323
  yAxisColumn,
1221
1324
  yAxisJoinOperation
1222
1325
  } = params;
1326
+ const source = _this6.getModelSource(filterOwner);
1327
+ const spatialFiltersResolution = _this6._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1223
1328
  // Make sure this is sync with the same constant in cloud-native/maps-api
1224
1329
  const HARD_LIMIT = 500;
1225
1330
  return Promise.resolve(executeModel({
1226
1331
  model: 'scatterplot',
1227
1332
  source: {
1228
- ..._this6.getModelSource(filterOwner),
1333
+ ...source,
1334
+ spatialFiltersResolution,
1335
+ spatialFiltersMode,
1229
1336
  spatialFilter
1230
1337
  },
1231
1338
  params: {
@@ -1262,6 +1369,8 @@ class WidgetBaseSource {
1262
1369
  const {
1263
1370
  filterOwner,
1264
1371
  spatialFilter,
1372
+ spatialFiltersMode,
1373
+ spatialIndexReferenceViewState,
1265
1374
  abortController,
1266
1375
  ...params
1267
1376
  } = options;
@@ -1272,10 +1381,14 @@ class WidgetBaseSource {
1272
1381
  offset = 0,
1273
1382
  limit = 10
1274
1383
  } = params;
1384
+ const source = _this7.getModelSource(filterOwner);
1385
+ const spatialFiltersResolution = _this7._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1275
1386
  return Promise.resolve(executeModel({
1276
1387
  model: 'table',
1277
1388
  source: {
1278
- ..._this7.getModelSource(filterOwner),
1389
+ ...source,
1390
+ spatialFiltersResolution,
1391
+ spatialFiltersMode,
1279
1392
  spatialFilter
1280
1393
  },
1281
1394
  params: {
@@ -1311,6 +1424,8 @@ class WidgetBaseSource {
1311
1424
  filterOwner,
1312
1425
  abortController,
1313
1426
  spatialFilter,
1427
+ spatialFiltersMode,
1428
+ spatialIndexReferenceViewState,
1314
1429
  ...params
1315
1430
  } = options;
1316
1431
  const {
@@ -1324,10 +1439,14 @@ class WidgetBaseSource {
1324
1439
  splitByCategoryLimit,
1325
1440
  splitByCategoryValues
1326
1441
  } = params;
1442
+ const source = _this8.getModelSource(filterOwner);
1443
+ const spatialFiltersResolution = _this8._getSpatialFiltersResolution(source, spatialFilter, spatialIndexReferenceViewState);
1327
1444
  return Promise.resolve(executeModel({
1328
1445
  model: 'timeseries',
1329
1446
  source: {
1330
- ..._this8.getModelSource(filterOwner),
1447
+ ...source,
1448
+ spatialFiltersResolution,
1449
+ spatialFiltersMode,
1331
1450
  spatialFilter
1332
1451
  },
1333
1452
  params: {
@@ -1358,8 +1477,7 @@ WidgetBaseSource.defaultProps = {
1358
1477
  apiBaseUrl: DEFAULT_API_BASE_URL,
1359
1478
  clientId: getClient(),
1360
1479
  filters: {},
1361
- filtersLogicalOperator: 'and',
1362
- geoColumn: DEFAULT_GEO_COLUMN
1480
+ filtersLogicalOperator: 'and'
1363
1481
  };
1364
1482
 
1365
1483
  /**
@@ -1455,7 +1573,12 @@ const h3QuerySource = function (options) {
1455
1573
  }
1456
1574
  return Promise.resolve(baseSource('query', options, urlParameters).then(result => ({
1457
1575
  ...result,
1458
- widgetSource: new WidgetQuerySource(options)
1576
+ widgetSource: new WidgetQuerySource({
1577
+ ...options,
1578
+ // NOTE: passing redundant spatialDataColumn here to apply the default value 'h3'
1579
+ spatialDataColumn,
1580
+ spatialDataType: 'h3'
1581
+ })
1459
1582
  })));
1460
1583
  } catch (e) {
1461
1584
  return Promise.reject(e);
@@ -1486,7 +1609,12 @@ const h3TableSource = function (options) {
1486
1609
  }
1487
1610
  return Promise.resolve(baseSource('table', options, urlParameters).then(result => ({
1488
1611
  ...result,
1489
- widgetSource: new WidgetTableSource(options)
1612
+ widgetSource: new WidgetTableSource({
1613
+ ...options,
1614
+ // NOTE: passing redundant spatialDataColumn here to apply the default value 'h3'
1615
+ spatialDataColumn,
1616
+ spatialDataType: 'h3'
1617
+ })
1490
1618
  })));
1491
1619
  } catch (e) {
1492
1620
  return Promise.reject(e);
@@ -1555,7 +1683,12 @@ const quadbinQuerySource = function (options) {
1555
1683
  }
1556
1684
  return Promise.resolve(baseSource('query', options, urlParameters).then(result => ({
1557
1685
  ...result,
1558
- widgetSource: new WidgetQuerySource(options)
1686
+ widgetSource: new WidgetQuerySource({
1687
+ ...options,
1688
+ // NOTE: passing redundant spatialDataColumn here to apply the default value 'quadbin'
1689
+ spatialDataColumn,
1690
+ spatialDataType: 'quadbin'
1691
+ })
1559
1692
  })));
1560
1693
  } catch (e) {
1561
1694
  return Promise.reject(e);
@@ -1586,7 +1719,12 @@ const quadbinTableSource = function (options) {
1586
1719
  }
1587
1720
  return Promise.resolve(baseSource('table', options, urlParameters).then(result => ({
1588
1721
  ...result,
1589
- widgetSource: new WidgetTableSource(options)
1722
+ widgetSource: new WidgetTableSource({
1723
+ ...options,
1724
+ // NOTE: passing redundant spatialDataColumn here to apply the default value 'quadbin'
1725
+ spatialDataColumn,
1726
+ spatialDataType: 'quadbin'
1727
+ })
1590
1728
  })));
1591
1729
  } catch (e) {
1592
1730
  return Promise.reject(e);
@@ -1640,7 +1778,10 @@ const vectorQuerySource = function (options) {
1640
1778
  }
1641
1779
  return Promise.resolve(baseSource('query', options, urlParameters).then(result => ({
1642
1780
  ...result,
1643
- widgetSource: new WidgetQuerySource(options)
1781
+ widgetSource: new WidgetQuerySource({
1782
+ ...options,
1783
+ spatialDataType: 'geo'
1784
+ })
1644
1785
  })));
1645
1786
  } catch (e) {
1646
1787
  return Promise.reject(e);
@@ -1675,7 +1816,10 @@ const vectorTableSource = function (options) {
1675
1816
  }
1676
1817
  return Promise.resolve(baseSource('table', options, urlParameters).then(result => ({
1677
1818
  ...result,
1678
- widgetSource: new WidgetTableSource(options)
1819
+ widgetSource: new WidgetTableSource({
1820
+ ...options,
1821
+ spatialDataType: 'geo'
1822
+ })
1679
1823
  })));
1680
1824
  } catch (e) {
1681
1825
  return Promise.reject(e);