Skip to content

Commit

Permalink
#8055 allow to load 3dtiles tileset via viewer parameters (#9021)
Browse files Browse the repository at this point in the history
Co-authored-by: Lorenzo Natali <lorenzo.natali@geosolutionsgroup.com>
  • Loading branch information
allyoucanmap and offtherailz authored Mar 24, 2023
1 parent 9aaa4a2 commit 450b0e5
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 17 deletions.
27 changes: 26 additions & 1 deletion docs/developer-guide/map-query-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ Requirements:
- The number of layers should match the number of sources
- The source name can be a string that must match a catalog service name present in the map or an object that defines an external catalog (see example)
Supported layer types are WMS, WMTS and WFS.
Supported layer types are WMS, WMTS, WFS and 3D Tiles.
Example:
Expand Down Expand Up @@ -371,3 +371,28 @@ Data of resulting layer can be additionally filtered by passing "CQL_FILTER" int
GET `#/viewer/config?actions=[{"type":"CATALOG:ADD_LAYERS_FROM_CATALOGS","layers":["layer1","layer2","workspace:externallayername"],"sources":["catalog1","catalog2",{"type":"WMS","url":"https://example.com/wms"}],"options": [{"params":{"CQL_FILTER":"NAME='value'"}}, {}, {"params":{"CQL_FILTER":"NAME='value2'"}}]}]`
Number of objects passed to the options can be different to the number of layers, in this case options will be applied to the first X layers, where X is the length of options array.
The 3D tiles service endpoint does not contain a default property for the name of the layer and it returns only a single record for this reason the name used in the layers array will be used to apply the title to the added 3D Tiles layer:
```json
{
"type": "CATALOG:ADD_LAYERS_FROM_CATALOGS",
"layers": ["My 3D Tiles Layer"],
"sources": [{ "type":"3dtiles", "url":"https://example.com/tileset-pathname/tileset.json" }]
}
```
GET: `#/viewer/config?actions=[{"type":"CATALOG:ADD_LAYERS_FROM_CATALOGS","layers":["My 3D Tiles Layer"],"sources":[{"type":"3dtiles","url":"https://example.com/tileset-pathname/tileset.json"}]}]`
For the 3D Tiles you can pass also the layer options, to customize the layer. Here and example to localize the title:
```json
{
"type": "CATALOG:ADD_LAYERS_FROM_CATALOGS",
"layers": ["My 3D Tiles Layer"],
"sources": [{ "type":"3dtiles", "url":"https://example.com/tileset-pathname/tileset.json" }],
"options":[{ "title": { "en-US": "LayerTitle", "it-IT": "TitoloLivello" }}]
}
```
GET: `#/viewer/config?actions=[{"type":"CATALOG:ADD_LAYERS_FROM_CATALOGS","layers":["My 3D Tiles Layer"],"sources":[{"type":"3dtiles","url":"https://example.com/tileset-pathname/tileset.json"}],"options":[{"title":{"en-US":"LayerTitle","it-IT":"TitoloLivello"}}]}]`
5 changes: 4 additions & 1 deletion web/client/api/catalog/ThreeDTiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function validateUrl(serviceUrl) {
}

