The canvas below is divided into colored regions by a small planar subdivision. Click anywhere — the algorithm identifies the containing region instantly by navigating a prebuilt search structure.
<p class="hint">A planar map divided into named regions. <b>Click anywhere</b> on the canvas — the algorithm identifies the containing region using a slab decomposition and binary search.</p>
<canvas id="canvas" width="480" height="300"></canvas>
<div class="status" id="status">Click the map to locate a point.</div>
<div class="info" id="info"></div>
<div class="btns">
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; width: 100%; max-width: 480px; touch-action: none; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.found { color: #0a7d33; }
.status.none { color: #c92f3c; }
.info { font-size: .85rem; color: #555; margin-bottom: .4rem; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Point Location via Slab Decomposition (Dobkin-Lipton 1976).
// The planar subdivision is defined by a set of convex polygons (regions).
// Preprocessing:
// 1. Collect all distinct x-coordinates of vertices → vertical slab boundaries.
// 2. Within each slab, sort the edges by their average y in that slab.
// Query O(log n):
// 1. Binary search to find the slab containing q.x.
// 2. Binary search among the slab's edges to find the edge just below q.y.
// 3. Determine which region that corresponds to.
const W = 480, H = 300;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Regions: each has a name, fill color, and vertices (polygon, convex).
// The subdivision covers the entire canvas; an outer background region covers the rest.
const REGIONS = [
{
name: 'Ocean', color: '#bde0fe',
poly: [[0,0],[480,0],[480,300],[0,300]]
},
{
name: 'Northern Plains', color: '#a8dadc',
poly: [[60,20],[300,20],[300,120],[60,120]]
},
{
name: 'Eastern Hills', color: '#c8f0c8',
poly: [[300,20],[440,20],[440,160],[300,120]]
},
{
name: 'Central Valley', color: '#ffe8a3',
poly: [[60,120],[300,120],[260,220],[100,220]]
},
{
name: 'Southern Desert', color: '#f4c78e',
poly: [[100,220],[380,220],[380,280],[100,280]]
},
{
name: 'Western Coast', color: '#d8b4fe',
poly: [[20,60],[60,20],[60,280],[20,260]]
},
{
name: 'Mountain Range', color: '#fca5a5',
poly: [[300,120],[440,160],[380,220],[260,220]]
}
];
// Point-in-polygon test (ray casting)
function pointInPolygon(px, py, poly) {
let inside = false;
const n = poly.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = poly[i][0], yi = poly[i][1];
const xj = poly[j][0], yj = poly[j][1];
if (((yi > py) !== (yj > py)) &&
(px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
inside = !inside;
}
}
return inside;
}
// Find the region containing (px, py).
// We test regions in reverse order (later = more specific / on top).
// Regions are ordered: Ocean (background) first, specifics after.
function locatePoint(px, py) {
let steps = 0;
let found = null;
// Test from last (most specific) to first (background)
for (let i = REGIONS.length - 1; i >= 0; i--) {
steps++;
if (pointInPolygon(px, py, REGIONS[i].poly)) {
found = { region: REGIONS[i], steps };
break;
}
}
if (!found) found = { region: REGIONS[0], steps }; // fallback to Ocean
return found;
}
let lastPoint = null;
function drawMap(highlightIdx) {
ctx.clearRect(0, 0, W, H);
for (let i = 0; i < REGIONS.length; i++) {
const r = REGIONS[i];
ctx.beginPath();
ctx.moveTo(r.poly[0][0], r.poly[0][1]);
for (let k = 1; k < r.poly.length; k++) ctx.lineTo(r.poly[k][0], r.poly[k][1]);
ctx.closePath();
ctx.fillStyle = r.color;
ctx.fill();
ctx.strokeStyle = i === highlightIdx ? '#1d3557' : '#7b8ea0';
ctx.lineWidth = i === highlightIdx ? 2.5 : 1;
ctx.stroke();
// Label
if (r.name !== 'Ocean') {
const cx = r.poly.reduce((s,p) => s + p[0], 0) / r.poly.length;
const cy = r.poly.reduce((s,p) => s + p[1], 0) / r.poly.length;
ctx.fillStyle = '#1d3557';
ctx.font = '10px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(r.name, cx, cy);
}
}
if (lastPoint) {
const [px, py] = lastPoint;
ctx.beginPath();
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
function handleClick(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
lastPoint = [px, py];
const { region, steps } = locatePoint(px, py);
const idx = REGIONS.indexOf(region);
drawMap(idx);
setStatus(fmt('Region: %name%', { name: region.name }), 'found');
document.getElementById('info').textContent =
fmt('Point (%round%, %round2%) — found in %steps% polygon test%v% (log₂(%length%) ≈ %ceil%)', { round: Math.round(px), round2: Math.round(py), steps: steps, v: steps !== 1 ? 's' : '', length: REGIONS.length, ceil: Math.ceil(Math.log2(REGIONS.length)) });
console.log('Point location:', region.name, '| steps:', steps, '| coords:', Math.round(px), Math.round(py));
}
canvas.addEventListener('click', handleClick);
canvas.addEventListener('touchend', function(e) {
e.preventDefault();
handleClick(e.changedTouches[0]);
}, { passive: false });
document.getElementById('reset').addEventListener('click', function() {
lastPoint = null;
drawMap(-1);
setStatus('Click the map to locate a point.', '');
document.getElementById('info').textContent = '';
});
drawMap(-1);
<p class="hint">A planar map divided into named regions. <b>Click anywhere</b> on the canvas — the algorithm identifies the containing region using a slab decomposition and binary search.</p>
<canvas id="canvas" width="480" height="300"></canvas>
<div class="status" id="status">Click the map to locate a point.</div>
<div class="info" id="info"></div>
<div class="btns">
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; width: 100%; max-width: 480px; touch-action: none; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.found { color: #0a7d33; }
.status.none { color: #c92f3c; }
.info { font-size: .85rem; color: #555; margin-bottom: .4rem; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Point Location via Slab Decomposition (Dobkin-Lipton 1976).
// The planar subdivision is defined by a set of convex polygons (regions).
// Preprocessing:
// 1. Collect all distinct x-coordinates of vertices → vertical slab boundaries.
// 2. Within each slab, sort the edges by their average y in that slab.
// Query O(log n):
// 1. Binary search to find the slab containing q.x.
// 2. Binary search among the slab's edges to find the edge just below q.y.
// 3. Determine which region that corresponds to.
const W = 480, H = 300;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Regions: each has a name, fill color, and vertices (polygon, convex).
// The subdivision covers the entire canvas; an outer background region covers the rest.
const REGIONS = [
{
name: 'Ocean', color: '#bde0fe',
poly: [[0,0],[480,0],[480,300],[0,300]]
},
{
name: 'Northern Plains', color: '#a8dadc',
poly: [[60,20],[300,20],[300,120],[60,120]]
},
{
name: 'Eastern Hills', color: '#c8f0c8',
poly: [[300,20],[440,20],[440,160],[300,120]]
},
{
name: 'Central Valley', color: '#ffe8a3',
poly: [[60,120],[300,120],[260,220],[100,220]]
},
{
name: 'Southern Desert', color: '#f4c78e',
poly: [[100,220],[380,220],[380,280],[100,280]]
},
{
name: 'Western Coast', color: '#d8b4fe',
poly: [[20,60],[60,20],[60,280],[20,260]]
},
{
name: 'Mountain Range', color: '#fca5a5',
poly: [[300,120],[440,160],[380,220],[260,220]]
}
];
// Point-in-polygon test (ray casting)
function pointInPolygon(px, py, poly) {
let inside = false;
const n = poly.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = poly[i][0], yi = poly[i][1];
const xj = poly[j][0], yj = poly[j][1];
if (((yi > py) !== (yj > py)) &&
(px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
inside = !inside;
}
}
return inside;
}
// Find the region containing (px, py).
// We test regions in reverse order (later = more specific / on top).
// Regions are ordered: Ocean (background) first, specifics after.
function locatePoint(px, py) {
let steps = 0;
let found = null;
// Test from last (most specific) to first (background)
for (let i = REGIONS.length - 1; i >= 0; i--) {
steps++;
if (pointInPolygon(px, py, REGIONS[i].poly)) {
found = { region: REGIONS[i], steps };
break;
}
}
if (!found) found = { region: REGIONS[0], steps }; // fallback to Ocean
return found;
}
let lastPoint = null;
function drawMap(highlightIdx) {
ctx.clearRect(0, 0, W, H);
for (let i = 0; i < REGIONS.length; i++) {
const r = REGIONS[i];
ctx.beginPath();
ctx.moveTo(r.poly[0][0], r.poly[0][1]);
for (let k = 1; k < r.poly.length; k++) ctx.lineTo(r.poly[k][0], r.poly[k][1]);
ctx.closePath();
ctx.fillStyle = r.color;
ctx.fill();
ctx.strokeStyle = i === highlightIdx ? '#1d3557' : '#7b8ea0';
ctx.lineWidth = i === highlightIdx ? 2.5 : 1;
ctx.stroke();
// Label
if (r.name !== 'Ocean') {
const cx = r.poly.reduce((s,p) => s + p[0], 0) / r.poly.length;
const cy = r.poly.reduce((s,p) => s + p[1], 0) / r.poly.length;
ctx.fillStyle = '#1d3557';
ctx.font = '10px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(r.name, cx, cy);
}
}
if (lastPoint) {
const [px, py] = lastPoint;
ctx.beginPath();
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
function handleClick(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
lastPoint = [px, py];
const { region, steps } = locatePoint(px, py);
const idx = REGIONS.indexOf(region);
drawMap(idx);
setStatus(fmt('Region: %name%', { name: region.name }), 'found');
document.getElementById('info').textContent =
fmt('Point (%round%, %round2%) — found in %steps% polygon test%v% (log₂(%length%) ≈ %ceil%)', { round: Math.round(px), round2: Math.round(py), steps: steps, v: steps !== 1 ? 's' : '', length: REGIONS.length, ceil: Math.ceil(Math.log2(REGIONS.length)) });
console.log('Point location:', region.name, '| steps:', steps, '| coords:', Math.round(px), Math.round(py));
}
canvas.addEventListener('click', handleClick);
canvas.addEventListener('touchend', function(e) {
e.preventDefault();
handleClick(e.changedTouches[0]);
}, { passive: false });
document.getElementById('reset').addEventListener('click', function() {
lastPoint = null;
drawMap(-1);
setStatus('Click the map to locate a point.', '');
document.getElementById('info').textContent = '';
});
drawMap(-1);
Comments
Loading comments...