@carto/kepler.gl 0.0.46 → 0.0.47-alpha.custom-markers.2

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.
@@ -56,6 +56,7 @@ import {getSampleData} from 'utils/table-utils/data-container-utils';
56
56
 
57
57
  import {hexToRgb, getColorGroupByName, reverseColorRange} from 'utils/color-utils';
58
58
  import {getMaskExtensionProps} from 'utils/layer-utils';
59
+ import {scaleOrdinal} from 'd3-scale';
59
60
 
60
61
  /** @typedef {import('./index').Layer} LayerClass} */
61
62
 
@@ -65,6 +66,10 @@ import {getMaskExtensionProps} from 'utils/layer-utils';
65
66
  */
66
67
  export const LAYER_ID_LENGTH = 6;
67
68
 
69
+ // Note, this is is SVG of solid circle encoded as data-uri
70
+ const FALLBACK_ICON =
71
+ 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiLz4NCjwvc3ZnPg==';
72
+
68
73
  const MAX_SAMPLE_SIZE = 5000;
69
74
  const defaultDomain = [0, 1];
70
75
  const dataFilterExtension = new DataFilterExtension({filterSize: MAX_GPU_FILTERS});
@@ -808,6 +813,30 @@ class Layer {
808
813
  return this.getVisChannelScale(colorScale, colorDomain, colorRange.colors.map(hexToRgb));
809
814
  }
810
815
 
