From f4e3ccc831203719071a486670c0651a3091fa79 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 14:52:02 +0000 Subject: [PATCH 1/2] Update READMEs with recent performance and library improvements Inundator: MapLibre 5, Turf.js 7, jsDelivr CDN, document running counters, parallel fetching, bitwise zoom, DOM caching. Isosmfar: MapLibre v5, Turf.js v7, document Voronoi Web Worker, O(n) Union-Find clustering, booleanWithin fast path, WebGL buffer reuse, squared distance shader optimization. https://claude.ai/code/session_013SyD3xCEEcuzNN69ghKo3z --- Inundator/README.md | 11 +++++++---- Isosmfar/README.md | 10 +++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Inundator/README.md b/Inundator/README.md index c451b2d..441fec5 100644 --- a/Inundator/README.md +++ b/Inundator/README.md @@ -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 @@ -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 diff --git a/Isosmfar/README.md b/Isosmfar/README.md index 73069ee..e15c90b 100644 --- a/Isosmfar/README.md +++ b/Isosmfar/README.md @@ -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 @@ -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 From 0f3f6a1b25b32a85268284b9f8f07b788cbfb1e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 15:06:39 +0000 Subject: [PATCH 2/2] Remove dead code and speculative debug fixes from Isosmfar - Remove synchronous computeVoronoi and coalescePoints (now in worker) - Remove renderingMode: '2d' (speculative fix that had no effect) - Remove gl.disable(gl.DEPTH_TEST) (speculative fix that had no effect) https://claude.ai/code/session_013SyD3xCEEcuzNN69ghKo3z --- Isosmfar/app.js | 253 ------------------------------------------------ 1 file changed, 253 deletions(-) diff --git a/Isosmfar/app.js b/Isosmfar/app.js index 8b996b3..5409570 100644 --- a/Isosmfar/app.js +++ b/Isosmfar/app.js @@ -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; @@ -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 { @@ -2272,7 +2021,6 @@ const gradientLayer = { id: 'gradient-field', type: 'custom', - renderingMode: '2d', features: features, bounds: bounds, boundaryCoords: boundaryCoords, @@ -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);