Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/developer-guide/maps-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ In the case of the background the `thumbURL` is used to show a preview of the la
- `terrain`: layers that define the elevation profile of the terrain
- `cog`: Cloud Optimized GeoTIFF layers
- `model`: 3D model layers like: IFC
- `flatgeobuf`: FlatGeobuf vector layers

#### WMS

Expand Down Expand Up @@ -1356,6 +1357,21 @@ Where:
}
```

#### FlatGeobuf(FGB) layer

This type of layer shows vector file in FlatGeobuf format also inside the Cesium viewer.
See format specifications for more info about FlatGeobuf [here](https://flatgeobuf.org/).

```javascript
{
"type": "flatgeobuf",
"url": "https://host-sample/countries.fgb",
"title": "Title",
"group": "",
"visibility": true
}
```

## Layer groups

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.
Expand Down
6 changes: 6 additions & 0 deletions docs/user-guide/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,9 @@ In **General Settings** of a ArcGIS source type, it is possible to specify the s
* *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
* 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
* *Remove* the layer <img src="../img/button/delete.jpg" class="ms-docbutton"/>

### FlatGeobuf Catalog

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.

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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"eventlistener": "0.0.1",
"file-saver": "1.3.3",
"filtrex": "2.1.0",
"flatgeobuf": "4.4.0",
"font-awesome": "4.7.0",
"fs-extra": "3.0.1",
"git-revision-webpack-plugin": "5.0.0",
Expand Down
79 changes: 79 additions & 0 deletions web/client/api/FlatGeobuf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import axios from '../libs/ajax';
import isEmpty from 'lodash/isEmpty';

/**
* constants of FlatGeobuf format
*/
export const FGB = 'fgb'; // extension file and format short name
export const FGB_LAYER_TYPE = 'flatgeobuf';
export const FGB_VERSION = '3.0.1'; // supported format version

export const getFlatGeobufGeojson = () => import('flatgeobuf/lib/mjs/geojson').then(mod => mod);
export const getFlatGeobufGeneric = () => import('flatgeobuf/lib/mjs/generic').then(mod => mod); // implement readMetadata()
export const getFlatGeobufOl = () => import('flatgeobuf/lib/mjs/ol').then(mod => mod);

function getFormat(url) {
const parts = url.split(/\./g);
const format = parts[parts.length - 1];
return format;
}

function getTitleFromUrl(url) {
const parts = url.split('/');
const filename = parts[parts.length - 1];
const nameNoExt = filename.split('.').slice(0, -1).join('.');
return nameNoExt || filename;
}

function extractCapabilities({url}) {
const version = FGB_VERSION; // flatgeobuf-ol not read file format version, it might be possible by reading the header
// https://flatgeobuf.org/doc/format-changelog.html
const format = getFormat(url || '') || FGB;
return {
version,
format
};
}

//
// copy and paste in catalog for testing: https://flatgeobuf.org/test/data/countries.fgb
//
export const getCapabilities = (url) => {
return getFlatGeobufGeneric().then(async flatgeobuf => { // load lib dynamically
return axios.get(url, {
adapter: async config => {
// fetch(config.url); // use fetch adapter to get a stream
return await flatgeobuf.readMetadata(config.url); // readMetadata accept also request headers as param
}
}).then(async(metadata) => {

// const metadata = await flatgeobuf.readMetadata(url);
metadata.title = !isEmpty(metadata?.title) ? metadata.title : getTitleFromUrl(url);

const bbox = {
bounds: {
minx: metadata.envelope[0],
miny: metadata.envelope[1],
maxx: metadata.envelope[2],
maxy: metadata.envelope[3]
},
crs: metadata.crs ? `${metadata.crs.org}:${metadata.crs.code}` : 'EPSG:4326'
};

const capabilities = extractCapabilities({flatgeobuf, url});

return {
...capabilities,
...metadata,
...(bbox && { bbox })
};
});
});
};
42 changes: 42 additions & 0 deletions web/client/api/__tests__/FlatGeobuf-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import expect from 'expect';

import {
FGB,
FGB_VERSION,
getCapabilities
} from '../FlatGeobuf';

const FGB_FILE = 'base/web/client/test-resources/flatgeobuf/UScounties_subset.fgb';

describe('Test FlatGeobuf API', () => {
it('getCapabilities from FlatGeobuf file', (done) => {
getCapabilities(FGB_FILE).then(({ bbox, format, version }) => {
try {
expect(format).toBeTruthy();
expect(format).toBe(FGB); // read from file extension
expect(version).toBeTruthy();
expect(version).toBe(FGB_VERSION);
expect(bbox).toBeTruthy();
expect(bbox.crs).toBe('EPSG:4326');
// TODO add on flatgebouf upgrade > v4.4.5
// expect(title).toBe('data');
// expect(crs).toBe('EPSG:4326');
// TODO get from test file
// expect(Math.round(bbox.bounds.minx)).toBe(0);
// expect(Math.round(bbox.bounds.miny)).toBe(0);
// expect(Math.round(bbox.bounds.maxx)).toBe(0);
// expect(Math.round(bbox.bounds.maxy)).toBe(0);
} catch (e) {
done(e);
}
done();
});
});
});
6 changes: 5 additions & 1 deletion web/client/api/catalog/CSW.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
} from './common';
import { THREE_D_TILES } from '../ThreeDTiles';
import { MODEL } from '../Model';
import { FGB } from '../FlatGeobuf';

const getBaseCatalogUrl = (url) => {
return url && url.replace(/\/csw$/, "/");
};
Expand Down Expand Up @@ -271,7 +273,9 @@ export const getCatalogRecords = (records, options, locales) => {
if (dc && dc.format === THREE_D_TILES) {
catRecord = getCatalogRecord3DTiles(record, metadata);
} else if (dc && dc.format === MODEL) {
// todo: handle get catalog record for ifc
// todo: handle get catalog record for Ifc Model
} else if (dc && dc.format === FGB) {
// todo: handle get catalog record for FlatGeobuf
} else {
const layerType = Object.keys(parsedReferences).filter(key => !ADDITIONAL_OGC_SERVICES.includes(key)).find(key => parsedReferences[key]);
const ogcReferences = layerType && layerType !== 'esri'
Expand Down
106 changes: 106 additions & 0 deletions web/client/api/catalog/FlatGeobuf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import { Observable } from 'rxjs';
import { isValidURL } from '../../utils/URLUtils';

import { preprocess as commonPreprocess } from './common';

import {
FGB,
FGB_LAYER_TYPE,
getCapabilities
} from '../FlatGeobuf';

// copy from ThreeDTiles.js
const getRecords = (url) => {
return getCapabilities(url)
.then(({ ...properties }) => {
const records = [{
...properties,
visibility: true,
type: FGB_LAYER_TYPE,
url
}];
return {
numberOfRecordsMatched: records.length,
numberOfRecordsReturned: records.length,
records
};
});
};

function validateUrl(serviceUrl) {
if (isValidURL(serviceUrl)) {
const parts = serviceUrl.split(/\./g);
// remove query params
const ext = (parts[parts.length - 1] || '').split(/\?/g)[0];
return ext === FGB
? true
: false;
}
return false;
}


/**
* Converts a FGB record into a layerNode
* maybe export as util method
*/
const recordToLayer = (record) => {
if (!record) {
return null;
}
const { format, properties } = record;
return {
type: FGB_LAYER_TYPE,
url: record.url,
title: record.title,
visibility: true,
...(format && { format }),
...(properties && { properties })
};
};

export const preprocess = commonPreprocess;
// export const validate = (service) => Observable.of(service);
export const validate = (service) => {
if (service.title && validateUrl(service.url)) {
return Observable.of(service);
}
throw new Error("catalog.config.notValidURLTemplate");
};
export const testService = (service) => Observable.of(service);

export const textSearch = (url, startPosition, maxRecords, text, info) => getRecords(url, startPosition, maxRecords, text, info);

export const getCatalogRecords = (response) => {
return response?.records
? response.records.map(record => {
const { format } = record;
const identifier = (record.url || '').split('?')[0];
return {
serviceType: FGB_LAYER_TYPE,
isValid: true,
title: record.title,
identifier,
url: record.url,
// ...(bbox && { bbox }), //DONT PASS bbox otherwise viewport will set a fixed bbox to the layer
...(format && { format }),
references: []
};
})
: null;
};

export const getLayerFromRecord = (record, options, asPromise) => {
if (asPromise) {
return Promise.resolve(recordToLayer(record, options));
}
return recordToLayer(record, options);
};
46 changes: 46 additions & 0 deletions web/client/api/catalog/__tests__/FlatGeobuf-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import expect from 'expect';

import {
FGB,
FGB_LAYER_TYPE
} from '../../FlatGeobuf';
import {
textSearch,
getLayerFromRecord
} from '../FlatGeobuf';

const FGB_FILE = 'base/web/client/test-resources/flatgeobuf/UScounties_subset.fgb';

describe('Test FlatGeobuf API catalog', () => {
it('should return a single record for flatGeobuf', (done) => {
textSearch(FGB_FILE)
.then((response) => {
expect(response.records.length).toBe(1);
expect(response.records[0].type).toBe(FGB_LAYER_TYPE);
expect(response.records[0].visibility).toBe(true);
expect(response.records[0].title).toBe('UScounties_subset');
expect(response.records[0].format).toBe(FGB);
done();
});
});
it('should extract the layer config from a catalog record', () => {
const catalogRecord = {
serviceType: FGB_LAYER_TYPE,
isValid: true,
identifier: FGB_FILE,
url: FGB_FILE
};
const layer = getLayerFromRecord(catalogRecord);
expect(layer.visibility).toBe(true);
expect(layer.type).toEqual(FGB_LAYER_TYPE);
expect(layer.url).toEqual(FGB_FILE);
});
});

5 changes: 4 additions & 1 deletion web/client/api/catalog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as threeDTiles from './ThreeDTiles';
import * as cog from './COG';
import * as model from './Model'; // todo: will change to model
import * as arcgis from './ArcGIS';
import * as flatgeobuf from './FlatGeobuf';
/**
* APIs collection for catalog.
* Each entry must implement:
Expand All @@ -44,6 +45,7 @@ export default {
// TODO: we should separate catalog specific API from OGC services API, to define better the real interfaces of each API.
// TODO: validate could be converted in a simple function
// TODO: testService could be converted in a simple Promise
// TODO: use LAYER_TYPE constants defined in each module as keys for the exported object
'csw': csw,
'wfs': wfs,
'wms': wms,
Expand All @@ -54,5 +56,6 @@ export default {
'3dtiles': threeDTiles,
'cog': cog,
'model': model,
'arcgis': arcgis
'arcgis': arcgis,
'flatgeobuf': flatgeobuf
};
Loading