816
+ getCustomMarkersScale(range) {
817
+ let unknownValue = FALLBACK_ICON;
818
+ if (!range) {
819
+ () => unknownValue;
820
+ }
821
+
822
+ const mapping = new Map();
823
+
824
+ for (const {value, markerUrl} of range) {
825
+ if (value === null) {
826
+ unknownValue = markerUrl;
827
+ } else if (markerUrl) {
828
+ mapping.set(value, markerUrl);
829
+ }
830
+ }
831
+
832
+ // @ts-ignore d3 scale
833
+ const scale = scaleOrdinal()
834
+ .domain(Array.from(mapping.keys()))
835
+ .range(Array.from(mapping.values()))
836
+ .unknown(unknownValue);
837
+ return scale;
838
+ }
839
+
811
840
  /**
812
841
  * Mapping from visual channels to deck.gl accesors
813
842
  * @param {Object} param Parameters
@@ -833,7 +862,7 @@ class Layer {
833
862
  aggregation
834
863
  } = this.visualChannels[channel];
835
864
 
836
- const shouldGetScale = this.config[field];
865
+ const shouldGetScale = Boolean(scale && this.config[field]);
837
866
 
838
867
  if (shouldGetScale) {
839
868
  const args = [this.config[scale], this.config[domain], this.config.visConfig[range]];
@@ -846,6 +875,8 @@ class Layer {
846
875
  const scaleFunction =
847
876
  channelScaleType === CHANNEL_SCALES.color
848
877
  ? this.getColorScale(...args)
878
+ : channelScaleType === CHANNEL_SCALES.customMarker
879
+ ? this.getCustomMarkersScale(this.config.visConfig[range])
849
880
  : this.getVisChannelScale(...args, isFixed);
850
881
 
851
882
  attributeAccessors[accessor] = d =>
@@ -1191,6 +1222,12 @@ class Layer {
1191
1222
  return fixed ? 1 : (this.config[field] ? 1 : radius);
1192
1223
  }
1193
1224
 
1225
+ getMaxMarkerSize() {
1226
+ const {visConfig} = this.config;
1227
+ const {sizeField, radiusRange, radius} = visConfig;
1228
+ return Math.ceil((sizeField ? radiusRange[1] : radius));
1229
+ }
1230
+
1194
1231
  getRadiusUnit(fixedRadius) {
1195
1232
  const radiusChannel = Object.values(this.visualChannels).find(vc => vc.property === 'radius');
1196
1233
 
@@ -1300,6 +1337,30 @@ class Layer {
1300
1337
  }, []);
1301
1338
  }
1302
1339
 
1340
+ /**
1341
+ * Create deck.gl `IconLayer` "autoPack" mode accessor for custom markers.
1342
+ *
1343
+ * @param {*} customMarkersStatus Record<url, UrlStatus> provided by builder, UrlStatus `ok` means URL is ok and ready to render
1344
+ * @param {*} iconUrlAccessor deck.gl accessor (or value) that resolves to URL of icon for given row
1345
+ * @returns deck.gl accessor that returns UnpackedIcon ready to consume by IconLayer
1346
+ */
1347
+ getCustomMarkerIconAccessor(customMarkersStatus, iconUrlAccessor) {
1348
+ const maxIconSize = this.getMaxMarkerSize();
1349
+
1350
+ return d => {
1351
+ const wantedUrl =
1352
+ (typeof iconUrlAccessor === 'function') ? iconUrlAccessor(d) : iconUrlAccessor;
1353
+ const url = customMarkersStatus?.[wantedUrl] === 'ok' ? wantedUrl : FALLBACK_ICON;
1354
+ return {
1355
+ id: `${url}@@${maxIconSize}`,
1356
+ url,
1357
+ width: maxIconSize,
1358
+ height: maxIconSize,
1359
+ mask: true
1360
+ };
1361
+ };
1362
+ }
1363
+
1303
1364
  calculateDataAttribute(keplerTable, getPosition) {
1304
1365
  // implemented in subclasses
1305
1366
  return [];
@@ -26,6 +26,7 @@ import {DEFAULT_COLOR_RANGE} from 'constants/color-ranges';
26
26
  export const PROPERTY_GROUPS = keyMirror({
27
27
  color: null,
28
28
  stroke: null,
29
+ customMarkers: null,
29
30
  radius: null,
30
31
  height: null,
31
32
  angle: null,
@@ -429,6 +430,27 @@ export const LAYER_VIS_CONFIGS = {
429
430
  step: 0.1,
430
431
  group: PROPERTY_GROUPS.cell,
431
432
  property: 'radius'
433
+ },
434
+ customMarkers: {
435
+ type: 'boolean',
436
+ defaultValue: false,
437
+ label: 'layerVisConfigs.customMarkers',
438
+ group: PROPERTY_GROUPS.customMarkers,
439
+ property: 'customMarkers'
440
+ },
441
+ customMarkersUrl: {
442
+ type: 'text',
443
+ defaultValue: '',
444
+ label: 'layerVisConfigs.addUrl',
445
+ group: PROPERTY_GROUPS.customMarkers,
446
+ property: 'customMarkersUrl'
447
+ },
448
+ customMarkersRange: {
449
+ type: 'markers-range-select',
450
+ defaultValue: undefined,
451
+ label: 'layerVisConfigs.colorRange',
452
+ group: PROPERTY_GROUPS.customMarkers,
453
+ property: 'customMarkersRange'
432
454
  }
433
455
  };
434
456
 
@@ -20,6 +20,7 @@
20
20
 
21
21
  import {BrushingExtension} from '@deck.gl/extensions';
22
22
  import {ScatterplotLayer} from '@deck.gl/layers';
23
+ import {IconLayer} from '@deck.gl/layers';
23
24
 
24
25
  import Layer from '../base-layer';
25
26
  import {hexToRgb} from 'utils/color-utils';
@@ -43,6 +44,9 @@ const brushingExtension = new BrushingExtension();
43
44
  export const pointVisConfigs = {
44
45
  radius: 'radius',
45
46
  fixedRadius: 'fixedRadius',
47
+ customMarkers: 'customMarkers',
48
+ customMarkersUrl: 'customMarkersUrl',
49
+ customMarkersRange: 'customMarkersRange',
46
50
  opacity: 'opacity',
47
51
  outline: 'outline',
48
52
  thickness: 'thickness',
@@ -122,6 +126,18 @@ export default class PointLayer extends Layer {
122
126
  channelScaleType: 'radius',
123
127
  accessor: 'getRadius',
124
128
  defaultValue: 1
129
+ },
130
+ customMarkers: {
131
+ property: 'customMarkers',
132
+ key: 'customMarkers',
133
+ field: 'customMarkersField',
134
+ scale: 'customMarkersScale',
135
+ domain: 'customMarkersDomain',
136
+ range: 'customMarkersRange',
137
+ channelScaleType: CHANNEL_SCALES.customMarker,
138
+ accessor: 'getIconUrl',
139
+ supportedFieldTypes: ['string'],
140
+ getAttributeValue: config => config.visConfig.customMarkersUrl
125
141
  }
126
142
  };
127
143
  }
@@ -181,7 +197,10 @@ export default class PointLayer extends Layer {
181
197
  // add stroke color visual channel
182
198
  strokeColorField: null,
183
199
  strokeColorDomain: [0, 1],
184
- strokeColorScale: 'quantile'
200
+ strokeColorScale: 'quantile',
201
+
202
+ customMarkersField: null,
203
+ customMarkersScale: 'ordinal'
185
204
  };
186
205
  }
