Skip to content

Commit e552541

Browse files
authored
Merge pull request #6217 from rldhont/fix-baselayer-opacity
[Bugfix] Base layer opacity is not set by sub-dock
2 parents e972b63 + b801dba commit e552541

File tree

4 files changed

+291
-14
lines changed

4 files changed

+291
-14
lines changed

assets/src/legacy/switcher-layers-actions.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ var lizLayerActionButtons = function() {
158158
html+= '<dd>';
159159
html+= '<input type="hidden" class="opacityLayer '+isBaselayer+'" value="'+aName+'">';
160160

161-
const currentOpacity = lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerOrGroupByName(aName).opacity;
161+
const currentOpacity = metadatas.isBaselayer ?
162+
lizMap.mainLizmap.state.baseLayers.getBaseLayerByName(aName).opacity :
163+
lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerOrGroupByName(aName).opacity;
162164
var opacities = lizMap.config.options.layersOpacities;
163165
if (typeof opacities === 'undefined') {
164166
opacities = [0.2, 0.4, 0.6, 0.8, 1];
@@ -480,8 +482,12 @@ var lizLayerActionButtons = function() {
480482
return false;
481483
}
482484

485+
const isBaselayer = lizMap.mainLizmap.map.getActiveBaseLayer()?.get("name") == eName;
486+
483487
// Get layer
484-
const layer = lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerOrGroupByName(eName);
488+
const layer = isBaselayer ?
489+
lizMap.mainLizmap.state.baseLayers.getBaseLayerByName(eName) :
490+
lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerOrGroupByName(eName);
485491

486492
// Set opacity
487493
if( layer ) {

assets/src/modules/map.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export default class map extends olMap {
481481
preload: Infinity,
482482
source: new Google({
483483
key: baseLayerState.key,
484-
mapType: baseLayerState.mapType,
484+
mapType: baseLayerState.googleMapType,
485485
}),
486486
});
487487
} else if (baseLayerState.type === BaseLayerTypes.Lizmap) {
@@ -750,7 +750,7 @@ export default class map extends olMap {
750750
['layer.visibility.changed', 'group.visibility.changed']
751751
);
752752

753-
rootMapGroup.addListener(
753+
baseLayersState.addListener(
754754
evt => {
755755
// conservative control since the opacity events should not be fired for single WMS layers
756756
if (this.isSingleWMSLayer(evt.name)) return;

assets/src/modules/state/BaseLayer.js

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import EventDispatcher from './../../utils/EventDispatcher.js';
10+
import { convertNumber } from './../utils/Converters.js';
1011
import { LayerConfig } from './../config/Layer.js';
1112
import { AttributionConfig } from './../config/Attribution.js'
1213
import { BaseLayerTypes, BaseLayersConfig, BaseLayerConfig, EmptyBaseLayerConfig, XyzBaseLayerConfig, BingBaseLayerConfig, GoogleBaseLayerConfig, WmtsBaseLayerConfig, WmsBaseLayerConfig } from './../config/BaseLayer.js';
@@ -28,10 +29,21 @@ export class BaseLayerState extends EventDispatcher {
2829
throw new TypeError('Base layer config and layer item state have not the same name!\n- `'+baseLayerCfg.name+'` for base layer config\n- `'+itemState.name+'` for layer item state');
2930
}
3031
super()
32+
this._opacity = 1;
3133
this._baseLayerConfig = baseLayerCfg;
3234
this._itemState = itemState;
3335
this._loadStatus = MapLayerLoadStatus.Undefined;
3436
this._singleWMSLayer = false;
37+
38+
// If item state is null, stop configuration
39+
if (itemState === null) {
40+
return;
41+
}
42+
// Dispatch opacity events
43+
itemState.addListener(
44+
this.dispatch.bind(this),
45+
itemState.mapType + '.opacity.changed'
46+
);
3547
}
3648

3749
/**
@@ -104,6 +116,59 @@ export class BaseLayerState extends EventDispatcher {
104116
return this._baseLayerConfig.attribution;
105117
}
106118

119+
/**
120+
* Base layer map type
121+
* @type {string}
122+
*/
123+
get mapType() {
124+
if (this.hasItemState) {
125+
return this._itemState.mapType;
126+
}
127+
128+
return 'layer';
129+
}
130+
131+
/**
132+
* Base layer opacity
133+
* @type {number}
134+
*/
135+
get opacity() {
136+
if (this.hasItemState) {
137+
return this._itemState.opacity;
138+
}
139+
140+
return this._opacity;
141+
}
142+
143+
/**
144+
* Set base layer opacity
145+
* @type {number}
146+
*/
147+
set opacity(val) {
148+
if (this.hasItemState) {
149+
this._itemState.opacity = val;
150+
return;
151+
}
152+
153+
const newVal = convertNumber(val);
154+
155+
if (newVal < 0 || newVal > 1) {
156+
throw new TypeError('Opacity must be in [0-1] interval!');
157+
}
158+
159+
// No changes
160+
if (this._opacity === newVal) {
161+
return;
162+
}
163+
this._opacity = newVal;
164+
165+
this.dispatch({
166+
type: this.mapType + '.opacity.changed',
167+
name: this.name,
168+
opacity: this.opacity,
169+
});
170+
}
171+
107172
/**
108173
* A Lizmap layer config is associated with this base layer
109174
* @type {boolean}
@@ -289,7 +354,7 @@ export class GoogleBaseLayerState extends BaseLayerState {
289354
* The google mapType
290355
* @type {string}
291356
*/
292-
get mapType() {
357+
get googleMapType() {
293358
return this._baseLayerConfig.mapType;
294359
}
295360
}
@@ -449,29 +514,38 @@ export class BaseLayersState extends EventDispatcher {
449514
if (blConfig.hasLayerConfig) {
450515
itemState = lgCollection.findLayerOrGroupByName(blConfig.name);
451516
}
517+
let baseLayer = null;
452518
switch(blConfig.type) {
453519
case BaseLayerTypes.Empty:
454-
this._baseLayersMap.set(blConfig.name, new EmptyBaseLayerState(blConfig, itemState));
520+
baseLayer = new EmptyBaseLayerState(blConfig, itemState);
455521
break;
456522
case BaseLayerTypes.XYZ:
457-
this._baseLayersMap.set(blConfig.name, new XyzBaseLayerState(blConfig, itemState));
523+
baseLayer = new XyzBaseLayerState(blConfig, itemState);
458524
break;
459525
case BaseLayerTypes.Bing:
460-
this._baseLayersMap.set(blConfig.name, new BingBaseLayerState(blConfig, itemState));
526+
baseLayer = new BingBaseLayerState(blConfig, itemState);
461527
break;
462528
case BaseLayerTypes.Google:
463-
this._baseLayersMap.set(blConfig.name, new GoogleBaseLayerState(blConfig, itemState));
529+
baseLayer = new GoogleBaseLayerState(blConfig, itemState);
464530
break;
465531
case BaseLayerTypes.WMTS:
466-
this._baseLayersMap.set(blConfig.name, new WmtsBaseLayerState(blConfig, itemState));
532+
baseLayer = new WmtsBaseLayerState(blConfig, itemState);
467533
break;
468534
case BaseLayerTypes.WMS:
469-
this._baseLayersMap.set(blConfig.name, new WmsBaseLayerState(blConfig, itemState));
535+
baseLayer = new WmsBaseLayerState(blConfig, itemState);
470536
break;
471537
default:
472-
this._baseLayersMap.set(blConfig.name, new BaseLayerState(blConfig, itemState));
538+
baseLayer = new BaseLayerState(blConfig, itemState);
473539
break;
474540
}
541+
if (baseLayer !== null) {
542+
this._baseLayersMap.set(blConfig.name, baseLayer);
543+
// Dispatch event
544+
baseLayer.addListener(
545+
this.dispatch.bind(this),
546+
baseLayer.mapType + '.opacity.changed'
547+
);
548+
}
475549
}
476550
this._selectedBaseLayerName = baseLayersCfg.startupBaselayerName;
477551
}

tests/end2end/playwright/sub-dock-metadata.spec.js renamed to tests/end2end/playwright/sub-dock.spec.js

Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,206 @@
11
// @ts-check
2+
import { dirname } from 'path';
3+
import * as fs from 'fs/promises'
4+
import { existsSync } from 'node:fs';
5+
import { Buffer } from 'node:buffer';
26
import { test, expect } from '@playwright/test';
3-
import { expectParametersToContain } from './globals';
7+
import { playwrightTestFile , expectParametersToContain } from './globals';
48
import { ProjectPage } from "./pages/project";
59

6-
test.describe('Sub dock', () => {
10+
// To update OSM and GeoPF tiles in the mock directory
11+
// IMPORTANT, this must not be set to `true` while committing, on GitHub. Set to `false`.
12+
const UPDATE_MOCK_FILES = false;
13+
// Source - https://stackoverflow.com/a
14+
// Posted by Martin Thomson, modified by community. See post 'Timeline' for change history
15+
// Retrieved 2025-11-13, License - CC BY-SA 4.0
16+
17+
/**
18+
* Convert a Buffer to an Uint8Array
19+
* @param {Buffer} buffer a buffer to convert to an Uint8Array
20+
* @returns {Uint8Array} the buffer converted to an Uint8Array
21+
*/
22+
function toUint8Array(buffer) {
23+
const arrayBuffer = new ArrayBuffer(buffer.length);
24+
const view = new Uint8Array(arrayBuffer);
25+
for (let i = 0; i < buffer.length; ++i) {
26+
view[i] = buffer[i];
27+
}
28+
return view;
29+
}
30+
31+
32+
test.describe('Sub dock @readonly', () => {
33+
34+
test('Test base layers opacities', async ({ page }) => {
35+
36+
await page.route('https://tile.openstreetmap.org/*/*/*.png', async (route) => {
37+
const request = await route.request();
38+
//GetTiles.push(request.url());
39+
40+
// Build path file in mock directory
41+
const pathFile = playwrightTestFile('mock', 'base_layers', 'osm', 'tiles' + (new URL(request.url()).pathname));
42+
if (UPDATE_MOCK_FILES) {
43+
// Save file in mock directory
44+
const response = await route.fetch();
45+
await fs.mkdir(dirname(pathFile), { recursive: true })
46+
const respBuff = await response.body();
47+
await fs.writeFile(pathFile, toUint8Array(respBuff), 'binary')
48+
} else if (existsSync(pathFile)) {
49+
// fulfill route's request with mock file
50+
await route.fulfill({
51+
path: pathFile
52+
})
53+
} else {
54+
// fulfill route's request with default transparent tile
55+
await route.fulfill({
56+
path: playwrightTestFile('mock', 'transparent_tile.png')
57+
})
58+
}
59+
});
60+
const clipScreenshot = {x:432, y:256, width:256, height:256};
61+
62+
const project = new ProjectPage(page, 'base_layers');
63+
await project.open();
64+
65+
// Check base layers
66+
await expect(page.locator('lizmap-base-layers select option')).toHaveCount(11);
67+
await expect(page.locator('lizmap-base-layers select')).toHaveValue('osm-mapnik');
68+
69+
// Display base layer metadata
70+
await expect(page.locator('#sub-dock')).toBeHidden();
71+
await page.locator('#get-baselayer-metadata').click();
72+
await expect(page.locator('#sub-dock')).toBeVisible();
73+
// Check sub dock metadata content
74+
await expect(page.locator('#sub-dock .sub-metadata h3 .text')).toHaveText('Information');
75+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt')).toHaveCount(4);
76+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(0)).toHaveText('Name');
77+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(1)).toHaveText('Type');
78+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(2)).toHaveText('Zoom to the layer extent');
79+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(3)).toHaveText('Opacity');
80+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd')).toHaveCount(4);
81+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(0)).toHaveText('osm-mapnik');
82+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(1)).toHaveText('Layer');
83+
await expect(page.locator('#sub-dock .btn-opacity-layer.active')).toHaveText('100');
84+
85+
// Get osm-mapnik 100 rendering
86+
let buffer = await page.screenshot({clip:clipScreenshot});
87+
const osm100ByteLength = buffer.byteLength;
88+
await expect(osm100ByteLength).toBeGreaterThan(110000); // 115892
89+
await expect(osm100ByteLength).toBeLessThan(120000) // 115892
90+
91+
// Change opacity for osm-mapnik to 60
92+
await page.locator('#sub-dock .btn-opacity-layer', { hasText: '60' }).click();
93+
await expect(page.locator('#sub-dock .btn-opacity-layer.active')).toHaveText('60');
94+
95+
// Get osm-mapnik 60 rendering
96+
buffer = await page.screenshot({clip:clipScreenshot});
97+
const osm60ByteLength = buffer.byteLength;
98+
await expect(osm60ByteLength).toBeLessThan(osm100ByteLength);
99+
await expect(osm60ByteLength).toBeLessThan(110000); // 106330
100+
101+
// Change base layer to quartiers_baselayer
102+
let getMapRequestPromise = page.waitForRequest(/REQUEST=GetMap/);
103+
await page.locator('lizmap-base-layers select').selectOption('quartiers_baselayer');
104+
await expect(page.locator('lizmap-base-layers select')).toHaveValue('quartiers_baselayer');
105+
// Wait for request and response
106+
let getMapRequest = await getMapRequestPromise;
107+
await getMapRequest.response();
108+
// Check GetMap request
109+
const getMapExpectedParameters = {
110+
'SERVICE': 'WMS',
111+
'VERSION': '1.3.0',
112+
'REQUEST': 'GetMap',
113+
'FORMAT': /^image\/png/,
114+
'TRANSPARENT': /\b(\w*^true$\w*)\b/gmi,
115+
'LAYERS': 'quartiers_baselayer',
116+
'CRS': 'EPSG:3857',
117+
'STYLES': '',
118+
'WIDTH': '958',
119+
'HEIGHT': '633',
120+
'BBOX': /412967.3\d+,5393197.8\d+,449580.6\d+,5417390.1\d+/,
121+
}
122+
await expectParametersToContain('GetMap', getMapRequest.url(), getMapExpectedParameters);
123+
// Check sub dock metadata content
124+
await expect(page.locator('#sub-dock .sub-metadata h3 .text')).toHaveText('Information');
125+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt')).toHaveCount(4);
126+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(0)).toHaveText('Name');
127+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(1)).toHaveText('Type');
128+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(2)).toHaveText('Zoom to the layer extent');
129+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(3)).toHaveText('Opacity');
130+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd')).toHaveCount(4);
131+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(0)).toHaveText('quartiers_baselayer');
132+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(1)).toHaveText('Layer');
133+
await expect(page.locator('#sub-dock .btn-opacity-layer.active')).toHaveText('100');
134+
135+
// Get quartiers_baselayer 20 rendering
136+
buffer = await page.screenshot({clip:clipScreenshot});
137+
const quartiers100ByteLength = buffer.byteLength;
138+
await expect(quartiers100ByteLength).toBeGreaterThan(15000); // 18024
139+
await expect(quartiers100ByteLength).toBeLessThan(20000) // 18024
140+
141+
// Change opacity for quartiers_baselayer to 20
142+
await page.locator('#sub-dock .btn-opacity-layer', { hasText: '20' }).click();
143+
await expect(page.locator('#sub-dock .btn-opacity-layer.active')).toHaveText('20');
144+
145+
// Get quartiers_baselayer 20 rendering
146+
buffer = await page.screenshot({clip:clipScreenshot});
147+
const quartiers20ByteLength = buffer.byteLength;
148+
await expect(quartiers20ByteLength).toBeLessThan(quartiers100ByteLength);
149+
await expect(quartiers20ByteLength).toBeLessThan(15000); // 106330
150+
151+
// Back to osm-mapnik
152+
await page.locator('lizmap-base-layers select').selectOption('osm-mapnik');
153+
await expect(page.locator('lizmap-base-layers select')).toHaveValue('osm-mapnik');
154+
// Check sub dock metadata content
155+
await expect(page.locator('#sub-dock .sub-metadata h3 .text')).toHaveText('Information');
156+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt')).toHaveCount(4);
157+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(0)).toHaveText('Name');
158+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(1)).toHaveText('Type');
159+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(2)).toHaveText('Zoom to the layer extent');
160+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(3)).toHaveText('Opacity');
161+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd')).toHaveCount(4);
162+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(0)).toHaveText('osm-mapnik');
163+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(1)).toHaveText('Layer');
164+
await expect(page.locator('#sub-dock .btn-opacity-layer.active')).toHaveText('60');
165+
166+
// Check osm-mapnik 60 buffer
167+
buffer = await page.screenshot({clip:clipScreenshot});
168+
await expect(buffer.byteLength).toBeLessThan(osm100ByteLength);
169+
await expect(buffer.byteLength).toBe(osm60ByteLength);
170+
171+
// Close sub-dock
172+
await expect(page.locator('#hide-sub-dock')).toBeVisible();
173+
await page.locator('#hide-sub-dock').click();
174+
await expect(page.locator('#sub-dock')).toBeHidden();
175+
176+
// Back to quartiers_baselayer
177+
await page.locator('lizmap-base-layers select').selectOption('quartiers_baselayer');
178+
await expect(page.locator('lizmap-base-layers select')).toHaveValue('quartiers_baselayer');
179+
180+
// Display base layer metadata
181+
await expect(page.locator('#sub-dock')).toBeHidden();
182+
await page.locator('#get-baselayer-metadata').click();
183+
await expect(page.locator('#sub-dock')).toBeVisible();
184+
// Check sub dock metadata content
185+
await expect(page.locator('#sub-dock .sub-metadata h3 .text')).toHaveText('Information');
186+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt')).toHaveCount(4);
187+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(0)).toHaveText('Name');
188+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(1)).toHaveText('Type');
189+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(2)).toHaveText('Zoom to the layer extent');
190+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dt').nth(3)).toHaveText('Opacity');
191+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd')).toHaveCount(4);
192+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(0)).toHaveText('quartiers_baselayer');
193+
await expect(page.locator('#sub-dock .sub-metadata .menu-content dd').nth(1)).toHaveText('Layer');
194+
await expect(page.locator('#sub-dock .btn-opacity-layer.active')).toHaveText('20');
195+
196+
// Check quartiers_baselayer 20 buffer
197+
buffer = await page.screenshot({clip:clipScreenshot});
198+
await expect(buffer.byteLength).toBeLessThan(quartiers100ByteLength);
199+
await expect(buffer.byteLength).toBe(quartiers20ByteLength);
200+
201+
// Remove listen to osm tiles
202+
await page.unroute('https://tile.openstreetmap.org/*/*/*.png');
203+
});
7204

8205
test('Metadata layer in attribute table project', async ({ page }) => {
9206
const project = new ProjectPage(page, 'attribute_table');

0 commit comments

Comments
 (0)