Skip to content

Commit

Permalink
[Bugfix] workaround for the issue when QGIS server timed out when req…
Browse files Browse the repository at this point in the history
…uesting the legend

If the request failed, try to get the legend graphic for each layer separately
This is a workaround for the issue when QGIS server timed out when requesting
the legend graphic for multiple layers at once (LAYER parameter with multiple values)

Related to #4521

Funded by CédéGIS https://www.cedegis.fr/
  • Loading branch information
rldhont committed Mar 7, 2025
1 parent 97e20ea commit e0e27d9
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 10 deletions.
28 changes: 26 additions & 2 deletions assets/src/modules/action/Symbology.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @license MPL-2.0
*/

import { HttpError } from '../Errors.js';
import WMS from './../WMS.js';
import {LayerTreeLayerState, LayerTreeGroupState} from './../state/LayerTree.js'

Expand Down Expand Up @@ -43,7 +44,28 @@ export async function updateLayerTreeLayersSymbology(treeLayers) {
treeLayersByName[node.name].symbology = node;
}
}
}).catch(console.error);
}).catch((error) => {
console.error(error);
// If the request failed, try to get the legend graphic for each layer separately
// This is a workaround for the issue when QGIS server timed out when requesting
// the legend graphic for multiple layers at once (LAYER parameter with multiple values)
if (treeLayers.length == 1) {
// If there is only one layer, there is no need to try to get the legend graphic
// for each layer separately
return treeLayers;
}
if (!(error instanceof HttpError) || error.statusCode != 504) {
// If the error is not a timeout, there is no need to try to get the legend graphic
// for each layer separately
return treeLayers;
}
// Try to get the legend graphic for each layer separately
Promise.all(
treeLayers.map(treeLayer => updateLayerTreeLayerSymbology(treeLayer))
).then((treeLayers) => {
return treeLayers;
});
});
return treeLayers;
}