187
206
 
@@ -237,22 +256,21 @@ export default class PointLayer extends Layer {
237
256
  this.updateMeta({bounds});
238
257
  }
239
258
 
259
+ isLayerHovered(objectInfo) {
260
+ return (
261
+ (objectInfo?.picked && objectInfo?.layer?.props?.id === this.id) ||
262
+ objectInfo?.layer?.props?.id === `${this.id}-icon`
263
+ );
264
+ }
265
+
240
266
  renderLayer(opts) {
241
- const {data, gpuFilter, objectHovered, mapState, interactionConfig} = opts;
267
+ const {data, gpuFilter, objectHovered, mapState, interactionConfig, customMarkersStatus} = opts;
242
268
 
243
269
  // if no field size is defined we need to pass fixed radius = false
244
270
  const fixedRadius = this.config.visConfig.fixedRadius && Boolean(this.config.sizeField);
245
271
  const radiusScale = this.getRadiusScale(fixedRadius);
246
272
  const radiusUnits = this.getRadiusUnit(fixedRadius);
247
273
 
248
- const layerProps = {
249
- stroked: this.config.visConfig.outline,
250
- filled: this.config.visConfig.filled,
251
- lineWidthScale: this.config.visConfig.thickness,
252
- radiusScale,
253
- ...(this.config.visConfig.fixedRadius ? {} : {radiusMaxPixels: 500})
254
- };
255
-
256
274
  const updateTriggers = {
257
275
  getPosition: this.config.columns,
258
276
  getFilterValue: gpuFilter.filterValueUpdateTriggers,
@@ -278,16 +296,101 @@ export default class PointLayer extends Layer {
278
296
  };
279
297
  const hoveredObject = this.hasHoveredObject(objectHovered);
280
298
 
299
+ const textLabelLayer = this.renderTextLabelLayer(
300
+ {
301
+ getPosition: data.getPosition,
302
+ sharedProps,
303
+ getPixelOffset,
304
+ updateTriggers
305
+ },
306
+ opts
307
+ );
308
+ const commonLayerProps = {
309
+ ...defaultLayerProps,
310
+ parameters: {
311
+ // circles will be flat on the map when the altitude column is not used
312
+ depthTest: this.config.columns.altitude?.fieldIdx > -1
313
+ }
314
+ };
315
+
316
+ const customMarkersField = this.config.customMarkersField;
317
+ const {customMarkers, customMarkersUrl, customMarkersRange} = this.config.visConfig;
318
+ const customMarkersConfigured =
319
+ customMarkers && Boolean(customMarkersUrl || (customMarkersField && customMarkersRange));
320
+ const customMarkersMisConfigured = customMarkers && !customMarkersConfigured;
321
+
322
+ if (customMarkersConfigured) {
323
+ const maxIconSize = this.getMaxMarkerSize();
324
+ const getIcon = this.getCustomMarkerIconAccessor(customMarkersStatus, data.getIconUrl);
325
+ const iconLayerProps = {
326
+ getIcon,
327
+ getPosition: data.getPosition,
328
+ getSize: data.getRadius,
329
+ sizeScale: radiusScale,
330
+ sizeUnits: 'pixels',
331
+ loadOptions: {
332
+ image: {
333
+ type: 'imagebitmap'
334
+ },
335
+ imagebitmap: {
336
+ resizeWidth: maxIconSize,
337
+ resizeHeight: maxIconSize,
338
+ resizeQuality: 'high'
339
+ }
340
+ }
341
+ };
342
+
343
+ return [
344
+ new IconLayer({
345
+ ...commonLayerProps,
346
+ ...iconLayerProps,
347
+ id: `${defaultLayerProps.id}-icon`,
348
+ data: data.data,
349
+ getColor: data.getFillColor,
350
+ getFilterValue: data.getFilterValue,
351
+ updateTriggers: {
352
+ getFilterValue: updateTriggers.getFilterValue,
353
+ getColor: updateTriggers.getFillColor,
354
+ getIcon: [
355
+ customMarkersUrl,
356
+ maxIconSize,
357
+ customMarkersStatus,
358
+ customMarkersField,
359
+ customMarkersRange
360
+ ],
361
+ getSize: updateTriggers.getRadius
362
+ }
363
+ }),
364
+ ...(hoveredObject
365
+ ? [
366
+ new IconLayer({
367
+ ...iconLayerProps,
368
+ ...this.getDefaultHoverLayerProps(),
369
+ id: `${defaultLayerProps.id}-icon-hovered`,
370
+ data: [hoveredObject],
371
+ getColor: this.config.highlightColor
372
+ })
373
+ ]
374
+ : []),
375
+ ...textLabelLayer
376
+ ];
377
+ }
378
+
379
+ const scatterPlotLayerProps = {
380
+ stroked: this.config.visConfig.outline,
381
+ filled: this.config.visConfig.filled,
382
+ lineWidthScale: this.config.visConfig.thickness,
383
+ // TODO: invent better way to render something when customMarkers are not yet configured
384
+ radiusScale: customMarkersMisConfigured ? radiusScale / 2 : radiusScale,
385
+ ...(this.config.visConfig.fixedRadius ? {} : {radiusMaxPixels: 500})
386
+ };
387
+
281
388
  return [
282
389
  new ScatterplotLayer({
283
- ...defaultLayerProps,
284
390
  ...brushingProps,
285
- ...layerProps,
286
391
  ...data,
287
- parameters: {
288
- // circles will be flat on the map when the altitude column is not used
289
- depthTest: this.config.columns.altitude?.fieldIdx > -1
290
- },
392
+ ...commonLayerProps,
393
+ ...scatterPlotLayerProps,
291
394
  radiusUnits,
292
395
  lineWidthUnits: 'pixels',
293
396
  updateTriggers,
@@ -298,7 +401,7 @@ export default class PointLayer extends Layer {
298
401
  ? [
299
402
  new ScatterplotLayer({
300
403
  ...this.getDefaultHoverLayerProps(),
301
- ...layerProps,
404
+ ...scatterPlotLayerProps,
302
405
  data: [hoveredObject],
303
406
  getLineColor: this.config.highlightColor,
304
407
  getFillColor: this.config.highlightColor,
@@ -308,15 +411,7 @@ export default class PointLayer extends Layer {
308
411
  ]
309
412
  : []),
310
413
  // text label layer
311
- ...this.renderTextLabelLayer(
312
- {
313
- getPosition: data.getPosition,
314
- sharedProps,
315
- getPixelOffset,
316
- updateTriggers
317
- },
318
- opts
319
- )
414
+ ...textLabelLayer
320
415
  ];
321
416
  }
322
417
  }
@@ -34,7 +34,8 @@ export default {
34
34
  density: 'density',
35
35
  height: 'height',
36
36
  sum: 'sum',
37
- pointCount: 'Point Count'
37
+ pointCount: 'Point Count',
38
+ customMarkers: 'Markers'
38
39
  },
39
40
  placeholder: {
40
41
  search: 'Search',
@@ -85,6 +86,7 @@ export default {
85
86
  layer: {
86
87
  required: 'Required*',
87
88
  radius: 'Radius',
89
+ size: 'Size',
88
90
  color: 'Color',
89
91
  fillColor: 'Fill Color',
90
92
  outline: 'Outline',
@@ -101,6 +103,7 @@ export default {
101
103
  elevationByDescription: 'When off, height is based on count of points',
102
104
  colorByDescription: 'When off, color is based on count of points',
103
105
  aggregateBy: 'Aggregate {field} by',
106
+ addUrl: 'Add URL',
104
107
  '3DModel': '3D Model',
105
108
  '3DModelOptions': '3D Model Options',
106
109
  type: {
@@ -161,7 +164,9 @@ export default {
161
164
  weightIntensity: 'Weight Intensity',
162
165
  zoomScale: 'Zoom Scale',
163
166
  heightRange: 'Height Range',
164
- heightMultiplier: 'Height Multiplier'
167
+ heightMultiplier: 'Height Multiplier',
168
+ customMarkers: 'Custom markers',
169
+ url: 'Add URL'
165
170
  },
166
171
  layerVisibility: {
167
172
  title: 'Visibility by zoom level'
@@ -126,7 +126,8 @@ export function renderDeckGlLayer(props, layerCallbacks, idx) {
126
126
  interactionConfig,
127
127
  animationConfig,
128
128
  mapLayers,
129
- maskPolygon
129
+ maskPolygon,
130
+ customMarkersStatus
130
131
  } = props;
131
132
  const layer = layers[idx];
132
133
  const data = layerData[idx];
@@ -144,7 +145,8 @@ export function renderDeckGlLayer(props, layerCallbacks, idx) {
144
145
  animationConfig,
145
146
  objectHovered,
146
147
  visible,
147
- maskPolygon
148
+ maskPolygon,
149
+ customMarkersStatus
148
150
  });
149
151
  }
150
152