Skip to content

Commit 4bfa0c1

Browse files
Fix #12044: fix wfs layer hidden in 3D + scale arrow not rendering (#12155)
* fix #12044: fix wfs layer hidden in 3D + scale arrow not rendering - fix not hidding wfs layers with scale limits - handle showing arrow for current scale in scales DD for scaleDenominator * - Fix: Arrow UI disappears when zooming out in 3D styler view * - fix the scale limit filter to exclude max value and include min scale value * - for 3D, ehnace getMapScaleForCesium to take camera prependicular poosition into account - enhance calc. scale value in geoStylerStyleToOlParserStyleFct to match the predefined scale values list - fix FE unit test * - fix FE unit test * - add unit tests for getMapScaleForCesium and isCameraPerpendicularToSurface
1 parent eefce12 commit 4bfa0c1

File tree

7 files changed

+179
-26
lines changed

7 files changed

+179
-26
lines changed

web/client/components/map/cesium/plugins/WFSLayer.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ const createLayer = (options, map) => {
7070
id: options?.id,
7171
map: map,
7272
opacity: options.opacity,
73-
queryable: options.queryable === undefined || options.queryable
73+
queryable: options.queryable === undefined || options.queryable,
74+
styleRules: options?.style?.body?.rules || []
7475
});
7576
let loader;
7677
let loadingBbox;
@@ -187,6 +188,11 @@ Layers.registerType('wfs', {
187188
return createLayer(newOptions, map);
188189
}
189190
if (layer?.styledFeatures && !isEqual(newOptions.style, oldOptions.style)) {
191+
// update style rules here
192+
if (!isEqual(newOptions?.style?.body?.rules, oldOptions?.style?.body?.rules)) {
193+
let styleRules = newOptions?.style?.body?.rules || [];
194+
layer.styledFeatures._setStyleRules(styleRules);
195+
}
190196
layerToGeoStylerStyle(newOptions)
191197
.then((style) => {
192198
getStyle(applyDefaultStyleToVectorLayer({

web/client/plugins/styleeditor/VectorStyleEditor.jsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { classifyGeoJSON, availableMethods } from '../../api/GeoJSONClassificati
3232
import { getLayerJSONFeature } from '../../observables/wfs';
3333
import { connect } from 'react-redux';
3434
import { createSelector } from 'reselect';
35-
import { scalesSelector } from '../../selectors/map';
35+
import { currentZoomLevelSelector, scalesSelector } from '../../selectors/map';
3636

3737
const { getColors } = SLDService;
3838

@@ -88,7 +88,8 @@ function VectorStyleEditor({
8888
'Brush Script MT'
8989
],
9090
onUpdateNode = () => {},
91-
scales = []
91+
scales = [],
92+
zoom = 0
9293
}) {
9394

9495
const request = capabilitiesRequest[layer?.type];
@@ -258,12 +259,14 @@ function VectorStyleEditor({
258259
supportedSymbolizerMenuOptions: ['Simple', 'Extrusion', 'Classification'],
259260
fonts,
260261
enableFieldExpression: ['vector', 'wfs'].includes(layer.type),
261-
scales
262+
scales,
263+
zoom: Math.round(zoom) // passing this for showing arrow of current scale for ScaleDenominator
262264
}}
263265
/>
264266
);
265267
}
266-
const ConnectedVectorStyleEditor = connect(createSelector([scalesSelector], (scales) => ({
267-
scales: scales.map(scale => Math.round(scale))
268+
const ConnectedVectorStyleEditor = connect(createSelector([scalesSelector, currentZoomLevelSelector], (scales, zoom) => ({
269+
scales: scales.map(scale => Math.round(scale)),
270+
zoom
268271
})))(VectorStyleEditor);
269272
export default ConnectedVectorStyleEditor;

web/client/utils/MapUtils.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,25 @@ export function getScale(projection, dpi, resolution) {
348348
return resolution * dpu;
349349
}
350350

351+
/**
352+
* Checks if the camera is looking perpendicular (nadir) to the surface
353+
* @param {Cesium.Camera} camera - The Cesium camera
354+
* @param {Cesium.Cartesian3} position - Position on the globe (Cartesian3)
355+
* @param {Cesium.Ellipsoid} ellipsoid - The ellipsoid (usually scene.globe.ellipsoid)
356+
* @param {number} threshold - Cosine threshold (0.95 = ~18°, 0.99 = ~8°)
357+
* @returns {boolean} True if camera is approximately perpendicular
358+
*/
359+
export function isCameraPerpendicularToSurface(camera, position, ellipsoid, threshold = 0.95) {
360+
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(position);
361+
const cameraDirection = camera.direction;
362+
363+
// Dot product: -1 = exactly opposite (straight down), 0 = parallel to surface
364+
const dot = Cesium.Cartesian3.dot(cameraDirection, surfaceNormal);
365+
366+
// Check if dot product is close to -1 (camera looking straight down)
367+
return dot < -threshold;
368+
}
369+
351370
/**
352371
* Calculates the map scale denominator at the center of the Cesium viewer's screen.
353372
*
@@ -362,7 +381,7 @@ export function getMapScaleForCesium(viewer) {
362381
const scene = viewer.scene;
363382
const camera = scene.camera;
364383
const canvas = scene.canvas;
365-
384+
const ellipsoid = scene.globe.ellipsoid;
366385
// 1. Get two points at the center of the screen, 1 pixel apart horizontally
367386
const centerX = Math.floor(canvas.clientWidth / 2);
368387
const centerY = Math.floor(canvas.clientHeight / 2);
@@ -377,15 +396,17 @@ export function getMapScaleForCesium(viewer) {
377396
const leftPos = scene.globe.pick(leftRay, scene);
378397
const rightPos = scene.globe.pick(rightRay, scene);
379398

399+
// Check if camera is perpendicular (only if we have a valid position to test against)
400+
const isPerpendicular = Cesium.defined(leftPos) ? isCameraPerpendicularToSurface(camera, leftPos, ellipsoid, 0.95) : false;
380401

381-
if (!Cesium.defined(leftPos) || !Cesium.defined(rightPos)) {
382-
console.warn('Camera is looking at space/sky');
383-
const cameraPosition = viewer.camera.positionCartographic;
402+
if (!Cesium.defined(leftPos) || !Cesium.defined(rightPos) || isPerpendicular) {
403+
console.warn('Camera is looking at space/sky or is perpendicular');
404+
const cameraPosition = camera.positionCartographic;
384405
const currentZoom = Math.log2(FALLBACK_EARTH_CIRCUMFERENCE_METERS / (cameraPosition.height)) + 1;
385406
const resolutions = getResolutions();
386407
const resolution = resolutions[Math.round(currentZoom)];
387408
const scaleVal = getScale(cesiumDefaultProj, DEFAULT_SCREEN_DPI, resolution);
388-
return scaleVal;
409+
return Math.round(scaleVal ?? 0);
389410
}
390411

391412
const leftCartographic = scene.globe.ellipsoid.cartesianToCartographic(leftPos);
@@ -394,7 +415,7 @@ export function getMapScaleForCesium(viewer) {
394415
const geodesic = new Cesium.EllipsoidGeodesic(leftCartographic, rightCartographic);
395416
const resolution = geodesic.surfaceDistance; // This is meters per 1 pixel [resolution]
396417
const scaleValue = getScale(cesiumDefaultProj, DEFAULT_SCREEN_DPI, resolution);
397-
return scaleValue;
418+
return Math.round(scaleValue ?? 0);
398419
}
399420
/**
400421
* get random coordinates within CRS extent

web/client/utils/__tests__/MapUtils-test.js

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88
import expect from 'expect';
9+
import * as Cesium from 'cesium';
910

1011
import { keys, sortBy } from 'lodash';
1112

@@ -47,7 +48,9 @@ import {
4748
recursiveIsChangedWithRules,
4849
filterFieldByRules,
4950
prepareObjectEntries,
50-
parseFieldValue
51+
parseFieldValue,
52+
isCameraPerpendicularToSurface,
53+
getMapScaleForCesium
5154
} from '../MapUtils';
5255
import { VisualizationModes } from '../MapTypeUtils';
5356

@@ -2560,3 +2563,127 @@ describe('prepareObjectEntries', () => {
25602563
expect(entries).toEqual([]);
25612564
});
25622565
});
2566+
describe('test calc scale utils for cesium', () => {
2567+
it('isCameraPerpendicularToSurface - nadir (straight down) returns true', () => {
2568+
const ellipsoid = Cesium.Ellipsoid.WGS84;
2569+
const position = Cesium.Cartesian3.fromDegrees(0, 0);
2570+
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(position);
2571+
2572+
// Camera direction opposite to surface normal = looking straight down
2573+
const camera = {
2574+
direction: Cesium.Cartesian3.negate(surfaceNormal, new Cesium.Cartesian3())
2575+
};
2576+
2577+
const result = isCameraPerpendicularToSurface(camera, position, ellipsoid, 0.95);
2578+
expect(result).toBe(true);
2579+
});
2580+
2581+
it('isCameraPerpendicularToSurface - oblique angle returns false', () => {
2582+
const ellipsoid = Cesium.Ellipsoid.WGS84;
2583+
const position = Cesium.Cartesian3.fromDegrees(0, 0);
2584+
2585+
// Tilted camera direction (~45° from nadir, dot ≈ -0.7)
2586+
const camera = {
2587+
direction: new Cesium.Cartesian3(0, -0.7, -0.7)
2588+
};
2589+
2590+
const result = isCameraPerpendicularToSurface(camera, position, ellipsoid, 0.95);
2591+
expect(result).toBe(false);
2592+
});
2593+
2594+
it('isCameraPerpendicularToSurface - horizontal view returns false', () => {
2595+
const ellipsoid = Cesium.Ellipsoid.WGS84;
2596+
const position = Cesium.Cartesian3.fromDegrees(0, 0);
2597+
2598+
// Camera looking horizontally (dot = 0)
2599+
const camera = {
2600+
direction: new Cesium.Cartesian3(1, 0, 0)
2601+
};
2602+
2603+
const result = isCameraPerpendicularToSurface(camera, position, ellipsoid, 0.95);
2604+
expect(result).toBe(false);
2605+
});
2606+
2607+
it('getMapScaleForCesium - returns number when camera looks at sky', () => {
2608+
// Minimal stub: globe.pick returns undefined = looking at sky
2609+
const mockViewer = {
2610+
scene: {
2611+
canvas: { clientWidth: 800, clientHeight: 600 },
2612+
camera: {
2613+
direction: new Cesium.Cartesian3(0, 0, 1),
2614+
positionCartographic: { height: 10000 },
2615+
getPickRay: () => ({})
2616+
},
2617+
globe: {
2618+
ellipsoid: Cesium.Ellipsoid.WGS84,
2619+
pick: () => undefined
2620+
}
2621+
}
2622+
};
2623+
2624+
const result = getMapScaleForCesium(mockViewer);
2625+
2626+
expect(typeof result).toBe('number');
2627+
expect(Number.isInteger(result)).toBe(true);
2628+
expect(result).toBeGreaterThan(0);
2629+
});
2630+
2631+
it('getMapScaleForCesium - returns number with valid positions + perpendicular camera', () => {
2632+
// Minimal stub: valid positions + perpendicular camera → triggers fallback by design
2633+
const mockPos = Cesium.Cartesian3.fromDegrees(0, 0, 0);
2634+
const mockViewer = {
2635+
scene: {
2636+
canvas: { clientWidth: 800, clientHeight: 600 },
2637+
camera: {
2638+
direction: new Cesium.Cartesian3(0, 0, -1), // straight down
2639+
positionCartographic: { height: 5000 },
2640+
getPickRay: () => ({})
2641+
},
2642+
globe: {
2643+
ellipsoid: Cesium.Ellipsoid.WGS84,
2644+
pick: () => mockPos,
2645+
cartesianToCartographic: () => ({
2646+
longitude: 0,
2647+
latitude: 0,
2648+
height: 0
2649+
})
2650+
}
2651+
}
2652+
};
2653+
2654+
const result = getMapScaleForCesium(mockViewer);
2655+
2656+
expect(typeof result).toBe('number');
2657+
expect(Number.isInteger(result)).toBe(true);
2658+
});
2659+
2660+
it('getMapScaleForCesium - returns number with valid positions + oblique camera', () => {
2661+
// Minimal stub: valid positions + oblique camera → uses EllipsoidGeodesic path
2662+
const mockPos = Cesium.Cartesian3.fromDegrees(0, 0, 0);
2663+
const mockViewer = {
2664+
scene: {
2665+
canvas: { clientWidth: 800, clientHeight: 600 },
2666+
camera: {
2667+
direction: new Cesium.Cartesian3(0, -0.5, -0.8), // tilted
2668+
positionCartographic: { height: 5000 },
2669+
getPickRay: () => ({})
2670+
},
2671+
globe: {
2672+
ellipsoid: Cesium.Ellipsoid.WGS84,
2673+
pick: () => mockPos,
2674+
cartesianToCartographic: () => ({
2675+
longitude: 0,
2676+
latitude: 0,
2677+
height: 0
2678+
})
2679+
}
2680+
}
2681+
};
2682+
2683+
const result = getMapScaleForCesium(mockViewer);
2684+
2685+
expect(typeof result).toBe('number');
2686+
expect(Number.isInteger(result)).toBe(true);
2687+
});
2688+
});
2689+

web/client/utils/styleparser/OLStyleParser.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { drawIcons } from './IconUtils';
6666

6767
import isString from 'lodash/isString';
6868
import { geometryFunctionsLibrary } from './GeometryFunctionsUtils';
69+
import { DEFAULT_SCREEN_DPI, getScale } from '../MapUtils';
6970

7071
const getGeometryFunction = geometryFunctionsLibrary.openlayers({
7172
Point: OlGeomPoint,
@@ -443,14 +444,9 @@ export class OlStyleParser {
443444
this._getMap = () => map;
444445
const styles = [];
445446

446-
// calculate scale for resolution (from ol-util MapUtil)
447-
const units = map
448-
? map.getView().getProjection().getUnits()
449-
: 'm';
450-
const dpi = 25.4 / 0.28;
451-
const mpu = METERS_PER_UNIT[units];
452-
const inchesPerMeter = 39.37;
453-
const scale = resolution * mpu * inchesPerMeter * dpi;
447+
// instead of using ol0util MapUtil -> calc. scale value using getScale() to be matched with the predefined scale list values
448+
const projection = map?.getView()?.getProjection()?.getCode() || "EPSG:3857";
449+
const scale = Math.round(getScale(projection, DEFAULT_SCREEN_DPI, resolution));
454450

455451
rules.forEach((rule) => {
456452
// handling scale denominator

web/client/utils/styleparser/StyleParserUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ export const geoStylerScaleDenominatorFilter = (rule = {}, mapViewScale) => {
442442
if (rule?.scaleDenominator && mapViewScale) {
443443
const {min, max} = rule?.scaleDenominator;
444444
if ((min !== undefined && mapViewScale < min) ||
445-
(max !== undefined && mapViewScale > max)) {
445+
(max !== undefined && mapViewScale >= max)) {
446446
return false;
447447
}
448448
}

web/client/utils/styleparser/__tests__/StyleParserUtils-test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ describe("StyleParserUtils ", () => {
171171
// tests for geoStylerScaleDenominatorFilter
172172
describe('geoStylerScaleDenominatorFilter', () => {
173173

174-
it('returns true when scale is within [min, max] range', () => {
174+
it('returns true when scale is within [min, max] range -> max excluded and min included', () => {
175175
const rule = { scaleDenominator: { min: 1000, max: 10000 } };
176+
expect(geoStylerScaleDenominatorFilter(rule, 1000)).toBe(true); // min included
176177
expect(geoStylerScaleDenominatorFilter(rule, 5000)).toBe(true);
177-
expect(geoStylerScaleDenominatorFilter(rule, 1000)).toBe(true);
178-
expect(geoStylerScaleDenominatorFilter(rule, 10000)).toBe(true);
178+
expect(geoStylerScaleDenominatorFilter(rule, 10000)).toBe(false); // max is excluded
179179
});
180180

181181
it('returns false when scale is outside [min, max] range', () => {
@@ -207,8 +207,8 @@ describe("StyleParserUtils ", () => {
207207
color: "#3388FF",
208208
scaleDenominator: { min: 2500, max: 25000 }
209209
};
210-
expect(geoStylerScaleDenominatorFilter(rule, 10000)).toBe(true);
211210
expect(geoStylerScaleDenominatorFilter(rule, 1000)).toBe(false);
211+
expect(geoStylerScaleDenominatorFilter(rule, 10000)).toBe(true);
212212
expect(geoStylerScaleDenominatorFilter(rule, 50000)).toBe(false);
213213
});
214214
});

0 commit comments

Comments
 (0)