Expand All @@ -66,7 +88,9 @@ export async function updateLayerTreeLayerSymbology(treeLayer) {
*/
export async function updateLayerTreeGroupLayersSymbology(treeGroup) {
if (!(treeGroup instanceof LayerTreeGroupState)) {
throw new TypeError('`updateLayerTreeGroupLayersSymbology` method required a LayerTreeGroupState as parameter!');
throw new TypeError(
'`updateLayerTreeGroupLayersSymbology` method required a LayerTreeGroupState as parameter!'
);
}
return updateLayerTreeLayersSymbology(treeGroup.findTreeLayers());
}
188 changes: 180 additions & 8 deletions tests/end2end/playwright/treeview.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ test.describe('Treeview', () => {
// Wait for WMS GetCapabilities promise
let getCapabilitiesWMSPromise = page.waitForRequest(/SERVICE=WMS&REQUEST=GetCapabilities/);
// Wait for WMS GetLegendGraphic promise
const getLegendGraphicPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData() != null && request.postData()?.includes('GetLegendGraphic') === true);
const getLegendGraphicPromise = page.waitForRequest(
request => request.method() === 'POST' &&
request.postData() != null &&
request.postData()?.includes('GetLegendGraphic') === true
);
await page.goto(url);
// Wait for WMS GetCapabilities
await getCapabilitiesWMSPromise;
// Wait for WMS GetLegendGraphic
let getLegendGraphicRequest = await getLegendGraphicPromise;

// Check WMS GetLegendGraphic postData
const getLegendGraphicRequestPostData = getLegendGraphicRequest.postData();
expect(getLegendGraphicRequestPostData).toContain('SERVICE=WMS')
expect(getLegendGraphicRequestPostData).toContain('REQUEST=GetLegendGraphic')
expect(getLegendGraphicRequestPostData).toContain('LAYER=sousquartiers%2Cquartiers%2Cshop_bakery_pg%2Ctramway_lines%2Cgroup_as_layer_1%2Cgroup_as_layer_2')
const searchParams = new URLSearchParams(getLegendGraphicRequest.postData() ?? '');
expect(searchParams.get('SERVICE')).toBe('WMS');
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
expect(searchParams.get('LAYER')).toBe(
'sousquartiers,quartiers,shop_bakery_pg,tramway_lines,group_as_layer_1,group_as_layer_2'
);

// Check that the map scale is the right one
await expect(page.locator('#overview-bar .ol-scale-text')).toHaveText('1 : ' + (100180).toLocaleString(locale))
Expand All @@ -40,8 +46,12 @@ test.describe('Treeview', () => {
await expect(page.getByTestId('sub-group1').getByTestId('subdistricts')).toHaveCount(1)
await expect(page.getByTestId('subdistricts')).toBeVisible()
await expect(page.getByTestId('group with space in name and shortname defined')).toHaveCount(1)
await expect(page.getByTestId('group with space in name and shortname defined').getByTestId('quartiers')).toHaveCount(1)
await expect(page.getByTestId('group with space in name and shortname defined').getByTestId('shop_bakery_pg')).toHaveCount(1)
await expect(
page.getByTestId('group with space in name and shortname defined').getByTestId('quartiers')
).toHaveCount(1)
await expect(
page.getByTestId('group with space in name and shortname defined').getByTestId('shop_bakery_pg')
).toHaveCount(1)
await expect(page.getByTestId('tramway_lines')).toHaveCount(1)
await expect(page.getByTestId('tramway_lines')).toHaveText('Tramway lines')
await expect(page.getByTestId('group-without-children')).toHaveCount(0)
Expand Down Expand Up @@ -169,7 +179,8 @@ test.describe('Treeview', () => {
});
});

test.describe('Treeview mocked with "Hide checkboxes for groups" option', () => {
test.describe('Treeview mocked', () => {

test('"Hide checkboxes for groups" option', async ({ page }) => {
await page.route('**/service/getProjectConfig*', async route => {
const response = await route.fetch();
Expand All @@ -183,4 +194,165 @@ test.describe('Treeview mocked with "Hide checkboxes for groups" option', () =>

await expect(page.locator('lizmap-treeview div.group > input')).toHaveCount(0);
});

test('Timeout on GetLegendGraphic with multi layers', async ({ page }) => {
const timedOutRequest = [];
const GetLegends = [];
await page.route('**/service*', async route => {
const request = await route.request();
if (request.method() !== 'POST') {
// GetLegendGraphic is a POST request
// Continue the request for non POST requests
await route.continue();
return;
}
const searchParams = new URLSearchParams(request.postData() ?? '');
if (searchParams.get('SERVICE') !== 'WMS' ||
!searchParams.has('REQUEST') ||
searchParams.get('REQUEST') !== 'GetLegendGraphic') {
// Continue the request for non GetLegendGraphic requests
await route.continue();
return;
}
if (!searchParams.has('LAYER')) {
// Continue the request for GetLegendGraphic without LAYER parameter
await route.continue();
return;
}
const layers = searchParams.get('LAYER')?.split(',');
if (layers?.length == 1) {
// Continue the request for GetLegendGraphic with one layer
GetLegends.push(searchParams);
await route.continue();
return;
}
timedOutRequest.push(searchParams);
// Timeout on GetLegendGraphic with multi layers
await route.fulfill({
status: 504,
contentType: 'text/plain',
body: 'Timeout',
});
});
const url = '/index.php/view/map/?repository=testsrepository&project=treeview';
// Wait for WMS GetCapabilities promise
let getCapabilitiesWMSPromise = page.waitForRequest(/SERVICE=WMS&REQUEST=GetCapabilities/);

await page.goto(url);

// Wait for WMS GetCapabilities
await getCapabilitiesWMSPromise;

let timeCount = 0;
while (GetLegends.length < 6) {
timeCount += 100;
if (timeCount > 1000) {
break;
}
await page.waitForTimeout(100);
}

await expect(GetLegends.length).toBeGreaterThanOrEqual(6);
await expect(timedOutRequest.length).toBeGreaterThanOrEqual(1);

// Check that the GetLegendGraphic requests are well formed
GetLegends.forEach((searchParams) => {
expect(searchParams.get('SERVICE')).toBe('WMS');
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
expect(searchParams.get('VERSION')).toBe('1.3.0');
expect(searchParams.get('FORMAT')).toBe('application/json');
expect(searchParams.get('LAYER')).toBeDefined();
expect(searchParams.get('LAYER')).not.toContain(',');
expect(searchParams.get('STYLES')).toBeDefined();
expect(searchParams.get('STYLES')).not.toContain(',');
});

// Check that the timed out GetLegendGraphic requests are well formed
timedOutRequest.forEach((searchParams) => {
expect(searchParams.get('SERVICE')).toBe('WMS');
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
expect(searchParams.get('VERSION')).toBe('1.3.0');
expect(searchParams.get('FORMAT')).toBe('application/json');
expect(searchParams.get('LAYER')).toBeDefined();
expect(searchParams.get('LAYER')).toContain(',');
expect(searchParams.get('STYLES')).toBeDefined();
expect(searchParams.get('STYLES')).toContain(',');
});

await page.unroute('**/service*');
});

test('Error on GetLegendGraphic', async ({ page }) => {
const abortedRequest = [];
const GetLegends = [];
await page.route('**/service*', async route => {
const request = await route.request();
if (request.method() !== 'POST') {
// GetLegendGraphic is a POST request
// Continue the request for non POST requests
await route.continue();
return;
}
const searchParams = new URLSearchParams(request.postData() ?? '');
if (searchParams.get('SERVICE') !== 'WMS' ||
!searchParams.has('REQUEST') ||
searchParams.get('REQUEST') !== 'GetLegendGraphic') {
// Continue the request for non GetLegendGraphic requests
await route.continue();
return;
}
if (!searchParams.has('LAYER')) {
// Continue the request for GetLegendGraphic without LAYER parameter
await route.continue();
return;
}
const layers = searchParams.get('LAYER')?.split(',');
if (layers?.length == 1) {
// Continue the request for GetLegendGraphic with one layer
GetLegends.push(searchParams);
await route.continue();
return;
}
abortedRequest.push(searchParams);
// Abort the request for GetLegendGraphic with multiple layers
await route.abort('failed');
});

const url = '/index.php/view/map/?repository=testsrepository&project=treeview';
// Wait for WMS GetCapabilities promise
let getCapabilitiesWMSPromise = page.waitForRequest(/SERVICE=WMS&REQUEST=GetCapabilities/);

await page.goto(url);

// Wait for WMS GetCapabilities
await getCapabilitiesWMSPromise;

// Wait for WMS GetLegendGraphic
let timeCount = 0;
while (GetLegends.length < 6) {
timeCount += 100;
if (timeCount > 200) {
break;
}
await page.waitForTimeout(100);
}

// Check if the GetLegendGraphic requests were all aborted
await expect(GetLegends.length).toBe(0);
await expect(abortedRequest.length).toBe(1);

// Check that the aborted GetLegendGraphic requests are well formed
abortedRequest.forEach((searchParams) => {
expect(searchParams.get('SERVICE')).toBe('WMS');
expect(searchParams.get('REQUEST')).toBe('GetLegendGraphic');
expect(searchParams.get('VERSION')).toBe('1.3.0');
expect(searchParams.get('FORMAT')).toBe('application/json');
expect(searchParams.get('LAYER')).toBeDefined();
expect(searchParams.get('LAYER')).toContain(',');
expect(searchParams.get('STYLES')).toBeDefined();
expect(searchParams.get('STYLES')).toContain(',');
});

await page.unroute('**/service*');
});
});

0 comments on commit e0e27d9

Please sign in to comment.