Table of Contents

Class SymbolThinning

Namespace
EncDotNet.S100.Pipelines.Coverage
Assembly
EncDotNet.S100.Core.dll

Screen-space thinning of a field of scaled point symbols (e.g. S-111 surface-current arrows), shared by the Mapsui and headless Skia renderers so both draw the same arrows at the same zoom.

public static class SymbolThinning
Inheritance
SymbolThinning
Inherited Members

Remarks

S-98 Ed 2.0.0 §13.1 requires thinning when gridded data is drawn as discrete symbols, and leaves the algorithm to the implementer. Its recommended algorithm (S-98 Appendix G-1.1, restated in S-111 Ed 2.0.0 §9.3.2 and Annex H Rule 11) is implemented by ThinGrid(int, int, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<float>, ReadOnlySpan<float>, double, SymbolRect, double, List<int>); the point-by-point method S-111 §9.3.2 describes for irregularly spaced data (and §9.3.3 allows for ungeorectified grids) is implemented by ThinPoints(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, double, List<int>).

Both methods use the same ratio Rmax between the largest symbol and the displayed spacing, so a regular grid and a mesh with the same node density thin to the same on-screen density. All coordinates and lengths passed to one call must share a unit (typically EPSG:3857 metres, with symbol lengths converted from pixels through the map resolution).

Fields

DefaultMaxSymbolToSpacingRatio

The default ratio Rmax of the largest symbol's length to the displayed symbol spacing: 0.5, the value S-111 Ed 2.0.0 §9.3.2 and S-98 Appendix G-1.1 give. With 0.5 the drawn grid-cell diagonal is at least twice the largest arrow (and the axis spacing about 1.41 arrows), so arrows never overlap.

public const double DefaultMaxSymbolToSpacingRatio = 0.5

Field Value

double

PixelsPerMillimetre

Device-independent pixels per millimetre (96 / 25.4). The renderers rasterise millimetre-dimensioned S-100 SVG symbols at 96 DPI, so a symbol length given in millimetres is converted to on-screen pixels with this factor.

public const double PixelsPerMillimetre = 3.7795275590551185

Field Value

double

Methods

GridIncrement(double, double, double)

The grid increment n of S-98 Appendix G-1.1 (S-111 Eqn 9.3, Annex H Eqn H.3): 1 when Lsmax / D < Rmax, otherwise 1 + fix(Lsmax / (D · Rmax)), so that every nth row and column is drawn.

public static int GridIncrement(double cellDiagonal, double maxSymbolLength, double maxRatio)

Parameters

cellDiagonal double

The on-screen grid cell diagonal D.

maxSymbolLength double

The length Lsmax of the largest symbol in the displayed field, in the same unit as cellDiagonal.

maxRatio double

The ratio Rmax; must be positive.

Returns

int

The increment, at least 1.

ThinGrid(int, int, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<float>, ReadOnlySpan<float>, double, SymbolRect, double, List<int>)

Thins a regular grid of symbols with the S-98 Appendix G-1.1 algorithm: draws every GridIncrement(double, double, double)th row and column, counted from a seed cell that holds the largest symbol in the grid, with the increment set by the largest symbol in the displayed field.

public static void ThinGrid(int rows, int cols, ReadOnlySpan<double> x, ReadOnlySpan<double> y, ReadOnlySpan<float> scale, ReadOnlySpan<float> priority, double lengthPerScale, SymbolRect displayed, double maxRatio, List<int> selected)

Parameters

rows int

Number of grid rows.

cols int

Number of grid columns.

x ReadOnlySpan<double>

Per-cell symbol x position, row-major (rows × cols). A cell with a non-finite position (beyond the Web Mercator latitude limit, say) is never drawn.

y ReadOnlySpan<double>

Per-cell symbol y position, row-major. y may increase up or down.

scale ReadOnlySpan<float>

Per-cell symbol scale, row-major. A cell with a NaN or non-positive scale has no symbol (no data, or out of every band).

priority ReadOnlySpan<float>

Per-cell priority, row-major (for S-111, the current speed). Among the cells with the largest symbol, the one with the highest priority seeds the lattice; remaining ties go to the first in row-major order.

lengthPerScale double

Length of a symbol at scale 1, in the unit of x / y.

displayed SymbolRect

The displayed field; only cells inside it are considered.

maxRatio double

The ratio Rmax.

selected List<int>

Receives the row-major indices of the cells to draw, in ascending order (cleared first).

Remarks

Choice (S-98 G-1.1 lets implementers "adapt the determination of the seed point"): the seed is the largest symbol in the whole grid, fastest first, not the largest in the displayed field. A seed taken from the view moves whenever panning brings a faster current or wind into it, and with it the whole drawn lattice, so the symbols jitter over an area that hasn't changed. The grid's seed is still "the row and column with the maximum vector" S-111 §9.3.2 asks to keep, and it never moves while panning. Only the increment depends on the view, through the largest displayed symbol, so the lattice changes with zoom (and when a larger symbol enters view) but not with panning.

Choice: the increment is worked out per axis and per cell rather than once from the seed cell's diagonal. A geographic grid's cells are not square in a Mercator display, and they grow taller towards the poles (by 1/cos(latitude)) while their width stays put. One diagonal increment for the whole view draws rows near the poles far apart and the symbols along each of them nearly touching. Instead, rows are walked out from the seed row along the seed column, and each row's columns out from the seed column along that row, keeping the next cell whose distance from the last kept one d satisfies Eqn 9.2 with D = √2 · d. On a uniform square grid that is exactly every GridIncrement(double, double, double)th row and column; on a geographic grid it draws fewer rows near the equator than near the poles, so the drawn spacing stays about the same on screen.

ThinPoints(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, double, List<int>)

Thins irregularly spaced symbols (station series, ungeorectified meshes) point by point, as S-111 Ed 2.0.0 §9.3.2 describes: symbols are visited in descending priority, and a symbol is dropped when it lies closer than L / (Rmax · √2) to an already kept symbol, L being the longer of the two.

public static void ThinPoints(ReadOnlySpan<double> x, ReadOnlySpan<double> y, ReadOnlySpan<double> length, ReadOnlySpan<double> priority, double maxRatio, List<int> kept)

Parameters

x ReadOnlySpan<double>

Per-point symbol x position.

y ReadOnlySpan<double>

Per-point symbol y position.

length ReadOnlySpan<double>

Per-point symbol length, in the unit of x / y. A NaN or non-positive length marks a point with no symbol; it is never kept.

priority ReadOnlySpan<double>

Per-point priority (for S-111, the current speed); higher is kept first, ties go to the lower index.

maxRatio double

The ratio Rmax.

kept List<int>

Receives the indices of the points to draw, in ascending order (cleared first).

Remarks

Choice: S-111 §9.3.2 says overlapping symbols are eliminated but does not define overlap. We reuse the grid method's spacing pairwise. The grid method bounds the drawn cell diagonal by Lsmax / Rmax, so in a thinned square grid the nearest drawn neighbours, along an axis, are Lsmax / (Rmax · √2) apart. A kept point symbol of length L therefore clears a radius of L / (Rmax · √2): a mesh and a grid of the same node density thin to the same nearest-neighbour spacing. With the default Rmax of 0.5 that is about 1.41 arrow lengths, more than the one length at which two arrows centred on their pivots could touch, so drawn arrows never overlap.

Visiting in descending priority keeps the fastest currents, the point-set counterpart of the grid method's "row and column with the maximum vector". The result depends only on the input points and the lengths, not on a viewport, so callers that pass every point (rather than only the visible ones) get a selection that is stable while panning.