Skip to content

Commit c080ca2

Browse files
committed
Merge branch 'master' into spring7
2 parents 73484ee + 8a1b239 commit c080ca2

71 files changed

Lines changed: 2470 additions & 170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/actions/__tests__/widgets-test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,26 @@ describe('Test correctness of the widgets actions', () => {
105105
expect(retval.deletedInteractions).toBe(deletedInteractions);
106106
expect(retval.widget.deletedInteractions).toBeFalsy();
107107
});
108+
it('insertWidget preserves the persisted widget payload (no transient flag stripping)', () => {
109+
const widget = {
110+
widgetType: 'filter',
111+
title: 'My Filter'
112+
};
113+
const retval = insertWidget(widget);
114+
expect(retval).toExist();
115+
expect(retval.type).toBe(INSERT);
116+
expect(retval.widget.title).toBe('My Filter');
117+
expect(retval.widget.widgetType).toBe('filter');
118+
});
119+
it('insertWidget persists the globalWidgetMode flag so it survives edit mode', () => {
120+
const widget = {
121+
widgetType: 'filter',
122+
globalWidgetMode: true,
123+
title: 'TOC Filter'
124+
};
125+
const retval = insertWidget(widget);
126+
expect(retval.widget.globalWidgetMode).toBe(true);
127+
});
108128
it('updateWidget', () => {
109129
const widget = {};
110130
const retval = updateWidget(widget);

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/GeoNode.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,35 @@ export const resolveTagFilterType = (service) => {
5353
return getGeoNodeDefaultTagFilterType();
5454
};
5555

56+
const getTagLabel = (tag, tagFilterType) => {
57+
if (!tag || typeof tag !== 'object') {
58+
return tag;
59+
}
60+
if (tagFilterType === 'category') {
61+
return tag.label || tag.gn_description || tag.identifier;
62+
}
63+
if (tagFilterType === 'keyword') {
64+
return tag.label || tag.name || tag.slug;
65+
}
66+
return tag.label || tag.name || tag.gn_description || tag.identifier || tag.slug;
67+
};
68+
69+
const normalizeTag = (tag, tagFilterType) => {
70+
if (!tag || typeof tag !== 'object') {
71+
return tag;
72+
}
73+
const label = getTagLabel(tag, tagFilterType);
74+
return label ? { ...tag, label } : tag;
75+
};
76+
5677
export const getCatalogRecords = (records, options) => {
5778
if (records && records.records) {
5879
const tagFilterType = resolveTagFilterType(options?.service);
5980
return records.records.map((record) => {
60-
const tags = tagFilterType === 'keyword'
81+
const tags = (tagFilterType === 'keyword'
6182
? (record.keywords || [])
62-
: (record.category ? [record.category] : []);
83+
: (record.category ? [record.category] : []))
84+
.map((tag) => normalizeTag(tag, tagFilterType));
6385
return {
6486
...record,
6587
serviceType: "geonode",

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/api/catalog/__tests__/GeoNode-test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,40 @@ describe('Test correctness of the GeoNode catalog APIs', () => {
3737
expect(records[0].pk).toBe(123);
3838
});
3939

40+
it('geonode catalog records mapping uses category label for tags', () => {
41+
const records = getCatalogRecords({
42+
records: [{
43+
title: 'Title',
44+
category: {
45+
identifier: 'boundaries',
46+
gn_description: 'Boundaries'
47+
},
48+
pk: 123
49+
}]
50+
});
51+
expect(records[0].tags).toEqual([{
52+
identifier: 'boundaries',
53+
gn_description: 'Boundaries',
54+
label: 'Boundaries'
55+
}]);
56+
});
57+
58+
it('geonode catalog records mapping falls back to category identifier for tag label', () => {
59+
const records = getCatalogRecords({
60+
records: [{
61+
title: 'Title',
62+
category: {
63+
identifier: 'boundaries'
64+
},
65+
pk: 123
66+
}]
67+
});
68+
expect(records[0].tags).toEqual([{
69+
identifier: 'boundaries',
70+
label: 'boundaries'
71+
}]);
72+
});
73+
4074
it('geonode catalog records returns null with no records', () => {
4175
expect(getCatalogRecords()).toBe(null);
4276
expect(getCatalogRecords({})).toBe(null);

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',

0 commit comments

Comments
 (0)