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

terrestris / react-geo / 18554106033

16 Oct 2025 07:45AM UTC coverage: 67.411% (+0.2%) from 67.169%
18554106033

push

github

web-flow
Merge pull request #4419 from marcjansen/tackle-more-sq-issues

Tackle issues found by SonarQube

652 of 1060 branches covered (61.51%)

Branch coverage included in aggregate %.

18 of 31 new or added lines in 8 files covered. (58.06%)

1 existing line in 1 file now uncovered.

1220 of 1717 relevant lines covered (71.05%)

12.84 hits per line

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

51.28
/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 {
NEW
224
          feature.setStyle();
×
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
    return;
27✔
254
  }, [highlightedRows, rowStyleFn]);
255

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

266
    const selectedRowKeys = getSelectedRowKeys();
×
267

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

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

276
    selectedFeatures.forEach(selectedFeature => {
×
277
      const key = keyFunction(selectedFeature);
×
NEW
278
      if (selectedRowKeys?.includes(key)) {
×
NEW
279
        selectedFeature.setStyle();
×
280
        const node = getRowFromFeatureKey(key);
×
281
        if (node) {
×
282
          node.setSelected(false);
×
283
        }
284
      } else {
285
        selectedFeature.setStyle(selectStyle);
×
286

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

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

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

315
      let filter;
316

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

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

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

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

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

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

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

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

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

403
    if (_isFunction(onRowMouseOver)) {
4!
404
      onRowMouseOver(row, feature, evt);
×
405
    }
406

407
    highlightFeatures([feature]);
4✔
408
  };
409

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

419
    if (_isFunction(onRowMouseOut)) {
×
420
      onRowMouseOut(row, feature, evt);
×
421
    }
422

423
    unHighlightFeatures([feature]);
×
424
  };
425

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

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

445
    featureArray
×
446
      .filter((f): f is OlFeature => !_isNil(f))
×
447
      .forEach(feature => {
448
        const key = keyFunction(feature);
×
NEW
449
        if (selectedRowKeys?.includes(key)) {
×
450
          feature.setStyle(selectStyle);
×
451
        } else {
NEW
452
          feature.setStyle();
×
453
        }
454
      });
455
  };
456

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

470
  /**
471
   * Resets the style of all features.
472
   */
473
  const resetFeatureStyles = () => {
21✔
NEW
474
    features.forEach(feature => feature.setStyle());
×
475
  };
476

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

488
    const deselectedRows = _differenceWith(selectedRows,
×
489
      selectedRowsAfter, (a, b) => a.key === b.key);
×
490

491
    const selectedFeatures = selectedRowsAfter.flatMap(row => {
×
492
      return row.key ? [getFeatureFromRowKey(row.key)] : [];
×
493
    });
494
    const deselectedFeatures = deselectedRows.flatMap(row => {
×
495
      return row.key ? [getFeatureFromRowKey(row.key)] : [];
×
496
    });
497

498
    // update state
499
    setSelectedRows(selectedRowsAfter);
×
500

501
    if (_isFunction(onRowSelectionChange)) {
×
502
      onRowSelectionChange(selectedRowsAfter, selectedFeatures, deselectedRows, deselectedFeatures, evt);
×
503
    }
504

505
    resetFeatureStyles();
×
506
    selectFeatures(selectedFeatures);
×
507
  };
508

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

521
  const onVisibilityChange = () => gridApi?.sizeColumnsToFit();
21✔
522

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

532
      if (selectable) {
20✔
533
        map.on('singleclick', onMapSingleClickInner);
3✔
534
      }
535
    }
536

537
    return () => {
21✔
538
      if (!_isNil(map)) {
21✔
539
        map.un('pointermove', onMapPointerMoveInner);
20✔
540

541
        if (selectable) {
20✔
542
          map.un('singleclick', onMapSingleClickInner);
3✔
543
        }
544
      }
545
    };
546
  }, [onMapPointerMoveInner, onMapSingleClickInner, map, selectable]);
547

548
  useEffect(() => {
21✔
549
    if (!_isNil(features) && features.length > 0 && !_isNil(map) && zoomToExtent) {
10✔
550
      MapUtil.zoomToFeatures(map,features);
1✔
551
    }
552
  }, [features, map, zoomToExtent]);
553

554
  // TODO: move to less?
555
  const outerDivStyle = useMemo(() => ({
21✔
556
    height,
557
    width
558
  }), [width, height]);
559

560
  const colDefs = useMemo(() => {
21✔
561
    if (!_isNil(columnDefs)) {
21✔
562
      return columnDefs;
2✔
563
    }
564
    return getColumnDefsFromFeature();
19✔
565
  }, [
566
    columnDefs,
567
    getColumnDefsFromFeature
568
  ]);
569

570
  const passedRowData = useMemo(() => !_isNil(rowData) ? rowData : getRowData(), [
21!
571
    rowData,
572
    getRowData
573
  ]);
574

575
  const finalClassName = className
21!
576
    ? `${className} ${defaultClassName}`
577
    : `${defaultClassName}`;
578

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

603
}
604

605
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