Setup: Import Libraries¶
We'll use JSTS for geometry operations. Since this is a web-based demo, we'll load from CDN.
In [23]:
// Load JSTS library (Mapped in deno.json)
import * as jsts from 'https://esm.sh/jsts@2.12.1/dist/jsts.min.js';
// Debug: Check what we actually imported
console.log('JSTS Type:', typeof jsts);
console.log('JSTS Keys:', Object.keys(jsts || {}));
// Create reader/writer for GeoJSON
// Depending on how Deno + npm interop works for this package,
// the export might be under 'default' or direct.
const lib = jsts.default || jsts;
// Access paths
const GeoJSONReader = lib.io?.GeoJSONReader || lib.IO?.GeoJSONReader;
const GeoJSONWriter = lib.io?.GeoJSONWriter || lib.IO?.GeoJSONWriter;
if (!GeoJSONReader) {
console.error("Available properties on import:", Object.keys(lib));
if (lib.io) console.error("Available 'io' properties:", Object.keys(lib.io));
throw new Error("Could not find GeoJSONReader. Import structure might be different.");
}
const reader = new GeoJSONReader();
const writer = new GeoJSONWriter();
console.log('✓ JSTS loaded successfully via npm/Deno');
JSTS Type: object JSTS Keys: [ "algorithm", "default", "densify", "dissolve", "geom", "geomgraph", "index", "io", "linearref", "noding", "operation", "precision", "simplify", "triangulate", "util", "version" ] ✓ JSTS loaded successfully via npm/Deno
Define Test Polygons¶
Three polygons with varying complexity levels.
In [24]:
// Simple polygon - Rectangle with noise
const simplePolygon = {
"type": "Polygon",
"coordinates": [[
[0, 0], [10, 0.1], [10, 10], [0.1, 10], [0, 0]
]]
};
// Medium complexity - Star shape
const mediumPolygon = {
"type": "Polygon",
"coordinates": [[
[5, 0], [6, 3], [9, 3.5], [6.5, 5.5], [7.5, 9],
[5, 7], [2.5, 9], [3.5, 5.5], [1, 3.5], [4, 3], [5, 0]
]]
};
// Complex polygon - Coastline-like
const complexPolygon = {
"type": "Polygon",
"coordinates": [[
[0, 0], [1, 0.2], [2, 0.1], [3, 0.5], [4, 0.3],
[5, 0.8], [6, 0.6], [7, 1.2], [8, 0.9], [9, 1.5],
[10, 1.3], [10.2, 2], [10.1, 3], [10.3, 4], [10, 5],
[9.8, 6], [9.5, 7], [9.2, 8], [8.8, 9], [8.5, 10],
[7, 10.2], [6, 9.8], [5, 10.1], [4, 9.9], [3, 10.2],
[2, 9.7], [1, 10], [0.5, 9], [0.2, 8], [0.1, 7],
[0, 6], [0.2, 5], [0.1, 4], [0.3, 3], [0.2, 2],
[0.1, 1], [0, 0]
]]
};
// Procedural generation: Noisy Circle
function generateNoisyCircle(radius, points, noise) {
const coords = [];
for (let i = 0; i < points; i++) {
const angle = (i / points) * Math.PI * 2;
// Add noise to radius
const r = radius + (Math.random() - 0.5) * noise;
const x = Math.cos(angle) * r;
const y = Math.sin(angle) * r;
coords.push([x + radius, y + radius]); // Offset to be positive
}
coords.push(coords[0]); // Close ring
return {
"type": "Polygon",
"coordinates": [coords]
};
}
const veryComplexPolygon = generateNoisyCircle(50, 200, 30);
console.log('Polygons defined:');
console.log('- Simple:', simplePolygon.coordinates[0].length - 1, 'vertices');
console.log('- Medium:', mediumPolygon.coordinates[0].length - 1, 'vertices');
console.log('- Complex:', complexPolygon.coordinates[0].length - 1, 'vertices');
console.log('- Very Complex (Generated):', veryComplexPolygon.coordinates[0].length - 1, 'vertices');
Polygons defined:
- Simple: 4 vertices - Medium: 10 vertices - Complex: 36 vertices - Very Complex (Generated): 200 vertices
Helper Functions¶
In [25]:
// Count vertices in polygon
function countVertices(geojson) {
return geojson.coordinates[0].length - 1;
}
// Check validity
function isValid(geojson) {
const geometry = reader.read(geojson);
return geometry.isValid();
}
// Simplify with different algorithms
function simplifyGeometry(geojson, tolerance, method = 'topology') {
const geometry = reader.read(geojson);
const lib = jsts.default || jsts;
let simplified;
switch(method) {
case 'topology':
simplified = lib.simplify.TopologyPreservingSimplifier.simplify(geometry, tolerance);
break;
case 'douglas':
simplified = lib.simplify.DouglasPeuckerSimplifier.simplify(geometry, tolerance);
break;
case 'vw':
simplified = lib.simplify.VWSimplifier.simplify(geometry, tolerance);
break;
default:
throw new Error('Unknown method');
}
return writer.write(simplified);
}
console.log('✓ Helper functions defined');
✓ Helper functions defined
Test 1: Simple Polygon Simplification¶
In [14]:
const tolerance = 0.5;
const methods = ['topology', 'douglas', 'vw'];
console.log('\n=== Simple Polygon Simplification (tolerance=' + tolerance + ') ===');
console.log('Original vertices:', countVertices(simplePolygon));
console.log('');
methods.forEach(method => {
const simplified = simplifyGeometry(simplePolygon, tolerance, method);
const vertices = countVertices(simplified);
const valid = isValid(simplified);
const reduction = ((1 - vertices / countVertices(simplePolygon)) * 100).toFixed(1);
console.log(method.padEnd(12) + ': ' + vertices + ' vertices (' + reduction + '% reduction) - Valid: ' + valid);
});
=== Simple Polygon Simplification (tolerance=0.5) === Original vertices: 4 topology : 4 vertices (0.0% reduction) - Valid: true douglas : 4 vertices (0.0% reduction) - Valid: true vw : 4 vertices (0.0% reduction) - Valid: true
Test 2: Medium Complexity Polygon¶
In [15]:
const tolerance2 = 1.0;
console.log('\n=== Medium Polygon Simplification (tolerance=' + tolerance2 + ') ===');
console.log('Original vertices:', countVertices(mediumPolygon));
console.log('');
const results = {};
methods.forEach(method => {
const simplified = simplifyGeometry(mediumPolygon, tolerance2, method);
const vertices = countVertices(simplified);
const valid = isValid(simplified);
const reduction = ((1 - vertices / countVertices(mediumPolygon)) * 100).toFixed(1);
results[method] = { vertices, valid, reduction, geometry: simplified };
console.log(method.padEnd(12) + ': ' + vertices + ' vertices (' + reduction + '% reduction) - Valid: ' + valid);
});
results;
=== Medium Polygon Simplification (tolerance=1) === Original vertices: 10 topology : 10 vertices (0.0% reduction) - Valid: true douglas : 10 vertices (0.0% reduction) - Valid: true vw : 10 vertices (0.0% reduction) - Valid: true
Out[15]:
{
topology: {
vertices: 10,
valid: true,
reduction: "0.0",
geometry: {
type: "Polygon",
coordinates: [
[
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array]
]
]
}
},
douglas: {
vertices: 10,
valid: true,
reduction: "0.0",
geometry: {
type: "Polygon",
coordinates: [
[
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array]
]
]
}
},
vw: {
vertices: 10,
valid: true,
reduction: "0.0",
geometry: {
type: "Polygon",
coordinates: [
[
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array], [Array],
[Array]
]
]
}
}
}
Test 3: Complex Polygon with Multiple Tolerances¶
In [20]:
const tolerances = [0.5, 1.0, 2.0];
console.log('\n=== Complex Polygon - Multiple Tolerances ===');
console.log('Original vertices:', countVertices(complexPolygon));
console.log('');
tolerances.forEach(tol => {
console.log('--- Tolerance: ' + tol + ' ---');
methods.forEach(method => {
const simplified = simplifyGeometry(complexPolygon, tol, method);
const vertices = countVertices(simplified);
const valid = isValid(simplified);
const reduction = ((1 - vertices / countVertices(complexPolygon)) * 100).toFixed(1);
console.log(' ' + method.padEnd(10) + ': ' + vertices + ' vertices (' + reduction + '% reduction) - Valid: ' + valid);
});
console.log('');
});
=== Complex Polygon - Multiple Tolerances === Original vertices: 36 --- Tolerance: 0.5 --- topology : 6 vertices (83.3% reduction) - Valid: true douglas : 6 vertices (83.3% reduction) - Valid: true vw : 21 vertices (41.7% reduction) - Valid: true --- Tolerance: 1 --- topology : 4 vertices (88.9% reduction) - Valid: true douglas : 4 vertices (88.9% reduction) - Valid: true vw : 6 vertices (83.3% reduction) - Valid: true --- Tolerance: 2 --- topology : 4 vertices (88.9% reduction) - Valid: true douglas : 4 vertices (88.9% reduction) - Valid: true vw : 4 vertices (88.9% reduction) - Valid: true
Visualization: SVG Output¶
In [26]:
// Helper to get bounding box
function getBounds(geojson) {
const coords = geojson.coordinates[0];
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
coords.forEach(c => {
minX = Math.min(minX, c[0]);
minY = Math.min(minY, c[1]);
maxX = Math.max(maxX, c[0]);
maxY = Math.max(maxY, c[1]);
});
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
}
// Create SVG visualization with auto-scaling
function createSVG(original, simplified, title) {
const bounds = getBounds(original);
// Settings for SVG
const width = 600;
const height = 300;
const padding = 20;
const panelWidth = width / 2;
// Calculate scale to fit in one panel (w/ padding)
const scaleX = (panelWidth - padding * 2) / bounds.width;
const scaleY = (height - padding * 2) / bounds.height;
const scale = Math.min(scaleX, scaleY);
// Function to transform coordinates to SVG space
const transform = (coords, offsetX) => {
return coords.map(c => {
const x = (c[0] - bounds.minX) * scale + padding + offsetX;
// SVG Y is top-down, so we flip it
const y = height - ((c[1] - bounds.minY) * scale + padding);
return `${x},${y}`;
}).join(' L ');
};
const path1 = 'M ' + transform(original.coordinates[0], 0);
const path2 = 'M ' + transform(simplified.coordinates[0], panelWidth);
return `
<svg width="${width}" height="${height}" style="border: 1px solid #ccc; background: white; margin: 10px;">
<!-- Title -->
<text x="${width/2}" y="25" text-anchor="middle" font-family="sans-serif" font-weight="bold" fill="#333">${title}</text>
<!-- Left Panel -->
<text x="${panelWidth/2}" y="${height - 10}" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#666">
Original (${countVertices(original)} pts)
</text>
<path d="${path1}" fill="rgba(0,136,255,0.1)" stroke="#0088ff" stroke-width="1.5"/>
<!-- Right Panel -->
<text x="${panelWidth * 1.5}" y="${height - 10}" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#666">
Simplified (${countVertices(simplified)} pts)
</text>
<path d="${path2}" fill="rgba(0,255,136,0.1)" stroke="#00b359" stroke-width="1.5"/>
<!-- Divider -->
<line x1="${panelWidth}" y1="40" x2="${panelWidth}" y2="${height-30}" stroke="#eee" />
</svg>
`;
}
// Generate visualization for the very complex polygon
// Use a higher tolerance since the shape is much larger (radius ~50)
const tolerance = 5.0;
const complexSimplified = simplifyGeometry(veryComplexPolygon, tolerance, 'topology');
const svg = createSVG(veryComplexPolygon, complexSimplified, 'Detailed Simplification (Topology Preserving)');
// Display
if (typeof Deno !== 'undefined' && Deno?.jupyter) {
await Deno.jupyter.display(Deno.jupyter.html`${svg}`);
} else {
// For Node.js/Console (approximate)
console.log("SVG Size:", svg.length, "bytes - Run in Deno Jupyter to view");
}
Comparison Table¶
In [22]:
// Create comparison summary
const comparison = [
['Algorithm', 'Speed', 'Validity', 'Aggressiveness', 'Best For'],
['TopologyPreserving', 'Medium', 'Always Valid', 'Conservative', 'GIS, CAD'],
['DouglasPeucker', 'Fast', 'May Break', 'Aggressive', 'Visualization'],
['Visvalingam-Whyatt', 'Medium', 'Usually Valid', 'Balanced', 'Cartography']
];
console.table(comparison);
┌───────┬──────────────────────┬──────────┬─────────────────┬──────────────────┬─────────────────┐ │ (idx) │ 0 │ 1 │ 2 │ 3 │ 4 │ ├───────┼──────────────────────┼──────────┼─────────────────┼──────────────────┼─────────────────┤ │ 0 │ "Algorithm" │ "Speed" │ "Validity" │ "Aggressiveness" │ "Best For" │ │ 1 │ "TopologyPreserving" │ "Medium" │ "Always Valid" │ "Conservative" │ "GIS, CAD" │ │ 2 │ "DouglasPeucker" │ "Fast" │ "May Break" │ "Aggressive" │ "Visualization" │ │ 3 │ "Visvalingam-Whyatt" │ "Medium" │ "Usually Valid" │ "Balanced" │ "Cartography" │ └───────┴──────────────────────┴──────────┴─────────────────┴──────────────────┴─────────────────┘
Conclusion¶
This notebook demonstrated:
- ✅ Three JSTS simplification algorithms
- ✅ Comparison on polygons of varying complexity
- ✅ Vertex reduction percentages
- ✅ Geometry validity checks
- ✅ Visual comparison
Key Takeaway: Use TopologyPreservingSimplifier when geometry validity is critical!