Skip to content
Draft
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
8 changes: 7 additions & 1 deletion web/client/components/map/cesium/plugins/WFSLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const createLayer = (options, map) => {
id: options?.id,
map: map,
opacity: options.opacity,
queryable: options.queryable === undefined || options.queryable
queryable: options.queryable === undefined || options.queryable,
styleRules: options?.style?.body?.rules || []
});
let loader;
let loadingBbox;
Expand Down Expand Up @@ -187,6 +188,11 @@ Layers.registerType('wfs', {
return createLayer(newOptions, map);
}
if (layer?.styledFeatures && !isEqual(newOptions.style, oldOptions.style)) {
// update style rules here
if (!isEqual(newOptions?.style?.body?.rules, oldOptions?.style?.body?.rules)) {
let styleRules = newOptions?.style?.body?.rules || [];
layer.styledFeatures._setStyleRules(styleRules);
}
layerToGeoStylerStyle(newOptions)
.then((style) => {
getStyle(applyDefaultStyleToVectorLayer({
Expand Down
13 changes: 8 additions & 5 deletions web/client/plugins/styleeditor/VectorStyleEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { classifyGeoJSON, availableMethods } from '../../api/GeoJSONClassificati
import { getLayerJSONFeature } from '../../observables/wfs';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { scalesSelector } from '../../selectors/map';
import { currentZoomLevelSelector, scalesSelector } from '../../selectors/map';

const { getColors } = SLDService;

Expand Down Expand Up @@ -88,7 +88,8 @@ function VectorStyleEditor({
'Brush Script MT'
],
onUpdateNode = () => {},
scales = []
scales = [],
zoom = 0
}) {

const request = capabilitiesRequest[layer?.type];
Expand Down Expand Up @@ -258,12 +259,14 @@ function VectorStyleEditor({
supportedSymbolizerMenuOptions: ['Simple', 'Extrusion', 'Classification'],
fonts,
enableFieldExpression: ['vector', 'wfs'].includes(layer.type),
scales
scales,
zoom: Math.round(zoom) // passing this for showing arrow of current scale for ScaleDenominator
}}
/>
);
}
const ConnectedVectorStyleEditor = connect(createSelector([scalesSelector], (scales) => ({
scales: scales.map(scale => Math.round(scale))
const ConnectedVectorStyleEditor = connect(createSelector([scalesSelector, currentZoomLevelSelector], (scales, zoom) => ({
scales: scales.map(scale => Math.round(scale)),
zoom
})))(VectorStyleEditor);
export default ConnectedVectorStyleEditor;
33 changes: 27 additions & 6 deletions web/client/utils/MapUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,25 @@ export function getScale(projection, dpi, resolution) {
return resolution * dpu;
}

/**
* Checks if the camera is looking perpendicular (nadir) to the surface
* @param {Cesium.Camera} camera - The Cesium camera
* @param {Cesium.Cartesian3} position - Position on the globe (Cartesian3)
* @param {Cesium.Ellipsoid} ellipsoid - The ellipsoid (usually scene.globe.ellipsoid)
* @param {number} threshold - Cosine threshold (0.95 = ~18°, 0.99 = ~8°)
* @returns {boolean} True if camera is approximately perpendicular
*/
export function isCameraPerpendicularToSurface(camera, position, ellipsoid, threshold = 0.95) {
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(position);
const cameraDirection = camera.direction;

// Dot product: -1 = exactly opposite (straight down), 0 = parallel to surface
const dot = Cesium.Cartesian3.dot(cameraDirection, surfaceNormal);

// Check if dot product is close to -1 (camera looking straight down)
return dot < -threshold;
}

/**
* Calculates the map scale denominator at the center of the Cesium viewer's screen.
*
Expand All @@ -362,7 +381,7 @@ export function getMapScaleForCesium(viewer) {
const scene = viewer.scene;
const camera = scene.camera;
const canvas = scene.canvas;

const ellipsoid = scene.globe.ellipsoid;
// 1. Get two points at the center of the screen, 1 pixel apart horizontally
const centerX = Math.floor(canvas.clientWidth / 2);
const centerY = Math.floor(canvas.clientHeight / 2);
Expand All @@ -377,15 +396,17 @@ export function getMapScaleForCesium(viewer) {
const leftPos = scene.globe.pick(leftRay, scene);
const rightPos = scene.globe.pick(rightRay, scene);

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

if (!Cesium.defined(leftPos) || !Cesium.defined(rightPos)) {
console.warn('Camera is looking at space/sky');
const cameraPosition = viewer.camera.positionCartographic;
if (!Cesium.defined(leftPos) || !Cesium.defined(rightPos) || isPerpendicular) {
console.warn('Camera is looking at space/sky or is perpendicular');
const cameraPosition = camera.positionCartographic;
const currentZoom = Math.log2(FALLBACK_EARTH_CIRCUMFERENCE_METERS / (cameraPosition.height)) + 1;
const resolutions = getResolutions();
const resolution = resolutions[Math.round(currentZoom)];
const scaleVal = getScale(cesiumDefaultProj, DEFAULT_SCREEN_DPI, resolution);
return scaleVal;
return Math.round(scaleVal ?? 0);
}

const leftCartographic = scene.globe.ellipsoid.cartesianToCartographic(leftPos);
Expand All @@ -394,7 +415,7 @@ export function getMapScaleForCesium(viewer) {
const geodesic = new Cesium.EllipsoidGeodesic(leftCartographic, rightCartographic);
const resolution = geodesic.surfaceDistance; // This is meters per 1 pixel [resolution]
const scaleValue = getScale(cesiumDefaultProj, DEFAULT_SCREEN_DPI, resolution);
return scaleValue;
return Math.round(scaleValue ?? 0);
}
/**
* get random coordinates within CRS extent
Expand Down
129 changes: 128 additions & 1 deletion web/client/utils/__tests__/MapUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
import expect from 'expect';
import * as Cesium from 'cesium';

import { keys, sortBy } from 'lodash';

Expand Down Expand Up @@ -47,7 +48,9 @@ import {
recursiveIsChangedWithRules,
filterFieldByRules,
prepareObjectEntries,
parseFieldValue
parseFieldValue,
isCameraPerpendicularToSurface,
getMapScaleForCesium
} from '../MapUtils';
import { VisualizationModes } from '../MapTypeUtils';

Expand Down Expand Up @@ -2560,3 +2563,127 @@ describe('prepareObjectEntries', () => {
expect(entries).toEqual([]);
});
});
describe('test calc scale utils for cesium', () => {
it('isCameraPerpendicularToSurface - nadir (straight down) returns true', () => {
const ellipsoid = Cesium.Ellipsoid.WGS84;
const position = Cesium.Cartesian3.fromDegrees(0, 0);
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(position);

// Camera direction opposite to surface normal = looking straight down
const camera = {
direction: Cesium.Cartesian3.negate(surfaceNormal, new Cesium.Cartesian3())
};

const result = isCameraPerpendicularToSurface(camera, position, ellipsoid, 0.95);
expect(result).toBe(true);
});

it('isCameraPerpendicularToSurface - oblique angle returns false', () => {
const ellipsoid = Cesium.Ellipsoid.WGS84;
const position = Cesium.Cartesian3.fromDegrees(0, 0);

// Tilted camera direction (~45° from nadir, dot ≈ -0.7)
const camera = {
direction: new Cesium.Cartesian3(0, -0.7, -0.7)
};

const result = isCameraPerpendicularToSurface(camera, position, ellipsoid, 0.95);
expect(result).toBe(false);
});

it('isCameraPerpendicularToSurface - horizontal view returns false', () => {
const ellipsoid = Cesium.Ellipsoid.WGS84;
const position = Cesium.Cartesian3.fromDegrees(0, 0);

// Camera looking horizontally (dot = 0)
const camera = {
direction: new Cesium.Cartesian3(1, 0, 0)
};

const result = isCameraPerpendicularToSurface(camera, position, ellipsoid, 0.95);
expect(result).toBe(false);
});

it('getMapScaleForCesium - returns number when camera looks at sky', () => {
// Minimal stub: globe.pick returns undefined = looking at sky
const mockViewer = {
scene: {
canvas: { clientWidth: 800, clientHeight: 600 },
camera: {
direction: new Cesium.Cartesian3(0, 0, 1),
positionCartographic: { height: 10000 },
getPickRay: () => ({})
},
globe: {
ellipsoid: Cesium.Ellipsoid.WGS84,
pick: () => undefined
}
}
};

const result = getMapScaleForCesium(mockViewer);

expect(typeof result).toBe('number');
expect(Number.isInteger(result)).toBe(true);
expect(result).toBeGreaterThan(0);
});

it('getMapScaleForCesium - returns number with valid positions + perpendicular camera', () => {
// Minimal stub: valid positions + perpendicular camera → triggers fallback by design
const mockPos = Cesium.Cartesian3.fromDegrees(0, 0, 0);
const mockViewer = {
scene: {
canvas: { clientWidth: 800, clientHeight: 600 },
camera: {
direction: new Cesium.Cartesian3(0, 0, -1), // straight down
positionCartographic: { height: 5000 },
getPickRay: () => ({})
},
globe: {
ellipsoid: Cesium.Ellipsoid.WGS84,
pick: () => mockPos,
cartesianToCartographic: () => ({
longitude: 0,
latitude: 0,
height: 0
})
}
}
};

const result = getMapScaleForCesium(mockViewer);

expect(typeof result).toBe('number');
expect(Number.isInteger(result)).toBe(true);
});

it('getMapScaleForCesium - returns number with valid positions + oblique camera', () => {
// Minimal stub: valid positions + oblique camera → uses EllipsoidGeodesic path
const mockPos = Cesium.Cartesian3.fromDegrees(0, 0, 0);
const mockViewer = {
scene: {
canvas: { clientWidth: 800, clientHeight: 600 },
camera: {
direction: new Cesium.Cartesian3(0, -0.5, -0.8), // tilted
positionCartographic: { height: 5000 },
getPickRay: () => ({})
},
globe: {
ellipsoid: Cesium.Ellipsoid.WGS84,
pick: () => mockPos,
cartesianToCartographic: () => ({
longitude: 0,
latitude: 0,
height: 0
})
}
}
};

const result = getMapScaleForCesium(mockViewer);

expect(typeof result).toBe('number');
expect(Number.isInteger(result)).toBe(true);
});
});

12 changes: 4 additions & 8 deletions web/client/utils/styleparser/OLStyleParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { drawIcons } from './IconUtils';

import isString from 'lodash/isString';
import { geometryFunctionsLibrary } from './GeometryFunctionsUtils';
import { DEFAULT_SCREEN_DPI, getScale } from '../MapUtils';

const getGeometryFunction = geometryFunctionsLibrary.openlayers({
Point: OlGeomPoint,
Expand Down Expand Up @@ -443,14 +444,9 @@ export class OlStyleParser {
this._getMap = () => map;
const styles = [];

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

rules.forEach((rule) => {
// handling scale denominator
Expand Down
2 changes: 1 addition & 1 deletion web/client/utils/styleparser/StyleParserUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export const geoStylerScaleDenominatorFilter = (rule = {}, mapViewScale) => {
if (rule?.scaleDenominator && mapViewScale) {
const {min, max} = rule?.scaleDenominator;
if ((min !== undefined && mapViewScale < min) ||
(max !== undefined && mapViewScale > max)) {
(max !== undefined && mapViewScale >= max)) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,11 @@ describe("StyleParserUtils ", () => {
// tests for geoStylerScaleDenominatorFilter
describe('geoStylerScaleDenominatorFilter', () => {

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

it('returns false when scale is outside [min, max] range', () => {
Expand Down Expand Up @@ -207,8 +207,8 @@ describe("StyleParserUtils ", () => {
color: "#3388FF",
scaleDenominator: { min: 2500, max: 25000 }
};
expect(geoStylerScaleDenominatorFilter(rule, 10000)).toBe(true);
expect(geoStylerScaleDenominatorFilter(rule, 1000)).toBe(false);
expect(geoStylerScaleDenominatorFilter(rule, 10000)).toBe(true);
expect(geoStylerScaleDenominatorFilter(rule, 50000)).toBe(false);
});
});
Expand Down
Loading