Skip to content

Commit 8ae5df1

Browse files
authored
#12467 Add maximum visible features in current view option for FlatGeobuf layers (#12481)
Fix #12467 Add maximum visible features in current view option for FlatGeobuf
1 parent 5bbec3d commit 8ae5df1

21 files changed

Lines changed: 430 additions & 29 deletions

File tree

docs/developer-guide/maps-configuration.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,13 +1396,19 @@ See format specifications for more info about FlatGeobuf [here](https://flatgeob
13961396
```javascript
13971397
{
13981398
"type": "flatgeobuf",
1399-
"url": "https://host-sample/countries.fgb",
1399+
"url": "https://host-sample/countries.fgb",
14001400
"title": "Title",
14011401
"group": "",
1402-
"visibility": true
1402+
"visibility": true,
1403+
"maxFeaturesInView": 1000
14031404
}
14041405
```
14051406

1407+
Where:
1408+
1409+
- `url` is the URL of the FlatGeobuf source.
1410+
- `maxFeaturesInView` (optional) the maximum number of features to load for the current map view. Used to limit rendering when the FlatGeobuf source contains many features.
1411+
14061412
## Layer groups
14071413

14081414
Inside the map configuration, near the `layers` entry, you can find also the `groups` entry. This array contains information about the groups in the TOC.

web/client/api/FlatGeobuf.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ export const FGB_FEATURE_BATCH_SIZE = 200;
2828
* when batches arrive faster than the network roundtrip, in cesium plugin
2929
*/
3030
export const FGB_STREAM_FLUSH_INTERVAL = 500;
31+
export const FGB_MEANINGFUL_VIEW_RATIO = 0.95;
3132

33+
export const getFlatGeobufMaxFeaturesInView = (options) => {
34+
const maxFeaturesInView = Number(options?.maxFeaturesInView);
35+
return maxFeaturesInView > 0 ? maxFeaturesInView : undefined;
36+
};
3237
export const getFlatGeobufGeojson = () => import('flatgeobuf/lib/mjs/geojson').then(mod => mod);
3338
export const getFlatGeobufGeneric = () => import('flatgeobuf/lib/mjs/generic').then(mod => mod);
3439

@@ -45,7 +50,7 @@ function getTitleFromUrl(url) {
4550
return nameNoExt || filename;
4651
}
4752

48-
function extractCapabilities({url}) {
53+
function extractCapabilities({ url }) {
4954
const version = FGB_VERSION;
5055
const format = getFormat(url || '') || FGB;
5156
return {
@@ -81,7 +86,7 @@ export const getCapabilities = (url) => {
8186
crs: getFlatGeobufCrsFromMetadata(metadata)
8287
};
8388

84-
const capabilities = extractCapabilities({flatgeobuf, url});
89+
const capabilities = extractCapabilities({ flatgeobuf, url });
8590

8691
return {
8792
...capabilities,

web/client/api/__tests__/FlatGeobuf-test.jsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
FGB_VERSION,
1313
getCapabilities,
1414
createFlatGeobufGeometryTypeResolver,
15+
getFlatGeobufMaxFeaturesInView,
1516
sniffFlatGeobufFirstGeometryType,
1617
sniffFlatGeobufFirstFeature
1718
} from '../FlatGeobuf';
@@ -32,6 +33,21 @@ const FGB_FILE_FIRST_FEATURE_PROPS = {
3233
}
3334
};
3435
describe('Test FlatGeobuf API', () => {
36+
describe('getFlatGeobufMaxFeaturesInView', () => {
37+
it('returns undefined when the option is not configured', () => {
38+
expect(getFlatGeobufMaxFeaturesInView()).toBe(undefined);
39+
expect(getFlatGeobufMaxFeaturesInView({})).toBe(undefined);
40+
});
41+
it('returns the positive integer option', () => {
42+
expect(getFlatGeobufMaxFeaturesInView({ maxFeaturesInView: 25 })).toBe(25);
43+
expect(getFlatGeobufMaxFeaturesInView({ maxFeaturesInView: '25' })).toBe(25);
44+
});
45+
it('ignores invalid values', () => {
46+
expect(getFlatGeobufMaxFeaturesInView({ maxFeaturesInView: 0 })).toBe(undefined);
47+
expect(getFlatGeobufMaxFeaturesInView({ maxFeaturesInView: -1 })).toBe(undefined);
48+
expect(getFlatGeobufMaxFeaturesInView({ maxFeaturesInView: 'wrong' })).toBe(undefined);
49+
});
50+
});
3551
it('getCapabilities from FlatGeobuf file', (done) => {
3652
getCapabilities(FGB_FILE).then(({ bbox, format, version, title}) => {
3753
try {

web/client/api/catalog/FlatGeobuf.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,14 @@ function validateUrl(serviceUrl) {
5858
* Converts a FGB record into a layerNode
5959
* maybe export as util method
6060
*/
61-
const recordToLayer = (record) => {
61+
const recordToLayer = (record, {
62+
service
63+
} = {}) => {
6264
if (!record) {
6365
return null;
6466
}
6567
const { format, properties, bbox } = record;
68+
const { layerOptions } = service || {};
6669
return {
6770
type: FGB_LAYER_TYPE,
6871
url: record.url,
@@ -72,7 +75,8 @@ const recordToLayer = (record) => {
7275
sourceMetadata: record.metadata,
7376
...(bbox && { bbox }),
7477
...(format && { format }),
75-
...(properties && { properties })
78+
...(properties && { properties }),
79+
...layerOptions
7680
};
7781
};
7882

web/client/api/catalog/__tests__/FlatGeobuf-test.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,20 @@ describe('Test FlatGeobuf API catalog', () => {
4242
expect(layer.type).toEqual(FGB_LAYER_TYPE);
4343
expect(layer.url).toEqual(FGB_FILE);
4444
});
45+
it('should merge service layer options into the layer config', () => {
46+
const catalogRecord = {
47+
serviceType: FGB_LAYER_TYPE,
48+
isValid: true,
49+
identifier: FGB_FILE,
50+
url: FGB_FILE
51+
};
52+
const layer = getLayerFromRecord(catalogRecord, {
53+
service: {
54+
layerOptions: {
55+
maxFeaturesInView: 10
56+
}
57+
}
58+
});
59+
expect(layer.maxFeaturesInView).toBe(10);
60+
});
4561
});
46-

web/client/components/TOC/fragments/settings/Display.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {Checkbox, Col, ControlLabel, FormGroup, Glyphicon, Grid, Row, Button as
1616

1717
import tooltip from '../../../misc/enhancers/buttonTooltip';
1818
const Button = tooltip(ButtonRB);
19-
import IntlNumberFormControl from '../../../I18N/IntlNumberFormControl';
19+
import MSIntlNumberFormControl from '../../../I18N/IntlNumberFormControl';
2020
import Message from '../../../I18N/Message';
2121
import InfoPopover from '../../../widgets/widget/InfoPopover';
22+
import localizedProps from '../../../misc/enhancers/localizedProps';
2223
import Legend from '../../../../plugins/TOC/components/Legend';
2324
import VisibilityLimitsForm from './VisibilityLimitsForm';
2425
import { ServerTypes } from '../../../../utils/LayersUtils';
@@ -32,6 +33,8 @@ import StyleBasedWMSJsonLegend from '../../../../plugins/TOC/components/StyleBas
3233
import VectorLegend from '../../../../plugins/TOC/components/VectorLegend';
3334
import { isMapServerUrl } from '../../../../utils/ArcGISUtils';
3435

36+
const IntlNumberFormControl = localizedProps('placeholder')(MSIntlNumberFormControl);
37+
3538
export default class extends React.Component {
3639
static propTypes = {
3740
opacityText: PropTypes.node,
@@ -124,6 +127,11 @@ export default class extends React.Component {
124127
});
125128
}
126129

130+
onMaxFeaturesInViewChange = (value) => {
131+
const maxFeaturesInView = parseInt(value, 10);
132+
this.props.onChange("maxFeaturesInView", maxFeaturesInView > 0 ? maxFeaturesInView : undefined);
133+
};
134+
127135
getValidationState = (name) =>{
128136
if (this.state.legendOptions && this.state.legendOptions[name]) {
129137
return parseInt(this.state.legendOptions[name], 10) < 12 && "error";
@@ -212,6 +220,22 @@ export default class extends React.Component {
212220
</Col>
213221
</Row>}
214222

223+
{this.props.element.type === "flatgeobuf" && <Row>
224+
<Col xs={12}>
225+
<FormGroup>
226+
<ControlLabel><Message msgId="layerProperties.maxFeaturesInView" /></ControlLabel>
227+
<IntlNumberFormControl
228+
data-qa="display-max-features-in-view"
229+
type="number"
230+
min={1}
231+
step={1}
232+
placeholder="layerProperties.maxFeaturesInViewPlaceholder"
233+
value={this.props.element.maxFeaturesInView === undefined ? '' : this.props.element.maxFeaturesInView}
234+
onChange={this.onMaxFeaturesInViewChange}/>
235+
</FormGroup>
236+
</Col>
237+
</Row>}
238+
215239
<Row>
216240
<Col xs={12}>
217241
<FormGroup>

web/client/components/TOC/fragments/settings/__tests__/Display-test.jsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@ describe('test Layer Properties Display module component', () => {
5353
ReactTestUtils.Simulate.focus(inputs[0]);
5454
expect(inputs[0].value).toBe('100');
5555
});
56+
it('tests Display component for FlatGeobuf max features in view field', () => {
57+
const l = {
58+
name: 'layer00',
59+
title: 'Layer',
60+
visibility: true,
61+
storeIndex: 9,
62+
type: 'flatgeobuf',
63+
url: 'fakeurl',
64+
maxFeaturesInView: 7
65+
};
66+
const settings = {
67+
options: {opacity: 1}
68+
};
69+
const handlers = {
70+
onChange() {}
71+
};
72+
const spyOn = expect.spyOn(handlers, 'onChange');
73+
ReactDOM.render(<Display element={l} settings={settings} onChange={handlers.onChange}/>, document.getElementById("container"));
74+
const maxFeaturesInView = document.querySelector('[data-qa="display-max-features-in-view"]');
75+
expect(maxFeaturesInView).toBeTruthy();
76+
expect(maxFeaturesInView.value).toBe('7');
77+
ReactTestUtils.Simulate.change(maxFeaturesInView, { target: { value: '15' } });
78+
expect(spyOn.calls[0].arguments).toEqual([ 'maxFeaturesInView', 15 ]);
79+
ReactTestUtils.Simulate.change(maxFeaturesInView, { target: { value: '' } });
80+
expect(spyOn.calls[1].arguments).toEqual([ 'maxFeaturesInView', undefined ]);
81+
});
5682
it('tests Display component for wms for map viewer', () => {
5783
const l = {
5884
name: 'layer00',

web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77
*/
88
import React from 'react';
99
import { isNil } from 'lodash';
10-
import { FormGroup, Checkbox } from "react-bootstrap";
10+
import { FormGroup, Checkbox, ControlLabel, FormControl } from "react-bootstrap";
1111

1212
import Message from "../../../I18N/Message";
1313
import InfoPopover from '../../../widgets/widget/InfoPopover';
14+
import localizedProps from '../../../misc/enhancers/localizedProps';
15+
16+
const LocalizedFormControl = localizedProps('placeholder')(FormControl);
17+
18+
const parseMaxFeaturesInView = (event) => {
19+
const maxFeaturesInView = Number(event?.target?.value);
20+
return maxFeaturesInView > 0 ? maxFeaturesInView : undefined;
21+
};
1422

1523
/**
1624
* Common Advanced settings form WMS/CSW/WMTS/WFS
@@ -55,6 +63,21 @@ export default ({
5563
<Message msgId="catalog.fetchMetadata.label" />&nbsp;<InfoPopover text={<Message msgId="catalog.fetchMetadata.tooltip" />} />
5664
</Checkbox>
5765
</FormGroup>}
66+
{!isNil(service.type) && service.type === "flatgeobuf" &&
67+
<FormGroup className="form-group" controlId="maxFeaturesInView" key="maxFeaturesInView">
68+
<ControlLabel><Message msgId="layerProperties.maxFeaturesInView" /></ControlLabel>
69+
<LocalizedFormControl
70+
data-qa="catalog-max-features-in-view"
71+
type="number"
72+
min={1}
73+
step={1}
74+
placeholder="layerProperties.maxFeaturesInViewPlaceholder"
75+
value={service.layerOptions?.maxFeaturesInView === undefined ? '' : service.layerOptions?.maxFeaturesInView}
76+
onChange={(e) => onChangeServiceProperty("layerOptions", {
77+
...service.layerOptions,
78+
maxFeaturesInView: parseMaxFeaturesInView(e)
79+
})} />
80+
</FormGroup>}
5881
{['wfs', 'vector'].includes(service.type) && <FormGroup className="wfs-vector-interactive-legend" controlId="enableInteractiveLegend" key="enableInteractiveLegend">
5982
<Checkbox data-qa="display-interactive-legend-option"
6083
onChange={(e) => onChangeServiceProperty("layerOptions", { ...service.layerOptions, enableInteractiveLegend: e.target.checked})}

web/client/components/catalog/editor/AdvancedSettings/__tests__/CommonAdvancedSettings-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,33 @@ describe('Test common advanced settings', () => {
105105
expect(interactiveLegendLabel).toBeTruthy();
106106
expect(interactiveLegendLabel.innerHTML).toEqual('layerProperties.enableInteractiveLegendInfo.label');
107107
});
108+
it('test FlatGeobuf max features in view input', () => {
109+
const action = {
110+
onChangeServiceProperty: () => {}
111+
};
112+
const spyOn = expect.spyOn(action, 'onChangeServiceProperty');
113+
ReactDOM.render(<CommonAdvancedSettings
114+
onChangeServiceProperty={action.onChangeServiceProperty}
115+
service={{type: "flatgeobuf", layerOptions: { maxFeaturesInView: 7 }}}
116+
/>, document.getElementById("container"));
117+
const maxFeaturesInView = document.querySelector("[data-qa='catalog-max-features-in-view']");
118+
expect(maxFeaturesInView).toBeTruthy();
119+
expect(maxFeaturesInView.value).toBe('7');
120+
TestUtils.Simulate.change(maxFeaturesInView, { "target": { "value": '15' }});
121+
expect(spyOn.calls[0].arguments).toEqual([
122+
'layerOptions',
123+
{
124+
maxFeaturesInView: 15
125+
}
126+
]);
127+
TestUtils.Simulate.change(maxFeaturesInView, { "target": { "value": '' }});
128+
expect(spyOn.calls[1].arguments).toEqual([
129+
'layerOptions',
130+
{
131+
maxFeaturesInView: undefined
132+
}
133+
]);
134+
});
108135

109136
it('test if thumbnail checkbox is checked when globalHideThumbnail is true', () => {
110137
ReactDOM.render(

web/client/components/map/cesium/__tests__/Layer-test.jsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ import '../plugins/TerrainLayer';
2727
import '../plugins/ElevationLayer';
2828
import '../plugins/ArcGISLayer';
2929
import '../plugins/ModelLayer';
30-
import '../plugins/FlatGeobufLayer';
3130

3231
import {setStore} from '../../../../utils/SecurityUtils';
3332
import ConfigUtils from '../../../../utils/ConfigUtils';
3433
import MockAdapter from 'axios-mock-adapter';
3534
import axios from '../../../../libs/ajax';
35+
import { isMeaningfulCappedRectRefinement } from '../../../../utils/FlatGeobufLayerUtils';
3636

3737
const tilesetMock = {
3838
"asset": {
@@ -1904,6 +1904,33 @@ describe('Cesium layer', () => {
19041904
metadata: { geometryType: 3 } // Polygon
19051905
};
19061906

1907+
it('identifies meaningful capped rect refinements', () => {
1908+
const loadedRect = {
1909+
capped: true,
1910+
rect: { minX: 0, minY: 0, maxX: 100, maxY: 100 }
1911+
};
1912+
expect(isMeaningfulCappedRectRefinement(
1913+
loadedRect,
1914+
{ minX: 0, minY: 0, maxX: 100, maxY: 100 }
1915+
)).toBe(false);
1916+
expect(isMeaningfulCappedRectRefinement(
1917+
loadedRect,
1918+
{ minX: 1, minY: 1, maxX: 99, maxY: 99 }
1919+
)).toBe(false);
1920+
expect(isMeaningfulCappedRectRefinement(
1921+
loadedRect,
1922+
{ minX: 25, minY: 25, maxX: 75, maxY: 75 }
1923+
)).toBe(true);
1924+
expect(isMeaningfulCappedRectRefinement(
1925+
loadedRect,
1926+
{ minX: 50, minY: 50, maxX: 150, maxY: 150 }
1927+
)).toBe(false);
1928+
expect(isMeaningfulCappedRectRefinement(
1929+
{ ...loadedRect, capped: false },
1930+
{ minX: 25, minY: 25, maxX: 75, maxY: 75 }
1931+
)).toBe(false);
1932+
});
1933+
19071934
it('exposes getStyledFeatures and getInferredGeometryType accessors', () => {
19081935
const cmp = ReactDOM.render(
19091936
<CesiumLayer
@@ -1947,5 +1974,31 @@ describe('Cesium layer', () => {
19471974
// Same GeoJSONStyledFeatures instance: features stay loaded.
19481975
expect(cmp.layer.getStyledFeatures()).toBe(styledFeaturesBefore);
19491976
});
1977+
1978+
it('recreates the layer when maxFeaturesInView changes', () => {
1979+
const cmp = ReactDOM.render(
1980+
<CesiumLayer
1981+
type="flatgeobuf"
1982+
options={{
1983+
...baseOptions,
1984+
maxFeaturesInView: 1
1985+
}}
1986+
map={map}
1987+
/>, document.getElementById('container'));
1988+
const layerBefore = cmp.layer;
1989+
expect(layerBefore).toBeTruthy();
1990+
1991+
ReactDOM.render(
1992+
<CesiumLayer
1993+
type="flatgeobuf"
1994+
options={{
1995+
...baseOptions,
1996+
maxFeaturesInView: 2
1997+
}}
1998+
map={map}
1999+
/>, document.getElementById('container'));
2000+
expect(cmp.layer).toNotBe(layerBefore);
2001+
expect(cmp.layer.getStyledFeatures()).toBeTruthy();
2002+
});
19502003
});
19512004
});

0 commit comments

Comments
 (0)