A streaming line simplification algorithm for .NET that processes points one at a time with O(1) time per point. Unlike batch algorithms like Douglas-Peucker, this works in real-time as points arrive.
- Drawing applications with pen/touch input
- GPS tracking and route simplification
- Real-time data visualization
- Any scenario where points arrive incrementally
The algorithm uses an angular tolerance cone to decide which points to keep and which to skip.
Given a tolerance (maximum allowed perpendicular distance from the simplified line), the algorithm maintains:
- An anchor point - the last emitted vertex
- An angular cone - the range of valid directions for the next segment
- A minimum distance constraint - prevents the line from doubling back
For each incoming point beyond the tolerance radius, the algorithm computes how much the line could deviate if it continued through that point. This deviation defines a cone of valid angles. As more points accumulate, the cone narrows (intersection of all individual cones). When a new point falls outside the cone, or when the line doubles back too far, the previous point is emitted as a vertex and the process restarts.
Instead of working with angles in radians and trigonometric functions, the algorithm represents rotations as unit complex numbers stored in Vector2(cos, sin). This is the Rotation2 utility class.
Why this is cleaner than angle arithmetic:
- Composing rotations is just complex multiplication (two multiplies, two adds) instead of
atan2+ addition +cos/sin - Comparing angles uses the perpendicular dot product (
a.X * b.Y - a.Y * b.X > 0= "is b counterclockwise of a") instead of modular angle subtraction with wraparound handling - Inverse rotation is just conjugation
(cos, -sin)instead of negating an angle - No wraparound discontinuities - the ±180 degree boundary that plagues angle-based code simply doesn't exist
The cone boundaries are maintained as unit vectors, and narrowing the cone is done by taking the max of the lower bounds and min of the upper bounds, all using perpendicular dot product comparisons.
Besides the angular cone, the algorithm tracks the farthest point seen from the anchor. If a new point is closer than (farthest - tolerance) to the anchor, it means the line has doubled back enough that the tolerance guarantee could be violated. This triggers an emit.
| Algorithm | Processing | Time Complexity | Streaming | Error Bound |
|---|---|---|---|---|
| StreamingLineSimplifier | One point at a time | O(1) per point | Yes | Guaranteed |
| Douglas-Peucker | Full line required | O(n log n) | No | Guaranteed |
| Visvalingam-Whyatt | Full line required | O(n log n) | No | Not guaranteed |
| Nth-point | One point at a time | O(1) per point | Yes | Not guaranteed |
| Radial distance | One point at a time | O(1) per point | Yes | Not guaranteed |
The key advantage over Douglas-Peucker is that you don't need the complete polyline before simplifying. The key advantage over simpler streaming methods (nth-point, radial distance) is the guaranteed error bound - the simplified line is always within the tolerance distance of any skipped point.
Add a reference to the StreamingLineSimplifier project or copy the source files directly.
using System.Numerics;
using StreamingLineSimplifier;
// Create simplifier with 5-pixel tolerance
var simplifier = new StreamingLineSimplifier(tolerance: 5f);
// Process points as they arrive
foreach (var point in inputPoints)
{
if (simplifier.TryAdd(point, out Vector2 emittedPoint))
{
// A simplified vertex was emitted
outputPoints.Add(emittedPoint);
}
}
// Don't forget to add the final point
outputPoints.Add(inputPoints.Last());public static List<Vector2> Simplify(IEnumerable<Vector2> points, float tolerance)
{
var result = new List<Vector2>();
var simplifier = new StreamingLineSimplifier(tolerance);
Vector2 lastPoint = default;
foreach (var point in points)
{
lastPoint = point;
if (simplifier.TryAdd(point, out var emitted))
{
result.Add(emitted);
}
}
// Add final point if different from last emitted
if (result.Count == 0 || result[^1] != lastPoint)
{
result.Add(lastPoint);
}
return result;
}// Reset to process a new line
simplifier.Reset();public StreamingLineSimplifier(float tolerance)Creates a new simplifier with the specified perpendicular distance tolerance.
| Method | Description |
|---|---|
bool TryAdd(Vector2 point, out Vector2 emittedPoint) |
Adds a point. Returns true if a vertex was emitted. |
void Reset() |
Resets state for processing a new line. |
| Property | Description |
|---|---|
Vector2 LastPoint |
The most recently processed point. |
Static utility class for 2D rotation math using complex number semantics.
| Method | Description |
|---|---|
Vector2 Rotate(Vector2 a, Vector2 b) |
Rotates vector a by rotation b. |
Vector2 Conjugate(Vector2 a) |
Returns the inverse rotation. |
Vector2 FromAngle(float radians) |
Creates a rotation from an angle. |
float ToAngle(Vector2 r) |
Extracts the angle from a rotation. |
float PerpDot(Vector2 a, Vector2 b) |
2D cross product (perpendicular dot). |
float Dot(Vector2 a, Vector2 b) |
Standard dot product. |
The included WinForms demo lets you draw lines and see the simplification in real-time.
dotnet run --project demo/StreamingLineSimplifier.DemoFeatures:
- Draw with mouse to create lines
- Adjust tolerance with the slider
- See original (gray) vs simplified (blue) lines
- Debug mode showing internal algorithm state (tolerance circle, angular cone, distance constraint)
- Point counter shows compression ratio
# Build entire solution
dotnet build
# Run demo
dotnet run --project demo/StreamingLineSimplifier.Demo
# Build library only
dotnet build src/StreamingLineSimplifier- Library: .NET Standard 2.1+
- Demo: .NET 8.0 (Windows)
The algorithm and its implementation are human-designed. AI coding assistants were used for the demo application, documentation, and other supporting code.
MIT License - see LICENSE for details.
