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");
}
Detailed Simplification (Topology Preserving) Original (200 pts) Simplified (84 pts)

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!