const recordToLayer = (record) => {
if (!record) {
return null;
}
const { bbox, format, properties } = record;
return {
type: '3dtiles',
Expand Down Expand Up @@ -53,7 +56,7 @@ const getRecords = (url, startPosition, maxRecords, text, info) => {
type: '3dtiles',
tileset,
...properties
}].filter(({ title }) => !text || title?.toLowerCase().includes(text?.toLowerCase() || ''));
}];
return {
numberOfRecordsMatched: records.length,
numberOfRecordsReturned: records.length,
Expand Down
4 changes: 2 additions & 2 deletions web/client/api/catalog/__tests__/ThreeDTiles-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ describe('Test 3D tiles catalog API', () => {
done();
});
});
it('should not return a single record if title not match the filter', (done) => {
it('should always return a single record even if title not match the filter', (done) => {
mockAxios.onGet().reply(200, TILSET_JSON);
textSearch('http://service.org/tileset.json', undefined, undefined, 'filter')
.then((response) => {
expect(response.records.length).toBe(0);
expect(response.records.length).toBe(1);
done();
});
});
Expand Down
4 changes: 2 additions & 2 deletions web/client/components/catalog/Catalog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,9 @@ class Catalog extends React.Component {
</InputGroup.Addon>
</InputGroup>
</FormGroup>
<FormGroup controlId="searchText" key="searchText">
{this.props.services?.[this.props.selectedService]?.type !== '3dtiles' && <FormGroup controlId="searchText" key="searchText">
{this.renderTextSearch()}
</FormGroup>
</FormGroup>}
<FormGroup controlId="buttons" key="buttons">
{this.renderButtons()}
{this.props.layerError ? this.renderError(this.props.layerError) : null}
Expand Down
130 changes: 130 additions & 0 deletions web/client/epics/__tests__/catalog-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ import {
ADD_CATALOG_SERVICE,
addService
} from '../../actions/catalog';
import MockAdapter from 'axios-mock-adapter';
import axios from '../../libs/ajax';

let mockAxios;

describe('catalog Epics', () => {
it('getMetadataRecordById', (done) => {
Expand Down Expand Up @@ -808,4 +811,131 @@ describe('catalog Epics', () => {
}
}, state, done);
});

describe('addLayersFromCatalogsEpic 3d tiles', () => {

beforeEach(done => {
mockAxios = new MockAdapter(axios);
setTimeout(done);
});

afterEach(done => {
mockAxios.restore();
setTimeout(done);
});
it('should add layer with title', (done) => {
const NUM_ACTIONS = 1;
const tileset = {
"asset": {
"version": "1.0"
},
"properties": {
"Height": {
"minimum": 0,
"maximum": 7
}
},
"geometricError": 70,
"root": {
"refine": "ADD",
"boundingVolume": {
"region": [
-1.3197004795898053,
0.6988582109,
-1.3196595204101946,
0.6988897891,
0,
20
]
},
"geometricError": 0,
"content": {
"uri": "model.b3dm"
}
}
};
mockAxios.onGet(/tileset\.json/).reply(() => ([ 200, tileset ]));
testEpic(
addLayersFromCatalogsEpic,
NUM_ACTIONS,
addLayersMapViewerUrl(["Title"], [{ url: 'https://server.org/name/tileset.json', type: '3dtiles' }]),
(actions) => {
try {
const [
addLayerAndDescribeAction
] = actions;
expect(addLayerAndDescribeAction.type).toBe(ADD_LAYER_AND_DESCRIBE);
expect(addLayerAndDescribeAction.layer).toBeTruthy();
expect(addLayerAndDescribeAction.layer.type).toBe("3dtiles");
expect(addLayerAndDescribeAction.layer.url).toBe("https://server.org/name/tileset.json");
expect(addLayerAndDescribeAction.layer.title).toBe("Title");
} catch (e) {
done(e);
}
done();
}, {});
});
it('should add layer with catalog id', (done) => {
const NUM_ACTIONS = 1;
const tileset = {
"asset": {
"version": "1.0"
},
"properties": {
"Height": {
"minimum": 0,
"maximum": 7
}
},
"geometricError": 70,
"root": {
"refine": "ADD",
"boundingVolume": {
"region": [
-1.3197004795898053,
0.6988582109,
-1.3196595204101946,
0.6988897891,
0,
20
]
},
"geometricError": 0,
"content": {
"uri": "model.b3dm"
}
}
};
mockAxios.onGet(/tileset\.json/).reply(() => ([ 200, tileset ]));
testEpic(
addLayersFromCatalogsEpic,
NUM_ACTIONS,
addLayersMapViewerUrl(["name"], ["3dTilesCatalog"]),
(actions) => {
try {
const [
addLayerAndDescribeAction
] = actions;
expect(addLayerAndDescribeAction.type).toBe(ADD_LAYER_AND_DESCRIBE);
expect(addLayerAndDescribeAction.layer).toBeTruthy();
expect(addLayerAndDescribeAction.layer.type).toBe("3dtiles");
expect(addLayerAndDescribeAction.layer.url).toBe("https://server.org/name/tileset.json");
expect(addLayerAndDescribeAction.layer.title).toBe("name");
} catch (e) {
done(e);
}
done();
}, {
catalog: {
selectedService: "3dTilesCatalog",
services: {
"3dTilesCatalog": {
url: 'https://server.org/name/tileset.json',
type: '3dtiles'
}
}
}
});
});
});
});
62 changes: 61 additions & 1 deletion web/client/epics/__tests__/queryparam-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ describe('queryparam epics', () => {
done();
}, state, false, true);
});
it('switch map type to cesium if cesium viewer options are found', (done) => {
it('switch map type to 3D if cesium viewer options are found', (done) => {
const state = {
maptype: {
mapType: 'openlayers'
Expand All @@ -622,6 +622,66 @@ describe('queryparam epics', () => {
}
}, state);
});
it('switch map type to 3D if actions param includes 3d tiles service', (done) => {
const state = {
maptype: {
mapType: 'openlayers'
},
router: {
location: {
search: '?actions=[{"type":"CATALOG:ADD_LAYERS_FROM_CATALOGS","layers":["Layer"],"sources":[{"type":"3dtiles","url":"https://tileset.org/tileset.json"}]}]'
}
}
};
const NUMBER_OF_ACTIONS = 2;
testEpic(addTimeoutEpic(readQueryParamsOnMapEpic, 10), NUMBER_OF_ACTIONS, [
onLocationChanged({}),
configureMap()
], (actions) => {
expect(actions.length).toBe(NUMBER_OF_ACTIONS);
try {
expect(actions[0].type).toBe(VISUALIZATION_MODE_CHANGED);
expect(actions[0].visualizationMode).toBe(VisualizationModes._3D);
done();
} catch (e) {
done(e);
}
}, state);
});
it('switch map type to 3D if addLayers param includes 3d tiles service', (done) => {
const state = {
maptype: {
mapType: 'openlayers'
},
router: {
location: {
search: '?addLayers=Layer;serviceId3DTiles'
}
},
catalog: {
services: {
serviceId3DTiles: {
type: "3dtiles",
url: "https://tileset.org/tileset.json"
}
}
}
};
const NUMBER_OF_ACTIONS = 2;
testEpic(addTimeoutEpic(readQueryParamsOnMapEpic, 10), NUMBER_OF_ACTIONS, [
onLocationChanged({}),
configureMap()
], (actions) => {
expect(actions.length).toBe(NUMBER_OF_ACTIONS);
try {
expect(actions[0].type).toBe(VISUALIZATION_MODE_CHANGED);
expect(actions[0].visualizationMode).toBe(VisualizationModes._3D);
done();
} catch (e) {
done(e);
}
}, state);
});
it('switch map type to cesium if cesium viewer options are found in sessionStorage', (done) => {
const state = {
maptype: {
Expand Down
32 changes: 25 additions & 7 deletions web/client/epics/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import {
import { getSupportedFormat, getCapabilities, describeLayers, flatLayers } from '../api/WMS';
import CoordinatesUtils from '../utils/CoordinatesUtils';
import ConfigUtils from '../utils/ConfigUtils';
import {getCapabilitiesUrl, getLayerId, getLayerUrl, removeWorkspace } from '../utils/LayersUtils';
import {getCapabilitiesUrl, getLayerId, getLayerUrl, removeWorkspace} from '../utils/LayersUtils';
import { wrapStartStop } from '../observables/epics';
import {zoomToExtent} from "../actions/map";
import CSW from '../api/CSW';
Expand Down Expand Up @@ -151,16 +151,31 @@ export default (API) => ({
const state = store.getState();
const services = servicesSelectorWithBackgrounds(state);
const actions = layers
.filter((l, i) => !!services[sources[i]] || typeof sources[i] === 'object') // check for catalog name or object definition
.filter((l, i) => !!services?.[sources[i]] || typeof sources[i] === 'object') // check for catalog name or object definition
.map((l, i) => {
const layerOptions = get(options, i, searchOptionsSelector(state));
const source = sources[i];
const service = typeof source === 'object' ? source : services[source];
const format = service.type.toLowerCase();
const url = service.url;
const text = layers[i];
const layerOptionsParam = get(options, i, searchOptionsSelector(state));
// use the selected layer text as title for 3d tiles
// because currently we get only a single record for this service type
const layerOptions = format === '3dtiles'
? {
...layerOptionsParam,
title: isObject(layerOptionsParam?.title)
? {
...layerOptionsParam?.title,
"default": layerOptionsParam?.title?.default || text
}
: layerOptionsParam?.title || text
}
: layerOptionsParam;
return Rx.Observable.defer(() =>
API[format].textSearch(url, startPosition, maxRecords, text, {...layerOptions, ...service}).catch(() => ({ results: [] }))
API[format]
.textSearch(url, startPosition, maxRecords, text, { ...layerOptions, ...service, options: { service } })
.catch(() => ({ records: [] }))
).map(r => ({ ...r, format, url, text, layerOptions, service }));
});
return Rx.Observable.forkJoin(actions)
Expand Down Expand Up @@ -207,12 +222,15 @@ export default (API) => ({
// return one notification for all records that have not been found
actions = [recordsNotFound(allRecordsNotFound)];
}

const layers = results
.filter(r => isObject(r[0]))
.map(r => merge({}, r[0], r[1]));

// add all layers found to the map
actions = [
...actions,
...results.filter(r => isObject(r[0])).map(r => {
return addLayer(merge({}, r[0], r[1]));
})
...layers.map(layer => addLayer(layer))
];
return Rx.Observable.from(actions);
}
Expand Down
Loading

0 comments on commit 450b0e5

Please sign in to comment.