• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

terrestris / react-geo / 18274503963

06 Oct 2025 08:18AM UTC coverage: 63.354%. Remained the same
18274503963

push

github

web-flow
Merge pull request #4396 from terrestris/dependabot/npm_and_yarn/commitlint/cli-20.1.0

build(deps-dev): bump @commitlint/cli from 19.8.1 to 20.1.0

597 of 1040 branches covered (57.4%)

Branch coverage included in aggregate %.

1137 of 1697 relevant lines covered (67.0%)

11.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.54
/src/Grid/AgFeatureGrid/AgFeatureGrid.tsx
1
import React, { Key, ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
2

3
import {
4
  AllCommunityModule,
5
  CellMouseOutEvent,
6
  CellMouseOverEvent,
7
  ColDef,
8
  ColDefField,
9
  ColGroupDef,
10
  GridApi,
11
  GridReadyEvent,
12
  ModuleRegistry,
13
  RowClassParams,
14
  RowClickedEvent,
15
  RowNode,
16
  RowStyle,
17
  SelectionChangedEvent,
18
  Theme,
19
  themeBalham} from 'ag-grid-community';
20
import {
21
  AgGridReact,
22
  AgGridReactProps
23
} from 'ag-grid-react';
24
import _differenceWith from 'lodash/differenceWith';
25
import _isFunction from 'lodash/isFunction';
26
import _isNil from 'lodash/isNil';
27
import _isNumber from 'lodash/isNumber';
28
import _isString from 'lodash/isString';
29
import { getUid } from 'ol';
30
import OlFeature from 'ol/Feature';
31
import OlGeometry from 'ol/geom/Geometry';
32
import OlLayerBase from 'ol/layer/Base';
33
import OlLayerVector from 'ol/layer/Vector';
34
import OlMapBrowserEvent from 'ol/MapBrowserEvent';
35
import OlSourceVector from 'ol/source/Vector';
36

37
import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil';
38
import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap';
39
import useOlLayer from '@terrestris/react-util/dist/Hooks/useOlLayer/useOlLayer';
40

41
import { CSS_PREFIX } from '../../constants';
42
import {
43
  defaultFeatureGridLayerName,
44
  defaultFeatureStyle,
45
  defaultHighlightStyle,
46
  defaultSelectStyle,
47
  highlightFillColor,
48
  RgCommonGridProps
49
} from '../commonGrid';
50

51
export type WithKey<T> = {
52
  key: Key;
53
} & T;
54

55
interface OwnProps<T> {
56
  /**
57
   * The height of the grid.
58
   */
59
  height?: number | string;
60
  /**
61
   * The theme to use for the grid. See https://www.ag-grid.com/angular-data-grid/theming/ for available options
62
   * and customization possibilities. Default is the balham theme.
63
   * NOTE: AG-Grid CSS should *not* be imported.
64
   */
65
  theme?: Theme;
66
  /**
67
   * Custom column definitions to apply to the given column (mapping via key).
68
   */
69
  columnDefs?: (ColDef<WithKey<T>> | ColGroupDef<WithKey<T>>)[] | null;
70
  /**
71
   * The width of the grid.
72
   */
73
  width?: number | string;
74
  /**
75
   * Custom row data to be shown in feature grid. This might be helpful if
76
   * original feature properties should be manipulated in some way before they
77
   * are represented in grid.
78
   * If provided, #getRowData method won't be called.
79
   */
80
  rowData?: WithKey<T>[];
81
  /**
82
   * Callback function, that will be called if the selection changes.
83
   */
84
  onRowSelectionChange?: (
85
    selectedRowsAfter: any[],
86
    selectedFeatures: OlFeature<OlGeometry>[],
87
    deselectedRows: any[],
88
    deselectedFeatures: OlFeature<OlGeometry>[],
89
    evt: SelectionChangedEvent
90
  ) => void;
91
  /**
92
   * Optional callback function, that will be called if `selectable` is set
93
   * `true` and the `click` event on the map occurs, e.g. a feature has been
94
   * selected in the map. The function receives the olEvt and the selected
95
   * features (if any).
96
   */
97
  onMapSingleClick?: (
98
    olEvt: OlMapBrowserEvent<PointerEvent | KeyboardEvent | WheelEvent>, selectedFeatures: OlFeature<OlGeometry>[]
99
  ) => void;
100
  /*
101
   * A Function that is called once the grid is ready.
102
   */
103
  onGridIsReady?: (gridReadyEvent: GridReadyEvent<WithKey<T>>) => void;
104
  /**
105
   * A custom rowStyle function (if used: row highlighting is overwritten)
106
   */
107
  rowStyleFn?: (params: RowClassParams<WithKey<T>>) => RowStyle | undefined;
108
}
109

110
const defaultClassName = `${CSS_PREFIX}ag-feature-grid`;
1✔
111

112
export type AgFeatureGridProps<T> = OwnProps<T> & RgCommonGridProps<T> & Omit<AgGridReactProps, 'theme'>;
113

114
ModuleRegistry.registerModules([
1✔
115
  AllCommunityModule
116
]);
117

118
/**
119
 * The AgFeatureGrid.
120
 */
121
export function AgFeatureGrid<T>({
122
  attributeBlacklist = [],
21✔
123
  className,
124
  columnDefs,
125
  featureStyle = defaultFeatureStyle,
19✔
126
  features = [],
1✔
127
  height = 250,
21✔
128
  highlightStyle = defaultHighlightStyle,
18✔
129
  keyFunction = getUid,
21✔
130
  layerName = defaultFeatureGridLayerName,
19✔
131
  onGridIsReady = () => undefined,
2✔
132
  onMapSingleClick,
133
  onRowClick,
134
  onRowMouseOut,
135
  onRowMouseOver,
136
  onRowSelectionChange,
137
  rowData,
138
  rowStyleFn,
139
  selectStyle = defaultSelectStyle,
18✔
140
  selectable = false,
18✔
141
  theme = themeBalham,
21✔
142
  width,
143
  zoomToExtent = false,
19✔
144
  ...agGridPassThroughProps
145
}: AgFeatureGridProps<T>): ReactElement<AgFeatureGridProps<WithKey<T>>> | null {
146

147
  /**
148
   * The default properties.
149
   */
150

151
  const [gridApi, setGridApi] = useState<GridApi<WithKey<T>> | undefined>(undefined);
21✔
152
  const [selectedRows, setSelectedRows] = useState<WithKey<T>[]>([]);
21✔
153
  const [highlightedRows, setHighlightedRows] = useState<Key[]>([]);
21✔
154

155
  const map = useMap();
21✔
156

157
  const gridVectorLayer = useOlLayer(() => new OlLayerVector({
21✔
158
    properties: {
159
      name: layerName,
160
    },
161
    source: new OlSourceVector<OlFeature>({
162
      features
163
    }),
164
    style: featureStyle
165
  }), [features, layerName], true);
166

167
  /**
168
   * Returns the currently selected row keys.
169
   *
170
   * @return An array with the selected row keys.
171
   */
172
  const getSelectedRowKeys = useCallback((): Key[] => {
21✔
173
    if (_isNil(gridApi)) {
×
174
      return [];
×
175
    }
176

177
    return gridApi.getSelectedRows()?.map(row => row.key) ?? [];
×
178
  }, [gridApi]);
179

180
  /**
181
   * Returns the corresponding rowNode for the given feature id.
182
   *
183
   * @param key The feature's key to obtain the row from.
184
   * @return he row candidate.
185
   */
186
  const getRowFromFeatureKey = useCallback((key: Key): RowNode | undefined => {
21✔
187
    let rowNode: RowNode | undefined = undefined;
×
188

189
    gridApi?.forEachNode((node: any) => {
×
190
      if (node.data.key === key) {
×
191
        rowNode = node;
×
192
      }
193
    });
194

195
    return rowNode;
×
196
  }, [gridApi]);
197

198
  /**
199
   * Highlights the feature beneath the cursor on the map and in the grid.
200
   *
201
   * @param olEvt The ol event.
202
   */
203
  const onMapPointerMoveInner = useCallback((olEvt: any) => {
21✔
204

205
    if (_isNil(gridApi) || _isNil(map)) {
×
206
      return;
×
207
    }
208

209
    const selectedRowKeys = getSelectedRowKeys();
×
210

211
    const highlightedFeatureArray = (map.getFeaturesAtPixel(olEvt.pixel, {
×
212
      layerFilter: layerCand => layerCand === gridVectorLayer
×
213
    }) || []) as OlFeature<OlGeometry>[];
214

215
    setHighlightedRows([]);
×
216

217
    features
×
218
      .filter((f): f is OlFeature => !_isNil(f))
×
219
      .forEach(feature => {
220
        const key = keyFunction(feature);
×
221
        if (selectedRowKeys.includes(key)) {
×
222
          feature.setStyle(selectStyle);
×
223
        } else {
224
          feature.setStyle(undefined);
×
225
        }
226
      });
227

228
    const rowsToHighlight: Key[] = [];
×
229
    highlightedFeatureArray
×
230
      .filter((f): f is OlFeature => !_isNil(f))
×
231
      .forEach(feat => {
232
        const key = keyFunction(feat);
×
233
        gridApi?.forEachNode((n) => {
×
234
          if (n?.data?.key === key) {
×
235
            rowsToHighlight.push(n?.data?.key);
×
236
            feat.setStyle(highlightStyle);
×
237
          }
238
        });
239
      });
240
    setHighlightedRows(rowsToHighlight);
×
241
  }, [gridVectorLayer, features, getSelectedRowKeys, gridApi, highlightStyle, keyFunction, map, selectStyle]);
242

243
  const getRowStyle = useCallback((params: RowClassParams<WithKey<T>>): RowStyle | undefined => {
21✔
244
    if (!_isNil(rowStyleFn)) {
27!
245
      return rowStyleFn(params);
×
246
    }
247

248
    if (!_isNil(params?.node?.data?.key) && highlightedRows?.includes(params?.node?.data?.key)) {
27!
249
      return {
×
250
        backgroundColor: highlightFillColor
251
      };
252
    }
253

254
    return;
27✔
255
  }, [highlightedRows, rowStyleFn]);
256

257
  /**
258
   * Selects the selected feature in the map and in the grid.
259
   *
260
   * @param olEvt The ol event.
261
   */
262
  const onMapSingleClickInner = useCallback((olEvt: OlMapBrowserEvent<PointerEvent | KeyboardEvent | WheelEvent>) => {
21✔
263
    if (_isNil(map)) {
×
264
      return;
×
265
    }
266

267
    const selectedRowKeys = getSelectedRowKeys();
×
268

269
    const selectedFeatures = (map.getFeaturesAtPixel(olEvt.pixel, {
×
270
      layerFilter: (layerCand: OlLayerBase) => layerCand === gridVectorLayer
×
271
    }) || []) as OlFeature<OlGeometry>[];
272

273
    if (_isFunction(onMapSingleClick)) {
×
274
      onMapSingleClick(olEvt, selectedFeatures);
×
275
    }
276

277
    selectedFeatures.forEach(selectedFeature => {
×
278
      const key = keyFunction(selectedFeature);
×
279
      if (selectedRowKeys && selectedRowKeys.includes(key)) {
×
280
        selectedFeature.setStyle(undefined);
×
281

282
        const node = getRowFromFeatureKey(key);
×
283
        if (node) {
×
284
          node.setSelected(false);
×
285
        }
286
      } else {
287
        selectedFeature.setStyle(selectStyle);
×
288

289
        const node = getRowFromFeatureKey(key);
×
290
        if (node) {
×
291
          node.setSelected(true);
×
292
        }
293
      }
294
    });
295
  }, [gridVectorLayer, getRowFromFeatureKey, getSelectedRowKeys, keyFunction, map, onMapSingleClick, selectStyle]);
296

297
  /**
298
   * Returns the column definitions out of the attributes of the first
299
   * given feature.
300
   *
301
   * @return The column definitions.
302
   */
303
  const getColumnDefsFromFeature = useCallback((): ColDef<WithKey<T>>[] | undefined => {
21✔
304
    if (features.length < 1) {
19✔
305
      return;
1✔
306
    }
307
    const columns: ColDef<WithKey<T>>[] = [];
18✔
308
    // assumption: all features in array have the same structure
309
    const feature = features[0];
18✔
310
    const props = feature.getProperties();
18✔
311

312
    const colDefsFromFeature = Object.keys(props).map((key: string): ColDef<WithKey<T>> | undefined => {
18✔
313
      if (attributeBlacklist.includes(key)) {
54!
314
        return;
×
315
      }
316

317
      let filter;
318

319
      if (props[key] instanceof OlGeometry) {
54✔
320
        return;
18✔
321
      }
322
      if (_isNumber(props[key])) {
36✔
323
        filter = 'agNumberColumnFilter';
18✔
324
      }
325
      if (_isString(props[key])) {
36✔
326
        filter = 'agTextColumnFilter';
18✔
327
      }
328

329
      return {
36✔
330
        colId: key,
331
        field: key as ColDefField<WithKey<T>>,
332
        filter,
333
        headerName: key,
334
        minWidth: 50,
335
        resizable: true,
336
        sortable: true
337
      };
338
    });
339

340
    return [
18✔
341
      ...columns,
342
      ...(colDefsFromFeature.filter(c => !_isNil(c)) as ColDef<WithKey<T>>[])
54✔
343
    ];
344
  }, [attributeBlacklist, features]);
345

346
  /**
347
   * Returns the table row data from all the given features.
348
   *
349
   * @return The table data.
350
   */
351
  const getRowData = useCallback((): WithKey<T>[] | undefined => {
21✔
352
    return features?.map((feature): WithKey<T> => {
10✔
353
      const properties = feature.getProperties();
27✔
354
      const filtered = Object.keys(properties)
27✔
355
        .filter(key => !(properties[key] instanceof OlGeometry))
81✔
356
        .reduce((obj: Record<string, any>, key) => {
357
          obj[key] = properties[key];
54✔
358
          return obj;
54✔
359
        }, {});
360

361
      return {
27✔
362
        key: keyFunction(feature),
363
        ...filtered
364
      } as WithKey<T>;
365
    });
366
  }, [features, keyFunction]);
367

368
  /**
369
   * Returns the corresponding feature for the given table row key.
370
   *
371
   * @param key The row key to obtain the feature from.
372
   * @return The feature candidate.
373
   */
374
  const getFeatureFromRowKey = (key: Key): OlFeature<OlGeometry> => {
21✔
375
    const feature = features.filter(f => keyFunction(f) === key);
21✔
376
    return feature[0];
7✔
377
  };
378

379
  /**
380
   * Called on row click and zooms the corresponding feature's extent.
381
   *
382
   * @param evt The RowClickedEvent.
383
   */
384
  const onRowClickInner = (evt: RowClickedEvent) => {
21✔
385
    const row = evt.data;
3✔
386
    const feature = getFeatureFromRowKey(row.key);
3✔
387

388
    if (_isFunction(onRowClick)) {
3!
389
      onRowClick(row, feature, evt);
×
390
    } else {
391
      if (!_isNil(map)) {
3!
392
        MapUtil.zoomToFeatures(map,[feature]);
3✔
393
      }
394
    }
395
  };
396

397
  /**
398
   * Called on row mouseover and hightlights the corresponding feature's
399
   * geometry.
400
   *
401
   * @param evt The ellMouseOverEvent.
402
   */
403
  const onRowMouseOverInner = (evt: CellMouseOverEvent) => {
21✔
404
    const row = evt.data;
4✔
405
    const feature = getFeatureFromRowKey(row.key);
4✔
406

407
    if (_isFunction(onRowMouseOver)) {
4!
408
      onRowMouseOver(row, feature, evt);
×
409
    }
410

411
    highlightFeatures([feature]);
4✔
412
  };
413

414
  /**
415
   * Called on mouseout and unhightlights any highlighted feature.
416
   *
417
   * @param evt The CellMouseOutEvent.
418
   */
419
  const onRowMouseOutInner = (evt: CellMouseOutEvent) => {
21✔
420
    const row = evt.data;
×
421
    const feature = getFeatureFromRowKey(row.key);
×
422

423
    if (_isFunction(onRowMouseOut)) {
×
424
      onRowMouseOut(row, feature, evt);
×
425
    }
426

427
    unHighlightFeatures([feature]);
×
428
  };
429

430
  /**
431
   * Highlights the given features in the map.
432
   *
433
   * @param featureArray The features to highlight.
434
   */
435
  const highlightFeatures = (featureArray: OlFeature<OlGeometry>[]) => {
21✔
436
    featureArray
4✔
437
      .filter((f): f is OlFeature => !_isNil(f))
4✔
438
      .forEach(feature => feature.setStyle(highlightStyle));
4✔
439
  };
440

441
  /**
442
   * Un-highlights the given features in the map.
443
   *
444
   * @param featureArray The features to un-highlight.
445
   */
446
  const unHighlightFeatures = (featureArray: OlFeature<OlGeometry>[]) => {
21✔
447
    const selectedRowKeys = getSelectedRowKeys();
×
448

449
    featureArray
×
450
      .filter((f): f is OlFeature => !_isNil(f))
×
451
      .forEach(feature => {
452
        const key = keyFunction(feature);
×
453
        if (selectedRowKeys && selectedRowKeys.includes(key)) {
×
454
          feature.setStyle(selectStyle);
×
455
        } else {
456
          feature.setStyle(undefined);
×
457
        }
458
      });
459
  };
460

461
  /**
462
   * Sets the select style to the given features in the map.
463
   *
464
   * @param featureArray The features to select.
465
   */
466
  const selectFeatures = (featureArray: OlFeature<OlGeometry>[]) => {
21✔
467
    featureArray.forEach(feature => {
×
468
      if (feature) {
×
469
        feature.setStyle(selectStyle);
×
470
      }
471
    });
472
  };
473

474
  /**
475
   * Resets the style of all features.
476
   */
477
  const resetFeatureStyles = () => {
21✔
478
    features.forEach(feature => feature.setStyle(undefined));
×
479
  };
480

481
  /**
482
   * Called if the selection changes.
483
   */
484
  const onSelectionChanged = (evt: SelectionChangedEvent) => {
21✔
485
    let selectedRowsAfter: WithKey<T>[];
486
    if (_isNil(gridApi)) {
×
487
      selectedRowsAfter = evt.api.getSelectedRows();
×
488
    } else {
489
      selectedRowsAfter = gridApi.getSelectedRows();
×
490
    }
491

492
    const deselectedRows = _differenceWith(selectedRows,
×
493
      selectedRowsAfter, (a, b) => a.key === b.key);
×
494

495
    const selectedFeatures = selectedRowsAfter.flatMap(row => {
×
496
      return row.key ? [getFeatureFromRowKey(row.key)] : [];
×
497
    });
498
    const deselectedFeatures = deselectedRows.flatMap(row => {
×
499
      return row.key ? [getFeatureFromRowKey(row.key)] : [];
×
500
    });
501

502
    // update state
503
    setSelectedRows(selectedRowsAfter);
×
504

505
    if (_isFunction(onRowSelectionChange)) {
×
506
      onRowSelectionChange(selectedRowsAfter, selectedFeatures, deselectedRows, deselectedFeatures, evt);
×
507
    }
508

509
    resetFeatureStyles();
×
510
    selectFeatures(selectedFeatures);
×
511
  };
512

513
  /**
514
   *
515
   * @param gridReadyEvent
516
   */
517
  const onGridReady = (gridReadyEvent: GridReadyEvent<WithKey<T>>) => {
21✔
518
    if (!_isNil(gridReadyEvent)) {
2!
519
      setGridApi(gridReadyEvent?.api);
2✔
520
      onVisibilityChange();
2✔
521
      onGridIsReady(gridReadyEvent);
2✔
522
    }
523
  };
524

525
  const onVisibilityChange = () => gridApi?.sizeColumnsToFit();
21✔
526

527
  /**
528
   * Adds map event callbacks to highlight and select features in the map (if
529
   * given) on pointermove and single-click. Hovered and selected features will
530
   * be highlighted and selected in the grid as well.
531
   */
532
  useEffect(() => {
21✔
533
    if (!_isNil(map)) {
21✔
534
      map.on('pointermove', onMapPointerMoveInner);
20✔
535

536
      if (selectable) {
20✔
537
        map.on('singleclick', onMapSingleClickInner);
3✔
538
      }
539
    }
540

541
    return () => {
21✔
542
      if (!_isNil(map)) {
21✔
543
        map.un('pointermove', onMapPointerMoveInner);
20✔
544

545
        if (selectable) {
20✔
546
          map.un('singleclick', onMapSingleClickInner);
3✔
547
        }
548
      }
549
    };
550
  }, [onMapPointerMoveInner, onMapSingleClickInner, map, selectable]);
551

552
  useEffect(() => {
21✔
553
    if (!_isNil(features) && features.length > 0 && !_isNil(map) && zoomToExtent) {
10✔
554
      MapUtil.zoomToFeatures(map,features);
1✔
555
    }
556
  }, [features, map, zoomToExtent]);
557

558
  // TODO: move to less?
559
  const outerDivStyle = useMemo(() => ({
21✔
560
    height,
561
    width
562
  }), [width, height]);
563

564
  const colDefs = useMemo(() => {
21✔
565
    if (!_isNil(columnDefs)) {
21✔
566
      return columnDefs;
2✔
567
    }
568
    return getColumnDefsFromFeature();
19✔
569
  }, [
570
    columnDefs,
571
    getColumnDefsFromFeature
572
  ]);
573

574
  const passedRowData = useMemo(() => !_isNil(rowData) ? rowData : getRowData(), [
21!
575
    rowData,
576
    getRowData
577
  ]);
578

579
  const finalClassName = className
21!
580
    ? `${className} ${defaultClassName}`
581
    : `${defaultClassName}`;
582

583
  return (
21✔
584
    <div
585
      className={finalClassName}
586
      style={outerDivStyle}
587
    >
588
      <AgGridReact<WithKey<T>>
589
        columnDefs={colDefs}
590
        getRowStyle={getRowStyle}
591
        onCellMouseOut={onRowMouseOutInner}
592
        onCellMouseOver={onRowMouseOverInner}
593
        onGridReady={onGridReady}
594
        onRowClicked={onRowClickInner}
595
        onSelectionChanged={onSelectionChanged}
596
        rowData={passedRowData}
597
        rowSelection={selectable ? {
21✔
598
          mode: 'multiRow',
599
          enableClickSelection: false
600
        } : undefined}
601
        theme={theme}
602
        {...agGridPassThroughProps}
603
      />
604
    </div>
605
  );
606

607
}
608

609
export default AgFeatureGrid;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc