Skip to content
Open
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/VectorLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const createLayer = (options, map) => {
map: map,
opacity: options.opacity,
queryable: options.queryable === undefined || options.queryable,
featureFilter: vectorFeatureFilter // make filter for features if filter is existing
featureFilter: vectorFeatureFilter, // make filter for features if filter is existing
styleRules: options?.style?.body?.rules || []
});

layerToGeoStylerStyle(options)
Expand Down Expand Up @@ -69,6 +70,11 @@ Layers.registerType('vector', {
}

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({ ...newOptions, style }), 'cesium')
Expand Down
5 changes: 4 additions & 1 deletion web/client/components/map/leaflet/Layer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,15 @@ class LeafletLayer extends React.Component {
};

generateOpts = (options, position, securityToken) => {
const zoom = Math.round(this.props?.map?.getZoom() || 0);

return Object.assign({}, options, position ? {zIndex: position, srs: this.props.srs } : null, {
zoomOffset: -this.props.zoomOffset,
onError: () => {
this.props.onCreationError(options);
},
securityToken
securityToken,
resolution: this.props?.resolutions?.[zoom]
});
};

Expand Down
42 changes: 38 additions & 4 deletions web/client/components/map/leaflet/plugins/VectorLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ import {
} from '../../../../utils/VectorStyleUtils';
import { applyDefaultStyleToVectorLayer } from '../../../../utils/StyleUtils';
import { createVectorFeatureFilter } from '../../../../utils/FilterUtils';
import { DEFAULT_SCREEN_DPI, getScale } from '../../../../utils/MapUtils';
import { geoStylerScaleDenominatorFilter } from '../../../../utils/styleparser/StyleParserUtils';

/**
* Check if layer should be visible at current scale
*/
const checkScaleVisibility = (options) => {
const styleRules = options?.style?.body?.rules || [];
if (styleRules.length === 0) {
return true; // No scale rules means always visible
}

const scale = getScale(options.srs, DEFAULT_SCREEN_DPI, options?.resolution) || 0;
if (!scale || isNaN(scale)) return true;
const validRules = styleRules.filter(rule =>
geoStylerScaleDenominatorFilter(rule, Math.round(scale))
);

return validRules.length > 0;
};

