Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions Inundator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,11 @@ The current implementation prioritizes reliability and simplicity over raw speed
**Architecture**:
- **Web Workers**: Computation runs in background thread (UI remains responsive)
- **Simple BFS Queue**: O(1) queue operations - just push/shift on array
- **Running counters**: Left/right side cell counts maintained incrementally during flood fill, avoiding full array scans
- **Stateful DEM Expansion**: Preserves flood state across terrain expansions (no restart)
- **Parallel Tile Fetching**: Up to 8 concurrent DEM tile requests
- **Parallel Tile Fetching**: Elevation tiles fetched concurrently with `Promise.all`
- **Bitwise zoom math**: `1 << zoom` instead of `Math.pow(2, zoom)` for tile coordinate calculations
- **DOM element caching**: Frequently updated status elements cached as instance properties
- **Progressive Loading**: DEM tiles fetched only as needed
- **Dynamic Expansion**: Starts with 10km radius, expands to 100km on demand
- **Optimized Progress Updates**: Every 50k iterations to minimize overhead
Expand All @@ -411,11 +414,11 @@ The current implementation prioritizes reliability and simplicity over raw speed

## Dependencies

- **MapLibre GL JS**: Map rendering (v3.6.2)
- **Turf.js**: Geospatial operations (v6)
- **MapLibre GL JS**: Map rendering (v5)
- **Turf.js**: Geospatial operations (v7)
- **D3 Arrays & Contours**: Marching squares algorithm for polygon generation (v3-4)

All dependencies are loaded from CDN.
All dependencies are loaded from jsDelivr CDN.

## Data Sources

Expand Down
10 changes: 7 additions & 3 deletions Isosmfar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ Isosmfar showcases several sophisticated web development techniques:
- **Local storage preferences** – remembers your palette, basemap, and mode choices

### Performance Optimizations
- **Voronoi Web Worker** – coalescing, Delaunay triangulation, and boundary clipping run in a dedicated Web Worker, keeping the UI responsive during slider adjustments
- **O(n) spatial clustering** – grid-based bucketing with Union-Find replaces O(n³) complete-linkage for Voronoi coalescing
- **Fast boundary clipping** – `turf.booleanWithin` skips expensive `turf.intersect` for Voronoi cells fully inside the boundary
- **WebGL buffer reuse** – pre-allocated Float32Array and persistent GL buffer with DYNAMIC_DRAW avoid per-frame allocations
- **Squared distance optimization** – fragment shader defers sqrt to the end, reducing per-feature cost in all four visualization modes
- **Throttled rendering** at 60fps for smooth slider interactions
- **Debounced operations** preventing excessive recomputation during user input
- **Haversine distance calculations** performed directly in shader code
- **Texture sampling** for both feature positions and color palettes
- **Dynamic level-of-detail** – processes only visible area at current zoom
- **Deduplication** of overlapping features to prevent redundant calculations
Expand All @@ -102,8 +106,8 @@ Isosmfar showcases several sophisticated web development techniques:

