CartoSym draft check

CartoSym 2.0 draft: judgement calls, rendered

Where the spec text does not settle how a renderer should behave, the draft requirements in PR #133 make a choice. Each card renders the same small map with the draft's choice and the alternatives. Because the draft renderer implements the draft itself, it can show what each choice looks like, but not whether that choice is the intended one: that is for the SWG to decide. Comparing with an independent renderer shows whether others read the requirements the same way, so each card links its style, data and view settings (all in kit.zip), and the playground lets you edit a style and see the result.

All 134 requirements and how each is checked → · Playground: edit a style and see it rendered →

draft the draft's choice. open in draft the draft does not settle the question; the variant labelled "draft renderer's choice" is the proposed resolution. Requirement text from branch requirements-placeholders at 0405c09.

Decision matrix

Proposed resolutions from the PR author, for discussion at the review.

QuestionDraft saysProposed resolution Status in PR #133Question for the SWG
Symbolizer opacity: whole symbol or each partwhole symbol (group opacity)Keep as drafted.unchanged
Casing beneath or above the strokecasing beneath the strokeKeep as drafted.unchanged
Casing at crossingsall casings with the same zOrder beneath the strokesDone, as Jérôme answered and as the painter's algorithm in clause 7 loops (casing passes inside each zOrder).resolved, #147
What casing width meansextends beyond each side of the strokeDone, as Jérôme answered (total = stroke + 2 × casing).resolved, #147
Font outline beneath or above the glyphsoutline beneath the glyphsKeep as drafted (as SE Halo).unchanged
Which zOrder is drawn on tophigher zOrder on topKeep as drafted.unchanged
Hatch angle directionclockwise (table)Probably counter-clockwise from 0 pointing right, as other angles drawn on the map (Jérôme); check graphics toolkits first.TODO(review), #146What do Win32 GDI, X and newer toolkits do? (@ebocher, @oertz, @maxcollombin)
Units of dashPatternpixels unless uom-qualifiedDone, as Jérôme answered (as SE).resolved
Default hot spot of an Imagecentre (0.5, 0.5)Keep as drafted (Jérôme confirmed; same as libCartoSym).resolved; tint and blackTint added, alphaThreshold for picking (permission for the alpha test), #145
The default markera single default Dot (white, 10 px)Keep, as intended (Jérôme); clearing it (marker: null) is now in the NOTE. Fix the schema (anyOf in graphicArray plus a marker change, #143). Done: Dot has size and color, the same values as its stroke's width and color, always a filled circle; CS-JSON forbids setting both in one object (#144).NOTE + TODO(review), #143; resolved, #144Schema change OK?
What a stipple or dot-pattern dot looks likenot statedAdd dot size and color properties; until then, small dots in the fill color.TODO(review), #84Should stipple/dot-pattern dots have their own size and color?
Which label priority winshigher value winsKeep as drafted (as QGIS and GeoServer, and as zOrder); libCartoSym's engine currently places lower values first.TODO(review), #148Higher wins, or rename the property if lower should come first?
Units of label minSpacingpixels unless uom-qualifiedDone, as Jérôme answered.resolved
Labels on top of everythingseparate pass on topKeep as drafted.unchanged
Example 5: colorMap between and beyond entriesinterpolate (method open), exact at entries, clamp outsideDone, as Jérôme answered; libCartoSym interpolates quadratically. SHOULD stay between the two closest entries; stepped classes via conditional expressions (Jérôme).recommendation + NOTE, #149
Direction of coverage gammac ^ gamma (darkens above 1)Remove from coverage (not in the model); if kept, use SE's direction.TODO(review), #86
Example 6: scaling colorChannels0..1; example 6 scales the bandsDone: as Jérôme's renderer, the style scales the bands (÷ 2500 in example 6).resolved; default: TODO(review), #150The default colorChannels (fields[0..2]) is unscaled too: keep it?
Example 7: NDVI with integer bandsreal quotientKeep as drafted (confirmed by Jérôme, as CQL2); libCartoSym should change (it returns 0 for example 7).unchangedOpen the libCartoSym issue?
Example 8: what hillShading factor meansmultiplies the horizontal components of the normalDone, as Jérôme's renderer, including the sample distance and the shade without a colorMap.resolved
Example 9: shading intensity mapped to color and opacitydot product, 0 (full shade) to 1 (no shade)Done, as Jérôme described.resolved
Direction of the sun azimuthclockwise from north, a compass bearingDone, as Jérôme answered (as GDAL).resolved
Gradient angle conventiondegrees, 0 pointing right, turning counter-clockwiseDone, as Jérôme answered.resolved
Gradient position and size: per feature or per mappixels unless uom-qualified; frame not stated (Part 2)Wait until two or three renderers can be compared (Jérôme).TODO(review), #153
notLike, notContains, notStartsWith, notEndsWithseparate operationsEncode as not applied to the positive form (as CQL2); no schema change.TODO(review), #140
caseInsensitize / accentInsensitize vs casei / accentilong names (model); short in the schemaUse the CQL2 names casei and accenti.TODO(review), #140
What counts as an accent (Ø)“removes accents”Reference CQL2 Requirement 9. Either remove nonspacing marks only (Ø stays, as this renderer does), or also fold with a named one-to-one table (Ø → O, as libCartoSym does).TODO(review), #152Which of the two?
Right-hand identifiers vs the CSCSS examplesany expression on the right-hand sideDone: where an enumeration is expected, a bare word is an enumeration value (clause 17); a NOTE now points there.resolved, #151
center vs centerLinecenter (UML)Decide one name.TODO(review), #120Which name is intended?
Schema: colors from expressions, empty arrays, and clearing the marker(schema on main)Make the oneOf branches exclusive (e.g., require r, g, b in the color object). For the marker: anyOf in graphicArray, and marker accepting null and an object without elements (tested).not in PR (schema), #140, #143
format string syntaxC printf conversion specifiers (ANSI C)Done, as Jérôme answered.resolved
intDiv in libCartoSym 0.0.1truncating integer divisionFix in libCartoSym: cs-canif crashes on 7 div 2.n/a (implementation), ecere/libCartoSym#5

Part 1: vector styling

Symbolizer opacity: whole symbol or each part

What the draft says
/req/core/opacity
  • An Encoding SHALL support defining an opacity Symbolizer property as a float between 0 (fully transparent) and 1 (fully opaque).
  • A Renderer SHALL support portraying a feature or coverage cell with the opacity specified by the resolved opacity property, applied to the portrayal as a whole.

With opacity: 0.5, is the symbol made translucent as a whole, or are fill and stroke made translucent separately? Separately, the stroke and fill overlap shows through.

Whole symbol (group opacity)draft
Fill and stroke separatelyWhere the stroke overlaps the fill, a darker band shows inside the stroke.

The draft says the opacity applies to the portrayal as a whole, as SVG and CSS group opacity do. Applied to fill and stroke separately, the overlap shows through.

Render it yourself: style.cs.json · Background.geojson · Parcels.geojson · view.json↑ Matrix

Casing beneath or above the stroke

What the draft says
/req/casing/casing
  • An Encoding SHALL support defining a casing Stroke property as a StrokeStyling object.
  • A Renderer SHALL support drawing the casing of a stroke along the same geometry as the stroke, using the StrokeStyling specified by the casing property, beneath the main stroke.
  • A Renderer SHALL support drawing the casing so that it extends the width of the casing beyond each side of the main stroke, for a total width of the main stroke's width plus twice the casing's width.
  • A Renderer SHALL support drawing, within a single visualization pass, the casings of all features with the same zOrder beneath the main strokes of all those features, so that the casings of crossing and joining lines merge.
/req/casing/center
  • An Encoding SHALL support defining a center Stroke property as a StrokeStyling object.
  • A Renderer SHALL support drawing the center line of a stroke along the same geometry as the stroke, using the StrokeStyling specified by the center property, on top of the main stroke.

A road with a 14 px yellow stroke, a red casing extending 4 px on each side at 60 % opacity (so what lies under it shows through) and a thin white centre line. Is the casing drawn beneath the stroke (an outline) or on top of it?

Casing beneathdraftThe yellow stroke lies on top; the casing shows as red edges.
Casing aboveThe casing lies over the stroke: the yellow shows through only as orange.

The draft says the casing is drawn beneath the main stroke. libCartoSym models casing and center as StrokeStyling, like the draft.

Render it yourself: style.cs.json · Roads.geojson · view.json↑ Matrix

Casing at crossings

What the draft says
/req/casing/casing
  • An Encoding SHALL support defining a casing Stroke property as a StrokeStyling object.
  • A Renderer SHALL support drawing the casing of a stroke along the same geometry as the stroke, using the StrokeStyling specified by the casing property, beneath the main stroke.
  • A Renderer SHALL support drawing the casing so that it extends the width of the casing beyond each side of the main stroke, for a total width of the main stroke's width plus twice the casing's width.
  • A Renderer SHALL support drawing, within a single visualization pass, the casings of all features with the same zOrder beneath the main strokes of all those features, so that the casings of crossing and joining lines merge.

Two cased roads crossing. Does each road draw its casing together with its own stroke, or are all casings drawn before any stroke, so the crossing merges?

All casings firstdraftThe crossing merges, as in most road maps.
Each road with its own casingThe second road's casing cuts across the first road.

The draft now says that within a visualization pass, the casings of all features with the same zOrder are drawn beneath their main strokes (Jérôme, #147; also the painter's algorithm in clause 7, which loops over casing passes inside each zOrder). A feature with a higher zOrder, such as a bridge, has its casing drawn over the roads below it. Drawing each feature in turn is possible with visualization or feature passes.

Sources: Jérôme, #147: equivalent to drawing all casings first

Render it yourself: style.cs.json · Roads.geojson · view.json↑ Matrix

What casing width means

What the draft says
/req/casing/casing
  • An Encoding SHALL support defining a casing Stroke property as a StrokeStyling object.
  • A Renderer SHALL support drawing the casing of a stroke along the same geometry as the stroke, using the StrokeStyling specified by the casing property, beneath the main stroke.
  • A Renderer SHALL support drawing the casing so that it extends the width of the casing beyond each side of the main stroke, for a total width of the main stroke's width plus twice the casing's width.
  • A Renderer SHALL support drawing, within a single visualization pass, the casings of all features with the same zOrder beneath the main strokes of all those features, so that the casings of crossing and joining lines merge.

The stroke is 12 px wide and casing.width is 3 px. Is 3 px the casing's total width, or how far it extends beyond each side of the stroke?

Extra on each side (12 + 2 × 3 px)draft
Total width: 3 px, hidden under the stroke

The draft now says the casing extends its width beyond each side of the main stroke, for a total of stroke width + 2 × casing width (Jérôme, #147). Annex C's road example agrees: its casings of 0.25 to 1 px under strokes of 2.5 to 6 px would be hidden under the total-width reading.

Sources: Jérôme, #147: total width = main width + 2 × casing width

Render it yourself: style.cs.json · Roads.geojson · view.json↑ Matrix

Font outline beneath or above the glyphs

What the draft says
/req/font-outlines/outline
  • An Encoding SHALL support defining an outline Font property as a FontOutline object including a size, an opacity and a color.
  • A Renderer SHALL support drawing an outline around the glyphs of text drawn with a Font specifying an outline, beneath the glyphs themselves.
/req/font-outlines/size
  • An Encoding SHALL support defining a size FontOutline property as a float.
  • A Renderer SHALL support drawing the outline extending outward from the glyph contours by the thickness specified by the size property.

Text with a 2 px white outline over a dark background. Is the outline drawn beneath the glyphs (a halo) or on top of them?

Trondheim
Outline beneath (halo)draft
Trondheim
Outline aboveThe outline is drawn over the glyphs and hides their shapes.

Outlines beneath the glyphs are the convention: SE draws a Halo on the backgrounds of the glyphs, QGIS a text buffer, MapLibre a text-halo.

Sources: SE 1.1, 11.4.5 Halo: applied to the backgrounds of font glyphs · QGIS label buffer

Render it yourself: style.cs.json · Background.geojson · Places.geojson · view.json↑ Matrix

Which zOrder is drawn on top

What the draft says
/req/core/z-order
  • An Encoding SHALL support defining a zOrder Symbolizer property as an integer.
  • A Renderer SHALL support portraying features and coverage cells of a single rendering pass in increasing order of their resolved zOrder property, so that portrayals with a higher zOrder are drawn on top of those with a lower zOrder.

Two overlapping features. Lakes come first in the data but have zOrder: 2; Forest has zOrder: 1. Is the higher zOrder drawn on top?

Higher zOrder on topdraft
Lower zOrder on top

The draft says portrayals with a higher zOrder are drawn on top. The earlier clause 7 text only said that zOrder modifies the default order, not in which direction.

Render it yourself: style.cs.json · Lakes.geojson · Forest.geojson · view.json↑ Matrix

Hatch angle direction

What the draft says
/req/hatches/angle
  • An Encoding SHALL support defining an angle property for the hatch fill symbolizer.
  • A Renderer SHALL support fill hatching vector geometry based on an angle property.

A hatch fill with angle: 30. Clockwise or counter-clockwise from horizontal?

Clockwise (draft table: "clockwise degree")draft
Counter-clockwise (mathematical convention)

The Part 1 Hatch table says "Hatches orientation clockwise degree". libCartoSym still uses the older forward/backward/cross hatch model.

Render it yourself: style.cs.json · Zones.geojson · view.json↑ Matrix

Units of dashPattern

What the draft says
/req/dashes/dash-pattern
  • An Encoding SHALL support defining a dashPattern property for the stroke symbolizer.
  • A Renderer SHALL support dashing vector geometry based on a dashPattern property, whose lengths are in pixels unless qualified with another unit (<<graphical-units-of-measure,Graphical Units of Measure>>).

A 6 px wide line with dashPattern: [4, 2]. Are the values pixels, or multiples of the stroke width?

Pixelsdraft
Multiples of stroke width

Jérôme: units are as in Graphical Units of Measure, pixels unless qualified with another unit. The PR now says so for dashPattern, as SE does for stroke-dasharray; MapLibre's line-dasharray is in multiples of the line width.

Sources: SE 1.1, Stroke: stroke-dasharray lengths in the symbolizer's units of measure · GeoServer: stroke-dasharray in pixels · MapLibre line-dasharray: units in line widths

Render it yourself: style.cs.json · Boundaries.geojson · view.json↑ Matrix

Default hot spot of an Image

What the draft says
/req/vector/image
  • An Encoding SHALL support defining an Image Graphic with an image property referencing an image resource, an optional uom-qualified hotSpot position within the image, optional tint and blackTint Colors, and an optional alphaThreshold real number between 0 and 1.
  • A Renderer SHALL support drawing the referenced image so that its hotSpot (by default, its center, { 0.5, 0.5 }) is located at the position of the Graphic.
  • A Renderer SHALL support recoloring the image so that each red, green and blue component _v_ (between 0 and 1) of each pixel becomes tint × _v_ + blackTint × (1 − _v_), using the same component of tint and blackTint.

A pin icon on a point (black cross = the point's location). Without an explicit hotSpot, which part of the image sits on the point?

Centre (0.5, 0.5)draft
Top left (0, 0)
Bottom centre (0.5, 1)

Jérôme: the default hot spot is the centre { 0.5, 0.5 }, since centred icons are by far the most common case (after first saying top-left). This is what the draft says and what libCartoSym does (GraphicalElement.ec). Pin-shaped icons need the bottom centre, so styles would set it explicitly.

Render it yourself: style.cs.json · POI.geojson · view.json↑ Matrix

The default marker

What the draft says
/req/core/default-values
  • An Encoding SHALL support omitting Symbolizer properties and object members for which a default value is defined in this section.
  • A Renderer SHALL support using the default values defined in this section for Symbolizer properties and object members which are not set by any selected StylingRule.
/req/vector/marker
  • An Encoding SHALL support defining a marker Symbolizer property as an array of graphical elements (Graphics).
  • A Renderer SHALL support drawing the graphical elements of a marker at the points of vector geometry (for points and line strings) and at the centroid of polygons.
/req/vector/dot
  • An Encoding SHALL support defining a Dot Graphic inheriting from a base Shape class specifying a stroke, with a uom-qualified size property and a color property that are the same values as the width and the color of its stroke.
  • A Renderer SHALL support setting the width and the color of the stroke of a Dot when its size and color are set, and the reverse, so that the value set last applies to both.
  • A Renderer SHALL support drawing a Dot Graphic as a filled circle whose diameter is its size (the width of its stroke), in its color (the color of its stroke).

A style that sets only a fill and a stroke, and no marker. Clause 7's defaults say the default Marker is a single default Dot (white, 10 px), and markers are drawn at points, line vertices and polygon centroids. Does every feature get a white dot?

The default Dot, as the table saysdraftA white 10 px dot at the point, at every line vertex and at the polygon centroid.
No markerWhat a style that clears the marker should give.

Jérôme: intended, so that every feature type is visible by default, and a single default Symbolizer is intended; a typical base style unsets the marker for lines and polygons, e.g. [dataLayer.featuresGeometryDimensions > 0] { marker: null; }. marker: { elements: [] } is equivalent, while marker: { } still yields the default Dot; the draft renderer does the same, and the PR's NOTE now says so. Open: the JSON schema rejects all three forms, and the Dot defaults (size 10 px, color white) set properties the Dot class does not have: clause 8 and the schema draw a Dot with its stroke's width and color. The other cards leave the default marker off, so they stay readable.

Render it yourself: style.cs.json · Things.geojson · view.json↑ Matrix

What a stipple or dot-pattern dot looks like open in draft

What the draft says
/req/stipples/ratio
  • An Encoding SHALL support defining a ratio property for the stipple fill symbolizer.
  • A Renderer SHALL support stipple fill vector geometry based on a ratio property.
/req/dot-patterns/dotpattern
  • An Encoding SHALL support defining a dotpattern property within the Fill class, defined as a Dotpattern class.
  • A Renderer SHALL support filling vector geometry with a regular grid of points based on a dotpattern property.
/req/dot-patterns/distance
  • An Encoding SHALL support defining a distance Dotpattern property as a Distance object including a horizontal and a vertical float.
  • A Renderer SHALL support spacing the rendered points of a dot pattern fill horizontally by the horizontal distance and vertically by the vertical distance.

A stipple fill (ratio: 0.15, left) and a dot pattern (distance: 14 px, right), both with a dark green fill color. Neither the requirements nor the model say how big a dot is or which color it has.

Small dots in the fill color (draft renderer's choice)
Dot class defaults (white, 10 px)

The draft leaves this open; #84 asks whether the Dot class defaults apply. Rendered, white 10 px dots read as a different symbol, and the fill color no longer shows.

Render it yourself: style.cs.json · Ground.geojson · Stipple.geojson · Dots.geojson · view.json↑ Matrix

Which label priority wins

What the draft says
/req/labels/label
  • An Encoding SHALL support defining a label Symbolizer property as a Label, which is a MultiGraphic that can contain multiple graphical elements.
  • A Renderer SHALL support drawing the graphical elements of the labels as a separate pass on top of all styled vector features and coverages.
  • A Renderer SHALL support omitting labels which cannot be placed without overlapping other labels.
/req/labels/placement
  • An Encoding SHALL support defining a placement Label property as a LabelPlacement object including a priority, a minSpacing and a maxSpacing.
  • A Renderer SHALL support placing labels so that a label with a higher priority is placed in preference to a conflicting label with a lower priority.
  • A Renderer SHALL support repeating a label so that the distance between repetitions of that label is at least minSpacing and, where space allows, at most maxSpacing, both in pixels unless qualified with another unit (<<graphical-units-of-measure,Graphical Units of Measure>>).

Two nearby places whose labels overlap. Lillestrøm has priority: 2, Strømmen priority: 8. Only one label fits. Which one is kept?

Strømmen
Higher value winsdraftStrømmen (priority 8) is kept.
Lillestrøm
Lower value wins (1 = most important)Lillestrøm (priority 2) is kept.

The draft proposes that a higher value wins (#148). In QGIS and GeoServer the label with the higher priority wins; MapLibre's symbol-sort-key places lower values first, and so, for now, does libCartoSym's (incomplete) placement engine (Jérôme). Still open; the proposal is to keep the name priority with higher winning, or rename it if lower should come first.

Sources: QGIS: the label with the higher priority is displayed · GeoServer: higher priority values take precedence · MapLibre symbol-sort-key: lower values placed first

Render it yourself: style.cs.json · Places.geojson · view.json↑ Matrix

Units of label minSpacing

What the draft says
/req/labels/placement
  • An Encoding SHALL support defining a placement Label property as a LabelPlacement object including a priority, a minSpacing and a maxSpacing.
  • A Renderer SHALL support placing labels so that a label with a higher priority is placed in preference to a conflicting label with a lower priority.
  • A Renderer SHALL support repeating a label so that the distance between repetitions of that label is at least minSpacing and, where space allows, at most maxSpacing, both in pixels unless qualified with another unit (<<graphical-units-of-measure,Graphical Units of Measure>>).

A road labelled with minSpacing: 60 at 5 m per pixel. Is 60 in pixels or in metres?

E6E6E6E6E6E6E6E6E6E6
PixelsdraftAt least 60 px between repeated labels.
E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6E6
Metres (map units)60 m is 12 px here, so labels repeat as closely as they fit.

Jérôme: units are as in Graphical Units of Measure, pixels unless qualified with another unit. The PR now says so for minSpacing and maxSpacing; a style that wants metres qualifies the value.

Render it yourself: style.cs.json · Roads.geojson · view.json↑ Matrix

Labels on top of everything

What the draft says
/req/labels/label
  • An Encoding SHALL support defining a label Symbolizer property as a Label, which is a MultiGraphic that can contain multiple graphical elements.
  • A Renderer SHALL support drawing the graphical elements of the labels as a separate pass on top of all styled vector features and coverages.
  • A Renderer SHALL support omitting labels which cannot be placed without overlapping other labels.

A labelled harbour point, and a pier polygon drawn after it (higher zOrder). Is the label drawn in a separate pass on top, or with its own feature?

Harbour
Separate pass on topdraft
Harbour
With its own featureThe pier, drawn later, covers part of the label.

The draft requires the separate pass. Drawn with the feature, a later feature can cover the label, as on the right.

Render it yourself: style.cs.json · Harbours.geojson · Piers.geojson · view.json↑ Matrix

Part 1: coverage styling (the spec's examples 5–9)

The cards use real data of Rondane, Norway, 45 × 30 km with 150 m cells: elevation (elevation data © Kartverket, CC BY 4.0), with the examples' color map stretched from 0–2500 m to 500–2000 m, and a Sentinel-2 L2A scene of 2 August 2024 (contains modified Copernicus Sentinel data 2024). Only the pyramids are synthetic (see the README). The styles are the spec's examples, unchanged unless the question says otherwise.

Example 5: colorMap between and beyond entries

What the draft says
/req/coverage/color-map
  • An Encoding SHALL support defining a colorMap Symbolizer property as an array of value (float) and color (Color) pairs which can be tagged with a name.
  • A Renderer SHALL support portraying each coverage cell with the color mapped from its singleChannel value by the colorMap, using the color of an entry for a value equal to that entry's value, interpolating between the colors of the two closest entries for other values within the range of the map, and using the color of the first or last entry for values outside the range of the map.
/req/coverage/opacity-map
  • An Encoding SHALL support defining a opacityMap Symbolizer property as an array of value (float) and opacity (float) pairs which can be tagged with a name.
  • A Renderer SHALL support portraying each coverage cell with the opacity mapped from its singleChannel value by the opacityMap, using the opacity of an entry for a value equal to that entry's value, interpolating between the opacities of the two closest entries for other values within the range of the map, and using the opacity of the first or last entry for values outside the range of the map.
/rec/coverage/map-interpolation
  • A Renderer SHOULD interpolate the colors of a colorMap and the opacities of an opacityMap so that the result lies between those of the two closest entries.

Example 5's color map, stretched from 0–2500 m to Rondane's 500–2000 m (same colors). Between entries, are colors interpolated or stepped? Below 500 m (the valley floors) and above 2000 m (the summits), is the first or last color used, or nothing?

Interpolate, clamp outsidedraft
Step between entries
Transparent outside the mapThe valley floors below 500 m are left out. So are the summits above 2000 m, but next to the map's near-white last color they hardly show.

Clamping is confirmed (Jérôme, #149). The draft now says that colors are interpolated between entries without fixing the method (libCartoSym interpolates quadratically; this renderer linearly), and that a value equal to an entry takes exactly that entry's color. A recommendation says the result should stay between the two closest entries (a parabola through three entries can overshoot). Stepped classes for continuous values are written with conditional expressions (Jérôme: OK for Part 1). SE 1.1 makes the choice explicit with Interpolate and Categorize functions (its own DEM example uses Categorize); GeoServer's ColorMap defaults to an interpolated ramp.

Sources: SE 1.1, RasterSymbolizer ColorMap: Interpolate or Categorize · GeoServer: ColorMap type ramp (interpolated) is the default

Render it yourself: style.cs.json · Elevation.elevation.asc · view.json↑ Matrix

Direction of coverage gamma

What the draft says
/req/coverage/gamma
  • An Encoding SHOULD support defining a gamma Symbolizer property as a float.
  • A Renderer SHOULD support applying a gamma correction to the portrayed color values, using the value of the gamma property as the exponent applied to normalized (0 to 1) color components.

Example 5 with gamma: 2 (its color map stretched to Rondane's 500–2000 m). Does a gamma above 1 darken or brighten?

c ^ gamma: darkensdraft
c ^ (1 / gamma): brightens, as SE

SE 1.1 GammaValue brightens for values above 1. The draft's exponent does the opposite. gamma is also absent from the coverage UML, tables and schema (#86), so removing it is an option.

Sources: SE 1.1, ContrastEnhancement: GammaValue above 1.0 brightens

Render it yourself: style.cs.json · Elevation.elevation.asc · view.json↑ Matrix

Example 6: scaling colorChannels

What the draft says
/req/coverage/color-channels
  • An Encoding SHALL support defining a colorChannels Symbolizer property as a Color object.
  • A Renderer SHALL support portraying each coverage cell with the red, green and blue components resolved from the colorChannels property for that cell, each in the range 0 to 1.
/req/coverage/alpha-channel
  • An Encoding SHALL support defining a alphaChannel Symbolizer property as a float.
  • A Renderer SHALL support portraying each coverage cell with the opacity resolved from the alphaChannel property for that cell, where 0 is fully transparent and 1 is fully opaque.

Example 6 set colorChannels: B04 B03 B02, Sentinel-2 digital numbers (reflectance × 10 000), while the schema types colorChannels as 0..1. The PR now scales the bands in the style: colorChannels.r: B04 / 2500 and so on. Here on a real Sentinel-2 L2A scene of Rondane (2 August 2024).

Scaled in the style, ÷ 2500draftTrue color: grey rock and lichen above the tree line, green valleys and forest. The white patches are clouds the example's SCL rules hide (alphaChannel 0 or 0.5).
The original example: unscaled bandsBand values in the hundreds and thousands clamp to 1, so every cell is drawn white. The faint shapes are the clouds, which the SCL rules make transparent or half transparent.

Jérôme: colorChannels is 0..1 and the example assumed pre-scaled bands; one of his working examples divides each band by 2500. The PR now states the 0..1 range and scales example 6 that way. Still to raise: the defaults table gives colorChannels as fields[0], fields[1], fields[2], unscaled (#150).

Render it yourself: style.cs.json · style-variant-2.cs.json · sentinel2-l2a.B02.asc · sentinel2-l2a.B03.asc · sentinel2-l2a.B04.asc · sentinel2-l2a.B08.asc · sentinel2-l2a.SCL.asc · view.json↑ Matrix

Example 7: NDVI with integer bands

What the draft says
/req/arithmetic/div
  • An Encoding SHALL support defining a div operation expression taking two numeric operands.
  • A Renderer SHALL support evaluating a div operation expression as the real-valued quotient of the first operand divided by the second operand, including when both operands are integers.
/req/coverage/single-channel
  • An Encoding SHALL support defining a singleChannel Symbolizer property as a float.
  • A Renderer SHALL support using the value resolved from the singleChannel property for each coverage cell as the input value to the colorMap and opacityMap properties.
/req/coverage/color-map
  • An Encoding SHALL support defining a colorMap Symbolizer property as an array of value (float) and color (Color) pairs which can be tagged with a name.
  • A Renderer SHALL support portraying each coverage cell with the color mapped from its singleChannel value by the colorMap, using the color of an entry for a value equal to that entry's value, interpolating between the colors of the two closest entries for other values within the range of the map, and using the color of the first or last entry for values outside the range of the map.

Example 7 computes singleChannel: (B08 - B04)/(B08 + B04). Sentinel-2 L2A bands are integers; here a real scene of Rondane (2 August 2024). Is / a real division?

Real quotientdraftGreen: forest and vegetated valleys. Brown: bare rock above the tree line. White: clouds hidden by the SCL rules.
Integer quotient for two integers (libCartoSym)Every cell evaluates to 0, so the whole scene gets the color for NDVI 0.0.

Jérôme confirmed: / is real-valued division and div is integer division, as in CQL2 arithmetic expressions. libCartoSym's cs-canif (0.0.1 and main) evaluates (3000 - 1000) / (3000 + 1000) to 0, and to 0.5 only when an operand is real; with integer division every NDVI value truncates to 0, so the example's color map collapses to one color. Cloud cells are hidden by the example's SCL rules (alphaChannel 0 and 0.5).

Sources: Test report: arithmetic/div cases

Render it yourself: style.cs.json · sentinel2-l2a.B02.asc · sentinel2-l2a.B03.asc · sentinel2-l2a.B04.asc · sentinel2-l2a.B08.asc · sentinel2-l2a.SCL.asc · view.json↑ Matrix

Example 8: what hillShading factor means

What the draft says
/req/hill-shading/hill-shading
  • An Encoding SHALL support defining a hillShading Symbolizer property as an object including factor, sun, colorMap and opacityMap properties.
  • A Renderer SHALL support computing the terrain surface normal of each cell from the differences in elevation between neighbouring samples of the elevation coverage at the resolution being rendered, over the horizontal distance between those samples, in the same units as the elevation.
  • A Renderer SHALL support portraying an elevation coverage with a shade value computed for each cell as the dot product of the unit normal of the terrain surface and the unit vector pointing towards the sun, clamped to the range 0 to 1, where 0 is full shade (the sun at 90 degrees or more from the normal) and 1 is no shade (the sun in the direction of the normal).
  • A Renderer SHALL support, where the HillShading has no colorMap, multiplying the red, green and blue components of each cell by its shade value.
/req/hill-shading/factor
  • An Encoding SHALL support defining a factor HillShading property as a float.
  • A Renderer SHALL support multiplying the horizontal components of the terrain surface normal by the factor property before normalizing it, so that a factor of 1 shades the terrain as it is and a larger factor exaggerates the relief.

Example 8 shades the DEM color map with factor: 56, sun at azimuth 45°, elevation 60°. Here on Rondane at 150 m cells, with the color map stretched from 0–2500 m to Rondane's 500–2000 m (same colors), and the factors of Jérôme's renders (1, 8, 32). Over what distance is the slope measured?

factor: 56, as vertical exaggeration (as in Jérôme's renderer)draft47 % of the cells are in full or near-full shade (below 0.05): the relief is saturated.
factor: 1The terrain as it is: no cell darker than 0.24.
factor: 8Strong relief: 12 % of the cells are in full or near-full shade.
factor: 3241 % of the cells are in full or near-full shade.
For reference: no hill shadingThe stretched color map alone.
factor: 56 as darkening × factor (the earlier draft text)99 % of the cells are at or near the darkest shade.

Jérôme: factor multiplies the horizontal components of the terrain normal before normalization, so factor 1 is the plain dot product of the sun direction and the normal. The PR now says so, and the draft renderer uses the same formula. His renders of the Tibetan Plateau, with pixels of several km, show faint relief at 1, clear relief at 8 and strong relief at 32 and 64. On Rondane at 150 m cells the slopes per cell are steeper than at his pixel size, so the same factor gives stronger relief: 1 already shows it clearly, 8 is strong, and the example's 56 saturates it. Jérôme: the normal is computed between the samples of the DEM at the resolution being rendered, over the horizontal distance between them, in the same units as the heights, so the factor that suits a style depends on that resolution; and without a hillShading colorMap the cell's color is multiplied by the shade, as in a typical shader. The draft renderer does both, and the PR now says so.

Sources: Jérôme's renders for factor 1, 8, 32 and 64 (PR #133) · gdaldem hillshade: -z is a vertical exaggeration

Render it yourself: style.cs.json · style-variant-2.cs.json · style-variant-3.cs.json · style-variant-4.cs.json · style-variant-5.cs.json · Elevation.elevation.asc · view.json↑ Matrix

Example 9: shading intensity mapped to color and opacity

What the draft says
/req/hill-shading/factor
  • An Encoding SHALL support defining a factor HillShading property as a float.
  • A Renderer SHALL support multiplying the horizontal components of the terrain surface normal by the factor property before normalizing it, so that a factor of 1 shades the terrain as it is and a larger factor exaggerates the relief.
/req/hill-shading/color-map
  • An Encoding SHALL support defining a colorMap HillShading property as an array of value (float) and color (Color) pairs.
  • A Renderer SHALL support portraying each cell with the color mapped from its computed shade value by the colorMap, as for the colorMap Symbolizer property.
/req/hill-shading/opacity-map
  • An Encoding SHALL support defining a opacityMap HillShading property as an array of value (float) and opacity (float) pairs.
  • A Renderer SHALL support portraying each cell with the opacity mapped from its computed shade value by the opacityMap, as for the opacityMap Symbolizer property.

Example 9 maps the shade (0..1) to black–white and to opacity 0.75–0, for use as an overlay. Here it is drawn over example 5's elevation colors (a second layer, Terrain), stretched from 0–2500 m to Rondane's 500–2000 m. With factor: 56, how much of the terrain does it darken?

factor: 56, as vertical exaggeration (as in Jérôme's renderer)draftThe overlay darkens 84 % of the cells: only a shade below 0.55 shows.
factor: 1Only the steepest slopes facing away from the sun (2 % of the cells) are darkened.
factor: 8The overlay darkens 48 % of the cells.
factor: 32The overlay darkens 76 % of the cells.
For reference: no overlayThe stretched color map alone.
factor: 56 as darkening × factor (the earlier draft text)99 % of the cells are at or near the darkest shade.

Jérôme: the shade is the dot product of the sun direction and the terrain normal, from 0 (the sun at 90° from the normal, full shade) to 1 (no shade), and is the key for the hillShading colorMap and opacityMap. His example uses a grayscale colorMap with an opacityMap that is more opaque in the shade, so imagery shows through on flat ground, as example 9 does. The PR now states this. How much is darkened depends on the factor and the distance the normal is computed over (see example 8).

Sources: Jérôme's renders for factor 1, 8, 32 and 64 (PR #133) · gdaldem hillshade: -z is a vertical exaggeration

Render it yourself: style.cs.json · style-variant-2.cs.json · style-variant-3.cs.json · style-variant-4.cs.json · style-variant-5.cs.json · Terrain.elevation.asc · Elevation.elevation.asc · view.json↑ Matrix

Direction of the sun azimuth

What the draft says
/req/hill-shading/sun
  • An Encoding SHALL support defining a sun HillShading property as an object consisting of azimuth and elevation angle members representing the orientation of the sun relative to the terrain.
  • A Renderer SHALL support computing the shade of each cell for a light source at the azimuth angle (in degrees clockwise from North, as a compass bearing) and elevation angle (relative to the ground) specified by the sun property.

Three pyramids, shaded with example 8's style, the sun at azimuth 315°, elevation 45°, factor: 1, in plain grey. Is 315° north-west (compass bearing) or south-east (counter-clockwise from east)?

Clockwise from north, a compass bearingdraftLight from the north-west: slopes facing the top left are bright.
Counter-clockwise from eastLight from the south-east: slopes facing the bottom right are bright.

Jérôme: directions on the ground, such as the sun azimuth, are compass bearings, clockwise from north. Angles drawn on the map, such as a gradient's, start at 0 pointing right and turn counter-clockwise. The PR now says so for the azimuth. Both examples use 45°, which is north-east as a bearing. GDAL and most GIS software use compass bearings too.

Sources: gdaldem hillshade: azimuth 0 = from the top, 90 = from the east

Render it yourself: style.cs.json · Elevation.elevation.asc · view.json↑ Matrix

Part 2: gradient fills

Gradient angle convention

What the draft says
/req/gradients/angle
  • An Encoding SHALL support defining an angle Gradient property as a float.
  • A Renderer SHALL support orienting the gradient according to the angle property, in degrees, where 0 points to the right and angles turn counter-clockwise as seen on the map.
/req/gradients/keys
  • An Encoding SHALL support defining a keys Gradient property as an array of ColorKey objects, each including a color, an optional opacity and an optional percent position along the gradient.
  • A Renderer SHALL support interpolating the color and opacity of the gradient between consecutive keys, according to their percent position along the gradient.

A linear gradient with angle: 120, from navy through red to gold. Where does 0° point, and which way does the angle turn?

Mathematical: 0° right, counter-clockwisedraft
CSS: 0° up, clockwise

Jérôme: angles drawn on the map are in degrees, start at 0 pointing right and turn counter-clockwise. The PR now says so for the gradient angle. The Part 1 hatch table, however, says "clockwise degree", and the sun azimuth is relative to north, so whether this applies to every angle is still to confirm.

Sources: CSS linear-gradient: 0deg = to top, clockwise

Render it yourself: style.cs.json · Zones.geojson · view.json↑ Matrix

Gradient position and size: per feature or per map open in draft

What the draft says
/req/gradients/position
  • An Encoding SHALL support defining a position Gradient property.
  • A Renderer SHALL support positioning the origin of the gradient according to the position property, in pixels unless qualified with another unit.
/req/gradients/size
  • An Encoding SHALL support defining a size Gradient property.
  • A Renderer SHALL support scaling the extent of the gradient according to the size property, in pixels unless qualified with another unit.
/req/gradients/type
  • An Encoding SHALL support defining a type Gradient property with a value of linear, radial, repeating or conic.
  • A Renderer SHALL support filling vector geometry with a linear, radial, repeating or conic gradient according to the type property.

Three parcels with a radial gradient, position: [0.5, 0.5], size: 1. Relative to each feature, or to the map?

Each feature's bounding box (draft renderer's choice)Each parcel has its own centred gradient.
The whole mapOne gradient centred on the map; each parcel shows its part of it.

position and size have no reference frame in Part 2. Per feature, every polygon gets its own centred gradient; per map, one gradient runs across all features. Jérôme: units are pixels unless qualified, which the PR now says; this card's unqualified 0.5 and 1 are read as fractions of the frame, which under the draft needs a percent unit. The repeating type also needs a definition (repeating what, and how often). Jérôme (#153): decide once two or three renderers can be compared.

Render it yourself: style.cs.json · Parcels.geojson · view.json↑ Matrix