diff --git a/docs/developer-guide/vector-style.md b/docs/developer-guide/vector-style.md index 9972c347ca..339a5442a6 100644 --- a/docs/developer-guide/vector-style.md +++ b/docs/developer-guide/vector-style.md @@ -210,6 +210,8 @@ The `symbolizer` could be of following `kinds`: | `opacity` | color opacity | | x | | `msHeightReference` | reference to compute the distance of the point geometry, one of **none**, **ground** or **clamp** | | x | | `msHeight` | height of the point, the original geometry is applied if undefined | | x | +| `msTranslateX` | move the model on the x axis with a value in meters (west negative value, east positive value) | | x | +| `msTranslateY` | move the model on the y axis with a value in meters (south negative value, north positive value) | | x | | `msLeaderLineColor` | color of the leading line connecting the point to the terrain | | x | | `msLeaderLineOpacity` | opacity of the leading line connecting the point to the terrain | | x | | `msLeaderLineWidth` | width of the leading line connecting the point to the terrain | | x | diff --git a/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js b/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js index eb3756c9f8..0ede08c984 100644 --- a/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js +++ b/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js @@ -526,6 +526,8 @@ describe('VisualStyleEditor', () => { 'styleeditor.color', 'styleeditor.heightReferenceFromGround', 'styleeditor.height', + 'styleeditor.msTranslateX', + 'styleeditor.msTranslateY', 'styleeditor.leaderLineColor', 'styleeditor.leaderLineWidth' ]); diff --git a/web/client/components/styleeditor/config/blocks.js b/web/client/components/styleeditor/config/blocks.js index 0b63053df8..30af9c81ee 100644 --- a/web/client/components/styleeditor/config/blocks.js +++ b/web/client/components/styleeditor/config/blocks.js @@ -136,7 +136,7 @@ const lineGeometryTransformation = () => ({ }) }); -const heightPoint3dOptions = ({ isDisabled }) => ({ +const heightPoint3dOptions = ({ isDisabled, enableTranslation }) => ({ msHeightReference: property.msHeightReference({ label: "styleeditor.heightReferenceFromGround", isDisabled @@ -148,6 +148,22 @@ const heightPoint3dOptions = ({ isDisabled }) => ({ placeholderId: 'styleeditor.pointHeight', isDisabled: (value, properties) => isDisabled() || properties?.msHeightReference === 'clamp' }), + ...(enableTranslation && { + msTranslateX: property.number({ + key: 'msTranslateX', + label: 'styleeditor.msTranslateX', + uom: 'm', + fallbackValue: 0, + isDisabled + }), + msTranslateY: property.number({ + key: 'msTranslateY', + label: 'styleeditor.msTranslateY', + uom: 'm', + fallbackValue: 0, + isDisabled + }) + }), msLeaderLineColor: property.color({ key: 'msLeaderLineColor', opacityKey: 'msLeaderLineOpacity', @@ -556,7 +572,8 @@ const getBlocks = ({ isDisabled: () => !enable3dStyleOptions }), ...heightPoint3dOptions({ - isDisabled: () => !enable3dStyleOptions + isDisabled: () => !enable3dStyleOptions, + enableTranslation: true }), ...(!shouldHideVectorStyleOptions && pointGeometryTransformation({})) }, diff --git a/web/client/components/styleeditor/config/property.js b/web/client/components/styleeditor/config/property.js index 28e6c33329..42a4633a93 100644 --- a/web/client/components/styleeditor/config/property.js +++ b/web/client/components/styleeditor/config/property.js @@ -363,8 +363,7 @@ const property = { }, getValue: (value) => { return { - [key]: value, - ...(value === 'clamp' && { msHeight: undefined }) + [key]: value }; }, isDisabled diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 9ea776526c..c590a91a88 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -2630,7 +2630,9 @@ "offset": "Offset", "propertyValue": "Eigenschaftswert", "colorPropertyInfoMessage": "Der Farbeigenschaftswert muss eine hexadezimale Zeichenfolge sein. Beispiel: \"#ffffff\"", - "pointCloudSizeInfo": "Der Punktwolkenradius wird nur angewendet, wenn die Dämpfungsoptionen deaktiviert sind. Die Dämpfungsoption hat Vorrang vor dieser Eigenschaft." + "pointCloudSizeInfo": "Der Punktwolkenradius wird nur angewendet, wenn die Dämpfungsoptionen deaktiviert sind. Die Dämpfungsoption hat Vorrang vor dieser Eigenschaft.", + "msTranslateX": "Versatz x", + "msTranslateY": "Versatz y" }, "playback": { "settings": { diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 77eaa51fe8..c88ddb0f6d 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -2602,7 +2602,9 @@ "offset": "Offset", "propertyValue": "Property value", "colorPropertyInfoMessage": "The color property value must be an hexadecimal string. Example: \"#ffffff\"", - "pointCloudSizeInfo": "The point cloud radius is applied only when the attenuation options is disabled. The attenuation option takes precedence over this property." + "pointCloudSizeInfo": "The point cloud radius is applied only when the attenuation options is disabled. The attenuation option takes precedence over this property.", + "msTranslateX": "Translate x", + "msTranslateY": "Translate y" }, "playback": { "settings": { diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 5399a20dae..fd0d046b9c 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -2592,7 +2592,9 @@ "offset": "Desplazamiento", "propertyValue": "Valor de la propiedad", "colorPropertyInfoMessage": "El valor de la propiedad de color debe ser una cadena hexadecimal. Ejemplo: \"#ffffff\"", - "pointCloudSizeInfo": "El radio de la nube de puntos se aplica sólo cuando las opciones de atenuación están deshabilitadas. La opción de atenuación tiene prioridad sobre esta propiedad." + "pointCloudSizeInfo": "El radio de la nube de puntos se aplica sólo cuando las opciones de atenuación están deshabilitadas. La opción de atenuación tiene prioridad sobre esta propiedad.", + "msTranslateX": "Desplazamiento x", + "msTranslateY": "Desplazamiento y" }, "playback": { "settings": { diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 5dbd33e7da..62d240912d 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -2592,7 +2592,9 @@ "offset": "Décalage", "propertyValue": "Valeur de la propriété", "colorPropertyInfoMessage": "La valeur de la propriété de couleur doit être une chaîne hexadécimale. Exemple : \"#ffffff\"", - "pointCloudSizeInfo": "Le rayon du nuage de points est appliqué uniquement lorsque les options d'atténuation sont désactivées. L'option d'atténuation est prioritaire sur cette propriété." + "pointCloudSizeInfo": "Le rayon du nuage de points est appliqué uniquement lorsque les options d'atténuation sont désactivées. L'option d'atténuation est prioritaire sur cette propriété.", + "msTranslateX": "Décalage x", + "msTranslateY": "Décalage y" }, "playback": { "settings": { diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index d700ff93ce..1b495958c7 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -2593,7 +2593,9 @@ "offset": "Offset", "propertyValue": "Valore della proprietà", "colorPropertyInfoMessage": "Il valore della proprietà colore deve essere una stringa esadecimale. Esempio: \"#ffffff\"", - "pointCloudSizeInfo": "Il raggio della nuvola di punti viene applicato solo quando le opzioni di attenuazione sono disabilitate. L'opzione di attenuazione ha la precedenza su questa proprietà." + "pointCloudSizeInfo": "Il raggio della nuvola di punti viene applicato solo quando le opzioni di attenuazione sono disabilitate. L'opzione di attenuazione ha la precedenza su questa proprietà.", + "msTranslateX": "Offset x", + "msTranslateY": "Offset y" }, "playback": { "settings": { diff --git a/web/client/utils/styleparser/CesiumStyleParser.js b/web/client/utils/styleparser/CesiumStyleParser.js index a2436d1512..e11b47fab5 100644 --- a/web/client/utils/styleparser/CesiumStyleParser.js +++ b/web/client/utils/styleparser/CesiumStyleParser.js @@ -159,6 +159,19 @@ function createLeaderLineCanvas({ return canvas; } +const translatePoint = (cartesian, symbolizer) => { + const { msTranslateX, msTranslateY } = symbolizer || {}; + const x = getNumberAttributeValue(msTranslateX); + const y = getNumberAttributeValue(msTranslateY); + return (x || y) + ? Cesium.Matrix4.multiplyByPoint( + Cesium.Transforms.eastNorthUpToFixedFrame(cartesian), + new Cesium.Cartesian3(x || 0, y || 0, 0), + new Cesium.Cartesian3() + ) + : cartesian; +}; + function addLeaderLineGraphic({ map, symbolizer, @@ -172,7 +185,9 @@ function addLeaderLineGraphic({ 'msLeaderLineWidth', 'msHeight', 'msHeightReference', - 'offset' + 'offset', + 'msTranslateX', + 'msTranslateY' ]; const shouldNotUpdateLeaderLine = entity._msSymbolizer && !isGlobalOpacityChanged(entity, globalOpacity) @@ -195,15 +210,39 @@ function addLeaderLineGraphic({ } const cartographic = Cesium.Cartographic.fromCartesian(entity.position.getValue(Cesium.JulianDate.now())); + const originalCartographic = Cesium.Cartographic.fromCartesian(entity._msPosition); const heightReference = symbolizer.msHeightReference; return ( ( symbolizer?.msHeight !== entity._msSymbolizer?.msHeight || symbolizer?.msHeightReference !== entity._msSymbolizer?.msHeightReference + || symbolizer?.msTranslateX !== entity._msSymbolizer?.msTranslateX + || symbolizer?.msTranslateY !== entity._msSymbolizer?.msTranslateY || !entity.polyline ) - ? getLeaderLinePositions({ map, cartographic, heightReference, sampleTerrain }) - .then((positions) => new Cesium.PolylineGraphics({ positions })) + ? getLeaderLinePositions({ + map, + // we create a cartographic that include: + // the original longitude and latitude + // and the modified height + // later we can translate the coordinate connected to the entity + cartographic: new Cesium.Cartographic( + originalCartographic.longitude, + originalCartographic.latitude, + cartographic.height), + heightReference, + sampleTerrain + }) + .then((positions) => { + return new Cesium.PolylineGraphics({ + positions: [ + // original position + positions[0], + // apply translation to the coordinate connected to the entity + translatePoint(positions[1], symbolizer) + ] + }); + }) : Promise.resolve(entity.polyline) ) .then((polyline) => { @@ -244,13 +283,14 @@ function modifyPointHeight({ entity, symbolizer }) { const height = getNumberAttributeValue(symbolizer.msHeight); if (height === null) { - entity.position.setValue(entity._msPosition); + entity.position.setValue(translatePoint(entity._msPosition, symbolizer)); return; } const cartographic = Cesium.Cartographic.fromCartesian(entity._msPosition); cartographic.height = height; - entity.position.setValue(Cesium.Cartographic.toCartesian(cartographic)); + const cartesian = Cesium.Cartographic.toCartesian(cartographic); + entity.position.setValue(translatePoint(cartesian, symbolizer)); return; } @@ -742,9 +782,15 @@ function getStyleFuncFromRules({ : getGeometryFunction({ msGeometry: { name: 'centerPoint' }, ...symbolizer }); if (geometryFunction) { const additionalEntity = entity.entityCollection.add({ - position: entity.position - ? entity.position.getValue(Cesium.JulianDate.now()).clone() - : new Cesium.Cartesian3(0, 0, 0) + // use the stored position when available + position: entity._msPosition + ? entity._msPosition + // if a point geometry we can access de initial value + : entity.position + ? entity.position.getValue(Cesium.JulianDate.now()).clone() + // for other computed point we use the geometry function + // so we can apply the origin cartesian + : new Cesium.Cartesian3(0, 0, 0) }); additionalEntity._msStoredCoordinates = entity._msStoredCoordinates; additionalEntity._msAdditional = true; diff --git a/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js b/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js index 9f93c56bfe..de2359ea28 100644 --- a/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js +++ b/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js @@ -811,5 +811,76 @@ describe('CesiumStyleParser', () => { }); }); }); + it('should write a style function with model symbolizer with x/y translation', (done) => { + + const translateDelta = 100; + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + kind: 'Model', + model: '/path/to/file.glb', + scale: 1, + heading: 0, + roll: 0, + pitch: 0, + color: '#ffffff', + opacity: 0.5, + msHeightReference: 'relative', + height: 10, + msTranslateX: translateDelta, + msTranslateY: translateDelta, + msLeaderLineColor: '#ff0000', + msLeaderLineOpacity: 0.5, + msLeaderLineWidth: 2 + } + ] + } + ] + }; + + const lng = 7; + const lat = 41; + + parser.writeStyle(style) + .then((styleFunc) => { + Cesium.GeoJsonDataSource.load({ + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [lng, lat] + } + }).then((dataSource) => { + const entities = dataSource?.entities?.values; + return styleFunc({ entities }) + .then(() => { + const expectedTranslatedDistance = Math.round(Math.sqrt(2) * translateDelta); + const distancePosition = Math.round(Cesium.Cartesian3.distance( + entities[0]._msPosition, + entities[0].position.getValue(Cesium.JulianDate.now()) + )); + expect(distancePosition).toBe(expectedTranslatedDistance); + const leaderLinePositions = entities[0].polyline.positions.getValue(); + const distanceLeaderLineA = Math.round(Cesium.Cartesian3.distance( + entities[0]._msPosition, + leaderLinePositions[0] + )); + expect(distanceLeaderLineA).toBe(0); + const distanceLeaderLineB = Math.round(Cesium.Cartesian3.distance( + entities[0]._msPosition, + leaderLinePositions[1] + )); + expect(distanceLeaderLineB).toBe(expectedTranslatedDistance); + done(); + }) + .catch(done); + }); + }); + }); }); });