From 4136a57c5a596d0aecbaea688b75b46d4395934e Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 12 Mar 2026 16:55:59 +0530 Subject: [PATCH 1/2] #12079: 3D - ExtrusionOutline for extruded features --- .../components/styleeditor/config/blocks.js | 18 +- web/client/translations/data.ca-ES.json | 2 + web/client/translations/data.da-DK.json | 2 + web/client/translations/data.de-DE.json | 2 + web/client/translations/data.en-US.json | 2 + web/client/translations/data.es-ES.json | 2 + web/client/translations/data.fr-FR.json | 2 + web/client/translations/data.it-IT.json | 2 + web/client/translations/data.nl-NL.json | 2 + web/client/translations/data.pt-BR.json | 2 + web/client/translations/data.sv-SE.json | 2 + .../utils/styleparser/CesiumStyleParser.js | 250 ++++++++++++-- .../__tests__/CesiumStyleParser-test.js | 319 ++++++++++++++++++ 13 files changed, 584 insertions(+), 23 deletions(-) diff --git a/web/client/components/styleeditor/config/blocks.js b/web/client/components/styleeditor/config/blocks.js index 2e6608e8f86..964f8daeafe 100644 --- a/web/client/components/styleeditor/config/blocks.js +++ b/web/client/components/styleeditor/config/blocks.js @@ -266,6 +266,18 @@ const line3dExtrusion = ({ isDisabled }) => ({ ]; }, isDisabled + }), + msExtrusionOutlineColor: property.color({ + key: 'msExtrusionOutlineColor', + opacityKey: 'msExtrusionOutlineOpacity', + label: 'styleeditor.msExtrusionOutlineColor', + stroke: true, + isDisabled + }), + msExtrusionOutlineWidth: property.width({ + key: 'msExtrusionOutlineWidth', + label: 'styleeditor.msExtrusionOutlineWidth', + isDisabled: (value, properties) => isDisabled(value, properties) || !!properties?.msExtrusionType }) }); @@ -494,13 +506,11 @@ const getBlocks = ({ key: 'outlineColor', opacityKey: 'outlineOpacity', label: 'styleeditor.outlineColor', - stroke: true, - isDisabled: (value, properties) => !properties?.msClampToGround && !!properties?.msExtrudedHeight + stroke: true }), outlineWidth: property.width({ key: 'outlineWidth', - label: 'styleeditor.outlineWidth', - isDisabled: (value, properties) => !properties?.msClampToGround && !!properties?.msExtrudedHeight + label: 'styleeditor.outlineWidth' }), ...(!shouldHideVectorStyleOptions && { outlineDasharray: property.dasharray({ diff --git a/web/client/translations/data.ca-ES.json b/web/client/translations/data.ca-ES.json index ed32184f821..10f6789d4fb 100644 --- a/web/client/translations/data.ca-ES.json +++ b/web/client/translations/data.ca-ES.json @@ -2790,6 +2790,8 @@ "msExtrudedHeight": "Al\u00e7ada extru\u00efda", "msExtrusionColor": "Color d'extrusi\u00f3", "msExtrusionType": "Tipus d'extrusi\u00f3", + "msExtrusionOutlineColor": "Color del contorn d'extrusió", + "msExtrusionOutlineWidth": "Amplada del contorn d'extrusió", "wall": "Paret", "enableBanding": "Band Styling", "selectChannel": "Seleccioneu una banda", diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json index c949ba7654e..2bcbe2d6bdc 100644 --- a/web/client/translations/data.da-DK.json +++ b/web/client/translations/data.da-DK.json @@ -2863,6 +2863,8 @@ "msExtrudedHeight": "Ekstruderet højde", "msExtrusionColor": "Ekstruderingsfarve", "msExtrusionType": "Ekstruderingstype", + "msExtrusionOutlineColor": "Ekstruderingskonturfærge", + "msExtrusionOutlineWidth": "Ekstruderingskonturbredde", "wall": "Væg", "enableBanding": "Båndstyling", "selectChannel": "Vælg en bånd", diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 2ec0edb9218..4506a65982e 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -2979,6 +2979,8 @@ "msExtrudedHeight": "Extrudierte Höhe", "msExtrusionColor": "Extrusionsfarbe", "msExtrusionType": "Extrusionstyp", + "msExtrusionOutlineColor": "Extrusionsumrissfarbe", + "msExtrusionOutlineWidth": "Extrusionsumrissbreite", "wall": "Wand", "enableBanding": "Band styling", "selectChannel": "Wählen Sie eine Band aus", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 13232600086..f1cf2af1f8d 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -2948,6 +2948,8 @@ "msExtrudedHeight": "Extruded height", "msExtrusionColor": "Extrusion color", "msExtrusionType": "Extrusion type", + "msExtrusionOutlineColor": "Extrusion outline color", + "msExtrusionOutlineWidth": "Extrusion outline width", "wall": "Wall", "enableBanding": "Band styling", "selectChannel": "Select a band", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 33f1fc66418..fea3b9f2acc 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -2936,6 +2936,8 @@ "msExtrudedHeight": "Altura extruida", "msExtrusionColor": "Color de extrusión", "msExtrusionType": "Tipo de extrusión", + "msExtrusionOutlineColor": "Color de contorno de extrusión", + "msExtrusionOutlineWidth": "Ancho de contorno de extrusión", "wall": "Muro", "enableBanding": "Estilo de banda", "selectChannel": "Selecciona una banda", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index c30a8953d5a..900a6f87457 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -2940,6 +2940,8 @@ "msExtrudedHeight": "Hauteur extrudée", "msExtrusionColor": "Couleur d'extrusion", "msExtrusionType": "Type d'extrusion", + "msExtrusionOutlineColor": "Couleur du contour d'extrusion", + "msExtrusionOutlineWidth": "Largeur du contour d'extrusion", "wall": "Mur", "enableBanding": "Style de bande", "selectChannel": "Sélectionnez un groupe", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 35bfe0c3e33..f0fbd3d03e1 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -2940,6 +2940,8 @@ "msExtrudedHeight": "Altezza estrusa", "msExtrusionColor": "Colore estrusione", "msExtrusionType": "Tipo estrusione", + "msExtrusionOutlineColor": "Colore contorno estrusione", + "msExtrusionOutlineWidth": "Larghezza contorno estrusione", "wall": "Muro", "enableBanding": "Stile a fascia", "selectChannel": "Seleziona una banda", diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json index cb997c8b385..f0073285df7 100644 --- a/web/client/translations/data.nl-NL.json +++ b/web/client/translations/data.nl-NL.json @@ -2795,6 +2795,8 @@ "msExtrudedHeight": "Extrusiehoogte", "msExtrusionColor": "Extrusiekleur", "msExtrusionType": "Extrusietype", + "msExtrusionOutlineColor": "Kleur extrusieomtrek", + "msExtrusionOutlineWidth": "Breedte extrusieomtrek", "wall": "Muur", "enableBanding": "Bandstijl", "selectChannel": "Selecteer een band", diff --git a/web/client/translations/data.pt-BR.json b/web/client/translations/data.pt-BR.json index 24a0d100bc1..b92435537ed 100644 --- a/web/client/translations/data.pt-BR.json +++ b/web/client/translations/data.pt-BR.json @@ -2808,6 +2808,8 @@ "msExtrudedHeight": "Altura extrusada", "msExtrusionColor": "Cor da extrusão", "msExtrusionType": "Tipo de extrusão", + "msExtrusionOutlineColor": "Cor do contorno da extrusão", + "msExtrusionOutlineWidth": "Largura do contorno da extrusão", "wall": "Parede", "enableBanding": "Estilização de banda", "selectChannel": "Selecionar uma banda", diff --git a/web/client/translations/data.sv-SE.json b/web/client/translations/data.sv-SE.json index 73370f2d94a..f6ce5ef8387 100644 --- a/web/client/translations/data.sv-SE.json +++ b/web/client/translations/data.sv-SE.json @@ -2791,6 +2791,8 @@ "msExtrudedHeight": "Extruderad höjd", "msExtrusionColor": "Extruderingsfärg", "msExtrusionType": "Typ av extrudering", + "msExtrusionOutlineColor": "Extruderingskonturfärg", + "msExtrusionOutlineWidth": "Extruderingskonturbredd", "wall": "Vägg", "enableBanding": "Bandstil", "selectChannel": "Välj ett band", diff --git a/web/client/utils/styleparser/CesiumStyleParser.js b/web/client/utils/styleparser/CesiumStyleParser.js index 747ed299f9f..dc608e221a5 100644 --- a/web/client/utils/styleparser/CesiumStyleParser.js +++ b/web/client/utils/styleparser/CesiumStyleParser.js @@ -465,7 +465,7 @@ const primitiveGeometryTypes = { wall: (options, configs) => { return Promise.resolve(primitiveGeometryTypes.polyline(options, configs)) .then(({ primitive }) => { - return { + const wallResult = { ...options, primitive: { ...primitive, @@ -482,6 +482,79 @@ const primitiveGeometryTypes = { }) } }; + + if (options.primitive.type === 'wallOutline') { + const outlineMaterial = options.primitive.outlineMaterial; + const outlineWidth = options.primitive.outlineWidth; + const outlineEntities = []; + + wallResult.primitive.geometry?.forEach((positions, gIdx) => { + const minHeights = wallResult.primitive.minimumHeights?.[gIdx] || []; + const maxHeights = wallResult.primitive.maximumHeights?.[gIdx] || []; + + const bottomPositions = positions.map((cartesian, i) => { + const carto = Cesium.Cartographic.fromCartesian(cartesian); + return Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, minHeights[i] ?? carto.height); + }); + const topPositions = positions.map((cartesian, i) => { + const carto = Cesium.Cartographic.fromCartesian(cartesian); + return Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, maxHeights[i] ?? carto.height); + }); + + outlineEntities.push( + { + type: 'wallOutline', + geometryType: 'polyline', + entity: { + polyline: { + material: outlineMaterial, + width: outlineWidth, + arcType: Cesium.ArcType.GEODESIC + } + }, + geometry: [bottomPositions] + }, + { + type: 'wallOutline', + geometryType: 'polyline', + entity: { + polyline: { + material: outlineMaterial, + width: outlineWidth, + arcType: Cesium.ArcType.GEODESIC + } + }, + geometry: [topPositions] + } + ); + + for (let i = 0; i < positions.length; i++) { + outlineEntities.push({ + type: 'wallOutline', + geometryType: 'polyline', + entity: { + polyline: { + material: outlineMaterial, + width: outlineWidth, + arcType: Cesium.ArcType.NONE + } + }, + geometry: [[bottomPositions[i], topPositions[i]]] + }); + } + }); + + return { + ...wallResult, + primitive: { + ...wallResult.primitive, + geometry: outlineEntities[0]?.geometry, + _extrusionOutlineEntities: outlineEntities + } + }; + } + + return wallResult; }); }, polygon: (options, { map, sampleTerrain }) => { @@ -563,6 +636,88 @@ const primitiveGeometryTypes = { } }; }, + extrusionOutline: (options, configs) => { + return Promise.resolve(primitiveGeometryTypes.polygon(options, configs)) + .then((resolved) => { + const { primitive } = resolved; + const geometry = primitive.geometry; + const extrudedHeight = primitive?.entity?.polygon?.extrudedHeight ?? primitive?.extrudedHeight; + const baseHeight = primitive?.entity?.polygon?.height ?? primitive?.height; + + if (!geometry || extrudedHeight === undefined) { + return resolved; + } + + const outlineMaterial = primitive.outlineMaterial; + const outlineWidth = primitive.outlineWidth; + const outlineEntities = []; + + geometry.forEach((ring) => { + const bottomPositions = ring.map((cartesian) => { + if (baseHeight !== undefined) { + const carto = Cesium.Cartographic.fromCartesian(cartesian); + return Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, baseHeight); + } + return cartesian; + }); + const topPositions = ring.map((cartesian) => { + const carto = Cesium.Cartographic.fromCartesian(cartesian); + return Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, extrudedHeight); + }); + + outlineEntities.push( + { + type: 'extrusionOutline', + geometryType: 'polyline', + entity: { + polyline: { + material: outlineMaterial, + width: outlineWidth, + arcType: Cesium.ArcType.GEODESIC + } + }, + geometry: [bottomPositions] + }, + { + type: 'extrusionOutline', + geometryType: 'polyline', + entity: { + polyline: { + material: outlineMaterial, + width: outlineWidth, + arcType: Cesium.ArcType.GEODESIC + } + }, + geometry: [topPositions] + } + ); + + for (let i = 0; i < ring.length; i++) { + outlineEntities.push({ + type: 'extrusionOutline', + geometryType: 'polyline', + entity: { + polyline: { + material: outlineMaterial, + width: outlineWidth, + arcType: Cesium.ArcType.NONE + } + }, + geometry: [[bottomPositions[i], topPositions[i]]] + }); + } + }); + + return { + ...resolved, + primitive: { + ...primitive, + geometry: outlineEntities[0]?.geometry, + _extrusionOutlineEntities: outlineEntities + } + }; + }); + }, circlePolyline: (options) => { const { feature, primitive, parsedSymbolizer } = options; if (feature.geometry.type === 'Point') { @@ -792,6 +947,17 @@ const symbolizerToPrimitives = { Line: ({ parsedSymbolizer, feature, globalOpacity }) => { const geometryFunction = getGeometryFunction(parsedSymbolizer); const additionalOptions = geometryFunction ? geometryFunction(feature) : {}; + const isWallExtrusion = !parsedSymbolizer.msClampToGround && parsedSymbolizer.msExtrudedHeight && !parsedSymbolizer.msExtrusionType; + const isVolumeExtrusion = !parsedSymbolizer.msClampToGround && parsedSymbolizer.msExtrudedHeight && parsedSymbolizer.msExtrusionType; + const extrusionOutlineColor = parsedSymbolizer.msExtrusionOutlineColor; + const extrusionOutlineWidth = parsedSymbolizer.msExtrusionOutlineWidth; + const hasExtrusionOutline = extrusionOutlineColor && extrusionOutlineWidth; + const extrusionOutlineMaterial = hasExtrusionOutline + ? getCesiumColor({ + color: extrusionOutlineColor, + opacity: (parsedSymbolizer.msExtrusionOutlineOpacity ?? 1) * globalOpacity + }) + : undefined; return [ ...(parsedSymbolizer.color && parsedSymbolizer.width !== 0 ? [{ type: 'polyline', @@ -817,7 +983,7 @@ const symbolizerToPrimitives = { } } }] : []), - ...((!parsedSymbolizer.msClampToGround && parsedSymbolizer.msExtrudedHeight && !parsedSymbolizer.msExtrusionType) ? [{ + ...(isWallExtrusion ? [{ type: 'polylineVolume', geometryType: 'wall', entity: { @@ -829,7 +995,14 @@ const symbolizerToPrimitives = { } } }] : []), - ...((!parsedSymbolizer.msClampToGround && parsedSymbolizer.msExtrudedHeight && parsedSymbolizer.msExtrusionType) ? [{ + ...(isWallExtrusion && hasExtrusionOutline ? [{ + type: 'wallOutline', + geometryType: 'wall', + outlineMaterial: extrusionOutlineMaterial, + outlineWidth: extrusionOutlineWidth, + entity: {} + }] : []), + ...(isVolumeExtrusion ? [{ type: 'polylineVolume', geometryType: 'polyline', entity: { @@ -838,7 +1011,12 @@ const symbolizerToPrimitives = { color: parsedSymbolizer.msExtrusionColor || '#000000', opacity: (parsedSymbolizer.msExtrusionOpacity ?? 1) * globalOpacity }), - shape: getVolumeShape(parsedSymbolizer.msExtrusionType, parsedSymbolizer.msExtrudedHeight / 2) + shape: getVolumeShape(parsedSymbolizer.msExtrusionType, parsedSymbolizer.msExtrudedHeight / 2), + ...(hasExtrusionOutline && { + outline: true, + outlineColor: extrusionOutlineMaterial, + outlineWidth: extrusionOutlineWidth + }) } } }] : []) @@ -848,6 +1026,17 @@ const symbolizerToPrimitives = { const isExtruded = !parsedSymbolizer.msClampToGround && !!parsedSymbolizer.msExtrudedHeight; const geometryFunction = getGeometryFunction(parsedSymbolizer); const additionalOptions = geometryFunction ? geometryFunction(feature) : {}; + const outlineMaterial = parsedSymbolizer?.outlineDasharray + ? getCesiumDashArray({ + color: parsedSymbolizer.outlineColor, + opacity: parsedSymbolizer.outlineOpacity * globalOpacity, + dasharray: parsedSymbolizer.outlineDasharray + }) + : getCesiumColor({ + color: parsedSymbolizer.outlineColor, + opacity: parsedSymbolizer.outlineOpacity * globalOpacity + }); + const hasOutline = parsedSymbolizer.outlineColor && parsedSymbolizer.outlineWidth !== 0; return [ { type: 'polygon', @@ -873,24 +1062,16 @@ const symbolizerToPrimitives = { } }, // outline properties is not working in some browser see https://github.com/CesiumGS/cesium/issues/40 - // this is a workaround to visualize the outline with the correct side - // this only for the footprint - ...(parsedSymbolizer.outlineColor && parsedSymbolizer.outlineWidth !== 0 && !isExtruded ? [ + // this is a workaround to visualize the outline with the correct width + // for non-extruded: draw footprint polyline + // for extruded: draw bottom ring, top ring, and vertical edge polylines + ...(hasOutline && !isExtruded ? [ { type: 'polyline', geometryType: 'polyline', entity: { polyline: { - material: parsedSymbolizer?.outlineDasharray - ? getCesiumDashArray({ - color: parsedSymbolizer.outlineColor, - opacity: parsedSymbolizer.outlineOpacity * globalOpacity, - dasharray: parsedSymbolizer.outlineDasharray - }) - : getCesiumColor({ - color: parsedSymbolizer.outlineColor, - opacity: parsedSymbolizer.outlineOpacity * globalOpacity - }), + material: outlineMaterial, width: parsedSymbolizer.outlineWidth, clampToGround: parsedSymbolizer.msClampToGround, ...(!parsedSymbolizer.msClampToGround ? undefined : {classificationType: parsedSymbolizer.msClassificationType === 'terrain' ? @@ -905,6 +1086,16 @@ const symbolizerToPrimitives = { } } } + ] : []), + ...(hasOutline && isExtruded ? [ + { + type: 'extrusionOutline', + geometryType: 'extrusionOutline', + clampToGround: false, + outlineMaterial, + outlineWidth: parsedSymbolizer.outlineWidth, + entity: {} + } ] : []) ]; }, @@ -986,7 +1177,13 @@ const isGeometryChanged = (previousSymbolizer, currentSymbolizer) => { || previousSymbolizer?.msTranslateY !== currentSymbolizer?.msTranslateY || previousSymbolizer?.heading !== currentSymbolizer?.heading || previousSymbolizer?.pitch !== currentSymbolizer?.pitch - || previousSymbolizer?.roll !== currentSymbolizer?.roll; + || previousSymbolizer?.roll !== currentSymbolizer?.roll + || previousSymbolizer?.outlineColor !== currentSymbolizer?.outlineColor + || previousSymbolizer?.outlineWidth !== currentSymbolizer?.outlineWidth + || previousSymbolizer?.outlineOpacity !== currentSymbolizer?.outlineOpacity + || previousSymbolizer?.msExtrusionOutlineColor !== currentSymbolizer?.msExtrusionOutlineColor + || previousSymbolizer?.msExtrusionOutlineWidth !== currentSymbolizer?.msExtrusionOutlineWidth + || previousSymbolizer?.msExtrusionOutlineOpacity !== currentSymbolizer?.msExtrusionOutlineOpacity; }; const isSymbolizerChanged = (previousSymbolizer, currentSymbolizer) => { @@ -1075,8 +1272,23 @@ function getStyleFuncFromRules({ })); }) .then((updatedStyledFeatures) => { + // expand extrusion outline entities into individual styled features + const expanded = updatedStyledFeatures.flatMap((styledFeature) => { + const outlineEntities = styledFeature.primitive?._extrusionOutlineEntities; + if (outlineEntities && outlineEntities.length > 0) { + return outlineEntities.map((outlineEntity, idx) => ({ + ...styledFeature, + id: `${styledFeature.id}:${idx}`, + primitive: { + ...outlineEntity + }, + action: styledFeature.action + })); + } + return [styledFeature]; + }); // remove all styled features without geometry - return updatedStyledFeatures.filter(({ primitive }) => !!primitive.geometry); + return expanded.filter(({ primitive }) => !!primitive.geometry); }); }; } diff --git a/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js b/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js index bdd8b2395d1..32b532af5b4 100644 --- a/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js +++ b/web/client/utils/styleparser/__tests__/CesiumStyleParser-test.js @@ -1459,6 +1459,325 @@ describe('CesiumStyleParser', () => { done(); }).catch(done); }); + it('should write style function with extruded fill symbolizer and outline (none height reference)', (done) => { + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + symbolizerId: 'symbolizer-01', + kind: 'Fill', + color: '#ff0000', + fillOpacity: 0.5, + outlineColor: '#00ff00', + outlineOpacity: 0.25, + outlineWidth: 3, + msHeight: 10, + msExtrudedHeight: 100, + msHeightReference: 'none', + msExtrusionRelativeToGeometry: false + } + ] + } + ] + }; + const feature = { + type: 'Feature', + properties: {}, + id: 'feature-01', + geometry: { + type: 'Polygon', + coordinates: [[[7, 41], [14, 41], [14, 46], [7, 46], [7, 41]]] + } + }; + parser.writeStyle(style) + .then((styleFunc) => styleFunc({ + features: [{ ...feature, positions: GeoJSONStyledFeatures.featureToCartesianPositions(feature) }] + })) + .then((styledFeatures) => { + const polygon = styledFeatures.find(f => f.primitive.type === 'polygon'); + expect(polygon).toBeTruthy(); + expect(polygon.primitive.entity.polygon.height).toBe(10); + expect(polygon.primitive.entity.polygon.extrudedHeight).toBe(100); + + const outlineFeatures = styledFeatures.filter(f => f.primitive.type === 'extrusionOutline'); + expect(outlineFeatures.length).toBeGreaterThan(0); + outlineFeatures.forEach(f => { + expect(f.primitive.entity.polyline).toBeTruthy(); + expect(f.primitive.entity.polyline.width).toBe(3); + }); + + const bottomRing = outlineFeatures.find(f => { + const positions = f.primitive.geometry[0]; + const height = Math.round(Cesium.Cartographic.fromCartesian(positions[0]).height); + return height === 10; + }); + expect(bottomRing).toBeTruthy(); + + const topRing = outlineFeatures.find(f => { + const positions = f.primitive.geometry[0]; + const height = Math.round(Cesium.Cartographic.fromCartesian(positions[0]).height); + return height === 100; + }); + expect(topRing).toBeTruthy(); + + done(); + }).catch(done); + }); + it('should write style function with wall extrusion outline', (done) => { + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + kind: 'Line', + symbolizerId: 'symbolizer-01', + msExtrusionColor: '#ff0000', + msExtrusionOpacity: 0.5, + msHeight: 10, + msExtrudedHeight: 50, + msHeightReference: 'none', + msExtrusionRelativeToGeometry: false, + msExtrusionOutlineColor: '#0000ff', + msExtrusionOutlineOpacity: 0.8, + msExtrusionOutlineWidth: 2 + } + ] + } + ] + }; + const feature = { + type: 'Feature', + properties: {}, + id: 'feature-01', + geometry: { + type: 'LineString', + coordinates: [[7, 41], [14, 41], [14, 46], [7, 46]] + } + }; + parser.writeStyle(style) + .then((styleFunc) => styleFunc({ + features: [{ ...feature, positions: GeoJSONStyledFeatures.featureToCartesianPositions(feature) }] + })) + .then((styledFeatures) => { + const wallFeatures = styledFeatures.filter(f => + f.primitive.type === 'polylineVolume' && f.primitive.entity?.wall + ); + expect(wallFeatures.length).toBe(1); + + const outlineFeatures = styledFeatures.filter(f => f.primitive.type === 'wallOutline'); + expect(outlineFeatures.length).toBeGreaterThan(0); + outlineFeatures.forEach(f => { + expect(f.primitive.entity.polyline).toBeTruthy(); + expect(f.primitive.entity.polyline.width).toBe(2); + }); + + done(); + }).catch(done); + }); + it('should write style function with polyline volume circle extrusion and outline', (done) => { + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + kind: 'Line', + symbolizerId: 'symbolizer-01', + msExtrudedHeight: 100, + msExtrusionColor: '#ff0000', + msExtrusionOpacity: 0.5, + msExtrusionType: 'Circle', + msExtrusionOutlineColor: '#0000ff', + msExtrusionOutlineOpacity: 0.5, + msExtrusionOutlineWidth: 2 + } + ] + } + ] + }; + const feature = { + type: 'Feature', + properties: {}, + id: 'feature-01', + geometry: { + type: 'LineString', + coordinates: [[7, 41], [14, 41], [14, 46], [7, 46]] + } + }; + parser.writeStyle(style) + .then((styleFunc) => styleFunc({ + features: [{ ...feature, positions: GeoJSONStyledFeatures.featureToCartesianPositions(feature) }] + })) + .then((styledFeatures) => { + expect(styledFeatures.length).toBe(1); + const [volume] = styledFeatures; + expect(volume.primitive.entity.polylineVolume.shape.length).toBe(360); + expect(volume.primitive.entity.polylineVolume.outline).toBe(true); + expect(volume.primitive.entity.polylineVolume.outlineWidth).toBe(2); + + done(); + }).catch(done); + }); + it('should generate vertical edge polylines for extruded fill outline', (done) => { + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + symbolizerId: 'symbolizer-01', + kind: 'Fill', + color: '#ff0000', + fillOpacity: 0.5, + outlineColor: '#00ff00', + outlineOpacity: 0.5, + outlineWidth: 2, + msHeight: 0, + msExtrudedHeight: 50, + msHeightReference: 'none', + msExtrusionRelativeToGeometry: false + } + ] + } + ] + }; + const feature = { + type: 'Feature', + properties: {}, + id: 'feature-01', + geometry: { + type: 'Polygon', + coordinates: [[[7, 41], [14, 41], [14, 46], [7, 46], [7, 41]]] + } + }; + parser.writeStyle(style) + .then((styleFunc) => styleFunc({ + features: [{ ...feature, positions: GeoJSONStyledFeatures.featureToCartesianPositions(feature) }] + })) + .then((styledFeatures) => { + const outlineFeatures = styledFeatures.filter(f => f.primitive.type === 'extrusionOutline'); + // 5 ring positions -> 2 rings (bottom + top) + 5 vertical edges = 7 + expect(outlineFeatures.length).toBe(7); + + outlineFeatures.forEach(f => { + expect(f.primitive.entity.polyline.material.toString()).toBe('(0, 1, 0, 0.5)'); + expect(f.primitive.entity.polyline.width).toBe(2); + }); + + const verticalEdges = outlineFeatures.filter(f => + f.primitive.entity.polyline.arcType === Cesium.ArcType.NONE + ); + expect(verticalEdges.length).toBe(5); + verticalEdges.forEach(f => { + const positions = f.primitive.geometry[0]; + expect(positions.length).toBe(2); + const bottomHeight = Math.round(Cesium.Cartographic.fromCartesian(positions[0]).height); + const topHeight = Math.round(Cesium.Cartographic.fromCartesian(positions[1]).height); + expect(bottomHeight).toBe(0); + expect(topHeight).toBe(50); + }); + + done(); + }).catch(done); + }); + it('should not add wall extrusion outline when outline properties are not set', (done) => { + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + kind: 'Line', + symbolizerId: 'symbolizer-01', + msExtrusionColor: '#ff0000', + msExtrusionOpacity: 0.5, + msHeight: 10, + msExtrudedHeight: 50, + msHeightReference: 'none', + msExtrusionRelativeToGeometry: false + } + ] + } + ] + }; + const feature = { + type: 'Feature', + properties: {}, + id: 'feature-01', + geometry: { + type: 'LineString', + coordinates: [[7, 41], [14, 41], [14, 46], [7, 46]] + } + }; + parser.writeStyle(style) + .then((styleFunc) => styleFunc({ + features: [{ ...feature, positions: GeoJSONStyledFeatures.featureToCartesianPositions(feature) }] + })) + .then((styledFeatures) => { + const outlineFeatures = styledFeatures.filter(f => f.primitive.type === 'wallOutline'); + expect(outlineFeatures.length).toBe(0); + done(); + }).catch(done); + }); + it('should not add extrusion outline when outline properties are not set', (done) => { + const style = { + name: '', + rules: [ + { + filter: undefined, + name: '', + symbolizers: [ + { + symbolizerId: 'symbolizer-01', + kind: 'Fill', + color: '#ff0000', + fillOpacity: 0.5, + msHeight: 10, + msExtrudedHeight: 100, + msHeightReference: 'none', + msExtrusionRelativeToGeometry: false + } + ] + } + ] + }; + const feature = { + type: 'Feature', + properties: {}, + id: 'feature-01', + geometry: { + type: 'Polygon', + coordinates: [[[7, 41], [14, 41], [14, 46], [7, 46], [7, 41]]] + } + }; + parser.writeStyle(style) + .then((styleFunc) => styleFunc({ + features: [{ ...feature, positions: GeoJSONStyledFeatures.featureToCartesianPositions(feature) }] + })) + .then((styledFeatures) => { + expect(styledFeatures.length).toBe(1); + const [polygon] = styledFeatures; + expect(polygon.primitive.type).toBe('polygon'); + expect(polygon.primitive.entity.polygon.height).toBe(10); + expect(polygon.primitive.entity.polygon.extrudedHeight).toBe(100); + const outlineFeatures = styledFeatures.filter(f => f.primitive.type === 'extrusionOutline'); + expect(outlineFeatures.length).toBe(0); + done(); + }).catch(done); + }); }); it('should not draw the marker when using radius property without argument', (done) => { const style = { From 283c2619fd1b3555aceb3ae524074d0700d00008 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 12 Mar 2026 20:02:22 +0530 Subject: [PATCH 2/2] update unit test --- .../styleeditor/__tests__/VisualStyleEditor-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js b/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js index 382f326f3aa..90bcb75fb2f 100644 --- a/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js +++ b/web/client/components/styleeditor/__tests__/VisualStyleEditor-test.js @@ -583,7 +583,9 @@ describe('VisualStyleEditor', () => { 'styleeditor.height', 'styleeditor.msExtrusionRelativeToGeometry', 'styleeditor.msExtrudedHeight', - 'styleeditor.msExtrusionColor' + 'styleeditor.msExtrusionColor', + 'styleeditor.msExtrusionOutlineColor', + 'styleeditor.msExtrusionOutlineWidth' ]); done(); }).catch(done);