- **JavaScript (ES6+)** – modern language features
- **WebGL 2.0/1.0** – hardware-accelerated graphics rendering with adaptive capability detection
- **[MapLibre GL JS](https://maplibre.org/)** – high-performance map display
- **[Turf.js](https://turfjs.org/)** – spatial analysis and area calculations
- **[MapLibre GL JS](https://maplibre.org/) v5** – high-performance map display
- **[Turf.js](https://turfjs.org/) v7** – spatial analysis and area calculations
- **[D3-Delaunay](https://d3js.org/d3-delaunay/voronoi)** – Voronoi diagram generation
- **[Overpass API](https://overpass-api.de/)** – OSM feature queries
- **[Nominatim](https://nominatim.org/)** – geocoding and area search
Expand Down
253 changes: 0 additions & 253 deletions Isosmfar/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1725,114 +1725,6 @@
}
}

coalescePoints(features, distanceKm) {
if (distanceKm === 0 || features.length === 0) {
return features;
}

// Convert km to degrees (approximate)
const distanceDeg = distanceKm / 111; // 1 degree ≈ 111 km
const distSq = distanceDeg * distanceDeg;

// Grid-based spatial clustering: O(n) average case
// Bucket points into grid cells of size distanceDeg
const cellSize = distanceDeg;
const grid = new Map();

const cellKey = (cx, cy) => `${cx},${cy}`;

for (let i = 0; i < features.length; i++) {
const cx = Math.floor(features[i].lon / cellSize);
const cy = Math.floor(features[i].lat / cellSize);
const key = cellKey(cx, cy);
if (!grid.has(key)) grid.set(key, []);
grid.get(key).push(i);
}

// Union-Find for merging clusters
const parent = new Int32Array(features.length);
const rank = new Uint8Array(features.length);
for (let i = 0; i < features.length; i++) parent[i] = i;

function find(x) {
while (parent[x] !== x) {
parent[x] = parent[parent[x]]; // path compression
x = parent[x];
}
return x;
}

function union(a, b) {
a = find(a);
b = find(b);
if (a === b) return;
if (rank[a] < rank[b]) { const t = a; a = b; b = t; }
parent[b] = a;
if (rank[a] === rank[b]) rank[a]++;
}

// For each point, check its grid cell and 8 neighbors
for (const [key, indices] of grid) {
const parts = key.split(',');
const cx = Number.parseInt(parts[0]);
const cy = Number.parseInt(parts[1]);

// Merge points within the same cell
for (let a = 0; a < indices.length; a++) {
for (let b = a + 1; b < indices.length; b++) {
const fa = features[indices[a]];
const fb = features[indices[b]];
const dlat = fa.lat - fb.lat;
const dlon = fa.lon - fb.lon;
if (dlat * dlat + dlon * dlon <= distSq) {
union(indices[a], indices[b]);
}
}
}

// Check adjacent cells
for (let dx = 0; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy <= 0) continue; // avoid double-checking
const neighborKey = cellKey(cx + dx, cy + dy);
const neighborIndices = grid.get(neighborKey);
if (!neighborIndices) continue;

for (const i of indices) {
for (const j of neighborIndices) {
const fi = features[i];
const fj = features[j];
const dlat = fi.lat - fj.lat;
const dlon = fi.lon - fj.lon;
if (dlat * dlat + dlon * dlon <= distSq) {
union(i, j);
}
}
}
}
}
}

// Collect clusters by root
const clusterMap = new Map();
for (let i = 0; i < features.length; i++) {
const root = find(i);
if (!clusterMap.has(root)) clusterMap.set(root, { sumLat: 0, sumLon: 0, count: 0 });
const c = clusterMap.get(root);
c.sumLat += features[i].lat;
c.sumLon += features[i].lon;
c.count++;
}

// Calculate centroids
const coalescedFeatures = [];
for (const { sumLat, sumLon, count } of clusterMap.values()) {
coalescedFeatures.push({ lat: sumLat / count, lon: sumLon / count });
}

return coalescedFeatures;
}

recomputeVoronoi() {
if (!this.originalFeatures) return;

Expand All @@ -1851,149 +1743,6 @@
});
}

computeVoronoi(features, bounds, boundary) {
// Coalesce points if needed
const coalescedFeatures = this.coalescePoints(features, this.voronoiCoalesceKm);

// Update info display
if (this.voronoiCoalesceKm > 0) {
document.getElementById('coalesce-info').textContent =
`${features.length} points → ${coalescedFeatures.length} clusters`;
} else {
document.getElementById('coalesce-info').textContent = '';
}

// Convert features to points array for Delaunay
const points = coalescedFeatures.map(f => [f.lon, f.lat]);

if (points.length < 3) {
// Not enough points for Voronoi
this.voronoiGeoJSON = {
type: 'FeatureCollection',
features: []
};
return;
}

// Use d3-delaunay to compute Voronoi
const delaunay = d3.Delaunay.from(points);
const voronoi = delaunay.voronoi(bounds);

// Convert boundary to proper turf feature for clipping
let boundaryFeature;
try {
if (boundary.type === 'Polygon') {
boundaryFeature = turf.polygon(boundary.coordinates);
} else if (boundary.type === 'MultiPolygon') {
boundaryFeature = turf.multiPolygon(boundary.coordinates);
} else {
console.warn('Unknown boundary type:', boundary.type);
boundaryFeature = null;
}
} catch (e) {
console.error('Error creating boundary feature:', e);
boundaryFeature = null;
}

// Convert Voronoi cells to GeoJSON lines, clipped to boundary
const lineFeatures = [];
const processedEdges = new Set();

for (let i = 0; i < points.length; i++) {
const cell = voronoi.cellPolygon(i);
if (cell && cell.length > 2) {
try {
// Create a polygon from the cell - ensure closed
const cellCoords = [...cell];
if (cellCoords[0][0] !== cellCoords.at(-1)[0] ||
cellCoords[0][1] !== cellCoords.at(-1)[1]) {
cellCoords.push(cellCoords[0]);
}

const cellPolygon = turf.polygon([cellCoords]);

// Clip to boundary if available
let clippedCell = null;
if (boundaryFeature) {
try {
// Fast path: if cell is fully inside boundary, skip expensive intersection
if (turf.booleanWithin(cellPolygon, boundaryFeature)) {
clippedCell = cellPolygon;
} else {
const intersection = turf.intersect(turf.featureCollection([cellPolygon, boundaryFeature]));
if (intersection && intersection.geometry) {
clippedCell = intersection;
} else {
continue;
}
}
} catch (e) {
// Fall back to using unclipped cell
console.warn('Error clipping Voronoi cell to boundary:', e);
clippedCell = cellPolygon;
}
} else {
clippedCell = cellPolygon;
}

if (clippedCell && clippedCell.geometry) {
// Handle both Polygon and MultiPolygon results from intersection
let polygons;
if (clippedCell.geometry.type === 'Polygon') {
polygons = [clippedCell.geometry.coordinates];
} else if (clippedCell.geometry.type === 'MultiPolygon') {
polygons = clippedCell.geometry.coordinates;
} else {
continue;
}

for (const polygonRings of polygons) {
const exteriorRing = polygonRings[0]; // Only process exterior ring

// Convert polygon to line segments
for (let j = 0; j < exteriorRing.length - 1; j++) {
const p1 = exteriorRing[j];
const p2 = exteriorRing[j + 1];

// Quantize to ~0.1m and build numeric edge key (avoids string allocation)
const x1 = Math.round(p1[0] * 1e6);
const y1 = Math.round(p1[1] * 1e6);
const x2 = Math.round(p2[0] * 1e6);
const y2 = Math.round(p2[1] * 1e6);
// Order endpoints so (A,B) and (B,A) produce the same key
const edgeKey = x1 < x2 || (x1 === x2 && y1 < y2)
? `${x1},${y1},${x2},${y2}`
: `${x2},${y2},${x1},${y1}`;

// Only add each edge once
if (!processedEdges.has(edgeKey)) {
processedEdges.add(edgeKey);
lineFeatures.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [p1, p2]
}
});
}
}
}
}
} catch (e) {
console.warn('Error processing Voronoi cell:', e);
}
}
}

this.voronoiGeoJSON = {
type: 'FeatureCollection',
features: lineFeatures
};

// Update overlay if checkbox is checked
this.updateVoronoiOverlay();
}

makeBoundingBox(bbox) {
const [minLat, maxLat, minLon, maxLon] = bbox.map(parseFloat);
return {
Expand Down Expand Up @@ -2272,7 +2021,6 @@
const gradientLayer = {
id: 'gradient-field',
type: 'custom',
renderingMode: '2d',
features: features,
bounds: bounds,
boundaryCoords: boundaryCoords,
Expand Down Expand Up @@ -2715,7 +2463,6 @@
gl.enableVertexAttribArray(this.positionLocation);
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0);

gl.disable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.TRIANGLES, 0, 6);
Expand Down
Loading