Class SymbolThinning
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
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
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
cellDiagonaldoubleThe on-screen grid cell diagonal
D.maxSymbolLengthdoubleThe length
Lsmaxof the largest symbol in the displayed field, in the same unit ascellDiagonal.maxRatiodoubleThe 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
rowsintNumber of grid rows.
colsintNumber of grid columns.
xReadOnlySpan<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.yReadOnlySpan<double>Per-cell symbol y position, row-major. y may increase up or down.
scaleReadOnlySpan<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).
priorityReadOnlySpan<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.
lengthPerScaledoubleLength of a symbol at scale 1, in the unit of
x/y.displayedSymbolRectThe displayed field; only cells inside it are considered.
maxRatiodoubleThe ratio
Rmax.selectedList<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
xReadOnlySpan<double>Per-point symbol x position.
yReadOnlySpan<double>Per-point symbol y position.
lengthReadOnlySpan<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.priorityReadOnlySpan<double>Per-point priority (for S-111, the current speed); higher is kept first, ties go to the lower index.
maxRatiodoubleThe ratio
Rmax.keptList<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.