const setOpacity = (layer, opacity) => {
if (layer.eachLayer) {
Expand Down Expand Up @@ -56,12 +76,16 @@ const createLayerLegacy = (options) => {
const createLayer = (options) => {
const { hideLoading } = options;
const vectorFeatureFilter = createVectorFeatureFilter(options);
const featuresToRender = options.features.filter(vectorFeatureFilter); // make filter for features if filter is existing

const isScaleVisible = checkScaleVisibility(options);
let featuresToRender = isScaleVisible
? options.features.filter(vectorFeatureFilter)
: [];
const layer = L.geoJson(featuresToRender, {
hideLoading: hideLoading
});

if (!isScaleVisible) {
layer._filteredWithScaleLimits = true;
}
getStyle(applyDefaultStyleToVectorLayer(options), 'leaflet')
.then((styleUtils) => {
styleUtils({ opacity: options.opacity, layer, features: featuresToRender })
Expand Down Expand Up @@ -97,8 +121,18 @@ const updateLayer = (layer, newOptions, oldOptions) => {
layer.remove();
return createLayer(newOptions);
}
const isScaleVisible = checkScaleVisibility(newOptions);
if (!isScaleVisible) {
if (!layer._filteredWithScaleLimits) {
layer.clearLayers();
layer._filteredWithScaleLimits = true;
}
return null;
}

if (!isEqual(oldOptions.style, newOptions.style)
|| newOptions.opacity !== oldOptions.opacity) {
|| newOptions.opacity !== oldOptions.opacity || layer._filteredWithScaleLimits) {
layer._filteredWithScaleLimits = false;
getStyle(applyDefaultStyleToVectorLayer(newOptions), 'leaflet')
.then((styleUtils) => {
styleUtils({ opacity: newOptions.opacity, layer, features: newOptions.features })
Expand Down
14 changes: 11 additions & 3 deletions web/client/plugins/styleeditor/VectorStyleEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import { classificationVector } from '../../api/StyleEditor';
import SLDService from '../../api/SLDService';
import { classifyGeoJSON, availableMethods } from '../../api/GeoJSONClassification';
import { getLayerJSONFeature } from '../../observables/wfs';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { scalesSelector } from '../../selectors/map';

const { getColors } = SLDService;

Expand Down Expand Up @@ -84,7 +87,8 @@ function VectorStyleEditor({
'Courier New',
'Brush Script MT'
],
onUpdateNode = () => {}
onUpdateNode = () => {},
scales = []
}) {

const request = capabilitiesRequest[layer?.type];
Expand Down Expand Up @@ -253,9 +257,13 @@ function VectorStyleEditor({
simple: !['vector', 'wfs'].includes(layer?.type),
supportedSymbolizerMenuOptions: ['Simple', 'Extrusion', 'Classification'],
fonts,
enableFieldExpression: ['vector', 'wfs'].includes(layer.type)
enableFieldExpression: ['vector', 'wfs'].includes(layer.type),
scales
}}
/>
);
}
export default VectorStyleEditor;
const ConnectedVectorStyleEditor = connect(createSelector([scalesSelector], (scales) => ({
scales: scales.map(scale => Math.round(scale))
})))(VectorStyleEditor);
export default ConnectedVectorStyleEditor;
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
import expect from 'expect';
import MockAdapter from "axios-mock-adapter";
import axios from "../../../libs/ajax";
import configureMockStore from 'redux-mock-store';

import defaultSettingsTabs, { getStyleTabPlugin } from '../defaultSettingsTabs';
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from "react-dom/test-utils";

import VectorStyleEditor from "../../styleeditor/VectorStyleEditor";
import { Provider } from 'react-redux';

const BASE_STYLE_TEST_DATA = {
settings: {},
Expand Down Expand Up @@ -178,6 +180,8 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
});

it("VectorStyleEditor displays an error message if the geometry is type GEOMETRY and the layer is wfs", (done) => {
const mockStore = configureMockStore()({});

const PROPS = {
...BASE_STYLE_TEST_DATA,
element: {
Expand All @@ -191,7 +195,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
mockFeatureRequestWithGeometryType("Geometry");

act(async() => {
ReactDOM.render(<VectorStyleEditor {...PROPS}/>, document.querySelector('#container'));
ReactDOM.render(<Provider store={mockStore}><VectorStyleEditor {...PROPS}/></Provider>, document.querySelector('#container'));
});
asyncValidation(()=>{
// Check if an error message is rendered
Expand All @@ -202,6 +206,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
});

it("VectorStyleEditor renders editor if the geometry is not of type GEOMETRY and the layer is wfs", (done) => {
const mockStore = configureMockStore()({});
const PROPS = {
...BASE_STYLE_TEST_DATA,
element: {
Expand All @@ -215,7 +220,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
mockFeatureRequestWithGeometryType("MultiPolygon");

act(async() => {
ReactDOM.render(<VectorStyleEditor {...PROPS}/>, document.querySelector('#container'));
ReactDOM.render(<Provider store={mockStore}><VectorStyleEditor {...PROPS}/></Provider>, document.querySelector('#container'));
});
asyncValidation(()=>{
// Check if the editor is rendered
Expand All @@ -225,6 +230,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
});

it("VectorStyleEditor renders editor if the geometry is of type GeometryCollection and the layer is wfs", (done) => {
const mockStore = configureMockStore()({});
const PROPS = {
...BASE_STYLE_TEST_DATA,
element: {
Expand All @@ -238,7 +244,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
mockFeatureRequestWithGeometryType("GeometryCollection");

act(async() => {
ReactDOM.render(<VectorStyleEditor {...PROPS}/>, document.querySelector('#container'));
ReactDOM.render(<Provider store={mockStore}><VectorStyleEditor {...PROPS}/></Provider>, document.querySelector('#container'));
});
asyncValidation(()=>{
// Check if the editor is rendered
Expand All @@ -248,6 +254,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
});

it("VectorStyleEditor renders an empty component if the geometry is not defined and the layer is wfs", (done) => {
const mockStore = configureMockStore()({});
const PROPS = {
...BASE_STYLE_TEST_DATA,
element: {
Expand All @@ -261,7 +268,7 @@ describe('TOCItemsSettings - VectorStyleEditor rendered items', () => {
mockFeatureRequestWithGeometryType("");

act(async() => {
ReactDOM.render(<VectorStyleEditor {...PROPS}/>, document.querySelector('#container'));
ReactDOM.render(<Provider store={mockStore}><VectorStyleEditor {...PROPS}/></Provider>, document.querySelector('#container'));
});
asyncValidation(()=>{
// Check if an empty container has been rendered
Expand Down
14 changes: 13 additions & 1 deletion web/client/selectors/__tests__/map-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
mapInfoAttributesSelector,
showEditableFeatureCheckboxSelector,
mapOptionsSelector,
mapEnableImageryOverlaySelector
mapEnableImageryOverlaySelector,
resolutionsSelector
} from '../map';

const center = {x: 1, y: 1};
Expand Down Expand Up @@ -78,7 +79,18 @@ describe('Test map selectors', () => {
expect(projection).toExist();
expect(projection).toBe(proj);
});
it('test resolutionsSelector from map', () => {
const resolutions = resolutionsSelector({...state, map: {...state.map, resolutions: [1000, 500, 250, 100]}});

expect(resolutions).toExist();
expect(resolutions.length).toEqual(4);
});
it('test resolutionsSelector from map if it there are no resolutions in map state like in case cesium map', () => {
const resolutions = resolutionsSelector(state);

expect(resolutions).toExist();
expect(resolutions.length).toEqual(22);
});
it('test mapSelector from map with history', () => {
const props = mapSelector({map: {present: {center}}});

Expand Down
4 changes: 2 additions & 2 deletions web/client/selectors/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import CoordinatesUtils from '../utils/CoordinatesUtils';

import { createSelector } from 'reselect';
import {get, memoize, round} from 'lodash';
import {detectIdentifyInMapPopUp} from "../utils/MapUtils";
import {detectIdentifyInMapPopUp, getResolutions} from "../utils/MapUtils";
import { isLoggedIn } from './security';

/**
Expand Down Expand Up @@ -80,7 +80,7 @@ export const configuredMinZoomSelector = state => {
export const mapLimitsSelector = state => get(mapSelector(state), "limits");
export const mapBboxSelector = state => get(mapSelector(state), "bbox");
export const minZoomSelector = state => get(mapLimitsSelector(state), "minZoom");
export const resolutionsSelector = state => get(mapSelector(state), "resolutions");
export const resolutionsSelector = state => get(mapSelector(state), "resolutions") || getResolutions();
export const currentZoomLevelSelector = state => get(mapSelector(state), "zoom");
export const currentResolutionSelector = createSelector(
resolutionsSelector,
Expand Down
50 changes: 50 additions & 0 deletions web/client/utils/MapUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as Cesium from 'cesium';

import {
pick,
Expand Down Expand Up @@ -343,6 +344,55 @@ export function getScale(projection, dpi, resolution) {
const dpu = dpi2dpu(dpi, projection);
return resolution * dpu;
}

/**
* Calculates the map scale denominator at the center of the Cesium viewer's screen.
*
* * @param {Cesium.Viewer} viewer - The Cesium Viewer instance containing the scene and camera.
* @returns {number} The map scale denominator (M in 1:M) at the screen center.
* Returns a fallback scale based on camera height if the camera
* is looking at space or the globe intersection fails.
**/
export function getMapScaleForCesium(viewer) {
const FALLBACK_EARTH_CIRCUMFERENCE_METERS = 80000000;
const cesiumDefaultProj = "EPSG:3857";
const scene = viewer.scene;
const camera = scene.camera;
const canvas = scene.canvas;

// 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);

const leftPoint = new Cesium.Cartesian2(centerX, centerY);
const rightPoint = new Cesium.Cartesian2(centerX + 1, centerY);

// 2. Convert screen pixels to Globe positions (Cartesian3)
const leftRay = camera.getPickRay(leftPoint);
const rightRay = camera.getPickRay(rightPoint);

const leftPos = scene.globe.pick(leftRay, scene);
const rightPos = scene.globe.pick(rightRay, scene);


if (!Cesium.defined(leftPos) || !Cesium.defined(rightPos)) {
console.warn('Camera is looking at space/sky');
const cameraPosition = viewer.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;
}

const leftCartographic = scene.globe.ellipsoid.cartesianToCartographic(leftPos);
const rightCartographic = scene.globe.ellipsoid.cartesianToCartographic(rightPos);

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;
}
/**
* get random coordinates within CRS extent
* @param {string} crs the code of the projection for example EPSG:4346
Expand Down
Loading
Loading