Research implementation exploring tile-based isoline generation for web browsers. Compares four distinct algorithmic approaches (LineString, Polygon, Strip-based, Buffer-based) for generating contour lines from tile data.
Example isoline visualization - contour lines show regions of equal value across gridded geospatial data
The Api currently expects gridded geospatial data in CSV format.
Get real atmospheric data from ECMWF Copernicus Climate Data Store:
- Visit: https://cds.climate.copernicus.eu/
- Create free account
- Search for "ERA5 monthly averaged data on single levels"
- Select variable (e.g., Mean Sea Level Pressure)
- Download as NetCDF or GeoTIFF
- Extract to csv using Panoply software:
git clone https://github.com/Uzo-Felix/isolines.git
cd isolines
npm install# Generate geojson using strip-based algorithm with included data
node src/tools/visualize/generate-geojson.js msl.csv '{"algorithm":"strip","tileSize":64, "downsampleFactor":1, "numLevels":5}'isolines/
βββ src/
β βββ algorithms/ # Algorithm implementations
β β βββ tiled/ # Tile-based approaches
β β β βββ linestring-based.js # Approach 1: Independent LineStrings
β β β βββ polygon-based.js # Approach 2: Forced closure polygons
β β β βββ strip-based.js # Approach 3: Boundary data strips β
β β β βββ buffered-tile.js # Approach 4: Overlapping buffers
β β βββ standard/
β β βββ index.js # Reference (complete grid) algorithm
β β
β βββ core/ # Core algorithms
β β βββ conrec.js # CONREC marching squares
β β βββ isolineBuilder.js # LineString/Polygon utilities
β β βββ spatialIndex.js # R-tree spatial indexing
β β
β βββ test/ # Test suite
β β βββ unit/ # Unit tests
β β β βββ conrec.test.js
β β β βββ test-strip-based-algorithm.js
β β β βββ test-strip-correctness.js
β β β βββ test_output/ # Test results
β β βββ integration/ # Integration tests
β β βββ performance/ # Performance benchmarks
β β βββ data/ # Test datasets
β β βββ synthetic/ # Generated test data
β β βββ real-world/ # ERA5 climate data
β β βββ edge-cases/ # Edge case scenarios
β β
β βββ tools/
β βββ benchmark/ # Performance profiling
β βββ visualize/ # Generate visualizations
β
β
β
βββ index.js # Main entry point
βββ package.json
βββ README.md
// Standard (complete grid) algorithm
const StandardAlgorithm = require('./src/algorithms/standard');
const std = new StandardAlgorithm(grid, levels);
const isolines = std.generate();
// Strip-based (tile) algorithm
const StripBasedAlgorithm = require('./src/algorithms/tiled/strip-based');
const builder = new StripBasedAlgorithm(levels, tileSize);
builder.addTile(row, col, tileData);
const isolines = builder.getIsolinesAsGeoJSON();
// Other tile approaches
const LineStringBased = require('./src/algorithms/tiled/linestring-based');
const PolygonBased = require('./src/algorithms/tiled/polygon-based');
const BufferBased = require('./src/algorithms/tiled/buffered-tile');# Test CONREC core algorithm
node src/test/unit/conrec.test.js
# Test strip-based approach
node src/test/unit/test-strip-based-algorithm.js
# Test boundary consistency
node src/test/unit/test-strip-correctness.js
# Run all unit tests
npm test# Performance comparison
node src/tools/benchmark/benchmark.js
# Memory profiling
node src/test/memory/memory-profiling-test.js# Process ERA5 climate data (requires msl.csv)
node src/tools/visualize/generate-geojson.jsStrategy: Process tiles independently, merge endpoints at boundaries
| Metric | Value |
|---|---|
| Continuity | 57.8% |
| Artifacts | 17 |
| Processing Time | 0.60s |
| Strengths | Simple, preserves natural topology |
| Weaknesses | Visible gaps at boundaries |
Strategy: Force closure of all segments within tile bounds
| Metric | Value |
|---|---|
| Continuity | 100% |
| Artifacts | 53 |
| Processing Time | 0.54s |
| Strengths | Fastest, perfect continuity metric |
| Weaknesses | Artificial closures, most artifacts |
Strategy: Share identical boundary data strips between adjacent tiles
| Metric | Value |
|---|---|
| Continuity | 92.8% |
| Artifacts | 13 |
| Processing Time | 0.59s |
| Strengths | Best balance, perfect equivalence for simple cases |
| Weaknesses | Complex strip management |
Strategy: Process tiles with overlapping buffers, clip to extent
| Metric | Value |
|---|---|
| Continuity | 0.4% |
| Artifacts | 74 |
| Processing Time | 1.77s |
| Strengths | Best geometric accuracy |
| Weaknesses | JavaScript clipping issues, poor continuity |
# Generate isolines with all four methods and compare
node src/tools/visualize/run-comparison.js
# Output: src/tools/visualize/output/all-algorithms-comparison.json# Test on generated synthetic datasets
node src/test/unit/test-strip-based-algorithm.js
# Output: src/test/unit/test_output/test_summary.json# Measure memory consumption per approach
node src/test/memory/memory-profiling-test.js
# Output: src/test/memory/memory-results.jsonTest different tile sizes, overlap amounts, and data resolutions:
// src/test/integration/tileProcessing.test.js
const builder = new StripBasedAlgorithm(levels, tileSizeVariations);
// Measure continuity for different tile configurationsThe implementation consists of three main algorithmic stages, based on the original CONREC-based three-stage isoline construction method:
File: src/core/conrec.js
The CONREC (Contour Line) algorithm generates raw contour segments by subdividing each cell into 4 triangles and processing each triangle independently.
How it works:
- For each grid cell (defined by 4 corner points):
- Computes center point value (average of 4 corners)
- Divides cell into 4 triangles using diagonals and center point
- For each triangle (3 vertices):
- Classifies into topological cases based on how many vertices are above/below/on the contour level
- Handles 10 distinct cases (a through j)
- For each case, determines if contour intersects the triangle:
- If yes, interpolates intersection points on triangle edges
- Generates line segment(s) connecting intersection points
- Output: unordered collection of line segments with metadata
Triangle Classification Cases:
The algorithm identifies 10 distinct topological configurations:
- Cases a, j: All vertices above or all below level β no intersection
- Cases c, f: 2 below + 1 above (or vice versa) β generates 1 segment
- Case e: 1 below + 1 on + 1 above β generates 1 segment
- Cases b, d, h: Special cases with vertices on contour level β generates segments connecting on-level vertices
- Case g: All 3 vertices on level (rare, ignored)
Conrec Algorithm triangle-based classification (10 cases)
Example Usage:
const CONREC = require('./src/core/conrec');
const conrec = new CONREC(grid, contourLevel);
const segments = conrec.generateSegments();
// Returns: [{x0, y0, x1, y1}, {x0, y0, x1, y1}, ...]Process Visualization:
CONREC processes each grid cell, examining corner values and generating segments at contour intersections using linear interpolation.
File: src/core/spatialIndex.js
CONREC produces unordered segments without explicit connectivity information. The R-Tree spatial index enables efficient proximity queries to find which segments connect.
How it works:
- Hierarchically organizes segments using nested bounding rectangles
- Each node covers a spatial region
- Leaf nodes contain individual segments
- Point Query: Find all segments with endpoints within tolerance Ξ΅
- Range Query: Retrieve segments intersecting a spatial region
- Reduces connectivity search from O(nΒ²) brute-force to O(log n)
Index Structure:
R-tree organizes segments hierarchically, enabling efficient spatial queries. The tree structure allows finding nearby endpoints quickly without comparing all segment pairs.
Example Usage:
const SpatialIndex = require('./src/core/spatialIndex');
const index = new SpatialIndex();
// Insert segments with their bounding rectangles
segments.forEach(seg => index.insert(seg));
// Find segments near a point
const nearby = index.query(point, tolerance);File: src/core/isolineBuilder.js
The final stage assembles individual segments into complete isolines (either closed Polygons or open LineStrings).
How it works:
- Start with first unprocessed segment
- Query R-Tree for segments with endpoints near current endpoint
- Select best matching candidate
- Append to current path and update endpoint
- Repeat until:
- Path closes (endpoints match) β create Polygon
- No more candidates found β create LineString
- Continue with next unprocessed segment
Process Visualization:
Segment assembly transforms scattered, unordered CONREC segments into complete connected polygons. The algorithm uses endpoint matching within tolerance Ξ΅ to determine connectivity.
Key Parameters:
- Endpoint Matching Tolerance (Ξ΅ = 10β»β΅ degrees): Floating-point arithmetic introduces small errors; endpoints within tolerance are considered connected
- Polygon Closure Detection: A path is closed if final endpoint matches initial endpoint within tolerance
Example Usage:
const IsolineBuilder = require('./src/core/isolineBuilder');
const builder = new IsolineBuilder(segments, tolerance);
const geometries = builder.buildGeometries();
// Returns: [{type: 'Polygon', coordinates: [...]}, ...]Each tile-based approach builds on these three core stages but adapts them for incremental tile-by-tile processing.
File: src/algorithms/tiled/linestring-based.js
Strategy: Process each tile independently using the standard three stages, then merge LineStrings at tile boundaries.
Algorithm Flow:
- Process each tile independently with CONREC
- Build LineStrings within tile
- When neighboring tile available, find overlapping endpoints
- Merge LineStrings that share endpoints
Key Code:
class LineStringBasedTiling {
processTile(row, col, tileData) {
const conrec = new CONREC(tileData, level);
const segments = conrec.generateSegments();
const linestrings = this.buildLineStrings(segments);
return linestrings;
}
mergeWithNeighbor(linestrings1, linestrings2) {
// Find overlapping endpoints and merge
}
}File: src/algorithms/tiled/polygon-based.js
Strategy: Force closure of all contour segments within each tile, creating Polygon geometries even if segments don't naturally connect.
Algorithm Flow:
- Process each tile with CONREC
- Build LineStrings within tile
- For any LineString that doesn't close:
- Detect if endpoints are at tile boundary
- Artificially connect endpoints with straight line
- Create Polygon from closed path
- Output: All segments converted to Polygons
Key Code:
class PolygonBasedTiling {
processTile(row, col, tileData) {
const conrec = new CONREC(tileData, level);
const segments = conrec.generateSegments();
const paths = this.buildPaths(segments);
// Force closure
const polygons = paths.map(path => {
if (!this.isClosed(path)) {
path = this.forceClose(path); // Add closure segment
}
return new Polygon(path);
});
return polygons;
}
}File: src/algorithms/tiled/strip-based.js
Strategy: Share identical boundary data strips between adjacent tiles. This ensures both sides of a tile boundary use the exact same numerical values for interpolation.
How Strip Sharing Works:
Boundary data strips (green) from neighboring tiles are attached to tile edges, ensuring mathematical continuity at boundaries.
Algorithm Flow:
- For each tile to process:
- Extract 2-pixel-wide strips from adjacent tile edges
- Attach strips to current tile edges (expanding tile slightly)
- Apply CONREC to expanded tile
- Both sides of boundary use identical raw data
- Eliminates floating-point precision mismatches
- Build LineStrings from segments
- Naturally close LineStrings where endpoints match
- Merge with already-processed neighbors using OVERLAPS predicate
Key Code:
class StripBasedTiling {
processTile(row, col, tileData) {
// Extract boundary strips from neighbors
const topStrip = this.neighbors[row-1]?.[col]?.bottom2Rows();
const bottomStrip = this.neighbors[row+1]?.[col]?.top2Rows();
const leftStrip = this.neighbors[row]?.[col-1]?.right2Cols();
const rightStrip = this.neighbors[row]?.[col+1]?.left2Cols();
// Attach strips to tile edges
const expandedTile = this.attachStrips(tileData,
{top: topStrip, bottom: bottomStrip, left: leftStrip, right: rightStrip});
// Apply CONREC with expanded data
const conrec = new CONREC(expandedTile, level);
const segments = conrec.generateSegments();
const linestrings = this.buildLineStrings(segments);
// Naturally close and convert to polygons
const geometries = linestrings.map(ls =>
this.isClosed(ls) ? new Polygon(ls) : ls
);
return geometries;
}
}Why Strip-Based Works:
- Both sides of boundary compute interpolations from identical data
- No floating-point precision differences at boundaries
- Eliminates artificial gaps caused by numerical errors
- Achieves perfect equivalence for simple topologies
File: src/algorithms/tiled/buffered-tile.js
Strategy: Process tiles with overlapping buffer zones (from neighbors), then clip results back to tile boundaries.
Algorithm Flow:
- For each tile to process:
- Retrieve 2-pixel buffer zone from neighbors
- Expand tile data to include buffer
- Apply standard three-stage algorithm to buffered tile
- Complete grid context enables correct CONREC processing
- Clip output geometries back to original tile boundaries
- Remove segments outside tile extent
- Store clipped results
Key Code:
class BufferBasedTiling {
processTile(row, col, tileData) {
// Expand tile with 2-pixel buffer from neighbors
const buffered = this.expandWithBuffer(tileData, row, col, bufferWidth=2);
// Apply standard algorithm
const conrec = new CONREC(buffered, level);
const segments = conrec.generateSegments();
const index = new SpatialIndex();
segments.forEach(s => index.insert(s));
const builder = new IsolineBuilder(segments, tolerance);
const geometries = builder.buildGeometries();
// Clip to tile boundaries
const clipped = geometries.map(geom =>
this.clipToTileBounds(geom, row, col)
);
return clipped;
}
}Challenges in JavaScript:
- Requires robust geometric clipping library (GEOS)
- JavaScript ecosystem has limited, unmaintained libraries
- Clipping edge cases produce degenerate geometries
- Geographic coordinates complicate Cartesian clipping algorithms
Here's how a complete isoline generation looks:
- Standard Complete-Grid Processing:
Input Grid β CONREC β Segments β R-Tree Index β Polygon Formation β Output
- Tile-Based Processing (Strip-Based Example):
Tile Data β Extract Strips β Expand Tile β CONREC β Segments β
R-Tree Index β Polygon Formation β Merge Overlapping β Final Output
| Aspect | LineString | Polygon | Strip-Based | Buffer |
|---|---|---|---|---|
| Core Algorithm | CONREC | CONREC | CONREC | CONREC |
| Boundary Strategy | Endpoint merge | Forced closure | Identical data | Overlapping buffer |
| Segment Assembly | Standard | Modified (force close) | Standard | Standard |
| Clipping Required | No | No | No | Yes |
| Floating-Point Issues | High | High | Low | Medium |
| Complexity | Low | Medium | High | Very High |
{
width: 360,
height: 181,
values: [/* flattened array of numbers */],
minLat: -90, maxLat: 90,
minLon: -180, maxLon: 180
}{
row: 0, col: 0,
width: 64, height: 64,
values: [/* tile values */],
bounds: { minLat, maxLat, minLon, maxLon }
}{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon" | "LineString",
"coordinates": [/*...*/]
},
"properties": {
"level": 100.47,
"type": "isoline"
}
}
]
}Pre-generated test grids with known topologies:
linear.json- Linear gradientradial.json- Radial patternsaddlePoint.json- Saddle point ambiguitycyclone.json- Simulated low-pressure systemcomplex.json- Complex branching
ERA5 climate data:
pressure.json- Mean sea level pressuretemperature.json- 2m temperatureprecipitation.json- Total precipitation
Special scenarios:
polar.json- Pole singularitiesantimeridian.json- Β±180Β° boundary crossingnan-data.json- Missing valuesprecision-errors.json- Floating-point edge cases
# Measure execution time
node src/tools/benchmark/performance-benchmark.js
# Profile memory usage
node --prof src/test/memory/memory-profiling-test.js
node --prof-process isolate-*.log > profile.txtnode src/test/unit/conrec.test.js
node src/test/unit/isolineBuilder.test.js
node src/test/unit/spatialIndex.test.jsnode src/test/integration/basicWorkflow.test.js
node src/test/integration/tileProcessing.test.js# Check polygon closure, continuity, self-intersections
node src/test/geometry/topologyValidation.test.js
node src/test/geometry/polygonClosure.test.js
node src/test/geometry/lineStringContinuity.test.jsconst tileSize = 64; // Typical: 64Γ64 cells
const builder = new StripBasedAlgorithm(levels, tileSize);// In strip-based.js
const stripWidth = 2; // pixels to extract from neighbors// In isolineBuilder.js
const epsilon = 1e-5; // degrees for geographic coordinatesconst levels = [96.53, 98.50, 100.47, 102.44, 104.41]; // kPa- Try increasing strip width
- Check if contour crosses many tile boundaries
- Verify grid data accuracy
- Increase endpoint matching tolerance
- Run additional merge passes
- Check for duplicate segments
- Use smaller tile sizes
- Process fewer contour levels
- Enable garbage collection between tiles
- Check for unnecessary iterations
- Profile with
--profflag - Consider buffer-based approach for accuracy
Academic research implementation. See paper for detailed methodology.
Current Status: Functional implementation
Primary Algorithm: Strip-based (recommended)
Test Coverage: Comprehensive unit and integration tests
Benchmark Data: ERA5 climate reanalysis