You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/07/01 11:20:30 UTC

[sis] branch geoapi-4.0 updated (9992bd5235 -> 1dacb571d6)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from 9992bd5235 Style : add internal implementation of GeoAPI Style
     new 4ee789634f Review of the new style package added in previous commit. Summary of changes:
     new b35d75f713 Rename `org.apache.sis.internal.style` package as `org.apache.sis.style.se1`. By moving the package outside `internal` namespace, we are making it public. However we still target SIS 1.5, not SIS 1.4, for release.
     new d38b17c046 Add a placeholder for a JAXB adapter for expressions, then verifies that current JAXB annotations are valid.
     new 1dacb571d6 Add `CoverageStyle` and reintroduce `StyleFactory` classes (in different form). `CoverageStyle` is defined by OGC 05-077r4 in complement to `FeatureTypeStyle`. Those two classes are identical except for the kind of data on which they work: `FeatureTypeStyle` styles `Feature` while `CoverageStyle` styles `BandedCoverage`. For making that difference possible, it was necessary to add parameterized type <R> on all classes in replacement for the previously hard-coded <Featu [...]

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/java/org/apache/sis/xml/Namespaces.java   |  18 +-
 core/sis-portrayal/pom.xml                         |   5 +
 .../sis/internal/map/ExceptionPresentation.java    |   4 +-
 .../apache/sis/internal/map/ListChangeEvent.java   |   4 +-
 .../org/apache/sis/internal/map/NotifiedList.java  |   4 +-
 .../org/apache/sis/internal/map/Presentation.java  |   4 +-
 .../sis/internal/map/PropertyNameCollector.java    |   4 +-
 .../sis/internal/map/ResourceSymbolizer.java       |  22 +-
 .../org/apache/sis/internal/map/SEPortrayer.java   |  60 +--
 .../apache/sis/internal/map/SEPresentation.java    |   9 +-
 .../apache/sis/internal/map/SymbologyVisitor.java  | 155 ++----
 .../org/apache/sis/internal/map/package-info.java  |   4 +-
 .../org/apache/sis/internal/style/AnchorPoint.java | 106 ----
 .../sis/internal/style/ChannelSelection.java       | 126 -----
 .../org/apache/sis/internal/style/ColorMap.java    |  88 ----
 .../sis/internal/style/ColorReplacement.java       |  88 ----
 .../sis/internal/style/ContrastEnhancement.java    | 107 ----
 .../org/apache/sis/internal/style/Description.java | 105 ----
 .../apache/sis/internal/style/Displacement.java    | 106 ----
 .../apache/sis/internal/style/ExternalGraphic.java | 137 -----
 .../apache/sis/internal/style/ExternalMark.java    | 128 -----
 .../sis/internal/style/FeatureTypeStyle.java       | 168 ------
 .../java/org/apache/sis/internal/style/Fill.java   | 122 -----
 .../java/org/apache/sis/internal/style/Font.java   | 131 -----
 .../org/apache/sis/internal/style/Graphic.java     | 174 -------
 .../org/apache/sis/internal/style/GraphicFill.java |  79 ---
 .../apache/sis/internal/style/GraphicLegend.java   |  79 ---
 .../apache/sis/internal/style/GraphicStroke.java   | 119 -----
 .../apache/sis/internal/style/GraphicalSymbol.java |  37 --
 .../java/org/apache/sis/internal/style/Halo.java   | 109 ----
 .../apache/sis/internal/style/LabelPlacement.java  |  36 --
 .../apache/sis/internal/style/LinePlacement.java   | 159 ------
 .../apache/sis/internal/style/LineSymbolizer.java  | 121 -----
 .../java/org/apache/sis/internal/style/Mark.java   | 132 -----
 .../apache/sis/internal/style/PointPlacement.java  | 118 -----
 .../apache/sis/internal/style/PointSymbolizer.java | 108 ----
 .../sis/internal/style/PolygonSymbolizer.java      | 146 ------
 .../sis/internal/style/RasterSymbolizer.java       | 186 -------
 .../java/org/apache/sis/internal/style/Rule.java   | 198 --------
 .../sis/internal/style/SelectedChannelType.java    | 100 ----
 .../apache/sis/internal/style/ShadedRelief.java    | 100 ----
 .../java/org/apache/sis/internal/style/Stroke.java | 223 --------
 .../java/org/apache/sis/internal/style/Style.java  | 142 ------
 .../apache/sis/internal/style/StyleFactory.java    | 563 ---------------------
 .../org/apache/sis/internal/style/Symbolizer.java  | 128 -----
 .../apache/sis/internal/style/TextSymbolizer.java  | 158 ------
 .../java/org/apache/sis/portrayal/MapLayer.java    |   2 +-
 .../org/apache/sis/style/se1/AbstractStyle.java    | 348 +++++++++++++
 .../java/org/apache/sis/style/se1/AnchorPoint.java | 160 ++++++
 .../org/apache/sis/style/se1/ChannelSelection.java | 193 +++++++
 .../java/org/apache/sis/style/se1/ColorMap.java    |  94 ++++
 .../org/apache/sis/style/se1/ColorReplacement.java |  87 ++++
 .../apache/sis/style/se1/ContrastEnhancement.java  | 169 +++++++
 .../org/apache/sis/style/se1/CoverageStyle.java    |  68 +++
 .../java/org/apache/sis/style/se1/Description.java | 160 ++++++
 .../org/apache/sis/style/se1/Displacement.java     | 163 ++++++
 .../java/org/apache/sis/style/se1/ElseFilter.java} |  41 +-
 .../apache/sis/style/se1/ExpressionAdapter.java}   |  43 +-
 .../org/apache/sis/style/se1/ExternalGraphic.java  | 139 +++++
 .../org/apache/sis/style/se1/FeatureTypeStyle.java |  81 +++
 .../main/java/org/apache/sis/style/se1/Fill.java   | 248 +++++++++
 .../main/java/org/apache/sis/style/se1/Font.java   | 224 ++++++++
 .../java/org/apache/sis/style/se1/Graphic.java     | 351 +++++++++++++
 .../java/org/apache/sis/style/se1/GraphicFill.java | 138 +++++
 .../org/apache/sis/style/se1/GraphicStroke.java    | 203 ++++++++
 .../org/apache/sis/style/se1/GraphicalElement.java |  52 ++
 .../org/apache/sis/style/se1/GraphicalSymbol.java  | 181 +++++++
 .../main/java/org/apache/sis/style/se1/Halo.java   | 170 +++++++
 .../org/apache/sis/style/se1/LabelPlacement.java   |  80 +++
 .../org/apache/sis/style/se1/LegendGraphic.java    | 130 +++++
 .../org/apache/sis/style/se1/LinePlacement.java    | 283 +++++++++++
 .../org/apache/sis/style/se1/LineSymbolizer.java   | 173 +++++++
 .../main/java/org/apache/sis/style/se1/Mark.java   | 293 +++++++++++
 .../org/apache/sis/style/se1/PointPlacement.java   | 209 ++++++++
 .../org/apache/sis/style/se1/PointSymbolizer.java  | 172 +++++++
 .../apache/sis/style/se1/PolygonSymbolizer.java    | 324 ++++++++++++
 .../org/apache/sis/style/se1/RasterSymbolizer.java | 359 +++++++++++++
 .../main/java/org/apache/sis/style/se1/Rule.java   | 513 +++++++++++++++++++
 .../org/apache/sis/style/se1/SelectedChannel.java  | 173 +++++++
 .../org/apache/sis/style/se1/ShadedRelief.java     | 155 ++++++
 .../main/java/org/apache/sis/style/se1/Stroke.java | 492 ++++++++++++++++++
 .../main/java/org/apache/sis/style/se1/Style.java  | 254 ++++++++++
 .../org/apache/sis/style/se1/StyleElement.java     | 220 ++++++++
 .../org/apache/sis/style/se1/StyleFactory.java     | 534 +++++++++++++++++++
 .../java/org/apache/sis/style/se1/Symbolizer.java  | 314 ++++++++++++
 .../org/apache/sis/style/se1/TextSymbolizer.java   | 288 +++++++++++
 .../java/org/apache/sis/style/se1/Translucent.java |  49 ++
 .../org/apache/sis/style/se1/package-info.java     |  69 +++
 .../apache/sis/internal/map/SEPortrayerTest.java   | 125 ++---
 .../sis/internal/style/AbstractStyleTests.java     |  41 --
 .../sis/internal/style/ChannelSelectionTest.java   |  66 ---
 .../sis/internal/style/ExternalGraphicTest.java    |  95 ----
 .../sis/internal/style/ExternalMarkTest.java       |  93 ----
 .../sis/internal/style/FeatureTypeStyleTest.java   | 161 ------
 .../org/apache/sis/internal/style/FillTest.java    |  76 ---
 .../org/apache/sis/internal/style/FontTest.java    |  87 ----
 .../org/apache/sis/internal/style/GraphicTest.java | 121 -----
 .../org/apache/sis/internal/style/HaloTest.java    |  60 ---
 .../sis/internal/style/LinePlacementTest.java      | 119 -----
 .../org/apache/sis/internal/style/MarkTest.java    |  91 ----
 .../sis/internal/style/PointPlacementTest.java     |  76 ---
 .../sis/internal/style/PolygonSymbolizerTest.java  |  92 ----
 .../sis/internal/style/RasterSymbolizerTest.java   | 140 -----
 .../org/apache/sis/internal/style/RuleTest.java    | 168 ------
 .../org/apache/sis/internal/style/StrokeTest.java  | 167 ------
 .../apache/sis/internal/style/SymbolizerTest.java  |  90 ----
 .../sis/internal/style/TextSymbolizerTest.java     | 108 ----
 .../style => style/se1}/AnchorPointTest.java       |  38 +-
 .../apache/sis/style/se1/ChannelSelectionTest.java |  71 +++
 .../se1}/ContrastEnhancementTest.java              |  42 +-
 .../style => style/se1}/DescriptionTest.java       |  41 +-
 .../style => style/se1}/DisplacementTest.java      |  38 +-
 .../apache/sis/style/se1/ExternalGraphicTest.java  | 102 ++++
 .../apache/sis/style/se1/FeatureTypeStyleTest.java | 137 +++++
 .../java/org/apache/sis/style/se1/FillTest.java    |  93 ++++
 .../java/org/apache/sis/style/se1/FontTest.java    | 100 ++++
 .../style => style/se1}/GraphicStrokeTest.java     |  46 +-
 .../java/org/apache/sis/style/se1/GraphicTest.java | 128 +++++
 .../java/org/apache/sis/style/se1/HaloTest.java    |  73 +++
 .../apache/sis/style/se1/LinePlacementTest.java    | 125 +++++
 .../style => style/se1}/LineSymbolizerTest.java    |  48 +-
 .../java/org/apache/sis/style/se1/MarkTest.java    |  86 ++++
 .../apache/sis/style/se1/PointPlacementTest.java   |  90 ++++
 .../style => style/se1}/PointSymbolizerTest.java   |  34 +-
 .../sis/style/se1/PolygonSymbolizerTest.java       | 109 ++++
 .../apache/sis/style/se1/RasterSymbolizerTest.java | 149 ++++++
 .../java/org/apache/sis/style/se1/RuleTest.java    | 184 +++++++
 .../se1/SelectedChannelTest.java}                  |  46 +-
 .../style => style/se1}/ShadedReliefTest.java      |  45 +-
 .../java/org/apache/sis/style/se1/StrokeTest.java  | 187 +++++++
 .../{internal/style => style/se1}/StyleTest.java   |  67 +--
 .../org/apache/sis/style/se1/StyleTestCase.java    | 120 +++++
 .../org/apache/sis/style/se1/SymbolizerTest.java   | 103 ++++
 .../apache/sis/style/se1/TextSymbolizerTest.java   | 122 +++++
 .../ColorMapTest.java => style/se1/XmlTest.java}   |  39 +-
 135 files changed, 10834 insertions(+), 6924 deletions(-)
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
 delete mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/CoverageStyle.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
 rename core/sis-portrayal/src/{test/java/org/apache/sis/internal/style/ColorReplacementTest.java => main/java/org/apache/sis/style/se1/ElseFilter.java} (54%)
 copy core/sis-portrayal/src/{test/java/org/apache/sis/internal/style/ColorMapTest.java => main/java/org/apache/sis/style/se1/ExpressionAdapter.java} (53%)
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
 create mode 100644 core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AbstractStyleTests.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalMarkTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java
 delete mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/AnchorPointTest.java (55%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/ContrastEnhancementTest.java (59%)
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/DescriptionTest.java (51%)
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/DisplacementTest.java (54%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/GraphicStrokeTest.java (53%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/LineSymbolizerTest.java (52%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/PointSymbolizerTest.java (59%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style/SelectedChannelTypeTest.java => style/se1/SelectedChannelTest.java} (51%)
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/ShadedReliefTest.java (52%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style => style/se1}/StyleTest.java (57%)
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
 create mode 100644 core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
 rename core/sis-portrayal/src/test/java/org/apache/sis/{internal/style/ColorMapTest.java => style/se1/XmlTest.java} (53%)


[sis] 01/04: Review of the new style package added in previous commit. Summary of changes:

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 4ee789634f2f325ef23259c37a46a07bad10602a
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Jun 19 13:13:23 2023 +0200

    Review of the new style package added in previous commit.
    Summary of changes:
    
    * Remove usages of GeoAPI-pending interfaces (see below).
    * Warning resolutions for raw parametric types and unchecked casts.
    * Add some properties defined by the SE standard but missing in the classes.
    * Empty placeholders when addition of missing properties would be too much work.
    * Use `java.util.Optional` for better identification of which properties are optional.
    * Add some (not all) JAXB annotations, as a help for identifying mandatory properties.
    * Lazy initialization of (sometime standardized) default values of mandatory properties.
    * Add credits to OGC GeoAPI 2 contributors since GeoAPI was a source of inspiration.
    * Set version number to 1.5 (we will not include this package in SIS 1.4 release).
    * Documentation.
    
    The most important change is the removal of usages of GeoAPI-pending interfaces.
    The GeoAPI `org.opengis.style` package is not part of a formal GeoAPI release.
    It is currently found only in the "GeoAPI-pending" module, which is not released.
    Those interfaces were based on XML encoding as defined in OGC 05-077r4 standard.
    Using XML schema instead of UML models has many drawbacks shown in OGC meetings.
    UML abstract model was not available when `org.opengis.style` was first designed.
    But today, the ISO 19117:2012 standard evolved with a more elaborated model.
    There is also various OGC working groups actively working on new style standards,
    with goals such as better representation of 3D data. For those reasons, the style
    interfaces in GeoAPI-pending may change and it would premature to commit to them.
    
    https://issues.apache.org/jira/browse/SIS-583
---
 .../main/java/org/apache/sis/xml/Namespaces.java   |  18 +-
 .../sis/internal/map/ExceptionPresentation.java    |   4 +-
 .../apache/sis/internal/map/ListChangeEvent.java   |   4 +-
 .../org/apache/sis/internal/map/NotifiedList.java  |   4 +-
 .../org/apache/sis/internal/map/Presentation.java  |   4 +-
 .../sis/internal/map/PropertyNameCollector.java    |   4 +-
 .../sis/internal/map/ResourceSymbolizer.java       |  15 +-
 .../org/apache/sis/internal/map/SEPortrayer.java   |  60 +--
 .../apache/sis/internal/map/SEPresentation.java    |   9 +-
 .../apache/sis/internal/map/SymbologyVisitor.java  | 103 +---
 .../org/apache/sis/internal/map/package-info.java  |   4 +-
 .../org/apache/sis/internal/style/AnchorPoint.java | 175 ++++---
 .../sis/internal/style/ChannelSelection.java       | 212 +++++---
 .../org/apache/sis/internal/style/ColorMap.java    |  99 ++--
 .../sis/internal/style/ColorReplacement.java       |  92 ++--
 .../sis/internal/style/ContrastEnhancement.java    | 165 +++---
 .../org/apache/sis/internal/style/Description.java | 158 +++---
 .../apache/sis/internal/style/Displacement.java    | 172 ++++---
 .../apache/sis/internal/style/ExternalGraphic.java | 179 +++----
 .../apache/sis/internal/style/ExternalMark.java    | 128 -----
 .../sis/internal/style/FeatureTypeStyle.java       | 381 ++++++++++----
 .../java/org/apache/sis/internal/style/Fill.java   | 269 +++++++---
 .../java/org/apache/sis/internal/style/Font.java   | 236 ++++++---
 .../org/apache/sis/internal/style/Graphic.java     | 384 ++++++++++----
 .../org/apache/sis/internal/style/GraphicFill.java | 126 +++--
 .../apache/sis/internal/style/GraphicLegend.java   | 118 +++--
 .../apache/sis/internal/style/GraphicStroke.java   | 206 +++++---
 .../sis/internal/style/GraphicalElement.java       |  49 ++
 .../apache/sis/internal/style/GraphicalSymbol.java | 156 +++++-
 .../java/org/apache/sis/internal/style/Halo.java   | 168 +++---
 .../apache/sis/internal/style/LabelPlacement.java  |  55 +-
 .../apache/sis/internal/style/LinePlacement.java   | 309 +++++++----
 .../apache/sis/internal/style/LineSymbolizer.java  | 180 ++++---
 .../java/org/apache/sis/internal/style/Mark.java   | 319 +++++++++---
 .../apache/sis/internal/style/PointPlacement.java  | 201 +++++---
 .../apache/sis/internal/style/PointSymbolizer.java | 172 ++++---
 .../sis/internal/style/PolygonSymbolizer.java      | 337 +++++++++---
 .../sis/internal/style/RasterSymbolizer.java       | 391 +++++++++-----
 .../java/org/apache/sis/internal/style/Rule.java   | 523 ++++++++++++++-----
 .../sis/internal/style/SelectedChannelType.java    | 172 +++++--
 .../apache/sis/internal/style/ShadedRelief.java    | 154 ++++--
 .../java/org/apache/sis/internal/style/Stroke.java | 567 ++++++++++++++++-----
 .../java/org/apache/sis/internal/style/Style.java  | 244 ++++++---
 .../apache/sis/internal/style/StyleElement.java    | 190 +++++++
 .../apache/sis/internal/style/StyleFactory.java    | 563 --------------------
 .../org/apache/sis/internal/style/Symbolizer.java  | 314 +++++++++---
 .../apache/sis/internal/style/TextSymbolizer.java  | 297 +++++++----
 .../org/apache/sis/internal/style/Translucent.java |  47 ++
 .../java/org/apache/sis/portrayal/MapLayer.java    |   2 +-
 .../apache/sis/internal/map/SEPortrayerTest.java   |  54 +-
 .../sis/internal/style/AbstractStyleTests.java     |  41 --
 .../apache/sis/internal/style/AnchorPointTest.java |  34 +-
 .../sis/internal/style/ChannelSelectionTest.java   |  55 +-
 .../apache/sis/internal/style/ColorMapTest.java    |  45 --
 .../sis/internal/style/ColorReplacementTest.java   |  44 --
 .../internal/style/ContrastEnhancementTest.java    |  36 +-
 .../apache/sis/internal/style/DescriptionTest.java |  35 +-
 .../sis/internal/style/DisplacementTest.java       |  34 +-
 .../sis/internal/style/ExternalGraphicTest.java    |  65 +--
 .../sis/internal/style/ExternalMarkTest.java       |  93 ----
 .../sis/internal/style/FeatureTypeStyleTest.java   | 131 ++---
 .../org/apache/sis/internal/style/FillTest.java    |  58 ++-
 .../org/apache/sis/internal/style/FontTest.java    |  69 ++-
 .../sis/internal/style/GraphicStrokeTest.java      |  40 +-
 .../org/apache/sis/internal/style/GraphicTest.java |  77 +--
 .../org/apache/sis/internal/style/HaloTest.java    |  45 +-
 .../sis/internal/style/LinePlacementTest.java      |  90 ++--
 .../sis/internal/style/LineSymbolizerTest.java     |  37 +-
 .../org/apache/sis/internal/style/MarkTest.java    |  64 ++-
 .../sis/internal/style/PointPlacementTest.java     |  51 +-
 .../sis/internal/style/PointSymbolizerTest.java    |  27 +-
 .../sis/internal/style/PolygonSymbolizerTest.java  |  67 ++-
 .../sis/internal/style/RasterSymbolizerTest.java   |  89 ++--
 .../org/apache/sis/internal/style/RuleTest.java    | 133 ++---
 .../internal/style/SelectedChannelTypeTest.java    |  38 +-
 .../sis/internal/style/ShadedReliefTest.java       |  39 +-
 .../org/apache/sis/internal/style/StrokeTest.java  | 132 +++--
 .../org/apache/sis/internal/style/StyleTest.java   |  63 ++-
 .../apache/sis/internal/style/StyleTestCase.java   | 110 ++++
 .../apache/sis/internal/style/SymbolizerTest.java  |  65 ++-
 .../sis/internal/style/TextSymbolizerTest.java     |  69 +--
 81 files changed, 6747 insertions(+), 4022 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/xml/Namespaces.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/Namespaces.java
index 7bd09ee1d8..c16bc77380 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/xml/Namespaces.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/Namespaces.java
@@ -90,7 +90,7 @@ import static org.apache.sis.internal.metadata.ImplementationHelper.ISO_NAMESPAC
  * @author  Guilhem Legal   (Geomatys)
  * @author  Cullen Rombach  (Image Matters)
  * @author  Alexis Gaillard (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   0.3
  */
 public final class Namespaces extends Static {
@@ -497,6 +497,22 @@ public final class Namespaces extends Static {
      */
     public static final String CSW = "http://www.opengis.net/cat/csw/3.0";
 
+    /**
+     * The <code>{@value}</code> URL.
+     * The usual prefix for this namespace is {@code "se"}.
+     *
+     * <p>History</p>
+     * <table class="sis">
+     *   <caption>Change log</caption>
+     *   <tr><th>SIS version</th> <th>URL</th></tr>
+     *   <tr><td>1.4</td>         <td>http://www.opengis.net/se</td></tr>
+     * </table>
+     *
+     * @category OGC
+     * @since 1.4
+     */
+    public static final String SE = "http://www.opengis.net/se";
+
     /**
      * The <code>{@value}</code> URL.
      * The usual prefix for this namespace is {@code "xsi"}.
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ExceptionPresentation.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ExceptionPresentation.java
index f48f82714b..4a2824a1a9 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ExceptionPresentation.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ExceptionPresentation.java
@@ -32,8 +32,8 @@ import org.opengis.feature.Feature;
  * </p>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public class ExceptionPresentation extends Presentation {
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java
index 73019d6a06..d7602d64f9 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ListChangeEvent.java
@@ -24,8 +24,8 @@ import org.apache.sis.measure.NumberRange;
  * Event generated by modified list properties.
  *
  * @author Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public final class ListChangeEvent<T> extends PropertyChangeEvent {
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java
index 0677f13c2a..5c39166f40 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/NotifiedList.java
@@ -25,8 +25,8 @@ import org.apache.sis.measure.NumberRange;
  * Decorate a CopyOnWriteArrayList and notify changes when elements are added or removed.
  *
  * @author Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public abstract class NotifiedList<T> extends AbstractList<T> {
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
index e407c988d9..89af40c0b8 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/Presentation.java
@@ -46,8 +46,8 @@ import org.opengis.feature.Feature;
  *       This would be consistent with legacy GO-1 specification (even if retired, it still have worthy material).
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public abstract class Presentation {
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PropertyNameCollector.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PropertyNameCollector.java
index adc8a41fb7..b6d4f7729c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PropertyNameCollector.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/PropertyNameCollector.java
@@ -29,8 +29,8 @@ import org.opengis.filter.ValueReference;
  * </p>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 final class PropertyNameCollector extends SymbologyVisitor {
     /**
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
index 18427e2c07..c043794985 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
@@ -16,7 +16,8 @@
  */
 package org.apache.sis.internal.map;
 
-import org.opengis.style.Symbolizer;
+import org.apache.sis.internal.style.Symbolizer;
+
 
 /**
  * Resource symbolizers act on a resource as a whole, not on individual features.
@@ -28,9 +29,13 @@ import org.opengis.style.Symbolizer;
  * </p>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
-public interface ResourceSymbolizer extends Symbolizer {
-
+public abstract class ResourceSymbolizer extends Symbolizer {
+    /**
+     * Constructs a new symbolozer.
+     */
+    protected ResourceSymbolizer() {
+    }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
index 6eff0d15a4..90370b7da9 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Predicate;
@@ -56,6 +57,9 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.Query;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.style.FeatureTypeStyle;
+import org.apache.sis.internal.style.Rule;
+import org.apache.sis.internal.style.Symbolizer;
 
 // Branch-dependent imports
 import org.opengis.feature.AttributeType;
@@ -67,10 +71,7 @@ import org.opengis.feature.PropertyType;
 import org.opengis.filter.Filter;
 import org.opengis.filter.FilterFactory;
 import org.opengis.filter.Expression;
-import org.opengis.style.FeatureTypeStyle;
-import org.opengis.style.Rule;
 import org.opengis.style.SemanticType;
-import org.opengis.style.Symbolizer;
 
 // Optional-dependencies (TODO: make library-independent)
 import org.locationtech.jts.geom.Geometry;
@@ -99,8 +100,8 @@ import org.locationtech.jts.geom.Polygon;
  * </ul>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public final class SEPortrayer {
     /**
@@ -244,7 +245,7 @@ public final class SEPortrayer {
         }
         final double seScale = getSEScale(canvas, objToDisp);
         for (FeatureTypeStyle fts : layer.getStyle().featureTypeStyles()) {
-            final List<Rule> rules = getValidRules(fts, seScale, type);
+            final List<Rule<Feature>> rules = getValidRules(fts, seScale, type);
             if (rules.isEmpty()) continue;
 
             // Prepare the renderers.
@@ -253,7 +254,7 @@ public final class SEPortrayer {
                 //resource symbolizers must be alone in a FTS
                 ResourceSymbolizer resourceSymbolizer = null;
                 int count = 0;
-                for (final Rule r : rules) {
+                for (final Rule<Feature> r : rules) {
                     for (final Symbolizer s : r.symbolizers()) {
                         count++;
                         if (s instanceof ResourceSymbolizer) {
@@ -308,7 +309,7 @@ public final class SEPortrayer {
                 final FeatureSet fs = (FeatureSet) resource;
                 // Calculate max symbol size, to expand search envelope.
                 double symbolsMargin = 0.0;
-                for (Rule rule : rules) {
+                for (Rule<Feature> rule : rules) {
                     for (Symbolizer symbolizer : rule.symbolizers()) {
                         symbolsMargin = Math.max(symbolsMargin, marginSolver.apply(canvas, symbolizer));
                     }
@@ -356,12 +357,12 @@ public final class SEPortrayer {
         return stream;
     }
 
-    private static Stream<Presentation> present(Rule rule, MapLayer layer,
+    private static Stream<Presentation> present(Rule<Feature> rule, MapLayer layer,
             Resource resource, Resource refResource, Feature feature)
     {
-        final Filter ruleFilter = rule.getFilter();             // TODO: type.
+        final Filter<Feature> ruleFilter = rule.getFilter();
         //test if the rule is valid for this resource/feature
-        if (ruleFilter == null || ruleFilter.test(feature == null ? resource : feature)) {
+        if (rule.isElseFilter() || ((Filter) ruleFilter).test(feature == null ? resource : feature)) {       // TODO: unsafe cast.
             Stream<Presentation> stream = Stream.empty();
             for (final Symbolizer symbolizer : rule.symbolizers()) {
                 final SEPresentation presentation = new SEPresentation(layer, refResource, feature, symbolizer);
@@ -377,7 +378,7 @@ public final class SEPortrayer {
      * the appropriate bounding box to filter.
      */
     private FeatureQuery prepareQuery(GridGeometry canvas, FeatureSet fs, Set<String> requiredProperties,
-            List<Rule> rules, double symbolsMargin) throws DataStoreException, TransformException
+            List<Rule<Feature>> rules, double symbolsMargin) throws DataStoreException, TransformException
     {
         final FeatureQuery query = new FeatureQuery();
         final FeatureType schema = fs.getType();
@@ -399,7 +400,7 @@ public final class SEPortrayer {
         boolean allDefined = true;
         final Set<Expression<Feature,?>> geomProperties = new HashSet<>();
         if (rules != null) {
-            for (final Rule r : rules) {
+            for (final Rule<Feature> r : rules) {
                 for (final Symbolizer s : r.symbolizers()) {
                     final Expression<Feature,?> expGeom = s.getGeometry();
                     if (expGeom != null) {
@@ -446,13 +447,13 @@ public final class SEPortrayer {
         ruleOpti:
         if (rules != null) {
             final List<Filter<Feature>> rulefilters = new ArrayList<>();
-            for (final Rule rule : rules) {
+            for (final Rule<Feature> rule : rules) {
                 if (rule.isElseFilter()) {
                     // We cannot append styling filters, an else rule match all features.
                     break ruleOpti;
                 } else {
                     final Filter<Feature> rf = rule.getFilter();
-                    if (rf == null || rf == Filter.<Feature>include()) {
+                    if (rf == Filter.<Feature>include()) {
                         // We cannot append styling filters, this rule matchs all features.
                         break ruleOpti;
                     }
@@ -549,18 +550,11 @@ public final class SEPortrayer {
     /**
      * List the valid rules for current scale and type.
      */
-    private static List<Rule> getValidRules(final FeatureTypeStyle fts, final double scale, final FeatureType type) {
-        final Set<GenericName> names = fts.featureTypeNames();
-        if (!names.isEmpty()) {
+    private static List<Rule<Feature>> getValidRules(final FeatureTypeStyle fts, final double scale, final FeatureType type) {
+        final Optional<GenericName> name = fts.getFeatureTypeName();
+        if (name.isPresent()) {
             // TODO: should we check parent types?
-            boolean found = false;
-            for (GenericName name : names) {
-                if (name.equals(type.getName())) {
-                    found = true;
-                    break;
-                }
-            }
-            if (!found) {
+            if (!name.get().equals(type.getName())) {
                 return List.of();
             }
         }
@@ -611,9 +605,9 @@ public final class SEPortrayer {
         //we move to next feature  type if not valid
         //if (typeName != null && !(typeName.equalsIgnoreCase(fts.getFeatureTypeName())) ) continue;
 
-        final List<? extends Rule> rules = fts.rules();
-        final List<Rule> validRules = new ArrayList<>();
-        for (final Rule rule : rules) {
+        final List<? extends Rule<Feature>> rules = fts.rules();
+        final List<Rule<Feature>> validRules = new ArrayList<>();
+        for (final Rule<Feature> rule : rules) {
             //test if the scale is valid for this rule
             if (rule.getMinScaleDenominator() - SE_EPSILON <= scale && rule.getMaxScaleDenominator() + SE_EPSILON > scale) {
                 validRules.add(rule);
@@ -625,9 +619,9 @@ public final class SEPortrayer {
     /**
      * Lists all properties used in given rules.
      */
-    private static Set<String> propertiesNames(final Collection<? extends Rule> rules) {
+    private static Set<String> propertiesNames(final Collection<? extends Rule<Feature>> rules) {
         final PropertyNameCollector collector = new PropertyNameCollector();
-        for (final Rule r : rules) {
+        for (final Rule<Feature> r : rules) {
             collector.visit(r);
             collector.visit(r.getFilter());
         }
@@ -639,10 +633,10 @@ public final class SEPortrayer {
      *
      * @return index of starting else rules.
      */
-    private static int sortByElseRule(final List<Rule> sortedRules){
+    private static int sortByElseRule(final List<Rule<Feature>> sortedRules){
         int elseRuleIndex = sortedRules.size();
         for (int i = 0; i < elseRuleIndex; i++) {
-            final Rule r = sortedRules.get(i);
+            final Rule<Feature> r = sortedRules.get(i);
             if (r.isElseFilter()) {
                 elseRuleIndex--;
                 // Move the rule at the end
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java
index b9f0ead83b..cf0aa7d3fb 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java
@@ -19,8 +19,11 @@ package org.apache.sis.internal.map;
 import java.util.Objects;
 import org.apache.sis.portrayal.MapLayer;
 import org.apache.sis.storage.Resource;
+import org.apache.sis.internal.style.Symbolizer;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
-import org.opengis.style.Symbolizer;
+
 
 /**
  * A presentation build with a standard Symbology Encoding Symbolizer.
@@ -30,8 +33,8 @@ import org.opengis.style.Symbolizer;
  * </p>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public final class SEPresentation extends Presentation {
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
index feea015fd8..42dfd8f3ea 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
@@ -21,40 +21,7 @@ import org.opengis.filter.Filter;
 import org.opengis.filter.Expression;
 import org.opengis.filter.Literal;
 import org.opengis.filter.ValueReference;
-import org.opengis.style.AnchorPoint;
-import org.opengis.style.ChannelSelection;
-import org.opengis.style.ColorMap;
-import org.opengis.style.ColorReplacement;
-import org.opengis.style.ContrastEnhancement;
-import org.opengis.style.Description;
-import org.opengis.style.Displacement;
-import org.opengis.style.ExtensionSymbolizer;
-import org.opengis.style.ExternalGraphic;
-import org.opengis.style.ExternalMark;
-import org.opengis.style.FeatureTypeStyle;
-import org.opengis.style.Fill;
-import org.opengis.style.Font;
-import org.opengis.style.Graphic;
-import org.opengis.style.GraphicFill;
-import org.opengis.style.GraphicLegend;
-import org.opengis.style.GraphicStroke;
-import org.opengis.style.GraphicalSymbol;
-import org.opengis.style.Halo;
-import org.opengis.style.LabelPlacement;
-import org.opengis.style.LinePlacement;
-import org.opengis.style.LineSymbolizer;
-import org.opengis.style.Mark;
-import org.opengis.style.PointPlacement;
-import org.opengis.style.PointSymbolizer;
-import org.opengis.style.PolygonSymbolizer;
-import org.opengis.style.RasterSymbolizer;
-import org.opengis.style.Rule;
-import org.opengis.style.SelectedChannelType;
-import org.opengis.style.ShadedRelief;
-import org.opengis.style.Stroke;
-import org.opengis.style.Style;
-import org.opengis.style.Symbolizer;
-import org.opengis.style.TextSymbolizer;
+import org.apache.sis.internal.style.*;
 
 import static org.apache.sis.internal.util.CollectionsExt.nonNull;
 
@@ -68,8 +35,8 @@ import static org.apache.sis.internal.util.CollectionsExt.nonNull;
  * </p>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 public abstract class SymbologyVisitor {
     protected SymbologyVisitor() {
@@ -87,7 +54,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final Rule candidate) {
+    protected void visit(final Rule<?> candidate) {
         if (candidate != null) {
             nonNull(candidate.symbolizers()).forEach(this::visit);
         }
@@ -104,8 +71,6 @@ public abstract class SymbologyVisitor {
             visit((TextSymbolizer) candidate);
         } else if (candidate instanceof RasterSymbolizer) {
             visit((RasterSymbolizer) candidate);
-        } else if (candidate instanceof ExtensionSymbolizer) {
-            visit((ExtensionSymbolizer) candidate);
         } else if (candidate != null) {
             throw new IllegalArgumentException("Unexpected symbolizer " + candidate);
         }
@@ -131,8 +96,8 @@ public abstract class SymbologyVisitor {
             visit(candidate.getGeometry());
             visit(candidate.getPerpendicularOffset());
             visit(candidate.getDisplacement());
-            visit(candidate.getFill());
-            visit(candidate.getStroke());
+            candidate.getFill().ifPresent(this::visit);
+            candidate.getStroke().ifPresent(this::visit);
         }
     }
 
@@ -142,7 +107,7 @@ public abstract class SymbologyVisitor {
             visit(candidate.getLabel());
             visit(candidate.getFill());
             visit(candidate.getFont());
-            visit(candidate.getHalo());
+            candidate.getHalo().ifPresent(this::visit);
             visit(candidate.getLabelPlacement());
         }
     }
@@ -151,18 +116,17 @@ public abstract class SymbologyVisitor {
         if (candidate != null) {
             visit(candidate.getGeometry());
             visit(candidate.getOpacity());
-            visit(candidate.getChannelSelection());
-            visit(candidate.getColorMap());
-            visit(candidate.getContrastEnhancement());
-            visit(candidate.getImageOutline());
-            visit(candidate.getShadedRelief());
+            candidate.getChannelSelection().ifPresent(this::visit);
+            candidate.getColorMap().ifPresent(this::visit);
+            candidate.getContrastEnhancement().ifPresent(this::visit);
+            candidate.getImageOutline().ifPresent(this::visit);
+            candidate.getShadedRelief().ifPresent(this::visit);
         }
     }
 
-    protected void visit(final ExtensionSymbolizer candidate) {
+    protected void visit(final GraphicalElement candidate) {
         if (candidate != null) {
-            visit(candidate.getGeometry());
-            candidate.getParameters().values().forEach(this::visit);
+            visit(candidate.getGraphic());
         }
     }
 
@@ -189,26 +153,22 @@ public abstract class SymbologyVisitor {
 
     protected void visit(final Mark candidate) {
         if (candidate != null) {
-            visit(candidate.getExternalMark());
-            visit(candidate.getFill());
-            visit(candidate.getStroke());
+            candidate.getFill().ifPresent(this::visit);
+            candidate.getStroke().ifPresent(this::visit);
             visit(candidate.getWellKnownName());
         }
     }
 
     protected void visit(final ExternalGraphic candidate) {
-        nonNull(candidate.getColorReplacements()).forEach(this::visit);
-    }
-
-    protected void visit(final ExternalMark candidate) {
+        nonNull(candidate.colorReplacements()).forEach(this::visit);
     }
 
     protected void visit(final Stroke candidate) {
         if (candidate != null) {
             visit(candidate.getColor());
             visit(candidate.getDashOffset());
-            visit(candidate.getGraphicFill());
-            visit(candidate.getGraphicStroke());
+            candidate.getGraphicFill().ifPresent(this::visit);
+            candidate.getGraphicStroke().ifPresent(this::visit);
             visit(candidate.getLineCap());
             visit(candidate.getLineJoin());
             visit(candidate.getOpacity());
@@ -228,7 +188,7 @@ public abstract class SymbologyVisitor {
 
     protected void visit(final Fill candidate) {
         if (candidate != null) {
-            visit(candidate.getGraphicFill());
+            candidate.getGraphicFill().ifPresent(this::visit);
             visit(candidate.getColor());
             visit(candidate.getOpacity());
         }
@@ -236,7 +196,7 @@ public abstract class SymbologyVisitor {
 
     protected void visit(final Font candidate) {
         if (candidate != null) {
-            candidate.getFamily().forEach(this::visit);
+            candidate.family().forEach(this::visit);
             visit(candidate.getSize());
             visit(candidate.getStyle());
             visit(candidate.getWeight());
@@ -244,12 +204,12 @@ public abstract class SymbologyVisitor {
     }
 
     protected void visit(final GraphicFill candidate) {
-        visit((Graphic) candidate);
+        visit((GraphicalElement) candidate);
     }
 
     protected void visit(final GraphicStroke candidate) {
         if (candidate != null) {
-            visit((Graphic) candidate);
+            visit((GraphicalElement) candidate);
             visit(candidate.getGap());
             visit(candidate.getInitialGap());
         }
@@ -289,7 +249,7 @@ public abstract class SymbologyVisitor {
     }
 
     protected void visit(final GraphicLegend candidate) {
-        visit((Graphic) candidate);
+        visit((GraphicalElement) candidate);
     }
 
     protected void visit(final Halo candidate) {
@@ -300,15 +260,9 @@ public abstract class SymbologyVisitor {
     }
 
     protected void visit(final ColorMap candidate) {
-        if (candidate != null) {
-            visit(candidate.getFunction());
-        }
     }
 
     protected void visit(final ColorReplacement candidate) {
-        if (candidate != null) {
-            visit(candidate.getRecoding());
-        }
     }
 
     protected void visit(final ContrastEnhancement candidate) {
@@ -319,10 +273,9 @@ public abstract class SymbologyVisitor {
 
     protected void visit(final ChannelSelection candidate) {
         if (candidate != null) {
-            visit(candidate.getGrayChannel());
-            final SelectedChannelType[] rgbChannels = candidate.getRGBChannels();
-            if (rgbChannels != null) {
-                for (final SelectedChannelType sct : rgbChannels) {
+            SelectedChannelType[] channels = candidate.getChannels();
+            if (channels != null) {
+                for (final SelectedChannelType sct : channels) {
                     visit(sct);
                 }
             }
@@ -331,7 +284,7 @@ public abstract class SymbologyVisitor {
 
     protected void visit(final SelectedChannelType candidate) {
         if (candidate != null) {
-            visit(candidate.getContrastEnhancement());
+            candidate.getContrastEnhancement().ifPresent(this::visit);
         }
     }
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
index 9cea797094..86fd09bec7 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/package-info.java
@@ -28,7 +28,7 @@
  * Synchronization, if desired, must be done by the caller.
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
- * @since   1.2
+ * @version 1.5
+ * @since   1.5
  */
 package org.apache.sis.internal.map;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
index 56382e3e19..84a6028180 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
@@ -16,91 +16,152 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.apache.sis.util.ArgumentChecks;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import org.opengis.filter.Literal;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.AnchorPoint}.
+ * The location inside a graphic or label to use as an "anchor" for positioning it relative to a point.
+ * The coordinates are given as (<var>x</var>,<var>y</var>) floating-point numbers between 0 and 1 inclusive.
+ * The bounding box of the graphic/label to be rendered is considered to be in a coordinate space from 0
+ * (lower-left corner) to 1 (upper-right corner), and the anchor position is specified as a point in this space.
+ * The default anchor point is <var>x</var> = 0.5, <var>y</var> = 0.5,
+ * which is at the middle height and middle length of the graphic/label.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Ian Turton (CCG)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class AnchorPoint implements org.opengis.style.AnchorPoint {
+@XmlType(name = "AnchorPointType", propOrder = {
+    "anchorPointX",
+    "anchorPointY"
+})
+@XmlRootElement(name = "AnchorPoint")
+public class AnchorPoint extends StyleElement {
+    /**
+     * Literal used as default value.
+     */
+    private static final Literal<Feature,Double> LITERAL_HALF = literal(0.5);;
 
-    private Expression<Feature,? extends Number> x;
-    private Expression<Feature,? extends Number> y;
+    /**
+     * The <var>x</var> coordinate of the anchor point.
+     * This property is mandatory.
+     *
+     * @see #getAnchorPointX()
+     * @see #setAnchorPointX(Expression)
+     */
+    @XmlElement(name = "AnchorPointX", required = true)
+    protected Expression<Feature, ? extends Number> anchorPointX;
 
-    public AnchorPoint() {
-        this(StyleFactory.DEFAULT_ANCHOR_POINT_X, StyleFactory.DEFAULT_ANCHOR_POINT_Y);
-    }
+    /**
+     * The <var>y</var> coordinate of the anchor point.
+     * This property is mandatory.
+     *
+     * @see #getAnchorPointY()
+     * @see #setAnchorPointY(Expression)
+     */
+    @XmlElement(name = "AnchorPointY", required = true)
+    protected Expression<Feature, ? extends Number> anchorPointY;
 
-    public AnchorPoint(Expression<Feature,? extends Number> x, Expression<Feature,? extends Number> y) {
-        ArgumentChecks.ensureNonNull("x", x);
-        ArgumentChecks.ensureNonNull("y", y);
-        this.x = x;
-        this.y = y;
+    /**
+     * Creates a anchor point initialized to <var>x</var> = 0.5 and <var>y</var> = 0.5.
+     * This initial position is the center of the graphic/label.
+     */
+    public AnchorPoint() {
+        anchorPointX = LITERAL_HALF;
+        anchorPointY = LITERAL_HALF;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getAnchorPointX() {
-        return x;
+    /**
+     * Creates a new anchor point initialized to the given position.
+     *
+     * @param  x  the initial <var>x</var> position.
+     * @param  y  the initial <var>y</var> position.
+     */
+    public AnchorPoint(final double x, final double y) {
+        anchorPointX = literal(x);
+        anchorPointY = literal(y);
     }
 
-    public void setAnchorPointX(Expression<Feature,? extends Number> x) {
-        ArgumentChecks.ensureNonNull("x", x);
-        this.x = x;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public AnchorPoint(final AnchorPoint source) {
+        super(source);
+        anchorPointX = source.anchorPointX;
+        anchorPointY = source.anchorPointY;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getAnchorPointY() {
-        return y;
+    /**
+     * Returns the <var>x</var> coordinate of the anchor point.
+     * It should be a floating point number between 0 and 1 inclusive.
+     *
+     * @return the expression fetching the <var>x</var> coordinate.
+     */
+    public Expression<Feature, ? extends Number> getAnchorPointX() {
+        return anchorPointX;
     }
 
-    public void setAnchorPointY(Expression<Feature,? extends Number> y) {
-        ArgumentChecks.ensureNonNull("y", y);
-        this.y = y;
+    /**
+     * Sets the <var>x</var> coordinate of the anchor point.
+     * If this method is never invoked, then the default value is literal 0.5.
+     *
+     * @param  value  new <var>x</var> coordinate, or {@code null} for resetting the default value.
+     */
+    public void setAnchorPointX(final Expression<Feature, ? extends Number> value) {
+        anchorPointX = (value != null) ? value : LITERAL_HALF;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns the <var>y</var> coordinate of the anchor point.
+     * It should be a floating point number between 0 and 1 inclusive.
+     *
+     * @return the expression fetching the <var>y</var> coordinate.
+     */
+    public Expression<Feature, ? extends Number> getAnchorPointY() {
+        return anchorPointY;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(x, y);
+    /**
+     * Sets the <var>y</var> coordinate of the anchor point.
+     * If this method is never invoked, then the default value is literal 0.5.
+     *
+     * @param  value  new <var>y</var> coordinate, or {@code null} for resetting the default value.
+     */
+    public void setAnchorPointY(final Expression<Feature, ? extends Number> value) {
+        anchorPointY = (value != null) ? value : LITERAL_HALF;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final AnchorPoint other = (AnchorPoint) obj;
-        return Objects.equals(this.x, other.x)
-            && Objects.equals(this.y, other.y);
+    final Object[] properties() {
+        return new Object[] {anchorPointX, anchorPointY};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static AnchorPoint castOrCopy(org.opengis.style.AnchorPoint candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof AnchorPoint) {
-            return (AnchorPoint) candidate;
-        }
-        return new AnchorPoint(candidate.getAnchorPointX(), candidate.getAnchorPointY());
+    @Override
+    public AnchorPoint clone() {
+        final var clone = (AnchorPoint) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
index d7f3e816fe..f5ab98095b 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
@@ -16,111 +16,163 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.opengis.style.StyleVisitor;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.apache.sis.util.resources.Errors;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.ChannelSelection}.
+ * Specifies the false-color channel selection for a multi-spectral raster source.
+ * It can be used for rasters such as a multi-band satellite-imagery.
+ * Either red, green, and blue channels are selected, or a single grayscale channel is selected.
+ * Contrast enhancement may be applied to each channel in isolation.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Ian Turton (CCG)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class ChannelSelection implements org.opengis.style.ChannelSelection {
-
-    private SelectedChannelType gray;
-    private SelectedChannelType red;
-    private SelectedChannelType green;
-    private SelectedChannelType blue;
+@XmlType(name = "ChannelSelectionType", propOrder = {
+    "red",
+    "green",
+    "blue",
+    "gray"
+})
+@XmlRootElement(name = "ChannelSelection")
+public class ChannelSelection extends StyleElement {
+    /**
+     * The red channel, or {@code null} if none.
+     * This property is mutually exclusive with {@link #gray}.
+     */
+    @XmlElement(name = "RedChannel")
+    protected SelectedChannelType red;
 
-    public ChannelSelection() {
-    }
+    /**
+     * The green channel, or {@code null} if none.
+     * This property is mutually exclusive with {@link #gray}.
+     */
+    @XmlElement(name = "GreenChannel")
+    protected SelectedChannelType green;
 
-    public ChannelSelection(SelectedChannelType gray) {
-        this.gray = gray;
-    }
+    /**
+     * The blue channel, or {@code null} if none.
+     * This property is mutually exclusive with {@link #gray}.
+     */
+    @XmlElement(name = "BlueChannel")
+    protected SelectedChannelType blue;
 
-    public ChannelSelection(SelectedChannelType red, SelectedChannelType green, SelectedChannelType blue) {
-        this.red = red;
-        this.green = green;
-        this.blue = blue;
-    }
+    /**
+     * The gray channel, or {@code null} if none.
+     * This property is mutually exclusive with {@link #red}, {@link #green} and {@link #blue}.
+     */
+    @XmlElement(name = "GrayChannel")
+    protected SelectedChannelType gray;
 
-    private ChannelSelection(org.opengis.style.SelectedChannelType gray, org.opengis.style.SelectedChannelType[] rgb) {
-        this.gray = SelectedChannelType.castOrCopy(gray);
-        if (rgb != null) {
-            this.red = SelectedChannelType.castOrCopy(rgb[0]);
-            this.green = SelectedChannelType.castOrCopy(rgb[1]);
-            this.blue = SelectedChannelType.castOrCopy(rgb[2]);
-        }
+    /**
+     * Creates an initially empty channel selection.
+     */
+    public ChannelSelection() {
     }
 
-    @Override
-    public SelectedChannelType[] getRGBChannels() {
-        return gray != null ? null :
-                red != null ? new SelectedChannelType[]{red, green, blue} : null;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public ChannelSelection(final ChannelSelection source) {
+        super(source);
+        red   = source.red;
+        green = source.green;
+        blue  = source.blue;
+        gray  = source.gray;
     }
 
-    public void setRGBChannels(SelectedChannelType[] rgb) {
-        if (rgb == null) {
-            this.red = null;
-            this.green = null;
-            this.blue = null;
+    /**
+     * Gets the channels to be used, or {@code null} if none.
+     * If non-null, then the array length is either 1 or 3.
+     * An array of length 1 contains the grayscale channel.
+     * An array of length 3 contains red, green and blue channels, in that order.
+     *
+     * @return array of channels, or {@code null} if none.
+     *
+     * @todo Replace null value by some default value.
+     */
+    public SelectedChannelType[] getChannels() {
+        if (red != null || green != null || blue != null) {
+            return new SelectedChannelType[] {red, green, blue};
+        } else if (gray != null) {
+            return new SelectedChannelType[] {gray};
         } else {
-            this.red = rgb[0];
-            this.green = rgb[1];
-            this.blue = rgb[2];
+            return null;
         }
     }
 
-    @Override
-    public SelectedChannelType getGrayChannel() {
-        return gray;
-    }
-
-    public void setGray(SelectedChannelType gray) {
-        this.gray = gray;
-    }
-
-
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets the channels to be used.
+     * The number of channels shall be 0, 1 or 3.
+     * If the given array is null or empty, then the channel selection is cleared.
+     * If only one channel is specified, then it is interpreted as grayscale.
+     * If three channels are specified, then they are interpreted as red, green and blue channels in that order.
+     * Otherwise an exception is thrown.
+     *
+     * @param  values  array of channels, or {@code null} if none.
+     * @throws IllegalArgumentException if the length of the specified array is not 0, 1 or 3.
+     */
+    public void setChannels(final SelectedChannelType... values) {
+        red   = null;
+        green = null;
+        blue  = null;
+        gray  = null;
+        if (values != null) {
+            switch (values.length) {
+                case 0: break;
+                case 1: gray = values[0]; break;
+                case 3: {
+                    red   = values[0];
+                    green = values[1];
+                    blue  = values[2];
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedArrayLength_2, 3, values.length));
+                }
+            }
+        }
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(gray, red, green, blue);
+    final Object[] properties() {
+        return new Object[] {red, green, blue, gray};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ChannelSelection other = (ChannelSelection) obj;
-        return Objects.equals(this.gray, other.gray)
-            && Objects.equals(this.red, other.red)
-            && Objects.equals(this.green, other.green)
-            && Objects.equals(this.blue, other.blue);
+    public ChannelSelection clone() {
+        final var clone = (ChannelSelection) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static ChannelSelection castOrCopy(org.opengis.style.ChannelSelection candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ChannelSelection) {
-            return (ChannelSelection) candidate;
-        }
-        return new ChannelSelection(candidate.getGrayChannel(), candidate.getRGBChannels());
+    private void selfClone() {
+        if (red   != null) red   = red  .clone();
+        if (green != null) green = green.clone();
+        if (blue  != null) blue  = blue .clone();
+        if (gray  != null) gray  = gray .clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
index ac5e3bc26b..6b4cfc2a49 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
@@ -16,73 +16,66 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.ColorMap}.
+ * Mapping of fixed-numeric pixel values to colors.
+ * It can be used for defining the colors of a palette-type raster source.
+ * For example, a DEM raster giving elevations in meters above sea level
+ * can be translated to a colored image.
+ *
+ * <p>This is a placeholder for future development.
+ * OGC 05-077r4 standard defines the dependent classes,
+ * but there is too many of them for this initial draft.</p>
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class ColorMap implements org.opengis.style.ColorMap {
-
-    private Expression<Feature,?> function;
-
+@XmlType(name = "ColorMapType", propOrder = {
+//  "categorize",
+//  "interpolate",
+//  "jenks"
+})
+@XmlRootElement(name = "ColorMap")
+public class ColorMap extends StyleElement {
+    /**
+     * Creates a color map.
+     */
     public ColorMap() {
     }
 
-    public ColorMap(Expression<Feature, ?> function) {
-        this.function = function;
-    }
-
-    @Override
-    public Expression<Feature,?> getFunction() {
-        return function;
-    }
-
-    public void setFunction(Expression<Feature, ?> function) {
-        this.function = function;
-    }
-
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(function);
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public ColorMap(final ColorMap source) {
+        super(source);
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ColorMap other = (ColorMap) obj;
-        return Objects.equals(this.function, other.function);
+    final Object[] properties() {
+        return new Object[] {};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static ColorMap castOrCopy(org.opengis.style.ColorMap candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ColorMap) {
-            return (ColorMap) candidate;
-        }
-        return new ColorMap(candidate.getFunction());
+    @Override
+    public ColorMap clone() {
+        final var clone = (ColorMap) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
index 8f2636ec77..aca082aaac 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
@@ -16,73 +16,59 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.ColorReplacement}.
+ * Replacement of a color in an external graphic.
+ *
+ * <p>This is a placeholder for future development.
+ * OGC 05-077r4 standard defines the dependent classes,
+ * but there is too many of them for this initial draft.</p>
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class ColorReplacement implements org.opengis.style.ColorReplacement {
-
-    private Expression<Feature,?> recoding;
-
+@XmlType(name = "ColorReplacementType")
+@XmlRootElement(name = "ColorReplacement")
+public class ColorReplacement extends StyleElement {
+    /**
+     * Creates a color replacement.
+     */
     public ColorReplacement() {
     }
 
-    public ColorReplacement(Expression<Feature, ?> recoding) {
-        this.recoding = recoding;
-    }
-
-    @Override
-    public Expression<Feature,?> getRecoding() {
-        return recoding;
-    }
-
-    public void setRecoding(Expression<Feature, ?> recoding) {
-        this.recoding = recoding;
-    }
-
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(recoding);
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public ColorReplacement(final ColorReplacement source) {
+        super(source);
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ColorReplacement other = (ColorReplacement) obj;
-        return Objects.equals(this.recoding, other.recoding);
+    final Object[] properties() {
+        return new Object[] {};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static ColorReplacement castOrCopy(org.opengis.style.ColorReplacement candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ColorReplacement) {
-            return (ColorReplacement) candidate;
-        }
-        return new ColorReplacement(candidate.getRecoding());
+    @Override
+    public ColorReplacement clone() {
+        final var clone = (ColorReplacement) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
index 9c47652c47..5dc499ae52 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
@@ -16,92 +16,143 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.apache.sis.util.ArgumentChecks;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 import org.opengis.style.ContrastMethod;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.ContrastEnhancement}.
+ * Contrast enhancement for an image channel.
+ * In the case of a color image, the relative grayscale brightness of a pixel color is used.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Ian Turton (CCG)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class ContrastEnhancement implements org.opengis.style.ContrastEnhancement {
+@XmlType(name = "ContrastEnhancementType", propOrder = {
+//  "normalize",
+//  "histogram",
+    "gammaValue"
+})
+@XmlRootElement(name = "ContrastEnhancement")
+public class ContrastEnhancement extends StyleElement {
+    /**
+     * Method to use for applying contrast enhancement, or {@code null} for the default value.
+     * The default value depends on whether or not a {@linkplain #gammaValue gamma value} is defined.
+     *
+     * @see #getMethod()
+     * @see #setMethod(ContrastMethod)
+     *
+     * @todo Marshall as empty "Normalize" or "Histogram" XML element.
+     */
+    protected ContrastMethod method;
 
-    private ContrastMethod method;
-    private Expression<Feature, ? extends Number> gammaValue;
+    /**
+     * How much to brighten or dim an image, or {@code null} for the default value.
+     *
+     * @see #getGammaValue()
+     * @see #setGammaValue(Expression)
+     *
+     * @todo Add a JAXB adapter for marshalling as a plain number.
+     */
+    @XmlElement(name = "GammaValue")
+    protected Expression<Feature, ? extends Number> gammaValue;
 
+    /**
+     * Creates a contrast enhancement initialized to no operation.
+     */
     public ContrastEnhancement() {
-        this(ContrastMethod.NONE, StyleFactory.LITERAL_ONE);
     }
 
-    public ContrastEnhancement(ContrastMethod method, Expression<Feature, ? extends Number> gammaValue) {
-        ArgumentChecks.ensureNonNull("method", method);
-        ArgumentChecks.ensureNonNull("gammaValue", gammaValue);
-        this.method = method;
-        this.gammaValue = gammaValue;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public ContrastEnhancement(final ContrastEnhancement source) {
+        super(source);
+        method     = source.method;
+        gammaValue = source.gammaValue;
     }
 
-    @Override
+    /**
+     * Returns the method to use for applying contrast enhancement.
+     *
+     * @return method to use for applying contrast enhancement.
+     */
     public ContrastMethod getMethod() {
-        return method;
+        final var value = method;
+        if (value != null) {
+            return value;
+        }
+        return (gammaValue != null) ? ContrastMethod.GAMMA : ContrastMethod.NONE;
     }
 
-    public void setMethod(ContrastMethod method) {
-        ArgumentChecks.ensureNonNull("method", method);
-        this.method = method;
+    /**
+     * Sets the method to use for applying contrast enhancement.
+     * Setting this method to anything else than {@link ContrastMethod#GAMMA}
+     * clears the {@linkplain #getGammaValue() gamma value}.
+     *
+     * @param  value  new method to use, or {@code null} for none.
+     */
+    public void setMethod(final ContrastMethod value) {
+        method = value;
+        if (value != ContrastMethod.GAMMA) {
+            gammaValue = null;
+        }
     }
 
-    @Override
+    /**
+     * Tells how much to brighten (values greater than 1) or dim (values less than 1) an image.
+     * A value of 1 means no change.
+     *
+     * @return expression to control gamma adjustment.
+     */
     public Expression<Feature, ? extends Number> getGammaValue() {
-        return gammaValue;
+        return defaultToOne(gammaValue);
     }
 
-    public void setGammaValue(Expression<Feature,? extends Number> gammaValue) {
-        ArgumentChecks.ensureNonNull("gammaValue", gammaValue);
-        this.gammaValue = gammaValue;
-    }
-
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(method, gammaValue);
+    /**
+     * Sets how much to brighten (values greater than 1) or dim (values less than 1) an image.
+     * Setting a non-null value sets the method to {@link ContrastMethod#GAMMA}.
+     * If this method is never invoked, then the default value is literal 1.
+     *
+     * @param  value  new expression to control gamma adjustment, or {@code null} for the default.
+     */
+    public void setGammaValue(final Expression<Feature, ? extends Number> value) {
+        gammaValue = value;
+        if (value != null) {
+            method = null;
+        }
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ContrastEnhancement other = (ContrastEnhancement) obj;
-        return Objects.equals(this.method, other.method)
-            && Objects.equals(this.gammaValue, other.gammaValue);
+    final Object[] properties() {
+        return new Object[] {method, gammaValue};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static ContrastEnhancement castOrCopy(org.opengis.style.ContrastEnhancement candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ContrastEnhancement) {
-            return (ContrastEnhancement) candidate;
-        }
-        return new ContrastEnhancement(candidate.getMethod(), candidate.getGammaValue());
+    @Override
+    public ContrastEnhancement clone() {
+        final var clone = (ContrastEnhancement) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
index f64331e809..4b121a2aa3 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
@@ -16,90 +16,130 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.apache.sis.util.ArgumentChecks;
-import org.opengis.style.StyleVisitor;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
 import org.opengis.util.InternationalString;
 
+
 /**
- * Mutable implementation of {@link org.opengis.style.Description}.
+ * Informative description of a style object being defined.
+ * Description values are mostly used in User Interfaces (lists, trees, …).
+ *
+ * <p>Note that most style object also have a name.
+ * But the name is not part of the description because a name
+ * has a functional use that is more than just descriptive.</p>
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Description implements org.opengis.style.Description {
+@XmlType(name = "DescriptionType", propOrder = {
+    "title",
+    "summary"
+})
+@XmlRootElement(name = "Description")
+public class Description extends StyleElement {
+    /**
+     * Human readable title of the style, or {@code null} if none.
+     *
+     * @see #getTitle()
+     * @see #setTitle(InternationalString)
+     */
+    @XmlElement(name = "Title")
+    protected InternationalString title;
 
-    private InternationalString title;
-    private InternationalString abstrat;
+    /**
+     * Human readable, prose description of this style, or {@code null} if none.
+     * This field should be named "abstract", but is named otherwise because "abstract" is a Java keyword.
+     *
+     * @see #getAbstract()
+     * @see #setAbstract(InternationalString)
+     */
+    @XmlElement(name = "Abstract")
+    protected InternationalString summary;
 
+    /**
+     * Creates an initially empty description.
+     */
     public Description() {
-        this(StyleFactory.EMPTY_STRING, StyleFactory.EMPTY_STRING);
-    }
-
-    public Description(InternationalString title, InternationalString abstrat) {
-        ArgumentChecks.ensureNonNull("title", title);
-        ArgumentChecks.ensureNonNull("abstrat", abstrat);
-        this.title = title;
-        this.abstrat = abstrat;
     }
 
-    @Override
-    public InternationalString getTitle() {
-        return title;
-    }
-
-    public void setTitle(InternationalString title) {
-        ArgumentChecks.ensureNonNull("title", title);
-        this.title = title;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Description(final Description source) {
+        super(source);
+        title   = source.title;
+        summary = source.summary;
     }
 
-    @Override
-    public InternationalString getAbstract() {
-        return abstrat;
+    /**
+     * Returns the human readable title of the style.
+     * This can be any string, but should be fairly short as it is intended to
+     * be used in list boxes or drop down menus or other selection interfaces.
+     *
+     * @return the human readable title of the style.
+     */
+    public Optional<InternationalString> getTitle() {
+        return Optional.ofNullable(title);
     }
 
-    public void setAbstract(InternationalString abstrat) {
-        ArgumentChecks.ensureNonNull("abstrat", abstrat);
-        this.abstrat = abstrat;
+    /**
+     * Sets a human readable title of the style.
+     * If this method is never invoked, then the default value is {@code null}.
+     *
+     * @param  value  new human readable title of the style, or {@code null} if none.
+     */
+    public void setTitle(final InternationalString value) {
+        title = value;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns a human readable, prose description of this style.
+     * This can be any string and can consist of any amount of text.
+     *
+     * @return a human readable, prose description of this style.
+     */
+    public Optional<InternationalString> getAbstract() {
+        return Optional.ofNullable(summary);
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(title, abstrat);
+    /**
+     * Sets a human readable, prose description of this style.
+     * If this method is never invoked, then the default value is {@code null}.
+     *
+     * @param  value  new human readable, prose description of this style, or {@code null} if none.
+     */
+    public void setAbstract(final InternationalString value) {
+        summary = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Description other = (Description) obj;
-        return Objects.equals(this.title, other.title)
-            && Objects.equals(this.abstrat, other.abstrat);
+    final Object[] properties() {
+        return new Object[] {title, summary};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a clone of this object.
+     * The {@link InternationalString} members are assumed immutable and not cloned.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return a clone of this object.
      */
-    public static Description castOrCopy(org.opengis.style.Description candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Description) {
-            return (Description) candidate;
-        }
-        return new Description(candidate.getTitle(), candidate.getAbstract());
+    @Override
+    public Description clone() {
+        final var clone = (Description) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
index b85202cac5..2afb41e7d0 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
@@ -16,91 +16,149 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.apache.sis.util.ArgumentChecks;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Displacement}.
+ * A two-dimensional displacements from the original geometry.
+ * Displacements may be used to avoid over-plotting, or for supplying shadows.
+ * The displacements units depend on the context:
+ * in {@linkplain Symbolizer#getUnitOfMeasure() symbolizer unit of measurements}
+ * when the displacement is applied by a {@link PolygonSymbolizer},
+ * but in pixels when the displacement is applied by a {@link TextSymbolizer}.
+ * The displacements are to the right of the point.
+ * The default displacement is <var>x</var> = 0, <var>y</var> = 0,
+ *
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Ian Turton (CCG)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  *
- * @author Johann Sorel (Geomatys)
+ * @see PointPlacement#getDisplacement()
+ * @see Graphic#getDisplacement()
+ * @see PolygonSymbolizer#getDisplacement()
  */
-public final class Displacement implements org.opengis.style.Displacement {
+@XmlType(name = "DisplacementType", propOrder = {
+    "displacementX",
+    "displacementY"
+})
+@XmlRootElement(name = "Displacement")
+public class Displacement extends StyleElement {
+    /**
+     * The <var>x</var> offset from the geometry point.
+     * This property is mandatory.
+     *
+     * @see #getDisplacementX()
+     * @see #setDisplacementX(Expression)
+     */
+    @XmlElement(name = "DisplacementX", required = true)
+    protected Expression<Feature, ? extends Number> displacementX;
 
-    private Expression<Feature,? extends Number> displacementX;
-    private Expression<Feature,? extends Number> displacementY;
+    /**
+     * The <var>y</var> offset from the geometry point.
+     * This property is mandatory.
+     *
+     * @see #getDisplacementY()
+     * @see #setDisplacementY(Expression)
+     */
+    @XmlElement(name = "DisplacementY", required = true)
+    protected Expression<Feature, ? extends Number> displacementY;
 
+    /**
+     * Creates a displacement initialized to zero offsets.
+     */
     public Displacement() {
-        this(StyleFactory.DEFAULT_DISPLACEMENT_X, StyleFactory.DEFAULT_DISPLACEMENT_Y);
+        displacementX = LITERAL_ZERO;
+        displacementY = LITERAL_ZERO;
     }
 
-    public Displacement(Expression<Feature, ? extends Number> displacementX, Expression<Feature, ? extends Number> displacementY) {
-        ArgumentChecks.ensureNonNull("displacementX", displacementX);
-        ArgumentChecks.ensureNonNull("displacementY", displacementY);
-        this.displacementX = displacementX;
-        this.displacementY = displacementY;
-    }
-
-    @Override
-    public Expression<Feature,? extends Number> getDisplacementX() {
-        return displacementX;
+    /**
+     * Creates a new displacement initialized to the given offsets.
+     *
+     * @param  x  the initial <var>x</var> displacement.
+     * @param  y  the initial <var>y</var> displacement.
+     */
+    public Displacement(final double x, final double y) {
+        displacementX = literal(x);
+        displacementY = literal(y);
     }
 
-    public void setDisplacementX(Expression<Feature, ? extends Number> displacementX) {
-        ArgumentChecks.ensureNonNull("displacementX", displacementX);
-        this.displacementX = displacementX;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Displacement(final Displacement source) {
+        super(source);
+        displacementX = source.displacementX;
+        displacementY = source.displacementY;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getDisplacementY() {
-        return displacementY;
+    /**
+     * Returns an expression fetching an <var>x</var> offset from the geometry point.
+     *
+     * @return <var>x</var> offset from the geometry point.
+     */
+    public Expression<Feature, ? extends Number> getDisplacementX() {
+        return displacementX;
     }
 
-    public void setDisplacementY(Expression<Feature, ? extends Number> displacementY) {
-        ArgumentChecks.ensureNonNull("displacementY", displacementY);
-        this.displacementY = displacementY;
+    /**
+     * Sets the expression fetching an <var>x</var> offset.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new <var>x</var> offset, or {@code null} for resetting the default value.
+     */
+    public void setDisplacementX(final Expression<Feature, ? extends Number> value) {
+        displacementX = defaultToZero(value);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns an expression fetching an <var>y</var> offset from the geometry point.
+     *
+     * @return <var>y</var> offset from the geometry point.
+     */
+    public Expression<Feature, ? extends Number> getDisplacementY() {
+        return displacementY;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(displacementX, displacementY);
+    /**
+     * Sets the expression fetching an <var>y</var> offset.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new <var>y</var> offset, or {@code null} for resetting the default value.
+     */
+    public void setDisplacementY(final Expression<Feature, ? extends Number> value) {
+        displacementY = defaultToZero(value);
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Displacement other = (Displacement) obj;
-        return Objects.equals(this.displacementX, other.displacementX)
-            && Objects.equals(this.displacementY, other.displacementY);
+    final Object[] properties() {
+        return new Object[] {displacementX, displacementY};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static Displacement castOrCopy(org.opengis.style.Displacement candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Displacement) {
-            return (Displacement) candidate;
-        }
-        return new Displacement(candidate.getDisplacementX(), candidate.getDisplacementY());
+    @Override
+    public Displacement clone() {
+        final var clone = (Displacement) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
index a141adcb82..be2d6fe82d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
@@ -16,122 +16,111 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
-import java.util.Objects;
-import javax.swing.Icon;
-import org.opengis.metadata.citation.OnlineResource;
-import org.opengis.style.StyleVisitor;
+import java.util.ArrayList;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.ExternalGraphic}.
+ * Reference to an external file that contains an image of some kind, such as a PNG or SVG.
+ * This is an alternative to {@link Mark} for {@linkplain Graphic#graphicalSymbols graphical symbols}.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class ExternalGraphic implements org.opengis.style.ExternalGraphic, GraphicalSymbol {
-
-    private OnlineResource onlineResource;
-    private Icon inlineContent;
-    private String format;
-    private Collection<ColorReplacement> colorReplacements;
+@XmlType(name = "ExternalGraphicType", propOrder = {
+//  "onlineResource",       // XML encoding not yet available.
+//  "inlineContent",        // Idem.
+    "format",
+    "colorReplacements"
+})
+@XmlRootElement(name = "ExternalGraphic")
+public class ExternalGraphic extends GraphicalSymbol {
+    /**
+     * A list of colors to replace, or {@code null} if none.
+     *
+     * <h4>XML marshalling</h4>
+     * A null value is not synonymous to an empty list.
+     * An empty list causes an empty {@code <ColorReplacement/>} element to be marshalled,
+     * while a {@code null} value results in no element being marshalled.
+     *
+     * @see #colorReplacements()
+     */
+    @XmlElement(name = "ColorReplacement")
+    protected List<ColorReplacement> colorReplacements;
 
+    /**
+     * Creates an initially empty external graphic.
+     */
     public ExternalGraphic() {
     }
 
-    public ExternalGraphic(OnlineResource onlineResource, Icon inlineContent, String format, Collection<ColorReplacement> colorReplacements) {
-        this.onlineResource = onlineResource;
-        this.inlineContent = inlineContent;
-        this.format = format;
-        this.colorReplacements = colorReplacements;
-    }
-
-    @Override
-    public OnlineResource getOnlineResource() {
-        return onlineResource;
-    }
-
-    public void setOnlineResource(OnlineResource onlineResource) {
-        this.onlineResource = onlineResource;
-    }
-
-    @Override
-    public Icon getInlineContent() {
-        return inlineContent;
-    }
-
-    public void setInlineContent(Icon inlineContent) {
-        this.inlineContent = inlineContent;
-    }
-
-    @Override
-    public String getFormat() {
-        return format;
-    }
-
-    public void setFormat(String format) {
-        this.format = format;
-    }
-
-    @Override
-    public Collection<org.opengis.style.ColorReplacement> getColorReplacements() {
-        return (Collection) colorReplacements;
-    }
-
-    public void setColorReplacements(Collection<ColorReplacement> colorReplacements) {
-        this.colorReplacements = colorReplacements;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public ExternalGraphic(final ExternalGraphic source) {
+        super(source);
+        final var value = source.colorReplacements;
+        if (value != null) {
+            colorReplacements = new ArrayList<>(value);
+        }
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns a list of colors to replace.
+     *
+     * <p>The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.</p>
+     *
+     * @return list of colors to replace, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<ColorReplacement> colorReplacements() {
+        if (colorReplacements == null) {
+            colorReplacements = new ArrayList<>();
+        }
+        return colorReplacements;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(onlineResource, inlineContent, format, colorReplacements);
+    final Object[] properties() {
+        return new Object[] {onlineResource, inlineContent, format, colorReplacements};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     * ISO 19115 metadata instances are also shared (not cloned).
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ExternalGraphic other = (ExternalGraphic) obj;
-        return Objects.equals(this.format, other.format)
-            && Objects.equals(this.onlineResource, other.onlineResource)
-            && Objects.equals(this.inlineContent, other.inlineContent)
-            && Objects.equals(this.colorReplacements, other.colorReplacements);
+    public ExternalGraphic clone() {
+        final var clone = (ExternalGraphic) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static ExternalGraphic castOrCopy(org.opengis.style.ExternalGraphic candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ExternalGraphic) {
-            return (ExternalGraphic) candidate;
+    private void selfClone() {
+        if (colorReplacements != null) {
+            colorReplacements = new ArrayList<>(colorReplacements);
+            colorReplacements.replaceAll(ColorReplacement::clone);
         }
-
-        final List<ColorReplacement> cs = new ArrayList<>();
-        for (org.opengis.style.ColorReplacement cr : candidate.getColorReplacements()) {
-            cs.add(ColorReplacement.castOrCopy(cr));
-        }
-
-        return new ExternalGraphic(
-                candidate.getOnlineResource(),
-                candidate.getInlineContent(),
-                candidate.getFormat(),
-                cs);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java
deleted file mode 100644
index c2f9fc8b81..0000000000
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.style;
-
-import java.util.Objects;
-import javax.swing.Icon;
-import org.opengis.metadata.citation.OnlineResource;
-import org.opengis.style.StyleVisitor;
-
-/**
- * Mutable implementation of {@link org.opengis.style.ExternalMark}.
- *
- * @author Johann Sorel (Geomatys)
- */
-public final class ExternalMark implements org.opengis.style.ExternalMark {
-
-    private OnlineResource onlineResource;
-    private Icon inlineContent;
-    private String format;
-    private int markIndex;
-
-    public ExternalMark() {
-    }
-
-    public ExternalMark(OnlineResource onlineResource, Icon inlineContent, String format, int markIndex) {
-        this.onlineResource = onlineResource;
-        this.inlineContent = inlineContent;
-        this.format = format;
-        this.markIndex = markIndex;
-    }
-
-    @Override
-    public OnlineResource getOnlineResource() {
-        return onlineResource;
-    }
-
-    public void setOnlineResource(OnlineResource onlineResource) {
-        this.onlineResource = onlineResource;
-    }
-
-    @Override
-    public Icon getInlineContent() {
-        return inlineContent;
-    }
-
-    public void setInlineContent(Icon inlineContent) {
-        this.inlineContent = inlineContent;
-    }
-
-    @Override
-    public String getFormat() {
-        return format;
-    }
-
-    public void setFormat(String format) {
-        this.format = format;
-    }
-
-    @Override
-    public int getMarkIndex() {
-        return markIndex;
-    }
-
-    public void setMarkIndex(int markIndex) {
-        this.markIndex = markIndex;
-    }
-
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(onlineResource, inlineContent, format, markIndex);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ExternalMark other = (ExternalMark) obj;
-        return this.markIndex == other.markIndex
-            && Objects.equals(this.format, other.format)
-            && Objects.equals(this.onlineResource, other.onlineResource)
-            && Objects.equals(this.inlineContent, other.inlineContent);
-    }
-
-    /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
-     */
-    public static ExternalMark castOrCopy(org.opengis.style.ExternalMark candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ExternalMark) {
-            return (ExternalMark) candidate;
-        }
-        return new ExternalMark(
-                candidate.getOnlineResource(),
-                candidate.getInlineContent(),
-                candidate.getFormat(),
-                candidate.getMarkIndex());
-    }
-}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
index f30a3e54d9..03251ed174 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
@@ -16,153 +16,322 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Objects;
 import java.util.Set;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Optional;
+import jakarta.xml.bind.Unmarshaller;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.opengis.util.GenericName;
+import org.apache.sis.util.collection.CodeListSet;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
 import org.opengis.filter.ResourceId;
-import org.opengis.metadata.citation.OnlineResource;
 import org.opengis.style.SemanticType;
-import org.opengis.style.StyleVisitor;
-import org.opengis.util.GenericName;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.FeatureTypeStyle}.
+ * Defines the styling that is to be applied to a single feature type.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class FeatureTypeStyle implements org.opengis.style.FeatureTypeStyle {
+@XmlType(name = "FeatureTypeStyleType", propOrder = {
+    "name",
+    "description",
+    "featureTypeName",
+//  "semanticTypeIdentifiers",
+    "rules"
+})
+@XmlRootElement(name = "FeatureTypeStyle")
+public class FeatureTypeStyle extends StyleElement {
+    /**
+     * Version number of the Symbology Encoding standard used.
+     * This value should not be changed, unless this style has been unmarshalled
+     * from a XML document that specifies a different version number.
+     *
+     * @see #getVersion()
+     */
+    @XmlAttribute
+    protected String version;
+
+    /**
+     * Name for this style, or {@code null} if none.
+     *
+     * @see #getName()
+     * @see #setName(String)
+     */
+    @XmlElement(name = "Name")
+    protected String name;
+
+    /**
+     * Information for user interfaces, or {@code null} if none.
+     *
+     * @see #getDescription()
+     * @see #setDescription(Description)
+     */
+    @XmlElement(name = "Description")
+    protected Description description;
+
+    /**
+     * Identification of feature instances on which to apply the style, or {@code null} if none.
+     *
+     * @see #getFeatureInstanceIDs()
+     * @see #setFeatureInstanceIDs(ResourceId)
+     */
+    protected ResourceId<? super Feature> featureInstanceIDs;
+
+    /**
+     * Name of the feature type that this style is meant to act upon, or {@code null} if none.
+     *
+     * @see #getFeatureTypeName()
+     * @see #setFeatureTypeName(GenericName)
+     */
+    @XmlElement(name = "FeatureTypeName")
+    protected GenericName featureTypeName;
 
-    private String name;
-    private Description description;
-    private ResourceId featureInstanceIDs;
-    private final Set<GenericName> featureTypeNames = new HashSet<>();
-    private final Set<SemanticType> semanticTypeIdentifiers = new HashSet<>();
-    private final List<Rule> rules = new ArrayList<>();
-    private OnlineResource onlineResource;
+    /**
+     * Types of geometry that this style is meant to act upon, as a live collection.
+     * May be empty but never null.
+     *
+     * @see #semanticTypeIdentifiers()
+     */
+//  @XmlElement(name = "SemanticTypeIdentifier")
+    private CodeListSet<SemanticType> semanticTypeIdentifiers;
+
+    /**
+     * List of rules.
+     *
+     * <h4>Online rules</h4>
+     * A XML document may contain links to rules instead of a full definitions.
+     * Such {@code se:OnlineResource} elements are handled at marshalling time.
+     * When reading a XML document, the rule links are resolved automatically.
+     * When writing a XML document, some rules may be replaced by online resources
+     * if {@link Rule#getOnlineSource()} is provided.
+     *
+     * @todo JAXB adapter for handling the online case is not yet written.
+     *
+     * @see #rules()
+     */
+    @XmlElement(name = "Rule")
+    private List<Rule<Feature>> rules;
+
+    /**
+     * Invoked by JAXB before unmarshalling this mark.
+     * Avoid giving the false impression that the XML document contained a version string.
+     */
+    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
+        version = "unspecified";
+    }
 
+    /**
+     * Creates an initially empty feature type style.
+     * Callers should set the following properties after construction:
+     *
+     * <ul>
+     *   <li>Either the {@linkplain #setFeatureTypeName feature type name}
+     *       or {@linkplain #semanticTypeIdentifiers() semantic type identifiers}, or both.</li>
+     *   <li>At least one {@linkplain #rules() rule} should be added.</li>
+     * </ul>
+     */
     public FeatureTypeStyle() {
+        version = VERSION;
+        semanticTypeIdentifiers = new CodeListSet<>(SemanticType.class);
+        rules = new ArrayList<>();
     }
 
-    public FeatureTypeStyle(String name, Description description, ResourceId resourceId, Set<GenericName> featureTypeNames, Set<SemanticType> semanticTypeIdentifiers, List<Rule> rules, OnlineResource OnlineResource) {
-        this.name = name;
-        this.description = description;
-        this.featureInstanceIDs = resourceId;
-        if (featureTypeNames != null) this.featureTypeNames.addAll(featureTypeNames);
-        if (semanticTypeIdentifiers != null) this.semanticTypeIdentifiers.addAll(semanticTypeIdentifiers);
-        if (rules != null) this.rules.addAll(rules);
-        this.onlineResource = OnlineResource;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public FeatureTypeStyle(final FeatureTypeStyle source) {
+        super(source);
+        version                 = source.version;
+        name                    = source.name;
+        description             = source.description;
+        featureInstanceIDs      = source.featureInstanceIDs;
+        featureTypeName         = source.featureTypeName;
+        semanticTypeIdentifiers = source.semanticTypeIdentifiers.clone();
+        rules                   = new ArrayList<>(source.rules);
     }
 
-    @Override
-    public String getName() {
-        return name;
+    /**
+     * Returns the version number of the Symbology Encoding standard used.
+     * This is fixed to {@value #VERSION} in current Apache SIS release.
+     * This value cannot be changed, unless this style has been unmarshalled
+     * from a XML document that specifies a different version number.
+     *
+     * @return version number of the Symbology Encoding standard used.
+     */
+    public String getVersion() {
+        return version;
     }
 
-    public void setName(String name) {
-        this.name = name;
+    /**
+     * Returns the name for this style.
+     * This can be any string that uniquely identifies this style within a given canvas.
+     * It is not meant to be human-friendly. For a human-friendly label,
+     * see the {@linkplain Description#getTitle() title} instead.
+     *
+     * @return a name for this style.
+     */
+    public Optional<String> getName() {
+        return Optional.ofNullable(name);
     }
 
-    @Override
-    public Description getDescription() {
-        return description;
+    /**
+     * Sets a name for this style.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new name for this style.
+     */
+    public void setName(final String value) {
+        name = value;
     }
 
-    public void setDescription(Description description) {
-        this.description = description;
+    /**
+     * Returns the description of this style.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this style, and conversely.
+     *
+     * @return information for user interfaces.
+     */
+    public Optional<Description> getDescription() {
+        return Optional.ofNullable(description);
     }
 
-    @Override
-    public ResourceId getFeatureInstanceIDs() {
-        return featureInstanceIDs;
+    /**
+     * Sets a description of this style.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new information for user interfaces, or {@code null} if none.
+     */
+    public void setDescription(final Description value) {
+        description = value;
     }
 
-    public void setFeatureInstanceIDs(ResourceId featureInstanceIDs) {
-        this.featureInstanceIDs = featureInstanceIDs;
+    /**
+     * Returns an identification of feature instances on which to apply the style.
+     * This method enable the possibility to use a feature type style on a given list
+     * of features only, instead of all instances of the feature type.
+     *
+     * @return identification of the feature instances.
+     */
+    public Optional<ResourceId<? super Feature>> getFeatureInstanceIDs() {
+        return Optional.ofNullable(featureInstanceIDs);
     }
 
-    @Override
-    public Set<GenericName> featureTypeNames() {
-        return featureTypeNames;
+    /**
+     * Sets an identification of feature instances on which to apply the style.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new identification of feature instances, or {@code null} if none.
+     */
+    public void setFeatureInstanceIDs(final ResourceId<? super Feature> value) {
+        featureInstanceIDs = value;
     }
 
-    @Override
-    public Set<SemanticType> semanticTypeIdentifiers() {
-        return semanticTypeIdentifiers;
+    /**
+     * Returns the name of the feature type that this style is meant to act upon.
+     * It is allowed to be null but only if the feature type can be inferred by other means,
+     * for example from context or using {@link SemanticType} identifiers.
+     *
+     * @return name of the feature type that this style is meant to act upon.
+     */
+    public Optional<GenericName> getFeatureTypeName() {
+        return Optional.ofNullable(featureTypeName);
     }
 
-    @Override
-    public List<Rule> rules() {
-        return rules;
+    /**
+     * Sets the name of the feature type that this style is meant to act upon.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new name of the feature type, or {@code null} if none.
+     */
+    public void setFeatureTypeName(final GenericName value) {
+        featureTypeName = value;
     }
 
-    @Override
-    public OnlineResource getOnlineResource() {
-        return onlineResource;
+    /**
+     * Returns the most general types of geometry that this style is meant to act upon.
+     * The syntax is currently undefined, but the following values are reserved to indicate
+     * that the style applies to feature with default geometry of specific type:
+     *
+     * <ul>
+     *   <li>{@code generic:point}</li>
+     *   <li>{@code generic:line}</li>
+     *   <li>{@code generic:polygon}</li>
+     *   <li>{@code generic:text}</li>
+     *   <li>{@code generic:raster}</li>
+     *   <li>{@code generic:any}</li>
+     * </ul>
+     *
+     * <p>The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.</p>
+     *
+     * @return types of geometry that this style is meant to act upon, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public Set<SemanticType> semanticTypeIdentifiers() {
+        return semanticTypeIdentifiers;
     }
 
-    public void setOnlineResource(OnlineResource OnlineResource) {
-        this.onlineResource = OnlineResource;
+    /**
+     * Returns the list of rules contained by this style.
+     * Order matter: the first item in a list will be the
+     * first item plotted and hence appears on the bottom.
+     *
+     * <p>The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.</p>
+     *
+     * @return ordered list of rules, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<Rule<Feature>> rules() {
+        return rules;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    final Object[] properties() {
+        return new Object[] {name, description, featureInstanceIDs, featureTypeName, semanticTypeIdentifiers, rules};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(name, description, featureInstanceIDs, featureTypeNames, semanticTypeIdentifiers, rules, onlineResource);
+    public FeatureTypeStyle clone() {
+        final var clone = (FeatureTypeStyle) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final FeatureTypeStyle other = (FeatureTypeStyle) obj;
-        return Objects.equals(this.name, other.name)
-            && Objects.equals(this.description, other.description)
-            && Objects.equals(this.featureInstanceIDs, other.featureInstanceIDs)
-            && Objects.equals(this.featureTypeNames, other.featureTypeNames)
-            && Objects.equals(this.semanticTypeIdentifiers, other.semanticTypeIdentifiers)
-            && Objects.equals(this.rules, other.rules)
-            && Objects.equals(this.onlineResource, other.onlineResource);
-    }
-
-    /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
-     */
-    public static FeatureTypeStyle castOrCopy(org.opengis.style.FeatureTypeStyle candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof FeatureTypeStyle) {
-            return (FeatureTypeStyle) candidate;
-        }
-
-        final List<Rule> rules = new ArrayList<>();
-        for (org.opengis.style.Rule cr : candidate.rules()) {
-            rules.add(Rule.castOrCopy(cr));
-        }
-
-        return new FeatureTypeStyle(
-                candidate.getName(),
-                Description.castOrCopy(candidate.getDescription()),
-                candidate.getFeatureInstanceIDs(),
-                candidate.featureTypeNames(),
-                candidate.semanticTypeIdentifiers(),
-                rules,
-                candidate.getOnlineResource()
-        );
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    @SuppressWarnings("unchecked")
+    private void selfClone() {
+        if (description != null) description = description.clone();
+        semanticTypeIdentifiers = semanticTypeIdentifiers.clone();
+        rules = new ArrayList<>(rules);
+        rules.replaceAll(Rule::clone);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
index 11b925d88b..c7759ffeec 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
@@ -17,106 +17,251 @@
 package org.apache.sis.internal.style;
 
 import java.awt.Color;
-import java.util.Objects;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
 import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import org.opengis.filter.Literal;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Fill}.
+ * Instructions about how to fill the interior of polygons.
+ * A fill can be a solid color or a repeated {@link GraphicFill}.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Fill implements org.opengis.style.Fill {
+@XmlType(name = "FillType", propOrder = {
+    "graphicFill",
+//  "svgParameter"
+})
+@XmlRootElement(name = "Fill")
+public class Fill extends StyleElement implements Translucent {
+    /**
+     * Literal for a predefined color which can be used as fill color.
+     */
+    public static final Literal<Feature,Color> BLACK, GRAY, WHITE;
+    static {
+        final var FF = FF();
+        BLACK = FF.literal(Color.BLACK);
+        GRAY  = FF.literal(Color.GRAY);
+        WHITE = FF.literal(Color.WHITE);
+    }
 
-    private GraphicFill graphicFill;
-    private Expression<Feature,Color> color;
-    private Expression<Feature, ? extends Number> opacity;
+    /**
+     * The image to use for filling the area, or {@code null} if a solid color should be used.
+     *
+     * @see #getGraphicFill()
+     * @see #setGraphicFill(GraphicFill)
+     */
+    @XmlElement(name = "GraphicFill")
+    protected GraphicFill graphicFill;
 
+    /**
+     * Color of the interior if it is to be solid-color filled, or {@code null} for the default value.
+     * The default value specified by OGC 05-077r4 standard is gray.
+     *
+     * <p>This property is used when {@link #graphicFill} is null.
+     * In XML documents, this is encoded inside a {@code <SvgParameter name="fill">} element.</p>
+     *
+     * @see #getColor()
+     * @see #setColor(Expression)
+     */
+    protected Expression<Feature,Color> color;
+
+    /**
+     * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
+     * The default value specified by OGC 05-077r4 standard is 1.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="fill-opacity">} element.</p>
+     *
+     * @see #getOpacity()
+     * @see #setOpacity(Expression)
+     */
+    protected Expression<Feature, ? extends Number> opacity;
+
+    /**
+     * Returns the opacity of the alpha value of the given color.
+     * If the color is totally opaque, then this method returns {@code null}.
+     *
+     * @param  color  color from which to get the opacity.
+     * @return opacity derived from the alpha value of the color, or {@code null} if totally opaque.
+     */
+    static Expression<Feature, ? extends Number> opacity(final Color color) {
+        final int alpha = color.getAlpha();
+        return (alpha != 255) ? literal(alpha / 256d) : null;
+        // Divide by 256 instead of 255 in order to get round numbers for alpha values 64, 128, etc.
+    }
+
+    /**
+     * Creates an opaque fill initialized to the gray color.
+     */
     public Fill() {
-        this(null, StyleFactory.DEFAULT_FILL_COLOR, StyleFactory.DEFAULT_FILL_OPACITY);
     }
 
-    public Fill(GraphicFill graphicFill, Expression<Feature, Color> color, Expression<Feature, ? extends Number> opacity) {
+    /**
+     * Creates a fill initialized to the given color.
+     * The opacity is derived from the alpha value of the given color.
+     *
+     * @param  color  the initial color.
+     */
+    public Fill(Color color) {
         ArgumentChecks.ensureNonNull("color", color);
-        ArgumentChecks.ensureNonNull("opacity", opacity);
-        this.graphicFill = graphicFill;
-        this.color = color;
-        this.opacity = opacity;
+        if ((opacity = opacity(color)) != null) {
+            color = new Color(color.getRGB() | 0xFF000000);
+        }
+        this.color = literal(color);
     }
 
-    @Override
-    public GraphicFill getGraphicFill() {
-        return graphicFill;
+    /**
+     * Creates an opaque fill initialized to the specified color.
+     *
+     * @param  color  the initial color.
+     */
+    Fill(final Expression<Feature,Color> color) {
+        this.color = color;
     }
 
-    public void setGraphicFill(GraphicFill graphicFill) {
-        this.graphicFill = graphicFill;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Fill(final Fill source) {
+        super(source);
+        graphicFill = source.graphicFill;
+        color       = source.color;
+        opacity     = source.opacity;
     }
 
-    @Override
-    public Expression<Feature, Color> getColor() {
-        return color;
+    /**
+     * If filling with tiled copies of an image, returns the image that should be drawn.
+     * Otherwise returns an empty value, which means that a solid color should be used.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.</p>
+     *
+     * @return image to repeat for filling the area.
+     *
+     * @see Stroke#getGraphicFill()
+     */
+    public Optional<GraphicFill> getGraphicFill() {
+        return Optional.ofNullable(graphicFill);
     }
 
-    public void setColor(Expression<Feature, Color> color) {
-        ArgumentChecks.ensureNonNull("color", color);
-        this.color = color;
+    /**
+     * Specifies that area should be filled with the given graphic.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new image to repeat for filling the area, or {@code null} if none.
+     *
+     * @see Stroke#setGraphicFill(GraphicFill)
+     */
+    public void setGraphicFill(final GraphicFill value) {
+        graphicFill = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getOpacity() {
-        return opacity;
+    /**
+     * Indicates the color of the area if it is to be solid-color filled.
+     * This is used when {@linkplain #getGraphicFill() graphic fill} is null.
+     *
+     * @return color of the area if it is to be solid-color filled.
+     *
+     * @see Stroke#getColor()
+     */
+    public Expression<Feature,Color> getColor() {
+        final var value = color;
+        return (value != null) ? value : GRAY;
     }
 
-    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
-        ArgumentChecks.ensureNonNull("opacity", opacity);
-        this.opacity = opacity;
+    /**
+     * Sets the color of the area if it is to be solid-color filled.
+     * If this method is never invoked, then the default value is {@link Fill#GRAY}.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * <p>Setting a non-null value clears the {@linkplain #getGraphicFill() graphic fill}
+     * because those two properties are mutually exclusive.</p>
+     *
+     * @param  value  color of the area if solid-color filled, or {@code null} for resetting the default value.
+     *
+     * @see Stroke#setColor(Expression)
+     */
+    public void setColor(final Expression<Feature,Color> value) {
+        color = value;
+        if (value != null) {
+            graphicFill = null;
+        }
     }
 
+    /**
+     * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+     *
+     * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+     *
+     * @see Stroke#getOpacity()
+     * @see Graphic#getOpacity()
+     * @see RasterSymbolizer#getOpacity()
+     */
     @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    public Expression<Feature, ? extends Number> getOpacity() {
+        return defaultToOne(opacity);
     }
 
+    /**
+     * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * If this method is never invoked, then the default value is literal 1 (totally opaque).
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new level of translucency, or {@code null} for resetting the default value.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(graphicFill, color, opacity);
+    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+        opacity = value;
     }
 
+    /*
+     * TODO: we need a private method like below for formatting above SVG parameters:
+     * See Stroke for more detais.
+     */
+
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Fill other = (Fill) obj;
-        return Objects.equals(this.graphicFill, other.graphicFill)
-            && Objects.equals(this.color, other.color)
-            && Objects.equals(this.opacity, other.opacity);
+    final Object[] properties() {
+        return new Object[] {graphicFill, color, opacity};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static Fill castOrCopy(org.opengis.style.Fill candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Fill) {
-            return (Fill) candidate;
-        }
-        return new Fill(
-                GraphicFill.castOrCopy(candidate.getGraphicFill()),
-                candidate.getColor(),
-                candidate.getOpacity());
+    @Override
+    public Fill clone() {
+        final var clone = (Fill) super.clone();
+        clone.selfClone();
+        return clone;
+    }
+
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (graphicFill != null) graphicFill = graphicFill.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
index e0d0f2c737..a167c05284 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
@@ -16,116 +16,208 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
+import java.util.ArrayList;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import org.opengis.filter.Literal;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Font}.
+ * Identification of a font of a certain family, style, and size.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Font implements org.opengis.style.Font {
+@XmlType(name = "FontType")
+@XmlRootElement(name = "Font")
+public class Font extends StyleElement {
+    /**
+     * The "normal" literal, used for default style and weight.
+     */
+    private static final Literal<Feature,String> NORMAL = literal("normal");
+
+    /**
+     * The default font size.
+     */
+    private static final Literal<Feature,Double> DEFAULT_SIZE = literal(10.0);
+
+    /**
+     * Family names of the font to use, in preference order.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-family">} element.</p>
+     *
+     * @see #family()
+     */
+    private List<Expression<Feature,String>> family;
+
+    /**
+     * Style (normal or italic) to use for a font.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-style">} element.</p>
+     *
+     * @see #getStyle()
+     * @see #setStyle(Expression)
+     */
+    protected Expression<Feature,String> style;
 
-    private final List<Expression> family = new ArrayList<>();
-    private Expression<Feature,String> style;
-    private Expression<Feature,String> weight;
-    private Expression<Feature,? extends Number> size;
+    /**
+     * Amount of weight or boldness to use for a font.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-weight">} element.</p>
+     *
+     * @see #getWeight()
+     * @see #setWeight(Expression)
+     */
+    protected Expression<Feature,String> weight;
 
+    /**
+     * Size (in pixels) to use for the font.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-size">} element.</p>
+     *
+     * @see #getSize()
+     * @see #setSize(Expression)
+     */
+    protected Expression<Feature, ? extends Number> size;
+
+    /**
+     * Creates a font initialized to normal style, normal weight and a size of 10 pixels.
+     */
     public Font() {
-        this(Collections.EMPTY_LIST,
-            StyleFactory.DEFAULT_FONT_STYLE,
-            StyleFactory.DEFAULT_FONT_WEIGHT,
-            StyleFactory.DEFAULT_FONT_SIZE);
+        family = new ArrayList<>();
     }
 
-    public Font(List<Expression> family, Expression<Feature, String> style, Expression<Feature, String> weight, Expression<Feature, ? extends Number> size) {
-        if (family != null) this.family.addAll(family);
-        this.style = style;
-        this.weight = weight;
-        this.size = size;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Font(final Font source) {
+        super(source);
+        family = new ArrayList<>(source.family);
+        style  = source.style;
+        weight = source.weight;
+        size   = source.size;
     }
 
-    @Override
-    public List<Expression> getFamily() {
+    /**
+     * Returns the family names of the font to use, in preference order.
+     * Allowed values are system-dependent.
+     *
+     * <p>The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.</p>
+     *
+     * @return the family names in preference order, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<Expression<Feature,String>> family() {
         return family;
     }
 
-    @Override
-    public Expression<Feature, String> getStyle() {
-        return style;
+    /**
+     * Returns the style (normal or italic) to use for a font.
+     * The allowed values are "normal", "italic", and "oblique".
+     *
+     * @return style to use for a font.
+     */
+    public Expression<Feature,String> getStyle() {
+        final var value = style;
+        return (value != null) ? value : NORMAL;
     }
 
-    public void setStyle(Expression<Feature, String> style) {
-        this.style = style;
+    /**
+     * Sets the style (normal or italic) to use for a font.
+     * If this method is never invoked, then the default value is the "normal" literal.
+     *
+     * @param  value  new style to use for a font, or {@code null} for resetting the default value.
+     */
+    public void setStyle(final Expression<Feature,String> value) {
+        style = value;
     }
 
-    @Override
-    public Expression<Feature, String> getWeight() {
-        return weight;
+    /**
+     * Returns the amount of weight or boldness to use for a font.
+     * The allowed values are "normal" and "bold".
+     *
+     * @return amount of weight or boldness to use for a font.
+     */
+    public Expression<Feature,String> getWeight() {
+        final var value = weight;
+        return (value != null) ? value : NORMAL;
     }
 
-    public void setWeight(Expression<Feature, String> weight) {
-        this.weight = weight;
+    /**
+     * Sets the amount of weight or boldness to use for a font.
+     * If this method is never invoked, then the default value is the "normal" literal.
+     *
+     * @param  value  new amount of weight to use for a font, or {@code null} for resetting the default value.
+     */
+    public void setWeight(final Expression<Feature,String> value) {
+        weight = value;
     }
 
-    @Override
+    /**
+     * Returns the size (in pixels) to use for the font.
+     *
+     * @return size (in pixels) to use for the font.
+     */
     public Expression<Feature, ? extends Number> getSize() {
-        return size;
+        final var value = size;
+        return (value != null) ? value : DEFAULT_SIZE;
     }
 
-    public void setSize(Expression<Feature, ? extends Number> size) {
-        this.size = size;
+    /**
+     * Sets the size (in pixels) to use for the font.
+     * If this method is never invoked, then the default value is 10 pixels.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new size to use for the font, or {@code null} for resetting the default value.
+     */
+    public void setSize(final Expression<Feature, ? extends Number> value) {
+        size = value;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
+    /*
+     * TODO: we need a private method like below for formatting above SVG parameters:
+     * See Stroke for more detais.
+     */
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(family, style, weight, size);
+    final Object[] properties() {
+        return new Object[] {family, style, weight, size};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Font other = (Font) obj;
-        return Objects.equals(this.family, other.family)
-            && Objects.equals(this.style, other.style)
-            && Objects.equals(this.weight, other.weight)
-            && Objects.equals(this.size, other.size);
+    public Font clone() {
+        final var clone = (Font) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static Font castOrCopy(org.opengis.style.Font candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Font) {
-            return (Font) candidate;
-        }
-        return new Font(
-                candidate.getFamily(),
-                candidate.getStyle(),
-                candidate.getWeight(),
-                candidate.getSize());
+    private void selfClone() {
+        family = new ArrayList<>(family);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
index ddc98bb455..c83413d1f4 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
@@ -16,159 +16,331 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
+import java.util.ArrayList;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlElements;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Graphic}.
+ * A "graphic symbol" with an inherent shape, color(s), and possibly size.
+ * A "graphic" can be very informally defined as "a little picture"
+ * and can be of either a raster or vector-graphic source type.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public class Graphic implements org.opengis.style.Graphic {
-
-    private final List<GraphicalSymbol> graphicalSymbols = new ArrayList<>();
-    private Expression<Feature,? extends Number> opacity;
-    private Expression<Feature,? extends Number> size;
-    private Expression<Feature,? extends Number> rotation;
-    private AnchorPoint anchorPoint;
-    private Displacement displacement;
-
-    public static Graphic createDefault() {
-        final List<GraphicalSymbol> symbols = new ArrayList<>();
-        symbols.add(new Mark());
-        return new Graphic(symbols,
-                StyleFactory.DEFAULT_GRAPHIC_OPACITY,
-                StyleFactory.DEFAULT_GRAPHIC_SIZE,
-                StyleFactory.DEFAULT_GRAPHIC_ROTATION,
-                new AnchorPoint(),
-                new Displacement());
-    }
+@XmlType(name = "GraphicType", propOrder = {
+    "graphicalSymbols",
+    "opacity",
+    "size",
+    "rotation",
+    "anchorPoint",
+    "displacement"
+})
+@XmlRootElement(name = "Graphic")
+public class Graphic extends StyleElement implements Translucent {
+    /**
+     * The default mark size, which is 6 pixels according the OGC 05-077r4 standard.
+     *
+     * @see #getSize()
+     */
+    private static final Expression<Feature,Double> DEFAULT_SIZE = literal(6.0);
 
-    public Graphic() {
+    /**
+     * List of external image files or marks that comprise this graphic.
+     * All elements of the list must be instances of either {@link Mark} or {@link ExternalGraphic}.
+     * If empty it is to be treated a single default mark.
+     *
+     * @see #graphicalSymbols()
+     */
+    @XmlElements({
+        @XmlElement(name = "Mark", type = Mark.class),
+        @XmlElement(name = "ExternalGraphic", type = ExternalGraphic.class)
+    })
+    private List<GraphicalSymbol> graphicalSymbols;
+
+    /**
+     * Level of translucency as a floating point number, or {@code null} for the default value.
+     *
+     * @see #getOpacity()
+     * @see #setOpacity(Expression)
+     *
+     * @todo Needs a JAXB adapter to {@code ParameterValueType}.
+     */
+    @XmlElement(name = "Opacity")
+    protected Expression<Feature, ? extends Number> opacity;
+
+    /**
+     * Absolute size of the graphic as a floating point number, or {@code null} for the default value.
+     *
+     * @see #getSize()
+     * @see #setSize(Expression)
+     *
+     * @todo Needs a JAXB adapter to {@code ParameterValueType}.
+     */
+    @XmlElement(name = "Size")
+    protected Expression<Feature, ? extends Number> size;
+
+    /**
+     * Rotation angle of the graphic when it is drawn, or {@code null} for the default value.
+     *
+     * @see #getRotation()
+     * @see #setRotation(Expression)
+     *
+     * @todo Needs a JAXB adapter to {@code ParameterValueType}.
+     */
+    @XmlElement(name = "Rotation")
+    protected Expression<Feature, ? extends Number> rotation;
 
+    /**
+     * Location to use for anchoring the graphic to the geometry, or {@code null} for lazily constructed default.
+     *
+     * @see #getAnchorPoint()
+     * @see #setAnchorPoint(AnchorPoint)
+     */
+    @XmlElement(name = "AnchorPoint")
+    protected AnchorPoint anchorPoint;
+
+    /**
+     * Displacement from the "hot-spot" point, or {@code null} for lazily constructed default.
+     *
+     * @see #getDisplacement()
+     * @see #setDisplacement(Displacement)
+     */
+    @XmlElement(name = "Displacement")
+    protected Displacement displacement;
+
+    /**
+     * Creates a graphic initialized to opaque default mark, default size and no rotation.
+     */
+    public Graphic() {
+        graphicalSymbols = new ArrayList<>();
     }
 
-    public Graphic(List<GraphicalSymbol> graphicalSymbols,
-            Expression<Feature, ? extends Number> opacity,
-            Expression<Feature, ? extends Number> size,
-            Expression<Feature, ? extends Number> rotation,
-            AnchorPoint anchorPoint,
-            Displacement displacement) {
-        if (graphicalSymbols!= null) this.graphicalSymbols.addAll(graphicalSymbols);
-        this.opacity = opacity;
-        this.size = size;
-        this.rotation = rotation;
-        this.anchorPoint = anchorPoint;
-        this.displacement = displacement;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Graphic(final Graphic source) {
+        super(source);
+        graphicalSymbols = new ArrayList<>(source.graphicalSymbols);
+        opacity          = source.opacity;
+        size             = source.size;
+        rotation         = source.rotation;
+        anchorPoint      = source.anchorPoint;
+        displacement     = source.displacement;
     }
 
-    @Override
-    public List<org.opengis.style.GraphicalSymbol> graphicalSymbols() {
-        return (List) graphicalSymbols;
+    /**
+     * Returns the list of external image files or marks that comprise this graphic.
+     * All elements of the list shall be instances of either {@link Mark} or {@link ExternalGraphic}.
+     * The list may contain multiple external URLs and marks with the semantic that they all provide
+     * the equivalent graphic in different {@linkplain GraphicalSymbol#getFormat() formats}.
+     *
+     * <p>If the list is empty, it is to be treated as a single default mark.
+     * That default is a square with with a 50%-gray fill and a black outline,
+     * with a size of 6 pixels, unless an explicit {@linkplain #getSize() size} is specified.</p>
+     *
+     * <p>The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.</p>
+     *
+     * @return list of marks or external graphics, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<GraphicalSymbol> graphicalSymbols() {
+        return graphicalSymbols;
     }
 
+    /**
+     * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+     *
+     * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+     *
+     * @see Fill#getOpacity()
+     * @see Stroke#getOpacity()
+     * @see RasterSymbolizer#getOpacity()
+     */
     @Override
-    public Expression<Feature,? extends Number> getOpacity() {
-        return opacity;
+    public Expression<Feature, ? extends Number> getOpacity() {
+        return defaultToOne(opacity);
     }
 
-    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
-        this.opacity = opacity;
+    /**
+     * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * If this method is never invoked, then the default value is literal 1 (totally opaque).
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new level of translucency, or {@code null} for resetting the default value.
+     */
+    @Override
+    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+        opacity = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getSize() {
-        return size;
+    /**
+     * Returns the absolute size of the graphic as a floating point number.
+     * If a size is specified, the height of the graphic will be scaled to
+     * that size and the aspect ratio will be used for computing the width.
+     * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+     * Value shall be positive.
+     *
+     * <h4>Default value</h4>
+     * The default size of an image format is the inherent size of the image.
+     * The default size of an image format without an inherent size is defined
+     * to be 16 pixels in height and the corresponding aspect in width.
+     * If no image or mark is specified, then the size of the default mark is 6 pixels.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @return absolute size of the graphic as a floating point number, or {@code null} for the default value.
+     */
+    public Expression<Feature, ? extends Number> getSize() {
+        final var value = size;
+        if (value == null && graphicalSymbols.isEmpty()) {
+            return DEFAULT_SIZE;
+        }
+        return value;
     }
 
-    public void setSize(Expression<Feature, ? extends Number> size) {
-        this.size = size;
+    /**
+     * Sets the absolute size of the graphic as a floating point number.
+     * If this method is never invoked, then the default value is {@code null}.
+     *
+     * @param  value  new absolute size of the graphic, or {@code null} for the default value.
+     */
+    public void setSize(final Expression<Feature, ? extends Number> value) {
+        size = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getRotation() {
-        return rotation;
+    /**
+     * Returns the rotation angle of the graphic when it is drawn.
+     * The rotation is in the clockwise direction around the graphic center point in decimal degrees.
+     * Negative values mean counter-clockwise rotation. The default value is 0.0 (no rotation).
+     *
+     * <p>The point within the graphic about which it is rotated is format dependent.
+     * If a format does not include an inherent rotation point,
+     * then the point of rotation should be the centroid.</p>
+     *
+     * @return the rotation angle of the graphic when it is drawn.
+     */
+    public Expression<Feature, ? extends Number> getRotation() {
+        return defaultToZero(rotation);
     }
 
-    public void setRotation(Expression<Feature, ? extends Number> rotation) {
-        this.rotation = rotation;
+    /**
+     * Sets the rotation angle of the graphic when it is drawn.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new rotation angle of the graphic, or {@code null} for resetting the default value.
+     */
+    public void setRotation(final Expression<Feature, ? extends Number> value) {
+        rotation = value;
     }
 
-    @Override
+    /**
+     * Returns the location inside of a graphic to use for anchoring the graphic to the main-geometry point.
+     * The coordinates are given as (<var>x</var>,<var>y</var>) floating-point numbers
+     * relative the the graphic bounding box, where (0,0) is the lower-left corner and
+     * (1,1) is the upper-right corner.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this graphic, and conversely.</p>
+     *
+     * @return the anchor point.
+     */
     public AnchorPoint getAnchorPoint() {
+        if (anchorPoint == null) {
+            anchorPoint = new AnchorPoint();
+        }
         return anchorPoint;
     }
 
-    public void setAnchorPoint(AnchorPoint anchorPoint) {
-        this.anchorPoint = anchorPoint;
+    /**
+     * Sets the location inside of a graphic to use for anchoring the graphic to the main-geometry point.
+     * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+     * then the default value is a {@linkplain AnchorPoint#AnchorPoint() default anchor point}.
+     *
+     * @param  value  new anchor point, or {@code null} for resetting the default value.
+     */
+    public void setAnchorPoint(final AnchorPoint value) {
+        anchorPoint = value;
     }
 
-    @Override
+    /**
+     * Returns the two-dimensional displacement from the "hot-spot" point.
+     * This element may be used to avoid over-plotting of multiple graphic symbols for the same point.
+     * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+     * Positive values are to the right of the point.
+     * The default displacement is <var>x</var> = 0, <var>y</var> = 0,
+     *
+     * <p>If displacement is used in conjunction with size and/or rotation
+     * then the graphic symbol shall be scaled and/or rotated before it is displaced.</p>
+     *
+     * @return displacement from the "hot-spot" point.
+     *
+     * @see PolygonSymbolizer#getDisplacement()
+     * @see PointPlacement#getDisplacement()
+     */
     public Displacement getDisplacement() {
+        if (displacement == null) {
+            displacement = new Displacement();
+        }
         return displacement;
     }
 
-    public void setDisplacement(Displacement displacement) {
-        this.displacement = displacement;
-    }
-
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets the two-dimensional displacement from the "hot-spot" point.
+     * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+     * then the default value is a {@linkplain Displacement#Displacement() default displacement}.
+     *
+     * @param  value  new displacement from the "hot-spot" point, or {@code null} for resetting the default value.
+     */
+    public void setDisplacement(final Displacement value) {
+        displacement = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
+    final Object[] properties() {
+        return new Object[] {graphicalSymbols, opacity, size, rotation, anchorPoint, displacement};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Graphic other = (Graphic) obj;
-        return Objects.equals(this.graphicalSymbols, other.graphicalSymbols)
-            && Objects.equals(this.opacity, other.opacity)
-            && Objects.equals(this.size, other.size)
-            && Objects.equals(this.rotation, other.rotation)
-            && Objects.equals(this.anchorPoint, other.anchorPoint)
-            && Objects.equals(this.displacement, other.displacement);
+    public Graphic clone() {
+        final var clone = (Graphic) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static Graphic castOrCopy(org.opengis.style.Graphic candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Graphic) {
-            return (Graphic) candidate;
-        }
-
-        final List<GraphicalSymbol> cs = new ArrayList<>();
-        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
-            cs.add(GraphicalSymbol.castOrCopy(cr));
-        }
-        return new Graphic(
-                cs,
-                candidate.getOpacity(),
-                candidate.getSize(),
-                candidate.getRotation(),
-                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
-                Displacement.castOrCopy(candidate.getDisplacement()));
+    private void selfClone() {
+        graphicalSymbols = new ArrayList<>(graphicalSymbols);
+        graphicalSymbols.replaceAll(GraphicalSymbol::clone);
+        if (anchorPoint  != null) anchorPoint  = anchorPoint .clone();
+        if (displacement != null) displacement = displacement.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
index ff3a7c2682..79dc7513fe 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
@@ -16,64 +16,110 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
-import java.util.List;
-import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.GraphicFill}.
+ * Stipple-fill repeated graphic.
+ * A graphic fill can be used by a {@link Fill} or by a {@link Stroke}.
+ * This class contains only a {@link Graphic} property,
+ * but its encapsulation in {@code GraphicFill} means that the graphic
+ * should be repeated in all the interior of the shape to be drawn.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class GraphicFill extends Graphic implements org.opengis.style.GraphicFill {
+@XmlType(name = "GraphicFillType")
+@XmlRootElement(name = "GraphicFill")
+public class GraphicFill extends StyleElement implements GraphicalElement {
+    /**
+     * The graphic to be repeated, or {@code null} for lazily constructed default.
+     * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
+     *
+     * @see #getGraphic()
+     * @see #setGraphic(Graphic)
+     */
+    protected Graphic graphic;
 
+    /**
+     * Creates a graphic fill initialized to a default graphic.
+     */
     public GraphicFill() {
     }
 
-    public GraphicFill(List<GraphicalSymbol> graphicalSymbols,
-            Expression<Feature, Number> opacity,
-            Expression<Feature, Number> size,
-            Expression<Feature, Number> rotation,
-            AnchorPoint anchorPoint,
-            Displacement displacement) {
-        super(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public GraphicFill(final GraphicFill source) {
+        super(source);
+        graphic = source.graphic;
+    }
+
+    /**
+     * Returns the graphic to be repeated.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.
+     *
+     * @return the graphic to be repeated.
+     *
+     * @see GraphicStroke#getGraphic()
+     */
+    @Override
+    @XmlElement(name = "Graphic", required = true)
+    public final Graphic getGraphic() {
+        if (graphic == null) {
+            graphic = new Graphic();
+        }
+        return graphic;
     }
 
+    /**
+     * Sets the graphic to be repeated.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a {@linkplain Graphic#Graphic() default instance}.
+     *
+     * @param  value  new graphic, or {@code null} for resetting the default value.
+     *
+     * @see GraphicStroke#setGraphic(Graphic)
+     */
     @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    public final void setGraphic(final Graphic value) {
+        graphic = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        return super.equals(obj)
-            && obj instanceof GraphicFill;
+    final Object[] properties() {
+        return new Object[] {graphic};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static GraphicFill castOrCopy(org.opengis.style.GraphicFill candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof GraphicFill) {
-            return (GraphicFill) candidate;
-        }
-        final List<org.apache.sis.internal.style.GraphicalSymbol> cs = new ArrayList<>();
-        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
-            cs.add(org.apache.sis.internal.style.GraphicalSymbol.castOrCopy(cr));
-        }
-        return new GraphicFill(
-                cs,
-                candidate.getOpacity(),
-                candidate.getSize(),
-                candidate.getRotation(),
-                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
-                Displacement.castOrCopy(candidate.getDisplacement()));
+    @Override
+    public GraphicFill clone() {
+        final var clone = (GraphicFill) super.clone();
+        clone.selfClone();
+        return clone;
+    }
+
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (graphic != null) graphic = graphic.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
index f410de820c..78721aae9b 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
@@ -16,64 +16,102 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
-import java.util.List;
-import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.GraphicLegend}.
+ * A graphic to do displayed in a legend for a rule.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class GraphicLegend extends Graphic implements org.opengis.style.GraphicLegend {
+@XmlType(name = "LegendGraphicType")
+@XmlRootElement(name = "LegendGraphic")
+public class GraphicLegend extends StyleElement implements GraphicalElement {
+    /**
+     * The graphic to use as a legend, or {@code null} for lazily constructed default.
+     * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
+     *
+     * @see #getGraphic()
+     * @see #setGraphic(Graphic)
+     */
+    protected Graphic graphic;
 
+    /**
+     * Creates a legend initialized to the default graphic.
+     */
     public GraphicLegend() {
     }
 
-    public GraphicLegend(List<GraphicalSymbol> graphicalSymbols,
-            Expression<Feature, Number> opacity,
-            Expression<Feature, Number> size,
-            Expression<Feature, Number> rotation,
-            AnchorPoint anchorPoint,
-            Displacement displacement) {
-        super(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public GraphicLegend(final GraphicLegend source) {
+        super(source);
+        graphic = source.graphic;
+    }
+
+    /**
+     * Returns the graphic of the legend.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.
+     *
+     * @return graphic of the legend.
+     */
+    @Override
+    @XmlElement(name = "Graphic", required = true)
+    public final Graphic getGraphic() {
+        if (graphic == null) {
+            graphic = new Graphic();
+        }
+        return graphic;
     }
 
+    /**
+     * Sets the graphic of the legend.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a {@linkplain Graphic#Graphic() default instance}.
+     *
+     * @param  value  new graphic of the legend, or {@code null} for resetting the default value.
+     */
     @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    public final void setGraphic(final Graphic value) {
+        graphic = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        return super.equals(obj)
-            && obj instanceof GraphicLegend;
+    final Object[] properties() {
+        return new Object[] {graphic};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static GraphicLegend castOrCopy(org.opengis.style.GraphicLegend candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof GraphicLegend) {
-            return (GraphicLegend) candidate;
-        }
-        final List<GraphicalSymbol> cs = new ArrayList<>();
-        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
-            cs.add(GraphicalSymbol.castOrCopy(cr));
-        }
-        return new GraphicLegend(
-                cs,
-                candidate.getOpacity(),
-                candidate.getSize(),
-                candidate.getRotation(),
-                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
-                Displacement.castOrCopy(candidate.getDisplacement()));
+    @Override
+    public GraphicLegend clone() {
+        final var clone = (GraphicLegend) super.clone();
+        clone.selfClone();
+        return clone;
+    }
+
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (graphic != null) graphic = graphic.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
index 60513f83c8..7eeb608ff7 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
@@ -16,104 +16,176 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.GraphicStroke}.
+ * Repeated-linear-graphic stroke.
+ * A graphic stroke can only be used by a {@link Stroke}.
+ * This class contains a {@link Graphic} property,
+ * but its encapsulation in {@code GraphicStroke} means that
+ * the graphic should be bent around the curves of the line string.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class GraphicStroke extends Graphic implements org.opengis.style.GraphicStroke {
+@XmlType(name = "GraphicStrokeType", propOrder = {
+    "graphic",
+    "initialGap",
+    "gap"
+})
+@XmlRootElement(name = "GraphicStroke")
+public class GraphicStroke extends StyleElement implements GraphicalElement {
+    /**
+     * The graphic to be repeated, or {@code null} for lazily constructed default.
+     * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
+     *
+     * @see #getGraphic()
+     * @see #setGraphic(Graphic)
+     */
+    protected Graphic graphic;
+
+    /**
+     * How far away the first graphic will be drawn, or {@code null} for the default value.
+     *
+     * @see #getInitialGap()
+     * @see #setInitialGap(Expression)
+     */
+    @XmlElement(name = "InitialGap")
+    protected Expression<Feature, ? extends Number> initialGap;
 
-    private Expression<Feature,? extends Number> initialGap;
-    private Expression<Feature,? extends Number> gap;
+    /**
+     * Distance between two graphics, or {@code null} for the default value.
+     *
+     * @see #getGap()
+     * @see #setGap(Expression)
+     */
+    @XmlElement(name = "Gap")
+    protected Expression<Feature, ? extends Number> gap;
 
+    /**
+     * Creates a graphic stroke initialized to a default graphic and no gap.
+     */
     public GraphicStroke() {
     }
 
-    public GraphicStroke(List<GraphicalSymbol> graphicalSymbols,
-            Expression<Feature, ? extends Number> opacity,
-            Expression<Feature, ? extends Number> size,
-            Expression<Feature, ? extends Number> rotation,
-            AnchorPoint anchorPoint,
-            Displacement displacement,
-            Expression<Feature, ? extends Number> initialGap,
-            Expression<Feature, ? extends Number> gap) {
-        super(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
-        this.initialGap = initialGap;
-        this.gap = gap;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public GraphicStroke(final GraphicStroke source) {
+        super(source);
+        graphic    = source.graphic;
+        initialGap = source.initialGap;
+        gap        = source.gap;
     }
 
+    /**
+     * Returns the graphic to be repeated.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.
+     *
+     * @return the graphic to be repeated.
+     *
+     * @see GraphicFill#getGraphic()
+     */
     @Override
-    public Expression<Feature,? extends Number> getInitialGap() {
-        return initialGap;
+    @XmlElement(name = "Graphic", required = true)
+    public final Graphic getGraphic() {
+        if (graphic == null) {
+            graphic = new Graphic();
+        }
+        return graphic;
     }
 
-    public void setInitialGap(Expression<Feature, ? extends Number> initialGap) {
-        this.initialGap = initialGap;
+    /**
+     * Sets the graphic to be repeated.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a {@linkplain Graphic#Graphic() default instance}.
+     *
+     * @param  value  new graphic, or {@code null} for resetting the default value.
+     *
+     * @see GraphicFill#setGraphic(Graphic)
+     */
+    @Override
+    public final void setGraphic(final Graphic value) {
+        graphic = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getGap() {
-        return gap;
+    /**
+     * Returns how far away the first graphic will be drawn relative to the start of the rendering line.
+     *
+     * @return distance of first graphic relative to the rendering start.
+     */
+    public Expression<Feature, ? extends Number> getInitialGap() {
+        return defaultToZero(initialGap);
     }
 
-    public void setGap(Expression<Feature, ? extends Number> gap) {
-        this.gap = gap;
+    /**
+     * Sets how far away the first graphic will be drawn relative to the start of the rendering line.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new distance relative to rendering start, or {@code null} for resetting the default value.
+     */
+    public void setInitialGap(final Expression<Feature, ? extends Number> value) {
+        initialGap = value;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns the distance between two graphics.
+     *
+     * @return distance between two graphics.
+     */
+    public Expression<Feature, ? extends Number> getGap() {
+        return defaultToZero(gap);
     }
 
-    @Override
-    public int hashCode() {
-        return super.hashCode() + Objects.hash(initialGap, gap);
+    /**
+     * Sets the distance between two graphics.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new distance between two graphics, or {@code null} for resetting the default value.
+     */
+    public void setGap(final Expression<Feature, ? extends Number> value) {
+        gap = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (!super.equals(obj)) {
-            return false;
-        }
-        if (!(obj instanceof GraphicStroke)) {
-            return false;
-        }
-        final GraphicStroke other = (GraphicStroke) obj;
-        return Objects.equals(this.initialGap, other.initialGap)
-            && Objects.equals(this.gap, other.gap);
+    final Object[] properties() {
+        return new Object[] {graphic, initialGap, gap};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static GraphicStroke castOrCopy(org.opengis.style.GraphicStroke candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof GraphicStroke) {
-            return (GraphicStroke) candidate;
-        }
-        final List<GraphicalSymbol> cs = new ArrayList<>();
-        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
-            cs.add(GraphicalSymbol.castOrCopy(cr));
-        }
-        return new GraphicStroke(
-                cs,
-                candidate.getOpacity(),
-                candidate.getSize(),
-                candidate.getRotation(),
-                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
-                Displacement.castOrCopy(candidate.getDisplacement()),
-                candidate.getInitialGap(),
-                candidate.getGap());
+    @Override
+    public GraphicStroke clone() {
+        final var clone = (GraphicStroke) super.clone();
+        clone.selfClone();
+        return clone;
+    }
+
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (graphic != null) graphic = graphic.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalElement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalElement.java
new file mode 100644
index 0000000000..c305b0cf48
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalElement.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.style;
+
+
+/**
+ * Object having a graphical representation as small picture.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+public interface GraphicalElement {
+    /**
+     * Returns the graphic.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.
+     *
+     * @return the picture.
+     */
+    Graphic getGraphic();
+
+    /**
+     * Sets the graphic.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a {@linkplain Graphic#Graphic() default instance}.
+     *
+     * <p>The interpretation of {@code null} argument value depends on whether the graphic is mandatory or optional.
+     * If mandatory, then a {@code null} argument value resets the {@linkplain Graphic#Graphic() default graphic}.
+     * If optional, then a {@code null} argument value specifies to plot nothing.
+     *
+     * @param  value  new picture, or {@code null} for none or for resetting the default value.
+     */
+    void setGraphic(Graphic value);
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
index 4aa0156bbd..27ca6c5401 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
@@ -16,22 +16,154 @@
  */
 package org.apache.sis.internal.style;
 
+import javax.swing.Icon;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlTransient;
+import org.opengis.metadata.citation.OnlineResource;
+
+
 /**
+ * Common superclass for the types of markers that can appear as children of a graphic object.
+ * Each {@link Graphic} instance contains an arbitrary amount of graphical symbols, when can be
+ * either well-known shapes ({@link Mark}) or references to image files ({@link ExternalGraphic}).
+ * Graphic content should be static.
+ *
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
  *
- * @author Johann Sorel (Geomatys)
+ * @see Graphic#graphicalSymbols()
+ *
+ * @since 1.5
  */
-public interface GraphicalSymbol extends org.opengis.style.GraphicalSymbol {
+@XmlTransient
+public abstract class GraphicalSymbol extends StyleElement {
+    /**
+     * URL to the image or mark, or {@code null} if none.
+     *
+     * @see #getOnlineResource()
+     * @see #setOnlineResource(OnlineResource)
+     */
+//  @XmlElement(name = "OnlineResource")
+    protected OnlineResource onlineResource;
+
+    /**
+     * Handler to the locally loaded image, or {@code null} if none.
+     * In Java this is represented by an {@link Icon}.
+     * But the XML representation shall be either XML or Base-64-encoded binary.
+     *
+     * @see #getInlineContent()
+     * @see #setInlineContent(Icon)
+     */
+//  @XmlElement(name = "InlineContent")
+    protected Icon inlineContent;
+
+    /**
+     * The expected document MIME type, or {@code null} if unspecified.
+     *
+     * @see #getFormat()
+     * @see #setFormat(String)
+     */
+    @XmlElement(name = "Format")       // Required in ExternalGraphic but not in Mark.
+    protected String format;
 
-    public static GraphicalSymbol castOrCopy(org.opengis.style.GraphicalSymbol candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof org.opengis.style.Mark) {
-            return Mark.castOrCopy((org.opengis.style.Mark) candidate);
-        } else if (candidate instanceof org.opengis.style.ExternalGraphic) {
-            return ExternalGraphic.castOrCopy((org.opengis.style.ExternalGraphic) candidate);
-        } else {
-            throw new IllegalArgumentException("Unexpected symbol type " + candidate.getClass().getName());
-        }
+    /**
+     * Creates a new graphical symbol.
+     * Intentionally restricted to this package because {@link #properties()} is package-private.
+     */
+    GraphicalSymbol() {
     }
 
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    GraphicalSymbol(final GraphicalSymbol source) {
+        super(source);
+        onlineResource = source.onlineResource;
+        inlineContent  = source.inlineContent;
+        format         = source.format;
+    }
+
+    /**
+     * Returns an online or local resource to a file that contains an image.
+     * The URL should not reference external graphics that may change at arbitrary times,
+     * because the system may cache that resource. The returned value may be empty.
+     * if the image is already loaded locally and provided by {@link #getInlineContent()}.
+     *
+     * @return URL to the image, or empty if the image or mark is provided by other means.
+     *
+     * @see #getFormat()
+     */
+    public Optional<OnlineResource> getOnlineResource() {
+        return Optional.ofNullable(onlineResource);
+    }
+
+    /**
+     * Sets an online or local resource to a file that contains an image.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new URL to the image, or {@code null} if none.
+     */
+    public void setOnlineResource(final OnlineResource value) {
+        onlineResource = value;
+    }
+
+    /**
+     * Returns a handler to the locally loaded image.
+     * If present, this property overrides {@link #getOnlineResource()}.
+     *
+     * @return handler to the locally loaded image.
+     */
+    public Optional<Icon> getInlineContent() {
+        return Optional.ofNullable(inlineContent);
+    }
+
+    /**
+     * Sets a handler to the locally loaded image.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new handler to the locally loaded image, or {@code null} if none.
+     */
+    public void setInlineContent(final Icon value) {
+        inlineContent = value;
+    }
+
+    /**
+     * Returns the mime type of the online resource or inline content.
+     * This information allows the styler to select the best-supported
+     * format from the list of URLs with equivalent content.
+     *
+     * @return mime type.
+     *
+     * @see #getOnlineResource()
+     */
+    public Optional<String> getFormat() {
+        return Optional.ofNullable(format);
+    }
+
+    /**
+     * Sets the mime type of the online resource or inline content.
+     *
+     * @param  value  new mime type, or {@code null} if unspecified.
+     */
+    public void setFormat(final String value) {
+        format = value;
+    }
+
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
+    @Override
+    public GraphicalSymbol clone() {
+        return (GraphicalSymbol) super.clone();
+    }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
index cc26f64032..7fae4fe368 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
@@ -16,94 +16,142 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.apache.sis.util.ArgumentChecks;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Halo}.
+ * Fill that is applied to the backgrounds of font glyphs.
+ * The use of halos improves the readability of text labels.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Halo implements org.opengis.style.Halo {
+@XmlType(name = "HaloType", propOrder = {
+    "radius",
+    "fill"
+})
+@XmlRootElement(name = "Halo")
+public class Halo extends StyleElement {
+    /**
+     * Radius (in pixels) of the  the halo around the text, or {@code null} for the default value.
+     *
+     * @see #getRadius()
+     * @see #setRadius(Expression)
+     */
+    @XmlElement(name = "Radius")
+    protected Expression<Feature, ? extends Number> radius;
 
-    private Fill fill;
-    private Expression<Feature,? extends Number> radius;
+    /**
+     * How the halo area around the text should be filled, or {@code null} for the default value.
+     *
+     * @see #getFill()
+     * @see #setFill(Fill)
+     */
+    @XmlElement(name = "Fill")
+    protected Fill fill;
 
+    /**
+     * Creates an halo initialized to a white color and a radius of 1 pixel.
+     */
     public Halo() {
-        this(new Fill(null, StyleFactory.LITERAL_WHITE, StyleFactory.DEFAULT_FILL_OPACITY),
-                StyleFactory.DEFAULT_HALO_RADIUS);
-    }
-
-    public Halo(Fill fill, Expression<Feature, ? extends Number> radius) {
-        ArgumentChecks.ensureNonNull("fill", fill);
-        ArgumentChecks.ensureNonNull("radius", radius);
-        this.fill = fill;
-        this.radius = radius;
     }
 
-    @Override
-    public Fill getFill() {
-        return fill;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Halo(final Halo source) {
+        super(source);
+        fill   = source.fill;
+        radius = source.radius;
     }
 
-    public void setFill(Fill fill) {
-        ArgumentChecks.ensureNonNull("fill", fill);
-        this.fill = fill;
+    /**
+     * Returns the expression fetching the pixel radius of the halo around the text.
+     * This is the absolute size of a halo radius in pixels.
+     * It extends the area to the outside edge of glyphs and the inside edge of "holes" in the glyphs.
+     * Negative values are not allowed.
+     *
+     * @return radius (in pixels) of the  the halo around the text.
+     */
+    public Expression<Feature, ? extends Number> getRadius() {
+        return defaultToOne(radius);
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getRadius() {
-        return radius;
+    /**
+     * Sets the expression fetching the pixel radius of the halo around the text.
+     * If this method is never invoked, then the default value is 1 pixel.
+     *
+     * @param  value  new radius (in pixels), or {@code null} for resetting the default value.
+     */
+    public void setRadius(Expression<Feature, ? extends Number> value) {
+        radius = value;
     }
 
-    public void setRadius(Expression<Feature, ? extends Number> radius) {
-        ArgumentChecks.ensureNonNull("radius", radius);
-        this.radius = radius;
+    /**
+     * Returns the fill for the halo area around the text.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this halo, and conversely.
+     *
+     * @return graphic, color and opacity of the text to draw.
+     */
+    public Fill getFill() {
+        if (fill == null) {
+            fill = new Fill(Fill.WHITE);
+        }
+        return fill;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets the fill for the halo area around the text.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a solid white color.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new fill of the text to draw, or {@code null} for resetting the default value.
+     */
+    public void setFill(final Fill value) {
+        fill = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(fill, radius);
+    final Object[] properties() {
+        return new Object[] {radius, fill};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Halo other = (Halo) obj;
-        return Objects.equals(this.fill, other.fill)
-            && Objects.equals(this.radius, other.radius);
+    public Halo clone() {
+        final var clone = (Halo) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static Halo castOrCopy(org.opengis.style.Halo candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Halo) {
-            return (Halo) candidate;
-        }
-        return new Halo(
-                Fill.castOrCopy(candidate.getFill()),
-                candidate.getRadius());
+    private void selfClone() {
+        if (fill != null) fill = fill.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
index 5c9adc8655..82399a87d2 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
@@ -16,21 +16,52 @@
  */
 package org.apache.sis.internal.style;
 
+import jakarta.xml.bind.annotation.XmlSeeAlso;
+
+
 /**
+ * Position of a label relative to a point, line string or polygon.
+ * It can be either a {@link PointPlacement} or a {@link LinePlacement}.
+ * The former allows a graphic to be plotted directly at the point,
+ * which might be useful to label a city.
+ * The latter allows to draw a label along a line,
+ * which might be useful for labeling a road or a river.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public interface LabelPlacement extends org.opengis.style.LabelPlacement{
+@XmlSeeAlso({
+    PointPlacement.class,
+    LinePlacement.class
+})
+public abstract class LabelPlacement extends StyleElement {
+    /**
+     * Creates a new label placement.
+     * Intentionally restricted to this package because {@link #properties()} is package-private.
+     */
+    LabelPlacement() {
+    }
+
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    LabelPlacement(final LabelPlacement source) {
+        super(source);
+    }
 
-    public static LabelPlacement castOrCopy(org.opengis.style.LabelPlacement candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof org.opengis.style.PointPlacement) {
-            return PointPlacement.castOrCopy((org.opengis.style.PointPlacement) candidate);
-        } else if (candidate instanceof org.opengis.style.LinePlacement) {
-            return LinePlacement.castOrCopy((org.opengis.style.LinePlacement) candidate);
-        } else {
-            throw new IllegalArgumentException("Unexpected symbol type " + candidate.getClass().getName());
-        }
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
+    @Override
+    public LabelPlacement clone() {
+        return (LabelPlacement) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
index 6874f3e4d9..490b5e7c74 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
@@ -16,144 +16,257 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.LinePlacement}.
+ * Instructions about where and how a text label should be rendered relative to a line.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Ian Turton (CCG)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class LinePlacement implements LabelPlacement, org.opengis.style.LinePlacement {
+@XmlType(name = "LinePlacementType", propOrder = {
+    "perpendicularOffset",
+    "isRepeated",
+    "initialGap",
+    "gap",
+    "isAligned",
+    "generalizeLine"
+})
+@XmlRootElement(name = "LinePlacement")
+public class LinePlacement extends LabelPlacement {
+    /**
+     * Perpendicular distance away from a line where to draw a label, or {@code null} for the default value.
+     *
+     * @see #getPerpendicularOffset()
+     * @see #setPerpendicularOffset(Expression)
+     */
+    @XmlElement(name = "PerpendicularOffset")
+    protected Expression<Feature, ? extends Number> perpendicularOffset;
 
-    private Expression<Feature,? extends Number> perpendicularOffset;
-    private Expression<Feature,? extends Number> initialGap;
-    private Expression<Feature,? extends Number> gap;
-    private boolean repeated;
-    private boolean aligned;
-    private boolean generalizeLine;
+    /**
+     * Whether the label will be repeatedly drawn along the line, or {@code null} for the default value.
+     *
+     * @see #isRepeated()
+     * @see #setRepeated(boolean)
+     *
+     * @todo Needs an adapter from expression to plain boolean.
+     */
+    @XmlElement(name = "IsRepeated")
+    protected Expression<Feature,Boolean> isRepeated;
 
-    public LinePlacement() {
-    }
+    /**
+     * How far away the first label will be drawn, or {@code null} for the default value.
+     *
+     * @see #getInitialGap()
+     * @see #setInitialGap(Expression)
+     */
+    @XmlElement(name = "InitialGap")
+    protected Expression<Feature, ? extends Number> initialGap;
+
+    /**
+     * Distance between two labels, or {@code null} for the default value.
+     *
+     * @see #getGap()
+     * @see #setGap(Expression)
+     */
+    @XmlElement(name = "Gap")
+    protected Expression<Feature, ? extends Number> gap;
+
+    /**
+     * Whether labels are aligned to the line geometry, or {@code null} for the default value.
+     * If false, labels are drawn horizontally.
+     *
+     * @see #isAligned()
+     * @see #setAligned(Expression)
+     *
+     * @todo Needs an adapter from expression to plain boolean.
+     */
+    @XmlElement(name = "IsAligned")
+    protected Expression<Feature,Boolean> isAligned;
 
-    public LinePlacement(
-            Expression<Feature, Number> perpendicularOffset,
-            Expression<Feature, Number> initialGap,
-            Expression<Feature, Number> gap,
-            boolean repeated, boolean aligned, boolean generalizeLine) {
-        this.perpendicularOffset = perpendicularOffset;
-        this.initialGap = initialGap;
-        this.gap = gap;
-        this.repeated = repeated;
-        this.aligned = aligned;
-        this.generalizeLine = generalizeLine;
+    /**
+     * Whether to allow the geometry to be generalized, or {@code null} for the default value.
+     *
+     * @see #getGeneralizeLine()
+     * @see #setGeneralizeLine(Expression)
+     *
+     * @todo Needs an adapter from expression to plain boolean.
+     */
+    @XmlElement(name = "GeneralizeLine")
+    protected Expression<Feature,Boolean> generalizeLine;
+
+    /**
+     * Creates a new line placement.
+     */
+    public LinePlacement() {
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getPerpendicularOffset() {
-        return perpendicularOffset;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public LinePlacement(final LinePlacement source) {
+        super(source);
+        perpendicularOffset = source.perpendicularOffset;
+        isRepeated          = source.isRepeated;
+        initialGap          = source.initialGap;
+        gap                 = source.gap;
+        isAligned           = source.isAligned;
+        generalizeLine      = source.generalizeLine;
     }
 
-    public void setPerpendicularOffset(Expression<Feature, ? extends Number> perpendicularOffset) {
-        this.perpendicularOffset = perpendicularOffset;
+    /**
+     * Returns a perpendicular distance away from a line where to draw a label.
+     * The distance units of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+     * The value is positive to the left-hand side of the line string.
+     * Negative numbers mean right. The default offset is 0.
+     *
+     * @return perpendicular distance away from a line where to draw a label.
+     */
+    public Expression<Feature, ? extends Number> getPerpendicularOffset() {
+        return defaultToZero(perpendicularOffset);
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getInitialGap() {
-        return initialGap;
+    /**
+     * Sets a perpendicular distance away from a line where to draw a label.
+     * If this method is never invoked, then the default value is literal 0.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new distance to apply for drawing label, or {@code null} for resetting the default value.
+     */
+    public void setPerpendicularOffset(final Expression<Feature, ? extends Number> value) {
+        perpendicularOffset = value;
     }
 
-    public void setInitialGap(Expression<Feature, ? extends Number> initialGap) {
-        this.initialGap = initialGap;
+    /**
+     * Returns whether the label will be repeatedly drawn along the line.
+     * If {@code true}, the repetition will use the {@linkplain #getInitialGap() initial gap}
+     * and {@linkplain #getGap() gap} for defining the spaces at the beginning and between labels.
+     *
+     * @return whether the label will be repeatedly drawn along the line.
+     */
+    public Expression<Feature,Boolean> isRepeated() {
+        return defaultToFalse(isRepeated);
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getGap() {
-        return gap;
+    /**
+     * Sets whether the label will be repeatedly drawn along the line.
+     * If this method is never invoked, then the default value is literal false.
+     *
+     * @param  value  whether the label will be repeated, or {@code null} for resetting the default value.
+     */
+    public void setRepeated(final Expression<Feature,Boolean> value) {
+        isRepeated = value;
     }
 
-    public void setGap(Expression<Feature, ? extends Number> gap) {
-        this.gap = gap;
+    /**
+     * Returns how far away the first label will be drawn relative to the start of the rendering line.
+     *
+     * @return distance of first label relative to the rendering start.
+     */
+    public Expression<Feature, ? extends Number> getInitialGap() {
+        return defaultToZero(initialGap);
     }
 
-    @Override
-    public boolean isRepeated() {
-        return repeated;
+    /**
+     * Sets how far away the first label will be drawn relative to the start of the rendering line.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new distance relative to rendering start, or {@code null} for resetting the default value.
+     */
+    public void setInitialGap(final Expression<Feature, ? extends Number> value) {
+        initialGap = value;
     }
 
-    public void setRepeated(boolean repeated) {
-        this.repeated = repeated;
+    /**
+     * Returns the distance between two labels.
+     *
+     * @return distance between two labels.
+     */
+    public Expression<Feature, ? extends Number> getGap() {
+        return defaultToZero(gap);
     }
 
-    @Override
-    public boolean IsAligned() {
-        return aligned;
+    /**
+     * Sets the distance between two labels.
+     * If this method is never invoked, then the default value is literal 0.
+     *
+     * @param  value  new distance between two labels, or {@code null} for resetting the default value.
+     */
+    public void setGap(final Expression<Feature, ? extends Number> value) {
+        gap = value;
     }
 
-    public void setAligned(boolean aligned) {
-        this.aligned = aligned;
+    /**
+     * Returns whether labels are aligned to the line geometry or drawn horizontally.
+     *
+     * @return whether labels are aligned to the line geometry or drawn horizontally.
+     */
+    public Expression<Feature,Boolean> isAligned() {
+        return defaultToTrue(isAligned);
     }
 
-    @Override
-    public boolean isGeneralizeLine() {
-        return generalizeLine;
+    /**
+     * Sets whether labels are aligned to the line geometry or drawn horizontally.
+     * If this method is never invoked, then the default value is literal true.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  whether labels are aligned to the line geometry, or {@code null} for resetting the default value.
+     */
+    public void setAligned(final Expression<Feature,Boolean> value) {
+        isAligned = value;
     }
 
-    public void setGeneralizeLine(boolean generalizeLine) {
-        this.generalizeLine = generalizeLine;
+    /**
+     * Returns whether to allow the geometry to be generalized for label placement.
+     *
+     * @return whether to allow the geometry to be generalized for label placement.
+     */
+    public Expression<Feature,Boolean> getGeneralizeLine() {
+        return defaultToFalse(generalizeLine);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets whether to allow the geometry to be generalized for label placement.
+     * If this method is never invoked, then the default value is literal false.
+     *
+     * @param  value whether to allow the geometry to be generalized, or {@code null} for resetting the default value.
+     */
+    public void setGeneralizeLine(final Expression<Feature,Boolean> value) {
+        generalizeLine = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(perpendicularOffset, initialGap, gap, repeated, aligned, generalizeLine);
+    final Object[] properties() {
+        return new Object[] {perpendicularOffset, isRepeated, initialGap, gap, isAligned, generalizeLine};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final LinePlacement other = (LinePlacement) obj;
-        return this.repeated == other.repeated
-            && this.aligned == other.aligned
-            && this.generalizeLine == other.generalizeLine
-            && Objects.equals(this.perpendicularOffset, other.perpendicularOffset)
-            && Objects.equals(this.initialGap, other.initialGap)
-            && Objects.equals(this.gap, other.gap);
-    }
-
-    /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
-     */
-    public static LinePlacement castOrCopy(org.opengis.style.LinePlacement candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof LinePlacement) {
-            return (LinePlacement) candidate;
-        }
-        return new LinePlacement(
-                candidate.getPerpendicularOffset(),
-                candidate.getInitialGap(),
-                candidate.getGap(),
-                candidate.isRepeated(),
-                candidate.IsAligned(),
-                candidate.isGeneralizeLine());
+    public LinePlacement clone() {
+        final var clone = (LinePlacement) super.clone();
+        return clone;
     }
-
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
index 4c4496f7a9..03c8da3b14 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
@@ -16,106 +16,146 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import javax.measure.Unit;
-import javax.measure.quantity.Length;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.LineSymbolizer}.
+ * Instructions about how to draw on a map the lines of a geometry.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class LineSymbolizer extends Symbolizer implements org.opengis.style.LineSymbolizer {
-
-    private Expression<Feature,? extends Number> perpendicularOffset;
-    private Stroke stroke;
+@XmlType(name = "LineSymbolizerType", propOrder = {
+    "stroke",
+    "perpendicularOffset"
+})
+@XmlRootElement(name = "LineSymbolizer")
+public class LineSymbolizer extends Symbolizer {
+    /**
+     * Information about how to draw lines, or {@code null} for lazily constructed default.
+     *
+     * @see #getStroke()
+     * @see #setStroke(Stroke)
+     */
+    @XmlElement(name = "Stroke")
+    protected Stroke stroke;
 
-    public static LineSymbolizer createDefault() {
-        return new LineSymbolizer(null, null, new Description(),
-                StyleFactory.DEFAULT_UOM, new Stroke(), StyleFactory.LITERAL_ZERO);
-    }
+    /**
+     * Distance to apply for drawing lines in parallel to geometry, or {@code null} for the default value.
+     *
+     * @see #getPerpendicularOffset()
+     * @see #setPerpendicularOffset(Expression)
+     */
+    @XmlElement(name = "PerpendicularOffset")
+    protected Expression<Feature, ? extends Number> perpendicularOffset;
 
+    /**
+     * Creates a line symbolizer with the default stroke and no perpendicular offset.
+     */
     public LineSymbolizer() {
-        super();
     }
 
-    public LineSymbolizer(String name, Expression geometry, Description description,
-            Unit<Length> unit, Stroke stroke, Expression offset) {
-        super(name, geometry, description, unit);
-        this.stroke = stroke;
-        this.perpendicularOffset = offset;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public LineSymbolizer(final LineSymbolizer source) {
+        super(source);
+        stroke = source.stroke;
+        perpendicularOffset = source.perpendicularOffset;
     }
 
-    @Override
+    /**
+     * Returns the object containing all the information necessary to draw styled lines.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this symbolizer, and conversely.
+     *
+     * @return information about how to draw lines.
+     */
     public Stroke getStroke() {
+        if (stroke == null) {
+            stroke = new Stroke();
+        }
         return stroke;
     }
 
-    public void setStroke(Stroke stroke) {
-        this.stroke = stroke;
-    }
-
-    @Override
-    public Expression<Feature,? extends Number> getPerpendicularOffset() {
-        return perpendicularOffset;
+    /**
+     * Sets the object containing all the information necessary to draw styled lines.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a {@linkplain Stroke#Stroke() default instance}.
+     *
+     * @param  value  new information about how to draw lines, or {@code null} for resetting the default value.
+     */
+    public void setStroke(final Stroke value) {
+        stroke = value;
     }
 
-    public void setPerpendicularOffset(Expression<Feature, ? extends Number> perpendicularOffset) {
-        this.perpendicularOffset = perpendicularOffset;
+    /**
+     * Returns a distance to apply for drawing lines in parallel to the original geometry.
+     * These parallel lines have to be constructed so that the distance between
+     * original geometry and drawn line stays equal.
+     * This construction may result in drawn lines that are
+     * actually smaller or longer than the original geometry.
+     *
+     * <p>The distance units of measurement is given by {@link #getUnitOfMeasure()}.
+     * The value is positive to the left-hand side of the line string.
+     * Negative numbers mean right. The default offset is 0.</p>
+     *
+     * @return distance to apply for drawing lines in parallel to the original geometry.
+     */
+    public Expression<Feature, ? extends Number> getPerpendicularOffset() {
+        return defaultToZero(perpendicularOffset);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets a distance to apply for drawing lines in parallel to the original geometry.
+     * If this method is never invoked, then the default value is literal 0.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new distance to apply for drawing lines, or {@code null} for resetting the default value.
+     */
+    public void setPerpendicularOffset(final Expression<Feature, ? extends Number> value) {
+        perpendicularOffset = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return super.hashCode() + Objects.hash(perpendicularOffset, stroke);
+    final Object[] properties() {
+        return new Object[] {perpendicularOffset, stroke};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (!super.equals(obj)) {
-            return false;
-        }
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final LineSymbolizer other = (LineSymbolizer) obj;
-        return Objects.equals(this.perpendicularOffset, other.perpendicularOffset)
-            && Objects.equals(this.stroke, other.stroke);
+    public LineSymbolizer clone() {
+        final var clone = (LineSymbolizer) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static LineSymbolizer castOrCopy(org.opengis.style.LineSymbolizer candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof LineSymbolizer) {
-            return (LineSymbolizer) candidate;
-        }
-        return new LineSymbolizer(
-                candidate.getName(),
-                candidate.getGeometry(),
-                Description.castOrCopy(candidate.getDescription()),
-                candidate.getUnitOfMeasure(),
-                Stroke.castOrCopy(candidate.getStroke()),
-                candidate.getPerpendicularOffset());
+    private void selfClone() {
+        stroke = stroke.clone();
     }
-
-
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
index b9f94b2c71..5a4d9bc90d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
@@ -16,117 +16,288 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
+import java.util.Optional;
+import jakarta.xml.bind.Marshaller;
+import jakarta.xml.bind.Unmarshaller;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import org.opengis.filter.Literal;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Mark}.
+ * Predefined shapes that can be drawn at the points of the geometry.
+ * This is an alternative to {@link ExternalGraphic} for
+ * {@linkplain Graphic#graphicalSymbols graphical symbols}.
+ * When marks are provided in the bottom of the graphical symbol list,
+ * it allows a style to be specified that can produce a usable result in a best-effort basis.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Mark implements org.opengis.style.Mark, GraphicalSymbol {
+@XmlType(name = "MarkType", propOrder = {
+    "wellKnownName",
+//  "onlineResource",       // XML encoding not yet available.
+//  "inlineContent",        // Idem.
+    "format",
+    "markIndex",
+    "fill",
+    "stroke"
+})
+@XmlRootElement(name = "Mark")
+public class Mark extends GraphicalSymbol {
+    /**
+     * Literal for a predefined well-known name which can be used as a mark.
+     */
+    public static final Literal<Feature,String> SQUARE, CIRCLE, TRIANGLE, STAR, CROSS, X;
+    static {
+        final var FF = FF();
+        SQUARE   = FF.literal("square");
+        CIRCLE   = FF.literal("circle");
+        TRIANGLE = FF.literal("triangle");
+        STAR     = FF.literal("star");
+        CROSS    = FF.literal("cross");
+        X        = FF.literal("x");
+    }
 
-    private Expression<Feature,String> wellKnownName;
-    private ExternalMark externalMark;
-    private Fill fill;
-    private Stroke stroke;
+    /**
+     * Expression whose value will indicate the symbol to draw, or {@code null} for the default value.
+     *
+     * @see #getWellKnownName()
+     * @see #setWellKnownName(Expression)
+     */
+    @XmlElement(name = "WellKnownName")
+    protected Expression<Feature,String> wellKnownName;
 
-    public Mark() {
-        this(StyleFactory.MARK_SQUARE, null, new Fill(), new Stroke());
+    /**
+     * Information about how the interior of marks should be filled, or {@code null} for no fill.
+     * If no value has been explicitly set (including null value),
+     * then a default fill will be lazily created when first requested.
+     *
+     * @see #getFill()
+     * @see #setFill(Fill)
+     */
+    @XmlElement(name = "Fill")
+    protected Fill fill;
+
+    /**
+     * Whether {@link #fill} has been explicitly set to some value, including null.
+     * If {@code false}, then a default fill will be created when first needed.
+     */
+    private boolean isFillSet;
+
+    /**
+     * Information about styled lines, or {@code null} if mark lines should not be drawn.
+     * If no value has been explicitly set (including null value),
+     * then a default stroke will be lazily created when first needed.
+     *
+     * @see #getStroke()
+     * @see #setStroke(Stroke)
+     */
+    @XmlElement(name = "Stroke")
+    protected Stroke stroke;
+
+    /**
+     * Whether {@link #stroke} has been explicitly set to some value, including null.
+     * If {@code false}, then a default stroke will be created when first requested.
+     */
+    private boolean isStrokeSet;
+
+    /**
+     * Individual mark to select in a mark archive, or {@code null} if none.
+     *
+     * @see #getMarkIndex()
+     * @see #setMarkIndex(Expression)
+     */
+    @XmlElement(name = "MarkIndex")
+    protected Expression<Feature,Integer> markIndex;
+
+    /**
+     * Invoked by JAXB before unmarshalling this mark.
+     * OGC 05-077r4 said that if the fill or the stroke is not specified,
+     * then no fill or stroke should be applied.
+     */
+    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
+        isFillSet   = true;
+        isStrokeSet = true;
     }
 
-    public Mark(Expression<Feature, String> wellKnownName,
-            ExternalMark externalMark,
-            Fill fill,
-            Stroke stroke) {
-        this.wellKnownName = wellKnownName;
-        this.externalMark = externalMark;
-        this.fill = fill;
-        this.stroke = stroke;
+    /**
+     * Invoked by JAXB before marshalling this mark.
+     * Creates the default fill and stroke if needed.
+     */
+    private void beforeMarshal(Marshaller caller) {
+        if (fill   == null && !isFillSet)   fill   = new Fill();
+        if (stroke == null && !isStrokeSet) stroke = new Stroke();
     }
 
-    @Override
-    public Expression getWellKnownName() {
-        return wellKnownName;
+    /**
+     * Creates a mark initialized to a gray square with black outline.
+     * The size is specified by {@link Graphic#getSize()} and should be 6 pixels by default.
+     */
+    public Mark() {
     }
 
-    public void setWellKnownName(Expression<Feature, String> wellKnownName) {
-        this.wellKnownName = wellKnownName;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Mark(final Mark source) {
+        super(source);
+        wellKnownName = source.wellKnownName;
+        fill          = source.fill;
+        stroke        = source.stroke;
     }
 
-    @Override
-    public ExternalMark getExternalMark() {
-        return externalMark;
+    /**
+     * Returns the expression whose value will indicate the symbol to draw.
+     * Allowed values include at least "square", "circle", "triangle", "star", "cross", and "x".
+     * Renderings of these marks may be made solid or hollow depending on
+     * {@linkplain #getFill() fill} and {@linkplain #getStroke() stroke} elements.
+     *
+     * <p>The well-known name may be ignored if the mark is also provided
+     * by {@linkplain #getInlineContent() inline content}
+     * or {@linkplain #getOnlineResource() online resource}.</p>
+     *
+     * @return well-known name of the mark to render.
+     *
+     * @see #getOnlineResource()
+     * @see #getInlineContent()
+     */
+    public Expression<Feature,String> getWellKnownName() {
+        final var value = wellKnownName;
+        return (value != null) ? value : SQUARE;
     }
 
-    public void setExternalMark(ExternalMark externalMark) {
-        this.externalMark = externalMark;
+    /**
+     * Sets the expression whose value will indicate the symbol to draw.
+     * If this method is never invoked, then the default value is {@link #SQUARE}.
+     *
+     * @param  value  well-known name of the mark to render, or {@code null} for resetting the default.
+     */
+    public void setWellKnownName(final Expression<Feature,String> value) {
+        wellKnownName = value;
     }
 
-    @Override
-    public Fill getFill() {
-        return fill;
+    /**
+     * Returns the object that indicates how the mark should be filled.
+     * If absent, then the marks are not to be filled at all.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this mark, and conversely.</p>
+     *
+     * @return information about how the interior of polygons should be filled.
+     *
+     * @see #getStroke()
+     */
+    public Optional<Fill> getFill() {
+        if (!isFillSet) {
+            isFillSet = true;
+            fill = new Fill();
+        }
+        return Optional.ofNullable(fill);
     }
 
-    public void setFill(Fill fill) {
-        this.fill = fill;
+    /**
+     * Sets information about how the interior of marks should be filled.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is the {@linkplain Fill#Fill() default fill}.
+     *
+     * @param  value  new information about the fill, or {@code null} for no fill.
+     */
+    public void setFill(final Fill value) {
+        isFillSet = true;
+        fill = value;
     }
 
-    @Override
-    public Stroke getStroke() {
-        return stroke;
+    /**
+     * Returns the object that indicates how the edges of the mark will be drawn.
+     * This is used for the edges of marks.
+     * Absent means that the edges will not be drawn at all.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this mark, and conversely.</p>
+     *
+     * @return information about styled lines, or {@code null} if lines should not be drawn.
+     *
+     * @see #getFill()
+     */
+    public Optional<Stroke> getStroke() {
+        if (!isStrokeSet) {
+            isStrokeSet = true;
+            stroke = new Stroke();
+        }
+        return Optional.ofNullable(stroke);
     }
 
-    public void setStroke(Stroke stroke) {
-        this.stroke = stroke;
+    /**
+     * Sets information about styled lines.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is the {@linkplain Stroke#Stroke() default stroke}.
+     *
+     * @param  value  new information about styled lines, or {@code null} if lines should not be drawn.
+     */
+    public void setStroke(final Stroke value) {
+        isStrokeSet = true;
+        stroke = value;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns an individual mark to select in a mark archive.
+     * For example it can be the index of a glyph to select in a TrueType fond file.
+     *
+     * @return individual mark to select in a mark archive.
+     */
+    public Optional<Expression<Feature,Integer>> getMarkIndex() {
+        return Optional.ofNullable(markIndex);
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(wellKnownName, externalMark, fill, stroke);
+    /**
+     * Sets an individual mark to select in a mark archive.
+     *
+     * @param  value  new index of an individual mark to select, or {@code null} if none.
+     */
+    public void setMarkIndex(final Expression<Feature,Integer> value) {
+        markIndex = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Mark other = (Mark) obj;
-        return Objects.equals(this.wellKnownName, other.wellKnownName)
-            && Objects.equals(this.externalMark, other.externalMark)
-            && Objects.equals(this.fill, other.fill)
-            && Objects.equals(this.stroke, other.stroke);
+    final Object[] properties() {
+        return new Object[] {wellKnownName, fill, isFillSet, stroke, isStrokeSet, markIndex};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static Mark castOrCopy(org.opengis.style.Mark candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Mark) {
-            return (Mark) candidate;
-        }
-        return new Mark(
-                candidate.getWellKnownName(),
-                ExternalMark.castOrCopy(candidate.getExternalMark()),
-                Fill.castOrCopy(candidate.getFill()),
-                Stroke.castOrCopy(candidate.getStroke()));
+    @Override
+    public Mark clone() {
+        final var clone = (Mark) super.clone();
+        clone.selfClone();
+        return clone;
+    }
+
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (fill   != null) fill   = fill.clone();
+        if (stroke != null) stroke = stroke.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
index 028feb7e84..79a84d8689 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
@@ -16,103 +16,182 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.PointPlacement}.
+ * Instructions about how a text label is positioned relative to a point.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Ian Turton (CCG)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class PointPlacement implements LabelPlacement, org.opengis.style.PointPlacement {
+@XmlType(name = "PointPlacementType", propOrder = {
+    "anchorPoint",
+    "displacement",
+    "rotation"
+})
+@XmlRootElement(name = "PointPlacement")
+public class PointPlacement extends LabelPlacement {
+    /**
+     * Location to use for anchoring the label to the point, or {@code null} for lazily constructed default.
+     *
+     * @see #getAnchorPoint()
+     * @see #setAnchorPoint(AnchorPoint)
+     */
+    @XmlElement(name = "AnchorPoint")
+    protected AnchorPoint anchorPoint;
+
+    /**
+     * Two-dimensional displacements from the "hot-spot" point, or {@code null} for lazily constructed default.
+     *
+     * @see #getDisplacement()
+     * @see #setDisplacement(Displacement)
+     */
+    @XmlElement(name = "Displacement")
+    protected Displacement displacement;
 
-    private AnchorPoint anchorPoint;
-    private Displacement displacement;
-    private Expression<Feature,? extends Number> rotation;
+    /**
+     * Expression fetching the rotation of the label when it is drawn.
+     *
+     * @see #getRotation()
+     * @see #setRotation(Expression)
+     */
+    @XmlElement(name = "Rotation")
+    protected Expression<Feature, ? extends Number> rotation;
 
+    /**
+     * Creates a new point placement.
+     */
     public PointPlacement() {
     }
 
-    public PointPlacement(
-            AnchorPoint anchorPoint,
-            Displacement displacement,
-            Expression<Feature, ? extends Number> rotation) {
-        this.anchorPoint = anchorPoint;
-        this.displacement = displacement;
-        this.rotation = rotation;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public PointPlacement(final PointPlacement source) {
+        super(source);
+        anchorPoint  = source.anchorPoint;
+        displacement = source.displacement;
+        rotation     = source.rotation;
     }
 
-    @Override
+    /**
+     * Returns the location inside of a label to use for anchoring the label to the point.
+     * The coordinates are given as (<var>x</var>,<var>y</var>) floating-point numbers
+     * relative the the label bounding box, where (0,0) is the lower-left corner and
+     * (1,1) is the upper-right corner.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this graphic, and conversely.</p>
+     *
+     * @return the anchor point.
+     */
     public AnchorPoint getAnchorPoint() {
+        if (anchorPoint == null) {
+            anchorPoint = new AnchorPoint();
+        }
         return anchorPoint;
     }
 
-    public void setAnchorPoint(AnchorPoint anchorPoint) {
-        this.anchorPoint = anchorPoint;
+    /**
+     * Sets the location inside of a label to use for anchoring the label to the point.
+     * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+     * then the default value is a {@linkplain AnchorPoint#AnchorPoint() default anchor point}.
+     *
+     * @param  value  new anchor point, or {@code null} for resetting the default value.
+     */
+    public void setAnchorPoint(final AnchorPoint value) {
+        anchorPoint = value;
     }
 
-    @Override
+    /**
+     * Returns the two-dimensional displacements from the "hot-spot" point.
+     * The displacements are in units of pixels above and to the right of the point.
+     *
+     * @return two-dimensional displacements from the "hot-spot" point.
+     *
+     * @see Graphic#getDisplacement()
+     * @see PolygonSymbolizer#getDisplacement()
+     */
     public Displacement getDisplacement() {
+        if (displacement == null) {
+            displacement = new Displacement();
+        }
         return displacement;
     }
 
-    public void setDisplacement(Displacement displacement) {
-        this.displacement = displacement;
-    }
-
-    @Override
-    public Expression<Feature,? extends Number> getRotation() {
-        return rotation;
+    /**
+     * Sets the two-dimensional displacements from the "hot-spot" point.
+     * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+     * then the default value is a {@linkplain Displacement#Displacement() default displacement}.
+     *
+     * @param  value  new displacements from the "hot-spot" point, or {@code null} for resetting the default value.
+     */
+    public void setDisplacement(final Displacement value) {
+        displacement = value;
     }
 
-    public void setRotation(Expression<Feature, ? extends Number> rotation) {
-        this.rotation = rotation;
+    /**
+     * Returns the expression fetching the rotation of the label when it is drawn.
+     * This is the clockwise rotation of the label in degrees from the normal direction for a font
+     * (left-to-right for Latin languages).
+     *
+     * @return rotation of the label when it is drawn.
+     */
+    public Expression<Feature, ? extends Number> getRotation() {
+        return defaultToZero(rotation);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets the expression fetching the rotation of the label when it is drawn.
+     * If this method is never invoked, then the default value is literal zero.
+     *
+     * @param  value  new rotation of the label, or {@code null} for resetting the default value.
+     */
+    public void setRotation(final Expression<Feature, ? extends Number> value) {
+        rotation = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(anchorPoint, displacement, rotation);
+    final Object[] properties() {
+        return new Object[] {anchorPoint, displacement, rotation};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final PointPlacement other = (PointPlacement) obj;
-        return Objects.equals(this.anchorPoint, other.anchorPoint)
-            && Objects.equals(this.displacement, other.displacement)
-            && Objects.equals(this.rotation, other.rotation);
+    public PointPlacement clone() {
+        final var clone = (PointPlacement) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static PointPlacement castOrCopy(org.opengis.style.PointPlacement candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof PointPlacement) {
-            return (PointPlacement) candidate;
-        }
-        return new PointPlacement(
-                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
-                Displacement.castOrCopy(candidate.getDisplacement()),
-                candidate.getRotation());
+    private void selfClone() {
+        if (anchorPoint  != null) anchorPoint  = anchorPoint .clone();
+        if (displacement != null) displacement = displacement.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
index 52cbe1d83f..d03fb752cc 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
@@ -16,93 +16,149 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import javax.measure.Unit;
-import javax.measure.quantity.Length;
-import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import jakarta.xml.bind.Marshaller;
+import jakarta.xml.bind.Unmarshaller;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.PointSymbolizer}.
+ * Instructions about how to draw a graphic at a point.
+ * if a line, polygon, or raster geometry is used with this symbolizer,
+ * then some representative point such as the centroid should be used.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class PointSymbolizer extends Symbolizer implements org.opengis.style.PointSymbolizer {
+@XmlType(name = "PointSymbolizerType")
+@XmlRootElement(name = "PointSymbolizer")
+public class PointSymbolizer extends Symbolizer implements GraphicalElement {
+    /**
+     * Information about how to draw graphics, or {@code null} for no graphic.
+     * If no value has been explicitly set (including null value),
+     * then a default graphic will be lazily created when first requested.
+     *
+     * @see #getGraphic()
+     * @see #setGraphic(Graphic)
+     */
+    @XmlElement(name = "Graphic")
+    protected Graphic graphic;
 
-    private Graphic graphic;
+    /**
+     * Whether {@link #graphic} has been explicitly set to some value, including null.
+     * If {@code false}, then a default graphic will be created when first needed.
+     */
+    private boolean isGraphicSet;
 
-    public static PointSymbolizer createDefault() {
-        return new PointSymbolizer(
-                null,
-                null,
-                new Description(),
-                StyleFactory.DEFAULT_UOM,
-                Graphic.createDefault());
+    /**
+     * Invoked by JAXB before unmarshalling this legend.
+     * OGC 05-077r4 said that if the graphic is not specified, then none should be used.
+     */
+    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
+        isGraphicSet = true;
     }
 
-    public PointSymbolizer() {
+    /**
+     * Invoked by JAXB before marshalling this point symbolizer.
+     * Creates the default graphic if needed.
+     */
+    private void beforeMarshal(Marshaller caller) {
+        if (graphic == null && !isGraphicSet) graphic = new Graphic();
     }
 
-    public PointSymbolizer(String name, Expression geometry, Description description, Unit<Length> unit, Graphic graphic) {
-        super(name, geometry, description, unit);
-        this.graphic = graphic;
+    /**
+     * Creates a symbolizer initialized to a default graphic.
+     */
+    public PointSymbolizer() {
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public PointSymbolizer(final PointSymbolizer source) {
+        super(source);
+        graphic = source.graphic;
     }
 
+    /**
+     * Returns the graphic that will be drawn at each point of the geometry.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.
+     *
+     * @return information about how to draw graphics.
+     *
+     * @see #isVisible()
+     */
     @Override
     public Graphic getGraphic() {
+        if (graphic == null) {
+            graphic = new Graphic();
+        }
         return graphic;
     }
 
-    public void setGraphic(Graphic graphic) {
-        this.graphic = graphic;
+    /**
+     * Specifies the graphic that will be drawn at each point of the geometry.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is a {@linkplain Graphic#Graphic() default instance}.
+     * If this method is invoked with an explicit {@code null}, then nothing will be plotted.
+     *
+     * @param  value  new information about how to draw graphics, or {@code null} for none.
+     */
+    @Override
+    public void setGraphic(final Graphic value) {
+        isGraphicSet = true;
+        graphic = value;
     }
 
+    /**
+     * Returns {@code true} if this symbolizer has a graphic.
+     * If {@code setGraphic(null)} has been explicitly invoked with a null argument value,
+     * then this method returns {@code false}.
+     *
+     * @return whether this symbolizer has a graphic.
+     *
+     * @see #setGraphic(Graphic)
+     */
     @Override
-    public int hashCode() {
-        return super.hashCode() + Objects.hashCode(this.graphic);
+    public boolean isVisible() {
+        return graphic != null || !isGraphicSet;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (!super.equals(obj)) {
-            return false;
-        }
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final PointSymbolizer other = (PointSymbolizer) obj;
-        return Objects.equals(this.graphic, other.graphic);
+    final Object[] properties() {
+        return new Object[] {graphic, isGraphicSet};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static PointSymbolizer castOrCopy(org.opengis.style.PointSymbolizer candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof PointSymbolizer) {
-            return (PointSymbolizer) candidate;
-        }
-        return new PointSymbolizer(
-                candidate.getName(),
-                candidate.getGeometry(),
-                Description.castOrCopy(candidate.getDescription()),
-                candidate.getUnitOfMeasure(),
-                Graphic.castOrCopy(candidate.getGraphic())
-            );
+    @Override
+    public PointSymbolizer clone() {
+        final var clone = (PointSymbolizer) super.clone();
+        clone.selfClone();
+        return clone;
+    }
+
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (graphic != null) graphic = graphic.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
index f10713fdc4..c0ad23911e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
@@ -16,131 +16,304 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import javax.measure.Unit;
-import javax.measure.quantity.Length;
+import java.util.Optional;
+import jakarta.xml.bind.Marshaller;
+import jakarta.xml.bind.Unmarshaller;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.PolygonSymbolizer}.
+ * Instructions about how to draw on a map the lines and the interior of polygons.
+ * Holes are not filled but borders around the holes are stroked.
+ * Islands within holes are filled and stroked, and so on.
+ *
+ * <p>If the geometry is not a polygon, then
+ * line strings should be closed for filling but not for stroking.
+ * Points should be rendered as small squares, and
+ * rasters should be rendered using the coverage extent as the polygon.</p>
  *
- * @author Johann Sorel (Geomatys)
+ * <p>The fill should be rendered first, then the stroke should be rendered on top of the fill.
+ * A missing stroke element means that the geometry will not be stroked.</p>
+ *
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class PolygonSymbolizer extends Symbolizer implements org.opengis.style.PolygonSymbolizer {
+@XmlType(name = "PolygonSymbolizerType", propOrder = {
+    "fill",
+    "stroke",
+    "displacement",
+    "perpendicularOffset"
+})
+@XmlRootElement(name = "PolygonSymbolizer")
+public class PolygonSymbolizer extends Symbolizer {
+    /**
+     * Information about how the interior of polygons should be filled, or {@code null} for no fill.
+     * If no value has been explicitly set (including null value),
+     * then a default fill will be lazily created when first requested.
+     *
+     * @see #getFill()
+     * @see #setFill(Fill)
+     */
+    @XmlElement(name = "Fill")
+    protected Fill fill;
+
+    /**
+     * Whether {@link #fill} has been explicitly set to some value, including null.
+     * If {@code false}, then a default fill will be created when first needed.
+     */
+    private boolean isFillSet;
+
+    /**
+     * Information about styled lines, or {@code null} if lines should not be drawn.
+     * If no value has been explicitly set (including null value),
+     * then a default stroke will be lazily created when first needed.
+     *
+     * @see #getStroke()
+     * @see #setStroke(Stroke)
+     */
+    @XmlElement(name = "Stroke")
+    protected Stroke stroke;
+
+    /**
+     * Whether {@link #stroke} has been explicitly set to some value, including null.
+     * If {@code false}, then a default stroke will be created when first requested.
+     */
+    private boolean isStrokeSet;
 
-    private Stroke stroke;
-    private Fill fill;
-    private Displacement displacement;
-    private Expression<Feature,? extends Number> offset;
+    /**
+     * Displacement from the "hot-spot" point, or {@code null} for lazily constructed default.
+     *
+     * @see #getDisplacement()
+     * @see #setDisplacement(Displacement)
+     */
+    @XmlElement(name = "Displacement")
+    protected Displacement displacement;
+
+    /**
+     * Distance to apply for drawing lines in parallel to geometry, or {@code null} for the default value.
+     *
+     * @see #getPerpendicularOffset()
+     * @see #setPerpendicularOffset(Expression)
+     */
+    @XmlElement(name = "PerpendicularOffset")
+    protected Expression<Feature, ? extends Number> perpendicularOffset;
 
-    public static PolygonSymbolizer createDefault() {
-        return new PolygonSymbolizer(null,null, new Description(),
-                StyleFactory.DEFAULT_UOM, new Stroke(), new Fill(),
-                new Displacement(), StyleFactory.LITERAL_ZERO);
+    /**
+     * Invoked by JAXB before unmarshalling this polyogon symbolizer.
+     * OGC 05-077r4 said that if the fill or the stroke is not specified,
+     * then no fill or stroke should be applied.
+     */
+    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
+        isFillSet   = true;
+        isStrokeSet = true;
     }
 
-    public PolygonSymbolizer() {
+    /**
+     * Invoked by JAXB before marshalling this polyogon symbolizer.
+     * Creates the default fill and stroke if needed.
+     */
+    private void beforeMarshal(Marshaller caller) {
+        if (fill   == null && !isFillSet)   fill   = new Fill();
+        if (stroke == null && !isStrokeSet) stroke = new Stroke();
     }
 
-    public PolygonSymbolizer(String name, Expression geometry, Description description, Unit<Length> unit, Stroke stroke, Fill fill, Displacement displacement, Expression offset) {
-        super(name, geometry, description, unit);
-        this.stroke = stroke;
-        this.fill = fill;
-        this.displacement = displacement;
-        this.offset = offset;
+    /**
+     * Creates a polygon symbolizer initialized to the default fill and default stroke.
+     */
+    public PolygonSymbolizer() {
     }
 
-    @Override
-    public Stroke getStroke() {
-        return stroke;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public PolygonSymbolizer(final PolygonSymbolizer source) {
+        super(source);
+        fill                = source.fill;
+        stroke              = source.stroke;
+        isFillSet           = source.isFillSet;
+        isStrokeSet         = source.isStrokeSet;
+        displacement        = source.displacement;
+        perpendicularOffset = source.perpendicularOffset;
     }
 
-    public void setStroke(Stroke stroke) {
-        this.stroke = stroke;
+    /**
+     * Returns information about how the interior of polygons should be filled.
+     * If absent, then the polygons are not to be filled at all.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+     *
+     * @return information about how the interior of polygons should be filled.
+     *
+     * @see #getStroke()
+     * @see #isVisible()
+     */
+    public Optional<Fill> getFill() {
+        if (!isFillSet) {
+            isFillSet = true;
+            fill = new Fill();
+        }
+        return Optional.ofNullable(fill);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets information about how the interior of polygons should be filled.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is the {@linkplain Fill#Fill() default fill}.
+     *
+     * @param  value  new information about the fill, or {@code null} for no fill.
+     */
+    public void setFill(final Fill value) {
+        isFillSet = true;
+        fill = value;
     }
 
-    @Override
-    public Fill getFill() {
-        return fill;
+    /**
+     * Returns information about styled lines.
+     * This is used for the edges of polygons.
+     * Absent means that lines should not be drawn
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+     *
+     * @return information about styled lines.
+     *
+     * @see #getFill()
+     * @see #isVisible()
+     */
+    public Optional<Stroke> getStroke() {
+        if (!isStrokeSet) {
+            isStrokeSet = true;
+            stroke = new Stroke();
+        }
+        return Optional.ofNullable(stroke);
     }
 
-    public void setFill(Fill fill) {
-        this.fill = fill;
+    /**
+     * Sets information about styled lines.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is the {@linkplain Stroke#Stroke() default stroke}.
+     *
+     * @param  value  new information about styled lines, or {@code null} if lines should not be drawn.
+     */
+    public void setStroke(final Stroke value) {
+        isStrokeSet = true;
+        stroke = value;
     }
 
-    @Override
+    /**
+     * Returns the two-dimensional displacement from the "hot-spot" point.
+     * This element may be used to avoid over-plotting of multiple polygons for one geometry.
+     * It may also be used for creating shadows of polygon geometries.
+     * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+     * Positive values are to the right of the point.
+     * The default displacement is <var>x</var> = 0, <var>y</var> = 0,
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+     *
+     * @return displacement from the "hot-spot" point.
+     *
+     * @see Graphic#getDisplacement()
+     * @see PointPlacement#getDisplacement()
+     */
     public Displacement getDisplacement() {
+        if (displacement == null) {
+            displacement = new Displacement();
+        }
         return displacement;
     }
 
-    public void setDisplacement(Displacement displacement) {
-        this.displacement = displacement;
+    /**
+     * Sets the two-dimensional displacement from the "hot-spot" point.
+     * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+     * then the default value is a {@linkplain Displacement#Displacement() default displacement}.
+     *
+     * @param  value  new displacement from the "hot-spot" point, or {@code null} for resetting the default value.
+     */
+    public void setDisplacement(final Displacement value) {
+        displacement = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getPerpendicularOffset() {
-        return offset;
+    /**
+     * Returns a distance to apply for drawing lines in parallel to the original polygon.
+     * This property allow to draw polygons smaller or larger than their actual geometry.
+     * The distance units of measurement is given by {@link #getUnitOfMeasure()}.
+     * The value is positive outside the polygon.
+     *
+     * @return distance to apply for drawing lines in parallel to the original geometry.
+     */
+    public Expression<Feature, ? extends Number> getPerpendicularOffset() {
+        return defaultToZero(perpendicularOffset);
     }
 
-    public void setPerpendicularOffset(Expression<Feature,? extends Number> offset) {
-        this.offset = offset;
+    /**
+     * Sets a distance to apply for drawing lines in parallel to the original geometry.
+     * If this method is never invoked, then the default value is literal 0.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new distance to apply for drawing lines, or {@code null} for resetting the default value.
+     */
+    public void setPerpendicularOffset(final Expression<Feature, ? extends Number> value) {
+        perpendicularOffset = value;
     }
 
+    /**
+     * Returns {@code true} if this symbolizer has a fill and/or a stroke.
+     * If both {@code setFill(null)} and {@code setStroke(null)} have been
+     * explicitly invoked with a null argument value, then this method returns {@code false}.
+     *
+     * @return whether this symbolizer has a fill and/or a stroke.
+     *
+     * @see #setFill(Fill)
+     * @see #setStroke(Stroke)
+     */
     @Override
-    public int hashCode() {
-        return super.hashCode() + Objects.hash(stroke, fill, displacement, offset);
+    public boolean isVisible() {
+        return fill != null || stroke != null || !(isFillSet | isStrokeSet);
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (!super.equals(obj)) {
-            return false;
-        }
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final PolygonSymbolizer other = (PolygonSymbolizer) obj;
-        return Objects.equals(this.stroke, other.stroke)
-            && Objects.equals(this.fill, other.fill)
-            && Objects.equals(this.displacement, other.displacement)
-            && Objects.equals(this.offset, other.offset);
+    final Object[] properties() {
+        return new Object[] {fill, isFillSet, stroke, isStrokeSet, displacement, perpendicularOffset};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static PolygonSymbolizer castOrCopy(org.opengis.style.PolygonSymbolizer candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof PolygonSymbolizer) {
-            return (PolygonSymbolizer) candidate;
-        }
-        return new PolygonSymbolizer(
-                candidate.getName(),
-                candidate.getGeometry(),
-                Description.castOrCopy(candidate.getDescription()),
-                candidate.getUnitOfMeasure(),
-                Stroke.castOrCopy(candidate.getStroke()),
-                Fill.castOrCopy(candidate.getFill()),
-                Displacement.castOrCopy(candidate.getDisplacement()),
-                candidate.getPerpendicularOffset()
-            );
+    @Override
+    public PolygonSymbolizer clone() {
+        final var clone = (PolygonSymbolizer) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (stroke       != null) stroke       = stroke.clone();
+        if (fill         != null) fill         = fill.clone();
+        if (displacement != null) displacement = displacement.clone();
+    }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
index 0102b4b976..9f045f40ba 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
@@ -16,171 +16,332 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import javax.measure.Unit;
-import javax.measure.quantity.Length;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 import org.opengis.style.OverlapBehavior;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.RasterSymbolizer}.
+ * Instructions about how to render raster, matrix or coverage data.
+ * It may be satellite photos or DEMs for example.
  *
- * @author Johann Sorel (Geomatys)
+ * <p>In the particular case of raster symbolizer, {@link #getGeometry()}
+ * should return a {@link org.apache.sis.coverage.BandedCoverage} instead
+ * of a geometry.</p>
+ *
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Ian Turton (CCG)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class RasterSymbolizer extends Symbolizer implements org.opengis.style.RasterSymbolizer {
+@XmlType(name = "RasterSymbolizerType", propOrder = {
+    "opacity",
+    "channelSelection",
+    "overlapBehavior",
+    "colorMap",
+    "contrastEnhancement",
+    "shadedRelief",
+    "imageOutline"
+})
+@XmlRootElement(name = "RasterSymbolizer")
+public class RasterSymbolizer extends Symbolizer implements Translucent {
+    /**
+     * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
+     * The default value specified by OGC 05-077r4 standard is 1.
+     *
+     * @see #getOpacity()
+     * @see #setOpacity(Expression)
+     */
+    @XmlElement(name = "Opacity")
+    protected Expression<Feature, ? extends Number> opacity;
 
-    private Expression<Feature,? extends Number> opacity;
-    private ChannelSelection channelSelection;
-    private OverlapBehavior overlapBehavior;
-    private ColorMap colorMap;
-    private ContrastEnhancement contrastEnhancement;
-    private ShadedRelief shadedRelief;
-    private org.opengis.style.Symbolizer imageOutline;
+    /**
+     * Selection of false-color channels for a multi-spectral raster source, or {@code null} if none.
+     *
+     * @see #getChannelSelection()
+     * @see #setChannelSelection(ChannelSelection)
+     */
+    @XmlElement(name = "ChannelSelection")
+    protected ChannelSelection channelSelection;
 
-    public static RasterSymbolizer createDefault() {
-        return new RasterSymbolizer();
-    }
+    /**
+     * Behavior when multiple raster images in a layer overlap each other, or {@code null} if unspecified.
+     * The default value is implementation-dependent.
+     *
+     * @see #getOverlapBehavior()
+     * @see #setOverlapBehavior(OverlapBehavior)
+     */
+    @XmlElement(name = "OverlapBehavior")
+    protected OverlapBehavior overlapBehavior;
+
+    /**
+     * Mapping of fixed-numeric pixel values to colors, or {@code null} if none.
+     *
+     * @see #getColorMap()
+     * @see #setColorMap(ColorMap)
+     */
+    @XmlElement(name = "ColorMap")
+    protected ColorMap colorMap;
+
+    /**
+     * Contrast enhancement for the whole image, or {@code null} if none.
+     *
+     * @see #getContrastEnhancement()
+     * @see #setContrastEnhancement(ContrastEnhancement)
+     */
+    @XmlElement(name = "ContrastEnhancement")
+    protected ContrastEnhancement contrastEnhancement;
+
+    /**
+     * Relief shading (or “hill shading”) to apply to the image for a three-dimensional visual effect.
+     *
+     * @see #getShadedRelief()
+     * @see #setShadedRelief(ShadedRelief)
+     */
+    @XmlElement(name = "ShadedRelief")
+    protected ShadedRelief shadedRelief;
 
+    /**
+     * Line or polygon symbolizer to use for outlining source rasters, or {@code null} if none.
+     *
+     * @see #getImageOutline()
+     * @see #setImageOutline(Symbolizer)
+     */
+    @XmlElement(name = "ImageOutline")
+    protected Symbolizer imageOutline;
+
+    /**
+     * Creates an initially opaque raster symbolizer with no contrast enhancement, shaded relief or outline.
+     */
     public RasterSymbolizer() {
     }
 
-    public RasterSymbolizer(String name, Expression geometry, Description description, Unit<Length> unit,
-            Expression opacity, ChannelSelection channelSelection, OverlapBehavior overlapsBehaviour,
-            ColorMap colorMap, ContrastEnhancement contrast, ShadedRelief shaded, org.opengis.style.Symbolizer outline) {
-        super(name, geometry, description, unit);
-        this.opacity = opacity;
-        this.channelSelection = channelSelection;
-        this.overlapBehavior = overlapsBehaviour;
-        this.colorMap = colorMap;
-        this.contrastEnhancement = contrast;
-        this.shadedRelief = shaded;
-        this.imageOutline = outline;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public RasterSymbolizer(final RasterSymbolizer source) {
+        super(source);
+        opacity             = source.opacity;
+        channelSelection    = source.channelSelection;
+        overlapBehavior     = source.overlapBehavior;
+        colorMap            = source.colorMap;
+        contrastEnhancement = source.contrastEnhancement;
+        shadedRelief        = source.shadedRelief;
+        imageOutline        = source.imageOutline;
     }
 
+    /**
+     * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+     *
+     * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+     *
+     * @see Fill#getOpacity()
+     * @see Stroke#getOpacity()
+     * @see Graphic#getOpacity()
+     */
     @Override
-    public Expression<Feature,? extends Number> getOpacity() {
-        return opacity;
+    public Expression<Feature, ? extends Number> getOpacity() {
+        return defaultToOne(opacity);
     }
 
-    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
-        this.opacity = opacity;
+    /**
+     * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * If this method is never invoked, then the default value is literal 1 (totally opaque).
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new level of translucency, or {@code null} for resetting the default value.
+     */
+    @Override
+    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+        opacity = value;
     }
 
-    @Override
-    public ChannelSelection getChannelSelection() {
-        return channelSelection;
+    /**
+     * Returns the selection of false-color channels for a multi-spectral raster source.
+     * Either red, green, and blue channels are selected, or a single grayscale channel is selected.
+     * Contrast enhancement may be applied to each channel in isolation.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.</p>
+     *
+     * @return the selection of channels.
+     */
+    public Optional<ChannelSelection> getChannelSelection() {
+        return Optional.ofNullable(channelSelection);
     }
 
-    public void setChannelSelection(ChannelSelection channelSelection) {
-        this.channelSelection = channelSelection;
+    /**
+     * Sets the selection of false-color channels for a multi-spectral raster source.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new selection of channels, or {@code null} for none.
+     */
+    public void setChannelSelection(final ChannelSelection value) {
+        channelSelection = value;
     }
 
-    @Override
+    /**
+     * Returns the behavior when multiple raster images in a layer overlap each other.
+     *
+     * @return behavior when multiple raster images in a layer overlap each other.
+     */
     public OverlapBehavior getOverlapBehavior() {
-        return overlapBehavior;
+        final var value = overlapBehavior;
+        return (value != null) ? value : OverlapBehavior.LATEST_ON_TOP;
+        // Default value is unspecified, we use LATEST_ON_TOP for now.
     }
 
-    public void setOverlapBehavior(OverlapBehavior overlapBehavior) {
-        this.overlapBehavior = overlapBehavior;
+    /**
+     * Set the behavior when multiple raster images in a layer overlap each other.
+     *
+     * @param  value  new behavior, or {@code null} for resetting the default value.
+     */
+    public void setOverlapBehavior(final OverlapBehavior value) {
+        overlapBehavior = value;
     }
 
-    @Override
-    public ColorMap getColorMap() {
-        return colorMap;
+    /**
+     * Returns the mapping of fixed-numeric pixel values to colors.
+     * It can be used for defining the olors of a palette-type raster source.
+     * For example, a DEM raster giving elevations in meters above sea level
+     * can be translated to a colored image.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this fill, and conversely.</p>
+     *
+     * @return color map for the raster.
+     */
+    public Optional<ColorMap> getColorMap() {
+        return Optional.ofNullable(colorMap);
     }
 
-    public void setColorMap(ColorMap colorMap) {
-        this.colorMap = colorMap;
+    /**
+     * Sets the mapping of fixed-numeric pixel values to colors.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new color map for the raster, or {@code null} if none.
+     */
+    public void setColorMap(final ColorMap value) {
+        colorMap = value;
     }
 
-    @Override
-    public ContrastEnhancement getContrastEnhancement() {
-        return contrastEnhancement;
+    /**
+     * Returns the contrast enhancement for the whole image.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.
+     *
+     * @return contrast enhancement for the whole image.
+     *
+     * @see SelectedChannelType#getContrastEnhancement()
+     */
+    public Optional<ContrastEnhancement> getContrastEnhancement() {
+        return Optional.ofNullable(contrastEnhancement);
     }
 
-    public void setContrastEnhancement(ContrastEnhancement contrastEnhancement) {
-        this.contrastEnhancement = contrastEnhancement;
+    /**
+     * Sets the contrast enhancement applied to the whole image.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new contrast enhancement, or {@code null} if none.
+     *
+     * @see SelectedChannelType#setContrastEnhancement(ContrastEnhancement)
+     */
+    public void setContrastEnhancement(final ContrastEnhancement value) {
+        contrastEnhancement = value;
     }
 
-    @Override
-    public ShadedRelief getShadedRelief() {
-        return shadedRelief;
+    /**
+     * Returns the relief shading to apply to the image for a three-dimensional visual effect.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.
+     *
+     * @return the relief shading to apply.
+     */
+    public Optional<ShadedRelief> getShadedRelief() {
+        return Optional.ofNullable(shadedRelief);
     }
 
-    public void setShadedRelief(ShadedRelief shadedRelief) {
-        this.shadedRelief = shadedRelief;
+    /**
+     * Sets the relief shading to apply to the image for a three-dimensional visual effect.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new relief shading to apply, or {@code null} if none.
+     */
+    public void setShadedRelief(final ShadedRelief value) {
+        shadedRelief = value;
     }
 
-    @Override
-    public org.opengis.style.Symbolizer getImageOutline() {
-        return imageOutline;
+    /**
+     * How to outline individual source rasters in a multi-raster set.
+     * The value should be either a {@link LineSymbolizer} or {@link PolygonSymbolizer}.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+     *
+     * @return Line or polygon symbolizer to use for outlining source rasters.
+     */
+    public Optional<Symbolizer> getImageOutline() {
+        return Optional.ofNullable(imageOutline);
     }
 
-    public void setImageOutline(org.opengis.style.Symbolizer imageOutline) {
-        this.imageOutline = imageOutline;
+    /**
+     * Sets how to outline individual source rasters in a multi-raster set.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new line or polygon symbolizer to use, or {@code null} if none.
+     */
+    public void setImageOutline(final Symbolizer value) {
+        imageOutline = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    final Object[] properties() {
+        return new Object[] {opacity, channelSelection, overlapBehavior,
+                colorMap, contrastEnhancement, shadedRelief, imageOutline};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public int hashCode() {
-        return super.hashCode() + Objects.hash(opacity, channelSelection, overlapBehavior,
-                colorMap, contrastEnhancement, shadedRelief, imageOutline);
+    public RasterSymbolizer clone() {
+        final var clone = (RasterSymbolizer) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
-    @Override
-    public boolean equals(Object obj) {
-        if (!super.equals(obj)) {
-            return false;
-        }
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final RasterSymbolizer other = (RasterSymbolizer) obj;
-        return Objects.equals(this.opacity, other.opacity)
-            && Objects.equals(this.channelSelection, other.channelSelection)
-            && this.overlapBehavior == other.overlapBehavior
-            && Objects.equals(this.colorMap, other.colorMap)
-            && Objects.equals(this.contrastEnhancement, other.contrastEnhancement)
-            && Objects.equals(this.shadedRelief, other.shadedRelief)
-            && Objects.equals(this.imageOutline, other.imageOutline);
-    }
-
-    /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
-     */
-    public static RasterSymbolizer castOrCopy(org.opengis.style.RasterSymbolizer candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof RasterSymbolizer) {
-            return (RasterSymbolizer) candidate;
-        }
-        return new RasterSymbolizer(
-                candidate.getName(),
-                candidate.getGeometry(),
-                Description.castOrCopy(candidate.getDescription()),
-                candidate.getUnitOfMeasure(),
-                candidate.getOpacity(),
-                ChannelSelection.castOrCopy(candidate.getChannelSelection()),
-                candidate.getOverlapBehavior(),
-                ColorMap.castOrCopy(candidate.getColorMap()),
-                ContrastEnhancement.castOrCopy(candidate.getContrastEnhancement()),
-                ShadedRelief.castOrCopy(candidate.getShadedRelief()),
-                candidate.getImageOutline()
-            );
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (channelSelection    != null) channelSelection    = channelSelection.clone();
+        if (colorMap            != null) colorMap            = colorMap.clone();
+        if (contrastEnhancement != null) contrastEnhancement = contrastEnhancement.clone();
+        if (shadedRelief        != null) shadedRelief        = shadedRelief.clone();
+        if (imageOutline        != null) imageOutline        = imageOutline.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
index f0597b0928..e0dffbfd4d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
@@ -16,183 +16,474 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
-import org.opengis.filter.Filter;
+import java.util.ArrayList;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlElementRef;
+import jakarta.xml.bind.annotation.XmlRootElement;
 import org.opengis.metadata.citation.OnlineResource;
-import org.opengis.style.StyleVisitor;
-import org.opengis.style.Symbolizer;
+import org.apache.sis.internal.jaxb.Context;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
+import org.opengis.filter.Filter;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Rule}.
+ * Rendering instructions grouped by feature-property conditions and map scales.
+ * A rule consists of two important parts: a {@linkplain Filter filter} and a list of symbols.
+ * When drawing a given feature, the rendering engine examines each rule in the style,
+ * first checking its filter. If the feature is accepted by the filter,
+ * then all {@link Symbolizer} for that rule are applied to the given feature.
+ *
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ *
+ * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) to filter.
  *
- * @author Johann Sorel (Geomatys)
+ * @since 1.5
  */
-public final class Rule implements org.opengis.style.Rule {
-
-    private String name;
-    private Description description;
-    private GraphicLegend legend;
-    private Filter filter;
-    private boolean elseFilter;
-    private double minScale;
-    private double maxScale = Double.MAX_VALUE;
-    private final List<Symbolizer> symbolizers = new ArrayList<>();
-    private OnlineResource onlineResource;
+@XmlType(name = "RuleType", propOrder = {
+    "name",
+    "description",
+    "legend",
+//  "filter",           // XML encoding not yet available.
+    "elseFilter",
+    "minScale",
+    "maxScale",
+    "symbolizers"
+})
+@XmlRootElement(name = "Rule")
+public class Rule<R> extends StyleElement {
+    /**
+     * Name for this rule, or {@code null} if none.
+     *
+     * @see #getName()
+     * @see #setName(String)
+     */
+    @XmlElement(name = "Name")
+    protected String name;
+
+    /**
+     * Information for user interfaces, or {@code null} if none.
+     *
+     * @see #getDescription()
+     * @see #setDescription(Description)
+     */
+    @XmlElement(name = "Description")
+    protected Description description;
+
+    /**
+     * Small graphic to draw in a legend window, or {@code null} if none.
+     *
+     * @see #getLegend()
+     * @see #setLegend(GraphicLegend)
+     */
+    @XmlElement(name = "LegendGraphic")
+    protected GraphicLegend legend;
+
+    /**
+     * Filter that will limit the features, or {@code null} if none.
+     *
+     * @see #getFilter()
+     * @see #setFilter(Filter)
+     */
+//  @XmlElement(name = "Filter", namespace = "http://www.opengis.net/ogc")
+    protected Filter<R> filter;
+
+    /**
+     * Whether this {@code Rule} will be applied only if no other rules in the containing style apply.
+     *
+     * @see #isElseFilter()
+     * @see #setElseFilter(boolean)
+     */
+    protected boolean isElseFilter;
+
+    /**
+     * Minimum value (inclusive) in the denominator of map scale at which this rule will apply.
+     *
+     * @see #getMinScaleDenominator()
+     * @see #setMaxScaleDenominator(double)
+     */
+    protected double minScale;
+
+    /**
+     * Maximum value (exclusive) in the denominator of map scale at which this rule will apply.
+     *
+     * @see #getMaxScaleDenominator()
+     * @see #setMaxScaleDenominator(double)
+     */
+    protected double maxScale;
+
+    /**
+     * Description of how a feature is to appear on a map.
+     *
+     * @see #symbolizers()
+     */
+    @XmlElementRef(name = "Symbolizer")
+    private List<Symbolizer> symbolizers;
 
+    /**
+     * If the style comes from an external XML file, the original source. Otherwise {@code null}.
+     *
+     * @see #getOnlineResource()
+     * @see #setOnlineSource(OnlineResource)
+     */
+    protected OnlineResource onlineSource;
+
+    /**
+     * Creates a new rule.
+     */
     public Rule() {
+        maxScale = Double.POSITIVE_INFINITY;
+        symbolizers = new ArrayList<>();
     }
 
-    public Rule(String name, Description description, GraphicLegend legend, Filter filter, boolean elseFilter, double minScale, double maxScale, List<Symbolizer> symbolizers, OnlineResource onlineResource) {
-        this.name = name;
-        this.description = description;
-        this.legend = legend;
-        this.filter = filter;
-        this.elseFilter = elseFilter;
-        this.minScale = minScale;
-        this.maxScale = maxScale;
-        if (symbolizers != null) this.symbolizers.addAll(symbolizers);
-        this.onlineResource = onlineResource;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Rule(final Rule<R> source) {
+        super(source);
+        name         = source.name;
+        description  = source.description;
+        legend       = source.legend;
+        filter       = source.filter;
+        minScale     = source.minScale;
+        maxScale     = source.maxScale;
+        onlineSource = source.onlineSource;
+        symbolizers  = new ArrayList<>(source.symbolizers);
     }
 
-    @Override
-    public String getName() {
-        return name;
+    /**
+     * Returns the name for this rule.
+     * This can be any string that uniquely identifies this rule within a given canvas.
+     * It is not meant to be human-friendly. For a human-friendly label,
+     * see the {@linkplain Description#getTitle() title} instead.
+     *
+     * @return a name for this rule.
+     */
+    public Optional<String> getName() {
+        return Optional.ofNullable(name);
     }
 
-    public void setName(String name) {
-        this.name = name;
+    /**
+     * Sets a name for this rule.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new name for this rule, or {@code null} if none.
+     */
+    public void setName(final String value) {
+        name = value;
     }
 
-    @Override
-    public Description getDescription() {
-        return description;
+    /**
+     * Returns the description of this rule.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this rule, and conversely.
+     *
+     * @return information for user interfaces.
+     */
+    public Optional<Description> getDescription() {
+        return Optional.ofNullable(description);
     }
 
-    public void setDescription(Description description) {
-        this.description = description;
+    /**
+     * Sets a description of this rule.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new information for user interfaces, or {@code null} if none.
+     */
+    public void setDescription(final Description value) {
+        description = value;
     }
 
-    @Override
-    public GraphicLegend getLegend() {
-        return legend;
+    /**
+     * Returns a small graphic that could be used by the rendering engine to draw a legend window.
+     * User interfaces may present the user with a legend that indicates how features of a given type are being portrayed.
+     * Through its {@code LegendGraphic} property, a {@code Rule} can provide a custom picture to be used in such a legend window.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this rule, and conversely.</p>
+     *
+     * @return small graphic to draw in a legend window.
+     */
+    public Optional<GraphicLegend> getLegend() {
+        return Optional.ofNullable(legend);
     }
 
-    public void setLegend(GraphicLegend legend) {
-        this.legend = legend;
+    /**
+     * Sets a small graphic that could be used by the rendering engine to draw a legend window.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new legend graphic, or {@code null} if none.
+     */
+    public void setLegend(final GraphicLegend value) {
+        legend = value;
     }
 
-    @Override
-    public Filter getFilter() {
-        return filter;
+    /**
+     * Returns the filter that will limit the features for which this rule will apply.
+     * This value should be used only if {@link #isElseFilter()} returns {@code false},
+     * in which case this rule applies to all features.
+     *
+     * @return the filter that will limit the features.
+     */
+    public Filter<R> getFilter() {
+        if (isElseFilter) {
+            return Filter.exclude();
+        }
+        final var value = filter;
+        return (value != null) ? value : Filter.include();
     }
 
-    public void setFilter(Filter filter) {
-        this.filter = filter;
+    /**
+     * Sets the filter that will limit the features for which this rule will apply.
+     * If this method is never invoked, then the default value is {@link Filter#include()}.
+     * Invoking this method forces {@link #isElseFilter()} to {@code false}.
+     *
+     * @param  value  new filter that will limit the features, or {@code null} if none.
+     */
+    public void setFilter(final Filter<R> value) {
+        isElseFilter = false;
+        filter = value;
     }
 
-    @Override
+    /**
+     * Returns true if this {@code Rule} will be applied only if no other rules in the containing style apply.
+     * If this is true, then the {@linkplain #getFilter() filter} should be ignored.
+     *
+     * <p>The "Else Filter" is implicitly a filter with a condition that depends on the enclosing style.
+     * Consequently, it can not be expressed as a standalone {@code Filter} expression in this rule.</p>
+     *
+     * @return true if the filter is an else filter.
+     */
     public boolean isElseFilter() {
-        return elseFilter;
+        return isElseFilter;
     }
 
-    public void setElseFilter(boolean elseFilter) {
-        this.elseFilter = elseFilter;
+    /**
+     * Sets or unset this filter to an else filter.
+     *
+     * @param  value  whether the filter is the "else" filter.
+     */
+    public void setElseFilter(final boolean value) {
+        isElseFilter = value;
     }
 
-    @Override
+    /**
+     * Returns the minimum value (inclusive) in the denominator of map scale at which this rule will apply.
+     * If, for example, this value was 10000, then this rule would only apply at scales of 1:<var>X</var>
+     * where <var>X</var> is greater than 10000. A value of zero indicates that there is no minimum.
+     *
+     * <h4>Relationship with real world lengths</h4>
+     * The values used are scale denominators relative to a “standardized rendering pixel size”.
+     * That size is defined as a square with sides of 0.28 millimeters. If the real pixel size
+     * is different or if the CRS uses angular units instead than linear, then the renderer shall
+     * take those information in account as described in OGC 05-077r4 §10.2.
+     *
+     * @return minimum scale value, inclusive.
+     */
     public double getMinScaleDenominator() {
         return minScale;
     }
 
-    public void setMinScaleDenominator(double minScale) {
-        this.minScale = minScale;
+    /**
+     * Invoked by JAXB for marshalling the minimum scale denominator.
+     * If both the minimum and maximum are the default values, then this property is omitted.
+     * If a maximum value exists, then the zero minimum is explicitly written for clarity.
+     */
+    @XmlElement(name = "MinScaleDenominator")
+    private Double getMinScale() {
+        final var value = minScale;
+        return (value > 0 || maxScale != Double.POSITIVE_INFINITY) ? value : null;
     }
 
-    @Override
+    /**
+     * Sets the minimum value (inclusive) in the denominator of map scale at which this rule will apply.
+     * If the given value is greater than the maximum scale, then that maximum is discarded.
+     * If this method is never invoked, then the default value is 0.
+     *
+     * @param  value  new minimum scale value (inclusive).
+     */
+    public void setMinScaleDenominator(final double value) {
+        ArgumentChecks.ensurePositive("MinScaleDenominator", value);
+        minScale = value;
+        if (value > maxScale) {
+            maxScale = Double.POSITIVE_INFINITY;
+        }
+    }
+
+    /**
+     * Invoked by JAXB for unmarshalling the minimum scale denominator.
+     * The argument validity check assumes that this method is invoked only once.
+     * If this assumption is violated, the check allows range restriction but not expansion.
+     * If the given value is invalid, a warning is reported but the unmarshalling continue.
+     */
+    private void setMinScale(final Double value) {
+        if (isValidScale("MinScaleDenominator", value)) {
+            minScale = value;
+        }
+    }
+
+    /**
+     * Returns the maximum value (exclusive) in the denominator of map scale at which this rule will apply.
+     * If, for example, this value was 10000, then this rule would only apply at scales of 1:<var>X</var>
+     * where <var>X</var> is less than 10000.
+     * An {@linkplain Double#POSITIVE_INFINITY infinite} value indicates that there is no maximum.
+     *
+     * <h4>Relationship with real world lengths</h4>
+     * The same discussion than {@link #getMinScaleDenominator()} applies also to the maximum scale value.
+     *
+     * @return maximum scale value, exclusive.
+     */
     public double getMaxScaleDenominator() {
         return maxScale;
     }
 
-    public void setMaxScaleDenominator(double maxScale) {
-        this.maxScale = maxScale;
+    /**
+     * Invoked by JAXB for marshalling the maximum scale denominator.
+     * If the value is positive infinity, then the property is omitted.
+     */
+    @XmlElement(name = "MaxScaleDenominator")
+    private Double getMaxScale() {
+        final var value = maxScale;
+        return (value != Double.POSITIVE_INFINITY) ? value : null;
     }
 
-    @Override
-    public List<Symbolizer> symbolizers() {
-        return symbolizers;
+    /**
+     * Sets the maximum value (exclusive) in the denominator of map scale at which this rule will apply.
+     * If the given value is less than the minimum scale, then that minimum is discarded.
+     * If this method is never invoked, then the default value is {@link Double#POSITIVE_INFINITY}.
+     *
+     * @param  value  new maximum scale value (exclusive).
+     */
+    public void setMaxScaleDenominator(final double value) {
+        ArgumentChecks.ensureStrictlyPositive("MaxScaleDenominator", value);
+        maxScale = value;
+        if (value < minScale) {
+            minScale = 0;
+        }
     }
 
-    @Override
-    public OnlineResource getOnlineResource() {
-        return onlineResource;
+    /**
+     * Invoked by JAXB for unmarshalling the maximum scale denominator.
+     * The argument validity check assumes that this method is invoked only once.
+     * If this assumption is violated, the check allows range restriction but not expansion.
+     * If the given value is invalid, a warning is reported but the unmarshalling continue.
+     */
+    private void setMaxScale(final Double value) {
+        if (isValidScale("MaxScaleDenominator", value)) {
+            maxScale = value;
+        }
     }
 
-    public void setOnlineResource(OnlineResource OnlineResource) {
-        this.onlineResource = OnlineResource;
+    /**
+     * Indicates whether an unmarshalled minimum or maximum scale denominator is inside the expected range of values.
+     * If the given value is invalid, a warning is emitted to the JAXB unmarshaller and the caller will keep the default value.
+     */
+    private boolean isValidScale(final String name, final Double value) {
+        boolean isValidScale = (value != null);
+        if (isValidScale) {
+            isValidScale = (value >= minScale && value <= maxScale);
+            if (!isValidScale) {
+                Context.warningOccured(Context.current(), Rule.class, "set".concat(name), Errors.class,
+                                       Errors.Keys.ValueOutOfRange_4, name, minScale, maxScale, value);
+            }
+        }
+        return isValidScale;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Returns the description of how a feature is to appear on a map.
+     * Each symbolizer describes how the shape should appear,
+     * together with graphical properties such as color and opacity.
+     * The predefined type of symbolizers are
+     * {@linkplain LineSymbolizer line},
+     * {@linkplain PolygonSymbolizer polygon},
+     * {@linkplain PointSymbolizer point},
+     * {@linkplain TextSymbolizer text}, and
+     * {@linkplain RasterSymbolizer raster} symbolizers.
+     *
+     * <p>The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.</p>
+     *
+     * @return the list of symbolizers, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<Symbolizer> symbolizers() {
+        return symbolizers;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(name, description, legend, filter, elseFilter, minScale, maxScale, symbolizers, onlineResource);
+    /**
+     * If the style comes from an external XML file, the original source.
+     * This property may be non-null if a XML document specified this rule
+     * by a link to another XML document.
+     *
+     * @return the original source of this rule.
+     */
+    public Optional<OnlineResource> getOnlineSource() {
+        return Optional.ofNullable(onlineSource);
     }
 
+    /**
+     * If the style comes from an external XML file, the original source.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * <h4>Effect on XML marshalling</h4>
+     * Setting this property to a non-null value has the following effect:
+     * When this rule is written in a XML document, then instead of writing
+     * the XML elements describing this rule,
+     * the specified link will be written instead.
+     *
+     * @todo Above-describing marshalling is not yet implemented.
+     *
+     * @param  value  new source of this rule, or {@code null} if none.
+     */
+    public void setOnlineSource(final OnlineResource value) {
+        onlineSource = value;
+    }
+
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Rule other = (Rule) obj;
-        return this.elseFilter == other.elseFilter
-            && this.minScale == other.minScale
-            && this.maxScale == other.maxScale
-            && Objects.equals(this.name, other.name)
-            && Objects.equals(this.description, other.description)
-            && Objects.equals(this.legend, other.legend)
-            && Objects.equals(this.filter, other.filter)
-            && Objects.equals(this.symbolizers, other.symbolizers)
-            && Objects.equals(this.onlineResource, other.onlineResource);
+    final Object[] properties() {
+        return new Object[] {name, description, legend, filter, minScale, maxScale, symbolizers, onlineSource};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static Rule castOrCopy(org.opengis.style.Rule candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Rule) {
-            return (Rule) candidate;
-        }
-
-        final List<Symbolizer> symbols = new ArrayList<>();
-        for (org.opengis.style.Symbolizer s : candidate.symbolizers()) {
-            symbols.add(org.apache.sis.internal.style.Symbolizer.tryCastOrCopy(s));
-        }
+    @Override
+    public Rule<R> clone() {
+        @SuppressWarnings("unchecked")
+        final var clone = (Rule<R>) super.clone();
+        clone.selfClone();
+        return clone;
+    }
 
-        return new Rule(
-                candidate.getName(),
-                Description.castOrCopy(candidate.getDescription()),
-                GraphicLegend.castOrCopy(candidate.getLegend()),
-                candidate.getFilter(),
-                candidate.isElseFilter(),
-                candidate.getMinScaleDenominator(),
-                candidate.getMaxScaleDenominator(),
-                symbols,
-                candidate.getOnlineResource());
+    /**
+     * Clones the mutable style fields of this element.
+     */
+    private void selfClone() {
+        if (description != null) description = description.clone();
+        if (legend      != null) legend      = legend.clone();
+        symbolizers = new ArrayList<>(symbolizers);
+        symbolizers.replaceAll(Symbolizer::clone);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
index 40cbc9b62c..e5a8cd9e34 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
@@ -16,85 +16,157 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
-import org.opengis.style.StyleVisitor;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.SelectedChannelType}.
+ * Information about a channel to use in a multi-spectral source.
+ * Channels are identified by data-dependent character identifiers.
+ * Commonly, channels will be labelled as "1", "2", <i>etc</i>.
+ * A set of selected channels is contained in {@link ChannelSelection}.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Ian Turton (CCG)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class SelectedChannelType implements org.opengis.style.SelectedChannelType {
+@XmlType(name = "SelectedChannelType", propOrder = {
+    "sourceChannelName",
+    "contrastEnhancement"
+})
+// No root element is specified in OGC 05-077r4.
+public class SelectedChannelType extends StyleElement {
+    /**
+     * The channel's name, or {@code null} if unspecified.
+     *
+     * @see #getSourceChannelName()
+     * @see #setSourceChannelName(Expression)
+     *
+     * @todo Needs an adapter from expression to plain string.
+     */
+    @XmlElement(name = "SourceChannelName", required = true)
+    protected Expression<Feature,String> sourceChannelName;
 
-    private String channelName;
-    private ContrastEnhancement contrastEnhancement;
+    /**
+     * Contrast enhancement applied to the selected channel in isolation, or {@code null} if none.
+     *
+     * @see #getContrastEnhancement()
+     * @see #setContrastEnhancement(ContrastEnhancement)
+     */
+    @XmlElement(name = "ContrastEnhancement")
+    protected ContrastEnhancement contrastEnhancement;
 
+    /**
+     * Creates an initially empty selected channel.
+     */
     public SelectedChannelType() {
     }
 
-    public SelectedChannelType(String channelName, ContrastEnhancement contrastEnhancement) {
-        this.channelName = channelName;
-        this.contrastEnhancement = contrastEnhancement;
+    /**
+     * Creates a selected channel for the specified name.
+     *
+     * @param  name  source channel name.
+     */
+    public SelectedChannelType(final String name) {
+        ArgumentChecks.ensureNonEmpty("name", name);
+        sourceChannelName = literal(name);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public SelectedChannelType(final SelectedChannelType source) {
+        super(source);
+        sourceChannelName   = source.sourceChannelName;
+        contrastEnhancement = source.contrastEnhancement;
     }
 
-    @Override
-    public String getChannelName() {
-        return channelName;
+    /**
+     * Returns the channel's name.
+     *
+     * @return the channel's name, or {@code null} if unspecified.
+     *
+     * @todo Shall never be {@code null}. We need to think about some default value.
+     */
+    public Expression<Feature,String> getSourceChannelName() {
+        return sourceChannelName;
     }
 
-    public void setChannelName(String channelName) {
-        this.channelName = channelName;
+    /**
+     * Sets the channel's name.
+     *
+     * @param  value  the channel's name, or {@code null} if unspecified.
+     */
+    public void setSourceChannelName(final Expression<Feature,String> value) {
+        sourceChannelName = value;
     }
 
-    @Override
-    public ContrastEnhancement getContrastEnhancement() {
-        return contrastEnhancement;
+    /**
+     * Returns the contrast enhancement applied to the selected channel in isolation.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.
+     *
+     * @return contrast enhancement for the selected channel.
+     *
+     * @see RasterSymbolizer#getContrastEnhancement()
+     */
+    public Optional<ContrastEnhancement> getContrastEnhancement() {
+        return Optional.ofNullable(contrastEnhancement);
     }
 
-    public void setContrastEnhancement(ContrastEnhancement contrastEnhancement) {
-        this.contrastEnhancement = contrastEnhancement;
+    /**
+     * Sets the contrast enhancement applied to the selected channel in isolation.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new contrast enhancement, or {@code null} if none.
+     *
+     * @see RasterSymbolizer#setContrastEnhancement(ContrastEnhancement)
+     */
+    public void setContrastEnhancement(final ContrastEnhancement value) {
+        contrastEnhancement = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(channelName, contrastEnhancement);
+    final Object[] properties() {
+        return new Object[] {sourceChannelName, contrastEnhancement};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final SelectedChannelType other = (SelectedChannelType) obj;
-        return Objects.equals(this.channelName, other.channelName)
-            && Objects.equals(this.contrastEnhancement, other.contrastEnhancement);
+    public SelectedChannelType clone() {
+        final var clone = (SelectedChannelType) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static SelectedChannelType castOrCopy(org.opengis.style.SelectedChannelType candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof SelectedChannelType) {
-            return (SelectedChannelType) candidate;
+    private void selfClone() {
+        if (contrastEnhancement != null) {
+            contrastEnhancement = contrastEnhancement.clone();
         }
-        return new SelectedChannelType(
-                candidate.getChannelName(),
-                ContrastEnhancement.castOrCopy(candidate.getContrastEnhancement()));
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
index 0178749bb3..4f5a300a9d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
@@ -16,85 +16,135 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.Objects;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.ShadedRelief}.
+ * Relief shading (or “hill shading”) applied to an image for a three-dimensional visual effect.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Ian Turton (CCG)
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class ShadedRelief implements org.opengis.style.ShadedRelief {
+@XmlType(name = "ShadedReliefType", propOrder = {
+    "brightnessOnly",
+    "reliefFactor"
+})
+@XmlRootElement(name = "ShadedRelief")
+public class ShadedRelief extends StyleElement {
+    /**
+     * Default value for {@link #getReliefFactor()}.
+     * No standard value is specified by OGC 05-077r4.
+     */
+    private static final Expression<Feature,Double> DEFAULT_VALUE = literal(55.0);
+
+    /**
+     * Whether to apply the shading to the image generated so far by other layers.
+     *
+     * @see #isBrightnessOnly()
+     * @see #setBrightnessOnly(Expression)
+     *
+     * @todo Needs an adapter from expression to plain boolean.
+     */
+    @XmlElement(name = "BrightnessOnly")
+    protected Expression<Feature,Boolean> brightnessOnly;
 
-    private boolean brightnessOnly;
-    private Expression<Feature,? extends Number> reliefFactor;
+    /**
+     * Amount of exaggeration to use for the height of the hills, or {@code null} for the default value.
+     *
+     * @see #getReliefFactor()
+     * @see #setReliefFactor(Expression)
+     */
+    @XmlElement(name = "ReliefFactor")
+    protected Expression<Feature, ? extends Number> reliefFactor;
 
+    /**
+     * Creates a shaded relief initialized to implementation-specific default values.
+     */
     public ShadedRelief() {
     }
 
-    public ShadedRelief(boolean brightnessOnly, Expression<Feature, ? extends Number> reliefFactor) {
-        this.brightnessOnly = brightnessOnly;
-        this.reliefFactor = reliefFactor;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public ShadedRelief(final ShadedRelief source) {
+        super(source);
+        brightnessOnly = source.brightnessOnly;
+        reliefFactor   = source.reliefFactor;
     }
 
-    @Override
-    public boolean isBrightnessOnly() {
-        return brightnessOnly;
+    /**
+     * Returns whether to apply the shading to the image generated so far by other layers.
+     * If {@code false}, then the shading is applied only on the layer being rendered by
+     * the current {@link RasterSymbolizer}.
+     *
+     * @return whether to apply the shading to the image generated so far by other layers.
+     */
+    public Expression<Feature,Boolean> isBrightnessOnly() {
+        return defaultToFalse(brightnessOnly);
     }
 
-    public void setBrightnessOnly(boolean brightnessOnly) {
-        this.brightnessOnly = brightnessOnly;
+    /**
+     * Sets whether to apply the shading to the image generated so far by other layers.
+     * If this method is never invoked, then the default value is literal false.
+     *
+     * @param  value  new policy, or {@code null} for resetting the default value.
+     */
+    public void setBrightnessOnly(final Expression<Feature,Boolean> value) {
+        brightnessOnly = value;
     }
 
-    @Override
+    /**
+     * Returns the amount of exaggeration to use for the height of the hills.
+     * A value of around 55 gives reasonable results for Earth-based DEMs.
+     *
+     * @return amount of exaggeration to use for the height of the hills.
+     */
     public Expression<Feature, ? extends Number> getReliefFactor() {
-        return reliefFactor;
-    }
-
-    public void setReliefFactor(Expression<Feature, ? extends Number> reliefFactor) {
-        this.reliefFactor = reliefFactor;
+        final var value = reliefFactor;
+        return (value != null) ? value : DEFAULT_VALUE;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(brightnessOnly, reliefFactor);
+    /**
+     * Sets the amount of exaggeration to use for the height of the hills.
+     * If this method is never invoked, then the default value is implementation-specific.
+     *
+     * @param  value  new amount of exaggeration, or {@code null} for resetting the default value.
+     */
+    public void setReliefFactor(final Expression<Feature, ? extends Number> value) {
+        reliefFactor = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final ShadedRelief other = (ShadedRelief) obj;
-        return this.brightnessOnly == other.brightnessOnly
-            && Objects.equals(this.reliefFactor, other.reliefFactor);
+    final Object[] properties() {
+        return new Object[] {brightnessOnly, reliefFactor};
     }
 
     /**
-     * Cast or copy to an SIS implementation.
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
      *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * @return deep clone of all style elements.
      */
-    public static ShadedRelief castOrCopy(org.opengis.style.ShadedRelief candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof ShadedRelief) {
-            return (ShadedRelief) candidate;
-        }
-        return new ShadedRelief(candidate.isBrightnessOnly(), candidate.getReliefFactor());
+    @Override
+    public ShadedRelief clone() {
+        final var clone = (ShadedRelief) super.clone();
+        return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
index 50011836b1..9a148d5922 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
@@ -17,207 +17,492 @@
 package org.apache.sis.internal.style;
 
 import java.awt.Color;
-import java.util.Arrays;
-import java.util.Objects;
+import java.util.Optional;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
 import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.StyleVisitor;
+import org.opengis.filter.Literal;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Stroke}.
+ * Instructions about how to draw styled lines.
+ * Stroke objects are contained by {@link LineSymbolizer} and {@link PolygonSymbolizer}.
+ * There are three basic types of strokes: solid-color, {@link GraphicFill} (stipple),
+ * and repeated linear {@link GraphicStroke}.
+ * A repeated linear graphic is plotted linearly and has its graphic symbol bent around the curves
+ * of the line string, and a graphic fill has the pixels of the line rendered with a repeating area-fill pattern.
+ * If neither a {@linkplain #getGraphicFill() graphic fill} nor {@linkplain #getGraphicStroke() graphic stroke}
+ * element is given, then the line symbolizer will render a solid color.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Stroke implements org.opengis.style.Stroke {
-
-    private GraphicFill graphicFill;
-    private GraphicStroke graphicStroke;
-    private Expression<Feature,Color> color;
-    private Expression<Feature,? extends Number> opacity;
-    private Expression<Feature,? extends Number> width;
-    private Expression<Feature,String> lineJoin;
-    private Expression<Feature,String> lineCap;
-    private float[] dashArray;
-    private Expression<Feature,? extends Number> dashOffset;
+@XmlType(name = "StrokeType", propOrder = {
+    "graphicFill",
+    "graphicStroke",
+//  "svgParameter"
+})
+@XmlRootElement(name = "Stroke")
+public class Stroke extends StyleElement implements Translucent {
+    /**
+     * Literal for a predefined join which can be used in strokes.
+     *
+     * @see #getLineJoin()
+     */
+    public static final Literal<Feature,String> JOIN_MITRE, JOIN_ROUND, JOIN_BEVEL;
+
+    /**
+     * Literal for a predefined cap which can be used in strokes.
+     *
+     * @see #getLineCap()
+     */
+    public static final Literal<Feature,String> CAP_BUTT, CAP_ROUND, CAP_SQUARE;
+
+    /**
+     * Literal for the default dash offset.
+     */
+    private static final Literal<Feature,Integer> ZERO;
+
+    static {
+        final var FF = FF();
+        ZERO       = FF.literal(0);
+        JOIN_MITRE = FF.literal("mitre");
+        JOIN_ROUND = FF.literal("round");
+        JOIN_BEVEL = FF.literal("bevel");
+        CAP_BUTT   = FF.literal("butt");
+        CAP_ROUND  = FF.literal("round");
+        CAP_SQUARE = FF.literal("square");
+    }
+
+    /**
+     * Graphic for tiling the (thin) area of the line, or {@code null} if none.
+     * This property and {@link #graphicStroke} are mutually exclusive.
+     *
+     * @see #getGraphicFill()
+     * @see #setGraphicFill(GraphicFill)
+     */
+    @XmlElement(name = "GraphicFill")
+    protected GraphicFill graphicFill;
+
+    /**
+     * Graphic to repeat along the path of the lines, or {@code null} if none.
+     * This property and {@link #graphicFill} are mutually exclusive.
+     *
+     * @see #getGraphicStroke()
+     * @see #setGraphicStroke(GraphicStroke)
+     */
+    @XmlElement(name = "GraphicStroke")
+    protected GraphicStroke graphicStroke;
+
+    /**
+     * Color of the line if it is to be solid-color filled, or {@code null} for the default value.
+     * The default value specified by OGC 05-077r4 standard is black.
+     *
+     * <p>This property is used when both {@link #graphicFill} and {@link #graphicStroke} are null.
+     * In XML documents, this is encoded inside a {@code <SvgParameter name="stroke">} element.</p>
+     *
+     * @see #getColor()
+     * @see #setColor(Expression)
+     */
+    protected Expression<Feature,Color> color;
+
+    /**
+     * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
+     * The default value specified by OGC 05-077r4 standard is 1.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-opacity">} element.</p>
+     *
+     * @see #getOpacity()
+     * @see #setOpacity(Expression)
+     */
+    protected Expression<Feature, ? extends Number> opacity;
+
+    /**
+     * Absolute width of the line stroke as a positive floating point number, or {@code null} for the default value.
+     * The default value specified by OGC 05-077r4 standard is 1.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-width">} element.</p>
+     *
+     * @see #getWidth()
+     * @see #setWidth(Expression)
+     */
+    protected Expression<Feature, ? extends Number> width;
+
+    /**
+     * How the various segments of a (thick) line string should be joined, or {@code null} the default value.
+     * The default value is implementation-specific.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-linejoin">} element.</p>
+     *
+     * @see #getLineJoin()
+     * @see #setLineJoin(Expression)
+     */
+    protected Expression<Feature,String> lineJoin;
 
+    /**
+     * How the beginning and ending segments of a line string will be terminated, or {@code null} the default value.
+     * The default value is implementation-specific.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-linecap">} element.</p>
+     *
+     * @see #getLineCap()
+     * @see #setLineCap(Expression)
+     */
+    protected Expression<Feature,String> lineCap;
+
+    /**
+     * Dash pattern as a space-separated sequence of numbers, or {@code null} for a solid line.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-dasharray">} element.</p>
+     *
+     * @see #getDashArray()
+     * @see #setDashArray(Expression)
+     */
+    protected Expression<Feature,float[]> dashArray;
+
+    /**
+     * Distance offset into the dash array to begin drawing, or {@code null} for the default value.
+     * The default value is zero.
+     *
+     * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-dashoffset">} element.</p>
+     *
+     * @see #getDashOffset()
+     * @see #setDashOffset(Expression)
+     */
+    protected Expression<Feature,Integer> dashOffset;
+
+    /**
+     * Creates a stroke initialized to solid line of black opaque color, 1 pixel width.
+     */
     public Stroke() {
-        this(null,null,
-             StyleFactory.DEFAULT_STROKE_COLOR,
-             StyleFactory.DEFAULT_STROKE_OPACITY,
-             StyleFactory.DEFAULT_STROKE_WIDTH,
-             StyleFactory.DEFAULT_STROKE_JOIN,
-             StyleFactory.DEFAULT_STROKE_CAP,
-             null,
-             StyleFactory.DEFAULT_STROKE_OFFSET);
-    }
-
-    public Stroke(GraphicFill graphicFill, GraphicStroke graphicStroke,
-            Expression<Feature, Color> color,
-            Expression<Feature, ? extends Number> opacity,
-            Expression<Feature, ? extends Number> width,
-            Expression<Feature, String> lineJoin,
-            Expression<Feature, String> lineCap,
-            float[] dashArray,
-            Expression<Feature, ? extends Number> dashOffset) {
+    }
+
+    /**
+     * Creates a stroke initialized to the given color.
+     * The opacity is derived from the alpha value of the given color.
+     *
+     * @param  color  the initial color.
+     */
+    public Stroke(Color color) {
         ArgumentChecks.ensureNonNull("color", color);
-        ArgumentChecks.ensureNonNull("opacity", opacity);
-        ArgumentChecks.ensureNonNull("width", width);
-        ArgumentChecks.ensureNonNull("lineJoin", lineJoin);
-        ArgumentChecks.ensureNonNull("lineCap", lineCap);
-        ArgumentChecks.ensureNonNull("dashOffset", dashOffset);
-        this.graphicFill = graphicFill;
-        this.graphicStroke = graphicStroke;
-        this.color = color;
-        this.opacity = opacity;
-        this.width = width;
-        this.lineJoin = lineJoin;
-        this.lineCap = lineCap;
-        this.dashArray = dashArray;
-        this.dashOffset = dashOffset;
+        if ((opacity = Fill.opacity(color)) != null) {
+            color = new Color(color.getRGB() | 0xFF000000);
+        }
+        this.color = literal(color);
     }
 
-    @Override
-    public GraphicFill getGraphicFill() {
-        return graphicFill;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Stroke(final Stroke source) {
+        super(source);
+        graphicFill   = source.graphicFill;
+        graphicStroke = source.graphicStroke;
+        color         = source.color;
+        opacity       = source.opacity;
+        width         = source.width;
+        lineJoin      = source.lineJoin;
+        lineCap       = source.lineCap;
+        dashArray     = source.dashArray;
+        dashOffset    = source.dashOffset;
     }
 
-    public void setGraphicFill(GraphicFill graphicFill) {
-        this.graphicFill = graphicFill;
+    /**
+     * Indicates that line should be drawn by tiling the (thin) area of the line with the given graphic.
+     * Between {@code getGraphicFill()} and {@link #getGraphicStroke()}, only one may return a non-null value
+     * because a {@code Stroke} can have a {@code GraphicFill} or a {@code GraphicStroke}, but not both.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+     *
+     * @return graphic for tiling the (thin) area of the line.
+     *
+     * @see Fill#getGraphicFill()
+     */
+    public Optional<GraphicFill> getGraphicFill() {
+        return Optional.ofNullable(graphicFill);
     }
 
-    @Override
-    public GraphicStroke getGraphicStroke() {
-        return graphicStroke;
+    /**
+     * Specifies that line should be drawn by tiling the (thin) area of the line with the given graphic.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * <p>Setting a non-null value causes {@link #getGraphicStroke()} to
+     * return {@code null} because those two properties are mutually exclusive.</p>
+     *
+     * @param  value  new graphic for tiling the (thin) area of the line, or {@code null} if none.
+     *
+     * @see Fill#setGraphicFill(GraphicFill)
+     */
+    public void setGraphicFill(final GraphicFill value) {
+        graphicFill = value;
+        if (value != null) {
+            graphicStroke = null;
+        }
     }
 
-    public void setGraphicStroke(GraphicStroke graphicStroke) {
-        this.graphicStroke = graphicStroke;
+    /**
+     * Indicates that lines should be drawn by repeatedly plotting the given graphic.
+     * The graphic is repeated along the path of the lines, rotating it according to the orientation of the line.
+     * Between {@link #getGraphicFill()} and {@code getGraphicStroke()}, only one may return a non-null value
+     * because a {@code Stroke} can have a {@link GraphicFill} or a {@link GraphicStroke}, but not both.
+     *
+     * <p>The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+     *
+     * @return graphic to repeat along the path of the lines.
+     */
+    public Optional<GraphicStroke> getGraphicStroke() {
+        return Optional.ofNullable(graphicStroke);
     }
 
-    @Override
+    /**
+     * Specifies that lines should be drawn by repeatedly plotting the given graphic.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * <p>Setting a non-null value causes {@link #getGraphicFill()} to
+     * return {@code null} because those two properties are mutually exclusive.</p>
+     *
+     * @param  value  new graphic to repeat along the path of the lines, or {@code null} if none.
+     */
+    public void setGraphicStroke(final GraphicStroke value) {
+        graphicStroke = value;
+        if (value != null) {
+            graphicFill = null;
+        }
+    }
+
+    /**
+     * Indicates the color of the line if it is to be solid-color filled.
+     * This is used when both {@linkplain #getGraphicFill() graphic fill}
+     * and {@linkplain #getGraphicStroke() graphic stroke} are null.
+     *
+     * @return color of the line if it is to be solid-color filled.
+     *
+     * @see Fill#getColor()
+     */
     public Expression<Feature,Color> getColor() {
-        return color;
+        final var value = color;
+        return (value != null) ? value : Fill.BLACK;
     }
 
-    public void setColor(Expression<Feature, Color> color) {
-        ArgumentChecks.ensureNonNull("color", color);
-        this.color = color;
+    /**
+     * Sets the color of the line if it is to be solid-color filled.
+     * If this method is never invoked, then the default value is {@link Fill#BLACK}.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * <p>Setting a non-null value clears the {@linkplain #getGraphicFill() graphic fill} and the
+     * {@linkplain #getGraphicStroke() graphic stroke} because those three properties are mutually exclusive.</p>
+     *
+     * @param  value  color of the line if solid-color filled, or {@code null} for resetting the default value.
+     *
+     * @see Fill#setColor(Expression)
+     */
+    public void setColor(final Expression<Feature,Color> value) {
+        color = value;
+        if (value != null) {
+            graphicFill   = null;
+            graphicStroke = null;
+        }
     }
 
+    /**
+     * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+     *
+     * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+     *
+     * @see Fill#getOpacity()
+     * @see Graphic#getOpacity()
+     * @see RasterSymbolizer#getOpacity()
+     */
     @Override
-    public Expression<Feature,? extends Number> getOpacity() {
-        return opacity;
+    public Expression<Feature, ? extends Number> getOpacity() {
+        return defaultToOne(opacity);
     }
 
-    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
-        ArgumentChecks.ensureNonNull("opacity", opacity);
-        this.opacity = opacity;
+    /**
+     * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+     * If this method is never invoked, then the default value is literal 1 (totally opaque).
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new level of translucency, or {@code null} for resetting the default value.
+     */
+    @Override
+    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+        opacity = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getWidth() {
-        return width;
+    /**
+     * Gives the absolute width of the line stroke as a floating point number.
+     * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+     * Fractional numbers are allowed, but negative numbers are not.
+     *
+     * @return absolute width of the line stroke as a positive floating point number.
+     */
+    public Expression<Feature, ? extends Number> getWidth() {
+        return defaultToOne(width);
     }
 
-    public void setWidth(Expression<Feature, ? extends Number> width) {
-        ArgumentChecks.ensureNonNull("width", width);
-        this.width = width;
+    /**
+     * Sets the absolute width of the line stroke as a floating point number.
+     * If this method is never invoked, then the default value 1.0.
+     * That default value is standardized by OGC 05-077r4.
+     *
+     * @param  value  new width of the line stroke, or {@code null} for resetting the default value.
+     */
+    public void setWidth(final Expression<Feature, ? extends Number> value) {
+        width = value;
     }
 
-    @Override
+    /**
+     * Indicates how the various segments of a (thick) line string should be joined.
+     * Valid values are "miter", "round", and "bevel".
+     *
+     * @return how segments of a (thick) line string should be joined.
+     */
     public Expression<Feature,String> getLineJoin() {
-        return lineJoin;
+        final var value = lineJoin;
+        return (value != null) ? value : JOIN_BEVEL;
     }
 
-    public void setLineJoin(Expression<Feature, String> lineJoin) {
-        ArgumentChecks.ensureNonNull("lineJoin", lineJoin);
-        this.lineJoin = lineJoin;
+    /**
+     * Sets how the various segments of a (thick) line string should be joined.
+     * If this method is never invoked, then the default value is {@link #JOIN_BEVEL}.
+     * That default value is implementation-specific.
+     *
+     * @param  value  how segments of a line string should be joined, or {@code null} for resetting the default value.
+     */
+    public void setLineJoin(final Expression<Feature,String> value) {
+        lineJoin = value;
     }
 
-    @Override
+    /**
+     * Indicates how the beginning and ending segments of a line string will be terminated.
+     * Valid values are "butt", "round", and "square".
+     *
+     * @return how the beginning and ending segments of a line string will be terminated.
+     */
     public Expression<Feature,String> getLineCap() {
-        return lineCap;
+        final var value = lineCap;
+        return (value != null) ? value : CAP_SQUARE;
     }
 
-    public void setLineCap(Expression<Feature, String> lineCap) {
-        ArgumentChecks.ensureNonNull("lineCap", lineCap);
-        this.lineCap = lineCap;
+    /**
+     * Sets how the beginning and ending segments of a line string will be terminated.
+     * If this method is never invoked, then the default value is {@link #CAP_SQUARE}.
+     * That default value is implementation-specific.
+     *
+     * @param  value  how a line string should be terminated, or {@code null} for resetting the default value.
+     */
+    public void setLineCap(final Expression<Feature,String> value) {
+        lineCap = value;
     }
 
-    @Override
-    public float[] getDashArray() {
-        return dashArray;
+    /**
+     * Indicates the dash pattern as a space-separated sequence of floating point numbers.
+     * The first number represents the length of the first dash to draw.
+     * The second number represents the length of space to leave.
+     * This continues to the end of the list then repeats.
+     * If {@code null}, then lines will be drawn as solid and unbroken.
+     *
+     * @return dash pattern as a space-separated sequence of numbers, or empty for a solid line.
+     */
+    public Optional<Expression<Feature,float[]>> getDashArray() {
+        return Optional.ofNullable(dashArray);
     }
 
-    public void setDashArray(float[] dashArray) {
-        this.dashArray = dashArray;
+    /**
+     * Sets the dash pattern as a space-separated sequence of floating point numbers.
+     * If this method is never invoked, then the default value is {@code null} (solid line).
+     *
+     * @param  value  new dash pattern as a space-separated sequence of numbers, or {@code null} for a solid line.
+     */
+    public void setDashArray(final Expression<Feature,float[]> value) {
+        dashArray = value;
     }
 
-    @Override
-    public Expression<Feature,? extends Number> getDashOffset() {
-        return dashOffset;
+    /**
+     * Indicates the distance offset into the dash array to begin drawing.
+     *
+     * @return distance offset into the dash array to begin drawing.
+     */
+    public Expression<Feature,Integer> getDashOffset() {
+        final var value = dashOffset;
+        return (value != null) ? value : ZERO;
     }
 
-    public void setDashOffset(Expression<Feature, ? extends Number> dashOffset) {
-        ArgumentChecks.ensureNonNull("dashOffset", dashOffset);
-        this.dashOffset = dashOffset;
+    /**
+     * Sets the distance offset into the dash array to begin drawing.
+     * If this method is never invoked, then the default value is 0.
+     *
+     * @param  value  new distance offset into the dash array, or {@code null} for resetting the default value.
+     */
+    public void setDashOffset(final Expression<Feature,Integer> value) {
+        dashOffset = value;
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
+    /*
+     * TODO: we need a private method like below for formatting above SVG parameters:
+     *
+     *     @XmlElement(name = "SvgParameter")
+     *     private List<SvgParameter> svgParameters();
+     *
+     * Where:
+     *
+     *     class SvgParameter {
+     *         @XmlAttribute(required = true)
+     *         private String name;
+     *
+     *         @XmlMixed
+     *         @XmlElementRef(name = "expression", namespace = "http://www.opengis.net/ogc")
+     *         private List<Expression<?,?>> content;
+     *     }
+     *
+     * See 05-077r4 §11.1.3.
+     */
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(graphicFill, graphicStroke, color, opacity, width, lineJoin, lineCap, dashArray, dashOffset);
+    final Object[] properties() {
+        return new Object[] {graphicFill, graphicStroke, color, opacity, width, lineJoin, lineCap, dashArray, dashOffset};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Stroke other = (Stroke) obj;
-        return Objects.equals(this.graphicFill, other.graphicFill)
-            && Objects.equals(this.graphicStroke, other.graphicStroke)
-            && Objects.equals(this.color, other.color)
-            && Objects.equals(this.opacity, other.opacity)
-            && Objects.equals(this.width, other.width)
-            && Objects.equals(this.lineJoin, other.lineJoin)
-            && Objects.equals(this.lineCap, other.lineCap)
-            && Arrays.equals(this.dashArray, other.dashArray)
-            && Objects.equals(this.dashOffset, other.dashOffset);
+    public Stroke clone() {
+        final var clone = (Stroke) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static Stroke castOrCopy(org.opengis.style.Stroke candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Stroke) {
-            return (Stroke) candidate;
-        }
-        return new Stroke(
-                GraphicFill.castOrCopy(candidate.getGraphicFill()),
-                GraphicStroke.castOrCopy(candidate.getGraphicStroke()),
-                candidate.getColor(),
-                candidate.getOpacity(),
-                candidate.getWidth(),
-                candidate.getLineJoin(),
-                candidate.getLineCap(),
-                candidate.getDashArray(),
-                candidate.getDashOffset());
+    private void selfClone() {
+        if (graphicFill   != null) graphicFill   = graphicFill.clone();
+        if (graphicStroke != null) graphicStroke = graphicStroke.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
index 826efd228d..17395fc1fb 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
@@ -16,127 +16,211 @@
  */
 package org.apache.sis.internal.style;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
-import org.opengis.style.StyleVisitor;
-import org.opengis.style.Symbolizer;
+import java.util.ArrayList;
+import java.util.Optional;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Style}.
+ * A set of styles to be applied on different types of features.
+ * This class contains a list of {@link FeatureTypeStyle}.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public final class Style implements org.opengis.style.Style {
-
+public class Style extends StyleElement {
+    /**
+     * Name for this style, or {@code null} if none.
+     *
+     * @see #getName()
+     * @see #setName(String)
+     */
     private String name;
+
+    /**
+     * Information for user interfaces, or {@code null} if none.
+     *
+     * @see #getDescription()
+     * @see #setDescription(Description)
+     */
     private Description description;
+
+    /**
+     * Whether this style is the default one.
+     *
+     * @see #isDefault()
+     * @see #setDefault(boolean)
+     */
     private boolean isDefault;
-    private final List<FeatureTypeStyle> fts = new ArrayList<>();
-    private Symbolizer defaultSymbolizer;
 
-    public Style() {
-    }
+    /**
+     * Collection of styles to apply for different types of features.
+     *
+     * @see #featureTypeStyles()
+     */
+    private List<FeatureTypeStyle> fts;
 
-    public Style(String name, Description description, boolean isDefault, List<FeatureTypeStyle> fts, Symbolizer defaultSymbolizer) {
-        this.name = name;
-        this.description = description;
-        this.isDefault = isDefault;
-        if (fts!=null) this.fts.addAll(fts);
-        this.defaultSymbolizer = defaultSymbolizer;
+    /**
+     * The default symbolizer to use if no rule return {@code true}.
+     *
+     * @see #getDefaultSpecification()
+     * @see #setDefaultSpecification(Symbolizer)
+     */
+    private Symbolizer defaultSpecification;
+
+    /**
+     * Creates an initially empty style.
+     */
+    public Style() {
+        fts = new ArrayList<>();
     }
 
-    @Override
-    public List<FeatureTypeStyle> featureTypeStyles() {
-        return fts;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    public Style(final Style source) {
+        super(source);
+        name        = source.name;
+        description = source.description;
+        isDefault   = source.isDefault;
+        defaultSpecification = source.defaultSpecification;
+        fts = new ArrayList<>(source.fts);
     }
 
-    @Override
-    public boolean isDefault() {
-        return isDefault;
+    /**
+     * Returns the name for this style.
+     * This can be any string that uniquely identifies this style within a given canvas.
+     * It is not meant to be human-friendly. For a human-friendly label,
+     * see the {@linkplain Description#getTitle() title} instead.
+     *
+     * @return a name for this style.
+     */
+    public Optional<String> getName() {
+        return Optional.ofNullable(name);
     }
 
-    public void setDefault(boolean isDefault) {
-        this.isDefault = isDefault;
+    /**
+     * Sets a name for this style.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new name for this style, or {@code null} if none.
+     */
+    public void setName(final String value) {
+        name = value;
     }
 
-    @Override
-    public Symbolizer getDefaultSpecification() {
-        return defaultSymbolizer;
+    /**
+     * Returns the description of this style.
+     * The returned object is <em>live</em>:
+     * changes in the returned instance will be reflected in this style, and conversely.
+     *
+     * @return information for user interfaces.
+     */
+    public Optional<Description> getDescription() {
+        return Optional.ofNullable(description);
     }
 
-    public void setDefaultSpecification(Symbolizer defaultSymbolizer) {
-        this.defaultSymbolizer = defaultSymbolizer;
+    /**
+     * Sets a description of this style.
+     * The given instance is stored by reference, it is not cloned.
+     * If this method is never invoked, then the default value is absence.
+     *
+     * @param  value  new information for user interfaces, or {@code null} if none.
+     */
+    public void setDescription(final Description value) {
+        description = value;
     }
 
-    @Override
-    public String getName() {
-        return name;
+    /**
+     * Returns the list of styles to apply for different types of features.
+     * The returned collection is <em>live</em>:
+     * changes in that collection are reflected into this object, and conversely.
+     *
+     * @return list of styles, as a live collection.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<FeatureTypeStyle> featureTypeStyles() {
+        return fts;
     }
 
-    public void setName(String name) {
-        this.name = name;
+    /**
+     * Returns whether this style is the default one.
+     *
+     * @return Whether this style is the default one.
+     */
+    public boolean isDefault() {
+        return isDefault;
     }
 
-    @Override
-    public Description getDescription() {
-        return description;
+    /**
+     * Sets whether this style is the default one.
+     *
+     * @param  value  whether this style is the default one.
+     */
+    public void setDefault(final boolean value) {
+        isDefault = value;
     }
 
-    public void setDescription(Description description) {
-        this.description = description;
+    /**
+     * Returns the default symbolizer to use if no rule return {@code true}.
+     * This specification should not use any external functions.
+     * This specification should use at least one spatial attribute.
+     *
+     * @return the default symbolizer to use if no rule return {@code true}.
+     */
+    public Optional<Symbolizer> getDefaultSpecification() {
+        return Optional.ofNullable(defaultSpecification);
     }
 
-    @Override
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
+    /**
+     * Sets the default symbolizer to use if no rule return {@code true}.
+     *
+     * @param  value  new default symbolizer to use if no rule return {@code true}.
+     */
+    public void setDefaultSpecification(final Symbolizer value) {
+        defaultSpecification = value;
     }
 
+    /**
+     * Returns all properties contained in this class.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     */
     @Override
-    public int hashCode() {
-        return Objects.hash(name, description, fts);
+    final Object[] properties() {
+        return new Object[] {name, description, isDefault, fts, defaultSpecification};
     }
 
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     *
+     * @return deep clone of all style elements.
+     */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final Style other = (Style) obj;
-        return Objects.equals(this.name, other.name)
-            && Objects.equals(this.description, other.description)
-            && Objects.equals(this.fts, other.fts);
+    public Style clone() {
+        final var clone = (Style) super.clone();
+        clone.selfClone();
+        return clone;
     }
 
     /**
-     * Cast or copy to an SIS implementation.
-     *
-     * @param candidate to copy, can be null.
-     * @return cast or copied object.
+     * Clones the mutable style fields of this element.
      */
-    public static Style castOrCopy(org.opengis.style.Style candidate) {
-        if (candidate == null) {
-            return null;
-        } else if (candidate instanceof Style) {
-            return (Style) candidate;
+    private void selfClone() {
+        if (description != null) {
+            description = description.clone();
         }
-
-        final List<FeatureTypeStyle> cs = new ArrayList<>();
-        for (org.opengis.style.FeatureTypeStyle cr : candidate.featureTypeStyles()) {
-            cs.add(FeatureTypeStyle.castOrCopy(cr));
+        if (defaultSpecification != null) {
+            defaultSpecification = defaultSpecification.clone();
         }
-        return new Style(
-                candidate.getName(),
-                Description.castOrCopy(candidate.getDescription()),
-                candidate.isDefault(),
-                cs,
-                org.apache.sis.internal.style.Symbolizer.tryCastOrCopy(candidate.getDefaultSpecification())
-                );
+        fts = new ArrayList<>(fts);
+        fts.replaceAll(FeatureTypeStyle::clone);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleElement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleElement.java
new file mode 100644
index 0000000000..c0178ff5b1
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleElement.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Arrays;
+import jakarta.xml.bind.annotation.XmlTransient;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.opengis.util.InternationalString;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.filter.FilterFactory;
+import org.opengis.filter.Literal;
+
+
+/**
+ * Base class of all style objects.
+ * This base class can not be extended directly.
+ * Instead, one of the subclasses can be extended.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+@XmlTransient
+public abstract class StyleElement implements Cloneable {
+    /**
+     * Version number of the Symbology Encoding Implementation Specification standard currently implemented.
+     * This version number may change in future Apache SIS releases if new standards are published.
+     * The current value is {@value}.
+     */
+    public static final String VERSION = "1.1.0";
+
+    /**
+     * Literal commonly used as a default value.
+     *
+     * @see #defaultToFalse(Expression)
+     */
+    private static final Literal<Feature,Boolean> FALSE = literal(Boolean.FALSE);
+
+    /**
+     * Literal commonly used as a default value.
+     *
+     * @see #defaultToTrue(Expression)
+     */
+    private static final Literal<Feature,Boolean> TRUE = literal(Boolean.TRUE);
+
+    /**
+     * Literal commonly used as a default value.
+     *
+     * @see #defaultToZero(Expression)
+     */
+    static final Literal<Feature,Double> LITERAL_ZERO = literal(0.0);
+
+    /**
+     * Literal commonly used as a default value.
+     *
+     * @see #defaultToOne(Expression)
+     */
+    private static final Literal<Feature,Double> LITERAL_ONE = literal(1.0);
+
+    /**
+     * Creates a new style element.
+     * Intentionally restricted to this package because {@link #properties()} is package-private.
+     */
+    StyleElement() {
+    }
+
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    StyleElement(final StyleElement source) {
+        // No property to copy yet, but some may be added in the future.
+    }
+
+    /**
+     * The factory for creating default expressions.
+     */
+    static FilterFactory<Feature,Object,Object> FF() {
+        return DefaultFilterFactory.forFeatures();
+    }
+
+    /**
+     * Returns a literal for the given value.
+     * This is used by convenience constructors.
+     *
+     * @param  <E>     type of value.
+     * @param  value   the value for which to return a literal.
+     * @return literal for the given value.
+     */
+    static <E> Literal<Feature,E> literal(final E value) {
+        return FF().literal(value);
+    }
+
+    /**
+     * Returns the given expression if non-null, or {@link #FALSE} otherwise.
+     */
+    static Expression<Feature,Boolean> defaultToFalse(Expression<Feature,Boolean> value) {
+        return (value != null) ? value : FALSE;
+    }
+
+    /**
+     * Returns the given expression if non-null, or {@link #TRUE} otherwise.
+     */
+    static Expression<Feature,Boolean> defaultToTrue(Expression<Feature,Boolean> value) {
+        return (value != null) ? value : TRUE;
+    }
+
+    /**
+     * Returns the given expression if non-null, or {@link #LITERAL_ZERO} otherwise.
+     */
+    static Expression<Feature, ? extends Number> defaultToZero(Expression<Feature, ? extends Number> value) {
+        return (value != null) ? value : LITERAL_ZERO;
+    }
+
+    /**
+     * Returns the given expression if non-null, or {@link #LITERAL_ONE} otherwise.
+     */
+    static Expression<Feature, ? extends Number> defaultToOne(Expression<Feature, ? extends Number> value) {
+        return (value != null) ? value : LITERAL_ONE;
+    }
+
+    /**
+     * Returns all properties contained in the subclasses.
+     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+     *
+     * @return all properties.
+     */
+    abstract Object[] properties();
+
+    /**
+     * Returns a hash code value for this object.
+     *
+     * @return a hash code value for this object.
+     */
+    @Override
+    public int hashCode() {
+        return getClass().hashCode() + Arrays.hashCode(properties());
+    }
+
+    /**
+     * Compares this element with the given object for equality.
+     *
+     * @param  obj  the other object to compare with this.
+     * @return whether the other object is equal to this.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        return (obj != null) && (obj.getClass() == getClass()) &&
+                Arrays.equals(properties(), ((StyleElement) obj).properties());
+    }
+
+    /**
+     * Returns a deep clone of this object. All style elements are cloned,
+     * but expressions are not on the assumption that they are immutable.
+     * ISO 19115 metadata and {@link InternationalString} members, if any,
+     * are not cloned neither in current Apache SIS version.
+     *
+     * @return a clone of this element.
+     */
+    @Override
+    public StyleElement clone() {
+        try {
+            return (StyleElement) super.clone();
+        } catch (CloneNotSupportedException e) {
+            throw new AssertionError(e);    // Should never happen since we are cloneable.
+        }
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java
deleted file mode 100644
index 8a591263e8..0000000000
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java
+++ /dev/null
@@ -1,563 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.internal.style;
-
-import java.awt.Color;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import javax.measure.Unit;
-import javax.measure.quantity.Length;
-import javax.swing.Icon;
-import org.apache.sis.filter.DefaultFilterFactory;
-import org.apache.sis.measure.Units;
-import org.apache.sis.util.SimpleInternationalString;
-import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterFactory;
-import org.opengis.filter.Literal;
-import org.opengis.filter.ResourceId;
-import org.opengis.metadata.citation.OnlineResource;
-import org.opengis.style.ContrastMethod;
-import org.opengis.style.ExtensionSymbolizer;
-import org.opengis.style.OverlapBehavior;
-import org.opengis.style.SemanticType;
-import org.opengis.util.GenericName;
-import org.opengis.util.InternationalString;
-
-/**
- * {@link org.opengis.style.StyleFactory} implementation.
- * Created objects are mutable.
- *
- * TODO : A proper review of GeoAPI Style API is required.
- * This class and style objets implementations are drafts used to continue
- * work on the rendering engine.
- *
- * @see StyleFactory
- *
- * @author Johann Sorel (Geomatys)
- */
-public class StyleFactory implements org.opengis.style.StyleFactory {
-
-    public static final SimpleInternationalString EMPTY_STRING = new SimpleInternationalString("");
-    public static final Literal<Feature,Double> LITERAL_ZERO;
-    public static final Literal<Feature,Double> LITERAL_HALF;
-    public static final Literal<Feature,Double> LITERAL_ONE;
-    public static final Literal<Feature,Color> LITERAL_WHITE;
-    public static final Literal<Feature,Color> LITERAL_GRAY;
-    public static final Literal<Feature,Color> LITERAL_BLACK;
-
-    public static final Literal<Feature,Double> DEFAULT_ANCHOR_POINT_X;
-    public static final Literal<Feature,Double> DEFAULT_ANCHOR_POINT_Y;
-
-    public static final ContrastMethod DEFAULT_CONTRAST_ENHANCEMENT_METHOD;
-    public static final Literal<Feature,Double> DEFAULT_CONTRAST_ENHANCEMENT_GAMMA;
-
-    public static final Literal<Feature,Double> DEFAULT_DISPLACEMENT_X;
-    public static final Literal<Feature,Double> DEFAULT_DISPLACEMENT_Y;
-
-    public static final Literal<Feature,Color> DEFAULT_FILL_COLOR;
-    public static final Literal<Feature,Double> DEFAULT_FILL_OPACITY;
-
-    public static final String STROKE_JOIN_MITRE_STRING = "mitre";
-    public static final String STROKE_JOIN_ROUND_STRING = "round";
-    public static final String STROKE_JOIN_BEVEL_STRING = "bevel";
-    public static final String STROKE_CAP_BUTT_STRING = "butt";
-    public static final String STROKE_CAP_ROUND_STRING = "round";
-    public static final String STROKE_CAP_SQUARE_STRING = "square";
-    public static final Literal<Feature,String> STROKE_JOIN_MITRE;
-    public static final Literal<Feature,String> STROKE_JOIN_ROUND;
-    public static final Literal<Feature,String> STROKE_JOIN_BEVEL;
-    public static final Literal<Feature,String> STROKE_CAP_BUTT;
-    public static final Literal<Feature,String> STROKE_CAP_ROUND;
-    public static final Literal<Feature,String> STROKE_CAP_SQUARE;
-    public static final Literal<Feature,Color> DEFAULT_STROKE_COLOR;
-    public static final Literal<Feature,Double> DEFAULT_STROKE_OPACITY;
-    public static final Literal<Feature,Double> DEFAULT_STROKE_WIDTH;
-    public static final Literal<Feature,String> DEFAULT_STROKE_JOIN;
-    public static final Literal<Feature,String> DEFAULT_STROKE_CAP;
-    public static final Literal<Feature,Double> DEFAULT_STROKE_OFFSET;
-
-    public static final String FONT_STYLE_NORMAL_STRING = "normal";
-    public static final String FONT_STYLE_ITALIC_STRING = "italic";
-    public static final String FONT_STYLE_OBLIQUE_STRING = "oblique";
-    public static final String FONT_WEIGHT_NORMAL_STRING = "normal";
-    public static final String FONT_WEIGHT_BOLD_STRING = "bold";
-    public static final Literal<Feature,String> FONT_STYLE_NORMAL;
-    public static final Literal<Feature,String> FONT_STYLE_ITALIC;
-    public static final Literal<Feature,String> FONT_STYLE_OBLIQUE;
-    public static final Literal<Feature,String> FONT_WEIGHT_NORMAL;
-    public static final Literal<Feature,String> FONT_WEIGHT_BOLD;
-    public static final Literal<Feature,String> DEFAULT_FONT_STYLE;
-    public static final Literal<Feature,String> DEFAULT_FONT_WEIGHT;
-    public static final Literal<Feature,Double> DEFAULT_FONT_SIZE;
-
-    public static final Literal<Feature,Double> DEFAULT_HALO_RADIUS;
-
-    public static final String MARK_SQUARE_STRING = "square";
-    public static final String MARK_CIRCLE_STRING = "circle";
-    public static final String MARK_TRIANGLE_STRING = "triangle";
-    public static final String MARK_STAR_STRING = "star";
-    public static final String MARK_CROSS_STRING = "cross";
-    public static final String MARK_X_STRING = "x";
-    public static final Literal<Feature,String> MARK_SQUARE;
-    public static final Literal<Feature,String> MARK_CIRCLE;
-    public static final Literal<Feature,String> MARK_TRIANGLE;
-    public static final Literal<Feature,String> MARK_STAR;
-    public static final Literal<Feature,String> MARK_CROSS;
-    public static final Literal<Feature,String> MARK_X;
-    public static final Literal<Feature,String> DEFAULT_MARK_WKN;
-
-    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_OPACITY;
-    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_ROTATION;
-    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_SIZE;
-    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_STROKE_INITIAL_GAP;
-    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_STROKE_GAP;
-
-    public static final Unit<Length> DEFAULT_UOM;
-    public static final String DEFAULT_GEOM;
-
-    public static final Literal<Feature,String> DEFAULT_TEXT_LABEL;
-
-    static {
-        final FilterFactory<Feature,Object,Object> FF = DefaultFilterFactory.forFeatures();
-        LITERAL_ZERO = FF.literal(0.0);
-        LITERAL_HALF = FF.literal(0.5);
-        LITERAL_ONE = FF.literal(1.0);
-        LITERAL_WHITE = FF.literal(Color.WHITE);
-        LITERAL_GRAY = FF.literal(Color.GRAY);
-        LITERAL_BLACK = FF.literal(Color.BLACK);
-
-        DEFAULT_UOM = Units.POINT;
-        DEFAULT_GEOM = null;
-
-        DEFAULT_ANCHOR_POINT_X = LITERAL_HALF;
-        DEFAULT_ANCHOR_POINT_Y = LITERAL_HALF;
-
-        DEFAULT_CONTRAST_ENHANCEMENT_METHOD = ContrastMethod.NONE;
-        DEFAULT_CONTRAST_ENHANCEMENT_GAMMA = LITERAL_ONE;
-
-        DEFAULT_DISPLACEMENT_X = LITERAL_ZERO;
-        DEFAULT_DISPLACEMENT_Y = LITERAL_ZERO;
-
-        DEFAULT_FILL_COLOR = FF.literal(Color.GRAY);
-        DEFAULT_FILL_OPACITY = LITERAL_ONE;
-
-        STROKE_JOIN_MITRE = FF.literal(STROKE_JOIN_MITRE_STRING);
-        STROKE_JOIN_ROUND = FF.literal(STROKE_JOIN_ROUND_STRING);
-        STROKE_JOIN_BEVEL = FF.literal(STROKE_JOIN_BEVEL_STRING);
-        STROKE_CAP_BUTT = FF.literal(STROKE_CAP_BUTT_STRING);
-        STROKE_CAP_ROUND = FF.literal(STROKE_CAP_ROUND_STRING);
-        STROKE_CAP_SQUARE = FF.literal(STROKE_CAP_SQUARE_STRING);
-        DEFAULT_STROKE_COLOR = FF.literal(Color.BLACK);
-        DEFAULT_STROKE_OPACITY = LITERAL_ONE;
-        DEFAULT_STROKE_WIDTH = LITERAL_ONE;
-        DEFAULT_STROKE_JOIN = STROKE_JOIN_BEVEL;
-        DEFAULT_STROKE_CAP = STROKE_CAP_SQUARE;
-        DEFAULT_STROKE_OFFSET = LITERAL_ZERO;
-
-        FONT_STYLE_NORMAL = FF.literal(FONT_STYLE_NORMAL_STRING);
-        FONT_STYLE_ITALIC = FF.literal(FONT_STYLE_ITALIC_STRING);
-        FONT_STYLE_OBLIQUE = FF.literal(FONT_STYLE_OBLIQUE_STRING);
-        FONT_WEIGHT_NORMAL = FF.literal(FONT_WEIGHT_NORMAL_STRING);
-        FONT_WEIGHT_BOLD = FF.literal(FONT_WEIGHT_BOLD_STRING);
-        DEFAULT_FONT_STYLE = FONT_STYLE_NORMAL;
-        DEFAULT_FONT_WEIGHT = FONT_WEIGHT_NORMAL;
-        DEFAULT_FONT_SIZE = FF.literal(10.0);
-
-        DEFAULT_HALO_RADIUS = LITERAL_ONE;
-
-        MARK_SQUARE = FF.literal(MARK_SQUARE_STRING);
-        MARK_CIRCLE = FF.literal(MARK_CIRCLE_STRING);
-        MARK_TRIANGLE = FF.literal(MARK_TRIANGLE_STRING);
-        MARK_STAR = FF.literal(MARK_STAR_STRING);
-        MARK_CROSS = FF.literal(MARK_CROSS_STRING);
-        MARK_X = FF.literal(MARK_X_STRING);
-        DEFAULT_MARK_WKN = MARK_SQUARE;
-
-        DEFAULT_GRAPHIC_STROKE_INITIAL_GAP = LITERAL_ZERO;
-        DEFAULT_GRAPHIC_STROKE_GAP = LITERAL_ZERO;
-
-        DEFAULT_GRAPHIC_OPACITY = LITERAL_ONE;
-        DEFAULT_GRAPHIC_ROTATION = LITERAL_ZERO;
-        DEFAULT_GRAPHIC_SIZE = FF.literal(6.0);
-
-        DEFAULT_TEXT_LABEL = FF.literal("Label");
-
-    }
-
-    public StyleFactory(){}
-
-    @Override
-    public AnchorPoint anchorPoint(Expression x, Expression y) {
-        return new org.apache.sis.internal.style.AnchorPoint(x, y);
-    }
-
-    @Override
-    public ChannelSelection channelSelection(org.opengis.style.SelectedChannelType gray) {
-        return new ChannelSelection(SelectedChannelType.castOrCopy(gray));
-    }
-
-    @Override
-    public ChannelSelection channelSelection(
-            org.opengis.style.SelectedChannelType red,
-            org.opengis.style.SelectedChannelType green,
-            org.opengis.style.SelectedChannelType blue) {
-        return new ChannelSelection(
-                SelectedChannelType.castOrCopy(red),
-                SelectedChannelType.castOrCopy(green),
-                SelectedChannelType.castOrCopy(blue));
-    }
-
-    @Override
-    public ColorMap colorMap(Expression propertyName, Expression... mapping) {
-        return new ColorMap(propertyName);
-    }
-
-    @Override
-    public ColorReplacement colorReplacement(Expression propertyName, Expression... mapping) {
-        return new ColorReplacement(propertyName);
-    }
-
-    @Override
-    public ContrastEnhancement contrastEnhancement(Expression gamma, ContrastMethod method) {
-        return new ContrastEnhancement(method, gamma);
-    }
-
-    @Override
-    public Description description(InternationalString title, InternationalString description) {
-        return new Description(title, description);
-    }
-
-    @Override
-    public Displacement displacement(Expression dx, Expression dy) {
-        return new Displacement(dx, dy);
-    }
-
-    @Override
-    public ExternalGraphic externalGraphic(OnlineResource resource, String format,
-            Collection<org.opengis.style.ColorReplacement> replacements) {
-
-        List<ColorReplacement> cs = null;
-        if (replacements != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.ColorReplacement cr : replacements) {
-                cs.add(ColorReplacement.castOrCopy(cr));
-            }
-        }
-        return new ExternalGraphic(resource, null, format, cs);
-    }
-
-    @Override
-    public ExternalGraphic externalGraphic(Icon inline,
-            Collection<org.opengis.style.ColorReplacement> replacements) {
-        List<ColorReplacement> cs = null;
-        if (replacements != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.ColorReplacement cr : replacements) {
-                cs.add(ColorReplacement.castOrCopy(cr));
-            }
-        }
-        return new ExternalGraphic(null, inline, null, cs);
-    }
-
-    @Override
-    public ExternalMark externalMark(OnlineResource resource, String format, int markIndex) {
-        return new ExternalMark(resource, null, format, markIndex);
-    }
-
-    @Override
-    public ExternalMark externalMark(Icon inline) {
-        return new ExternalMark(null, inline, null, 0);
-    }
-
-    @Override
-    public FeatureTypeStyle featureTypeStyle(String name,
-            org.opengis.style.Description description,
-            ResourceId definedFor, Set<GenericName> featureTypeNames,
-            Set<SemanticType> types, List<org.opengis.style.Rule> rules) {
-        List<Rule> cs = null;
-        if (rules != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.Rule cr : rules) {
-                cs.add(Rule.castOrCopy(cr));
-            }
-        }
-        return new FeatureTypeStyle(name, Description.castOrCopy(description),
-                definedFor, featureTypeNames, types, cs, null);
-    }
-
-    @Override
-    public Fill fill(org.opengis.style.GraphicFill fill, Expression color, Expression opacity) {
-        return new Fill(GraphicFill.castOrCopy(fill), color, opacity);
-    }
-
-    @Override
-    public Font font(List<Expression> family, Expression style, Expression weight, Expression size) {
-        return new Font(family, style, weight, size);
-    }
-
-    @Override
-    public Graphic graphic( List<org.opengis.style.GraphicalSymbol> symbols,
-            Expression opacity, Expression size, Expression rotation,
-            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp) {
-        List<GraphicalSymbol> cs = null;
-        if (symbols != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.GraphicalSymbol cr : symbols) {
-                cs.add(GraphicalSymbol.castOrCopy(cr));
-            }
-        }
-        return new Graphic(cs, opacity, size, rotation,
-                AnchorPoint.castOrCopy(anchor),
-                Displacement.castOrCopy(disp));
-    }
-
-    @Override
-    public GraphicFill graphicFill(List<org.opengis.style.GraphicalSymbol> symbols,
-            Expression opacity, Expression size, Expression rotation,
-            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp) {
-        List<GraphicalSymbol> cs = null;
-        if (symbols != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.GraphicalSymbol cr : symbols) {
-                cs.add(GraphicalSymbol.castOrCopy(cr));
-            }
-        }
-        return new GraphicFill(cs, opacity, size, rotation,
-                AnchorPoint.castOrCopy(anchor),
-                Displacement.castOrCopy(disp));
-    }
-
-    @Override
-    public GraphicLegend graphicLegend(List<org.opengis.style.GraphicalSymbol> symbols,
-            Expression opacity, Expression size, Expression rotation,
-            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp) {
-        List<GraphicalSymbol> cs = null;
-        if (symbols != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.GraphicalSymbol cr : symbols) {
-                cs.add(GraphicalSymbol.castOrCopy(cr));
-            }
-        }
-        return new GraphicLegend(cs, opacity, size, rotation,
-                AnchorPoint.castOrCopy(anchor),
-                Displacement.castOrCopy(disp));
-    }
-
-    @Override
-    public GraphicStroke graphicStroke(List<org.opengis.style.GraphicalSymbol> symbols,
-            Expression opacity, Expression size, Expression rotation,
-            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp,
-            Expression initialGap, Expression gap) {
-        List<GraphicalSymbol> cs = null;
-        if (symbols != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.GraphicalSymbol cr : symbols) {
-                cs.add(GraphicalSymbol.castOrCopy(cr));
-            }
-        }
-        return new GraphicStroke(cs, opacity, size, rotation,
-                AnchorPoint.castOrCopy(anchor),
-                Displacement.castOrCopy(disp),
-                initialGap, gap);
-    }
-
-    @Override
-    public Halo halo(org.opengis.style.Fill fill, Expression radius) {
-        return new Halo(Fill.castOrCopy(fill), radius);
-    }
-
-    @Override
-    public LinePlacement linePlacement(Expression offset, Expression initialGap,
-            Expression gap, boolean repeated, boolean aligned, boolean generalizedLine) {
-        return new LinePlacement(offset, initialGap, gap, repeated, aligned, generalizedLine);
-    }
-
-    @Override
-    public LineSymbolizer lineSymbolizer(String name, Expression geometry,
-            org.opengis.style.Description description, Unit<?> unit,
-            org.opengis.style.Stroke stroke, Expression offset) {
-        return new LineSymbolizer(name, geometry,
-                Description.castOrCopy(description),
-                (Unit<Length>) unit,
-                Stroke.castOrCopy(stroke), offset);
-    }
-
-    @Override
-    public Mark mark(Expression wellKnownName, org.opengis.style.Fill fill,
-            org.opengis.style.Stroke stroke) {
-        return new Mark(wellKnownName, null,
-                Fill.castOrCopy(fill),
-                Stroke.castOrCopy(stroke));
-    }
-
-    @Override
-    public Mark mark(org.opengis.style.ExternalMark externalMark,
-            org.opengis.style.Fill fill, org.opengis.style.Stroke stroke) {
-        return new Mark(null, ExternalMark.castOrCopy(externalMark),
-                Fill.castOrCopy(fill),
-                Stroke.castOrCopy(stroke));
-    }
-
-    @Override
-    public PointPlacement pointPlacement(org.opengis.style.AnchorPoint anchor,
-            org.opengis.style.Displacement displacement, Expression rotation) {
-        return new PointPlacement(
-                AnchorPoint.castOrCopy(anchor),
-                Displacement.castOrCopy(displacement),
-                rotation);
-    }
-
-    @Override
-    public PointSymbolizer pointSymbolizer(String name, Expression geometry,
-            org.opengis.style.Description description, Unit<?> unit,
-            org.opengis.style.Graphic graphic) {
-        return new PointSymbolizer(name, geometry,
-                Description.castOrCopy(description), (Unit<Length>) unit,
-                Graphic.castOrCopy(graphic));
-    }
-
-    @Override
-    public PolygonSymbolizer polygonSymbolizer(String name, Expression geometry,
-            org.opengis.style.Description description, Unit<?> unit,
-            org.opengis.style.Stroke stroke, org.opengis.style.Fill fill,
-            org.opengis.style.Displacement displacement, Expression offset) {
-        return new PolygonSymbolizer(name, geometry,
-                Description.castOrCopy(description), (Unit<Length>) unit,
-                Stroke.castOrCopy(stroke),
-                Fill.castOrCopy(fill),
-                Displacement.castOrCopy(displacement), offset);
-    }
-
-    @Override
-    public RasterSymbolizer rasterSymbolizer(String name, Expression geometry,
-            org.opengis.style.Description description, Unit<?> unit,
-            Expression opacity, org.opengis.style.ChannelSelection channelSelection,
-            OverlapBehavior overlapsBehaviour,
-            org.opengis.style.ColorMap colorMap,
-            org.opengis.style.ContrastEnhancement contrast,
-            org.opengis.style.ShadedRelief shaded,
-            org.opengis.style.Symbolizer outline) {
-        return new RasterSymbolizer(name, geometry,
-                Description.castOrCopy(description),
-                (Unit<Length>) unit, opacity,
-                ChannelSelection.castOrCopy(channelSelection),
-                overlapsBehaviour,
-                ColorMap.castOrCopy(colorMap),
-                ContrastEnhancement.castOrCopy(contrast),
-                ShadedRelief.castOrCopy(shaded),
-                outline);
-    }
-
-    @Override
-    public ExtensionSymbolizer extensionSymbolizer(String name, String geometry,
-            org.opengis.style.Description description, Unit<?> unit, String extensionName,
-            Map<String, Expression> parameters) {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
-    @Override
-    public ExtensionSymbolizer extensionSymbolizer(String name, Expression geometry,
-            org.opengis.style.Description description, Unit<?> unit, String extensionName,
-            Map<String, Expression> parameters) {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
-    @Override
-    public Rule rule(String name, org.opengis.style.Description description,
-            org.opengis.style.GraphicLegend legend, double min, double max,
-            List<org.opengis.style.Symbolizer> symbolizers, Filter filter) {
-        return new Rule(name,
-                Description.castOrCopy(description),
-                GraphicLegend.castOrCopy(legend),
-                filter, false, min, max, symbolizers, null);
-    }
-
-    @Override
-    public SelectedChannelType selectedChannelType(String channelName,
-            org.opengis.style.ContrastEnhancement contrastEnhancement) {
-        return new SelectedChannelType(channelName,
-                ContrastEnhancement.castOrCopy(contrastEnhancement));
-    }
-
-    @Override
-    public ShadedRelief shadedRelief(Expression reliefFactor, boolean brightnessOnly) {
-        return new ShadedRelief(brightnessOnly, reliefFactor);
-    }
-
-    @Override
-    public Stroke stroke(Expression color, Expression opacity, Expression width,
-            Expression join, Expression cap, float[] dashes, Expression offset) {
-        return new Stroke(null, null, color, opacity, width, join, cap, dashes, offset);
-    }
-
-    @Override
-    public Stroke stroke(org.opengis.style.GraphicFill fill, Expression color,
-            Expression opacity, Expression width, Expression join, Expression cap,
-            float[] dashes, Expression offset) {
-        return new Stroke(GraphicFill.castOrCopy(fill),
-                null, color, opacity, width, join, cap, dashes, offset);
-    }
-
-    @Override
-    public Stroke stroke(org.opengis.style.GraphicStroke stroke,
-            Expression color, Expression opacity, Expression width,
-            Expression join, Expression cap, float[] dashes, Expression offset) {
-        return new Stroke(null, GraphicStroke.castOrCopy(stroke),
-                color, opacity, width, join, cap, dashes, offset);
-    }
-
-    @Override
-    public Style style(String name, org.opengis.style.Description description,
-            boolean isDefault, List<org.opengis.style.FeatureTypeStyle> featureTypeStyles,
-            org.opengis.style.Symbolizer defaultSymbolizer) {
-        List<FeatureTypeStyle> cs = null;
-        if (featureTypeStyles != null) {
-            cs = new ArrayList<>();
-            for (org.opengis.style.FeatureTypeStyle cr : featureTypeStyles) {
-                cs.add(FeatureTypeStyle.castOrCopy(cr));
-            }
-        }
-        return new Style(
-                name,
-                Description.castOrCopy(description),
-                isDefault,
-                cs,
-                defaultSymbolizer);
-    }
-
-    @Override
-    public TextSymbolizer textSymbolizer(String name,Expression geometry,
-            org.opengis.style.Description description, Unit<?> unit,
-            Expression label, org.opengis.style.Font font,
-            org.opengis.style.LabelPlacement placement,
-            org.opengis.style.Halo halo,
-            org.opengis.style.Fill fill) {
-        return new TextSymbolizer(name, geometry,
-                Description.castOrCopy(description),
-                (Unit<Length>) unit, label,
-                Font.castOrCopy(font),
-                LabelPlacement.castOrCopy(placement),
-                Halo.castOrCopy(halo),
-                Fill.castOrCopy(fill));
-    }
-
-}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
index 689606bfc2..2aaeed071d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
@@ -17,112 +17,298 @@
 package org.apache.sis.internal.style;
 
 import java.util.Objects;
+import java.util.Optional;
 import javax.measure.Unit;
-import javax.measure.quantity.Length;
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlSchemaType;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlSeeAlso;
+import org.apache.sis.measure.Units;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.feature.AttributeConvention;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.style.Description;
+import org.opengis.filter.ValueReference;
+
 
 /**
- * Mutable implementation of {@link org.opengis.style.Symbolizer}.
+ * Description of how a feature is to appear on a map.
+ * A symbolizer describes how the shape should appear,
+ * together with graphical properties such as color and opacity.
+ * A symbolizer is obtained by specifying one of a small number of different types
+ * and then supplying parameters to override its default behavior.
+ * The predefined type of symbolizers are
+ * {@linkplain LineSymbolizer line},
+ * {@linkplain PolygonSymbolizer polygon},
+ * {@linkplain PointSymbolizer point},
+ * {@linkplain TextSymbolizer text}, and
+ * {@linkplain RasterSymbolizer raster} symbolizers.
  *
- * @author Johann Sorel (Geomatys)
+ * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+ * @author  Johann Sorel (Geomatys)
+ * @author  Chris Dillard (SYS Technologies)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-public abstract class Symbolizer implements org.opengis.style.Symbolizer {
+@XmlType(name = "SymbolizerType", propOrder = {
+    "name",
+    "description",
+    "geometry"
+})
+@XmlSeeAlso({
+    LineSymbolizer.class,
+    PointSymbolizer.class,
+    PolygonSymbolizer.class,
+    TextSymbolizer.class,
+    RasterSymbolizer.class
+})
+@XmlRootElement(name = "Symbolizer")
+public abstract class Symbolizer extends StyleElement {
+    /**
+     * An expression for fetching the default geometry.
+     *
+     * @todo According SE specification, the default expression in the context of some symbolizers should
+     *       fetch all geometries, not only a default one. The default seems to depend on the symbolizer type.
+     */
+    private static final ValueReference<Feature,?> DEFAULT_GEOMETRY =
+                            FF().property(AttributeConvention.GEOMETRY);
+
+    /**
+     * Name for this style, or {@code null} if none.
+     *
+     * @see #getName()
+     * @see #setName(String)
+     */
+    @XmlElement(name = "Name")
+    protected String name;
+
+    /**
+     * Information for user interfaces, or {@code null} if none.
+     *
+     * @see #getDescription()
+     * @see #setDescription(Description)
+     */
+    @XmlElement(name = "Description")
+    protected Description description;
+
+    /**
+     * Expression fetching the geometry to draw, or {@code null} for the default geometries.
+     * The Symbology Encoding restrict the XML representation to {@code <ogc:PropertyName>}
+     * (defined in the Filter Specification), but Apache SIS accepts any expression.
+     *
+     * @see #getGeometry()
+     * @see #setGeometry(Expression)
+     */
+    @XmlElement(name = "Geometry")
+    protected Expression<Feature,?> geometry;
 
-    private String name;
-    private Description description;
-    private Expression<Feature,?> geometry;
-    private Unit<Length> unit;
+    /**
+     * Unit of measurement for all lengths inside this symbolizer, or {@code null} for the default value.
+     * The recommended XML representations of this attribute are like below:
+     *
+     * <ul>
+     *   <li>uom="http://www.opengeospatial.org/se/units/metre"</li>
+     *   <li>uom="http://www.opengeospatial.org/se/units/foot"</li>
+     *   <li>uom="http://www.opengeospatial.org/se/units/pixel"</li>
+     * </ul>
+     *
+     * @todo Recommended XML representation is not yet implemented.
+     *
+     * @see #getUnitOfMeasure()
+     * @see #setUnitOfMeasure(Unit)
+     */
+    @XmlAttribute(name = "uom")
+    @XmlSchemaType(name = "anyURI")
+    protected Unit<?> unit;
 
-    public Symbolizer() {
+    /**
+     * Creates a symbolizer initialized to default geometries and pixel unit of measurement.
+     */
+    protected Symbolizer() {
     }
 
-    public Symbolizer(String name, Expression geometry, Description description, Unit<Length> unit) {
-        this.name = name;
-        this.geometry = geometry;
-        this.description = description;
-        this.unit = unit;
+    /**
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
+     *
+     * @param  source  the object to copy.
+     */
+    protected Symbolizer(final Symbolizer source) {
+        super(source);
+        name        = source.name;
+        geometry    = source.geometry;
... 4126 lines suppressed ...


[sis] 03/04: Add a placeholder for a JAXB adapter for expressions, then verifies that current JAXB annotations are valid.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit d38b17c046b889b1457ca83b071bcc467a4dec02
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jun 30 10:46:13 2023 +0200

    Add a placeholder for a JAXB adapter for expressions,
    then verifies that current JAXB annotations are valid.
---
 core/sis-portrayal/pom.xml                         |  5 ++
 .../java/org/apache/sis/style/se1/ElseFilter.java  | 51 ++++++++++++++++
 .../apache/sis/style/se1/ExpressionAdapter.java    | 50 +++++++++++++++
 .../main/java/org/apache/sis/style/se1/Rule.java   | 15 +++++
 .../org/apache/sis/style/se1/package-info.java     | 71 ++++++++++++++++++++++
 .../java/org/apache/sis/style/se1/XmlTest.java     | 50 +++++++++++++++
 6 files changed, 242 insertions(+)

diff --git a/core/sis-portrayal/pom.xml b/core/sis-portrayal/pom.xml
index 521d22b4f7..d599f2e4fe 100644
--- a/core/sis-portrayal/pom.xml
+++ b/core/sis-portrayal/pom.xml
@@ -141,6 +141,11 @@
       <type>test-jar</type>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.glassfish.jaxb</groupId>
+      <artifactId>jaxb-runtime</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
 </project>
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ElseFilter.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ElseFilter.java
new file mode 100644
index 0000000000..a6e4fce3f5
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ElseFilter.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.style.se1;
+
+import jakarta.xml.bind.annotation.XmlType;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+
+/**
+ * The element to marshall when no other rule matches the conditions.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+@XmlType(name = "ElseFilterType")
+@XmlRootElement(name = "ElseFilter")
+final class ElseFilter {
+    /**
+     * The singleton instance.
+     */
+    static final ElseFilter INSTANCE = new ElseFilter();
+
+    /**
+     * Creates the singleton instance.
+     */
+    private ElseFilter() {
+    }
+
+    /**
+     * Returns a string representation of this element.
+     */
+    @Override
+    public String toString() {
+        return "ElseFilter";
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
new file mode 100644
index 0000000000..97d24dfbaf
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.style.se1;
+
+import jakarta.xml.bind.annotation.adapters.XmlAdapter;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+
+
+/**
+ * Adapter for expression in style.
+ * This is a place-holder for future work.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+final class ExpressionAdapter extends XmlAdapter<String, Expression<Feature,?>> {
+    /**
+     * Creates an adapter.
+     */
+    public ExpressionAdapter() {
+    }
+
+    @Override
+    public String marshal(Expression<Feature,?> value) {
+        return null;
+    }
+
+    @Override
+    public Expression<Feature,?> unmarshal(String value) {
+        return null;
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
index 8d72197b73..dd376e43ea 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
@@ -281,6 +281,21 @@ public class Rule<R> extends StyleElement {
         isElseFilter = value;
     }
 
+    /**
+     * Invoked by JAXB at marshalling time for expressing the boolean {@code isElseFilter} value as an XML element.
+     */
+    @XmlElement(name = "ElseFilter")
+    private ElseFilter getElseFilter() {
+        return isElseFilter ? ElseFilter.INSTANCE : null;
+    }
+
+    /**
+     * Invoked at JAXB unmarshalling time when an {@code <ElseFilter/>} element is found.
+     */
+    private void setElseFilter(final ElseFilter value) {
+        isElseFilter = (value != null);
+    }
+
     /**
      * Returns the minimum value (inclusive) in the denominator of map scale at which this rule will apply.
      * If, for example, this value was 10000, then this rule would only apply at scales of 1:<var>X</var>
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java
new file mode 100644
index 0000000000..d70474e0f3
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * Symbology encoding for styling map data independently of their source.
+ * The classes in this package are derived from
+ * OGC 05-077r4 — <a href="https://www.ogc.org/standard/se/">Symbology Encoding</a> Implementation Specification 1.1.0.
+ * That document defines an XML encoding that can be used for styling feature and coverage data.
+ * The root elements are
+ * {@link org.apache.sis.style.se1.FeatureTypeStyle} and
+ * {@link org.apache.sis.style.se1.CoverageStyle}.
+ * Those classes include different kinds of {@link org.apache.sis.style.se1.Symbolizer}.
+ *
+ * @todo Add {@code CoverageStyle}. May require a common parent with {@code FeatureTypeStyle}.
+ *
+ * <h2>Future evolution</h2>
+ * This package defines a XML encoding.
+ * It is not an abstract model for sophisticated styling.
+ * Apache SIS temporarily uses the classes of the XML encoding as a style API,
+ * but a future version may replace this API by a more abstract one.
+ * A good candidate may be ISO 19117:2012 — Portrayal.
+ * As of 2023, various OGC working groups are also working on new style API.
+ * The final form of such API has not yet been settled down.
+ *
+ * <h2>Synchronization</h2>
+ * Classes in this package are not thread-safe.
+ * Synchronization, if desired, must be done by the caller.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+@XmlSchema(location="https://schemas.opengis.net/se/1.1.0/FeatureStyle.xsd",
+           elementFormDefault=XmlNsForm.QUALIFIED, namespace=Namespaces.SE,
+           xmlns = {
+                @XmlNs(prefix = "se", namespaceURI = Namespaces.SE)
+})
+@XmlAccessorType(XmlAccessType.NONE)
+@XmlJavaTypeAdapters({
+    @XmlJavaTypeAdapter(ExpressionAdapter.class),
+    @XmlJavaTypeAdapter(InternationalStringConverter.class),
+    @XmlJavaTypeAdapter(UnitAdapter.class)
+})
+package org.apache.sis.style.se1;
+
+import jakarta.xml.bind.annotation.XmlAccessType;
+import jakarta.xml.bind.annotation.XmlAccessorType;
+import jakarta.xml.bind.annotation.XmlNs;
+import jakarta.xml.bind.annotation.XmlNsForm;
+import jakarta.xml.bind.annotation.XmlSchema;
+import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
+import org.apache.sis.internal.jaxb.gco.InternationalStringConverter;
+import org.apache.sis.internal.jaxb.gco.UnitAdapter;
+import org.apache.sis.xml.Namespaces;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/XmlTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/XmlTest.java
new file mode 100644
index 0000000000..e3a72d8b48
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/XmlTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.style.se1;
+
+import jakarta.xml.bind.JAXBContext;
+import jakarta.xml.bind.JAXBException;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+
+/**
+ * Test of XML marshalling.
+ * The current version only verifies that {@link JAXBContext} can be created.
+ * We do not yet have sufficient JAXB annotations and adapters for real marshalling.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
+ */
+public final class XmlTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public XmlTest() {
+    }
+
+    /**
+     * Tests the creation of a JAXB context.
+     *
+     * @throws JAXBException if some invalid annotations were found.
+     */
+    @Test
+    public void testContext() throws JAXBException {
+        JAXBContext.newInstance(Symbolizer.class);
+    }
+}


[sis] 04/04: Add `CoverageStyle` and reintroduce `StyleFactory` classes (in different form). `CoverageStyle` is defined by OGC 05-077r4 in complement to `FeatureTypeStyle`. Those two classes are identical except for the kind of data on which they work: `FeatureTypeStyle` styles `Feature` while `CoverageStyle` styles `BandedCoverage`. For making that difference possible, it was necessary to add parameterized type on all classes in replacement for the previously hard-coded type. Since the < [...]

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 1dacb571d6071c36edd85050af1116d14c3b1736
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jun 30 18:26:24 2023 +0200

    Add `CoverageStyle` and reintroduce `StyleFactory` classes (in different form).
    `CoverageStyle` is defined by OGC 05-077r4 in complement to `FeatureTypeStyle`.
    Those two classes are identical except for the kind of data on which they work:
    `FeatureTypeStyle` styles `Feature` while `CoverageStyle` styles `BandedCoverage`.
    For making that difference possible, it was necessary to add parameterized type
    <R> on all classes in replacement for the previously hard-coded <Feature> type.
    Since the <R> type depends on the filter factory, it become necessary to use a
    `StyleFactory` for creating the style objects.
---
 .../sis/internal/map/ResourceSymbolizer.java       |  13 +-
 .../apache/sis/internal/map/SymbologyVisitor.java  |  56 +--
 .../{FeatureTypeStyle.java => AbstractStyle.java}  |  49 +-
 .../java/org/apache/sis/style/se1/AnchorPoint.java |  57 +--
 .../org/apache/sis/style/se1/ChannelSelection.java |  39 +-
 .../java/org/apache/sis/style/se1/ColorMap.java    |  27 +-
 .../org/apache/sis/style/se1/ColorReplacement.java |  27 +-
 .../apache/sis/style/se1/ContrastEnhancement.java  |  35 +-
 .../{ColorReplacement.java => CoverageStyle.java}  |  40 +-
 .../java/org/apache/sis/style/se1/Description.java |  29 +-
 .../org/apache/sis/style/se1/Displacement.java     |  47 +-
 .../apache/sis/style/se1/ExpressionAdapter.java    |  12 +-
 .../org/apache/sis/style/se1/ExternalGraphic.java  |  29 +-
 .../org/apache/sis/style/se1/FeatureTypeStyle.java | 270 +----------
 .../main/java/org/apache/sis/style/se1/Fill.java   | 103 ++--
 .../main/java/org/apache/sis/style/se1/Font.java   |  65 +--
 .../java/org/apache/sis/style/se1/Graphic.java     |  73 +--
 .../java/org/apache/sis/style/se1/GraphicFill.java |  33 +-
 .../org/apache/sis/style/se1/GraphicStroke.java    |  46 +-
 .../org/apache/sis/style/se1/GraphicalElement.java |  11 +-
 .../org/apache/sis/style/se1/GraphicalSymbol.java  |  22 +-
 .../main/java/org/apache/sis/style/se1/Halo.java   |  41 +-
 .../org/apache/sis/style/se1/LabelPlacement.java   |  23 +-
 .../org/apache/sis/style/se1/LegendGraphic.java    |  33 +-
 .../org/apache/sis/style/se1/LinePlacement.java    |  65 +--
 .../org/apache/sis/style/se1/LineSymbolizer.java   |  40 +-
 .../main/java/org/apache/sis/style/se1/Mark.java   |  90 ++--
 .../org/apache/sis/style/se1/PointPlacement.java   |  50 +-
 .../org/apache/sis/style/se1/PointSymbolizer.java  |  46 +-
 .../apache/sis/style/se1/PolygonSymbolizer.java    |  73 +--
 .../org/apache/sis/style/se1/RasterSymbolizer.java |  62 ++-
 .../main/java/org/apache/sis/style/se1/Rule.java   |  35 +-
 .../org/apache/sis/style/se1/SelectedChannel.java  |  41 +-
 .../org/apache/sis/style/se1/ShadedRelief.java     |  47 +-
 .../main/java/org/apache/sis/style/se1/Stroke.java | 148 +++---
 .../main/java/org/apache/sis/style/se1/Style.java  |  54 ++-
 .../org/apache/sis/style/se1/StyleElement.java     | 146 +++---
 .../org/apache/sis/style/se1/StyleFactory.java     | 534 +++++++++++++++++++++
 .../java/org/apache/sis/style/se1/Symbolizer.java  |  52 +-
 .../org/apache/sis/style/se1/TextSymbolizer.java   |  63 ++-
 .../java/org/apache/sis/style/se1/Translucent.java |  12 +-
 .../org/apache/sis/style/se1/package-info.java     |  10 +-
 .../apache/sis/internal/map/SEPortrayerTest.java   |  99 ++--
 .../org/apache/sis/style/se1/AnchorPointTest.java  |   6 +-
 .../apache/sis/style/se1/ChannelSelectionTest.java |  24 +-
 .../sis/style/se1/ContrastEnhancementTest.java     |   6 +-
 .../org/apache/sis/style/se1/DescriptionTest.java  |   4 +-
 .../org/apache/sis/style/se1/DisplacementTest.java |   6 +-
 .../apache/sis/style/se1/ExternalGraphicTest.java  |  10 +-
 .../apache/sis/style/se1/FeatureTypeStyleTest.java |  19 +-
 .../java/org/apache/sis/style/se1/FillTest.java    |  13 +-
 .../java/org/apache/sis/style/se1/FontTest.java    |  16 +-
 .../apache/sis/style/se1/GraphicStrokeTest.java    |   8 +-
 .../java/org/apache/sis/style/se1/GraphicTest.java |  28 +-
 .../java/org/apache/sis/style/se1/HaloTest.java    |  24 +-
 .../apache/sis/style/se1/LinePlacementTest.java    |  24 +-
 .../apache/sis/style/se1/LineSymbolizerTest.java   |  15 +-
 .../java/org/apache/sis/style/se1/MarkTest.java    |  23 +-
 .../apache/sis/style/se1/PointPlacementTest.java   |  19 +-
 .../apache/sis/style/se1/PointSymbolizerTest.java  |  11 +-
 .../sis/style/se1/PolygonSymbolizerTest.java       |  26 +-
 .../apache/sis/style/se1/RasterSymbolizerTest.java |  26 +-
 .../java/org/apache/sis/style/se1/RuleTest.java    |  27 +-
 .../apache/sis/style/se1/SelectedChannelTest.java  |   8 +-
 .../org/apache/sis/style/se1/ShadedReliefTest.java |   8 +-
 .../java/org/apache/sis/style/se1/StrokeTest.java  |  36 +-
 .../java/org/apache/sis/style/se1/StyleTest.java   |   4 +-
 .../org/apache/sis/style/se1/StyleTestCase.java    |  26 +-
 .../org/apache/sis/style/se1/SymbolizerTest.java   |  12 +-
 .../apache/sis/style/se1/TextSymbolizerTest.java   |  29 +-
 70 files changed, 1941 insertions(+), 1364 deletions(-)

diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
index 07ea6abd40..e690e57b02 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.map;
 
+import org.apache.sis.style.se1.StyleFactory;
 import org.apache.sis.style.se1.Symbolizer;
 
 
@@ -30,12 +31,18 @@ import org.apache.sis.style.se1.Symbolizer;
  *
  * @author  Johann Sorel (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
-public abstract class ResourceSymbolizer extends Symbolizer {
+public abstract class ResourceSymbolizer<R> extends Symbolizer<R> {
     /**
      * Constructs a new symbolozer.
+     *
+     * @param  context  context (features or coverages) in which this style element will be used.
      */
-    protected ResourceSymbolizer() {
+    public ResourceSymbolizer(final StyleFactory<R> context) {
+        super(context);
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
index 1ee4aeea0e..723526fc1b 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
@@ -60,7 +60,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final Symbolizer candidate) {
+    protected void visit(final Symbolizer<?> candidate) {
         if (candidate instanceof PointSymbolizer) {
             visit((PointSymbolizer) candidate);
         } else if (candidate instanceof LineSymbolizer) {
@@ -76,14 +76,14 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final PointSymbolizer candidate) {
+    protected void visit(final PointSymbolizer<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGeometry());
             visit(candidate.getGraphic());
         }
     }
 
-    protected void visit(final LineSymbolizer candidate) {
+    protected void visit(final LineSymbolizer<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGeometry());
             visit(candidate.getPerpendicularOffset());
@@ -91,7 +91,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final PolygonSymbolizer candidate) {
+    protected void visit(final PolygonSymbolizer<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGeometry());
             visit(candidate.getPerpendicularOffset());
@@ -101,7 +101,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final TextSymbolizer candidate) {
+    protected void visit(final TextSymbolizer<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGeometry());
             visit(candidate.getLabel());
@@ -112,7 +112,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final RasterSymbolizer candidate) {
+    protected void visit(final RasterSymbolizer<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGeometry());
             visit(candidate.getOpacity());
@@ -124,13 +124,13 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final GraphicalElement candidate) {
+    protected void visit(final GraphicalElement<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGraphic());
         }
     }
 
-    protected void visit(final Graphic candidate) {
+    protected void visit(final Graphic<?> candidate) {
         if (candidate != null) {
             visit(candidate.getOpacity());
             visit(candidate.getRotation());
@@ -151,7 +151,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final Mark candidate) {
+    protected void visit(final Mark<?> candidate) {
         if (candidate != null) {
             candidate.getFill().ifPresent(this::visit);
             candidate.getStroke().ifPresent(this::visit);
@@ -159,11 +159,11 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final ExternalGraphic candidate) {
+    protected void visit(final ExternalGraphic<?> candidate) {
         nonNull(candidate.colorReplacements()).forEach(this::visit);
     }
 
-    protected void visit(final Stroke candidate) {
+    protected void visit(final Stroke<?> candidate) {
         if (candidate != null) {
             visit(candidate.getColor());
             visit(candidate.getDashOffset());
@@ -179,14 +179,14 @@ public abstract class SymbologyVisitor {
     protected void visit(final Description candidate) {
     }
 
-    protected void visit(final Displacement candidate) {
+    protected void visit(final Displacement<?> candidate) {
         if (candidate != null) {
             visit(candidate.getDisplacementX());
             visit(candidate.getDisplacementY());
         }
     }
 
-    protected void visit(final Fill candidate) {
+    protected void visit(final Fill<?> candidate) {
         if (candidate != null) {
             candidate.getGraphicFill().ifPresent(this::visit);
             visit(candidate.getColor());
@@ -194,7 +194,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final Font candidate) {
+    protected void visit(final Font<?> candidate) {
         if (candidate != null) {
             candidate.family().forEach(this::visit);
             visit(candidate.getSize());
@@ -203,11 +203,11 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final GraphicFill candidate) {
+    protected void visit(final GraphicFill<?> candidate) {
         visit((GraphicalElement) candidate);
     }
 
-    protected void visit(final GraphicStroke candidate) {
+    protected void visit(final GraphicStroke<?> candidate) {
         if (candidate != null) {
             visit((GraphicalElement) candidate);
             visit(candidate.getGap());
@@ -225,7 +225,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final PointPlacement candidate) {
+    protected void visit(final PointPlacement<?> candidate) {
         if (candidate != null) {
             visit(candidate.getAnchorPoint());
             visit(candidate.getDisplacement());
@@ -233,14 +233,14 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final AnchorPoint candidate) {
+    protected void visit(final AnchorPoint<?> candidate) {
         if (candidate != null) {
             visit(candidate.getAnchorPointX());
             visit(candidate.getAnchorPointY());
         }
     }
 
-    protected void visit(final LinePlacement candidate) {
+    protected void visit(final LinePlacement<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGap());
             visit(candidate.getInitialGap());
@@ -248,47 +248,47 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final LegendGraphic candidate) {
+    protected void visit(final LegendGraphic<?> candidate) {
         visit((GraphicalElement) candidate);
     }
 
-    protected void visit(final Halo candidate) {
+    protected void visit(final Halo<?> candidate) {
         if (candidate != null) {
             visit(candidate.getFill());
             visit(candidate.getRadius());
         }
     }
 
-    protected void visit(final ColorMap candidate) {
+    protected void visit(final ColorMap<?> candidate) {
     }
 
-    protected void visit(final ColorReplacement candidate) {
+    protected void visit(final ColorReplacement<?> candidate) {
     }
 
-    protected void visit(final ContrastEnhancement candidate) {
+    protected void visit(final ContrastEnhancement<?> candidate) {
         if (candidate != null) {
             visit(candidate.getGammaValue());
         }
     }
 
-    protected void visit(final ChannelSelection candidate) {
+    protected void visit(final ChannelSelection<?> candidate) {
         if (candidate != null) {
             SelectedChannel[] channels = candidate.getChannels();
             if (channels != null) {
-                for (final SelectedChannel sct : channels) {
+                for (final SelectedChannel<?> sct : channels) {
                     visit(sct);
                 }
             }
         }
     }
 
-    protected void visit(final SelectedChannel candidate) {
+    protected void visit(final SelectedChannel<?> candidate) {
         if (candidate != null) {
             candidate.getContrastEnhancement().ifPresent(this::visit);
         }
     }
 
-    protected void visit(final ShadedRelief candidate) {
+    protected void visit(final ShadedRelief<?> candidate) {
         if (candidate != null) {
             visit(candidate.getReliefFactor());
         }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
similarity index 87%
copy from core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
copy to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
index fd27c07876..78fc11bda8 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
@@ -23,36 +23,45 @@ import java.util.Optional;
 import jakarta.xml.bind.Unmarshaller;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
-import jakarta.xml.bind.annotation.XmlRootElement;
 import jakarta.xml.bind.annotation.XmlAttribute;
 import org.opengis.util.GenericName;
 import org.apache.sis.util.collection.CodeListSet;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.ResourceId;
 import org.opengis.style.SemanticType;
 
 
 /**
- * Defines the styling that is to be applied to a single feature type.
+ * Defines the styling that is to be applied on data of some arbitrary type.
+ * The type of data is specified by the {@code <R>} parameterized type and depends on the concrete subclass.
+ * The two main types are {@link org.opengis.feature.Feature} and {@link org.apache.sis.coverage.BandedCoverage}.
  *
  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
  * @author  Johann Sorel (Geomatys)
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
  * @since   1.5
  */
-@XmlType(name = "FeatureTypeStyleType", propOrder = {
+@XmlType(propOrder = {
     "name",
     "description",
     "featureTypeName",
 //  "semanticTypeIdentifiers",
     "rules"
 })
-@XmlRootElement(name = "FeatureTypeStyle")
-public class FeatureTypeStyle extends StyleElement {
+public abstract class AbstractStyle<R> extends StyleElement<R> {
+    /**
+     * Version number of the Symbology Encoding Implementation Specification standard currently implemented.
+     * This version number may change in future Apache SIS releases if new standards are published.
+     * The current value is {@value}.
+     */
+    public static final String VERSION = "1.1.0";
+
     /**
      * Version number of the Symbology Encoding standard used.
      * This value should not be changed, unless this style has been unmarshalled
@@ -79,7 +88,7 @@ public class FeatureTypeStyle extends StyleElement {
      * @see #setDescription(Description)
      */
     @XmlElement(name = "Description")
-    protected Description description;
+    protected Description<R> description;
 
     /**
      * Identification of feature instances on which to apply the style, or {@code null} if none.
@@ -87,7 +96,7 @@ public class FeatureTypeStyle extends StyleElement {
      * @see #getFeatureInstanceIDs()
      * @see #setFeatureInstanceIDs(ResourceId)
      */
-    protected ResourceId<? super Feature> featureInstanceIDs;
+    protected ResourceId<? super R> featureInstanceIDs;
 
     /**
      * Name of the feature type that this style is meant to act upon, or {@code null} if none.
@@ -122,7 +131,7 @@ public class FeatureTypeStyle extends StyleElement {
      * @see #rules()
      */
     @XmlElement(name = "Rule")
-    private List<Rule<Feature>> rules;
+    private List<Rule<R>> rules;
 
     /**
      * Invoked by JAXB before unmarshalling this mark.
@@ -141,8 +150,11 @@ public class FeatureTypeStyle extends StyleElement {
      *       or {@linkplain #semanticTypeIdentifiers() semantic type identifiers}, or both.</li>
      *   <li>At least one {@linkplain #rules() rule} should be added.</li>
      * </ul>
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public FeatureTypeStyle() {
+    public AbstractStyle(final StyleFactory<R> factory) {
+        super(factory);
         version = VERSION;
         semanticTypeIdentifiers = new CodeListSet<>(SemanticType.class);
         rules = new ArrayList<>();
@@ -154,7 +166,7 @@ public class FeatureTypeStyle extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public FeatureTypeStyle(final FeatureTypeStyle source) {
+    public AbstractStyle(final AbstractStyle<R> source) {
         super(source);
         version                 = source.version;
         name                    = source.name;
@@ -206,7 +218,7 @@ public class FeatureTypeStyle extends StyleElement {
      *
      * @return information for user interfaces.
      */
-    public Optional<Description> getDescription() {
+    public Optional<Description<R>> getDescription() {
         return Optional.ofNullable(description);
     }
 
@@ -217,7 +229,7 @@ public class FeatureTypeStyle extends StyleElement {
      *
      * @param  value  new information for user interfaces, or {@code null} if none.
      */
-    public void setDescription(final Description value) {
+    public void setDescription(final Description<R> value) {
         description = value;
     }
 
@@ -228,7 +240,7 @@ public class FeatureTypeStyle extends StyleElement {
      *
      * @return identification of the feature instances.
      */
-    public Optional<ResourceId<? super Feature>> getFeatureInstanceIDs() {
+    public Optional<ResourceId<? super R>> getFeatureInstanceIDs() {
         return Optional.ofNullable(featureInstanceIDs);
     }
 
@@ -238,7 +250,7 @@ public class FeatureTypeStyle extends StyleElement {
      *
      * @param  value  new identification of feature instances, or {@code null} if none.
      */
-    public void setFeatureInstanceIDs(final ResourceId<? super Feature> value) {
+    public void setFeatureInstanceIDs(final ResourceId<? super R> value) {
         featureInstanceIDs = value;
     }
 
@@ -298,7 +310,7 @@ public class FeatureTypeStyle extends StyleElement {
      * @return ordered list of rules, as a live collection.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<Rule<Feature>> rules() {
+    public List<Rule<R>> rules() {
         return rules;
     }
 
@@ -318,8 +330,8 @@ public class FeatureTypeStyle extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public FeatureTypeStyle clone() {
-        final var clone = (FeatureTypeStyle) super.clone();
+    public AbstractStyle<R> clone() {
+        final var clone = (AbstractStyle<R>) super.clone();
         clone.selfClone();
         return clone;
     }
@@ -327,7 +339,6 @@ public class FeatureTypeStyle extends StyleElement {
     /**
      * Clones the mutable style fields of this element.
      */
-    @SuppressWarnings("unchecked")
     private void selfClone() {
         if (description != null) description = description.clone();
         semanticTypeIdentifiers = semanticTypeIdentifiers.clone();
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
index 4bd9637fd5..bd2c32907f 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
@@ -21,9 +21,7 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.filter.Literal;
 
 
 /**
@@ -39,19 +37,17 @@ import org.opengis.filter.Literal;
  * @author  Ian Turton (CCG)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "AnchorPointType", propOrder = {
     "anchorPointX",
     "anchorPointY"
 })
 @XmlRootElement(name = "AnchorPoint")
-public class AnchorPoint extends StyleElement {
-    /**
-     * Literal used as default value.
-     */
-    private static final Literal<Feature,Double> LITERAL_HALF = literal(0.5);;
-
+public class AnchorPoint<R> extends StyleElement<R> {
     /**
      * The <var>x</var> coordinate of the anchor point.
      * This property is mandatory.
@@ -60,7 +56,7 @@ public class AnchorPoint extends StyleElement {
      * @see #setAnchorPointX(Expression)
      */
     @XmlElement(name = "AnchorPointX", required = true)
-    protected Expression<Feature, ? extends Number> anchorPointX;
+    protected Expression<R, ? extends Number> anchorPointX;
 
     /**
      * The <var>y</var> coordinate of the anchor point.
@@ -70,26 +66,24 @@ public class AnchorPoint extends StyleElement {
      * @see #setAnchorPointY(Expression)
      */
     @XmlElement(name = "AnchorPointY", required = true)
-    protected Expression<Feature, ? extends Number> anchorPointY;
+    protected Expression<R, ? extends Number> anchorPointY;
 
     /**
-     * Creates a anchor point initialized to <var>x</var> = 0.5 and <var>y</var> = 0.5.
-     * This initial position is the center of the graphic/label.
+     * For JAXB unmarshalling only.
      */
-    public AnchorPoint() {
-        anchorPointX = LITERAL_HALF;
-        anchorPointY = LITERAL_HALF;
+    private AnchorPoint() {
+        // Thread-local factory will be used.
     }
 
     /**
-     * Creates a new anchor point initialized to the given position.
+     * Creates an anchor point initialized to <var>x</var> = 0.5 and <var>y</var> = 0.5.
+     * This initial position is the center of the graphic/label.
      *
-     * @param  x  the initial <var>x</var> position.
-     * @param  y  the initial <var>y</var> position.
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public AnchorPoint(final double x, final double y) {
-        anchorPointX = literal(x);
-        anchorPointY = literal(y);
+    public AnchorPoint(final StyleFactory<R> factory) {
+        super(factory);
+        anchorPointX = anchorPointY = factory.half;
     }
 
     /**
@@ -98,7 +92,7 @@ public class AnchorPoint extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public AnchorPoint(final AnchorPoint source) {
+    public AnchorPoint(final AnchorPoint<R> source) {
         super(source);
         anchorPointX = source.anchorPointX;
         anchorPointY = source.anchorPointY;
@@ -110,7 +104,7 @@ public class AnchorPoint extends StyleElement {
      *
      * @return the expression fetching the <var>x</var> coordinate.
      */
-    public Expression<Feature, ? extends Number> getAnchorPointX() {
+    public Expression<R, ? extends Number> getAnchorPointX() {
         return anchorPointX;
     }
 
@@ -120,8 +114,8 @@ public class AnchorPoint extends StyleElement {
      *
      * @param  value  new <var>x</var> coordinate, or {@code null} for resetting the default value.
      */
-    public void setAnchorPointX(final Expression<Feature, ? extends Number> value) {
-        anchorPointX = (value != null) ? value : LITERAL_HALF;
+    public void setAnchorPointX(final Expression<R, ? extends Number> value) {
+        anchorPointX = defaultToHalf(value);
     }
 
     /**
@@ -130,7 +124,7 @@ public class AnchorPoint extends StyleElement {
      *
      * @return the expression fetching the <var>y</var> coordinate.
      */
-    public Expression<Feature, ? extends Number> getAnchorPointY() {
+    public Expression<R, ? extends Number> getAnchorPointY() {
         return anchorPointY;
     }
 
@@ -140,8 +134,8 @@ public class AnchorPoint extends StyleElement {
      *
      * @param  value  new <var>y</var> coordinate, or {@code null} for resetting the default value.
      */
-    public void setAnchorPointY(final Expression<Feature, ? extends Number> value) {
-        anchorPointY = (value != null) ? value : LITERAL_HALF;
+    public void setAnchorPointY(final Expression<R, ? extends Number> value) {
+        anchorPointY = defaultToHalf(value);
     }
 
     /**
@@ -160,8 +154,7 @@ public class AnchorPoint extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public AnchorPoint clone() {
-        final var clone = (AnchorPoint) super.clone();
-        return clone;
+    public AnchorPoint<R> clone() {
+        return (AnchorPoint<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
index 49e3ddc585..c6ba9961ae 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
@@ -33,7 +33,10 @@ import org.apache.sis.util.resources.Errors;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "ChannelSelectionType", propOrder = {
     "red",
@@ -42,39 +45,49 @@ import org.apache.sis.util.resources.Errors;
     "gray"
 })
 @XmlRootElement(name = "ChannelSelection")
-public class ChannelSelection extends StyleElement {
+public class ChannelSelection<R> extends StyleElement<R> {
     /**
      * The red channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #gray}.
      */
     @XmlElement(name = "RedChannel")
-    protected SelectedChannel red;
+    protected SelectedChannel<R> red;
 
     /**
      * The green channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #gray}.
      */
     @XmlElement(name = "GreenChannel")
-    protected SelectedChannel green;
+    protected SelectedChannel<R> green;
 
     /**
      * The blue channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #gray}.
      */
     @XmlElement(name = "BlueChannel")
-    protected SelectedChannel blue;
+    protected SelectedChannel<R> blue;
 
     /**
      * The gray channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #red}, {@link #green} and {@link #blue}.
      */
     @XmlElement(name = "GrayChannel")
-    protected SelectedChannel gray;
+    protected SelectedChannel<R> gray;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private ChannelSelection() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates an initially empty channel selection.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public ChannelSelection() {
+    public ChannelSelection(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -83,7 +96,7 @@ public class ChannelSelection extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public ChannelSelection(final ChannelSelection source) {
+    public ChannelSelection(final ChannelSelection<R> source) {
         super(source);
         red   = source.red;
         green = source.green;
@@ -101,7 +114,8 @@ public class ChannelSelection extends StyleElement {
      *
      * @todo Replace null value by some default value.
      */
-    public SelectedChannel[] getChannels() {
+    @SuppressWarnings({"rawtypes", "unchecked"})        // Generic array creation.
+    public SelectedChannel<R>[] getChannels() {
         if (red != null || green != null || blue != null) {
             return new SelectedChannel[] {red, green, blue};
         } else if (gray != null) {
@@ -122,7 +136,8 @@ public class ChannelSelection extends StyleElement {
      * @param  values  array of channels, or {@code null} if none.
      * @throws IllegalArgumentException if the length of the specified array is not 0, 1 or 3.
      */
-    public void setChannels(final SelectedChannel... values) {
+    @SafeVarargs
+    public final void setChannels(final SelectedChannel<R>... values) {
         red   = null;
         green = null;
         blue  = null;
@@ -160,8 +175,8 @@ public class ChannelSelection extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public ChannelSelection clone() {
-        final var clone = (ChannelSelection) super.clone();
+    public ChannelSelection<R> clone() {
+        final var clone = (ChannelSelection<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
index bae948b067..093ac18bfe 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
@@ -33,7 +33,10 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "ColorMapType", propOrder = {
 //  "categorize",
@@ -41,11 +44,21 @@ import jakarta.xml.bind.annotation.XmlRootElement;
 //  "jenks"
 })
 @XmlRootElement(name = "ColorMap")
-public class ColorMap extends StyleElement {
+public class ColorMap<R> extends StyleElement<R> {
     /**
-     * Creates a color map.
+     * For JAXB unmarshalling only.
+     */
+    private ColorMap() {
+        // Thread-local factory will be used.
+    }
+
+    /**
+     * Creates an initially empty color map.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public ColorMap() {
+    public ColorMap(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -54,7 +67,7 @@ public class ColorMap extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public ColorMap(final ColorMap source) {
+    public ColorMap(final ColorMap<R> source) {
         super(source);
     }
 
@@ -74,8 +87,8 @@ public class ColorMap extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public ColorMap clone() {
-        final var clone = (ColorMap) super.clone();
+    public ColorMap<R> clone() {
+        final var clone = (ColorMap<R>) super.clone();
         return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
index d07590de4c..a86fdb969c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
@@ -30,15 +30,28 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "ColorReplacementType")
 @XmlRootElement(name = "ColorReplacement")
-public class ColorReplacement extends StyleElement {
+public class ColorReplacement<R> extends StyleElement<R> {
     /**
-     * Creates a color replacement.
+     * For JAXB unmarshalling only.
+     */
+    private ColorReplacement() {
+        // Thread-local factory will be used.
+    }
+
+    /**
+     * Creates an initially empty color replacement.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public ColorReplacement() {
+    public ColorReplacement(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -47,7 +60,7 @@ public class ColorReplacement extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public ColorReplacement(final ColorReplacement source) {
+    public ColorReplacement(final ColorReplacement<R> source) {
         super(source);
     }
 
@@ -67,8 +80,8 @@ public class ColorReplacement extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public ColorReplacement clone() {
-        final var clone = (ColorReplacement) super.clone();
+    public ColorReplacement<R> clone() {
+        final var clone = (ColorReplacement<R>) super.clone();
         return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
index 1cf6c910b0..36c5f5d76e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
@@ -21,13 +21,12 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 import org.opengis.style.ContrastMethod;
 
 
 /**
- * Contrast enhancement for an image channel.
+ * Contrast enhancement for an image or an individual image channel.
  * In the case of a color image, the relative grayscale brightness of a pixel color is used.
  *
  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
@@ -35,7 +34,10 @@ import org.opengis.style.ContrastMethod;
  * @author  Ian Turton (CCG)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "ContrastEnhancementType", propOrder = {
 //  "normalize",
@@ -43,7 +45,7 @@ import org.opengis.style.ContrastMethod;
     "gammaValue"
 })
 @XmlRootElement(name = "ContrastEnhancement")
-public class ContrastEnhancement extends StyleElement {
+public class ContrastEnhancement<R> extends StyleElement<R> {
     /**
      * Method to use for applying contrast enhancement, or {@code null} for the default value.
      * The default value depends on whether or not a {@linkplain #gammaValue gamma value} is defined.
@@ -64,12 +66,22 @@ public class ContrastEnhancement extends StyleElement {
      * @todo Add a JAXB adapter for marshalling as a plain number.
      */
     @XmlElement(name = "GammaValue")
-    protected Expression<Feature, ? extends Number> gammaValue;
+    protected Expression<R, ? extends Number> gammaValue;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private ContrastEnhancement() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a contrast enhancement initialized to no operation.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public ContrastEnhancement() {
+    public ContrastEnhancement(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -78,7 +90,7 @@ public class ContrastEnhancement extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public ContrastEnhancement(final ContrastEnhancement source) {
+    public ContrastEnhancement(final ContrastEnhancement<R> source) {
         super(source);
         method     = source.method;
         gammaValue = source.gammaValue;
@@ -117,7 +129,7 @@ public class ContrastEnhancement extends StyleElement {
      *
      * @return expression to control gamma adjustment.
      */
-    public Expression<Feature, ? extends Number> getGammaValue() {
+    public Expression<R, ? extends Number> getGammaValue() {
         return defaultToOne(gammaValue);
     }
 
@@ -128,7 +140,7 @@ public class ContrastEnhancement extends StyleElement {
      *
      * @param  value  new expression to control gamma adjustment, or {@code null} for the default.
      */
-    public void setGammaValue(final Expression<Feature, ? extends Number> value) {
+    public void setGammaValue(final Expression<R, ? extends Number> value) {
         gammaValue = value;
         if (value != null) {
             method = null;
@@ -151,8 +163,7 @@ public class ContrastEnhancement extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public ContrastEnhancement clone() {
-        final var clone = (ContrastEnhancement) super.clone();
-        return clone;
+    public ContrastEnhancement<R> clone() {
+        return (ContrastEnhancement<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/CoverageStyle.java
similarity index 64%
copy from core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
copy to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/CoverageStyle.java
index d07590de4c..733f7d55b8 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/CoverageStyle.java
@@ -18,27 +18,30 @@ package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlRootElement;
+import org.apache.sis.coverage.BandedCoverage;
 
 
 /**
- * Replacement of a color in an external graphic.
+ * Defines the styling that is to be applied to a coverage.
  *
- * <p>This is a placeholder for future development.
- * OGC 05-077r4 standard defines the dependent classes,
- * but there is too many of them for this initial draft.</p>
- *
- * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
  * @since   1.5
  */
-@XmlType(name = "ColorReplacementType")
-@XmlRootElement(name = "ColorReplacement")
-public class ColorReplacement extends StyleElement {
+@XmlType(name = "CoverageStyleType")
+@XmlRootElement(name = "CoverageStyle")
+public class CoverageStyle extends AbstractStyle<BandedCoverage> {
     /**
-     * Creates a color replacement.
+     * The default style factory for coverages.
      */
-    public ColorReplacement() {
+    public static final StyleFactory<BandedCoverage> FACTORY =
+            new StyleFactory<>(FeatureTypeStyle.FACTORY);
+
+    /**
+     * Creates an initially empty coverage style.
+     */
+    public CoverageStyle() {
+        super(FACTORY);
     }
 
     /**
@@ -47,19 +50,10 @@ public class ColorReplacement extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public ColorReplacement(final ColorReplacement source) {
+    public CoverageStyle(final CoverageStyle source) {
         super(source);
     }
 
-    /**
-     * Returns all properties contained in this class.
-     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
-     */
-    @Override
-    final Object[] properties() {
-        return new Object[] {};
-    }
-
     /**
      * Returns a deep clone of this object. All style elements are cloned,
      * but expressions are not on the assumption that they are immutable.
@@ -67,8 +61,8 @@ public class ColorReplacement extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public ColorReplacement clone() {
-        final var clone = (ColorReplacement) super.clone();
+    public CoverageStyle clone() {
+        final var clone = (CoverageStyle) super.clone();
         return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
index 805fc2a33d..aac01a939e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
@@ -24,7 +24,7 @@ import org.opengis.util.InternationalString;
 
 
 /**
- * Informative description of a style object being defined.
+ * Informative (human-readable) description of a style object being defined.
  * Description values are mostly used in User Interfaces (lists, trees, …).
  *
  * <p>Note that most style object also have a name.
@@ -36,14 +36,19 @@ import org.opengis.util.InternationalString;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
+ *
+ * @todo Consider replacing this class by {@link org.opengis.metadata.identification.Identification}.
  */
 @XmlType(name = "DescriptionType", propOrder = {
     "title",
     "summary"
 })
 @XmlRootElement(name = "Description")
-public class Description extends StyleElement {
+public class Description<R> extends StyleElement<R> {
     /**
      * Human readable title of the style, or {@code null} if none.
      *
@@ -63,10 +68,20 @@ public class Description extends StyleElement {
     @XmlElement(name = "Abstract")
     protected InternationalString summary;
 
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private Description() {
+        // Thread-local factory will be used.
+    }
+
     /**
      * Creates an initially empty description.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Description() {
+    public Description(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -75,7 +90,7 @@ public class Description extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public Description(final Description source) {
+    public Description(final Description<R> source) {
         super(source);
         title   = source.title;
         summary = source.summary;
@@ -138,8 +153,8 @@ public class Description extends StyleElement {
      * @return a clone of this object.
      */
     @Override
-    public Description clone() {
-        final var clone = (Description) super.clone();
+    public Description<R> clone() {
+        final var clone = (Description<R>) super.clone();
         return clone;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
index 4cd83e8d4e..84702112c9 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
@@ -21,12 +21,11 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
 /**
- * A two-dimensional displacements from the original geometry.
+ * A two-dimensional offset from the original geometry.
  * Displacements may be used to avoid over-plotting, or for supplying shadows.
  * The displacements units depend on the context:
  * in {@linkplain Symbolizer#getUnitOfMeasure() symbolizer unit of measurements}
@@ -40,7 +39,10 @@ import org.opengis.filter.Expression;
  * @author  Ian Turton (CCG)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  *
  * @see PointPlacement#getDisplacement()
  * @see Graphic#getDisplacement()
@@ -51,7 +53,7 @@ import org.opengis.filter.Expression;
     "displacementY"
 })
 @XmlRootElement(name = "Displacement")
-public class Displacement extends StyleElement {
+public class Displacement<R> extends StyleElement<R> {
     /**
      * The <var>x</var> offset from the geometry point.
      * This property is mandatory.
@@ -60,7 +62,7 @@ public class Displacement extends StyleElement {
      * @see #setDisplacementX(Expression)
      */
     @XmlElement(name = "DisplacementX", required = true)
-    protected Expression<Feature, ? extends Number> displacementX;
+    protected Expression<R, ? extends Number> displacementX;
 
     /**
      * The <var>y</var> offset from the geometry point.
@@ -70,25 +72,23 @@ public class Displacement extends StyleElement {
      * @see #setDisplacementY(Expression)
      */
     @XmlElement(name = "DisplacementY", required = true)
-    protected Expression<Feature, ? extends Number> displacementY;
+    protected Expression<R, ? extends Number> displacementY;
 
     /**
-     * Creates a displacement initialized to zero offsets.
+     * For JAXB unmarshalling only.
      */
-    public Displacement() {
-        displacementX = LITERAL_ZERO;
-        displacementY = LITERAL_ZERO;
+    private Displacement() {
+        // Thread-local factory will be used.
     }
 
     /**
-     * Creates a new displacement initialized to the given offsets.
+     * Creates a displacement initialized to zero offsets.
      *
-     * @param  x  the initial <var>x</var> displacement.
-     * @param  y  the initial <var>y</var> displacement.
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Displacement(final double x, final double y) {
-        displacementX = literal(x);
-        displacementY = literal(y);
+    public Displacement(final StyleFactory<R> factory) {
+        super(factory);
+        displacementX = displacementY = factory.zero;
     }
 
     /**
@@ -97,7 +97,7 @@ public class Displacement extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public Displacement(final Displacement source) {
+    public Displacement(final Displacement<R> source) {
         super(source);
         displacementX = source.displacementX;
         displacementY = source.displacementY;
@@ -108,7 +108,7 @@ public class Displacement extends StyleElement {
      *
      * @return <var>x</var> offset from the geometry point.
      */
-    public Expression<Feature, ? extends Number> getDisplacementX() {
+    public Expression<R, ? extends Number> getDisplacementX() {
         return displacementX;
     }
 
@@ -118,7 +118,7 @@ public class Displacement extends StyleElement {
      *
      * @param  value  new <var>x</var> offset, or {@code null} for resetting the default value.
      */
-    public void setDisplacementX(final Expression<Feature, ? extends Number> value) {
+    public void setDisplacementX(final Expression<R, ? extends Number> value) {
         displacementX = defaultToZero(value);
     }
 
@@ -127,7 +127,7 @@ public class Displacement extends StyleElement {
      *
      * @return <var>y</var> offset from the geometry point.
      */
-    public Expression<Feature, ? extends Number> getDisplacementY() {
+    public Expression<R, ? extends Number> getDisplacementY() {
         return displacementY;
     }
 
@@ -137,7 +137,7 @@ public class Displacement extends StyleElement {
      *
      * @param  value  new <var>y</var> offset, or {@code null} for resetting the default value.
      */
-    public void setDisplacementY(final Expression<Feature, ? extends Number> value) {
+    public void setDisplacementY(final Expression<R, ? extends Number> value) {
         displacementY = defaultToZero(value);
     }
 
@@ -157,8 +157,7 @@ public class Displacement extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public Displacement clone() {
-        final var clone = (Displacement) super.clone();
-        return clone;
+    public Displacement<R> clone() {
+        return (Displacement<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
index 97d24dfbaf..3ea09310de 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
@@ -19,7 +19,6 @@ package org.apache.sis.style.se1;
 import jakarta.xml.bind.annotation.adapters.XmlAdapter;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -29,9 +28,12 @@ import org.opengis.filter.Expression;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
-final class ExpressionAdapter extends XmlAdapter<String, Expression<Feature,?>> {
+final class ExpressionAdapter<R> extends XmlAdapter<String, Expression<R,?>> {
     /**
      * Creates an adapter.
      */
@@ -39,12 +41,12 @@ final class ExpressionAdapter extends XmlAdapter<String, Expression<Feature,?>>
     }
 
     @Override
-    public String marshal(Expression<Feature,?> value) {
+    public String marshal(Expression<R,?> value) {
         return null;
     }
 
     @Override
-    public Expression<Feature,?> unmarshal(String value) {
+    public Expression<R,?> unmarshal(String value) {
         return null;
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
index 11d5899f04..05f5abbdb8 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
@@ -32,7 +32,10 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "ExternalGraphicType", propOrder = {
 //  "onlineResource",       // XML encoding not yet available.
@@ -41,7 +44,7 @@ import jakarta.xml.bind.annotation.XmlRootElement;
     "colorReplacements"
 })
 @XmlRootElement(name = "ExternalGraphic")
-public class ExternalGraphic extends GraphicalSymbol {
+public class ExternalGraphic<R> extends GraphicalSymbol<R> {
     /**
      * A list of colors to replace, or {@code null} if none.
      *
@@ -53,12 +56,22 @@ public class ExternalGraphic extends GraphicalSymbol {
      * @see #colorReplacements()
      */
     @XmlElement(name = "ColorReplacement")
-    protected List<ColorReplacement> colorReplacements;
+    protected List<ColorReplacement<R>> colorReplacements;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private ExternalGraphic() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates an initially empty external graphic.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public ExternalGraphic() {
+    public ExternalGraphic(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -67,7 +80,7 @@ public class ExternalGraphic extends GraphicalSymbol {
      *
      * @param  source  the object to copy.
      */
-    public ExternalGraphic(final ExternalGraphic source) {
+    public ExternalGraphic(final ExternalGraphic<R> source) {
         super(source);
         final var value = source.colorReplacements;
         if (value != null) {
@@ -84,7 +97,7 @@ public class ExternalGraphic extends GraphicalSymbol {
      * @return list of colors to replace, as a live collection.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<ColorReplacement> colorReplacements() {
+    public List<ColorReplacement<R>> colorReplacements() {
         if (colorReplacements == null) {
             colorReplacements = new ArrayList<>();
         }
@@ -108,8 +121,8 @@ public class ExternalGraphic extends GraphicalSymbol {
      * @return deep clone of all style elements.
      */
     @Override
-    public ExternalGraphic clone() {
-        final var clone = (ExternalGraphic) super.clone();
+    public ExternalGraphic<R> clone() {
+        final var clone = (ExternalGraphic<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
index fd27c07876..6aa50648ee 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
@@ -16,22 +16,12 @@
  */
 package org.apache.sis.style.se1;
 
-import java.util.Set;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Optional;
-import jakarta.xml.bind.Unmarshaller;
 import jakarta.xml.bind.annotation.XmlType;
-import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
-import jakarta.xml.bind.annotation.XmlAttribute;
-import org.opengis.util.GenericName;
-import org.apache.sis.util.collection.CodeListSet;
+import org.apache.sis.filter.DefaultFilterFactory;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
-import org.opengis.filter.ResourceId;
-import org.opengis.style.SemanticType;
 
 
 /**
@@ -44,93 +34,14 @@ import org.opengis.style.SemanticType;
  * @version 1.5
  * @since   1.5
  */
-@XmlType(name = "FeatureTypeStyleType", propOrder = {
-    "name",
-    "description",
-    "featureTypeName",
-//  "semanticTypeIdentifiers",
-    "rules"
-})
+@XmlType(name = "FeatureTypeStyleType")
 @XmlRootElement(name = "FeatureTypeStyle")
-public class FeatureTypeStyle extends StyleElement {
+public class FeatureTypeStyle extends AbstractStyle<Feature> {
     /**
-     * Version number of the Symbology Encoding standard used.
-     * This value should not be changed, unless this style has been unmarshalled
-     * from a XML document that specifies a different version number.
-     *
-     * @see #getVersion()
-     */
-    @XmlAttribute
-    protected String version;
-
-    /**
-     * Name for this style, or {@code null} if none.
-     *
-     * @see #getName()
-     * @see #setName(String)
-     */
-    @XmlElement(name = "Name")
-    protected String name;
-
-    /**
-     * Information for user interfaces, or {@code null} if none.
-     *
-     * @see #getDescription()
-     * @see #setDescription(Description)
-     */
-    @XmlElement(name = "Description")
-    protected Description description;
-
-    /**
-     * Identification of feature instances on which to apply the style, or {@code null} if none.
-     *
-     * @see #getFeatureInstanceIDs()
-     * @see #setFeatureInstanceIDs(ResourceId)
-     */
-    protected ResourceId<? super Feature> featureInstanceIDs;
-
-    /**
-     * Name of the feature type that this style is meant to act upon, or {@code null} if none.
-     *
-     * @see #getFeatureTypeName()
-     * @see #setFeatureTypeName(GenericName)
-     */
-    @XmlElement(name = "FeatureTypeName")
-    protected GenericName featureTypeName;
-
-    /**
-     * Types of geometry that this style is meant to act upon, as a live collection.
-     * May be empty but never null.
-     *
-     * @see #semanticTypeIdentifiers()
-     */
-//  @XmlElement(name = "SemanticTypeIdentifier")
-    private CodeListSet<SemanticType> semanticTypeIdentifiers;
-
-    /**
-     * List of rules.
-     *
-     * <h4>Online rules</h4>
-     * A XML document may contain links to rules instead of a full definitions.
-     * Such {@code se:OnlineResource} elements are handled at marshalling time.
-     * When reading a XML document, the rule links are resolved automatically.
-     * When writing a XML document, some rules may be replaced by online resources
-     * if {@link Rule#getOnlineSource()} is provided.
-     *
-     * @todo JAXB adapter for handling the online case is not yet written.
-     *
-     * @see #rules()
+     * The default style factory for features.
      */
-    @XmlElement(name = "Rule")
-    private List<Rule<Feature>> rules;
-
-    /**
-     * Invoked by JAXB before unmarshalling this mark.
-     * Avoid giving the false impression that the XML document contained a version string.
-     */
-    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
-        version = "unspecified";
-    }
+    public static final StyleFactory<Feature> FACTORY =
+            new StyleFactory<>(DefaultFilterFactory.forFeatures());
 
     /**
      * Creates an initially empty feature type style.
@@ -143,9 +54,7 @@ public class FeatureTypeStyle extends StyleElement {
      * </ul>
      */
     public FeatureTypeStyle() {
-        version = VERSION;
-        semanticTypeIdentifiers = new CodeListSet<>(SemanticType.class);
-        rules = new ArrayList<>();
+        super(FACTORY);
     }
 
     /**
@@ -156,159 +65,6 @@ public class FeatureTypeStyle extends StyleElement {
      */
     public FeatureTypeStyle(final FeatureTypeStyle source) {
         super(source);
-        version                 = source.version;
-        name                    = source.name;
-        description             = source.description;
-        featureInstanceIDs      = source.featureInstanceIDs;
-        featureTypeName         = source.featureTypeName;
-        semanticTypeIdentifiers = source.semanticTypeIdentifiers.clone();
-        rules                   = new ArrayList<>(source.rules);
-    }
-
-    /**
-     * Returns the version number of the Symbology Encoding standard used.
-     * This is fixed to {@value #VERSION} in current Apache SIS release.
-     * This value cannot be changed, unless this style has been unmarshalled
-     * from a XML document that specifies a different version number.
-     *
-     * @return version number of the Symbology Encoding standard used.
-     */
-    public String getVersion() {
-        return version;
-    }
-
-    /**
-     * Returns the name for this style.
-     * This can be any string that uniquely identifies this style within a given canvas.
-     * It is not meant to be human-friendly. For a human-friendly label,
-     * see the {@linkplain Description#getTitle() title} instead.
-     *
-     * @return a name for this style.
-     */
-    public Optional<String> getName() {
-        return Optional.ofNullable(name);
-    }
-
-    /**
-     * Sets a name for this style.
-     * If this method is never invoked, then the default value is absence.
-     *
-     * @param  value  new name for this style.
-     */
-    public void setName(final String value) {
-        name = value;
-    }
-
-    /**
-     * Returns the description of this style.
-     * The returned object is <em>live</em>:
-     * changes in the returned instance will be reflected in this style, and conversely.
-     *
-     * @return information for user interfaces.
-     */
-    public Optional<Description> getDescription() {
-        return Optional.ofNullable(description);
-    }
-
-    /**
-     * Sets a description of this style.
-     * The given instance is stored by reference, it is not cloned.
-     * If this method is never invoked, then the default value is absence.
-     *
-     * @param  value  new information for user interfaces, or {@code null} if none.
-     */
-    public void setDescription(final Description value) {
-        description = value;
-    }
-
-    /**
-     * Returns an identification of feature instances on which to apply the style.
-     * This method enable the possibility to use a feature type style on a given list
-     * of features only, instead of all instances of the feature type.
-     *
-     * @return identification of the feature instances.
-     */
-    public Optional<ResourceId<? super Feature>> getFeatureInstanceIDs() {
-        return Optional.ofNullable(featureInstanceIDs);
-    }
-
-    /**
-     * Sets an identification of feature instances on which to apply the style.
-     * If this method is never invoked, then the default value is absence.
-     *
-     * @param  value  new identification of feature instances, or {@code null} if none.
-     */
-    public void setFeatureInstanceIDs(final ResourceId<? super Feature> value) {
-        featureInstanceIDs = value;
-    }
-
-    /**
-     * Returns the name of the feature type that this style is meant to act upon.
-     * It is allowed to be null but only if the feature type can be inferred by other means,
-     * for example from context or using {@link SemanticType} identifiers.
-     *
-     * @return name of the feature type that this style is meant to act upon.
-     */
-    public Optional<GenericName> getFeatureTypeName() {
-        return Optional.ofNullable(featureTypeName);
-    }
-
-    /**
-     * Sets the name of the feature type that this style is meant to act upon.
-     * If this method is never invoked, then the default value is absence.
-     *
-     * @param  value  new name of the feature type, or {@code null} if none.
-     */
-    public void setFeatureTypeName(final GenericName value) {
-        featureTypeName = value;
-    }
-
-    /**
-     * Returns the most general types of geometry that this style is meant to act upon.
-     * The syntax is currently undefined, but the following values are reserved to indicate
-     * that the style applies to feature with default geometry of specific type:
-     *
-     * <ul>
-     *   <li>{@code generic:point}</li>
-     *   <li>{@code generic:line}</li>
-     *   <li>{@code generic:polygon}</li>
-     *   <li>{@code generic:text}</li>
-     *   <li>{@code generic:raster}</li>
-     *   <li>{@code generic:any}</li>
-     * </ul>
-     *
-     * <p>The returned collection is <em>live</em>:
-     * changes in that collection are reflected into this object, and conversely.</p>
-     *
-     * @return types of geometry that this style is meant to act upon, as a live collection.
-     */
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public Set<SemanticType> semanticTypeIdentifiers() {
-        return semanticTypeIdentifiers;
-    }
-
-    /**
-     * Returns the list of rules contained by this style.
-     * Order matter: the first item in a list will be the
-     * first item plotted and hence appears on the bottom.
-     *
-     * <p>The returned collection is <em>live</em>:
-     * changes in that collection are reflected into this object, and conversely.</p>
-     *
-     * @return ordered list of rules, as a live collection.
-     */
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<Rule<Feature>> rules() {
-        return rules;
-    }
-
-    /**
-     * Returns all properties contained in this class.
-     * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
-     */
-    @Override
-    final Object[] properties() {
-        return new Object[] {name, description, featureInstanceIDs, featureTypeName, semanticTypeIdentifiers, rules};
     }
 
     /**
@@ -320,18 +76,6 @@ public class FeatureTypeStyle extends StyleElement {
     @Override
     public FeatureTypeStyle clone() {
         final var clone = (FeatureTypeStyle) super.clone();
-        clone.selfClone();
         return clone;
     }
-
-    /**
-     * Clones the mutable style fields of this element.
-     */
-    @SuppressWarnings("unchecked")
-    private void selfClone() {
-        if (description != null) description = description.clone();
-        semanticTypeIdentifiers = semanticTypeIdentifiers.clone();
-        rules = new ArrayList<>(rules);
-        rules.replaceAll(Rule::clone);
-    }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
index 094d3b04a0..0f8717a441 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
@@ -21,12 +21,9 @@ import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
-import org.apache.sis.util.ArgumentChecks;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.filter.Literal;
 
 
 /**
@@ -38,25 +35,17 @@ import org.opengis.filter.Literal;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "FillType", propOrder = {
     "graphicFill",
 //  "svgParameter"
 })
 @XmlRootElement(name = "Fill")
-public class Fill extends StyleElement implements Translucent {
-    /**
-     * Literal for a predefined color which can be used as fill color.
-     */
-    public static final Literal<Feature,Color> BLACK, GRAY, WHITE;
-    static {
-        final var FF = FF();
-        BLACK = FF.literal(Color.BLACK);
-        GRAY  = FF.literal(Color.GRAY);
-        WHITE = FF.literal(Color.WHITE);
-    }
-
+public class Fill<R> extends StyleElement<R> implements Translucent<R> {
     /**
      * The image to use for filling the area, or {@code null} if a solid color should be used.
      *
@@ -64,7 +53,7 @@ public class Fill extends StyleElement implements Translucent {
      * @see #setGraphicFill(GraphicFill)
      */
     @XmlElement(name = "GraphicFill")
-    protected GraphicFill graphicFill;
+    protected GraphicFill<R> graphicFill;
 
     /**
      * Color of the interior if it is to be solid-color filled, or {@code null} for the default value.
@@ -76,7 +65,7 @@ public class Fill extends StyleElement implements Translucent {
      * @see #getColor()
      * @see #setColor(Expression)
      */
-    protected Expression<Feature,Color> color;
+    protected Expression<R,Color> color;
 
     /**
      * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
@@ -87,48 +76,22 @@ public class Fill extends StyleElement implements Translucent {
      * @see #getOpacity()
      * @see #setOpacity(Expression)
      */
-    protected Expression<Feature, ? extends Number> opacity;
+    protected Expression<R, ? extends Number> opacity;
 
     /**
-     * Returns the opacity of the alpha value of the given color.
-     * If the color is totally opaque, then this method returns {@code null}.
-     *
-     * @param  color  color from which to get the opacity.
-     * @return opacity derived from the alpha value of the color, or {@code null} if totally opaque.
+     * For JAXB unmarshalling only.
      */
-    static Expression<Feature, ? extends Number> opacity(final Color color) {
-        final int alpha = color.getAlpha();
-        return (alpha != 255) ? literal(alpha / 256d) : null;
-        // Divide by 256 instead of 255 in order to get round numbers for alpha values 64, 128, etc.
+    private Fill() {
+        // Thread-local factory will be used.
     }
 
     /**
      * Creates an opaque fill initialized to the gray color.
-     */
-    public Fill() {
-    }
-
-    /**
-     * Creates a fill initialized to the given color.
-     * The opacity is derived from the alpha value of the given color.
      *
-     * @param  color  the initial color.
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Fill(Color color) {
-        ArgumentChecks.ensureNonNull("color", color);
-        if ((opacity = opacity(color)) != null) {
-            color = new Color(color.getRGB() | 0xFF000000);
-        }
-        this.color = literal(color);
-    }
-
-    /**
-     * Creates an opaque fill initialized to the specified color.
-     *
-     * @param  color  the initial color.
-     */
-    Fill(final Expression<Feature,Color> color) {
-        this.color = color;
+    public Fill(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -137,7 +100,7 @@ public class Fill extends StyleElement implements Translucent {
      *
      * @param  source  the object to copy.
      */
-    public Fill(final Fill source) {
+    public Fill(final Fill<R> source) {
         super(source);
         graphicFill = source.graphicFill;
         color       = source.color;
@@ -155,7 +118,7 @@ public class Fill extends StyleElement implements Translucent {
      *
      * @see Stroke#getGraphicFill()
      */
-    public Optional<GraphicFill> getGraphicFill() {
+    public Optional<GraphicFill<R>> getGraphicFill() {
         return Optional.ofNullable(graphicFill);
     }
 
@@ -168,7 +131,7 @@ public class Fill extends StyleElement implements Translucent {
      *
      * @see Stroke#setGraphicFill(GraphicFill)
      */
-    public void setGraphicFill(final GraphicFill value) {
+    public void setGraphicFill(final GraphicFill<R> value) {
         graphicFill = value;
     }
 
@@ -180,9 +143,9 @@ public class Fill extends StyleElement implements Translucent {
      *
      * @see Stroke#getColor()
      */
-    public Expression<Feature,Color> getColor() {
+    public Expression<R,Color> getColor() {
         final var value = color;
-        return (value != null) ? value : GRAY;
+        return (value != null) ? value : factory.gray;
     }
 
     /**
@@ -197,13 +160,31 @@ public class Fill extends StyleElement implements Translucent {
      *
      * @see Stroke#setColor(Expression)
      */
-    public void setColor(final Expression<Feature,Color> value) {
+    public void setColor(final Expression<R,Color> value) {
         color = value;
         if (value != null) {
             graphicFill = null;
         }
     }
 
+    /**
+     * Sets the color and opacity together.
+     * The opacity is derived from the alpha value of the given color.
+     *
+     * @param  value  new color and opacity, or {@code null} for resetting the defaults.
+     */
+    public void setColorAndOpacity(Color value) {
+        if (value  == null) {
+            color   = null;
+            opacity = null;
+        } else {
+            if ((opacity = opacity(value)) != null) {
+                value = new Color(value.getRGB() | 0xFF000000);
+            }
+            color = literal(value);
+        }
+    }
+
     /**
      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
@@ -215,7 +196,7 @@ public class Fill extends StyleElement implements Translucent {
      * @see RasterSymbolizer#getOpacity()
      */
     @Override
-    public Expression<Feature, ? extends Number> getOpacity() {
+    public Expression<R, ? extends Number> getOpacity() {
         return defaultToOne(opacity);
     }
 
@@ -227,7 +208,7 @@ public class Fill extends StyleElement implements Translucent {
      * @param  value  new level of translucency, or {@code null} for resetting the default value.
      */
     @Override
-    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+    public void setOpacity(final Expression<R, ? extends Number> value) {
         opacity = value;
     }
 
@@ -252,8 +233,8 @@ public class Fill extends StyleElement implements Translucent {
      * @return deep clone of all style elements.
      */
     @Override
-    public Fill clone() {
-        final var clone = (Fill) super.clone();
+    public Fill<R> clone() {
+        final var clone = (Fill<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
index d5bfae3557..ea91e8d00c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
@@ -22,9 +22,7 @@ import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.filter.Literal;
 
 
 /**
@@ -35,21 +33,14 @@ import org.opengis.filter.Literal;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "FontType")
 @XmlRootElement(name = "Font")
-public class Font extends StyleElement {
-    /**
-     * The "normal" literal, used for default style and weight.
-     */
-    private static final Literal<Feature,String> NORMAL = literal("normal");
-
-    /**
-     * The default font size.
-     */
-    private static final Literal<Feature,Double> DEFAULT_SIZE = literal(10.0);
-
+public class Font<R> extends StyleElement<R> {
     /**
      * Family names of the font to use, in preference order.
      *
@@ -57,7 +48,7 @@ public class Font extends StyleElement {
      *
      * @see #family()
      */
-    private List<Expression<Feature,String>> family;
+    private List<Expression<R,String>> family;
 
     /**
      * Style (normal or italic) to use for a font.
@@ -67,7 +58,7 @@ public class Font extends StyleElement {
      * @see #getStyle()
      * @see #setStyle(Expression)
      */
-    protected Expression<Feature,String> style;
+    protected Expression<R,String> style;
 
     /**
      * Amount of weight or boldness to use for a font.
@@ -77,7 +68,7 @@ public class Font extends StyleElement {
      * @see #getWeight()
      * @see #setWeight(Expression)
      */
-    protected Expression<Feature,String> weight;
+    protected Expression<R,String> weight;
 
     /**
      * Size (in pixels) to use for the font.
@@ -87,12 +78,22 @@ public class Font extends StyleElement {
      * @see #getSize()
      * @see #setSize(Expression)
      */
-    protected Expression<Feature, ? extends Number> size;
+    protected Expression<R, ? extends Number> size;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private Font() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a font initialized to normal style, normal weight and a size of 10 pixels.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Font() {
+    public Font(final StyleFactory<R> factory) {
+        super(factory);
         family = new ArrayList<>();
     }
 
@@ -102,7 +103,7 @@ public class Font extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public Font(final Font source) {
+    public Font(final Font<R> source) {
         super(source);
         family = new ArrayList<>(source.family);
         style  = source.style;
@@ -120,7 +121,7 @@ public class Font extends StyleElement {
      * @return the family names in preference order, as a live collection.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<Expression<Feature,String>> family() {
+    public List<Expression<R,String>> family() {
         return family;
     }
 
@@ -130,9 +131,9 @@ public class Font extends StyleElement {
      *
      * @return style to use for a font.
      */
-    public Expression<Feature,String> getStyle() {
+    public Expression<R,String> getStyle() {
         final var value = style;
-        return (value != null) ? value : NORMAL;
+        return (value != null) ? value : factory.normal;
     }
 
     /**
@@ -141,7 +142,7 @@ public class Font extends StyleElement {
      *
      * @param  value  new style to use for a font, or {@code null} for resetting the default value.
      */
-    public void setStyle(final Expression<Feature,String> value) {
+    public void setStyle(final Expression<R,String> value) {
         style = value;
     }
 
@@ -151,9 +152,9 @@ public class Font extends StyleElement {
      *
      * @return amount of weight or boldness to use for a font.
      */
-    public Expression<Feature,String> getWeight() {
+    public Expression<R,String> getWeight() {
         final var value = weight;
-        return (value != null) ? value : NORMAL;
+        return (value != null) ? value : factory.normal;
     }
 
     /**
@@ -162,7 +163,7 @@ public class Font extends StyleElement {
      *
      * @param  value  new amount of weight to use for a font, or {@code null} for resetting the default value.
      */
-    public void setWeight(final Expression<Feature,String> value) {
+    public void setWeight(final Expression<R,String> value) {
         weight = value;
     }
 
@@ -171,9 +172,9 @@ public class Font extends StyleElement {
      *
      * @return size (in pixels) to use for the font.
      */
-    public Expression<Feature, ? extends Number> getSize() {
+    public Expression<R, ? extends Number> getSize() {
         final var value = size;
-        return (value != null) ? value : DEFAULT_SIZE;
+        return (value != null) ? value : factory.ten;
     }
 
     /**
@@ -183,7 +184,7 @@ public class Font extends StyleElement {
      *
      * @param  value  new size to use for the font, or {@code null} for resetting the default value.
      */
-    public void setSize(final Expression<Feature, ? extends Number> value) {
+    public void setSize(final Expression<R, ? extends Number> value) {
         size = value;
     }
 
@@ -208,8 +209,8 @@ public class Font extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public Font clone() {
-        final var clone = (Font) super.clone();
+    public Font<R> clone() {
+        final var clone = (Font<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
index f3a6139c61..4864a81be2 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
@@ -24,7 +24,6 @@ import jakarta.xml.bind.annotation.XmlElements;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -38,7 +37,10 @@ import org.opengis.filter.Expression;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "GraphicType", propOrder = {
     "graphicalSymbols",
@@ -49,14 +51,7 @@ import org.opengis.filter.Expression;
     "displacement"
 })
 @XmlRootElement(name = "Graphic")
-public class Graphic extends StyleElement implements Translucent {
-    /**
-     * The default mark size, which is 6 pixels according the OGC 05-077r4 standard.
-     *
-     * @see #getSize()
-     */
-    private static final Expression<Feature,Double> DEFAULT_SIZE = literal(6.0);
-
+public class Graphic<R> extends StyleElement<R> implements Translucent<R> {
     /**
      * List of external image files or marks that comprise this graphic.
      * All elements of the list must be instances of either {@link Mark} or {@link ExternalGraphic}.
@@ -68,7 +63,7 @@ public class Graphic extends StyleElement implements Translucent {
         @XmlElement(name = "Mark", type = Mark.class),
         @XmlElement(name = "ExternalGraphic", type = ExternalGraphic.class)
     })
-    private List<GraphicalSymbol> graphicalSymbols;
+    private List<GraphicalSymbol<R>> graphicalSymbols;
 
     /**
      * Level of translucency as a floating point number, or {@code null} for the default value.
@@ -79,7 +74,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @todo Needs a JAXB adapter to {@code ParameterValueType}.
      */
     @XmlElement(name = "Opacity")
-    protected Expression<Feature, ? extends Number> opacity;
+    protected Expression<R, ? extends Number> opacity;
 
     /**
      * Absolute size of the graphic as a floating point number, or {@code null} for the default value.
@@ -90,7 +85,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @todo Needs a JAXB adapter to {@code ParameterValueType}.
      */
     @XmlElement(name = "Size")
-    protected Expression<Feature, ? extends Number> size;
+    protected Expression<R, ? extends Number> size;
 
     /**
      * Rotation angle of the graphic when it is drawn, or {@code null} for the default value.
@@ -101,7 +96,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @todo Needs a JAXB adapter to {@code ParameterValueType}.
      */
     @XmlElement(name = "Rotation")
-    protected Expression<Feature, ? extends Number> rotation;
+    protected Expression<R, ? extends Number> rotation;
 
     /**
      * Location to use for anchoring the graphic to the geometry, or {@code null} for lazily constructed default.
@@ -110,7 +105,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @see #setAnchorPoint(AnchorPoint)
      */
     @XmlElement(name = "AnchorPoint")
-    protected AnchorPoint anchorPoint;
+    protected AnchorPoint<R> anchorPoint;
 
     /**
      * Displacement from the "hot-spot" point, or {@code null} for lazily constructed default.
@@ -119,12 +114,22 @@ public class Graphic extends StyleElement implements Translucent {
      * @see #setDisplacement(Displacement)
      */
     @XmlElement(name = "Displacement")
-    protected Displacement displacement;
+    protected Displacement<R> displacement;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private Graphic() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a graphic initialized to opaque default mark, default size and no rotation.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Graphic() {
+    public Graphic(final StyleFactory<R> factory) {
+        super(factory);
         graphicalSymbols = new ArrayList<>();
     }
 
@@ -134,7 +139,7 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @param  source  the object to copy.
      */
-    public Graphic(final Graphic source) {
+    public Graphic(final Graphic<R> source) {
         super(source);
         graphicalSymbols = new ArrayList<>(source.graphicalSymbols);
         opacity          = source.opacity;
@@ -160,7 +165,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @return list of marks or external graphics, as a live collection.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<GraphicalSymbol> graphicalSymbols() {
+    public List<GraphicalSymbol<R>> graphicalSymbols() {
         return graphicalSymbols;
     }
 
@@ -175,7 +180,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @see RasterSymbolizer#getOpacity()
      */
     @Override
-    public Expression<Feature, ? extends Number> getOpacity() {
+    public Expression<R, ? extends Number> getOpacity() {
         return defaultToOne(opacity);
     }
 
@@ -187,7 +192,7 @@ public class Graphic extends StyleElement implements Translucent {
      * @param  value  new level of translucency, or {@code null} for resetting the default value.
      */
     @Override
-    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+    public void setOpacity(final Expression<R, ? extends Number> value) {
         opacity = value;
     }
 
@@ -207,10 +212,10 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @return absolute size of the graphic as a floating point number, or {@code null} for the default value.
      */
-    public Expression<Feature, ? extends Number> getSize() {
+    public Expression<R, ? extends Number> getSize() {
         final var value = size;
         if (value == null && graphicalSymbols.isEmpty()) {
-            return DEFAULT_SIZE;
+            return factory.six;
         }
         return value;
     }
@@ -221,7 +226,7 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @param  value  new absolute size of the graphic, or {@code null} for the default value.
      */
-    public void setSize(final Expression<Feature, ? extends Number> value) {
+    public void setSize(final Expression<R, ? extends Number> value) {
         size = value;
     }
 
@@ -236,7 +241,7 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @return the rotation angle of the graphic when it is drawn.
      */
-    public Expression<Feature, ? extends Number> getRotation() {
+    public Expression<R, ? extends Number> getRotation() {
         return defaultToZero(rotation);
     }
 
@@ -246,7 +251,7 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @param  value  new rotation angle of the graphic, or {@code null} for resetting the default value.
      */
-    public void setRotation(final Expression<Feature, ? extends Number> value) {
+    public void setRotation(final Expression<R, ? extends Number> value) {
         rotation = value;
     }
 
@@ -261,9 +266,9 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @return the anchor point.
      */
-    public AnchorPoint getAnchorPoint() {
+    public AnchorPoint<R> getAnchorPoint() {
         if (anchorPoint == null) {
-            anchorPoint = new AnchorPoint();
+            anchorPoint = factory.createAnchorPoint();
         }
         return anchorPoint;
     }
@@ -275,7 +280,7 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @param  value  new anchor point, or {@code null} for resetting the default value.
      */
-    public void setAnchorPoint(final AnchorPoint value) {
+    public void setAnchorPoint(final AnchorPoint<R> value) {
         anchorPoint = value;
     }
 
@@ -294,9 +299,9 @@ public class Graphic extends StyleElement implements Translucent {
      * @see PolygonSymbolizer#getDisplacement()
      * @see PointPlacement#getDisplacement()
      */
-    public Displacement getDisplacement() {
+    public Displacement<R> getDisplacement() {
         if (displacement == null) {
-            displacement = new Displacement();
+            displacement = factory.createDisplacement();
         }
         return displacement;
     }
@@ -308,7 +313,7 @@ public class Graphic extends StyleElement implements Translucent {
      *
      * @param  value  new displacement from the "hot-spot" point, or {@code null} for resetting the default value.
      */
-    public void setDisplacement(final Displacement value) {
+    public void setDisplacement(final Displacement<R> value) {
         displacement = value;
     }
 
@@ -328,8 +333,8 @@ public class Graphic extends StyleElement implements Translucent {
      * @return deep clone of all style elements.
      */
     @Override
-    public Graphic clone() {
-        final var clone = (Graphic) super.clone();
+    public Graphic<R> clone() {
+        final var clone = (Graphic<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
index 0eb96ea48f..0ef72d6c45 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
@@ -31,11 +31,14 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "GraphicFillType")
 @XmlRootElement(name = "GraphicFill")
-public class GraphicFill extends StyleElement implements GraphicalElement {
+public class GraphicFill<R> extends StyleElement<R> implements GraphicalElement<R> {
     /**
      * The graphic to be repeated, or {@code null} for lazily constructed default.
      * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
@@ -43,12 +46,22 @@ public class GraphicFill extends StyleElement implements GraphicalElement {
      * @see #getGraphic()
      * @see #setGraphic(Graphic)
      */
-    protected Graphic graphic;
+    protected Graphic<R> graphic;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private GraphicFill() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a graphic fill initialized to a default graphic.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public GraphicFill() {
+    public GraphicFill(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -57,7 +70,7 @@ public class GraphicFill extends StyleElement implements GraphicalElement {
      *
      * @param  source  the object to copy.
      */
-    public GraphicFill(final GraphicFill source) {
+    public GraphicFill(final GraphicFill<R> source) {
         super(source);
         graphic = source.graphic;
     }
@@ -73,9 +86,9 @@ public class GraphicFill extends StyleElement implements GraphicalElement {
      */
     @Override
     @XmlElement(name = "Graphic", required = true)
-    public final Graphic getGraphic() {
+    public final Graphic<R> getGraphic() {
         if (graphic == null) {
-            graphic = new Graphic();
+            graphic = factory.createGraphic();
         }
         return graphic;
     }
@@ -90,7 +103,7 @@ public class GraphicFill extends StyleElement implements GraphicalElement {
      * @see GraphicStroke#setGraphic(Graphic)
      */
     @Override
-    public final void setGraphic(final Graphic value) {
+    public final void setGraphic(final Graphic<R> value) {
         graphic = value;
     }
 
@@ -110,8 +123,8 @@ public class GraphicFill extends StyleElement implements GraphicalElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public GraphicFill clone() {
-        final var clone = (GraphicFill) super.clone();
+    public GraphicFill<R> clone() {
+        final var clone = (GraphicFill<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
index 186ade25d6..7d528f9a28 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
@@ -21,7 +21,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -35,7 +34,10 @@ import org.opengis.filter.Expression;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "GraphicStrokeType", propOrder = {
     "graphic",
@@ -43,7 +45,7 @@ import org.opengis.filter.Expression;
     "gap"
 })
 @XmlRootElement(name = "GraphicStroke")
-public class GraphicStroke extends StyleElement implements GraphicalElement {
+public class GraphicStroke<R> extends StyleElement<R> implements GraphicalElement<R> {
     /**
      * The graphic to be repeated, or {@code null} for lazily constructed default.
      * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
@@ -51,7 +53,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      * @see #getGraphic()
      * @see #setGraphic(Graphic)
      */
-    protected Graphic graphic;
+    protected Graphic<R> graphic;
 
     /**
      * How far away the first graphic will be drawn, or {@code null} for the default value.
@@ -60,7 +62,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      * @see #setInitialGap(Expression)
      */
     @XmlElement(name = "InitialGap")
-    protected Expression<Feature, ? extends Number> initialGap;
+    protected Expression<R, ? extends Number> initialGap;
 
     /**
      * Distance between two graphics, or {@code null} for the default value.
@@ -69,12 +71,22 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      * @see #setGap(Expression)
      */
     @XmlElement(name = "Gap")
-    protected Expression<Feature, ? extends Number> gap;
+    protected Expression<R, ? extends Number> gap;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private GraphicStroke() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a graphic stroke initialized to a default graphic and no gap.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public GraphicStroke() {
+    public GraphicStroke(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -83,7 +95,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      *
      * @param  source  the object to copy.
      */
-    public GraphicStroke(final GraphicStroke source) {
+    public GraphicStroke(final GraphicStroke<R> source) {
         super(source);
         graphic    = source.graphic;
         initialGap = source.initialGap;
@@ -101,9 +113,9 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      */
     @Override
     @XmlElement(name = "Graphic", required = true)
-    public final Graphic getGraphic() {
+    public final Graphic<R> getGraphic() {
         if (graphic == null) {
-            graphic = new Graphic();
+            graphic = factory.createGraphic();
         }
         return graphic;
     }
@@ -118,7 +130,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      * @see GraphicFill#setGraphic(Graphic)
      */
     @Override
-    public final void setGraphic(final Graphic value) {
+    public final void setGraphic(final Graphic<R> value) {
         graphic = value;
     }
 
@@ -127,7 +139,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      *
      * @return distance of first graphic relative to the rendering start.
      */
-    public Expression<Feature, ? extends Number> getInitialGap() {
+    public Expression<R, ? extends Number> getInitialGap() {
         return defaultToZero(initialGap);
     }
 
@@ -137,7 +149,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      *
      * @param  value  new distance relative to rendering start, or {@code null} for resetting the default value.
      */
-    public void setInitialGap(final Expression<Feature, ? extends Number> value) {
+    public void setInitialGap(final Expression<R, ? extends Number> value) {
         initialGap = value;
     }
 
@@ -146,7 +158,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      *
      * @return distance between two graphics.
      */
-    public Expression<Feature, ? extends Number> getGap() {
+    public Expression<R, ? extends Number> getGap() {
         return defaultToZero(gap);
     }
 
@@ -156,7 +168,7 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      *
      * @param  value  new distance between two graphics, or {@code null} for resetting the default value.
      */
-    public void setGap(final Expression<Feature, ? extends Number> value) {
+    public void setGap(final Expression<R, ? extends Number> value) {
         gap = value;
     }
 
@@ -176,8 +188,8 @@ public class GraphicStroke extends StyleElement implements GraphicalElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public GraphicStroke clone() {
-        final var clone = (GraphicStroke) super.clone();
+    public GraphicStroke<R> clone() {
+        final var clone = (GraphicStroke<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
index c8aa67cfd4..4b7ff09810 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
@@ -22,9 +22,12 @@ package org.apache.sis.style.se1;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
-public interface GraphicalElement {
+public interface GraphicalElement<R> {
     /**
      * Returns the graphic.
      * The returned object is <em>live</em>:
@@ -32,7 +35,7 @@ public interface GraphicalElement {
      *
      * @return the picture.
      */
-    Graphic getGraphic();
+    Graphic<R> getGraphic();
 
     /**
      * Sets the graphic.
@@ -45,5 +48,5 @@ public interface GraphicalElement {
      *
      * @param  value  new picture, or {@code null} for none or for resetting the default value.
      */
-    void setGraphic(Graphic value);
+    void setGraphic(Graphic<R> value);
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
index 52938ad56c..55901fe87c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
@@ -35,12 +35,14 @@ import org.opengis.metadata.citation.OnlineResource;
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
  *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
  * @see Graphic#graphicalSymbols()
  *
  * @since 1.5
  */
 @XmlTransient
-public abstract class GraphicalSymbol extends StyleElement {
+public abstract class GraphicalSymbol<R> extends StyleElement<R> {
     /**
      * URL to the image or mark, or {@code null} if none.
      *
@@ -70,11 +72,21 @@ public abstract class GraphicalSymbol extends StyleElement {
     @XmlElement(name = "Format")       // Required in ExternalGraphic but not in Mark.
     protected String format;
 
+    /**
+     * For JAXB unmarshalling only.
+     */
+    GraphicalSymbol() {
+        // Thread-local factory will be used.
+    }
+
     /**
      * Creates a new graphical symbol.
      * Intentionally restricted to this package because {@link #properties()} is package-private.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    GraphicalSymbol() {
+    GraphicalSymbol(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -83,7 +95,7 @@ public abstract class GraphicalSymbol extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    GraphicalSymbol(final GraphicalSymbol source) {
+    GraphicalSymbol(final GraphicalSymbol<R> source) {
         super(source);
         onlineResource = source.onlineResource;
         inlineContent  = source.inlineContent;
@@ -163,7 +175,7 @@ public abstract class GraphicalSymbol extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public GraphicalSymbol clone() {
-        return (GraphicalSymbol) super.clone();
+    public GraphicalSymbol<R> clone() {
+        return (GraphicalSymbol<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
index 8bf8e32fe3..1639204ca7 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
@@ -21,7 +21,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -34,14 +33,17 @@ import org.opengis.filter.Expression;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "HaloType", propOrder = {
     "radius",
     "fill"
 })
 @XmlRootElement(name = "Halo")
-public class Halo extends StyleElement {
+public class Halo<R> extends StyleElement<R> {
     /**
      * Radius (in pixels) of the  the halo around the text, or {@code null} for the default value.
      *
@@ -49,7 +51,7 @@ public class Halo extends StyleElement {
      * @see #setRadius(Expression)
      */
     @XmlElement(name = "Radius")
-    protected Expression<Feature, ? extends Number> radius;
+    protected Expression<R, ? extends Number> radius;
 
     /**
      * How the halo area around the text should be filled, or {@code null} for the default value.
@@ -58,12 +60,22 @@ public class Halo extends StyleElement {
      * @see #setFill(Fill)
      */
     @XmlElement(name = "Fill")
-    protected Fill fill;
+    protected Fill<R> fill;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private Halo() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates an halo initialized to a white color and a radius of 1 pixel.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Halo() {
+    public Halo(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -72,7 +84,7 @@ public class Halo extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public Halo(final Halo source) {
+    public Halo(final Halo<R> source) {
         super(source);
         fill   = source.fill;
         radius = source.radius;
@@ -86,7 +98,7 @@ public class Halo extends StyleElement {
      *
      * @return radius (in pixels) of the  the halo around the text.
      */
-    public Expression<Feature, ? extends Number> getRadius() {
+    public Expression<R, ? extends Number> getRadius() {
         return defaultToOne(radius);
     }
 
@@ -96,7 +108,7 @@ public class Halo extends StyleElement {
      *
      * @param  value  new radius (in pixels), or {@code null} for resetting the default value.
      */
-    public void setRadius(Expression<Feature, ? extends Number> value) {
+    public void setRadius(Expression<R, ? extends Number> value) {
         radius = value;
     }
 
@@ -107,9 +119,10 @@ public class Halo extends StyleElement {
      *
      * @return graphic, color and opacity of the text to draw.
      */
-    public Fill getFill() {
+    public Fill<R> getFill() {
         if (fill == null) {
-            fill = new Fill(Fill.WHITE);
+            fill = factory.createFill();
+            fill.setColor(factory.white);
         }
         return fill;
     }
@@ -122,7 +135,7 @@ public class Halo extends StyleElement {
      *
      * @param  value  new fill of the text to draw, or {@code null} for resetting the default value.
      */
-    public void setFill(final Fill value) {
+    public void setFill(final Fill<R> value) {
         fill = value;
     }
 
@@ -142,8 +155,8 @@ public class Halo extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public Halo clone() {
-        final var clone = (Halo) super.clone();
+    public Halo<R> clone() {
+        final var clone = (Halo<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
index 72ef4133c1..40d2ffd087 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
@@ -30,18 +30,31 @@ import jakarta.xml.bind.annotation.XmlSeeAlso;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlSeeAlso({
     PointPlacement.class,
     LinePlacement.class
 })
-public abstract class LabelPlacement extends StyleElement {
+public abstract class LabelPlacement<R> extends StyleElement<R> {
     /**
      * Creates a new label placement.
      * Intentionally restricted to this package because {@link #properties()} is package-private.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
+     */
+    public LabelPlacement(final StyleFactory<R> factory) {
+        super(factory);
+    }
+
+    /**
+     * For JAXB unmarshalling only.
      */
     LabelPlacement() {
+        // Thread-local factory will be used.
     }
 
     /**
@@ -50,7 +63,7 @@ public abstract class LabelPlacement extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    LabelPlacement(final LabelPlacement source) {
+    LabelPlacement(final LabelPlacement<R> source) {
         super(source);
     }
 
@@ -61,7 +74,7 @@ public abstract class LabelPlacement extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public LabelPlacement clone() {
-        return (LabelPlacement) super.clone();
+    public LabelPlacement<R> clone() {
+        return (LabelPlacement<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
index 5fa7f34cf6..c5aaac6d50 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
@@ -27,11 +27,14 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "LegendGraphicType")
 @XmlRootElement(name = "LegendGraphic")
-public class LegendGraphic extends StyleElement implements GraphicalElement {
+public class LegendGraphic<R> extends StyleElement<R> implements GraphicalElement<R> {
     /**
      * The graphic to use as a legend, or {@code null} for lazily constructed default.
      * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
@@ -39,12 +42,22 @@ public class LegendGraphic extends StyleElement implements GraphicalElement {
      * @see #getGraphic()
      * @see #setGraphic(Graphic)
      */
-    protected Graphic graphic;
+    protected Graphic<R> graphic;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private LegendGraphic() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a legend initialized to the default graphic.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public LegendGraphic() {
+    public LegendGraphic(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -53,7 +66,7 @@ public class LegendGraphic extends StyleElement implements GraphicalElement {
      *
      * @param  source  the object to copy.
      */
-    public LegendGraphic(final LegendGraphic source) {
+    public LegendGraphic(final LegendGraphic<R> source) {
         super(source);
         graphic = source.graphic;
     }
@@ -67,9 +80,9 @@ public class LegendGraphic extends StyleElement implements GraphicalElement {
      */
     @Override
     @XmlElement(name = "Graphic", required = true)
-    public final Graphic getGraphic() {
+    public final Graphic<R> getGraphic() {
         if (graphic == null) {
-            graphic = new Graphic();
+            graphic = factory.createGraphic();
         }
         return graphic;
     }
@@ -82,7 +95,7 @@ public class LegendGraphic extends StyleElement implements GraphicalElement {
      * @param  value  new graphic of the legend, or {@code null} for resetting the default value.
      */
     @Override
-    public final void setGraphic(final Graphic value) {
+    public final void setGraphic(final Graphic<R> value) {
         graphic = value;
     }
 
@@ -102,8 +115,8 @@ public class LegendGraphic extends StyleElement implements GraphicalElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public LegendGraphic clone() {
-        final var clone = (LegendGraphic) super.clone();
+    public LegendGraphic<R> clone() {
+        final var clone = (LegendGraphic<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
index 961189eac0..2e6d001ceb 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
@@ -21,7 +21,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -33,7 +32,10 @@ import org.opengis.filter.Expression;
  * @author  Ian Turton (CCG)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "LinePlacementType", propOrder = {
     "perpendicularOffset",
@@ -44,7 +46,7 @@ import org.opengis.filter.Expression;
     "generalizeLine"
 })
 @XmlRootElement(name = "LinePlacement")
-public class LinePlacement extends LabelPlacement {
+public class LinePlacement<R> extends LabelPlacement<R> {
     /**
      * Perpendicular distance away from a line where to draw a label, or {@code null} for the default value.
      *
@@ -52,7 +54,7 @@ public class LinePlacement extends LabelPlacement {
      * @see #setPerpendicularOffset(Expression)
      */
     @XmlElement(name = "PerpendicularOffset")
-    protected Expression<Feature, ? extends Number> perpendicularOffset;
+    protected Expression<R, ? extends Number> perpendicularOffset;
 
     /**
      * Whether the label will be repeatedly drawn along the line, or {@code null} for the default value.
@@ -63,7 +65,7 @@ public class LinePlacement extends LabelPlacement {
      * @todo Needs an adapter from expression to plain boolean.
      */
     @XmlElement(name = "IsRepeated")
-    protected Expression<Feature,Boolean> isRepeated;
+    protected Expression<R,Boolean> isRepeated;
 
     /**
      * How far away the first label will be drawn, or {@code null} for the default value.
@@ -72,7 +74,7 @@ public class LinePlacement extends LabelPlacement {
      * @see #setInitialGap(Expression)
      */
     @XmlElement(name = "InitialGap")
-    protected Expression<Feature, ? extends Number> initialGap;
+    protected Expression<R, ? extends Number> initialGap;
 
     /**
      * Distance between two labels, or {@code null} for the default value.
@@ -81,7 +83,7 @@ public class LinePlacement extends LabelPlacement {
      * @see #setGap(Expression)
      */
     @XmlElement(name = "Gap")
-    protected Expression<Feature, ? extends Number> gap;
+    protected Expression<R, ? extends Number> gap;
 
     /**
      * Whether labels are aligned to the line geometry, or {@code null} for the default value.
@@ -93,7 +95,7 @@ public class LinePlacement extends LabelPlacement {
      * @todo Needs an adapter from expression to plain boolean.
      */
     @XmlElement(name = "IsAligned")
-    protected Expression<Feature,Boolean> isAligned;
+    protected Expression<R,Boolean> isAligned;
 
     /**
      * Whether to allow the geometry to be generalized, or {@code null} for the default value.
@@ -104,12 +106,22 @@ public class LinePlacement extends LabelPlacement {
      * @todo Needs an adapter from expression to plain boolean.
      */
     @XmlElement(name = "GeneralizeLine")
-    protected Expression<Feature,Boolean> generalizeLine;
+    protected Expression<R,Boolean> generalizeLine;
 
     /**
-     * Creates a new line placement.
+     * For JAXB unmarshalling only.
+     */
+    private LinePlacement() {
+        // Thread-local factory will be used.
+    }
+
+    /**
+     * Creates a line placement initialized to no offset, no repetition and no gap.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public LinePlacement() {
+    public LinePlacement(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -118,7 +130,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  source  the object to copy.
      */
-    public LinePlacement(final LinePlacement source) {
+    public LinePlacement(final LinePlacement<R> source) {
         super(source);
         perpendicularOffset = source.perpendicularOffset;
         isRepeated          = source.isRepeated;
@@ -136,7 +148,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @return perpendicular distance away from a line where to draw a label.
      */
-    public Expression<Feature, ? extends Number> getPerpendicularOffset() {
+    public Expression<R, ? extends Number> getPerpendicularOffset() {
         return defaultToZero(perpendicularOffset);
     }
 
@@ -147,7 +159,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  value  new distance to apply for drawing label, or {@code null} for resetting the default value.
      */
-    public void setPerpendicularOffset(final Expression<Feature, ? extends Number> value) {
+    public void setPerpendicularOffset(final Expression<R, ? extends Number> value) {
         perpendicularOffset = value;
     }
 
@@ -158,7 +170,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @return whether the label will be repeatedly drawn along the line.
      */
-    public Expression<Feature,Boolean> isRepeated() {
+    public Expression<R,Boolean> isRepeated() {
         return defaultToFalse(isRepeated);
     }
 
@@ -168,7 +180,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  value  whether the label will be repeated, or {@code null} for resetting the default value.
      */
-    public void setRepeated(final Expression<Feature,Boolean> value) {
+    public void setRepeated(final Expression<R,Boolean> value) {
         isRepeated = value;
     }
 
@@ -177,7 +189,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @return distance of first label relative to the rendering start.
      */
-    public Expression<Feature, ? extends Number> getInitialGap() {
+    public Expression<R, ? extends Number> getInitialGap() {
         return defaultToZero(initialGap);
     }
 
@@ -187,7 +199,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  value  new distance relative to rendering start, or {@code null} for resetting the default value.
      */
-    public void setInitialGap(final Expression<Feature, ? extends Number> value) {
+    public void setInitialGap(final Expression<R, ? extends Number> value) {
         initialGap = value;
     }
 
@@ -196,7 +208,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @return distance between two labels.
      */
-    public Expression<Feature, ? extends Number> getGap() {
+    public Expression<R, ? extends Number> getGap() {
         return defaultToZero(gap);
     }
 
@@ -206,7 +218,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  value  new distance between two labels, or {@code null} for resetting the default value.
      */
-    public void setGap(final Expression<Feature, ? extends Number> value) {
+    public void setGap(final Expression<R, ? extends Number> value) {
         gap = value;
     }
 
@@ -215,7 +227,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @return whether labels are aligned to the line geometry or drawn horizontally.
      */
-    public Expression<Feature,Boolean> isAligned() {
+    public Expression<R,Boolean> isAligned() {
         return defaultToTrue(isAligned);
     }
 
@@ -226,7 +238,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  value  whether labels are aligned to the line geometry, or {@code null} for resetting the default value.
      */
-    public void setAligned(final Expression<Feature,Boolean> value) {
+    public void setAligned(final Expression<R,Boolean> value) {
         isAligned = value;
     }
 
@@ -235,7 +247,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @return whether to allow the geometry to be generalized for label placement.
      */
-    public Expression<Feature,Boolean> getGeneralizeLine() {
+    public Expression<R,Boolean> getGeneralizeLine() {
         return defaultToFalse(generalizeLine);
     }
 
@@ -245,7 +257,7 @@ public class LinePlacement extends LabelPlacement {
      *
      * @param  value whether to allow the geometry to be generalized, or {@code null} for resetting the default value.
      */
-    public void setGeneralizeLine(final Expression<Feature,Boolean> value) {
+    public void setGeneralizeLine(final Expression<R,Boolean> value) {
         generalizeLine = value;
     }
 
@@ -265,8 +277,7 @@ public class LinePlacement extends LabelPlacement {
      * @return deep clone of all style elements.
      */
     @Override
-    public LinePlacement clone() {
-        final var clone = (LinePlacement) super.clone();
-        return clone;
+    public LinePlacement<R> clone() {
+        return (LinePlacement<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
index 351baeba00..2119f059dc 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
@@ -21,7 +21,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -33,14 +32,17 @@ import org.opengis.filter.Expression;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "LineSymbolizerType", propOrder = {
     "stroke",
     "perpendicularOffset"
 })
 @XmlRootElement(name = "LineSymbolizer")
-public class LineSymbolizer extends Symbolizer {
+public class LineSymbolizer<R> extends Symbolizer<R> {
     /**
      * Information about how to draw lines, or {@code null} for lazily constructed default.
      *
@@ -48,7 +50,7 @@ public class LineSymbolizer extends Symbolizer {
      * @see #setStroke(Stroke)
      */
     @XmlElement(name = "Stroke")
-    protected Stroke stroke;
+    protected Stroke<R> stroke;
 
     /**
      * Distance to apply for drawing lines in parallel to geometry, or {@code null} for the default value.
@@ -57,12 +59,22 @@ public class LineSymbolizer extends Symbolizer {
      * @see #setPerpendicularOffset(Expression)
      */
     @XmlElement(name = "PerpendicularOffset")
-    protected Expression<Feature, ? extends Number> perpendicularOffset;
+    protected Expression<R, ? extends Number> perpendicularOffset;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private LineSymbolizer() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a line symbolizer with the default stroke and no perpendicular offset.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public LineSymbolizer() {
+    public LineSymbolizer(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -71,7 +83,7 @@ public class LineSymbolizer extends Symbolizer {
      *
      * @param  source  the object to copy.
      */
-    public LineSymbolizer(final LineSymbolizer source) {
+    public LineSymbolizer(final LineSymbolizer<R> source) {
         super(source);
         stroke = source.stroke;
         perpendicularOffset = source.perpendicularOffset;
@@ -84,9 +96,9 @@ public class LineSymbolizer extends Symbolizer {
      *
      * @return information about how to draw lines.
      */
-    public Stroke getStroke() {
+    public Stroke<R> getStroke() {
         if (stroke == null) {
-            stroke = new Stroke();
+            stroke = factory.createStroke();
         }
         return stroke;
     }
@@ -98,7 +110,7 @@ public class LineSymbolizer extends Symbolizer {
      *
      * @param  value  new information about how to draw lines, or {@code null} for resetting the default value.
      */
-    public void setStroke(final Stroke value) {
+    public void setStroke(final Stroke<R> value) {
         stroke = value;
     }
 
@@ -115,7 +127,7 @@ public class LineSymbolizer extends Symbolizer {
      *
      * @return distance to apply for drawing lines in parallel to the original geometry.
      */
-    public Expression<Feature, ? extends Number> getPerpendicularOffset() {
+    public Expression<R, ? extends Number> getPerpendicularOffset() {
         return defaultToZero(perpendicularOffset);
     }
 
@@ -126,7 +138,7 @@ public class LineSymbolizer extends Symbolizer {
      *
      * @param  value  new distance to apply for drawing lines, or {@code null} for resetting the default value.
      */
-    public void setPerpendicularOffset(final Expression<Feature, ? extends Number> value) {
+    public void setPerpendicularOffset(final Expression<R, ? extends Number> value) {
         perpendicularOffset = value;
     }
 
@@ -146,8 +158,8 @@ public class LineSymbolizer extends Symbolizer {
      * @return deep clone of all style elements.
      */
     @Override
-    public LineSymbolizer clone() {
-        final var clone = (LineSymbolizer) super.clone();
+    public LineSymbolizer<R> clone() {
+        final var clone = (LineSymbolizer<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
index b15f9d202d..bb97c23182 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
@@ -18,15 +18,12 @@ package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.Marshaller;
-import jakarta.xml.bind.Unmarshaller;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.filter.Literal;
 
 
 /**
@@ -41,7 +38,10 @@ import org.opengis.filter.Literal;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "MarkType", propOrder = {
     "wellKnownName",
@@ -53,21 +53,7 @@ import org.opengis.filter.Literal;
     "stroke"
 })
 @XmlRootElement(name = "Mark")
-public class Mark extends GraphicalSymbol {
-    /**
-     * Literal for a predefined well-known name which can be used as a mark.
-     */
-    public static final Literal<Feature,String> SQUARE, CIRCLE, TRIANGLE, STAR, CROSS, X;
-    static {
-        final var FF = FF();
-        SQUARE   = FF.literal("square");
-        CIRCLE   = FF.literal("circle");
-        TRIANGLE = FF.literal("triangle");
-        STAR     = FF.literal("star");
-        CROSS    = FF.literal("cross");
-        X        = FF.literal("x");
-    }
-
+public class Mark<R> extends GraphicalSymbol<R> {
     /**
      * Expression whose value will indicate the symbol to draw, or {@code null} for the default value.
      *
@@ -75,7 +61,7 @@ public class Mark extends GraphicalSymbol {
      * @see #setWellKnownName(Expression)
      */
     @XmlElement(name = "WellKnownName")
-    protected Expression<Feature,String> wellKnownName;
+    protected Expression<R,String> wellKnownName;
 
     /**
      * Information about how the interior of marks should be filled, or {@code null} for no fill.
@@ -86,7 +72,7 @@ public class Mark extends GraphicalSymbol {
      * @see #setFill(Fill)
      */
     @XmlElement(name = "Fill")
-    protected Fill fill;
+    protected Fill<R> fill;
 
     /**
      * Whether {@link #fill} has been explicitly set to some value, including null.
@@ -103,7 +89,7 @@ public class Mark extends GraphicalSymbol {
      * @see #setStroke(Stroke)
      */
     @XmlElement(name = "Stroke")
-    protected Stroke stroke;
+    protected Stroke<R> stroke;
 
     /**
      * Whether {@link #stroke} has been explicitly set to some value, including null.
@@ -118,32 +104,36 @@ public class Mark extends GraphicalSymbol {
      * @see #setMarkIndex(Expression)
      */
     @XmlElement(name = "MarkIndex")
-    protected Expression<Feature,Integer> markIndex;
+    protected Expression<R,Integer> markIndex;
 
     /**
-     * Invoked by JAXB before unmarshalling this mark.
-     * OGC 05-077r4 said that if the fill or the stroke is not specified,
-     * then no fill or stroke should be applied.
+     * Invoked by JAXB before marshalling this mark.
+     * Creates the default fill and stroke if needed.
      */
-    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
-        isFillSet   = true;
-        isStrokeSet = true;
+    private void beforeMarshal(Marshaller caller) {
+        if (fill   == null && !isFillSet)   fill   = factory.createFill();
+        if (stroke == null && !isStrokeSet) stroke = factory.createStroke();
     }
 
     /**
-     * Invoked by JAXB before marshalling this mark.
-     * Creates the default fill and stroke if needed.
+     * For JAXB unmarshalling only. This constructor disables the lazy creation of default values.
+     * This is because OGC 05-077r4 said that if the fill or the stroke is not specified,
+     * then no fill or stroke should be applied.
      */
-    private void beforeMarshal(Marshaller caller) {
-        if (fill   == null && !isFillSet)   fill   = new Fill();
-        if (stroke == null && !isStrokeSet) stroke = new Stroke();
+    private Mark() {
+        // Thread-local factory will be used.
+        isFillSet   = true;
+        isStrokeSet = true;
     }
 
     /**
      * Creates a mark initialized to a gray square with black outline.
      * The size is specified by {@link Graphic#getSize()} and should be 6 pixels by default.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Mark() {
+    public Mark(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -152,7 +142,7 @@ public class Mark extends GraphicalSymbol {
      *
      * @param  source  the object to copy.
      */
-    public Mark(final Mark source) {
+    public Mark(final Mark<R> source) {
         super(source);
         wellKnownName = source.wellKnownName;
         fill          = source.fill;
@@ -174,18 +164,18 @@ public class Mark extends GraphicalSymbol {
      * @see #getOnlineResource()
      * @see #getInlineContent()
      */
-    public Expression<Feature,String> getWellKnownName() {
+    public Expression<R,String> getWellKnownName() {
         final var value = wellKnownName;
-        return (value != null) ? value : SQUARE;
+        return (value != null) ? value : factory.square;
     }
 
     /**
      * Sets the expression whose value will indicate the symbol to draw.
-     * If this method is never invoked, then the default value is {@link #SQUARE}.
+     * If this method is never invoked, then the default value is literal "square".
      *
      * @param  value  well-known name of the mark to render, or {@code null} for resetting the default.
      */
-    public void setWellKnownName(final Expression<Feature,String> value) {
+    public void setWellKnownName(final Expression<R,String> value) {
         wellKnownName = value;
     }
 
@@ -200,10 +190,10 @@ public class Mark extends GraphicalSymbol {
      *
      * @see #getStroke()
      */
-    public Optional<Fill> getFill() {
+    public Optional<Fill<R>> getFill() {
         if (!isFillSet) {
             isFillSet = true;
-            fill = new Fill();
+            fill = factory.createFill();
         }
         return Optional.ofNullable(fill);
     }
@@ -215,7 +205,7 @@ public class Mark extends GraphicalSymbol {
      *
      * @param  value  new information about the fill, or {@code null} for no fill.
      */
-    public void setFill(final Fill value) {
+    public void setFill(final Fill<R> value) {
         isFillSet = true;
         fill = value;
     }
@@ -232,10 +222,10 @@ public class Mark extends GraphicalSymbol {
      *
      * @see #getFill()
      */
-    public Optional<Stroke> getStroke() {
+    public Optional<Stroke<R>> getStroke() {
         if (!isStrokeSet) {
             isStrokeSet = true;
-            stroke = new Stroke();
+            stroke = factory.createStroke();
         }
         return Optional.ofNullable(stroke);
     }
@@ -247,7 +237,7 @@ public class Mark extends GraphicalSymbol {
      *
      * @param  value  new information about styled lines, or {@code null} if lines should not be drawn.
      */
-    public void setStroke(final Stroke value) {
+    public void setStroke(final Stroke<R> value) {
         isStrokeSet = true;
         stroke = value;
     }
@@ -258,7 +248,7 @@ public class Mark extends GraphicalSymbol {
      *
      * @return individual mark to select in a mark archive.
      */
-    public Optional<Expression<Feature,Integer>> getMarkIndex() {
+    public Optional<Expression<R,Integer>> getMarkIndex() {
         return Optional.ofNullable(markIndex);
     }
 
@@ -267,7 +257,7 @@ public class Mark extends GraphicalSymbol {
      *
      * @param  value  new index of an individual mark to select, or {@code null} if none.
      */
-    public void setMarkIndex(final Expression<Feature,Integer> value) {
+    public void setMarkIndex(final Expression<R,Integer> value) {
         markIndex = value;
     }
 
@@ -287,8 +277,8 @@ public class Mark extends GraphicalSymbol {
      * @return deep clone of all style elements.
      */
     @Override
-    public Mark clone() {
-        final var clone = (Mark) super.clone();
+    public Mark<R> clone() {
+        final var clone = (Mark<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
index 6864f4aa30..6efa32f95b 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
@@ -21,7 +21,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -33,7 +32,10 @@ import org.opengis.filter.Expression;
  * @author  Ian Turton (CCG)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "PointPlacementType", propOrder = {
     "anchorPoint",
@@ -41,7 +43,7 @@ import org.opengis.filter.Expression;
     "rotation"
 })
 @XmlRootElement(name = "PointPlacement")
-public class PointPlacement extends LabelPlacement {
+public class PointPlacement<R> extends LabelPlacement<R> {
     /**
      * Location to use for anchoring the label to the point, or {@code null} for lazily constructed default.
      *
@@ -49,7 +51,7 @@ public class PointPlacement extends LabelPlacement {
      * @see #setAnchorPoint(AnchorPoint)
      */
     @XmlElement(name = "AnchorPoint")
-    protected AnchorPoint anchorPoint;
+    protected AnchorPoint<R> anchorPoint;
 
     /**
      * Two-dimensional displacements from the "hot-spot" point, or {@code null} for lazily constructed default.
@@ -58,7 +60,7 @@ public class PointPlacement extends LabelPlacement {
      * @see #setDisplacement(Displacement)
      */
     @XmlElement(name = "Displacement")
-    protected Displacement displacement;
+    protected Displacement<R> displacement;
 
     /**
      * Expression fetching the rotation of the label when it is drawn.
@@ -67,12 +69,22 @@ public class PointPlacement extends LabelPlacement {
      * @see #setRotation(Expression)
      */
     @XmlElement(name = "Rotation")
-    protected Expression<Feature, ? extends Number> rotation;
+    protected Expression<R, ? extends Number> rotation;
 
     /**
-     * Creates a new point placement.
+     * For JAXB unmarshalling only.
+     */
+    private PointPlacement() {
+        // Thread-local factory will be used.
+    }
+
+    /**
+     * Creates a point placement initialized to anchor at the middle and no displacement.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public PointPlacement() {
+    public PointPlacement(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -81,7 +93,7 @@ public class PointPlacement extends LabelPlacement {
      *
      * @param  source  the object to copy.
      */
-    public PointPlacement(final PointPlacement source) {
+    public PointPlacement(final PointPlacement<R> source) {
         super(source);
         anchorPoint  = source.anchorPoint;
         displacement = source.displacement;
@@ -99,9 +111,9 @@ public class PointPlacement extends LabelPlacement {
      *
      * @return the anchor point.
      */
-    public AnchorPoint getAnchorPoint() {
+    public AnchorPoint<R> getAnchorPoint() {
         if (anchorPoint == null) {
-            anchorPoint = new AnchorPoint();
+            anchorPoint = factory.createAnchorPoint();
         }
         return anchorPoint;
     }
@@ -113,7 +125,7 @@ public class PointPlacement extends LabelPlacement {
      *
      * @param  value  new anchor point, or {@code null} for resetting the default value.
      */
-    public void setAnchorPoint(final AnchorPoint value) {
+    public void setAnchorPoint(final AnchorPoint<R> value) {
         anchorPoint = value;
     }
 
@@ -126,9 +138,9 @@ public class PointPlacement extends LabelPlacement {
      * @see Graphic#getDisplacement()
      * @see PolygonSymbolizer#getDisplacement()
      */
-    public Displacement getDisplacement() {
+    public Displacement<R> getDisplacement() {
         if (displacement == null) {
-            displacement = new Displacement();
+            displacement = factory.createDisplacement();
         }
         return displacement;
     }
@@ -140,7 +152,7 @@ public class PointPlacement extends LabelPlacement {
      *
      * @param  value  new displacements from the "hot-spot" point, or {@code null} for resetting the default value.
      */
-    public void setDisplacement(final Displacement value) {
+    public void setDisplacement(final Displacement<R> value) {
         displacement = value;
     }
 
@@ -151,7 +163,7 @@ public class PointPlacement extends LabelPlacement {
      *
      * @return rotation of the label when it is drawn.
      */
-    public Expression<Feature, ? extends Number> getRotation() {
+    public Expression<R, ? extends Number> getRotation() {
         return defaultToZero(rotation);
     }
 
@@ -161,7 +173,7 @@ public class PointPlacement extends LabelPlacement {
      *
      * @param  value  new rotation of the label, or {@code null} for resetting the default value.
      */
-    public void setRotation(final Expression<Feature, ? extends Number> value) {
+    public void setRotation(final Expression<R, ? extends Number> value) {
         rotation = value;
     }
 
@@ -181,8 +193,8 @@ public class PointPlacement extends LabelPlacement {
      * @return deep clone of all style elements.
      */
     @Override
-    public PointPlacement clone() {
-        final var clone = (PointPlacement) super.clone();
+    public PointPlacement<R> clone() {
+        final var clone = (PointPlacement<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
index a27782684c..fa263e821d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
@@ -17,7 +17,6 @@
 package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.Marshaller;
-import jakarta.xml.bind.Unmarshaller;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
@@ -33,11 +32,14 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "PointSymbolizerType")
 @XmlRootElement(name = "PointSymbolizer")
-public class PointSymbolizer extends Symbolizer implements GraphicalElement {
+public class PointSymbolizer<R> extends Symbolizer<R> implements GraphicalElement<R> {
     /**
      * Information about how to draw graphics, or {@code null} for no graphic.
      * If no value has been explicitly set (including null value),
@@ -47,7 +49,7 @@ public class PointSymbolizer extends Symbolizer implements GraphicalElement {
      * @see #setGraphic(Graphic)
      */
     @XmlElement(name = "Graphic")
-    protected Graphic graphic;
+    protected Graphic<R> graphic;
 
     /**
      * Whether {@link #graphic} has been explicitly set to some value, including null.
@@ -56,25 +58,31 @@ public class PointSymbolizer extends Symbolizer implements GraphicalElement {
     private boolean isGraphicSet;
 
     /**
-     * Invoked by JAXB before unmarshalling this legend.
-     * OGC 05-077r4 said that if the graphic is not specified, then none should be used.
+     * Invoked by JAXB before marshalling this point symbolizer.
+     * Creates the default graphic if needed.
      */
-    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
-        isGraphicSet = true;
+    private void beforeMarshal(Marshaller caller) {
+        if (graphic == null && !isGraphicSet) {
+            graphic = factory.createGraphic();
+        }
     }
 
     /**
-     * Invoked by JAXB before marshalling this point symbolizer.
-     * Creates the default graphic if needed.
+     * For JAXB unmarshalling only. This constructor disables the lazy creation of default values.
+     * This is because OGC 05-077r4 said that if the graphic is not specified, then none should be used.
      */
-    private void beforeMarshal(Marshaller caller) {
-        if (graphic == null && !isGraphicSet) graphic = new Graphic();
+    private PointSymbolizer() {
+        // Thread-local factory will be used.
+        isGraphicSet = true;
     }
 
     /**
      * Creates a symbolizer initialized to a default graphic.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public PointSymbolizer() {
+    public PointSymbolizer(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -83,7 +91,7 @@ public class PointSymbolizer extends Symbolizer implements GraphicalElement {
      *
      * @param  source  the object to copy.
      */
-    public PointSymbolizer(final PointSymbolizer source) {
+    public PointSymbolizer(final PointSymbolizer<R> source) {
         super(source);
         graphic = source.graphic;
     }
@@ -98,9 +106,9 @@ public class PointSymbolizer extends Symbolizer implements GraphicalElement {
      * @see #isVisible()
      */
     @Override
-    public Graphic getGraphic() {
+    public Graphic<R> getGraphic() {
         if (graphic == null) {
-            graphic = new Graphic();
+            graphic = factory.createGraphic();
         }
         return graphic;
     }
@@ -114,7 +122,7 @@ public class PointSymbolizer extends Symbolizer implements GraphicalElement {
      * @param  value  new information about how to draw graphics, or {@code null} for none.
      */
     @Override
-    public void setGraphic(final Graphic value) {
+    public void setGraphic(final Graphic<R> value) {
         isGraphicSet = true;
         graphic = value;
     }
@@ -149,8 +157,8 @@ public class PointSymbolizer extends Symbolizer implements GraphicalElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public PointSymbolizer clone() {
-        final var clone = (PointSymbolizer) super.clone();
+    public PointSymbolizer<R> clone() {
+        final var clone = (PointSymbolizer<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
index 719cf6d692..2a3e6acbc2 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
@@ -18,13 +18,11 @@ package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.Marshaller;
-import jakarta.xml.bind.Unmarshaller;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -46,7 +44,10 @@ import org.opengis.filter.Expression;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "PolygonSymbolizerType", propOrder = {
     "fill",
@@ -55,7 +56,7 @@ import org.opengis.filter.Expression;
     "perpendicularOffset"
 })
 @XmlRootElement(name = "PolygonSymbolizer")
-public class PolygonSymbolizer extends Symbolizer {
+public class PolygonSymbolizer<R> extends Symbolizer<R> {
     /**
      * Information about how the interior of polygons should be filled, or {@code null} for no fill.
      * If no value has been explicitly set (including null value),
@@ -65,7 +66,7 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see #setFill(Fill)
      */
     @XmlElement(name = "Fill")
-    protected Fill fill;
+    protected Fill<R> fill;
 
     /**
      * Whether {@link #fill} has been explicitly set to some value, including null.
@@ -82,7 +83,7 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see #setStroke(Stroke)
      */
     @XmlElement(name = "Stroke")
-    protected Stroke stroke;
+    protected Stroke<R> stroke;
 
     /**
      * Whether {@link #stroke} has been explicitly set to some value, including null.
@@ -97,7 +98,7 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see #setDisplacement(Displacement)
      */
     @XmlElement(name = "Displacement")
-    protected Displacement displacement;
+    protected Displacement<R> displacement;
 
     /**
      * Distance to apply for drawing lines in parallel to geometry, or {@code null} for the default value.
@@ -106,31 +107,35 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see #setPerpendicularOffset(Expression)
      */
     @XmlElement(name = "PerpendicularOffset")
-    protected Expression<Feature, ? extends Number> perpendicularOffset;
+    protected Expression<R, ? extends Number> perpendicularOffset;
 
     /**
-     * Invoked by JAXB before unmarshalling this polyogon symbolizer.
-     * OGC 05-077r4 said that if the fill or the stroke is not specified,
-     * then no fill or stroke should be applied.
+     * Invoked by JAXB before marshalling this polyogon symbolizer.
+     * Creates the default fill and stroke if needed.
      */
-    private void beforeUnmarshal(Unmarshaller caller, Object parent) {
-        isFillSet   = true;
-        isStrokeSet = true;
+    private void beforeMarshal(Marshaller caller) {
+        if (fill   == null && !isFillSet)   fill   = factory.createFill();
+        if (stroke == null && !isStrokeSet) stroke = factory.createStroke();
     }
 
     /**
-     * Invoked by JAXB before marshalling this polyogon symbolizer.
-     * Creates the default fill and stroke if needed.
+     * For JAXB unmarshalling only. This constructor disables the lazy creation of default values.
+     * This is because OGC 05-077r4 said that if the fill or the stroke is not specified,
+     * then no fill or stroke should be applied.
      */
-    private void beforeMarshal(Marshaller caller) {
-        if (fill   == null && !isFillSet)   fill   = new Fill();
-        if (stroke == null && !isStrokeSet) stroke = new Stroke();
+    private PolygonSymbolizer() {
+        // Thread-local factory will be used.
+        isFillSet   = true;
+        isStrokeSet = true;
     }
 
     /**
      * Creates a polygon symbolizer initialized to the default fill and default stroke.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public PolygonSymbolizer() {
+    public PolygonSymbolizer(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -139,7 +144,7 @@ public class PolygonSymbolizer extends Symbolizer {
      *
      * @param  source  the object to copy.
      */
-    public PolygonSymbolizer(final PolygonSymbolizer source) {
+    public PolygonSymbolizer(final PolygonSymbolizer<R> source) {
         super(source);
         fill                = source.fill;
         stroke              = source.stroke;
@@ -161,10 +166,10 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see #getStroke()
      * @see #isVisible()
      */
-    public Optional<Fill> getFill() {
+    public Optional<Fill<R>> getFill() {
         if (!isFillSet) {
             isFillSet = true;
-            fill = new Fill();
+            fill = factory.createFill();
         }
         return Optional.ofNullable(fill);
     }
@@ -176,7 +181,7 @@ public class PolygonSymbolizer extends Symbolizer {
      *
      * @param  value  new information about the fill, or {@code null} for no fill.
      */
-    public void setFill(final Fill value) {
+    public void setFill(final Fill<R> value) {
         isFillSet = true;
         fill = value;
     }
@@ -194,10 +199,10 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see #getFill()
      * @see #isVisible()
      */
-    public Optional<Stroke> getStroke() {
+    public Optional<Stroke<R>> getStroke() {
         if (!isStrokeSet) {
             isStrokeSet = true;
-            stroke = new Stroke();
+            stroke = factory.createStroke();
         }
         return Optional.ofNullable(stroke);
     }
@@ -209,7 +214,7 @@ public class PolygonSymbolizer extends Symbolizer {
      *
      * @param  value  new information about styled lines, or {@code null} if lines should not be drawn.
      */
-    public void setStroke(final Stroke value) {
+    public void setStroke(final Stroke<R> value) {
         isStrokeSet = true;
         stroke = value;
     }
@@ -230,9 +235,9 @@ public class PolygonSymbolizer extends Symbolizer {
      * @see Graphic#getDisplacement()
      * @see PointPlacement#getDisplacement()
      */
-    public Displacement getDisplacement() {
+    public Displacement<R> getDisplacement() {
         if (displacement == null) {
-            displacement = new Displacement();
+            displacement = factory.createDisplacement();
         }
         return displacement;
     }
@@ -244,7 +249,7 @@ public class PolygonSymbolizer extends Symbolizer {
      *
      * @param  value  new displacement from the "hot-spot" point, or {@code null} for resetting the default value.
      */
-    public void setDisplacement(final Displacement value) {
+    public void setDisplacement(final Displacement<R> value) {
         displacement = value;
     }
 
@@ -256,7 +261,7 @@ public class PolygonSymbolizer extends Symbolizer {
      *
      * @return distance to apply for drawing lines in parallel to the original geometry.
      */
-    public Expression<Feature, ? extends Number> getPerpendicularOffset() {
+    public Expression<R, ? extends Number> getPerpendicularOffset() {
         return defaultToZero(perpendicularOffset);
     }
 
@@ -267,7 +272,7 @@ public class PolygonSymbolizer extends Symbolizer {
      *
      * @param  value  new distance to apply for drawing lines, or {@code null} for resetting the default value.
      */
-    public void setPerpendicularOffset(final Expression<Feature, ? extends Number> value) {
+    public void setPerpendicularOffset(final Expression<R, ? extends Number> value) {
         perpendicularOffset = value;
     }
 
@@ -302,8 +307,8 @@ public class PolygonSymbolizer extends Symbolizer {
      * @return deep clone of all style elements.
      */
     @Override
-    public PolygonSymbolizer clone() {
-        final var clone = (PolygonSymbolizer) super.clone();
+    public PolygonSymbolizer<R> clone() {
+        final var clone = (PolygonSymbolizer<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
index 0e0b983296..e0c257370e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
@@ -22,7 +22,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 import org.opengis.style.OverlapBehavior;
 
@@ -40,7 +39,10 @@ import org.opengis.style.OverlapBehavior;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "RasterSymbolizerType", propOrder = {
     "opacity",
@@ -52,7 +54,7 @@ import org.opengis.style.OverlapBehavior;
     "imageOutline"
 })
 @XmlRootElement(name = "RasterSymbolizer")
-public class RasterSymbolizer extends Symbolizer implements Translucent {
+public class RasterSymbolizer<R> extends Symbolizer<R> implements Translucent<R> {
     /**
      * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
      * The default value specified by OGC 05-077r4 standard is 1.
@@ -61,7 +63,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see #setOpacity(Expression)
      */
     @XmlElement(name = "Opacity")
-    protected Expression<Feature, ? extends Number> opacity;
+    protected Expression<R, ? extends Number> opacity;
 
     /**
      * Selection of false-color channels for a multi-spectral raster source, or {@code null} if none.
@@ -70,7 +72,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see #setChannelSelection(ChannelSelection)
      */
     @XmlElement(name = "ChannelSelection")
-    protected ChannelSelection channelSelection;
+    protected ChannelSelection<R> channelSelection;
 
     /**
      * Behavior when multiple raster images in a layer overlap each other, or {@code null} if unspecified.
@@ -89,7 +91,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see #setColorMap(ColorMap)
      */
     @XmlElement(name = "ColorMap")
-    protected ColorMap colorMap;
+    protected ColorMap<R> colorMap;
 
     /**
      * Contrast enhancement for the whole image, or {@code null} if none.
@@ -98,7 +100,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see #setContrastEnhancement(ContrastEnhancement)
      */
     @XmlElement(name = "ContrastEnhancement")
-    protected ContrastEnhancement contrastEnhancement;
+    protected ContrastEnhancement<R> contrastEnhancement;
 
     /**
      * Relief shading (or “hill shading”) to apply to the image for a three-dimensional visual effect.
@@ -107,7 +109,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see #setShadedRelief(ShadedRelief)
      */
     @XmlElement(name = "ShadedRelief")
-    protected ShadedRelief shadedRelief;
+    protected ShadedRelief<R> shadedRelief;
 
     /**
      * Line or polygon symbolizer to use for outlining source rasters, or {@code null} if none.
@@ -116,12 +118,22 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see #setImageOutline(Symbolizer)
      */
     @XmlElement(name = "ImageOutline")
-    protected Symbolizer imageOutline;
+    protected Symbolizer<R> imageOutline;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private RasterSymbolizer() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates an initially opaque raster symbolizer with no contrast enhancement, shaded relief or outline.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public RasterSymbolizer() {
+    public RasterSymbolizer(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -130,7 +142,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @param  source  the object to copy.
      */
-    public RasterSymbolizer(final RasterSymbolizer source) {
+    public RasterSymbolizer(final RasterSymbolizer<R> source) {
         super(source);
         opacity             = source.opacity;
         channelSelection    = source.channelSelection;
@@ -152,7 +164,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @see Graphic#getOpacity()
      */
     @Override
-    public Expression<Feature, ? extends Number> getOpacity() {
+    public Expression<R, ? extends Number> getOpacity() {
         return defaultToOne(opacity);
     }
 
@@ -164,7 +176,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @param  value  new level of translucency, or {@code null} for resetting the default value.
      */
     @Override
-    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+    public void setOpacity(final Expression<R, ? extends Number> value) {
         opacity = value;
     }
 
@@ -178,7 +190,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @return the selection of channels.
      */
-    public Optional<ChannelSelection> getChannelSelection() {
+    public Optional<ChannelSelection<R>> getChannelSelection() {
         return Optional.ofNullable(channelSelection);
     }
 
@@ -189,7 +201,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @param  value  new selection of channels, or {@code null} for none.
      */
-    public void setChannelSelection(final ChannelSelection value) {
+    public void setChannelSelection(final ChannelSelection<R> value) {
         channelSelection = value;
     }
 
@@ -224,7 +236,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @return color map for the raster.
      */
-    public Optional<ColorMap> getColorMap() {
+    public Optional<ColorMap<R>> getColorMap() {
         return Optional.ofNullable(colorMap);
     }
 
@@ -235,7 +247,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @param  value  new color map for the raster, or {@code null} if none.
      */
-    public void setColorMap(final ColorMap value) {
+    public void setColorMap(final ColorMap<R> value) {
         colorMap = value;
     }
 
@@ -248,7 +260,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @see SelectedChannel#getContrastEnhancement()
      */
-    public Optional<ContrastEnhancement> getContrastEnhancement() {
+    public Optional<ContrastEnhancement<R>> getContrastEnhancement() {
         return Optional.ofNullable(contrastEnhancement);
     }
 
@@ -261,7 +273,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @see SelectedChannel#setContrastEnhancement(ContrastEnhancement)
      */
-    public void setContrastEnhancement(final ContrastEnhancement value) {
+    public void setContrastEnhancement(final ContrastEnhancement<R> value) {
         contrastEnhancement = value;
     }
 
@@ -272,7 +284,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @return the relief shading to apply.
      */
-    public Optional<ShadedRelief> getShadedRelief() {
+    public Optional<ShadedRelief<R>> getShadedRelief() {
         return Optional.ofNullable(shadedRelief);
     }
 
@@ -283,7 +295,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @param  value  new relief shading to apply, or {@code null} if none.
      */
-    public void setShadedRelief(final ShadedRelief value) {
+    public void setShadedRelief(final ShadedRelief<R> value) {
         shadedRelief = value;
     }
 
@@ -296,7 +308,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @return Line or polygon symbolizer to use for outlining source rasters.
      */
-    public Optional<Symbolizer> getImageOutline() {
+    public Optional<Symbolizer<R>> getImageOutline() {
         return Optional.ofNullable(imageOutline);
     }
 
@@ -307,7 +319,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @param  value  new line or polygon symbolizer to use, or {@code null} if none.
      */
-    public void setImageOutline(final Symbolizer value) {
+    public void setImageOutline(final Symbolizer<R> value) {
         imageOutline = value;
     }
 
@@ -328,8 +340,8 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      * @return deep clone of all style elements.
      */
     @Override
-    public RasterSymbolizer clone() {
-        final var clone = (RasterSymbolizer) super.clone();
+    public RasterSymbolizer<R> clone() {
+        final var clone = (RasterSymbolizer<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
index dd376e43ea..36055b9671 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
@@ -45,7 +45,7 @@ import org.opengis.filter.Filter;
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
  *
- * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) to filter.
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
  *
  * @since 1.5
  */
@@ -60,7 +60,7 @@ import org.opengis.filter.Filter;
     "symbolizers"
 })
 @XmlRootElement(name = "Rule")
-public class Rule<R> extends StyleElement {
+public class Rule<R> extends StyleElement<R> {
     /**
      * Name for this rule, or {@code null} if none.
      *
@@ -77,7 +77,7 @@ public class Rule<R> extends StyleElement {
      * @see #setDescription(Description)
      */
     @XmlElement(name = "Description")
-    protected Description description;
+    protected Description<R> description;
 
     /**
      * Small graphic to draw in a legend window, or {@code null} if none.
@@ -86,7 +86,7 @@ public class Rule<R> extends StyleElement {
      * @see #setLegend(LegendGraphic)
      */
     @XmlElement(name = "LegendGraphic")
-    protected LegendGraphic legend;
+    protected LegendGraphic<R> legend;
 
     /**
      * Filter that will limit the features, or {@code null} if none.
@@ -127,7 +127,7 @@ public class Rule<R> extends StyleElement {
      * @see #symbolizers()
      */
     @XmlElementRef(name = "Symbolizer")
-    private List<Symbolizer> symbolizers;
+    private List<Symbolizer<R>> symbolizers;
 
     /**
      * If the style comes from an external XML file, the original source. Otherwise {@code null}.
@@ -138,9 +138,19 @@ public class Rule<R> extends StyleElement {
     protected OnlineResource onlineSource;
 
     /**
-     * Creates a new rule.
+     * For JAXB unmarshalling only.
      */
-    public Rule() {
+    private Rule() {
+        // Thread-local factory will be used.
+    }
+
+    /**
+     * Creates an initially empty rule.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
+     */
+    public Rule(final StyleFactory<R> factory) {
+        super(factory);
         maxScale = Double.POSITIVE_INFINITY;
         symbolizers = new ArrayList<>();
     }
@@ -192,7 +202,7 @@ public class Rule<R> extends StyleElement {
      *
      * @return information for user interfaces.
      */
-    public Optional<Description> getDescription() {
+    public Optional<Description<R>> getDescription() {
         return Optional.ofNullable(description);
     }
 
@@ -203,7 +213,7 @@ public class Rule<R> extends StyleElement {
      *
      * @param  value  new information for user interfaces, or {@code null} if none.
      */
-    public void setDescription(final Description value) {
+    public void setDescription(final Description<R> value) {
         description = value;
     }
 
@@ -217,7 +227,7 @@ public class Rule<R> extends StyleElement {
      *
      * @return small graphic to draw in a legend window.
      */
-    public Optional<LegendGraphic> getLegend() {
+    public Optional<LegendGraphic<R>> getLegend() {
         return Optional.ofNullable(legend);
     }
 
@@ -228,7 +238,7 @@ public class Rule<R> extends StyleElement {
      *
      * @param  value  new legend graphic, or {@code null} if none.
      */
-    public void setLegend(final LegendGraphic value) {
+    public void setLegend(final LegendGraphic<R> value) {
         legend = value;
     }
 
@@ -436,7 +446,7 @@ public class Rule<R> extends StyleElement {
      * @return the list of symbolizers, as a live collection.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<Symbolizer> symbolizers() {
+    public List<Symbolizer<R>> symbolizers() {
         return symbolizers;
     }
 
@@ -486,7 +496,6 @@ public class Rule<R> extends StyleElement {
      */
     @Override
     public Rule<R> clone() {
-        @SuppressWarnings("unchecked")
         final var clone = (Rule<R>) super.clone();
         clone.selfClone();
         return clone;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
index ec46a85107..48298dd9cb 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
@@ -19,10 +19,8 @@ package org.apache.sis.style.se1;
 import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
-import org.apache.sis.util.ArgumentChecks;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -37,14 +35,17 @@ import org.opengis.filter.Expression;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "SelectedChannelType", propOrder = {
     "sourceChannelName",
     "contrastEnhancement"
 })
 // No root element is specified in OGC 05-077r4.
-public class SelectedChannel extends StyleElement {
+public class SelectedChannel<R> extends StyleElement<R> {
     /**
      * The channel's name, or {@code null} if unspecified.
      *
@@ -54,7 +55,7 @@ public class SelectedChannel extends StyleElement {
      * @todo Needs an adapter from expression to plain string.
      */
     @XmlElement(name = "SourceChannelName", required = true)
-    protected Expression<Feature,String> sourceChannelName;
+    protected Expression<R,String> sourceChannelName;
 
     /**
      * Contrast enhancement applied to the selected channel in isolation, or {@code null} if none.
@@ -63,22 +64,22 @@ public class SelectedChannel extends StyleElement {
      * @see #setContrastEnhancement(ContrastEnhancement)
      */
     @XmlElement(name = "ContrastEnhancement")
-    protected ContrastEnhancement contrastEnhancement;
+    protected ContrastEnhancement<R> contrastEnhancement;
 
     /**
-     * Creates an initially empty selected channel.
+     * For JAXB unmarshalling only.
      */
-    public SelectedChannel() {
+    private SelectedChannel() {
+        // Thread-local factory will be used.
     }
 
     /**
-     * Creates a selected channel for the specified name.
+     * Creates an initially empty selected channel.
      *
-     * @param  name  source channel name.
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public SelectedChannel(final String name) {
-        ArgumentChecks.ensureNonEmpty("name", name);
-        sourceChannelName = literal(name);
+    public SelectedChannel(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -87,7 +88,7 @@ public class SelectedChannel extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public SelectedChannel(final SelectedChannel source) {
+    public SelectedChannel(final SelectedChannel<R> source) {
         super(source);
         sourceChannelName   = source.sourceChannelName;
         contrastEnhancement = source.contrastEnhancement;
@@ -100,7 +101,7 @@ public class SelectedChannel extends StyleElement {
      *
      * @todo Shall never be {@code null}. We need to think about some default value.
      */
-    public Expression<Feature,String> getSourceChannelName() {
+    public Expression<R,String> getSourceChannelName() {
         return sourceChannelName;
     }
 
@@ -109,7 +110,7 @@ public class SelectedChannel extends StyleElement {
      *
      * @param  value  the channel's name, or {@code null} if unspecified.
      */
-    public void setSourceChannelName(final Expression<Feature,String> value) {
+    public void setSourceChannelName(final Expression<R,String> value) {
         sourceChannelName = value;
     }
 
@@ -122,7 +123,7 @@ public class SelectedChannel extends StyleElement {
      *
      * @see RasterSymbolizer#getContrastEnhancement()
      */
-    public Optional<ContrastEnhancement> getContrastEnhancement() {
+    public Optional<ContrastEnhancement<R>> getContrastEnhancement() {
         return Optional.ofNullable(contrastEnhancement);
     }
 
@@ -135,7 +136,7 @@ public class SelectedChannel extends StyleElement {
      *
      * @see RasterSymbolizer#setContrastEnhancement(ContrastEnhancement)
      */
-    public void setContrastEnhancement(final ContrastEnhancement value) {
+    public void setContrastEnhancement(final ContrastEnhancement<R> value) {
         contrastEnhancement = value;
     }
 
@@ -155,8 +156,8 @@ public class SelectedChannel extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public SelectedChannel clone() {
-        final var clone = (SelectedChannel) super.clone();
+    public SelectedChannel<R> clone() {
+        final var clone = (SelectedChannel<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
index 9bdf2b6641..a01dea558c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
@@ -21,7 +21,6 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -33,20 +32,17 @@ import org.opengis.filter.Expression;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "ShadedReliefType", propOrder = {
     "brightnessOnly",
     "reliefFactor"
 })
 @XmlRootElement(name = "ShadedRelief")
-public class ShadedRelief extends StyleElement {
-    /**
-     * Default value for {@link #getReliefFactor()}.
-     * No standard value is specified by OGC 05-077r4.
-     */
-    private static final Expression<Feature,Double> DEFAULT_VALUE = literal(55.0);
-
+public class ShadedRelief<R> extends StyleElement<R> {
     /**
      * Whether to apply the shading to the image generated so far by other layers.
      *
@@ -56,7 +52,7 @@ public class ShadedRelief extends StyleElement {
      * @todo Needs an adapter from expression to plain boolean.
      */
     @XmlElement(name = "BrightnessOnly")
-    protected Expression<Feature,Boolean> brightnessOnly;
+    protected Expression<R,Boolean> brightnessOnly;
 
     /**
      * Amount of exaggeration to use for the height of the hills, or {@code null} for the default value.
@@ -65,12 +61,22 @@ public class ShadedRelief extends StyleElement {
      * @see #setReliefFactor(Expression)
      */
     @XmlElement(name = "ReliefFactor")
-    protected Expression<Feature, ? extends Number> reliefFactor;
+    protected Expression<R, ? extends Number> reliefFactor;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private ShadedRelief() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a shaded relief initialized to implementation-specific default values.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public ShadedRelief() {
+    public ShadedRelief(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -79,7 +85,7 @@ public class ShadedRelief extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public ShadedRelief(final ShadedRelief source) {
+    public ShadedRelief(final ShadedRelief<R> source) {
         super(source);
         brightnessOnly = source.brightnessOnly;
         reliefFactor   = source.reliefFactor;
@@ -92,7 +98,7 @@ public class ShadedRelief extends StyleElement {
      *
      * @return whether to apply the shading to the image generated so far by other layers.
      */
-    public Expression<Feature,Boolean> isBrightnessOnly() {
+    public Expression<R,Boolean> isBrightnessOnly() {
         return defaultToFalse(brightnessOnly);
     }
 
@@ -102,7 +108,7 @@ public class ShadedRelief extends StyleElement {
      *
      * @param  value  new policy, or {@code null} for resetting the default value.
      */
-    public void setBrightnessOnly(final Expression<Feature,Boolean> value) {
+    public void setBrightnessOnly(final Expression<R,Boolean> value) {
         brightnessOnly = value;
     }
 
@@ -112,9 +118,9 @@ public class ShadedRelief extends StyleElement {
      *
      * @return amount of exaggeration to use for the height of the hills.
      */
-    public Expression<Feature, ? extends Number> getReliefFactor() {
+    public Expression<R, ? extends Number> getReliefFactor() {
         final var value = reliefFactor;
-        return (value != null) ? value : DEFAULT_VALUE;
+        return (value != null) ? value : factory.relief;
     }
 
     /**
@@ -123,7 +129,7 @@ public class ShadedRelief extends StyleElement {
      *
      * @param  value  new amount of exaggeration, or {@code null} for resetting the default value.
      */
-    public void setReliefFactor(final Expression<Feature, ? extends Number> value) {
+    public void setReliefFactor(final Expression<R, ? extends Number> value) {
         reliefFactor = value;
     }
 
@@ -143,8 +149,7 @@ public class ShadedRelief extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public ShadedRelief clone() {
-        final var clone = (ShadedRelief) super.clone();
-        return clone;
+    public ShadedRelief<R> clone() {
+        return (ShadedRelief<R>) super.clone();
     }
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
index c25c3b999e..70f52769f3 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
@@ -21,12 +21,9 @@ import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
-import org.apache.sis.util.ArgumentChecks;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.filter.Literal;
 
 
 /**
@@ -44,7 +41,10 @@ import org.opengis.filter.Literal;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "StrokeType", propOrder = {
     "graphicFill",
@@ -52,37 +52,7 @@ import org.opengis.filter.Literal;
 //  "svgParameter"
 })
 @XmlRootElement(name = "Stroke")
-public class Stroke extends StyleElement implements Translucent {
-    /**
-     * Literal for a predefined join which can be used in strokes.
-     *
-     * @see #getLineJoin()
-     */
-    public static final Literal<Feature,String> JOIN_MITRE, JOIN_ROUND, JOIN_BEVEL;
-
-    /**
-     * Literal for a predefined cap which can be used in strokes.
-     *
-     * @see #getLineCap()
-     */
-    public static final Literal<Feature,String> CAP_BUTT, CAP_ROUND, CAP_SQUARE;
-
-    /**
-     * Literal for the default dash offset.
-     */
-    private static final Literal<Feature,Integer> ZERO;
-
-    static {
-        final var FF = FF();
-        ZERO       = FF.literal(0);
-        JOIN_MITRE = FF.literal("mitre");
-        JOIN_ROUND = FF.literal("round");
-        JOIN_BEVEL = FF.literal("bevel");
-        CAP_BUTT   = FF.literal("butt");
-        CAP_ROUND  = FF.literal("round");
-        CAP_SQUARE = FF.literal("square");
-    }
-
+public class Stroke<R> extends StyleElement<R> implements Translucent<R> {
     /**
      * Graphic for tiling the (thin) area of the line, or {@code null} if none.
      * This property and {@link #graphicStroke} are mutually exclusive.
@@ -91,7 +61,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #setGraphicFill(GraphicFill)
      */
     @XmlElement(name = "GraphicFill")
-    protected GraphicFill graphicFill;
+    protected GraphicFill<R> graphicFill;
 
     /**
      * Graphic to repeat along the path of the lines, or {@code null} if none.
@@ -101,7 +71,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #setGraphicStroke(GraphicStroke)
      */
     @XmlElement(name = "GraphicStroke")
-    protected GraphicStroke graphicStroke;
+    protected GraphicStroke<R> graphicStroke;
 
     /**
      * Color of the line if it is to be solid-color filled, or {@code null} for the default value.
@@ -113,7 +83,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getColor()
      * @see #setColor(Expression)
      */
-    protected Expression<Feature,Color> color;
+    protected Expression<R,Color> color;
 
     /**
      * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
@@ -124,7 +94,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getOpacity()
      * @see #setOpacity(Expression)
      */
-    protected Expression<Feature, ? extends Number> opacity;
+    protected Expression<R, ? extends Number> opacity;
 
     /**
      * Absolute width of the line stroke as a positive floating point number, or {@code null} for the default value.
@@ -135,7 +105,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getWidth()
      * @see #setWidth(Expression)
      */
-    protected Expression<Feature, ? extends Number> width;
+    protected Expression<R, ? extends Number> width;
 
     /**
      * How the various segments of a (thick) line string should be joined, or {@code null} the default value.
@@ -146,7 +116,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getLineJoin()
      * @see #setLineJoin(Expression)
      */
-    protected Expression<Feature,String> lineJoin;
+    protected Expression<R,String> lineJoin;
 
     /**
      * How the beginning and ending segments of a line string will be terminated, or {@code null} the default value.
@@ -157,7 +127,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getLineCap()
      * @see #setLineCap(Expression)
      */
-    protected Expression<Feature,String> lineCap;
+    protected Expression<R,String> lineCap;
 
     /**
      * Dash pattern as a space-separated sequence of numbers, or {@code null} for a solid line.
@@ -167,7 +137,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getDashArray()
      * @see #setDashArray(Expression)
      */
-    protected Expression<Feature,float[]> dashArray;
+    protected Expression<R,float[]> dashArray;
 
     /**
      * Distance offset into the dash array to begin drawing, or {@code null} for the default value.
@@ -178,26 +148,22 @@ public class Stroke extends StyleElement implements Translucent {
      * @see #getDashOffset()
      * @see #setDashOffset(Expression)
      */
-    protected Expression<Feature,Integer> dashOffset;
+    protected Expression<R,Integer> dashOffset;
 
     /**
-     * Creates a stroke initialized to solid line of black opaque color, 1 pixel width.
+     * For JAXB unmarshalling only.
      */
-    public Stroke() {
+    private Stroke() {
+        // Thread-local factory will be used.
     }
 
     /**
-     * Creates a stroke initialized to the given color.
-     * The opacity is derived from the alpha value of the given color.
+     * Creates a stroke initialized to solid line of black opaque color, 1 pixel width.
      *
-     * @param  color  the initial color.
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public Stroke(Color color) {
-        ArgumentChecks.ensureNonNull("color", color);
-        if ((opacity = Fill.opacity(color)) != null) {
-            color = new Color(color.getRGB() | 0xFF000000);
-        }
-        this.color = literal(color);
+    public Stroke(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -206,7 +172,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @param  source  the object to copy.
      */
-    public Stroke(final Stroke source) {
+    public Stroke(final Stroke<R> source) {
         super(source);
         graphicFill   = source.graphicFill;
         graphicStroke = source.graphicStroke;
@@ -231,7 +197,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @see Fill#getGraphicFill()
      */
-    public Optional<GraphicFill> getGraphicFill() {
+    public Optional<GraphicFill<R>> getGraphicFill() {
         return Optional.ofNullable(graphicFill);
     }
 
@@ -247,7 +213,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @see Fill#setGraphicFill(GraphicFill)
      */
-    public void setGraphicFill(final GraphicFill value) {
+    public void setGraphicFill(final GraphicFill<R> value) {
         graphicFill = value;
         if (value != null) {
             graphicStroke = null;
@@ -265,7 +231,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @return graphic to repeat along the path of the lines.
      */
-    public Optional<GraphicStroke> getGraphicStroke() {
+    public Optional<GraphicStroke<R>> getGraphicStroke() {
         return Optional.ofNullable(graphicStroke);
     }
 
@@ -279,7 +245,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @param  value  new graphic to repeat along the path of the lines, or {@code null} if none.
      */
-    public void setGraphicStroke(final GraphicStroke value) {
+    public void setGraphicStroke(final GraphicStroke<R> value) {
         graphicStroke = value;
         if (value != null) {
             graphicFill = null;
@@ -295,9 +261,9 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @see Fill#getColor()
      */
-    public Expression<Feature,Color> getColor() {
+    public Expression<R,Color> getColor() {
         final var value = color;
-        return (value != null) ? value : Fill.BLACK;
+        return (value != null) ? value : factory.black;
     }
 
     /**
@@ -312,7 +278,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @see Fill#setColor(Expression)
      */
-    public void setColor(final Expression<Feature,Color> value) {
+    public void setColor(final Expression<R,Color> value) {
         color = value;
         if (value != null) {
             graphicFill   = null;
@@ -320,6 +286,24 @@ public class Stroke extends StyleElement implements Translucent {
         }
     }
 
+    /**
+     * Sets the color and opacity together.
+     * The opacity is derived from the alpha value of the given color.
+     *
+     * @param  value  new color and opacity, or {@code null} for resetting the defaults.
+     */
+    public void setColorAndOpacity(Color value) {
+        if (value  == null) {
+            color   = null;
+            opacity = null;
+        } else {
+            if ((opacity = opacity(value)) != null) {
+                value = new Color(value.getRGB() | 0xFF000000);
+            }
+            color = literal(value);
+        }
+    }
+
     /**
      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
@@ -331,7 +315,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @see RasterSymbolizer#getOpacity()
      */
     @Override
-    public Expression<Feature, ? extends Number> getOpacity() {
+    public Expression<R, ? extends Number> getOpacity() {
         return defaultToOne(opacity);
     }
 
@@ -343,7 +327,7 @@ public class Stroke extends StyleElement implements Translucent {
      * @param  value  new level of translucency, or {@code null} for resetting the default value.
      */
     @Override
-    public void setOpacity(final Expression<Feature, ? extends Number> value) {
+    public void setOpacity(final Expression<R, ? extends Number> value) {
         opacity = value;
     }
 
@@ -354,7 +338,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @return absolute width of the line stroke as a positive floating point number.
      */
-    public Expression<Feature, ? extends Number> getWidth() {
+    public Expression<R, ? extends Number> getWidth() {
         return defaultToOne(width);
     }
 
@@ -365,7 +349,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @param  value  new width of the line stroke, or {@code null} for resetting the default value.
      */
-    public void setWidth(final Expression<Feature, ? extends Number> value) {
+    public void setWidth(final Expression<R, ? extends Number> value) {
         width = value;
     }
 
@@ -375,19 +359,19 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @return how segments of a (thick) line string should be joined.
      */
-    public Expression<Feature,String> getLineJoin() {
+    public Expression<R,String> getLineJoin() {
         final var value = lineJoin;
-        return (value != null) ? value : JOIN_BEVEL;
+        return (value != null) ? value : factory.bevel;
     }
 
     /**
      * Sets how the various segments of a (thick) line string should be joined.
-     * If this method is never invoked, then the default value is {@link #JOIN_BEVEL}.
+     * If this method is never invoked, then the default value is literal "bevel".
      * That default value is implementation-specific.
      *
      * @param  value  how segments of a line string should be joined, or {@code null} for resetting the default value.
      */
-    public void setLineJoin(final Expression<Feature,String> value) {
+    public void setLineJoin(final Expression<R,String> value) {
         lineJoin = value;
     }
 
@@ -397,19 +381,19 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @return how the beginning and ending segments of a line string will be terminated.
      */
-    public Expression<Feature,String> getLineCap() {
+    public Expression<R,String> getLineCap() {
         final var value = lineCap;
-        return (value != null) ? value : CAP_SQUARE;
+        return (value != null) ? value : factory.square;
     }
 
     /**
      * Sets how the beginning and ending segments of a line string will be terminated.
-     * If this method is never invoked, then the default value is {@link #CAP_SQUARE}.
+     * If this method is never invoked, then the default value is literal "square".
      * That default value is implementation-specific.
      *
      * @param  value  how a line string should be terminated, or {@code null} for resetting the default value.
      */
-    public void setLineCap(final Expression<Feature,String> value) {
+    public void setLineCap(final Expression<R,String> value) {
         lineCap = value;
     }
 
@@ -422,7 +406,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @return dash pattern as a space-separated sequence of numbers, or empty for a solid line.
      */
-    public Optional<Expression<Feature,float[]>> getDashArray() {
+    public Optional<Expression<R,float[]>> getDashArray() {
         return Optional.ofNullable(dashArray);
     }
 
@@ -432,7 +416,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @param  value  new dash pattern as a space-separated sequence of numbers, or {@code null} for a solid line.
      */
-    public void setDashArray(final Expression<Feature,float[]> value) {
+    public void setDashArray(final Expression<R,float[]> value) {
         dashArray = value;
     }
 
@@ -441,9 +425,9 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @return distance offset into the dash array to begin drawing.
      */
-    public Expression<Feature,Integer> getDashOffset() {
+    public Expression<R,Integer> getDashOffset() {
         final var value = dashOffset;
-        return (value != null) ? value : ZERO;
+        return (value != null) ? value : factory.zeroAsInt;
     }
 
     /**
@@ -452,7 +436,7 @@ public class Stroke extends StyleElement implements Translucent {
      *
      * @param  value  new distance offset into the dash array, or {@code null} for resetting the default value.
      */
-    public void setDashOffset(final Expression<Feature,Integer> value) {
+    public void setDashOffset(final Expression<R,Integer> value) {
         dashOffset = value;
     }
 
@@ -492,8 +476,8 @@ public class Stroke extends StyleElement implements Translucent {
      * @return deep clone of all style elements.
      */
     @Override
-    public Stroke clone() {
-        final var clone = (Stroke) super.clone();
+    public Stroke<R> clone() {
+        final var clone = (Stroke<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
index 8cd2d62272..47b97e063c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
@@ -17,6 +17,7 @@
 package org.apache.sis.style.se1;
 
 import java.util.List;
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Optional;
 
@@ -32,7 +33,7 @@ import java.util.Optional;
  * @version 1.5
  * @since   1.5
  */
-public class Style extends StyleElement {
+public class Style implements Cloneable {
     /**
      * Name for this style, or {@code null} if none.
      *
@@ -47,7 +48,7 @@ public class Style extends StyleElement {
      * @see #getDescription()
      * @see #setDescription(Description)
      */
-    private Description description;
+    private Description<?> description;
 
     /**
      * Whether this style is the default one.
@@ -70,7 +71,7 @@ public class Style extends StyleElement {
      * @see #getDefaultSpecification()
      * @see #setDefaultSpecification(Symbolizer)
      */
-    private Symbolizer defaultSpecification;
+    private Symbolizer<?> defaultSpecification;
 
     /**
      * Creates an initially empty style.
@@ -86,7 +87,6 @@ public class Style extends StyleElement {
      * @param  source  the object to copy.
      */
     public Style(final Style source) {
-        super(source);
         name        = source.name;
         description = source.description;
         isDefault   = source.isDefault;
@@ -123,7 +123,7 @@ public class Style extends StyleElement {
      *
      * @return information for user interfaces.
      */
-    public Optional<Description> getDescription() {
+    public Optional<Description<?>> getDescription() {
         return Optional.ofNullable(description);
     }
 
@@ -134,7 +134,7 @@ public class Style extends StyleElement {
      *
      * @param  value  new information for user interfaces, or {@code null} if none.
      */
-    public void setDescription(final Description value) {
+    public void setDescription(final Description<?> value) {
         description = value;
     }
 
@@ -175,7 +175,7 @@ public class Style extends StyleElement {
      *
      * @return the default symbolizer to use if no rule return {@code true}.
      */
-    public Optional<Symbolizer> getDefaultSpecification() {
+    public Optional<Symbolizer<?>> getDefaultSpecification() {
         return Optional.ofNullable(defaultSpecification);
     }
 
@@ -184,7 +184,7 @@ public class Style extends StyleElement {
      *
      * @param  value  new default symbolizer to use if no rule return {@code true}.
      */
-    public void setDefaultSpecification(final Symbolizer value) {
+    public void setDefaultSpecification(final Symbolizer<?> value) {
         defaultSpecification = value;
     }
 
@@ -192,11 +192,35 @@ public class Style extends StyleElement {
      * Returns all properties contained in this class.
      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
      */
-    @Override
-    final Object[] properties() {
+    private Object[] properties() {
         return new Object[] {name, description, isDefault, fts, defaultSpecification};
     }
 
+    /**
+     * Returns a hash code value for this object.
+     *
+     * @return a hash code value for this object.
+     */
+    @Override
+    public int hashCode() {
+        return getClass().hashCode() + Arrays.hashCode(properties());
+    }
+
+    /**
+     * Compares this style with the given object for equality.
+     *
+     * @param  obj  the other object to compare with this.
+     * @return whether the other object is equal to this.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        return (obj != null) && (obj.getClass() == getClass()) &&
+                Arrays.equals(properties(), ((Style) obj).properties());
+    }
+
     /**
      * Returns a deep clone of this object. All style elements are cloned,
      * but expressions are not on the assumption that they are immutable.
@@ -205,9 +229,13 @@ public class Style extends StyleElement {
      */
     @Override
     public Style clone() {
-        final var clone = (Style) super.clone();
-        clone.selfClone();
-        return clone;
+        try {
+            final var clone = (Style) super.clone();
+            clone.selfClone();
+            return clone;
+        } catch (CloneNotSupportedException e) {
+            throw new AssertionError(e);
+        }
     }
 
     /**
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
index 6626c50cec..55df55df04 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
@@ -16,15 +16,14 @@
  */
 package org.apache.sis.style.se1;
 
+import java.awt.Color;
 import java.util.Arrays;
 import jakarta.xml.bind.annotation.XmlTransient;
-import org.apache.sis.filter.DefaultFilterFactory;
 import org.opengis.util.InternationalString;
+import org.apache.sis.util.ArgumentChecks;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
-import org.opengis.filter.FilterFactory;
 import org.opengis.filter.Literal;
 
 
@@ -35,107 +34,137 @@ import org.opengis.filter.Literal;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlTransient
-public abstract class StyleElement implements Cloneable {
+public abstract class StyleElement<R> implements Cloneable {
     /**
-     * Version number of the Symbology Encoding Implementation Specification standard currently implemented.
-     * This version number may change in future Apache SIS releases if new standards are published.
-     * The current value is {@value}.
-     */
-    public static final String VERSION = "1.1.0";
-
-    /**
-     * Literal commonly used as a default value.
+     * The factory to use for creating expressions and child elements.
+     * This is typically the same factory than the one used for creating this element.
      *
-     * @see #defaultToFalse(Expression)
+     * @see FeatureTypeStyle#FACTORY
+     * @see CoverageStyle#FACTORY
      */
-    private static final Literal<Feature,Boolean> FALSE = literal(Boolean.FALSE);
+    protected final StyleFactory<R> factory;
 
     /**
-     * Literal commonly used as a default value.
+     * Creates a new style element.
+     * Intentionally restricted to this package because {@link #properties()} is package-private.
      *
-     * @see #defaultToTrue(Expression)
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    private static final Literal<Feature,Boolean> TRUE = literal(Boolean.TRUE);
+    StyleElement(final StyleFactory<R> factory) {
+        ArgumentChecks.ensureNonNull("factory", factory);
+        this.factory = factory;
+    }
 
     /**
-     * Literal commonly used as a default value.
+     * Creates a shallow copy of the given object.
+     * For a deep copy, see {@link #clone()} instead.
      *
-     * @see #defaultToZero(Expression)
+     * @param  source  the object to copy.
      */
-    static final Literal<Feature,Double> LITERAL_ZERO = literal(0.0);
+    StyleElement(final StyleElement<R> source) {
+        factory = source.factory;
+    }
 
     /**
-     * Literal commonly used as a default value.
+     * Creates a style element for XML unmarshalling.
+     * <em>This constructor is unsafe</em> and should be used only by JAXB reflection.
      *
-     * @see #defaultToOne(Expression)
-     */
-    private static final Literal<Feature,Double> LITERAL_ONE = literal(1.0);
-
-    /**
-     * Creates a new style element.
-     * Intentionally restricted to this package because {@link #properties()} is package-private.
+     * @todo Allow the factory to be set according the parent {@link AbstractStyle} being unmarshalled.
+     *       We will need to use {@link ThreadLocal}.
      */
     StyleElement() {
+        factory = null;     // TODO
     }
 
     /**
-     * Creates a shallow copy of the given object.
-     * For a deep copy, see {@link #clone()} instead.
+     * Returns a literal for the given value.
+     * This is a convenience method for use with setter methods that expect an expression.
      *
-     * @param  source  the object to copy.
+     * @param  <E>    type of value.
+     * @param  value  the value for which to return a literal, or {@code null} if none.
+     * @return literal for the given value, or {@code null} if the given value was null.
      */
-    StyleElement(final StyleElement source) {
-        // No property to copy yet, but some may be added in the future.
+    public final <E> Literal<R,E> literal(final E value) {
+        return (value == null) ? null : factory.filterFactory.literal(value);
     }
 
     /**
-     * The factory for creating default expressions.
+     * Returns the given expression if non-null, or literal {@code true} otherwise.
+     * This is a convenience method for the implementation of getter methods when
+     * a default value exists.
+     *
+     * @param  value  the value for which to apply a default value if {@code null}.
+     * @return the given value if non-null, or {@code true} literal otherwise.
      */
-    static FilterFactory<Feature,Object,Object> FF() {
-        return DefaultFilterFactory.forFeatures();
+    protected final Expression<R,Boolean> defaultToTrue(final Expression<R,Boolean> value) {
+        return (value != null) ? value : factory.enabled;
     }
 
     /**
-     * Returns a literal for the given value.
-     * This is used by convenience constructors.
+     * Returns the given expression if non-null, or literal {@code false} otherwise.
+     * This is a convenience method for the implementation of getter methods when
+     * a default value exists.
      *
-     * @param  <E>     type of value.
-     * @param  value   the value for which to return a literal.
-     * @return literal for the given value.
+     * @param  value  the value for which to apply a default value if {@code null}.
+     * @return the given value if non-null, or a {@code false} literal otherwise.
      */
-    static <E> Literal<Feature,E> literal(final E value) {
-        return FF().literal(value);
+    protected final Expression<R,Boolean> defaultToFalse(final Expression<R,Boolean> value) {
+        return (value != null) ? value : factory.disabled;
     }
 
     /**
-     * Returns the given expression if non-null, or {@link #FALSE} otherwise.
+     * Returns the given expression if non-null, or literal {@code 0.0} otherwise.
+     * This is a convenience method for the implementation of getter methods when
+     * a default value exists.
+     *
+     * @param  value  the value for which to apply a default value if {@code null}.
+     * @return the given value if non-null, or {@code 0.0} literal otherwise.
      */
-    static Expression<Feature,Boolean> defaultToFalse(Expression<Feature,Boolean> value) {
-        return (value != null) ? value : FALSE;
+    protected final Expression<R, ? extends Number> defaultToZero(final Expression<R, ? extends Number> value) {
+        return (value != null) ? value : factory.zero;
     }
 
     /**
-     * Returns the given expression if non-null, or {@link #TRUE} otherwise.
+     * Returns the given expression if non-null, or literal {@code 0.5} otherwise.
+     * This is a convenience method for the implementation of getter methods when
+     * a default value exists.
+     *
+     * @param  value  the value for which to apply a default value if {@code null}.
+     * @return the given value if non-null, or {@code 0.5} literal otherwise.
      */
-    static Expression<Feature,Boolean> defaultToTrue(Expression<Feature,Boolean> value) {
-        return (value != null) ? value : TRUE;
+    protected final Expression<R, ? extends Number> defaultToHalf(final Expression<R, ? extends Number> value) {
+        return (value != null) ? value : factory.half;
     }
 
     /**
-     * Returns the given expression if non-null, or {@link #LITERAL_ZERO} otherwise.
+     * Returns the given expression if non-null, or literal {@code 1.0} otherwise.
+     * This is a convenience method for the implementation of getter methods when
+     * a default value exists.
+     *
+     * @param  value  the value for which to apply a default value if {@code null}.
+     * @return the given value if non-null, or {@code 1.0} literal otherwise.
      */
-    static Expression<Feature, ? extends Number> defaultToZero(Expression<Feature, ? extends Number> value) {
-        return (value != null) ? value : LITERAL_ZERO;
+    protected final Expression<R, ? extends Number> defaultToOne(final Expression<R, ? extends Number> value) {
+        return (value != null) ? value : factory.one;
     }
 
     /**
-     * Returns the given expression if non-null, or {@link #LITERAL_ONE} otherwise.
+     * Returns the opacity of the alpha value of the given color.
+     * If the color is totally opaque, then this method returns {@code null}.
+     *
+     * @param  color  color from which to get the opacity.
+     * @return opacity derived from the alpha value of the color, or {@code null} if totally opaque.
      */
-    static Expression<Feature, ? extends Number> defaultToOne(Expression<Feature, ? extends Number> value) {
-        return (value != null) ? value : LITERAL_ONE;
+    final Expression<R, ? extends Number> opacity(final Color color) {
+        final int alpha = color.getAlpha();
+        return (alpha != 255) ? literal(alpha / 256d) : null;
+        // Divide by 256 instead of 255 in order to get round numbers for alpha values 64, 128, etc.
     }
 
     /**
@@ -180,9 +209,10 @@ public abstract class StyleElement implements Cloneable {
      * @return a clone of this element.
      */
     @Override
-    public StyleElement clone() {
+    @SuppressWarnings("unchecked")
+    public StyleElement<R> clone() {
         try {
-            return (StyleElement) super.clone();
+            return (StyleElement<R>) super.clone();
         } catch (CloneNotSupportedException e) {
             throw new AssertionError(e);    // Should never happen since we are cloneable.
         }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java
new file mode 100644
index 0000000000..51137d965e
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java
@@ -0,0 +1,534 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.style.se1;
+
+import java.net.URI;
+import java.awt.Color;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+
+// Branch-dependent imports
+import org.opengis.filter.Literal;
+import org.opengis.filter.FilterFactory;
+import org.opengis.filter.ValueReference;
+
+
+/**
+ * Factory of style elements.
+ * Style factory uses a {@link FilterFactory} instance that depends on the type of data to be styled.
+ * That type of data is specified by the parameterized type {@code <R>}.
+ * The two main types are {@link org.opengis.feature.Feature} and {@link org.apache.sis.coverage.BandedCoverage}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since   1.5
+ */
+public class StyleFactory<R> {
+    /**
+     * The factory to use for creating expressions.
+     */
+    final FilterFactory<R,?,?> filterFactory;
+
+    /**
+     * Literal commonly used as a default value.
+     *
+     * @see StyleElement#defaultToTrue(Expression)
+     * @see StyleElement#defaultToFalse(Expression)
+     */
+    final Literal<R,Boolean> enabled, disabled;
+
+    /**
+     * Literal commonly used as a default value.
+     */
+    final Literal<R,Integer> zeroAsInt;
+
+    /**
+     * Literal commonly used as a default value.
+     *
+     * @see StyleElement#defaultToZero(Expression)
+     * @see StyleElement#defaultToHalf(Expression)
+     * @see StyleElement#defaultToOne(Expression)
+     */
+    final Literal<R,Double> zero, half, one, six, ten;
+
+    /**
+     * Default factor for shaded relief.
+     * This is an arbitrary suggested but not standardized by OGC 05-077r4.
+     */
+    final Literal<R,Double> relief;
+
+    /**
+     * Literal commonly used as a default value.
+     */
+    final Literal<R,String> normal, square, bevel;
+
+    /**
+     * Literal for a predefined color which can be used as fill color.
+     */
+    final Literal<R,Color> black, gray, white;
+
+    /**
+     * An expression for fetching the default geometry.
+     *
+     * @todo According SE specification, the default expression in the context of some symbolizers should
+     *       fetch all geometries, not only a default one. The default seems to depend on the symbolizer type.
+     */
+    final ValueReference<R,?> defaultGeometry;
+
+    /**
+     * Creates a new style factory.
+     *
+     * @param  filterFactory  the factory to use for creating expressions.
+     */
+    public StyleFactory(final FilterFactory<R,?,?> filterFactory) {
+        ArgumentChecks.ensureNonNull("filterFactory", filterFactory);
+        this.filterFactory = filterFactory;
+        enabled   = filterFactory.literal(Boolean.TRUE);
+        disabled  = filterFactory.literal(Boolean.FALSE);
+        zeroAsInt = filterFactory.literal(0);
+        zero      = filterFactory.literal(0.0);
+        half      = filterFactory.literal(0.5);
+        one       = filterFactory.literal(1.0);
+        six       = filterFactory.literal(6.0);
+        ten       = filterFactory.literal(10.0);
+        relief    = filterFactory.literal(55.0);
+        normal    = filterFactory.literal("normal");
+        square    = filterFactory.literal("square");
+        bevel     = filterFactory.literal("bevel");
+        black     = filterFactory.literal(Color.BLACK);
+        gray      = filterFactory.literal(Color.GRAY);
+        white     = filterFactory.literal(Color.WHITE);
+
+        defaultGeometry = filterFactory.property(AttributeConvention.GEOMETRY);
+    }
+
+    /**
+     * Creates a new style factory with the same literals than the given factory.
+     * This constructor shall not be public because it assumes that all literals
+     * are implementations that ignore the type {@code <R>} of data to style,
+     * in which case the unchecked cast is safe.
+     *
+     * @param  source  the style factory to copy.
+     */
+    @SuppressWarnings("unchecked")
+    StyleFactory(final StyleFactory<?> source) {
+        enabled   = (Literal<R,Boolean>) source.enabled;
+        disabled  = (Literal<R,Boolean>) source.disabled;
+        zeroAsInt = (Literal<R,Integer>) source.zeroAsInt;
+        zero      = (Literal<R,Double>)  source.zero;
+        half      = (Literal<R,Double>)  source.half;
+        one       = (Literal<R,Double>)  source.one;
+        six       = (Literal<R,Double>)  source.six;
+        ten       = (Literal<R,Double>)  source.ten;
+        relief    = (Literal<R,Double>)  source.relief;
+        normal    = (Literal<R,String>)  source.normal;
+        square    = (Literal<R,String>)  source.square;
+        bevel     = (Literal<R,String>)  source.bevel;
+        black     = (Literal<R,Color>)   source.black;
+        gray      = (Literal<R,Color>)   source.gray;
+        white     = (Literal<R,Color>)   source.white;
+
+        filterFactory   = null;   // TODO: FilterFactory for coverage is not yet available.
+        defaultGeometry = null;   // Idem.
+    }
+
+    /**
+     * Creates an initially empty rule.
+     * A rule is a set of tendering instructions grouped by feature-property conditions and map scales.
+     *
+     * @return new initially empty rule.
+     */
+    public Rule<R> createRule() {
+        return new Rule<>(this);
+    }
+
+    /**
+     * Creates a point symbolizer initialized to a default graphic.
+     * A point symbolizer is a set of instructions about how to draw a graphic at a point.
+     *
+     * @return new point symbolizer initialized to a default graphic.
+     */
+    public PointSymbolizer<R> createPointSymbolizer() {
+        return new PointSymbolizer<>(this);
+    }
+
+    /**
+     * Creates a line symbolizer with the default stroke and no perpendicular offset.
+     * A line symbolizer is a set of instructions about how to draw on a map the lines of a geometry.
+     *
+     * @return new ine symbolizer with the default stroke and no perpendicular offset.
+     */
+    public LineSymbolizer<R> createLineSymbolizer() {
+        return new LineSymbolizer<>(this);
+    }
+
+    /**
+     * Creates a polygon symbolizer initialized to the default fill and default stroke.
+     * A polygon symbolizer is a set of instructions about how to draw on a map the lines and the interior of polygons.
+     *
+     * @return new polygon symbolizer initialized to the default fill and default stroke.
+     */
+    public PolygonSymbolizer<R> createPolygonSymbolizer() {
+        return new PolygonSymbolizer<>(this);
+    }
+
+    /**
+     * Creates a text symbolizer with default placement and default font.
+     * A text symbolizer is a set of instructions about how to drawn text on a map.
+     * The new symbolizer has no initial label.
+     *
+     * @return new text symbolizer with default placement and default font.
+     *
+     * @see #createTextSymbolizer(String)
+     */
+    public TextSymbolizer<R> createTextSymbolizer() {
+        return new TextSymbolizer<>(this);
+    }
+
+    /**
+     * Creates a text symbolizer initialized with the specified label literal.
+     *
+     * @param  label  initial label literal, or {@code null} if none.
+     * @return new text symbolizer with default placement and default font.
+     */
+    public TextSymbolizer<R> createTextSymbolizer(final String label) {
+        final var s = createTextSymbolizer();
+        s.label = s.literal(label);
+        return s;
+    }
+
+    /**
+     * Creates an initially opaque raster symbolizer with no contrast enhancement, shaded relief or outline.
+     * A raster symbolizer is a set of instructions about how to render raster, matrix or coverage data.
+     *
+     * @return new initially opaque raster symbolizer.
+     */
+    public RasterSymbolizer<R> createRasterSymbolizer() {
+        return new RasterSymbolizer<>(this);
+    }
+
+    /**
+     * Creates an initially empty description.
+     * A description is a set of human-readable information about a style object being defined.
+     *
+     * @return new initially empty description.
+     */
+    public Description<R> createDescription() {
+        return new Description<>(this);
+    }
+
+    /**
+     * Creates a point placement initialized to anchor at the middle and no displacement.
+     * A point placement is a set of instructions about how a text label is positioned relative to a point.
+     *
+     * @return new point placement initialized to anchor at the middle and no displacement.
+     */
+    public PointPlacement<R> createPointPlacement() {
+        return new PointPlacement<>(this);
+    }
+
+    /**
+     * Creates a line placement initialized to no offset, no repetition and no gap.
+     * A line placement is a set of instructions about where and how a text label should be rendered relative to a line.
+     *
+     * @return new line placement initialized to no offset, no repetition and no gap.
+     */
+    public LinePlacement<R> createLinePlacement() {
+        return new LinePlacement<>(this);
+    }
+
+    /**
+     * Creates an anchor point initialized to <var>x</var> = 0.5 and <var>y</var> = 0.5.
+     * An anchor point is the location inside a graphic or label to use as an "anchor"
+     * for positioning it relative to a point.
+     *
+     * @return new anchor point initialized to center.
+     *
+     * @see #createAnchorPoint(double, double)
+     */
+    public AnchorPoint<R> createAnchorPoint() {
+        return new AnchorPoint<>(this);
+    }
+
+    /**
+     * Creates an anchor point initialized to the given position.
+     * This is a convenience method for a frequently used operation.
+     *
+     * @param  x  the initial <var>x</var> position.
+     * @param  y  the initial <var>y</var> position.
+     * @return new anchor point initialized to the given position.
+     */
+    public AnchorPoint<R> createAnchorPoint(final double x, final double y) {
+        final var s = createAnchorPoint();
+        s.anchorPointX = filterFactory.literal(x);
+        s.anchorPointY = filterFactory.literal(y);
+        return s;
+    }
+
+    /**
+     * Creates a displacement initialized to zero offsets.
+     * A displacement is the two-dimensional offsets from the original geometry.
+     *
+     * @return new displacement initialized to zero offsets.
+     *
+     * @see #createDisplacement(double, double)
+     */
+    public Displacement<R> createDisplacement() {
+        return new Displacement<>(this);
+    }
+
+    /**
+     * Creates a displacement initialized to the given offsets.
+     * This is a convenience method for a frequently used operation.
+     *
+     * @param  x  the <var>x</var> displacement.
+     * @param  y  the <var>y</var> displacement.
+     * @return new displacement initialized to the given offsets.
+     */
+    public Displacement<R> createDisplacement(final double x, final double y) {
+        final var s = createDisplacement();
+        s.displacementX = filterFactory.literal(x);
+        s.displacementY = filterFactory.literal(y);
+        return s;
+    }
+
+    /**
+     * Creates a mark initialized to a gray square with black outline.
+     * A mark is a predefined shape that can be drawn at the points of the geometry.
+     *
+     * @return new mark initialized to a gray square with black outline.
+     */
+    public Mark<R> createMark() {
+        return new Mark<>(this);
+    }
+
+    /**
+     * Creates an initially empty external graphic.
+     * An external graphic is a reference to an external file that contains an image of some kind,
+     * such as a PNG or SVG.
+     *
+     * @return new initially empty external graphic.
+     *
+     * @see #createExternalGraphic(URI, String)
+     */
+    public ExternalGraphic<R> createExternalGraphic() {
+        return new ExternalGraphic<>(this);
+    }
+
+    /**
+     * Creates an external graphic initialized to the given URI.
+     *
+     * @param  linkage  URI to the external graphic, or {@code null} if none.
+     * @param  format   MIME type of the external graphic, or {@code null} if unspecified.
+     * @return new external graphic initialized to the given URI.
+     */
+    public ExternalGraphic<R> createExternalGraphic(final URI linkage, final String format) {
+        final var s = createExternalGraphic();
+        s.format = format;
+        if (linkage != null) {
+            s.onlineResource = new DefaultOnlineResource(linkage);
+        }
+        return s;
+    }
+
+    /**
+     * Creates a stroke initialized to solid line of black opaque color, 1 pixel width.
+     * A stroke is a set of instructions about how to draw styled lines.
+     *
+     * @return new stroke initialized to solid line of black opaque color, 1 pixel width.
+     *
+     * @see #createStroke(Color)
+     */
+    public Stroke<R> createStroke() {
+        return new Stroke<>(this);
+    }
+
+    /**
+     * Creates a stroke initialized to the given color and opacity.
+     * The alpha channel of the given color is used for determining the opacity.
+     *
+     * @param  color  the initial color, or {@code null} if none.
+     * @return new stroke initialized to the given color and opacity.
+     */
+    public Stroke<R> createStroke(final Color color) {
+        final var s = createStroke();
+        s.setColorAndOpacity(color);
+        return s;
+    }
+
+    /**
+     * Creates an opaque fill initialized to the gray color.
+     * A fill is a set of instructions about how to fill the interior of polygons.
+     *
+     * @return new opaque fill initialized to the gray color.
+     *
+     * @see #createFill(Color)
+     */
+    public Fill<R> createFill() {
+        return new Fill<>(this);
+    }
+
+    /**
+     * Creates a fill initialized to the given color and opacity.
+     * The alpha channel of the given color is used for determining the opacity.
+     *
+     * @param  color  the initial color, or {@code null} if none.
+     * @return new fill initialized to the given color and opacity.
+     */
+    public Fill<R> createFill(final Color color) {
+        final var s = createFill();
+        s.setColorAndOpacity(color);
+        return s;
+    }
+
+    /**
+     * Creates an halo initialized to a white color and a radius of 1 pixel.
+     * A halo is a fill that are applied to the backgrounds of font glyphs.
+     *
+     * @return new halo initialized to a white color and a radius of 1 pixel.
+     */
+    public Halo<R> createHalo() {
+        return new Halo<>(this);
+    }
+
+    /**
+     * Creates a font initialized to normal style, normal weight and a size of 10 pixels.
+     * A font is the identification of a font of a certain family, style, and size.
+     *
+     * @return new font initialized to normal style, normal weight and a size of 10 pixels.
+     */
+    public Font<R> createFont() {
+        return new Font<>(this);
+    }
+
+    /**
+     * Creates a graphic initialized to opaque default mark, default size and no rotation.
+     * A graphic is a symbol with an inherent shape, color(s), and possibly size.
+     *
+     * @return new graphic initialized to opaque default mark, default size and no rotation.
+     */
+    public Graphic<R> createGraphic() {
+        return new Graphic<>(this);
+    }
+
+    /**
+     * Creates a graphic fill initialized to a default graphic.
+     * A graphic fill is a stipple-fill repeated graphic.
+     *
+     * @return new graphic fill initialized to a default graphic.
+     */
+    public GraphicFill<R> createGraphicFill() {
+        return new GraphicFill<>(this);
+    }
+
+    /**
+     * Creates a graphic stroke initialized to a default graphic and no gap.
+     * A graphic stroke is a repeated-linear-graphic stroke.
+     *
+     * @return new graphic stroke initialized to a default graphic and no gap.
+     */
+    public GraphicStroke<R> createGraphicStroke() {
+        return new GraphicStroke<>(this);
+    }
+
+    /**
+     * Creates a legend initialized to the default graphic.
+     * A legend graphic is a graphic to do displayed in a legend for a rule.
+     *
+     * @return new legend initialized to the default graphic.
+     */
+    public LegendGraphic<R> createLegendGraphic() {
+        return new LegendGraphic<>(this);
+    }
+
+    /**
+     * Creates an initially empty color replacement.
+     * A color replacement defines the replacement of a color in an external graphic.
+     *
+     * @return new initially empty color replacement.
+     */
+    public ColorReplacement<R> createColorReplacement() {
+        return new ColorReplacement<>(this);
+    }
+
+    /**
+     * Creates an initially empty color map.
+     * A color map is the mapping of fixed-numeric pixel values to colors.
+     *
+     * @return new initially empty color map.
+     */
+    public ColorMap<R> createColorMap() {
+        return new ColorMap<>(this);
+    }
+
+    /**
+     * Creates an initially empty channel selection.
+     * A channel selection specifies the false-color channel selection for a multi-spectral raster source.
+     *
+     * @return new initially empty channel selection.
+     */
+    public ChannelSelection<R> createChannelSelection() {
+        return new ChannelSelection<>(this);
+    }
+
+    /**
+     * Creates an initially empty selected channel.
+     * A selected channel is information about a channel to use in a multi-spectral source.
+     *
+     * @return new initially empty selected channel.
+     *
+     * @see #createSelectedChannel(String)
+     */
+    public SelectedChannel<R> createSelectedChannel() {
+        return new SelectedChannel<>(this);
+    }
+
+    /**
+     * Creates a selected channel initialized to the given channel name.
+     *
+     * @param  sourceChannelName  the channel's name, or {@code null} if unspecified.
+     * @return new selected channel for the given name.
+     */
+    public SelectedChannel<R> createSelectedChannel(final String sourceChannelName) {
+        final var s = createSelectedChannel();
+        s.sourceChannelName = s.literal(sourceChannelName);
+        return s;
+    }
+
+    /**
+     * Creates a contrast enhancement initialized to no operation.
+     *
+     * @return new contrast enhancement initialized to no operation.
+     */
+    public ContrastEnhancement<R> createContrastEnhancement() {
+        return new ContrastEnhancement<> (this);
+    }
+
+    /**
+     * Creates a shaded relief initialized to implementation-specific default values.
+     * A shaded relief is a “hill shading” applied to an image for a three-dimensional visual effect.
+     *
+     * @return new shaded relief initialized to implementation-specific default values.
+     */
+    public ShadedRelief<R> createShadedRelief() {
+        return new ShadedRelief<>(this);
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
index f1723cfc2e..743e2d87a1 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
@@ -27,10 +27,6 @@ import jakarta.xml.bind.annotation.XmlAttribute;
 import jakarta.xml.bind.annotation.XmlSeeAlso;
 import org.apache.sis.measure.Units;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.internal.feature.AttributeConvention;
-
-// Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 import org.opengis.filter.ValueReference;
 
@@ -53,7 +49,10 @@ import org.opengis.filter.ValueReference;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "SymbolizerType", propOrder = {
     "name",
@@ -68,16 +67,7 @@ import org.opengis.filter.ValueReference;
     RasterSymbolizer.class
 })
 @XmlRootElement(name = "Symbolizer")
-public abstract class Symbolizer extends StyleElement {
-    /**
-     * An expression for fetching the default geometry.
-     *
-     * @todo According SE specification, the default expression in the context of some symbolizers should
-     *       fetch all geometries, not only a default one. The default seems to depend on the symbolizer type.
-     */
-    private static final ValueReference<Feature,?> DEFAULT_GEOMETRY =
-                            FF().property(AttributeConvention.GEOMETRY);
-
+public abstract class Symbolizer<R> extends StyleElement<R> {
     /**
      * Name for this style, or {@code null} if none.
      *
@@ -94,7 +84,7 @@ public abstract class Symbolizer extends StyleElement {
      * @see #setDescription(Description)
      */
     @XmlElement(name = "Description")
-    protected Description description;
+    protected Description<R> description;
 
     /**
      * Expression fetching the geometry to draw, or {@code null} for the default geometries.
@@ -105,7 +95,7 @@ public abstract class Symbolizer extends StyleElement {
      * @see #setGeometry(Expression)
      */
     @XmlElement(name = "Geometry")
-    protected Expression<Feature,?> geometry;
+    protected Expression<R,?> geometry;
 
     /**
      * Unit of measurement for all lengths inside this symbolizer, or {@code null} for the default value.
@@ -126,10 +116,20 @@ public abstract class Symbolizer extends StyleElement {
     @XmlSchemaType(name = "anyURI")
     protected Unit<?> unit;
 
+    /**
+     * For JAXB unmarshalling only.
+     */
+    Symbolizer() {
+        // Thread-local factory will be used.
+    }
+
     /**
      * Creates a symbolizer initialized to default geometries and pixel unit of measurement.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    protected Symbolizer() {
+    public Symbolizer(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -138,7 +138,7 @@ public abstract class Symbolizer extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    protected Symbolizer(final Symbolizer source) {
+    protected Symbolizer(final Symbolizer<R> source) {
         super(source);
         name        = source.name;
         geometry    = source.geometry;
@@ -175,7 +175,7 @@ public abstract class Symbolizer extends StyleElement {
      *
      * @return information for user interfaces.
      */
-    public Optional<Description> getDescription() {
+    public Optional<Description<R>> getDescription() {
         return Optional.ofNullable(description);
     }
 
@@ -186,7 +186,7 @@ public abstract class Symbolizer extends StyleElement {
      *
      * @param  value  new information for user interfaces, or {@code null} if none.
      */
-    public void setDescription(final Description value) {
+    public void setDescription(final Description<R> value) {
         description = value;
     }
 
@@ -233,9 +233,9 @@ public abstract class Symbolizer extends StyleElement {
      *
      * @return expression fetching the geometry or the coverage to draw.
      */
-    public Expression<Feature,?> getGeometry() {
+    public Expression<R,?> getGeometry() {
         final var value = geometry;
-        return (value != null) ? value : DEFAULT_GEOMETRY;
+        return (value != null) ? value : factory.defaultGeometry;
     }
 
     /**
@@ -248,7 +248,7 @@ public abstract class Symbolizer extends StyleElement {
      *
      * @param  value  new expression fetching the geometry to draw, or {@code null} for resetting the default value.
      */
-    public void setGeometry(final Expression<Feature, ?> value) {
+    public void setGeometry(final Expression<R,?> value) {
         geometry = value;
     }
 
@@ -299,8 +299,8 @@ public abstract class Symbolizer extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public Symbolizer clone() {
-        final var clone = (Symbolizer) super.clone();
+    public Symbolizer<R> clone() {
+        final var clone = (Symbolizer<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
index 4fe9b344f5..6a47d5c873 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
@@ -23,7 +23,6 @@ import jakarta.xml.bind.annotation.XmlElementRef;
 import jakarta.xml.bind.annotation.XmlRootElement;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -38,7 +37,10 @@ import org.opengis.filter.Expression;
  * @author  Chris Dillard (SYS Technologies)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
 @XmlType(name = "TextSymbolizerType", propOrder = {
     "label",
@@ -48,7 +50,7 @@ import org.opengis.filter.Expression;
     "fill"
 })
 @XmlRootElement(name = "TextSymbolizer")
-public class TextSymbolizer extends Symbolizer {
+public class TextSymbolizer<R> extends Symbolizer<R> {
     /**
      * Text to display, or {@code null} if none.
      *
@@ -56,7 +58,7 @@ public class TextSymbolizer extends Symbolizer {
      * @see #setLabel(Expression)
      */
     @XmlElement(name = "Label")
-    protected Expression<Feature,String> label;
+    protected Expression<R,String> label;
 
     /**
      * Font to apply on the text, or {@code null} for lazily constructed default.
@@ -65,7 +67,7 @@ public class TextSymbolizer extends Symbolizer {
      * @see #setFont(Font)
      */
     @XmlElement(name = "Font")
-    protected Font font;
+    protected Font<R> font;
 
     /**
      * Indications about how the text should be placed with respect to the feature geometry.
@@ -75,7 +77,7 @@ public class TextSymbolizer extends Symbolizer {
      * @see #setLabelPlacement(LabelPlacement)
      */
     @XmlElementRef(name = "LabelPlacement")
-    protected LabelPlacement labelPlacement;
+    protected LabelPlacement<R> labelPlacement;
 
     /**
      * Indication about a halo to draw around the text, or {@code null} if none.
@@ -84,7 +86,7 @@ public class TextSymbolizer extends Symbolizer {
      * @see #setHalo(Halo)
      */
     @XmlElement(name = "Halo")
-    protected Halo halo;
+    protected Halo<R> halo;
 
     /**
      * Graphic, color and opacity of the text to draw, or {@code null} for lazily constructed default.
@@ -93,13 +95,23 @@ public class TextSymbolizer extends Symbolizer {
      * @see #setFill(Fill)
      */
     @XmlElement(name = "Fill")
-    protected Fill fill;
+    protected Fill<R> fill;
+
+    /**
+     * For JAXB unmarshalling only.
+     */
+    private TextSymbolizer() {
+        // Thread-local factory will be used.
+    }
 
     /**
      * Creates a text symbolizer with default placement and default font.
      * The new symbolizer has no initial label.
+     *
+     * @param  factory  the factory to use for creating expressions and child elements.
      */
-    public TextSymbolizer() {
+    public TextSymbolizer(final StyleFactory<R> factory) {
+        super(factory);
     }
 
     /**
@@ -108,7 +120,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @param  source  the object to copy.
      */
-    public TextSymbolizer(final TextSymbolizer source) {
+    public TextSymbolizer(final TextSymbolizer<R> source) {
         super(source);
         label          = source.label;
         font           = source.font;
@@ -125,7 +137,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @todo Replace {@code null} by a default expression searching for a default text property in the feature.
      */
-    public Expression<Feature,String> getLabel() {
+    public Expression<R,String> getLabel() {
         return label;
     }
 
@@ -135,7 +147,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @param  value  new text to display, or {@code null} if none.
      */
-    public void setLabel(final Expression<Feature,String> value) {
+    public void setLabel(final Expression<R,String> value) {
         label = value;
     }
 
@@ -146,9 +158,9 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @return font to apply on the text.
      */
-    public Font getFont() {
+    public Font<R> getFont() {
         if (font == null) {
-            font = new Font();
+            font = factory.createFont();
         }
         return font;
     }
@@ -160,7 +172,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @param  value  new font to apply on the text, or {@code null} for resetting the default value.
      */
-    public void setFont(final Font value) {
+    public void setFont(final Font<R> value) {
         font = value;
     }
 
@@ -173,9 +185,9 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @return how the text should be placed with respect to the feature geometry.
      */
-    public LabelPlacement getLabelPlacement() {
+    public LabelPlacement<R> getLabelPlacement() {
         if (labelPlacement == null) {
-            labelPlacement = new PointPlacement();
+            labelPlacement = factory.createPointPlacement();
         }
         return labelPlacement;
     }
@@ -188,7 +200,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @param  value  new indications about text placement, or {@code null} for resetting the default value.
      */
-    public void setLabelPlacement(final LabelPlacement value) {
+    public void setLabelPlacement(final LabelPlacement<R> value) {
         labelPlacement = value;
     }
 
@@ -200,7 +212,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @return indication about a halo to draw around the text.
      */
-    public Optional<Halo> getHalo() {
+    public Optional<Halo<R>> getHalo() {
         return Optional.ofNullable(halo);
     }
 
@@ -211,7 +223,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @param  value  new indication about a halo to draw around the text, or {@code null} if none.
      */
-    public void setHalo(final Halo value) {
+    public void setHalo(final Halo<R> value) {
         halo = value;
     }
 
@@ -222,9 +234,10 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @return graphic, color and opacity of the text to draw.
      */
-    public Fill getFill() {
+    public Fill<R> getFill() {
         if (fill == null) {
-            fill = new Fill(Fill.BLACK);
+            fill = factory.createFill();
+            fill.setColor(factory.black);
         }
         return fill;
     }
@@ -237,7 +250,7 @@ public class TextSymbolizer extends Symbolizer {
      *
      * @param  value  new fill of the text to draw, or {@code null} for resetting the default value.
      */
-    public void setFill(final Fill value) {
+    public void setFill(final Fill<R> value) {
         fill = value;
     }
 
@@ -257,8 +270,8 @@ public class TextSymbolizer extends Symbolizer {
      * @return deep clone of all style elements.
      */
     @Override
-    public TextSymbolizer clone() {
-        final var clone = (TextSymbolizer) super.clone();
+    public TextSymbolizer<R> clone() {
+        final var clone = (TextSymbolizer<R>) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
index ae8d42628b..f8d9e11624 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.style.se1;
 
-import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
 
 
@@ -25,16 +24,19 @@ import org.opengis.filter.Expression;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
- * @since   1.5
+ *
+ * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+ *
+ * @since 1.5
  */
-public interface Translucent {
+public interface Translucent<R> {
     /**
      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
      *
      * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
      */
-    Expression<Feature, ? extends Number> getOpacity();
+    Expression<R, ? extends Number> getOpacity();
 
     /**
      * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
@@ -43,5 +45,5 @@ public interface Translucent {
      *
      * @param  value  new level of translucency, or {@code null} for resetting the default value.
      */
-    void setOpacity(Expression<Feature, ? extends Number> value);
+    void setOpacity(Expression<R, ? extends Number> value);
 }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java
index d70474e0f3..fb73dd6b9a 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/package-info.java
@@ -26,19 +26,17 @@
  * {@link org.apache.sis.style.se1.CoverageStyle}.
  * Those classes include different kinds of {@link org.apache.sis.style.se1.Symbolizer}.
  *
- * @todo Add {@code CoverageStyle}. May require a common parent with {@code FeatureTypeStyle}.
- *
  * <h2>Future evolution</h2>
- * This package defines a XML encoding.
+ * This package defines an XML encoding.
  * It is not an abstract model for sophisticated styling.
  * Apache SIS temporarily uses the classes of the XML encoding as a style API,
  * but a future version may replace this API by a more abstract one.
- * A good candidate may be ISO 19117:2012 — Portrayal.
- * As of 2023, various OGC working groups are also working on new style API.
+ * A good candidate may be <cite>ISO 19117:2012 — Portrayal</cite>.
+ * As of 2023, various OGC working groups are also working on new style APIs.
  * The final form of such API has not yet been settled down.
  *
  * <h2>Synchronization</h2>
- * Classes in this package are not thread-safe.
+ * Unless otherwise specified in the Javadoc, classes in this package are not thread-safe.
  * Synchronization, if desired, must be done by the caller.
  *
  * @author  Johann Sorel (Geomatys)
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
index e2b3d026ee..5198a054f3 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
@@ -39,8 +39,6 @@ import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.storage.MemoryFeatureSet;
 import org.apache.sis.style.se1.FeatureTypeStyle;
-import org.apache.sis.style.se1.LineSymbolizer;
-import org.apache.sis.style.se1.Rule;
 import org.apache.sis.style.se1.Style;
 import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.portrayal.MapItem;
@@ -54,6 +52,7 @@ import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.style.se1.StyleFactory;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.style.se1.Symbolizer;
@@ -82,6 +81,10 @@ import static org.junit.Assert.*;
  * @author Johann Sorel (Geomatys)
  */
 public class SEPortrayerTest extends TestCase {
+    /**
+     * The factory to use for creating style elements.
+     */
+    private final StyleFactory<Feature> factory = FeatureTypeStyle.FACTORY;
 
     private final FilterFactory<Feature,Object,Object> filterFactory;
     private final FeatureSet fishes;
@@ -196,8 +199,8 @@ public class SEPortrayerTest extends TestCase {
     public void testSanity() {
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
-        final var rule = new Rule<Feature>();
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var rule = factory.createRule();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -233,8 +236,8 @@ public class SEPortrayerTest extends TestCase {
 
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
-        final var rule = new Rule<Feature>();
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var rule = factory.createRule();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -262,8 +265,8 @@ public class SEPortrayerTest extends TestCase {
     public void testUserQuery() {
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
-        final var rule = new Rule<Feature>();
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var rule = factory.createRule();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -301,8 +304,8 @@ public class SEPortrayerTest extends TestCase {
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
         fts.setFeatureTypeName(Names.createLocalName(null, null, "boat"));
-        final var rule = new Rule<Feature>();
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var rule = factory.createRule();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -332,8 +335,8 @@ public class SEPortrayerTest extends TestCase {
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
         fts.semanticTypeIdentifiers().add(SemanticType.POINT);
-        final var rule = new Rule<Feature>();
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var rule = factory.createRule();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -365,9 +368,9 @@ public class SEPortrayerTest extends TestCase {
 
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
-        final var rule = new Rule<Feature>();
+        final var rule = factory.createRule();
         rule.setFilter(filter);
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -393,20 +396,20 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testRuleScale() {
-        final LineSymbolizer symbolizerAbove = new LineSymbolizer();
-        final LineSymbolizer symbolizerUnder = new LineSymbolizer();
-        final LineSymbolizer symbolizerMatch = new LineSymbolizer();
+        final var symbolizerAbove = factory.createLineSymbolizer();
+        final var symbolizerUnder = factory.createLineSymbolizer();
+        final var symbolizerMatch = factory.createLineSymbolizer();
 
         //Symbology rendering scale here is 3.944391406060875E8
-        final var ruleAbove = new Rule<Feature>();
+        final var ruleAbove = factory.createRule();
         ruleAbove.symbolizers().add(symbolizerAbove);
         ruleAbove.setMinScaleDenominator(4e8);
         ruleAbove.setMaxScaleDenominator(Double.MAX_VALUE);
-        final var ruleUnder = new Rule<Feature>();
+        final var ruleUnder = factory.createRule();
         ruleUnder.symbolizers().add(symbolizerUnder);
         ruleUnder.setMinScaleDenominator(0.0);
         ruleUnder.setMaxScaleDenominator(3e8);
-        final var ruleMatch = new Rule<Feature>();
+        final var ruleMatch = factory.createRule();
         ruleMatch.symbolizers().add(symbolizerMatch);
         ruleMatch.setMinScaleDenominator(3e8);
         ruleMatch.setMaxScaleDenominator(4e8);
@@ -430,11 +433,11 @@ public class SEPortrayerTest extends TestCase {
 
         final Set<Match> presentations = present(layers);
         assertEquals(5, presentations.size());
-        assertTrue(presentations.contains(new Match("1", fishLayer, fishes, symbolizerMatch)));
-        assertTrue(presentations.contains(new Match("2", fishLayer, fishes, symbolizerMatch)));
+        assertTrue(presentations.contains(new Match(  "1", fishLayer, fishes, symbolizerMatch)));
+        assertTrue(presentations.contains(new Match(  "2", fishLayer, fishes, symbolizerMatch)));
         assertTrue(presentations.contains(new Match("100", fishLayer, fishes, symbolizerMatch)));
-        assertTrue(presentations.contains(new Match("10", boatLayer, boats, symbolizerMatch)));
-        assertTrue(presentations.contains(new Match("20", boatLayer, boats, symbolizerMatch)));
+        assertTrue(presentations.contains(new Match( "10", boatLayer, boats,  symbolizerMatch)));
+        assertTrue(presentations.contains(new Match( "20", boatLayer, boats,  symbolizerMatch)));
     }
 
     /**
@@ -451,9 +454,9 @@ public class SEPortrayerTest extends TestCase {
 
         final Style style = new Style();
         final FeatureTypeStyle fts = new FeatureTypeStyle();
-        final var rule = new Rule<Feature>();
+        final var rule = factory.createRule();
         rule.setFilter(filter);
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var symbolizer = factory.createLineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -483,13 +486,13 @@ public class SEPortrayerTest extends TestCase {
     public void testRuleElseCondition() {
         final Filter<Feature> filter = filterFactory.resourceId("10");
 
-        final LineSymbolizer symbolizerBase = new LineSymbolizer();
-        final LineSymbolizer symbolizerElse = new LineSymbolizer();
+        final var symbolizerBase = factory.createLineSymbolizer();
+        final var symbolizerElse = factory.createLineSymbolizer();
 
-        final var ruleBase = new Rule<Feature>();
+        final var ruleBase = factory.createRule();
         ruleBase.symbolizers().add(symbolizerBase);
         ruleBase.setFilter(filter);
-        final var ruleOther = new Rule<Feature>();
+        final var ruleOther = factory.createRule();
         ruleOther.setElseFilter(true);
         ruleOther.symbolizers().add(symbolizerElse);
 
@@ -511,11 +514,11 @@ public class SEPortrayerTest extends TestCase {
 
         final Set<Match> presentations = present(layers);
         assertEquals(5, presentations.size());
-        assertTrue(presentations.contains(new Match("1", fishLayer, fishes, symbolizerElse)));
-        assertTrue(presentations.contains(new Match("2", fishLayer, fishes, symbolizerElse)));
+        assertTrue(presentations.contains(new Match(  "1", fishLayer, fishes, symbolizerElse)));
+        assertTrue(presentations.contains(new Match(  "2", fishLayer, fishes, symbolizerElse)));
         assertTrue(presentations.contains(new Match("100", fishLayer, fishes, symbolizerElse)));
-        assertTrue(presentations.contains(new Match("10", boatLayer, boats, symbolizerBase)));
-        assertTrue(presentations.contains(new Match("20", boatLayer, boats, symbolizerElse)));
+        assertTrue(presentations.contains(new Match( "10", boatLayer, boats,  symbolizerBase)));
+        assertTrue(presentations.contains(new Match( "20", boatLayer, boats,  symbolizerElse)));
     }
 
     /**
@@ -524,9 +527,9 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testAggregateResource() {
-        final LineSymbolizer symbolizerBase = new LineSymbolizer();
+        final var symbolizerBase = factory.createLineSymbolizer();
 
-        final var ruleBase = new Rule<Feature>();
+        final var ruleBase = factory.createRule();
         ruleBase.symbolizers().add(symbolizerBase);
 
         final Style style = new Style();
@@ -566,11 +569,11 @@ public class SEPortrayerTest extends TestCase {
 
         final Set<Match> presentations = present(layers);
         assertEquals(5, presentations.size());
-        assertTrue(presentations.contains(new Match("1", aggLayer, fishes, symbolizerBase)));
-        assertTrue(presentations.contains(new Match("2", aggLayer, fishes, symbolizerBase)));
+        assertTrue(presentations.contains(new Match(  "1", aggLayer, fishes, symbolizerBase)));
+        assertTrue(presentations.contains(new Match(  "2", aggLayer, fishes, symbolizerBase)));
         assertTrue(presentations.contains(new Match("100", aggLayer, fishes, symbolizerBase)));
-        assertTrue(presentations.contains(new Match("10", aggLayer, boats, symbolizerBase)));
-        assertTrue(presentations.contains(new Match("20", aggLayer, boats, symbolizerBase)));
+        assertTrue(presentations.contains(new Match( "10", aggLayer, boats,  symbolizerBase)));
+        assertTrue(presentations.contains(new Match( "20", aggLayer, boats,  symbolizerBase)));
     }
 
     /**
@@ -579,9 +582,9 @@ public class SEPortrayerTest extends TestCase {
     @Test
     public void testPreserveProperties() {
         final Filter<Feature> filter = filterFactory.resourceId("2");
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var symbolizer = factory.createLineSymbolizer();
 
-        final var rule = new Rule<Feature>();
+        final var rule = factory.createRule();
         rule.symbolizers().add(symbolizer);
         rule.setFilter(filter);
 
@@ -644,11 +647,11 @@ public class SEPortrayerTest extends TestCase {
                 filterFactory.literal("2"),
                 true, MatchAction.ANY);
 
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var symbolizer = factory.createLineSymbolizer();
         symbolizer.setPerpendicularOffset((Expression)filterFactory.property("description", String.class));
         // TODO: use a numeric property above.
 
-        final var rule = new Rule<Feature>();
+        final var rule = factory.createRule();
         rule.symbolizers().add(symbolizer);
         rule.setFilter(filter);
 
@@ -687,10 +690,10 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testGeometryExpression() {
-        final LineSymbolizer symbolizer = new LineSymbolizer();
+        final var symbolizer = factory.createLineSymbolizer();
         symbolizer.setGeometry(filterFactory.function("ST_Centroid", filterFactory.property("geom")));
 
-        final var rule = new Rule<Feature>();
+        final var rule = factory.createRule();
         rule.symbolizers().add(symbolizer);
 
         final Style style = new Style();
@@ -721,10 +724,10 @@ public class SEPortrayerTest extends TestCase {
         private final String identifier;
         private final MapLayer layer;
         private final Resource resource;
-        private final Symbolizer symbolizer;
+        private final Symbolizer<?> symbolizer;
         private final Exception exception;
 
-        public Match(String identifier, MapLayer layer, Resource resource, Symbolizer symbolizer) {
+        public Match(String identifier, MapLayer layer, Resource resource, Symbolizer<?> symbolizer) {
             this.identifier = identifier;
             this.layer = layer;
             this.resource = resource;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java
index 4267cb5f22..5430bba26a 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java
@@ -38,15 +38,15 @@ public final class AnchorPointTest extends StyleTestCase {
      */
     @Test
     public void testAnchorPointXY() {
-        AnchorPoint cdt = new AnchorPoint();
+        final var cdt = factory.createAnchorPoint();
 
         // Check defaults
         assertLiteralEquals(0.5, cdt.getAnchorPointX());
         assertLiteralEquals(0.5, cdt.getAnchorPointY());
 
         // Check get/set
-        cdt.setAnchorPointX(FF.literal(8));
-        cdt.setAnchorPointY(FF.literal(3));
+        cdt.setAnchorPointX(literal(8));
+        cdt.setAnchorPointY(literal(3));
         assertLiteralEquals(8, cdt.getAnchorPointX());
         assertLiteralEquals(3, cdt.getAnchorPointY());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
index c8469efb97..ff802a2f3b 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
@@ -39,19 +39,17 @@ public final class ChannelSelectionTest extends StyleTestCase {
      */
     @Test
     public void testRGBChannels() {
-        ChannelSelection cdt = new ChannelSelection();
+        final var cdt = factory.createChannelSelection();
 
         // Check default
         assertNull(cdt.getChannels());
 
         // Check get/set
-        var values = new SelectedChannel[] {
-            new SelectedChannel("R"),
-            new SelectedChannel("G"),
-            new SelectedChannel("B")
-        };
-        cdt.setChannels(values);
-        assertArrayEquals(values, cdt.getChannels());
+        final var red   = factory.createSelectedChannel("R");
+        final var green = factory.createSelectedChannel("G");
+        final var blue  = factory.createSelectedChannel("B");
+        cdt.setChannels(red, green, blue);
+        assertArrayEquals(new Object[] {red, green, blue}, cdt.getChannels());
     }
 
 
@@ -60,16 +58,14 @@ public final class ChannelSelectionTest extends StyleTestCase {
      */
     @Test
     public void testGrayChannel() {
-        ChannelSelection cdt = new ChannelSelection();
+        final var cdt = factory.createChannelSelection();
 
         // Check default
         assertNull(cdt.getChannels());
 
         // Check get/set
-        var values = new SelectedChannel[] {
-            new SelectedChannel("Gray")
-        };
-        cdt.setChannels(values);
-        assertArrayEquals(values, cdt.getChannels());
+        final var gray = factory.createSelectedChannel("Gray");
+        cdt.setChannels(gray);
+        assertArrayEquals(new Object[] {gray}, cdt.getChannels());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java
index a2a562f551..86ac956a29 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java
@@ -42,7 +42,7 @@ public final class ContrastEnhancementTest extends StyleTestCase {
      */
     @Test
     public void testMethod() {
-        ContrastEnhancement cdt = new ContrastEnhancement();
+        final var cdt = factory.createContrastEnhancement();
 
         // Check default
         assertEquals(ContrastMethod.NONE, cdt.getMethod());
@@ -57,13 +57,13 @@ public final class ContrastEnhancementTest extends StyleTestCase {
      */
     @Test
     public void testGammaValue() {
-        ContrastEnhancement cdt = new ContrastEnhancement();
+        final var cdt = factory.createContrastEnhancement();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getGammaValue());
 
         // Check get/set
-        cdt.setGammaValue(FF.literal(2));
+        cdt.setGammaValue(literal(2));
         assertLiteralEquals(2, cdt.getGammaValue());
         assertEquals(ContrastMethod.GAMMA, cdt.getMethod());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java
index 313f9398c0..64e6ec737c 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java
@@ -40,7 +40,7 @@ public final class DescriptionTest extends StyleTestCase {
     @Test
     public void testTitle() {
         var i18n = new SimpleInternationalString("A random title");
-        final Description cdt = new Description();
+        final var cdt = factory.createDescription();
         assertEmpty(cdt.getTitle());
         cdt.setTitle(i18n);
         assertOptionalEquals(i18n, cdt.getTitle());
@@ -52,7 +52,7 @@ public final class DescriptionTest extends StyleTestCase {
     @Test
     public void testAbstract() {
         var i18n = new SimpleInternationalString("A random abstract");
-        final Description cdt = new Description();
+        final var cdt = factory.createDescription();
         assertEmpty(cdt.getAbstract());
         cdt.setAbstract(i18n);
         assertOptionalEquals(i18n, cdt.getAbstract());
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java
index 6d06aa1ee2..e56c839f34 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java
@@ -38,15 +38,15 @@ public final class DisplacementTest extends StyleTestCase {
      */
     @Test
     public void testGetDisplacementXY() {
-        Displacement cdt = new Displacement();
+        final var cdt = factory.createDisplacement();
 
         // Check defaults
         assertLiteralEquals(0.0, cdt.getDisplacementX());
         assertLiteralEquals(0.0, cdt.getDisplacementY());
 
         // Check get/set
-        cdt.setDisplacementX(FF.literal(-7));
-        cdt.setDisplacementY(FF.literal(15));
+        cdt.setDisplacementX(literal(-7));
+        cdt.setDisplacementY(literal(15));
         assertLiteralEquals(-7, cdt.getDisplacementX());
         assertLiteralEquals(15, cdt.getDisplacementY());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
index d49bd468a3..08e596b74e 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
@@ -42,7 +42,7 @@ public final class ExternalGraphicTest extends StyleTestCase {
      */
     @Test
     public void testOnlineResource() {
-        ExternalGraphic cdt = new ExternalGraphic();
+        final var cdt = factory.createExternalGraphic();
 
         // Check defaults
         assertEmpty(cdt.getOnlineResource());
@@ -57,7 +57,7 @@ public final class ExternalGraphicTest extends StyleTestCase {
      */
     @Test
     public void testInlineContent() {
-        ExternalGraphic cdt = new ExternalGraphic();
+        final var cdt = factory.createExternalGraphic();
 
         // Check defaults
         assertEmpty(cdt.getInlineContent());
@@ -73,7 +73,7 @@ public final class ExternalGraphicTest extends StyleTestCase {
      */
     @Test
     public void testFormat() {
-        ExternalGraphic cdt = new ExternalGraphic();
+        final var cdt = factory.createExternalGraphic();
 
         // Check defaults
         assertEmpty(cdt.getFormat());
@@ -89,13 +89,13 @@ public final class ExternalGraphicTest extends StyleTestCase {
      */
     @Test
     public void testColorReplacements() {
-        ExternalGraphic cdt = new ExternalGraphic();
+        final var cdt = factory.createExternalGraphic();
 
         // Check defaults
         assertTrue(cdt.colorReplacements().isEmpty());
 
         // Check get/set
-        List<ColorReplacement> value = List.of(new ColorReplacement());
+        var value = List.of(factory.createColorReplacement());
         cdt.colorReplacements().addAll(value);
         assertEquals(value, cdt.colorReplacements());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
index aa5fe112a6..cff66ade8e 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
@@ -23,7 +23,6 @@ import org.junit.Test;
 import static org.junit.Assert.*;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.style.SemanticType;
 
 
@@ -46,7 +45,7 @@ public final class FeatureTypeStyleTest extends StyleTestCase {
      */
     @Test
     public void testName() {
-        FeatureTypeStyle cdt = new FeatureTypeStyle();
+        final var cdt = new FeatureTypeStyle();
 
         // Check defaults
         assertEmpty(cdt.getName());
@@ -62,13 +61,13 @@ public final class FeatureTypeStyleTest extends StyleTestCase {
      */
     @Test
     public void testDescription() {
-        FeatureTypeStyle cdt = new FeatureTypeStyle();
+        final var cdt = new FeatureTypeStyle();
 
         // Check defaults
         assertEmpty(cdt.getDescription());
 
         // Check get/set
-        Description desc = anyDescription();
+        var desc = anyDescription();
         cdt.setDescription(desc);
         assertOptionalEquals(desc, cdt.getDescription());
     }
@@ -78,13 +77,13 @@ public final class FeatureTypeStyleTest extends StyleTestCase {
      */
     @Test
     public void testFeatureInstanceIDs() {
-        FeatureTypeStyle cdt = new FeatureTypeStyle();
+        final var cdt = new FeatureTypeStyle();
 
         // Check defaults
         assertEmpty(cdt.getFeatureInstanceIDs());
 
         // Check get/set
-        final var rid = FF.resourceId("A random identifier");
+        final var rid = factory.filterFactory.resourceId("A random identifier");
         cdt.setFeatureInstanceIDs(rid);
         assertOptionalEquals(rid, cdt.getFeatureInstanceIDs());
     }
@@ -94,7 +93,7 @@ public final class FeatureTypeStyleTest extends StyleTestCase {
      */
     @Test
     public void testFeatureTypeNames() {
-        FeatureTypeStyle cdt = new FeatureTypeStyle();
+        final var cdt = new FeatureTypeStyle();
 
         // Check defaults
         assertEmpty(cdt.getFeatureTypeName());
@@ -110,7 +109,7 @@ public final class FeatureTypeStyleTest extends StyleTestCase {
      */
     @Test
     public void testSemanticTypeIdentifiers() {
-        FeatureTypeStyle cdt = new FeatureTypeStyle();
+        final var cdt = new FeatureTypeStyle();
 
         // Check defaults
         assertTrue(cdt.semanticTypeIdentifiers().isEmpty());
@@ -125,13 +124,13 @@ public final class FeatureTypeStyleTest extends StyleTestCase {
      */
     @Test
     public void testRules() {
-        FeatureTypeStyle cdt = new FeatureTypeStyle();
+        final var cdt = new FeatureTypeStyle();
 
         // Check defaults
         assertTrue(cdt.rules().isEmpty());
 
         // Check get/set
-        var rule = new Rule<Feature>();
+        var rule = factory.createRule();
         cdt.rules().add(rule);
         assertEquals(List.of(rule), cdt.rules());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
index 9fb3ee5bdd..44247911ae 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
@@ -39,13 +39,13 @@ public final class FillTest extends StyleTestCase {
      */
     @Test
     public void testGraphicFill() {
-        Fill cdt = new Fill();
+        final var cdt = factory.createFill();
 
         // Check default
         assertEmpty(cdt.getGraphicFill());
 
         // Check get/set
-        final GraphicFill value = new GraphicFill();
+        var value = factory.createGraphicFill();
         cdt.setGraphicFill(value);
         assertOptionalEquals(value, cdt.getGraphicFill());
     }
@@ -55,7 +55,7 @@ public final class FillTest extends StyleTestCase {
      */
     @Test
     public void testColor() {
-        Fill cdt = new Fill();
+        final var cdt = factory.createFill();
 
         // Check default
         assertLiteralEquals(Color.GRAY, cdt.getColor());
@@ -70,13 +70,13 @@ public final class FillTest extends StyleTestCase {
      */
     @Test
     public void testOpacity() {
-        Fill cdt = new Fill();
+        final var cdt = factory.createFill();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getOpacity());
 
         // Check get/set
-        cdt.setOpacity(FF.literal(0.75));
+        cdt.setOpacity(literal(0.75));
         assertLiteralEquals(0.75, cdt.getOpacity());
     }
 
@@ -85,7 +85,8 @@ public final class FillTest extends StyleTestCase {
      */
     @Test
     public void testColorAndOpacity() {
-        Fill cdt = new Fill(new Color(255, 255, 0, 128));
+        final var cdt = factory.createFill();
+        cdt.setColorAndOpacity(new Color(255, 255, 0, 128));
         assertLiteralEquals(Color.YELLOW, cdt.getColor());
         assertLiteralEquals(0.5, cdt.getOpacity());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
index 5d1a76d750..76c150dcc8 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
@@ -40,13 +40,13 @@ public final class FontTest extends StyleTestCase {
      */
     @Test
     public void testFamily() {
-        Font cdt = new Font();
+        final var cdt = factory.createFont();
 
         // Check default
         assertTrue(cdt.family().isEmpty());
 
         // Check get/set
-        var value = FF.literal("A random family");
+        var value = literal("A random family");
         cdt.family().add(value);
         assertEquals(List.of(value), cdt.family());
     }
@@ -55,13 +55,13 @@ public final class FontTest extends StyleTestCase {
      */
     @Test
     public void testStyle() {
-        Font cdt = new Font();
+        final var cdt = factory.createFont();
 
         // Check default
         assertLiteralEquals("normal", cdt.getStyle());
 
         // Check get/set
-        var value = FF.literal("A random style");
+        var value = literal("A random style");
         cdt.setStyle(value);
         assertEquals(value, cdt.getStyle());
     }
@@ -71,13 +71,13 @@ public final class FontTest extends StyleTestCase {
      */
     @Test
     public void testWeight() {
-        Font cdt = new Font();
+        final var cdt = factory.createFont();
 
         // Check default
         assertLiteralEquals("normal", cdt.getWeight());
 
         // Check get/set
-        var value = FF.literal("A random weight");
+        var value = literal("A random weight");
         cdt.setWeight(value);
         assertEquals(value, cdt.getWeight());
     }
@@ -87,13 +87,13 @@ public final class FontTest extends StyleTestCase {
      */
     @Test
     public void testSize() {
-        Font cdt = new Font();
+        final var cdt = factory.createFont();
 
         // Check default
         assertLiteralEquals(10.0, cdt.getSize());
 
         // Check get/set
-        var value = FF.literal(12);
+        var value = literal(12);
         cdt.setSize(value);
         assertEquals(value, cdt.getSize());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java
index b608915782..e1151e1caa 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java
@@ -38,13 +38,13 @@ public final class GraphicStrokeTest extends StyleTestCase {
      */
     @Test
     public void testInitialGap() {
-        GraphicStroke cdt = new GraphicStroke();
+        final var cdt = factory.createGraphicStroke();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getInitialGap());
 
         // Check get/set
-        cdt.setInitialGap(FF.literal(9));
+        cdt.setInitialGap(literal(9));
         assertLiteralEquals(9, cdt.getInitialGap());
     }
 
@@ -53,13 +53,13 @@ public final class GraphicStrokeTest extends StyleTestCase {
      */
     @Test
     public void testGap() {
-        GraphicStroke cdt = new GraphicStroke();
+        final var cdt = factory.createGraphicStroke();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getGap());
 
         // Check get/set
-        cdt.setGap(FF.literal(6));
+        cdt.setGap(literal(6));
         assertLiteralEquals(6, cdt.getGap());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
index 130332ca4d..6721284fa2 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
@@ -39,13 +39,13 @@ public final class GraphicTest extends StyleTestCase {
      */
     @Test
     public void testGraphicalSymbols() {
-        Graphic cdt = new Graphic();
+        final var cdt = factory.createGraphic();
 
         // Check defaults
         assertTrue(cdt.graphicalSymbols().isEmpty());
 
         // Check get/set
-        cdt.graphicalSymbols().add(new Mark());
+        cdt.graphicalSymbols().add(factory.createMark());
         assertEquals(1, cdt.graphicalSymbols().size());
     }
 
@@ -54,13 +54,13 @@ public final class GraphicTest extends StyleTestCase {
      */
     @Test
     public void testOpacity() {
-        Graphic cdt = new Graphic();
+        final var cdt = factory.createGraphic();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getOpacity());
 
         // Check get/set
-        cdt.setOpacity(FF.literal(0.4));
+        cdt.setOpacity(literal(0.4));
         assertLiteralEquals(0.4, cdt.getOpacity());
     }
 
@@ -69,13 +69,13 @@ public final class GraphicTest extends StyleTestCase {
      */
     @Test
     public void testSize() {
-        Graphic cdt = new Graphic();
+        final var cdt = factory.createGraphic();
 
         // Check default
         assertLiteralEquals(6.0, cdt.getSize());
 
         // Check get/set
-        cdt.setSize(FF.literal(13));
+        cdt.setSize(literal(13));
         assertLiteralEquals(13, cdt.getSize());
     }
 
@@ -84,13 +84,13 @@ public final class GraphicTest extends StyleTestCase {
      */
     @Test
     public void testRotation() {
-        Graphic cdt = new Graphic();
+        final var cdt = factory.createGraphic();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getRotation());
 
         // Check get/set
-        cdt.setRotation(FF.literal(90));
+        cdt.setRotation(literal(90));
         assertLiteralEquals(90, cdt.getRotation());
     }
 
@@ -99,13 +99,13 @@ public final class GraphicTest extends StyleTestCase {
      */
     @Test
     public void testAnchorPoint() {
-        Graphic cdt = new Graphic();
+        final var cdt = factory.createGraphic();
 
         // Check default
-        assertEquals(new AnchorPoint(), cdt.getAnchorPoint());
+        assertEquals(factory.createAnchorPoint(), cdt.getAnchorPoint());
 
         // Check get/set
-        var value = new AnchorPoint(-7, 3);
+        var value = factory.createAnchorPoint(-7, 3);
         cdt.setAnchorPoint(value);
         assertEquals(value, cdt.getAnchorPoint());
     }
@@ -115,13 +115,13 @@ public final class GraphicTest extends StyleTestCase {
      */
     @Test
     public void testDisplacement() {
-        Graphic cdt = new Graphic();
+        final var cdt = factory.createGraphic();
 
         // Check default
-        assertEquals(new Displacement(), cdt.getDisplacement());
+        assertEquals(factory.createDisplacement(), cdt.getDisplacement());
 
         // Check get/set
-        var value = new Displacement(12, -5);
+        var value = factory.createDisplacement(12, -5);
         cdt.setDisplacement(value);
         assertEquals(value, cdt.getDisplacement());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
index 95b316e2d0..a0e2fe0f62 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
@@ -17,9 +17,11 @@
 package org.apache.sis.style.se1;
 
 import org.junit.Test;
-
 import static org.junit.Assert.*;
 
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
 
 /**
  * Tests for {@link Halo}.
@@ -40,18 +42,18 @@ public final class HaloTest extends StyleTestCase {
      */
     @Test
     public void testFill() {
-        Halo cdt = new Halo();
+        final var cdt = factory.createHalo();
 
         // Check default
-        Fill fill = cdt.getFill();
-        assertEquals(Fill.WHITE, fill.getColor());
-        assertLiteralEquals(1.0, fill.getOpacity());
+        Fill<Feature> value = cdt.getFill();
+        assertEquals(factory.white, value.getColor());
+        assertLiteralEquals(1.0, value.getOpacity());
 
         // Check get/set
-        fill.setColor(anyColor());
-        fill.setOpacity(FF.literal(0.8));
-        cdt.setFill(fill);
-        assertEquals(fill, cdt.getFill());
+        value.setColor(anyColor());
+        value.setOpacity(literal(0.8));
+        cdt.setFill(value);
+        assertEquals(value, cdt.getFill());
     }
 
     /**
@@ -59,13 +61,13 @@ public final class HaloTest extends StyleTestCase {
      */
     @Test
     public void testRadius() {
-        Halo cdt = new Halo();
+        final var cdt = factory.createHalo();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getRadius());
 
         // Check get/set
-        cdt.setRadius(FF.literal(40));
+        cdt.setRadius(literal(40));
         assertLiteralEquals(40, cdt.getRadius());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
index a224c51109..eb54c957b9 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
@@ -38,13 +38,13 @@ public final class LinePlacementTest extends StyleTestCase {
      */
     @Test
     public void testPerpendicularOffset() {
-        LinePlacement cdt = new LinePlacement();
+        final var cdt = factory.createLinePlacement();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getPerpendicularOffset());
 
         // Check get/set
-        cdt.setPerpendicularOffset(FF.literal(15));
+        cdt.setPerpendicularOffset(literal(15));
         assertLiteralEquals(15, cdt.getPerpendicularOffset());
     }
 
@@ -53,13 +53,13 @@ public final class LinePlacementTest extends StyleTestCase {
      */
     @Test
     public void testInitialGap() {
-        LinePlacement cdt = new LinePlacement();
+        final var cdt = factory.createLinePlacement();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getInitialGap());
 
         // Check get/set
-        cdt.setInitialGap(FF.literal(6));
+        cdt.setInitialGap(literal(6));
         assertLiteralEquals(6, cdt.getInitialGap());
     }
 
@@ -68,13 +68,13 @@ public final class LinePlacementTest extends StyleTestCase {
      */
     @Test
     public void testGap() {
-        LinePlacement cdt = new LinePlacement();
+        final var cdt = factory.createLinePlacement();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getGap());
 
         // Check get/set
-        cdt.setGap(FF.literal(9));
+        cdt.setGap(literal(9));
         assertLiteralEquals(9, cdt.getGap());
     }
 
@@ -83,13 +83,13 @@ public final class LinePlacementTest extends StyleTestCase {
      */
     @Test
     public void testIsRepeated() {
-        LinePlacement cdt = new LinePlacement();
+        final var cdt = factory.createLinePlacement();
 
         // Check default
         assertLiteralEquals(Boolean.FALSE, cdt.isRepeated());
 
         // Check get/set
-        cdt.setRepeated(FF.literal(true));
+        cdt.setRepeated(literal(true));
         assertLiteralEquals(Boolean.TRUE, cdt.isRepeated());
     }
 
@@ -98,13 +98,13 @@ public final class LinePlacementTest extends StyleTestCase {
      */
     @Test
     public void testIsAligned() {
-        LinePlacement cdt = new LinePlacement();
+        final var cdt = factory.createLinePlacement();
 
         // Check default
         assertLiteralEquals(Boolean.TRUE, cdt.isAligned());
 
         // Check get/set
-        cdt.setAligned(FF.literal(false));
+        cdt.setAligned(literal(false));
         assertLiteralEquals(Boolean.FALSE, cdt.isAligned());
     }
 
@@ -113,13 +113,13 @@ public final class LinePlacementTest extends StyleTestCase {
      */
     @Test
     public void testGeneralizeLine() {
-        LinePlacement cdt = new LinePlacement();
+        final var cdt = factory.createLinePlacement();
 
         // Check default
         assertLiteralEquals(Boolean.FALSE, cdt.getGeneralizeLine());
 
         // Check get/set
-        cdt.setGeneralizeLine(FF.literal(true));
+        cdt.setGeneralizeLine(literal(true));
         assertLiteralEquals(Boolean.TRUE, cdt.getGeneralizeLine());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
index d986a23e87..9a71c7282e 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
@@ -20,6 +20,9 @@ import java.awt.Color;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
 
 /**
  * Tests for {@link LineSymbolizer}.
@@ -40,14 +43,14 @@ public final class LineSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testStroke() {
-        LineSymbolizer cdt = new LineSymbolizer();
+        final var cdt = factory.createLineSymbolizer();
 
         // Check default
-        Stroke stroke = cdt.getStroke();
-        assertLiteralEquals(Color.BLACK, stroke.getColor());
+        Stroke<Feature> value = cdt.getStroke();
+        assertLiteralEquals(Color.BLACK, value.getColor());
 
         // Check get/set
-        Stroke value = new Stroke();
+        value = factory.createStroke();
         cdt.setStroke(value);
         assertEquals(value, cdt.getStroke());
     }
@@ -57,13 +60,13 @@ public final class LineSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testPerpendicularOffset() {
-        LineSymbolizer cdt = new LineSymbolizer();
+        final var cdt = factory.createLineSymbolizer();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getPerpendicularOffset());
 
         // Check get/set
-        cdt.setPerpendicularOffset(FF.literal(20));
+        cdt.setPerpendicularOffset(literal(20));
         assertLiteralEquals(20, cdt.getPerpendicularOffset());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
index 26691fd522..84a50dd462 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
@@ -39,13 +39,13 @@ public final class MarkTest extends StyleTestCase {
      */
     @Test
     public void testWellKnownName() {
-        Mark cdt = new Mark();
+        final var cdt = factory.createMark();
 
         // Check default
         assertLiteralEquals("square", cdt.getWellKnownName());
 
         // Check get/set
-        var value = FF.literal("A random name");
+        var value = literal("A random name");
         cdt.setWellKnownName(value);
         assertEquals(value, cdt.getWellKnownName());
     }
@@ -55,15 +55,16 @@ public final class MarkTest extends StyleTestCase {
      */
     @Test
     public void testFill() {
-        Mark cdt = new Mark();
+        final var cdt = factory.createMark();
 
         // Check default
-        assertOptionalEquals(new Fill(), cdt.getFill());
+        assertOptionalEquals(factory.createFill(), cdt.getFill());
 
         // Check get/set
-        var fill = new Fill(ANY_COLOR);
-        cdt.setFill(fill);
-        assertOptionalEquals(fill, cdt.getFill());
+        var value = factory.createFill();
+        value.setColorAndOpacity(ANY_COLOR);
+        cdt.setFill(value);
+        assertOptionalEquals(value, cdt.getFill());
     }
 
     /**
@@ -71,14 +72,14 @@ public final class MarkTest extends StyleTestCase {
      */
     @Test
     public void testStroke() {
-        Mark cdt = new Mark();
+        final var cdt = factory.createMark();
 
         // Check default
-        assertOptionalEquals(new Stroke(), cdt.getStroke());
+        assertOptionalEquals(factory.createStroke(), cdt.getStroke());
 
         // Check get/set
-        Stroke value = new Stroke();
-        value.setOpacity(FF.literal(0.75));
+        var value = factory.createStroke();
+        value.setOpacity(literal(0.75));
         cdt.setStroke(value);
         assertOptionalEquals(value, cdt.getStroke());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
index 9d89994565..3b4a8729ab 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
@@ -19,6 +19,9 @@ package org.apache.sis.style.se1;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
 
 /**
  * Tests for {@link PointPlacement}.
@@ -39,15 +42,15 @@ public final class PointPlacementTest extends StyleTestCase {
      */
     @Test
     public void testAnchorPoint() {
-        PointPlacement cdt = new PointPlacement();
+        final var cdt = factory.createPointPlacement();
 
         // Check default
-        AnchorPoint value = cdt.getAnchorPoint();
+        AnchorPoint<Feature> value = cdt.getAnchorPoint();
         assertLiteralEquals(0.5, value.getAnchorPointX());
         assertLiteralEquals(0.5, value.getAnchorPointY());
 
         // Check get/set
-        value = new AnchorPoint(3, 1);
+        value = factory.createAnchorPoint(3, 1);
         cdt.setAnchorPoint(value);
         assertEquals(value, cdt.getAnchorPoint());
     }
@@ -57,15 +60,15 @@ public final class PointPlacementTest extends StyleTestCase {
      */
     @Test
     public void testDisplacement() {
-        PointPlacement cdt = new PointPlacement();
+        final var cdt = factory.createPointPlacement();
 
         // Check default
-        Displacement value = cdt.getDisplacement();
+        Displacement<Feature> value = cdt.getDisplacement();
         assertLiteralEquals(0.0, value.getDisplacementX());
         assertLiteralEquals(0.0, value.getDisplacementY());
 
         // Check get/set
-        value = new Displacement(1, 2);
+        value = factory.createDisplacement(1, 2);
         cdt.setDisplacement(value);
         assertEquals(value, cdt.getDisplacement());
     }
@@ -75,13 +78,13 @@ public final class PointPlacementTest extends StyleTestCase {
      */
     @Test
     public void testRotation() {
-        PointPlacement cdt = new PointPlacement();
+        final var cdt = factory.createPointPlacement();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getRotation());
 
         // Check get/set
-        cdt.setRotation(FF.literal(180));
+        cdt.setRotation(literal(180));
         assertLiteralEquals(180, cdt.getRotation());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
index 80e24161c4..44598828eb 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
@@ -19,6 +19,9 @@ package org.apache.sis.style.se1;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
 
 /**
  * Tests for {@link PointSymbolizer}.
@@ -39,15 +42,15 @@ public final class PointSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testGraphic() {
-        PointSymbolizer cdt = new PointSymbolizer();
+        final var cdt = factory.createPointSymbolizer();
 
         // Check default
-        Graphic value = cdt.getGraphic();
+        Graphic<Feature> value = cdt.getGraphic();
         assertLiteralEquals(1.0, value.getOpacity());
 
         // Check get/set
-        value = new Graphic();
-        value.setOpacity(FF.literal(0.8));
+        value = factory.createGraphic();
+        value.setOpacity(literal(0.8));
         cdt.setGraphic(value);
         assertEquals(value, cdt.getGraphic());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
index e1074f47e3..08fc95a730 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
@@ -20,6 +20,9 @@ import java.awt.Color;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
 
 /**
  * Tests for {@link PolygonSymbolizer}.
@@ -40,15 +43,15 @@ public final class PolygonSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testStroke() {
-        PolygonSymbolizer cdt = new PolygonSymbolizer();
+        final var cdt = factory.createPolygonSymbolizer();
 
         // Check default
-        Stroke value = cdt.getStroke().orElseThrow();
+        var value = cdt.getStroke().orElseThrow();
         assertLiteralEquals("bevel",  value.getLineJoin());
         assertLiteralEquals("square", value.getLineCap());
 
         // Check get/set
-        value = new Stroke();
+        value = factory.createStroke();
         cdt.setStroke(value);
         assertOptionalEquals(value, cdt.getStroke());
     }
@@ -58,14 +61,15 @@ public final class PolygonSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testFill() {
-        PolygonSymbolizer cdt = new PolygonSymbolizer();
+        final var cdt = factory.createPolygonSymbolizer();
 
         // Check default
-        Fill value = cdt.getFill().orElseThrow();
+        Fill<Feature> value = cdt.getFill().orElseThrow();
         assertLiteralEquals(Color.GRAY, value.getColor());
 
         // Check get/set
-        value = new Fill(ANY_COLOR);
+        value = factory.createFill();
+        value.setColorAndOpacity(ANY_COLOR);
         cdt.setFill(value);
         assertOptionalEquals(value, cdt.getFill());
     }
@@ -75,15 +79,15 @@ public final class PolygonSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testDisplacement() {
-        PolygonSymbolizer cdt = new PolygonSymbolizer();
+        final var cdt = factory.createPolygonSymbolizer();
 
         // Check default
-        Displacement value = cdt.getDisplacement();
+        Displacement<Feature> value = cdt.getDisplacement();
         assertLiteralEquals(0.0, value.getDisplacementX());
         assertLiteralEquals(0.0, value.getDisplacementY());
 
         // Check get/set
-        value = new Displacement(4, 1);
+        value = factory.createDisplacement(4, 1);
         cdt.setDisplacement(value);
         assertEquals(value, cdt.getDisplacement());
     }
@@ -93,13 +97,13 @@ public final class PolygonSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testPerpendicularOffset() {
-        PolygonSymbolizer cdt = new PolygonSymbolizer();
+        final var cdt = factory.createPolygonSymbolizer();
 
         // Check default
         assertLiteralEquals(0.0, cdt.getPerpendicularOffset());
 
         // Check get/set
-        cdt.setPerpendicularOffset(FF.literal(10));
+        cdt.setPerpendicularOffset(literal(10));
         assertLiteralEquals(10, cdt.getPerpendicularOffset());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
index 2d6284fcea..6be7723072 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
@@ -42,13 +42,13 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testOpacity() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getOpacity());
 
         // Check get/set
-        cdt.setOpacity(FF.literal(0.7));
+        cdt.setOpacity(literal(0.7));
         assertLiteralEquals(0.7, cdt.getOpacity());
     }
 
@@ -57,13 +57,13 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testChannelSelection() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertEmpty(cdt.getChannelSelection());
 
         // Check get/set
-        ChannelSelection value = new ChannelSelection();
+        var value = factory.createChannelSelection();
         cdt.setChannelSelection(value);
         assertOptionalEquals(value, cdt.getChannelSelection());
     }
@@ -73,7 +73,7 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testOverlapBehavior() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertNotNull(cdt.getOverlapBehavior());
@@ -88,13 +88,13 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testColorMap() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertEmpty(cdt.getColorMap());
 
         // Check get/set
-        ColorMap value = new ColorMap();
+        var value = factory.createColorMap();
         cdt.setColorMap(value);
         assertOptionalEquals(value, cdt.getColorMap());
     }
@@ -104,13 +104,13 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testGetContrastEnhancement() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertEmpty(cdt.getContrastEnhancement());
 
         // Check get/set
-        ContrastEnhancement value = new ContrastEnhancement();
+        var value = factory.createContrastEnhancement();
         cdt.setContrastEnhancement(value);
         assertOptionalEquals(value, cdt.getContrastEnhancement());
     }
@@ -120,13 +120,13 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testGetShadedRelief() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertEmpty(cdt.getShadedRelief());
 
         // Check get/set
-        ShadedRelief value = new ShadedRelief();
+        var value = factory.createShadedRelief();
         cdt.setShadedRelief(value);
         assertOptionalEquals(value, cdt.getShadedRelief());
     }
@@ -136,13 +136,13 @@ public final class RasterSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testImageOutline() {
-        RasterSymbolizer cdt = new RasterSymbolizer();
+        final var cdt = factory.createRasterSymbolizer();
 
         // Check default
         assertEmpty(cdt.getImageOutline());
 
         // Check get/set
-        Symbolizer value = new LineSymbolizer();
+        var value = factory.createLineSymbolizer();
         cdt.setImageOutline(value);
         assertOptionalEquals(value, cdt.getImageOutline());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
index 9acd4ceb92..c4ccfc7da2 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
@@ -23,7 +23,6 @@ import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
 import static org.junit.Assert.*;
 
 // Branch-dependent imports
-import org.opengis.feature.Feature;
 import org.opengis.filter.Filter;
 
 
@@ -46,7 +45,7 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testGetName() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEmpty(cdt.getName());
@@ -62,13 +61,13 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testDescription() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEmpty(cdt.getDescription());
 
         // Check get/set
-        Description desc = anyDescription();
+        var desc = anyDescription();
         cdt.setDescription(desc);
         assertOptionalEquals(desc, cdt.getDescription());
     }
@@ -78,13 +77,13 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testLegend() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEmpty(cdt.getLegend());
 
         // Check get/set
-        LegendGraphic value = new LegendGraphic();
+        var value = factory.createLegendGraphic();
         cdt.setLegend(value);
         assertOptionalEquals(value, cdt.getLegend());
     }
@@ -94,13 +93,13 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testFilter() {
-        var cdt = new Rule<Feature>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEquals(Filter.include(), cdt.getFilter());
 
         // Check get/set
-        var value = FF.equal(FF.literal("A"), FF.literal("B"));
+        var value = factory.filterFactory.equal(literal("A"), literal("B"));
         cdt.setFilter(value);
         assertEquals(value, cdt.getFilter());
     }
@@ -110,7 +109,7 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testIsElseFilter() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertFalse(cdt.isElseFilter());
@@ -125,7 +124,7 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testMinScaleDenominator() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEquals(0.0, cdt.getMinScaleDenominator(), 0.0);
@@ -140,7 +139,7 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testGetMaxScaleDenominator() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEquals(Double.POSITIVE_INFINITY, cdt.getMaxScaleDenominator(), 0.0);
@@ -155,13 +154,13 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testSymbolizers() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertTrue(cdt.symbolizers().isEmpty());
 
         // Check get/set
-        var value = new LineSymbolizer();
+        var value = factory.createLineSymbolizer();
         cdt.symbolizers().add(value);
         assertEquals(List.of(value), cdt.symbolizers());
     }
@@ -171,7 +170,7 @@ public final class RuleTest extends StyleTestCase {
      */
     @Test
     public void testGetOnlineSource() {
-        var cdt = new Rule<Object>();
+        final var cdt = factory.createRule();
 
         // Check defaults
         assertEmpty(cdt.getOnlineSource());
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java
index 76b214eb52..b6f44edcdf 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java
@@ -39,14 +39,14 @@ public final class SelectedChannelTest extends StyleTestCase {
      */
     @Test
     public void testChannelName() {
-        SelectedChannel cdt = new SelectedChannel();
+        final var cdt = factory.createSelectedChannel();
 
         // Check defaults
         assertNull(cdt.getSourceChannelName());
 
         // Check get/set
         String value = "A random channel";
-        cdt.setSourceChannelName(FF.literal(value));
+        cdt.setSourceChannelName(literal(value));
         assertLiteralEquals(value, cdt.getSourceChannelName());
     }
 
@@ -55,13 +55,13 @@ public final class SelectedChannelTest extends StyleTestCase {
      */
     @Test
     public void testContrastEnhancement() {
-        SelectedChannel cdt = new SelectedChannel();
+        final var cdt = factory.createSelectedChannel();
 
         // Check defaults
         assertEmpty(cdt.getContrastEnhancement());
 
         // Check get/set
-        ContrastEnhancement value = new ContrastEnhancement();
+        var value = factory.createContrastEnhancement();
         cdt.setContrastEnhancement(value);
         assertOptionalEquals(value, cdt.getContrastEnhancement());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java
index a247aa89fb..3a4c7d0224 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java
@@ -39,13 +39,13 @@ public final class ShadedReliefTest extends StyleTestCase {
      */
     @Test
     public void testIsBrightnessOnly() {
-        ShadedRelief cdt = new ShadedRelief();
+        final var cdt = factory.createShadedRelief();
 
         // Check default
         assertLiteralEquals(Boolean.FALSE, cdt.isBrightnessOnly());
 
         // Check get/set
-        cdt.setBrightnessOnly(FF.literal(true));
+        cdt.setBrightnessOnly(literal(true));
         assertLiteralEquals(Boolean.TRUE, cdt.isBrightnessOnly());
     }
 
@@ -54,13 +54,13 @@ public final class ShadedReliefTest extends StyleTestCase {
      */
     @Test
     public void testReliefFactor() {
-        ShadedRelief cdt = new ShadedRelief();
+        final var cdt = factory.createShadedRelief();
 
         // Check default
         assertNotNull(cdt.getReliefFactor());
 
         // Check get/set
-        cdt.setReliefFactor(FF.literal(0.1));
+        cdt.setReliefFactor(literal(0.1));
         assertLiteralEquals(0.1, cdt.getReliefFactor());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
index 61d5993e56..6706149c79 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
@@ -40,13 +40,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testGraphicFill() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertEmpty(cdt.getGraphicFill());
 
         // Check get/set
-        GraphicFill value = new GraphicFill();
+        var value = factory.createGraphicFill();
         cdt.setGraphicFill(value);
         assertOptionalEquals(value, cdt.getGraphicFill());
     }
@@ -56,13 +56,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testGraphicStroke() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertEmpty(cdt.getGraphicStroke());
 
         // Check get/set
-        GraphicStroke value = new GraphicStroke();
+        var value = factory.createGraphicStroke();
         cdt.setGraphicStroke(value);
         assertOptionalEquals(value, cdt.getGraphicStroke());
     }
@@ -72,7 +72,7 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testColor() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertLiteralEquals(Color.BLACK, cdt.getColor());
@@ -87,13 +87,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testOpacity() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getOpacity());
 
         // Check get/set
-        cdt.setOpacity(FF.literal(0.7));
+        cdt.setOpacity(literal(0.7));
         assertLiteralEquals(0.7, cdt.getOpacity());
     }
 
@@ -102,7 +102,7 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testColorAndOpacity() {
-        Stroke cdt = new Stroke(new Color(255, 255, 0, 128));
+        final var cdt = factory.createStroke(new Color(255, 255, 0, 128));
         assertLiteralEquals(Color.YELLOW, cdt.getColor());
         assertLiteralEquals(0.5, cdt.getOpacity());
     }
@@ -112,13 +112,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testWidth() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertLiteralEquals(1.0, cdt.getWidth());
 
         // Check get/set
-        cdt.setWidth(FF.literal(14));
+        cdt.setWidth(literal(14));
         assertLiteralEquals(14, cdt.getWidth());
     }
 
@@ -127,13 +127,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testLineJoin() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertLiteralEquals("bevel", cdt.getLineJoin());
 
         // Check get/set
-        var value = FF.literal("A random join");
+        var value = literal("A random join");
         cdt.setLineJoin(value);
         assertEquals(value, cdt.getLineJoin());
     }
@@ -143,13 +143,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testLineCap() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertLiteralEquals("square", cdt.getLineCap());
 
         // Check get/set
-        var value = FF.literal("A random cap");
+        var value = literal("A random cap");
         cdt.setLineCap(value);
         assertEquals(value, cdt.getLineCap());
     }
@@ -159,14 +159,14 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testDashArray() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertEmpty(cdt.getDashArray());
 
         // Check get/set
         final var value = new float[] {1,2,3};
-        cdt.setDashArray(FF.literal(value));
+        cdt.setDashArray(literal(value));
         assertLiteralEquals(value, cdt.getDashArray().orElseThrow());
     }
 
@@ -175,13 +175,13 @@ public final class StrokeTest extends StyleTestCase {
      */
     @Test
     public void testDashOffset() {
-        Stroke cdt = new Stroke();
+        final var cdt = factory.createStroke();
 
         // Check default
         assertLiteralEquals(0, cdt.getDashOffset());
 
         // Check get/set
-        cdt.setDashOffset(FF.literal(21));
+        cdt.setDashOffset(literal(21));
         assertLiteralEquals(21, cdt.getDashOffset());
     }
 }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java
index 34c66e45c1..7273586f27 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java
@@ -75,7 +75,7 @@ public final class StyleTest extends StyleTestCase {
         assertEmpty(cdt.getDefaultSpecification());
 
         // Check get/set
-        Symbolizer value = new LineSymbolizer();
+        var value = factory.createLineSymbolizer();
         cdt.setDefaultSpecification(value);
         assertOptionalEquals(value, cdt.getDefaultSpecification());
     }
@@ -107,7 +107,7 @@ public final class StyleTest extends StyleTestCase {
         assertEmpty(cdt.getDescription());
 
         // Check get/set
-        Description desc = anyDescription();
+        var desc = anyDescription();
         cdt.setDescription(desc);
         assertOptionalEquals(desc, cdt.getDescription());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
index 7171c4d445..bd2fe221ca 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
@@ -18,7 +18,6 @@ package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import java.util.Optional;
-import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.util.SimpleInternationalString;
 import org.apache.sis.test.TestCase;
 
@@ -29,7 +28,6 @@ import static org.opengis.test.Assert.assertInstanceOf;
 import org.opengis.feature.Feature;
 import org.opengis.filter.Literal;
 import org.opengis.filter.Expression;
-import org.opengis.filter.FilterFactory;
 
 
 /**
@@ -41,21 +39,33 @@ import org.opengis.filter.FilterFactory;
  */
 abstract class StyleTestCase extends TestCase {
     /**
-     * The factory to use for creating literal values.
+     * The factory to use for creating style elements.
      */
-    static final FilterFactory<Feature,Object,Object> FF = DefaultFilterFactory.forFeatures();
+    final StyleFactory<Feature> factory;
 
     /**
      * Creates a new test case.
      */
     StyleTestCase() {
+        factory = FeatureTypeStyle.FACTORY;
+    }
+
+    /**
+     * Returns a literal for the given value.
+     *
+     * @param  <E>    type of value.
+     * @param  value  the value for which to return a literal.
+     * @return literal for the given value.
+     */
+    final <E> Literal<Feature,E> literal(final E value) {
+        return factory.filterFactory.literal(value);
     }
 
     /**
      * Creates a dummy description with arbitrary title and abstract.
      */
-    static Description anyDescription() {
-        var value = new Description();
+    final Description<Feature> anyDescription() {
+        final var value = factory.createDescription();
         value.setTitle(new SimpleInternationalString("A random title"));
         value.setAbstract(new SimpleInternationalString("A random abstract"));
         return value;
@@ -65,8 +75,8 @@ abstract class StyleTestCase extends TestCase {
      * Returns an expression with a random color.
      * The color is {@link #ANY_COLOR}.
      */
-    static Expression<Feature,Color> anyColor() {
-        return FF.literal(ANY_COLOR);
+    final Expression<Feature,Color> anyColor() {
+        return literal(ANY_COLOR);
     }
 
     /**
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
index ccc02fea67..d9889903b0 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
@@ -44,7 +44,7 @@ public final class SymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testUnitOfMeasure() {
-        Symbolizer cdt = new LineSymbolizer();
+        final var cdt = factory.createLineSymbolizer();
 
         // Check default
         assertEquals(Units.PIXEL, cdt.getUnitOfMeasure());
@@ -59,13 +59,13 @@ public final class SymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testGeometry() {
-        Symbolizer cdt = new LineSymbolizer();
+        final var cdt = factory.createLineSymbolizer();
 
         // Check default
         assertInstanceOf("geometry", ValueReference.class, cdt.getGeometry());
 
         // Check get/set
-        cdt.setGeometry(FF.literal(8));
+        cdt.setGeometry(literal(8));
         assertLiteralEquals(8, cdt.getGeometry());
     }
 
@@ -74,7 +74,7 @@ public final class SymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testName() {
-        Symbolizer cdt = new LineSymbolizer();
+        final var cdt = factory.createLineSymbolizer();
 
         // Check defaults
         assertEmpty(cdt.getName());
@@ -90,13 +90,13 @@ public final class SymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testDescription() {
-        Symbolizer cdt = new LineSymbolizer();
+        final var cdt = factory.createLineSymbolizer();
 
         // Check defaults
         assertEmpty(cdt.getDescription());
 
         // Check get/set
-        Description desc = anyDescription();
+        var desc = anyDescription();
         cdt.setDescription(desc);
         assertOptionalEquals(desc, cdt.getDescription());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
index 8c7ff576e2..5954377dcd 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
@@ -19,6 +19,9 @@ package org.apache.sis.style.se1;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
 
 /**
  * Tests for {@link TextSymbolizer}.
@@ -39,13 +42,13 @@ public final class TextSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testLabel() {
-        TextSymbolizer cdt = new TextSymbolizer();
+        final var cdt = factory.createTextSymbolizer();
 
         // Check default
         assertNull(cdt.getLabel());
 
         // Check get/set
-        var value = FF.literal("A random label");
+        var value = literal("A random label");
         cdt.setLabel(value);
         assertEquals(value, cdt.getLabel());
     }
@@ -55,15 +58,15 @@ public final class TextSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testFont() {
-        TextSymbolizer cdt = new TextSymbolizer();
+        final var cdt = factory.createTextSymbolizer();
 
         // Check default
-        Font value = cdt.getFont();
+        var value = cdt.getFont();
         assertLiteralEquals("normal", value.getStyle());
 
         // Check get/set
-        value = new Font();
-        value.setStyle(FF.literal("italic"));
+        value = factory.createFont();
+        value.setStyle(literal("italic"));
         cdt.setFont(value);
         assertEquals(value, cdt.getFont());
     }
@@ -73,14 +76,14 @@ public final class TextSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testLabelPlacement() {
-        TextSymbolizer cdt = new TextSymbolizer();
+        final var cdt = factory.createTextSymbolizer();
 
         // Check default
-        LabelPlacement value = cdt.getLabelPlacement();
+        LabelPlacement<Feature> value = cdt.getLabelPlacement();
         assertNotNull(value);
 
         // Check get/set
-        value = new PointPlacement();
+        value = factory.createPointPlacement();
         cdt.setLabelPlacement(value);
         assertEquals(value, cdt.getLabelPlacement());
     }
@@ -90,13 +93,13 @@ public final class TextSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testHalo() {
-        TextSymbolizer cdt = new TextSymbolizer();
+        final var cdt = factory.createTextSymbolizer();
 
         // Check default
         assertEmpty(cdt.getHalo());
 
         // Check get/set
-        Halo value = new Halo();
+        var value = factory.createHalo();
         cdt.setHalo(value);
         assertOptionalEquals(value, cdt.getHalo());
     }
@@ -106,13 +109,13 @@ public final class TextSymbolizerTest extends StyleTestCase {
      */
     @Test
     public void testFill() {
-        TextSymbolizer cdt = new TextSymbolizer();
+        final var cdt = factory.createTextSymbolizer();
 
         // Check default
         assertNotNull(cdt.getFill());
 
         // Check get/set
-        Fill value = new Fill();
+        var value = factory.createFill();
         cdt.setFill(value);
         assertEquals(value, cdt.getFill());
     }


[sis] 02/04: Rename `org.apache.sis.internal.style` package as `org.apache.sis.style.se1`. By moving the package outside `internal` namespace, we are making it public. However we still target SIS 1.5, not SIS 1.4, for release.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit b35d75f713175e6252a30ff47cc6da5b9b6dd41d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jun 30 10:29:59 2023 +0200

    Rename `org.apache.sis.internal.style` package as `org.apache.sis.style.se1`.
    By moving the package outside `internal` namespace, we are making it public.
    However we still target SIS 1.5, not SIS 1.4, for release.
    
    The `se1` name stands for "OGC Symbology Encoding 1".
    We do not commit this package as a generic style API,
    because there is revision in progress in OGC and ISO 19117.
    We use SE1 as an API that we can use for now, but with the
    intend to make a more generic API available in the future.
---
 .../apache/sis/internal/map/ResourceSymbolizer.java    |  2 +-
 .../java/org/apache/sis/internal/map/SEPortrayer.java  |  6 +++---
 .../org/apache/sis/internal/map/SEPresentation.java    |  2 +-
 .../org/apache/sis/internal/map/SymbologyVisitor.java  | 10 +++++-----
 .../main/java/org/apache/sis/portrayal/MapLayer.java   |  2 +-
 .../sis/{internal/style => style/se1}/AnchorPoint.java |  2 +-
 .../style => style/se1}/ChannelSelection.java          | 18 +++++++++---------
 .../sis/{internal/style => style/se1}/ColorMap.java    |  2 +-
 .../style => style/se1}/ColorReplacement.java          |  2 +-
 .../style => style/se1}/ContrastEnhancement.java       |  2 +-
 .../sis/{internal/style => style/se1}/Description.java |  2 +-
 .../{internal/style => style/se1}/Displacement.java    |  2 +-
 .../{internal/style => style/se1}/ExternalGraphic.java |  2 +-
 .../style => style/se1}/FeatureTypeStyle.java          |  2 +-
 .../apache/sis/{internal/style => style/se1}/Fill.java |  2 +-
 .../apache/sis/{internal/style => style/se1}/Font.java |  2 +-
 .../sis/{internal/style => style/se1}/Graphic.java     |  2 +-
 .../sis/{internal/style => style/se1}/GraphicFill.java |  2 +-
 .../{internal/style => style/se1}/GraphicStroke.java   |  2 +-
 .../style => style/se1}/GraphicalElement.java          |  2 +-
 .../{internal/style => style/se1}/GraphicalSymbol.java |  2 +-
 .../apache/sis/{internal/style => style/se1}/Halo.java |  2 +-
 .../{internal/style => style/se1}/LabelPlacement.java  |  2 +-
 .../se1/LegendGraphic.java}                            | 12 ++++++------
 .../{internal/style => style/se1}/LinePlacement.java   |  2 +-
 .../{internal/style => style/se1}/LineSymbolizer.java  |  2 +-
 .../apache/sis/{internal/style => style/se1}/Mark.java |  2 +-
 .../{internal/style => style/se1}/PointPlacement.java  |  2 +-
 .../{internal/style => style/se1}/PointSymbolizer.java |  2 +-
 .../style => style/se1}/PolygonSymbolizer.java         |  2 +-
 .../style => style/se1}/RasterSymbolizer.java          |  6 +++---
 .../apache/sis/{internal/style => style/se1}/Rule.java | 10 +++++-----
 .../se1/SelectedChannel.java}                          | 14 +++++++-------
 .../{internal/style => style/se1}/ShadedRelief.java    |  2 +-
 .../sis/{internal/style => style/se1}/Stroke.java      |  2 +-
 .../sis/{internal/style => style/se1}/Style.java       |  2 +-
 .../{internal/style => style/se1}/StyleElement.java    |  2 +-
 .../sis/{internal/style => style/se1}/Symbolizer.java  |  2 +-
 .../{internal/style => style/se1}/TextSymbolizer.java  |  2 +-
 .../sis/{internal/style => style/se1}/Translucent.java |  2 +-
 .../org/apache/sis/internal/map/SEPortrayerTest.java   | 10 +++++-----
 .../{internal/style => style/se1}/AnchorPointTest.java |  2 +-
 .../style => style/se1}/ChannelSelectionTest.java      | 14 +++++++-------
 .../style => style/se1}/ContrastEnhancementTest.java   |  2 +-
 .../{internal/style => style/se1}/DescriptionTest.java |  2 +-
 .../style => style/se1}/DisplacementTest.java          |  2 +-
 .../style => style/se1}/ExternalGraphicTest.java       |  2 +-
 .../style => style/se1}/FeatureTypeStyleTest.java      |  2 +-
 .../sis/{internal/style => style/se1}/FillTest.java    |  2 +-
 .../sis/{internal/style => style/se1}/FontTest.java    |  2 +-
 .../style => style/se1}/GraphicStrokeTest.java         |  2 +-
 .../sis/{internal/style => style/se1}/GraphicTest.java |  2 +-
 .../sis/{internal/style => style/se1}/HaloTest.java    |  2 +-
 .../style => style/se1}/LinePlacementTest.java         |  2 +-
 .../style => style/se1}/LineSymbolizerTest.java        |  2 +-
 .../sis/{internal/style => style/se1}/MarkTest.java    |  2 +-
 .../style => style/se1}/PointPlacementTest.java        |  2 +-
 .../style => style/se1}/PointSymbolizerTest.java       |  2 +-
 .../style => style/se1}/PolygonSymbolizerTest.java     |  2 +-
 .../style => style/se1}/RasterSymbolizerTest.java      |  2 +-
 .../sis/{internal/style => style/se1}/RuleTest.java    |  4 ++--
 .../se1/SelectedChannelTest.java}                      | 12 ++++++------
 .../style => style/se1}/ShadedReliefTest.java          |  2 +-
 .../sis/{internal/style => style/se1}/StrokeTest.java  |  2 +-
 .../sis/{internal/style => style/se1}/StyleTest.java   |  2 +-
 .../{internal/style => style/se1}/StyleTestCase.java   |  2 +-
 .../{internal/style => style/se1}/SymbolizerTest.java  |  2 +-
 .../style => style/se1}/TextSymbolizerTest.java        |  2 +-
 68 files changed, 115 insertions(+), 115 deletions(-)

diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
index c043794985..07ea6abd40 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/ResourceSymbolizer.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.internal.map;
 
-import org.apache.sis.internal.style.Symbolizer;
+import org.apache.sis.style.se1.Symbolizer;
 
 
 /**
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
index 90370b7da9..5814406f92 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
@@ -57,9 +57,9 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.Query;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.internal.style.FeatureTypeStyle;
-import org.apache.sis.internal.style.Rule;
-import org.apache.sis.internal.style.Symbolizer;
+import org.apache.sis.style.se1.FeatureTypeStyle;
+import org.apache.sis.style.se1.Rule;
+import org.apache.sis.style.se1.Symbolizer;
 
 // Branch-dependent imports
 import org.opengis.feature.AttributeType;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java
index cf0aa7d3fb..7df7cf5f05 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPresentation.java
@@ -19,7 +19,7 @@ package org.apache.sis.internal.map;
 import java.util.Objects;
 import org.apache.sis.portrayal.MapLayer;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.internal.style.Symbolizer;
+import org.apache.sis.style.se1.Symbolizer;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
index 42dfd8f3ea..1ee4aeea0e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SymbologyVisitor.java
@@ -21,7 +21,7 @@ import org.opengis.filter.Filter;
 import org.opengis.filter.Expression;
 import org.opengis.filter.Literal;
 import org.opengis.filter.ValueReference;
-import org.apache.sis.internal.style.*;
+import org.apache.sis.style.se1.*;
 
 import static org.apache.sis.internal.util.CollectionsExt.nonNull;
 
@@ -248,7 +248,7 @@ public abstract class SymbologyVisitor {
         }
     }
 
-    protected void visit(final GraphicLegend candidate) {
+    protected void visit(final LegendGraphic candidate) {
         visit((GraphicalElement) candidate);
     }
 
@@ -273,16 +273,16 @@ public abstract class SymbologyVisitor {
 
     protected void visit(final ChannelSelection candidate) {
         if (candidate != null) {
-            SelectedChannelType[] channels = candidate.getChannels();
+            SelectedChannel[] channels = candidate.getChannels();
             if (channels != null) {
-                for (final SelectedChannelType sct : channels) {
+                for (final SelectedChannel sct : channels) {
                     visit(sct);
                 }
             }
         }
     }
 
-    protected void visit(final SelectedChannelType candidate) {
+    protected void visit(final SelectedChannel candidate) {
         if (candidate != null) {
             candidate.getContrastEnhancement().ifPresent(this::visit);
         }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java
index 2362f4a1c1..7c93058e94 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java
@@ -24,7 +24,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Query;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.internal.style.Style;
+import org.apache.sis.style.se1.Style;
 import org.opengis.coverage.Coverage;
 import org.opengis.feature.Feature;
 import org.opengis.geometry.Envelope;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
index 84a6028180..4bd9637fd5 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
similarity index 92%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
index f5ab98095b..49e3ddc585 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ChannelSelection.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
@@ -48,28 +48,28 @@ public class ChannelSelection extends StyleElement {
      * This property is mutually exclusive with {@link #gray}.
      */
     @XmlElement(name = "RedChannel")
-    protected SelectedChannelType red;
+    protected SelectedChannel red;
 
     /**
      * The green channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #gray}.
      */
     @XmlElement(name = "GreenChannel")
-    protected SelectedChannelType green;
+    protected SelectedChannel green;
 
     /**
      * The blue channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #gray}.
      */
     @XmlElement(name = "BlueChannel")
-    protected SelectedChannelType blue;
+    protected SelectedChannel blue;
 
     /**
      * The gray channel, or {@code null} if none.
      * This property is mutually exclusive with {@link #red}, {@link #green} and {@link #blue}.
      */
     @XmlElement(name = "GrayChannel")
-    protected SelectedChannelType gray;
+    protected SelectedChannel gray;
 
     /**
      * Creates an initially empty channel selection.
@@ -101,11 +101,11 @@ public class ChannelSelection extends StyleElement {
      *
      * @todo Replace null value by some default value.
      */
-    public SelectedChannelType[] getChannels() {
+    public SelectedChannel[] getChannels() {
         if (red != null || green != null || blue != null) {
-            return new SelectedChannelType[] {red, green, blue};
+            return new SelectedChannel[] {red, green, blue};
         } else if (gray != null) {
-            return new SelectedChannelType[] {gray};
+            return new SelectedChannel[] {gray};
         } else {
             return null;
         }
@@ -122,7 +122,7 @@ public class ChannelSelection extends StyleElement {
      * @param  values  array of channels, or {@code null} if none.
      * @throws IllegalArgumentException if the length of the specified array is not 0, 1 or 3.
      */
-    public void setChannels(final SelectedChannelType... values) {
+    public void setChannels(final SelectedChannel... values) {
         red   = null;
         green = null;
         blue  = null;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
similarity index 98%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
index 6b4cfc2a49..bae948b067 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorMap.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlRootElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
similarity index 98%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
index aca082aaac..d07590de4c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ColorReplacement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlRootElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
index 5dc499ae52..1cf6c910b0 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
index 4b121a2aa3..805fc2a33d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Description.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
index 2afb41e7d0..4cd83e8d4e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
index be2d6fe82d..11d5899f04 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExternalGraphic.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import java.util.ArrayList;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
index 03251ed174..fd27c07876 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Set;
 import java.util.List;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
index c7759ffeec..094d3b04a0 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import java.util.Optional;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
index a167c05284..d5bfae3557 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import java.util.ArrayList;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
index c83413d1f4..f3a6139c61 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import java.util.ArrayList;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
index 79dc7513fe..0eb96ea48f 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicFill.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
index 7eeb608ff7..186ade25d6 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalElement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
similarity index 98%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalElement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
index c305b0cf48..c8aa67cfd4 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalElement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalElement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 
 /**
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
index 27ca6c5401..52938ad56c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicalSymbol.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import javax.swing.Icon;
 import java.util.Optional;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
index 7fae4fe368..8bf8e32fe3 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
similarity index 98%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
index 82399a87d2..72ef4133c1 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LabelPlacement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlSeeAlso;
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
similarity index 92%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
index 78721aae9b..5fa7f34cf6 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LegendGraphic.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
@@ -31,7 +31,7 @@ import jakarta.xml.bind.annotation.XmlRootElement;
  */
 @XmlType(name = "LegendGraphicType")
 @XmlRootElement(name = "LegendGraphic")
-public class GraphicLegend extends StyleElement implements GraphicalElement {
+public class LegendGraphic extends StyleElement implements GraphicalElement {
     /**
      * The graphic to use as a legend, or {@code null} for lazily constructed default.
      * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
@@ -44,7 +44,7 @@ public class GraphicLegend extends StyleElement implements GraphicalElement {
     /**
      * Creates a legend initialized to the default graphic.
      */
-    public GraphicLegend() {
+    public LegendGraphic() {
     }
 
     /**
@@ -53,7 +53,7 @@ public class GraphicLegend extends StyleElement implements GraphicalElement {
      *
      * @param  source  the object to copy.
      */
-    public GraphicLegend(final GraphicLegend source) {
+    public LegendGraphic(final LegendGraphic source) {
         super(source);
         graphic = source.graphic;
     }
@@ -102,8 +102,8 @@ public class GraphicLegend extends StyleElement implements GraphicalElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public GraphicLegend clone() {
-        final var clone = (GraphicLegend) super.clone();
+    public LegendGraphic clone() {
+        final var clone = (LegendGraphic) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
index 490b5e7c74..961189eac0 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
index 03c8da3b14..351baeba00 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
index 5a4d9bc90d..b15f9d202d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.Marshaller;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
index 79a84d8689..6864f4aa30 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
index d03fb752cc..a27782684c 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointSymbolizer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.Marshaller;
 import jakarta.xml.bind.Unmarshaller;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
index c0ad23911e..719cf6d692 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.Marshaller;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
similarity index 98%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
index 9f045f40ba..0e0b983296 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
@@ -246,7 +246,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @return contrast enhancement for the whole image.
      *
-     * @see SelectedChannelType#getContrastEnhancement()
+     * @see SelectedChannel#getContrastEnhancement()
      */
     public Optional<ContrastEnhancement> getContrastEnhancement() {
         return Optional.ofNullable(contrastEnhancement);
@@ -259,7 +259,7 @@ public class RasterSymbolizer extends Symbolizer implements Translucent {
      *
      * @param  value  new contrast enhancement, or {@code null} if none.
      *
-     * @see SelectedChannelType#setContrastEnhancement(ContrastEnhancement)
+     * @see SelectedChannel#setContrastEnhancement(ContrastEnhancement)
      */
     public void setContrastEnhancement(final ContrastEnhancement value) {
         contrastEnhancement = value;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
similarity index 98%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
index e0dffbfd4d..8d72197b73 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import java.util.ArrayList;
@@ -83,10 +83,10 @@ public class Rule<R> extends StyleElement {
      * Small graphic to draw in a legend window, or {@code null} if none.
      *
      * @see #getLegend()
-     * @see #setLegend(GraphicLegend)
+     * @see #setLegend(LegendGraphic)
      */
     @XmlElement(name = "LegendGraphic")
-    protected GraphicLegend legend;
+    protected LegendGraphic legend;
 
     /**
      * Filter that will limit the features, or {@code null} if none.
@@ -217,7 +217,7 @@ public class Rule<R> extends StyleElement {
      *
      * @return small graphic to draw in a legend window.
      */
-    public Optional<GraphicLegend> getLegend() {
+    public Optional<LegendGraphic> getLegend() {
         return Optional.ofNullable(legend);
     }
 
@@ -228,7 +228,7 @@ public class Rule<R> extends StyleElement {
      *
      * @param  value  new legend graphic, or {@code null} if none.
      */
-    public void setLegend(final GraphicLegend value) {
+    public void setLegend(final LegendGraphic value) {
         legend = value;
     }
 
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
similarity index 93%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
index e5a8cd9e34..ec46a85107 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
@@ -44,7 +44,7 @@ import org.opengis.filter.Expression;
     "contrastEnhancement"
 })
 // No root element is specified in OGC 05-077r4.
-public class SelectedChannelType extends StyleElement {
+public class SelectedChannel extends StyleElement {
     /**
      * The channel's name, or {@code null} if unspecified.
      *
@@ -68,7 +68,7 @@ public class SelectedChannelType extends StyleElement {
     /**
      * Creates an initially empty selected channel.
      */
-    public SelectedChannelType() {
+    public SelectedChannel() {
     }
 
     /**
@@ -76,7 +76,7 @@ public class SelectedChannelType extends StyleElement {
      *
      * @param  name  source channel name.
      */
-    public SelectedChannelType(final String name) {
+    public SelectedChannel(final String name) {
         ArgumentChecks.ensureNonEmpty("name", name);
         sourceChannelName = literal(name);
     }
@@ -87,7 +87,7 @@ public class SelectedChannelType extends StyleElement {
      *
      * @param  source  the object to copy.
      */
-    public SelectedChannelType(final SelectedChannelType source) {
+    public SelectedChannel(final SelectedChannel source) {
         super(source);
         sourceChannelName   = source.sourceChannelName;
         contrastEnhancement = source.contrastEnhancement;
@@ -155,8 +155,8 @@ public class SelectedChannelType extends StyleElement {
      * @return deep clone of all style elements.
      */
     @Override
-    public SelectedChannelType clone() {
-        final var clone = (SelectedChannelType) super.clone();
+    public SelectedChannel clone() {
+        final var clone = (SelectedChannel) super.clone();
         clone.selfClone();
         return clone;
     }
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
index 4f5a300a9d..9bdf2b6641 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlElement;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
index 9a148d5922..c25c3b999e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import java.util.Optional;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
index 17395fc1fb..8cd2d62272 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Style.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import java.util.ArrayList;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleElement.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleElement.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
index c0178ff5b1..6626c50cec 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleElement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Arrays;
 import jakarta.xml.bind.annotation.XmlTransient;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
index 2aaeed071d..f1723cfc2e 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Objects;
 import java.util.Optional;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
similarity index 99%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
index efa37960ee..4fe9b344f5 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Optional;
 import jakarta.xml.bind.annotation.XmlType;
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Translucent.java b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
similarity index 97%
rename from core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Translucent.java
rename to core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
index c4be8f3487..ae8d42628b 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Translucent.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.opengis.feature.Feature;
 import org.opengis.filter.Expression;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
index c98e1b3839..e2b3d026ee 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
@@ -38,10 +38,10 @@ import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.storage.MemoryFeatureSet;
-import org.apache.sis.internal.style.FeatureTypeStyle;
-import org.apache.sis.internal.style.LineSymbolizer;
-import org.apache.sis.internal.style.Rule;
-import org.apache.sis.internal.style.Style;
+import org.apache.sis.style.se1.FeatureTypeStyle;
+import org.apache.sis.style.se1.LineSymbolizer;
+import org.apache.sis.style.se1.Rule;
+import org.apache.sis.style.se1.Style;
 import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.portrayal.MapItem;
 import org.apache.sis.portrayal.MapLayer;
@@ -56,7 +56,7 @@ import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.util.iso.Names;
-import org.apache.sis.internal.style.Symbolizer;
+import org.apache.sis.style.se1.Symbolizer;
 import org.locationtech.jts.geom.CoordinateXY;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.Point;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AnchorPointTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AnchorPointTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java
index d2fec10f12..4267cb5f22 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AnchorPointTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/AnchorPointTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
similarity index 86%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
index 848f7a1c08..c8469efb97 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ChannelSelectionTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
@@ -45,10 +45,10 @@ public final class ChannelSelectionTest extends StyleTestCase {
         assertNull(cdt.getChannels());
 
         // Check get/set
-        var values = new SelectedChannelType[] {
-            new SelectedChannelType("R"),
-            new SelectedChannelType("G"),
-            new SelectedChannelType("B")
+        var values = new SelectedChannel[] {
+            new SelectedChannel("R"),
+            new SelectedChannel("G"),
+            new SelectedChannel("B")
         };
         cdt.setChannels(values);
         assertArrayEquals(values, cdt.getChannels());
@@ -66,8 +66,8 @@ public final class ChannelSelectionTest extends StyleTestCase {
         assertNull(cdt.getChannels());
 
         // Check get/set
-        var values = new SelectedChannelType[] {
-            new SelectedChannelType("Gray")
+        var values = new SelectedChannel[] {
+            new SelectedChannel("Gray")
         };
         cdt.setChannels(values);
         assertArrayEquals(values, cdt.getChannels());
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ContrastEnhancementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ContrastEnhancementTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java
index e7990bc4b2..a2a562f551 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ContrastEnhancementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ContrastEnhancementTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DescriptionTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DescriptionTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java
index 65b8aac7df..313f9398c0 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DescriptionTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DescriptionTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.apache.sis.util.SimpleInternationalString;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DisplacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DisplacementTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java
index 5af1471ce6..6d06aa1ee2 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DisplacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/DisplacementTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
index 23032f3376..d49bd468a3 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ExternalGraphicTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import javax.swing.ImageIcon;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
index 2132cb1f86..aa5fe112a6 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FeatureTypeStyleTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.Set;
 import java.util.List;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
index ab99e646ad..9fb3ee5bdd 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FillTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
index 44d187961a..5d1a76d750 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/FontTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicStrokeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicStrokeTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java
index 2e19774f96..b608915782 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicStrokeTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicStrokeTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
index 0fd1555220..130332ca4d 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/GraphicTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
index 96f9345b77..95b316e2d0 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
index cbab30a0e5..a224c51109 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LinePlacementTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LineSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LineSymbolizerTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
index 14b3220b42..d986a23e87 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LineSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
index 2e97b14eb7..26691fd522 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/MarkTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
index 73564c4bb3..9d89994565 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointSymbolizerTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
index e24852610f..80e24161c4 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
index 449f8d58f4..e1074f47e3 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
similarity index 99%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
index dc98f45e8e..2d6284fcea 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RasterSymbolizerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
index 68376cab96..9acd4ceb92 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.util.List;
 import org.junit.Test;
@@ -84,7 +84,7 @@ public final class RuleTest extends StyleTestCase {
         assertEmpty(cdt.getLegend());
 
         // Check get/set
-        GraphicLegend value = new GraphicLegend();
+        LegendGraphic value = new LegendGraphic();
         cdt.setLegend(value);
         assertOptionalEquals(value, cdt.getLegend());
     }
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SelectedChannelTypeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java
similarity index 85%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SelectedChannelTypeTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java
index 1f3c219ec7..76b214eb52 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SelectedChannelTypeTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SelectedChannelTest.java
@@ -14,24 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
 
 
 /**
- * Tests for {@link SelectedChannelType}.
+ * Tests for {@link SelectedChannel}.
  *
  * @author  Johann Sorel (Geomatys)
  * @version 1.5
  * @since   1.5
  */
-public final class SelectedChannelTypeTest extends StyleTestCase {
+public final class SelectedChannelTest extends StyleTestCase {
     /**
      * Creates a new test case.
      */
-    public SelectedChannelTypeTest() {
+    public SelectedChannelTest() {
     }
 
     /**
@@ -39,7 +39,7 @@ public final class SelectedChannelTypeTest extends StyleTestCase {
      */
     @Test
     public void testChannelName() {
-        SelectedChannelType cdt = new SelectedChannelType();
+        SelectedChannel cdt = new SelectedChannel();
 
         // Check defaults
         assertNull(cdt.getSourceChannelName());
@@ -55,7 +55,7 @@ public final class SelectedChannelTypeTest extends StyleTestCase {
      */
     @Test
     public void testContrastEnhancement() {
-        SelectedChannelType cdt = new SelectedChannelType();
+        SelectedChannel cdt = new SelectedChannel();
 
         // Check defaults
         assertEmpty(cdt.getContrastEnhancement());
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ShadedReliefTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java
similarity index 97%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ShadedReliefTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java
index 1f0083302c..a247aa89fb 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ShadedReliefTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/ShadedReliefTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
similarity index 99%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
index 5f56393021..61d5993e56 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StrokeTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java
index c51313f0e3..34c66e45c1 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTestCase.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTestCase.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
index 6e45f605b3..7171c4d445 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTestCase.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import java.awt.Color;
 import java.util.Optional;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
index 86b5043535..ccc02fea67 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.apache.sis.measure.Units;
 import org.junit.Test;
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
similarity index 98%
rename from core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java
rename to core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
index 38e79d2904..8c7ff576e2 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.internal.style;
+package org.apache.sis.style.se1;
 
 import org.junit.Test;
 import static org.junit.Assert.*;