Skip to content

Commit 9d25ef7

Browse files
committed
load flatgeobuf catalog
api catalog in metadataexplorer load dynamic fgb module unit tests flatgebouf api FlatGeobuf OL Loader FlatGeobuf Cesium Loader upgrade flatgeobuf 4.3.3 enable Mapinfo identify flatgeobuf layers update docs fix strategy style editor enabled flatgeobuf layer type
1 parent 967602a commit 9d25ef7

File tree

22 files changed

+769
-13
lines changed

22 files changed

+769
-13
lines changed

build/tests.webpack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
var context = require.context('../web', true, /(-test\.jsx?)|(-test-chrome\.jsx?)$/);
1+
var context = require.context('../web/client/plugins/tocitemssettings', true, /(-test\.jsx?)|(-test-chrome\.jsx?)$/);
22
context.keys().forEach(context);
33
module.exports = context;

docs/developer-guide/maps-configuration.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ In the case of the background the `thumbURL` is used to show a preview of the la
209209
- `terrain`: layers that define the elevation profile of the terrain
210210
- `cog`: Cloud Optimized GeoTIFF layers
211211
- `model`: 3D model layers like: IFC
212+
- `flatgeobuf`: FlatGeobuf vector layers
212213

213214
#### WMS
214215

@@ -1356,6 +1357,21 @@ Where:
13561357
}
13571358
```
13581359

1360+
#### FlatGeobuf(FGB) layer
1361+
1362+
This type of layer shows vector file in FlatGeobuf format also inside the Cesium viewer.
1363+
See format specifications for more info about FlatGeobuf [here](https://flatgeobuf.org/).
1364+
1365+
```javascript
1366+
{
1367+
"type": "flatgeobuf",
1368+
"url": "https://host-sample/countries.fgb",
1369+
"title": "Title",
1370+
"group": "",
1371+
"visibility": true
1372+
}
1373+
```
1374+
13591375
## Layer groups
13601376

13611377
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.

docs/user-guide/catalog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,3 +457,9 @@ In **General Settings** of a ArcGIS source type, it is possible to specify the s
457457
* *Zoom to selected layer extent* <img src="../img/button/zoom-layer.jpg" class="ms-docbutton"/>: in order to zoom the map to the layer's extent
458458
* Access the [Layer Settings](layer-settings.md#layer-settings) <img src="../img/button/properties.jpg" class="ms-docbutton"/> to view/edit the [General Information](layer-settings.md#general-information) and the [Display](layer-settings.md#ifc-layer) options
459459
* *Remove* the layer <img src="../img/button/delete.jpg" class="ms-docbutton"/>
460+
461+
### FlatGeobuf Catalog
462+
463+
A [FlatGeobuf (FGB)](https://flatgeobuf.org/) is a vector file format designed to be served through a standard HTTP server. Its internal structure enables fast and selective data access by leveraging HTTP Range Requests: clients can request only the portions of the file they need, without having to download the entire dataset.
464+
465+
In MapStore, FGB files can be added as layers. Through the Catalog tool, it is possible to configure multiple FlatGeobuf URL sources: each URL is interpreted as a single layer and added to the map directly and efficiently.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"eventlistener": "0.0.1",
160160
"file-saver": "1.3.3",
161161
"filtrex": "2.1.0",
162+
"flatgeobuf": "4.3.3",
162163
"font-awesome": "4.7.0",
163164
"fs-extra": "3.0.1",
164165
"git-revision-webpack-plugin": "5.0.0",
@@ -301,4 +302,4 @@
301302
},
302303
"author": "GeoSolutions",
303304
"license": "BSD-2-Clause"
304-
}
305+
}

web/client/api/FlatGeobuf.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import axios from '../libs/ajax';
9+
import proj4 from 'proj4';
10+
import isEmpty from 'lodash/isEmpty';
11+
import { extend, createEmpty } from 'ol/extent';
12+
import { GeoJSON } from 'ol/format';
13+
14+
/**
15+
* constants of FlatGeobuf format
16+
*/
17+
export const FGB = 'fgb'; // extension file and format short name
18+
export const FGB_LAYER_TYPE = 'flatgeobuf';
19+
export const FGB_VERSION = '3.0.1'; // supported format version
20+
21+
export const getFlatGeobufGeojson = () => import('flatgeobuf/lib/mjs/geojson').then(mod => mod);
22+
export const getFlatGeobufOl = () => import('flatgeobuf/lib/mjs/ol').then(mod => mod);
23+
24+
function getFormat(url) {
25+
const parts = url.split(/\./g);
26+
const format = parts[parts.length - 1];
27+
return format;
28+
}
29+
30+
function getTitleFromUrl(url) {
31+
const parts = url.split('/');
32+
return parts[parts.length - 2];
33+
}
34+
35+
function extractCapabilities({flatgeobuf, url}) {
36+
const version = FGB_VERSION; // flatgeobuf-ol not read file format version, it might be possible by reading the header
37+
//https://flatgeobuf.org/doc/format-changelog.html
38+
const format = getFormat(url || '') || FGB;
39+
return {
40+
version,
41+
format
42+
};
43+
}
44+
45+
//
46+
// copy and paste in catalog for testing: https://flatgeobuf.org/test/data/countries.fgb
47+
//
48+
export const getCapabilities = (url) => {
49+
return getFlatGeobufGeojson().then(async flatgeobuf => { //load lib dynamically
50+
return axios.get(url, {
51+
adapter: config => {
52+
return fetch(config.url); // use fetch adapter to get a stream
53+
}
54+
})
55+
.then(async ({body}) => {
56+
57+
const features = [];
58+
let metadata = {};
59+
let rect; //if undefined read all features
60+
61+
/////// TODO replace .deserialize() with new method .readMetadata()
62+
// when available in flatgeobuf lib > v4.4.5
63+
// just merged here: https://github.com/flatgeobuf/flatgeobuf/commit/15f137cdf30495dd84afda6159e537df39d5309c
64+
65+
/**
66+
* flatgeobuf.deserialize() return a AsyncGenerator
67+
*/
68+
for await (let feature of flatgeobuf.deserialize(
69+
body,
70+
rect,
71+
function HeaderMetaFn(meta) {
72+
// sample of metadata structure /web/client/test-resources/flatgeobuf/UScounties_subset.metadata.json
73+
const crs = `${meta?.crs?.org}:${meta?.crs?.code}`;
74+
const title = !isEmpty(meta?.title) ? meta.title : getTitleFromUrl(url);
75+
metadata = {
76+
...metadata,
77+
title,
78+
crs
79+
};
80+
})
81+
) {
82+
features.push(new GeoJSON().readFeature(feature));
83+
}
84+
85+
//TODO simplify using new method readMetadata(url) when available in flatgeobuf lib > v4.4.5
86+
// that return envelope
87+
88+
const totExtent = features.reduce((extent, feature) => {
89+
return extend(extent, feature.getGeometry().getExtent());
90+
}, createEmpty());
91+
92+
const bbox = {
93+
bounds: {
94+
minx: totExtent[0],
95+
miny: totExtent[1],
96+
maxx: totExtent[2],
97+
maxy: totExtent[3]
98+
},
99+
crs: metadata.crs || 'EPSG:4326'
100+
};
101+
102+
const capabilities = extractCapabilities({flatgeobuf, url});
103+
104+
return {
105+
...capabilities,
106+
...metadata,
107+
...(bbox && { bbox })
108+
};
109+
});
110+
});
111+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import expect from 'expect';
9+
10+
import {
11+
FGB,
12+
FGB_VERSION,
13+
FGB_LAYER_TYPE,
14+
getCapabilities
15+
} from '../FlatGeobuf';
16+
17+
const FGB_FILE = 'base/web/client/test-resources/flatgeobuf/UScounties_subset.fgb';
18+
19+
describe('Test FlatGeobuf API', () => {
20+
it('getCapabilities from FlatGeobuf file', (done) => {
21+
getCapabilities(FGB_FILE).then(({ bbox, format, version }) => {
22+
try {
23+
expect(format).toBeTruthy();
24+
expect(format).toBe('fgb'); //read from file extension
25+
expect(version).toBeTruthy();
26+
expect(version).toBe(FGB_VERSION);
27+
expect(bbox).toBeTruthy();
28+
expect(bbox.crs).toBe('EPSG:4326');
29+
// TODO add on flatgebouf upgrade > v4.4.5
30+
// expect(title).toBe('data');
31+
// expect(crs).toBe('EPSG:4326');
32+
//TODO get from test file
33+
// expect(Math.round(bbox.bounds.minx)).toBe(0);
34+
// expect(Math.round(bbox.bounds.miny)).toBe(0);
35+
// expect(Math.round(bbox.bounds.maxx)).toBe(0);
36+
// expect(Math.round(bbox.bounds.maxy)).toBe(0);
37+
} catch (e) {
38+
done(e);
39+
}
40+
done();
41+
});
42+
});
43+
});

web/client/api/catalog/CSW.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
} from './common';
2020
import { THREE_D_TILES } from '../ThreeDTiles';
2121
import { MODEL } from '../Model';
22+
import { FGB } from '../FlatGeobuf';
23+
2224
const getBaseCatalogUrl = (url) => {
2325
return url && url.replace(/\/csw$/, "/");
2426
};
@@ -271,7 +273,9 @@ export const getCatalogRecords = (records, options, locales) => {
271273
if (dc && dc.format === THREE_D_TILES) {
272274
catRecord = getCatalogRecord3DTiles(record, metadata);
273275
} else if (dc && dc.format === MODEL) {
274-
// todo: handle get catalog record for ifc
276+
// todo: handle get catalog record for Ifc Model
277+
} else if (dc && dc.format === FGB) {
278+
// todo: handle get catalog record for FlatGeobuf
275279
} else {
276280
const layerType = Object.keys(parsedReferences).filter(key => !ADDITIONAL_OGC_SERVICES.includes(key)).find(key => parsedReferences[key]);
277281
const ogcReferences = layerType && layerType !== 'esri'
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { Observable } from 'rxjs';
10+
import { isValidURL } from '../../utils/URLUtils';
11+
12+
import { preprocess as commonPreprocess } from './common';
13+
14+
import {
15+
FGB,
16+
FGB_VERSION,
17+
FGB_LAYER_TYPE,
18+
getCapabilities
19+
} from '../FlatGeobuf';
20+
21+
//copy from ThreeDTiles.js
22+
const getRecords = (url, startPosition, maxRecords, text) => {
23+
return getCapabilities(url)
24+
.then(({ ...properties }) => {
25+
const records = [{
26+
...properties,
27+
type: FGB_LAYER_TYPE,
28+
url,
29+
}];
30+
return {
31+
numberOfRecordsMatched: records.length,
32+
numberOfRecordsReturned: records.length,
33+
records
34+
};
35+
});
36+
};
37+
38+
function validateUrl(serviceUrl) {
39+
if (isValidURL(serviceUrl)) {
40+
const parts = serviceUrl.split(/\./g);
41+
// remove query params
42+
const ext = (parts[parts.length - 1] || '').split(/\?/g)[0];
43+
return ext === FGB
44+
? true
45+
: false;
46+
}
47+
return false;
48+
}
49+
50+
51+
/**
52+
* Converts a FGB record into a layerNode
53+
* maybe export as util method
54+
*/
55+
const recordToLayer = (record) => {
56+
if (!record) {
57+
return null;
58+
}
59+
const { bbox, format, properties } = record;
60+
return {
61+
type: FGB_LAYER_TYPE,
62+
url: record.url,
63+
title: record.title,
64+
visibility: true,
65+
...(bbox && { bbox }),
66+
...(format && { format }),
67+
...(properties && { properties })
68+
};
69+
};
70+
71+
export const preprocess = commonPreprocess;
72+
//export const validate = (service) => Observable.of(service);
73+
export const validate = (service) => {
74+
if (service.title && validateUrl(service.url)) {
75+
return Observable.of(service);
76+
}
77+
throw new Error("catalog.config.notValidURLTemplate");
78+
};
79+
export const testService = (service) => Observable.of(service);
80+
export const textSearch = (url, startPosition, maxRecords, text, info) => getRecords(url, startPosition, maxRecords, text, info);
81+
82+
export const getCatalogRecords = (response) => {
83+
return response?.records
84+
? response.records.map(record => {
85+
const { version, bbox, format, properties } = record;
86+
const identifier = (record.url || '').split('?')[0];
87+
return {
88+
serviceType: FGB_LAYER_TYPE,
89+
isValid: true,
90+
title: record.title,
91+
identifier,
92+
url: record.url,
93+
//...(bbox && { bbox }), //DONT PASS bbox otherwise viewport will set a fixed bbox to the layer
94+
...(format && { format }),
95+
references: []
96+
};
97+
})
98+
: null;
99+
};
100+
101+
export const getLayerFromRecord = (record, options, asPromise) => {
102+
if (asPromise) {
103+
return Promise.resolve(recordToLayer(record, options));
104+
}
105+
return recordToLayer(record, options);
106+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import expect from 'expect';
9+
10+
import {
11+
FGB,
12+
FGB_LAYER_TYPE
13+
} from '../../FlatGeobuf';
14+
import {
15+
textSearch,
16+
getLayerFromRecord,
17+
getCatalogRecords,
18+
validate
19+
} from '../FlatGeobuf';
20+
21+
const FGB_FILE = 'base/web/client/test-resources/flatgeobuf/UScounties_subset.fgb';
22+
23+
describe('Test FlatGeobuf API catalog', () => {
24+
it('should return a single record for flatGeobuf', (done) => {
25+
textSearch(FGB_FILE)
26+
.then((response) => {
27+
expect(response.records.length).toBe(1);
28+
expect(response.records[0].type).toBe(FGB_LAYER_TYPE);
29+
expect(response.records[0].visibility).toBe(true);
30+
expect(response.records[0].format).toBe(FGB);
31+
//TOOD test bbox on flatgeobuf upgrade > v4.4.5
32+
done();
33+
});
34+
});
35+
it('should extract the layer config from a catalog record', () => {
36+
const catalogRecord = {
37+
serviceType: FGB_LAYER_TYPE,
38+
isValid: true,
39+
identifier: FGB_FILE,
40+
url: FGB_FILE
41+
};
42+
const layer = getLayerFromRecord(catalogRecord);
43+
expect(layer.type).toEqual("model");
44+
expect(layer.url).toEqual(FGB_FILE);
45+
expect(layer.title).toEqual("Title");
46+
expect(layer.visibility).toEqual(true);
47+
expect(layer.bbox.crs).toEqual('EPSG:4326');
48+
});
49+
});
50+

0 commit comments

Comments
 (0)