StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
StackGenVis/frontend/node_modules/polybooljs/dist/demo.html

1362 lines
33 KiB

4 years ago
<!doctype html>
<html lang="en">
<head>
<title>Polygon Clipping - Based somewhat on F. Martinez et al. (2008)</title>
<!--
/*
* @copyright 2016 Sean Connelly (@voidqk), http://syntheti.cc
* @license MIT
* @preserve Project Home: https://github.com/voidqk/polybooljs
*/
-->
<style>
body, html {
background-color: #ddf;
font-family: sans-serif;
color: #333;
}
a {
color: #33d;
text-decoration: none;
}
a:hover, a:active {
text-decoration: underline;
}
p {
text-align: center;
}
</style>
<script src="./polybool.js"></script>
</head>
<body onload="javascript: init();">
<script>
var cnv, ctx;
var rscale = 2;
var wscale = 2;
var cnvWidth, cnvHeight;
var hover = false;
var mode = 'Intersect';
var polyCases = [{
name: 'Assorted Polygons',
poly1: {
regions: [
[ [500,60],[500,150],[320,150],[260,210],[200,150],[200,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [500,60],[500,150],[460,190],[460,110],[400,180],[160,90] ],
[ [220,170],[260,30],[310,160],[310,210],[260,170],[240,190] ]
],
inverted: false
}
}, {
name: 'Simple Rectangles',
poly1: {
regions: [
[ [200, 50], [600, 50], [600, 150], [200, 150] ]
],
inverted: false
},
poly2: {
regions: [
[ [300, 150], [500, 150], [500, 200], [300, 200] ]
],
inverted: false
}
}, {
name: 'Shared Right Edge',
poly1: {
regions: [
[ [500,60],[500,150],[200,150],[200,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [500,60],[500,150],[450,230],[400,180],[590,60] ]
],
inverted: false
}
}, {
name: 'Simple Boxes',
poly1: {
regions: [
[ [500,60],[500,150],[200,150],[200,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [500,60],[500,150],[380,150],[380,60] ]
],
inverted: false
}
}, {
name: 'Simple Self-Overlap',
poly1: {
regions: [
[ [200,50],[400,50],[400,150],[200,150] ]
],
inverted: false
},
poly2: {
regions: [
[ [400,150],[500,150],[300,50],[400,50] ]
],
inverted: false
}
}, {
name: 'M Shape',
poly1: {
regions: [
[ [570,60],[570,150],[60,150],[60,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [500,60],[500,130],[330,20],[180,130],[120,60] ]
],
inverted: false
}
}, {
name: 'Two Triangles With Common Edge',
poly1: {
regions: [
[ [620,60],[620,150],[90,150],[90,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [350,60],[480,200],[180,60] ],
[ [180,60],[500,60],[180,220] ]
],
inverted: false
}
}, {
name: 'Two Trianges With Common Edge, pt. 2',
poly1: {
regions: [
[ [620,60],[620,150],[90,150],[90,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [400,60],[270,120],[210,60] ],
[ [210,60],[530,60],[210,220] ]
],
inverted: false
}
}, {
name: 'Two Triangles With Common Edge, pt. 3',
poly1: {
regions: [
[ [620,60],[620,150],[90,150],[90,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [370,60],[300,220],[560,60] ],
[ [180,60],[500,60],[180,220] ]
],
inverted: false
}
}, {
name: 'Three Triangles',
poly1: {
regions: [
[ [500,60],[500,150],[320,150] ]
],
inverted: false
},
poly2: {
regions: [
[ [500,60],[500,150],[460,190] ],
[ [220,170],[260,30],[310,160] ],
[ [260,210],[200,150],[200,60] ]
],
inverted: false
}
}, {
name: 'Adjacent Edges in Status',
poly1: {
regions: [
[ [620,60],[620,150],[90,150],[90,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [110,60],[420,230],[540,60] ],
[ [180,60],[430,160],[190,200] ]
],
inverted: false
}
}, {
name: 'Coincident Self-Intersection',
poly1: {
regions: [
[ [500,60],[500,150],[320,150],[260,210],[200,150],[200,60] ]
],
inverted: false
},
poly2: {
regions: [
[ [500,60],[500,150],[460,190],[460,110],[400,180],[70,90] ],
[ [220,170],[580,130],[310,160],[310,210],[260,170],[240,190] ]
],
inverted: false
}
}, {
name: 'Coincident Self-Intersection, pt. 2',
poly1: {
regions: [
[ [100, 100], [200, 200], [300, 100] ],
[ [200, 100], [300, 200], [400, 100] ]
],
inverted: false
},
poly2: {
regions: [
[ [50, 50], [200, 50], [300, 150] ]
],
inverted: false
}
}, {
name: 'Triple Overlap',
poly1: {
regions: [
[ [500, 60], [500, 150], [320, 150], [260, 210], [200, 150], [200, 60] ]
],
inverted: false
},
poly2: {
regions:[
[ [500, 60], [500, 150], [370, 60], [310, 60], [400, 180], [230, 60] ],
[ [260, 60], [410, 60], [310, 160], [310, 210], [260, 170], [240, 190] ]
],
inverted: false
}
}];
var nextDemoIndex = -1;
var caseName, poly1, poly2, polyBox;
var scaleToFit = false;
function nextDemo(d){
nextDemoIndex = (nextDemoIndex + d) % polyCases.length;
if (nextDemoIndex < 0)
nextDemoIndex += polyCases.length;
var demo = polyCases[nextDemoIndex];
caseName = (nextDemoIndex + 1) + '. ' + demo.name;
poly1 = demo.poly1;
poly2 = demo.poly2;
polyBox = { min: [false, false], max: [false, false] };
function calcBox(regions){
for (var r = 0; r < regions.length; r++){
var region = regions[r];
for (var p = 0; p < region.length; p++){
var pt = region[p];
if (polyBox.min[0] === false || pt[0] < polyBox.min[0])
polyBox.min[0] = pt[0];
if (polyBox.min[1] === false || pt[1] < polyBox.min[1])
polyBox.min[1] = pt[1];
if (polyBox.max[0] === false || pt[0] > polyBox.max[0])
polyBox.max[0] = pt[0];
if (polyBox.max[1] === false || pt[1] > polyBox.max[1])
polyBox.max[1] = pt[1];
}
}
}
calcBox(poly1.regions);
calcBox(poly2.regions);
recalc();
}
function setMode(txt){
mode = txt;
recalc();
}
function colComp(n){
n = Math.floor(n * 256);
if (n > 255)
return 255;
if (n < 0)
return 0;
return n;
}
function colToHex(color){
function hex(n){
n = colComp(n).toString(16);
return (n.length <= 1 ? '0' : '') + n;
}
return '#' + hex(color[0]) + hex(color[1]) + hex(color[2]);
}
function vertRadius(region, vert){
if (vert === false)
return 0;
if (hover !== false && hover.region === region && hover.vert === vert)
return 6;
return 3;
}
function drawVerts(poly, offset){
poly.regions.forEach(function(region, region_i){
for (var i = 0; i < region.length; i++){
ctx.beginPath();
ctx.arc(scaleX(region[i][0]), scaleY(region[i][1]) + offset, vertRadius(region, i),
0, Math.PI * 2);
ctx.fill();
}
});
}
var buildLogMax = 0;
var buildLogTimer = false;
function buildLogNext(delta){
buildLogMax += delta;
if (buildLogMax < 0)
buildLogMax = 0;
if (buildLogMax > clipResult.build_log.length)
buildLogMax = clipResult.build_log.length;
redraw();
return buildLogMax >= clipResult.build_log.length;
}
function buildLogNextWrap(delta){
buildLogStop();
if (buildLogMax <= 0 && delta < 0){
buildLogMax = clipResult.build_log.length;
redraw();
}
else if (buildLogMax >= clipResult.build_log.length && delta > 0){
buildLogMax = 0;
redraw();
}
else
buildLogNext(delta);
}
function buildLogStop(){
if (buildLogTimer === false)
return;
clearInterval(buildLogTimer);
buildLogTimer = false;
document.getElementById('bl_play').innerHTML = 'Play';
}
function buildLogPlay(){
if (buildLogTimer === false){
if (buildLogNext(1))
buildLogMax = -1;
buildLogTimer = setInterval(function(){
if (buildLogNext(1)){
buildLogStop();
buildLogTimer = setInterval(function(){
buildLogNextWrap(1);
clearInterval(buildLogTimer);
}, 100);
}
}, 100);
}
else{
clearInterval(buildLogTimer);
buildLogTimer = false;
}
document.getElementById('bl_play').innerHTML = buildLogTimer === false ? 'Play' : 'Stop';
}
var clipResult;
function recalc(){
buildLogStop();
buildLogMax = 0;
var func = ({
'Intersect' : PolyBool.intersect,
'Union' : PolyBool.union,
'Red - Blue': PolyBool.difference,
'Blue - Red': PolyBool.differenceRev,
'Xor' : PolyBool.xor
})[mode];
var BL = PolyBool.buildLog(true);
clipResult = {
result: func(poly1, poly2),
build_log: BL
};
redraw();
// output GeoJSON
var geojson = PolyBool.polygonToGeoJSON(clipResult.result);
function scalePoly(p){
// we need to scale the result because pixel coordinates are around 500, and that's not
// valid long/lat coordinates... so we just divide everything by 10
// (and out of pure luck this tends to place our polygons over Ethiopia...!)
for (var i = 0; i < p.length; i++){
for (var j = 0; j < p[i].length; j++)
p[i][j] = [p[i][j][0] * 0.1, p[i][j][1] * 0.1];
}
}
// I suppose we could just JSON.stringify(geojson, null, ' '), but that doesn't look so
// pretty (imho), so this is a bit stupid but I format it myself so it looks better :-P
var out = ['{', ' "type": ' + JSON.stringify(geojson.type) + ','];
function outLine(line, space, tail){
var o = space + '[';
for (var i = 0; i < line.length; i++){
o += '[' + line[i] + ']';
if (i < line.length - 1)
o += ', ';
}
out.push(o + ']' + (tail ? '' : ','));
}
if (geojson.type == 'Polygon'){
scalePoly(geojson.coordinates);
out.push(' "coordinates": [');
for (var i = 0; i < geojson.coordinates.length; i++)
outLine(geojson.coordinates[i], ' ', i === geojson.coordinates.length - 1);
out.push(' ]');
}
else{
for (var i = 0; i < geojson.coordinates.length; i++)
scalePoly(geojson.coordinates[i]);
out.push(' "coordinates": [[');
for (var i = 0; i < geojson.coordinates.length; i++){
for (var j = 0; j < geojson.coordinates[i].length; j++)
outLine(geojson.coordinates[i][j], ' ', j === geojson.coordinates[i].length - 1);
if (i < geojson.coordinates.length - 1)
out.push(' ], [');
}
out.push(' ]]');
}
out.push('}', '');
document.getElementById('geojson').value = out.join('\n');
}
function scaleX(x){
if (!scaleToFit)
return x;
return (x - polyBox.min[0]) * 650 / (polyBox.max[0] - polyBox.min[0]) + 25;
}
function scaleY(y){
if (!scaleToFit)
return y;
return (y - polyBox.min[1]) * 200 / (polyBox.max[1] - polyBox.min[1]) + 25;
}
function unscaleX(x){
if (!scaleToFit)
return x;
return (x - 25) * (polyBox.max[0] - polyBox.min[0]) / 650 + polyBox.min[0];
}
function unscaleY(y){
if (!scaleToFit)
return y;
return (y - 25) * (polyBox.max[1] - polyBox.min[1]) / 200 + polyBox.min[1];
}
function drawRegions(regions, offset){
regions.forEach(function(region, i){
if (region.length <= 0)
return;
ctx.moveTo(scaleX(region[0][0]), scaleY(region[0][1]) + offset);
for (var i = 1; i < region.length; i++)
ctx.lineTo(scaleX(region[i][0]), scaleY(region[i][1]) + offset);
ctx.closePath();
});
}
function polyStroke(result, offset){
ctx.beginPath();
drawRegions(result.regions, offset);
ctx.stroke();
}
function polyFill(result, rect1, rect2, offset){
ctx.beginPath();
if (result.inverted){
ctx.moveTo(rect1[0], rect1[1] + offset);
ctx.lineTo(rect1[0], rect2[1] + offset);
ctx.lineTo(rect2[0], rect2[1] + offset);
ctx.lineTo(rect2[0], rect1[1] + offset);
ctx.closePath();
}
drawRegions(result.regions, offset);
ctx.fill('evenodd');
}
function redraw(){
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, cnvWidth, cnvHeight);
ctx.lineWidth = 2;
var labels = [];
// this is quite the mess... sorry...
var bl_oldsegs = [];
var bl_segs = [];
var bl_segid = {};
var bl_vert = 0;
var bl_last_check = false;
var bl_last_check_i1 = false;
var bl_last_check_i2 = false;
var bl_last_div_seg = false;
var bl_last_pop_seg = false;
var bl_last_pop_seg_i = false;
var bl_last_status = false;
var bl_last_temp_status = false;
var bl_last_temp_status_i = false;
var bl_last_chop = false;
var bl_last_seg_keep = false;
var bl_last_seg_keep_match = false;
var bl_last_done = false;
var bl_finish = false;
var bl_status = [];
var bl_chains = [];
var bl_chainids = [];
var bl_nextchainid = 0;
var bl_oldchains = [];
var bl_oldchainids = [];
var bl_phase = 0;
var bl_selected = false;
clipResult.build_log.forEach(function(blw, i){
if (i >= buildLogMax)
return;
var bl = blw.data;
bl_last_check = false;
bl_last_div_seg = false;
bl_last_pop_seg = false;
bl_last_status = false;
bl_last_chop = false;
bl_last_done = false;
switch (blw.type){
case 'vert':
bl_vert = bl.x;
break;
case 'new_seg':
for (var i = 0; i < bl_segs.length; i++){
if (bl_segs[i].id === bl.seg.id){
bl_segs.splice(i, 1);
break;
}
}
for (var i = 0; i < bl_oldsegs.length; i++){
if (bl_oldsegs[i].id === bl.seg.id){
bl_oldsegs.splice(i, 1);
break;
}
}
bl_segs.push(bl.seg);
bl_segid[bl.seg.id] = {
phase: bl_phase,
primary: bl.primary
};
break;
case 'rem_seg':
for (var i = 0; i < bl_segs.length; i++){
if (bl_segs[i].id === bl.seg.id){
bl_segs.splice(i, 1);
break;
}
}
for (var i = 0; i < bl_oldsegs.length; i++){
if (bl_oldsegs[i].id === bl.seg.id){
bl_oldsegs.splice(i, 1);
break;
}
}
break;
case 'check':
bl_last_check = bl;
bl_last_check_i1 = false;
bl_last_check_i2 = false;
for (var i = 0; i < bl_status.length; i++){
if (bl_status[i].id === bl.seg1.id)
bl_last_check_i1 = i;
if (bl_status[i].id === bl.seg2.id)
bl_last_check_i2 = i;
}
break;
case 'pop_seg':
bl_last_pop_seg = bl;
for (var i = 0; i < bl_segs.length; i++){
if (bl_segs[i].id === bl.seg.id){
var r = bl_segs.splice(i, 1)[0];
bl_oldsegs.push(r);
break;
}
}
for (var i = 0; i < bl_status.length; i++){
if (bl_status[i].id === bl.seg.id){
bl_last_pop_seg_i = i;
bl_status.splice(i, 1);
break;
}
}
break;
case 'div_seg':
bl_last_div_seg = bl;
break;
case 'temp_status':
bl_last_temp_status = bl;
if (bl.above === false)
bl_last_temp_status_i = -0.5;
else{
for (var i = 0; i < bl_status.length; i++){
if (bl_status[i].id === bl.above.id){
bl_last_temp_status_i = i + 0.5;
break;
}
}
}
break;
case 'status':
bl_last_temp_status = false;
bl_last_status = bl.seg.id;
for (var i = 0; i < bl_segs.length; i++){
if (bl_segs[i].id === bl.seg.id){
bl_segs[i] = bl.seg;
break;
}
}
if (bl.above === false)
bl_status.unshift(bl.seg);
else{
for (var i = 0; i < bl_status.length; i++){
if (bl_status[i].id === bl.above.id){
bl_status.splice(i + 1, 0, bl.seg);
break;
}
}
}
break;
case 'rewind':
bl_last_temp_status = false;
for (var i = 0; i < bl_segs.length; i++){
if (bl_segs[i].id === bl.seg.id){
bl_segs.splice(i, 1);
break;
}
}
break;
case 'chop':
bl_last_chop = bl;
for (var i = 0; i < bl_segs.length; i++){
if (bl_segs[i].id === bl.seg.id){
bl_segs[i] = JSON.parse(JSON.stringify(bl_segs[i]));
bl_segs[i].end = bl.pt;
}
}
break;
case 'seg_update':
function chk_seg(seg){
if (seg.id !== bl.seg.id)
return seg;
return bl.seg;
}
for (var i = 0; i < bl_segs.length; i++)
bl_segs[i] = chk_seg(bl_segs[i]);
for (var i = 0; i < bl_oldsegs.length; i++)
bl_oldsegs[i] = chk_seg(bl_oldsegs[i]);
break;
case 'log':
if (i === buildLogMax - 1)
console.log(bl.txt);
break;
case 'reset':
bl_segs = [];
bl_oldsegs = [];
bl_segid = {};
bl_status = [];
bl_last_temp_status = false;
break;
case 'selected':
bl_selected = bl.segs;
bl_phase++;
break;
case 'chain_start':
bl_last_seg_keep = bl;
bl_last_seg_keep_match = false;
break;
case 'chain_new':
bl_chains.push([ bl.pt1, bl.pt2 ]);
bl_chainids.push(bl_nextchainid++);
bl_last_seg_keep = false;
break;
case 'chain_rev':
bl_chains[bl.index].reverse();
break;
case 'chain_add_head':
bl_chains[bl.index].unshift(bl.pt);
bl_last_seg_keep = false;
break;
case 'chain_add_tail':
bl_chains[bl.index].push(bl.pt);
bl_last_seg_keep = false;
break;
case 'chain_rem_head':
bl_chains[bl.index].shift();
break;
case 'chain_rem_tail':
bl_chains[bl.index].pop();
break;
case 'chain_match':
bl_last_seg_keep_match = bl.index;
break;
case 'chain_con':
bl_last_seg_keep_match = bl.index1;
break;
case 'chain_join':
bl_chains[bl.index1] = bl_chains[bl.index1].concat(bl_chains[bl.index2]);
bl_chains.splice(bl.index2, 1);
bl_chainids.splice(bl.index2, 1);
bl_last_seg_keep = false;
break;
case 'chain_close':
bl_oldchains.push(bl_chains.splice(bl.index, 1)[0]);
bl_oldchainids.push(bl_chainids.splice(bl.index, 1)[0]);
bl_last_seg_keep = false;
break;
case 'done':
bl_last_done = true;
bl_vert = false;
bl_phase++;
if (bl_phase === 5)
bl_finish = true;
break;
default: console.log(blw.type, bl);
}
});
function drawseg(seg, fade, size){
var poly = bl_segid[seg.id];
var poly1 = poly.phase === 0 || (((poly.phase === 2 && !bl_last_done) || bl_phase === 3) && poly.primary);
if (!poly1 && bl_phase === 0)
return;
if (poly1 && bl_phase === 1 && !bl_last_done)
return;
if (bl_phase === 2 && bl_last_done && poly1)
return;
var ang = Math.atan2(seg.end[1] - seg.start[1], seg.end[0] - seg.start[0]);
ctx.beginPath();
ctx.moveTo(
seg.start[0],
seg.start[1]
);
ctx.lineTo(
seg.end[0],
seg.end[1]
);
ctx.strokeStyle = poly1 ? (fade ? '#faa' : '#f00') : (fade ? '#aaf' : '#00f');
ctx.lineWidth = size;
ctx.stroke();
function drawfill(ang, fill, mine){
var poly1 =
poly.phase === 0 ? mine :
poly.phase === 1 ? !mine :
mine === poly.primary;
var dist = 6;
ctx.beginPath();
ctx.arc(
dist * Math.cos(ang) + (seg.start[0] + seg.end[0]) / 2,
dist * Math.sin(ang) + (seg.start[1] + seg.end[1]) / 2,
fill === null ? 1 : 3,
0, Math.PI * 2
);
ctx.fillStyle = (fill === true || fill === null) ? (poly1 ? '#f00' : '#00f') : '#fff';
ctx.fill();
ctx.strokeStyle = (fill === true) ? '#000' : (poly1 ? '#f00' : '#00f');
ctx.lineWidth = 1;
ctx.stroke();
}
var d = (bl_phase <= 1 || (bl_phase === 2 && bl_last_done)) ? 0 : 0.75;
drawfill(ang + Math.PI * 0.5 - d, seg.myFill.above, true);
drawfill(ang - Math.PI * 0.5 + d, seg.myFill.below, true);
if (bl_phase > 1 && !(bl_phase === 2 && bl_last_done)){
drawfill(ang + Math.PI * 0.5 + d, seg.otherFill ? seg.otherFill.above : null, false);
drawfill(ang - Math.PI * 0.5 - d, seg.otherFill ? seg.otherFill.below : null, false);
}
}
if (bl_phase < 4){
bl_oldsegs.forEach(function(seg){
drawseg(seg, true, 1);
});
bl_segs.forEach(function(seg){
drawseg(seg, false, 2);
labels.push({
txt: seg.id,
x: (seg.start[0] + seg.end[0]) / 2,
y: (seg.start[1] + seg.end[1]) / 2
});
});
}
var stw = 40;
var sth = 20;
function stpos(i){
return [ stw * 1.5 + 20, cnvHeight / 2 - i * sth - 30 - sth / 2 ];
}
if (bl_phase < 4 && bl_last_check){
ctx.beginPath();
ctx.moveTo(bl_last_check.seg1.start[0], bl_last_check.seg1.start[1]);
ctx.lineTo(bl_last_check.seg1.end[0], bl_last_check.seg1.end[1]);
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bl_last_check.seg2.start[0], bl_last_check.seg2.start[1]);
ctx.lineTo(bl_last_check.seg2.end[0], bl_last_check.seg2.end[1]);
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 2;
ctx.stroke();
if (bl_last_check_i1 === false || bl_last_check_i2 === false){
ctx.beginPath();
var c1 = stpos(bl_last_temp_status_i);
c1[0] += stw + 10;
var c2 = stpos(bl_last_check_i1 === false ? bl_last_check_i2 : bl_last_check_i1);
ctx.moveTo(c1[0], c1[1]);
ctx.lineTo(c2[0], c2[1]);
ctx.lineWidth = 2;
ctx.strokeStyle = '#0f0';
ctx.stroke();
}
else{
ctx.beginPath();
var c1 = stpos(bl_last_check_i1);
var c2 = stpos(bl_last_check_i2);
ctx.arc(c1[0] + stw / 2, (c2[1] + c1[1]) / 2,
Math.abs(c2[1] - c1[1]) / 2, 0, Math.PI * 2);
ctx.lineWidth = 2;
ctx.strokeStyle = '#0f0';
ctx.stroke();
}
}
if (bl_phase < 4 && bl_last_div_seg){
ctx.beginPath();
ctx.arc(bl_last_div_seg.pt[0], bl_last_div_seg.pt[1], 6, 0, Math.PI * 2);
ctx.fillStyle = '#aa0';
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.stroke();
}
if (bl_phase < 4){
for (var i = 0; i < bl_status.length; i++){
var c = stpos(i);
labels.push({
txt: bl_status[i].id,
x: c[0],
y: c[1]
});
ctx.beginPath();
ctx.rect(c[0] - stw / 2, c[1] - sth / 2, stw, sth);
ctx.fillStyle = bl_last_status === bl_status[i].id ? '#ff6' : '#fff';
ctx.fill();
ctx.strokeStyle = bl_last_status === bl_status[i].id ? '#444' : '#aaa';
ctx.lineWidth = 1;
ctx.stroke();
}
}
if (bl_phase < 4 && bl_last_pop_seg){
var c = stpos(bl_last_pop_seg_i);
c[0] -= stw + 10;
labels.push({
txt: bl_last_pop_seg.seg.id,
x: c[0],
y: c[1]
});
ctx.beginPath();
ctx.rect(c[0] - stw / 2, c[1] - sth / 2, stw, sth);
ctx.fillStyle = '#eee';
ctx.fill();
ctx.strokeStyle = '#ddd'
ctx.lineWidth = 1;
ctx.stroke();
}
if (bl_phase < 4 && bl_last_temp_status){
var c = stpos(bl_last_temp_status_i);
c[0] += stw + 10;
labels.push({
txt: bl_last_temp_status.seg.id,
x: c[0],
y: c[1]
});
ctx.beginPath();
ctx.rect(c[0] - stw / 2, c[1] - sth / 2, stw, sth);
ctx.fillStyle = '#dfd';
ctx.fill();
ctx.strokeStyle = '#cfc'
ctx.lineWidth = 1;
ctx.stroke();
}
if (bl_phase < 4 && bl_last_chop){
ctx.beginPath();
ctx.moveTo(bl_last_chop.seg.start[0], bl_last_chop.seg.start[1]);
ctx.lineTo(bl_last_chop.pt[0], bl_last_chop.pt[1]);
ctx.lineWidth = 3;
ctx.strokeStyle = '#f00';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bl_last_chop.pt[0], bl_last_chop.pt[1]);
ctx.lineTo(bl_last_chop.seg.end[0], bl_last_chop.seg.end[1]);
ctx.lineWidth = 2;
ctx.strokeStyle = '#ddd';
ctx.stroke();
}
if (bl_phase < 4 && bl_vert !== false){
ctx.beginPath();
ctx.moveTo(bl_vert, cnvHeight / 2);
ctx.lineTo(bl_vert, 0);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
ctx.lineWidth = 1;
ctx.stroke();
}
if (bl_phase === 4){
bl_selected.forEach(function(seg){
ctx.beginPath();
ctx.moveTo(seg.start[0], seg.start[1]);
ctx.lineTo(seg.end[0], seg.end[1]);
ctx.strokeStyle = bl_last_done ? '#070' : '#e2e2e2';
ctx.lineWidth = 1;
ctx.stroke();
ctx.beginPath();
ctx.arc(seg.start[0], seg.start[1], 3, 0, Math.PI * 2);
ctx.fillStyle = bl_last_done ? '#070' : '#e2e2e2';
ctx.fill();
ctx.beginPath();
ctx.arc(seg.end[0], seg.end[1], 3, 0, Math.PI * 2);
ctx.fillStyle = bl_last_done ? '#070' : '#e2e2e2';
ctx.fill();
});
}
if (bl_phase === 4 || (bl_phase === 5 && bl_last_done)){
ctx.beginPath();
bl_oldchains.forEach(function(ch){
for (var i = 0; i < ch.length; i++){
var pt = ch[i];
if (i === 0)
ctx.moveTo(pt[0], pt[1]);
else
ctx.lineTo(pt[0], pt[1]);
}
ctx.lineTo(ch[0][0], ch[0][1]);
});
ctx.fillStyle = 'rgba(0, 255, 0, 0.2)';
ctx.fill('evenodd');
ctx.strokeStyle = '#7f7';
ctx.lineWidth = 1;
ctx.stroke();
bl_oldchains.forEach(function(ch, chi){
var totx = 0;
var toty = 0;
for (var i = 0; i < ch.length; i++){
var pt = ch[i];
totx += pt[0];
toty += pt[1];
ctx.beginPath();
ctx.arc(pt[0], pt[1], 3, 0, Math.PI * 2);
ctx.fillStyle = '#7f7';
ctx.fill();
}
labels.push({
txt: bl_oldchainids[chi],
x: totx / ch.length,
y: toty / ch.length
});
});
function drawarrow(pt1, pt2, rad){
var arrowLen = 8;
var arrowAng = Math.PI - 0.45;
ctx.beginPath();
ctx.moveTo(pt1[0], pt1[1]);
ctx.lineTo(pt2[0], pt2[1]);
var ang = Math.atan2(pt2[1] - pt1[1], pt2[0] - pt1[0]);
var ax = pt2[0] - Math.cos(ang) * rad;
var ay = pt2[1] - Math.sin(ang) * rad;
ctx.moveTo(ax, ay);
ctx.lineTo(
ax + Math.cos(ang + arrowAng) * arrowLen,
ay + Math.sin(ang + arrowAng) * arrowLen
);
ctx.moveTo(ax, ay);
ctx.lineTo(
ax + Math.cos(ang - arrowAng) * arrowLen,
ay + Math.sin(ang - arrowAng) * arrowLen
);
}
bl_chains.forEach(function(ch, chi){
var rad = 3;
for (var i = 0; i < ch.length - 1; i++){
var pt1 = ch[i];
var pt2 = ch[i + 1];
drawarrow(pt1, pt2, rad);
ctx.lineWidth = 1;
ctx.strokeStyle = '#070';
ctx.stroke();
ctx.beginPath();
ctx.arc(pt1[0], pt1[1], rad, 0, Math.PI * 2);
ctx.fillStyle = '#070';
ctx.fill();
ctx.beginPath();
ctx.arc(pt2[0], pt2[1], rad, 0, Math.PI * 2);
ctx.fillStyle = '#070';
ctx.fill();
labels.push({
txt: bl_chainids[chi],
x: (pt1[0] + pt2[0]) / 2,
y: (pt1[1] + pt2[1]) / 2
});
}
});
if (bl_last_seg_keep){
var pt1 = bl_last_seg_keep.seg.start;
var pt2 = bl_last_seg_keep.seg.end;
ctx.beginPath();
drawarrow(pt1, pt2, 3.5);
ctx.lineWidth = 2;
ctx.strokeStyle = '#0f0';
ctx.stroke();
ctx.beginPath();
ctx.arc(pt1[0], pt1[1], 3.5, 0, Math.PI * 2);
ctx.fillStyle = '#0f0';
ctx.fill();
ctx.beginPath();
ctx.arc(pt2[0], pt2[1], 3.5, 0, Math.PI * 2);
ctx.fillStyle = '#0f0';
ctx.fill();
if (bl_last_seg_keep_match !== false){
labels.push({
txt: bl_chainids[bl_last_seg_keep_match],
x: (pt1[0] + pt2[0]) / 2,
y: (pt1[1] + pt2[1]) / 2
});
}
}
}
// move labels around so that they don't overlap
for (var i = 1; i < labels.length; i++){
if (labels[i].txt === '')
continue;
for (var j = 0; j < i; j++){
if (labels[j].txt === '')
continue;
var dx = labels[i].x - labels[j].x;
var dy = labels[i].y - labels[j].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 5){
labels[j].txt += ', ' + labels[i].txt;
labels[i].txt = '';
}
else if (dist < 16){
labels[j].y -= 4;
labels[i].y += 12;
}
}
}
ctx.fillStyle = 'rgba(255, 0, 0, 0.2)';
polyFill(poly1, [0, 0], [cnvWidth, cnvHeight / 2], cnvHeight / 2);
ctx.fillStyle = 'rgba(0, 0, 255, 0.2)';
polyFill(poly2, [0, 0], [cnvWidth, cnvHeight / 2], cnvHeight / 2);
ctx.lineWidth = 1;
ctx.strokeStyle = '#f00';
polyStroke(poly1, cnvHeight / 2);
ctx.lineWidth = 1;
ctx.strokeStyle = '#00f';
polyStroke(poly2, cnvHeight / 2);
ctx.fillStyle = '#f00';
drawVerts(poly1, cnvHeight / 2);
ctx.fillStyle = '#00f';
drawVerts(poly2, cnvHeight / 2);
// draw labels
//*
ctx.save();
ctx.setTransform(rscale, 0, 0, rscale, 0, 0);
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
labels.forEach(function(lbl){
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.fillText(lbl.txt, lbl.x + 1, cnvHeight - lbl.y + 1);
ctx.fillText(lbl.txt, lbl.x + 1, cnvHeight - lbl.y + 0);
ctx.fillText(lbl.txt, lbl.x + 1, cnvHeight - lbl.y + -1);
ctx.fillText(lbl.txt, lbl.x + 0, cnvHeight - lbl.y + 1);
ctx.fillText(lbl.txt, lbl.x + 0, cnvHeight - lbl.y + -1);
ctx.fillText(lbl.txt, lbl.x + -1, cnvHeight - lbl.y + 1);
ctx.fillText(lbl.txt, lbl.x + -1, cnvHeight - lbl.y + 0);
ctx.fillText(lbl.txt, lbl.x + -1, cnvHeight - lbl.y + -1);
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillText(lbl.txt, lbl.x, cnvHeight - lbl.y);
});
ctx.restore(); // */
if (buildLogMax === 0){
ctx.fillStyle = 'rgba(0, 255, 0, 0.7)';
ctx.strokeStyle = '#070';
ctx.lineWidth = 1;
polyFill(clipResult.result, [0, 0], [cnvWidth, cnvHeight / 2], 0);
polyStroke(clipResult.result, 0);
ctx.fillStyle = '#070';
drawVerts(clipResult.result, 0);
}
ctx.save();
ctx.setTransform(rscale, 0, 0, rscale, 0, 0);
ctx.font = '13px sans-serif';
ctx.fillStyle = '#000';
ctx.fillText(mode, 4, cnvHeight / 2 + 16);
ctx.fillText(caseName, 4, 16);
ctx.restore();
var phase = buildLogMax === 0 ? 'Result' :
bl_phase === 0 ? 'Phase 1. Self-Intersect Red' :
bl_phase === 1 ? (bl_last_done ? 'Phase 1 Result' : 'Phase 2. Self-Intersect Blue') :
bl_phase === 2 ? (bl_last_done ? 'Phase 2 Result' : 'Phase 3. Red vs. Blue') :
bl_phase === 3 ? 'Phase 3 Result' :
bl_phase === 4 ? (bl_last_done ? 'Segment Selection' : 'Phase 4. Segment Chaining') :
'Phase 4 Result';
ctx.save();
ctx.setTransform(rscale, 0, 0, rscale, 0, 0);
ctx.font = '13px sans-serif';
ctx.textAlign = 'right';
ctx.fillStyle = '#000';
ctx.fillText(phase, cnvWidth - 4, cnvHeight / 2 + 16);
ctx.restore();
ctx.fillStyle = '#999';
ctx.fillRect(0, 0, cnvWidth * buildLogMax / clipResult.build_log.length, 3);
ctx.beginPath();
for (var x = 0; x < cnvWidth; x += 10){
ctx.moveTo(x, cnvHeight / 2);
ctx.lineTo(x + 5, cnvHeight / 2);
}
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
ctx.stroke();
}
function init(){
cnv = document.getElementById('cnv');
ctx = cnv.getContext('2d');
cnv.style.width = cnv.width / wscale + 'px';
cnv.style.height = cnv.height / wscale + 'px';
// make y go up and scale by 2 (for high DPI screens)
ctx.transform(rscale, 0, 0, -rscale, 0, cnv.height);
cnvWidth = cnv.width / rscale;
cnvHeight = cnv.height / rscale;
nextDemo(1);
function mousePos(e){
var rect = cnv.getBoundingClientRect();
return [
e.clientX - rect.left,
cnvHeight - e.clientY + rect.top
];
}
function closestPoint(poly, x, y){
x = unscaleX(x);
y = unscaleY(y);
var reg = false;
var vert = false;
var len2 = false;
poly.regions.forEach(function(region){
for (var i = 0; i < region.length; i++){
var dx = scaleX(region[i][0] - x);
var dy = scaleY(region[i][1] - y);
var thisLen2 = dx * dx + dy * dy;
if (len2 === false || thisLen2 < len2){
reg = region;
vert = i;
len2 = thisLen2;
}
}
});
return {
region: reg,
vert: vert,
len: Math.sqrt(len2)
};
}
function mouseTrackHover(mp){
// look for the closest node
var p1 = closestPoint(poly1, mp[0], mp[1] - cnvHeight / 2);
var p2 = closestPoint(poly2, mp[0], mp[1] - cnvHeight / 2);
if (p2.len < p1.len)
p1 = p2;
if (p1.len > 10){
if (hover !== false){
hover = false;
redraw();
}
return;
}
if (hover === false || hover.region !== p1.region || hover.vert !== p1.vert){
hover = p1;
redraw();
}
}
var dragging = false;
cnv.addEventListener('mousemove', function(e){
var mp = mousePos(e);
if (dragging){
var dx = mp[0] - dragging[0];
var dy = mp[1] - dragging[1];
var pt = hover.region[hover.vert];
if (pt[1] + dy < 0)
dy = -pt[1];
if (document.getElementById('snap').checked){
var tx = pt[0] + dx;
var ty = pt[1] + dy;
tx = Math.round(tx / 10) * 10;
ty = Math.round(ty / 10) * 10;
dx = tx - pt[0];
dy = ty - pt[1];
}
if (dx !== 0 || dy !== 0){
dragging = [dragging[0] + dx, dragging[1] + dy];
pt[0] = unscaleX(scaleX(pt[0]) + dx);
pt[1] = unscaleY(scaleY(pt[1]) + dy);
recalc();
}
}
else
mouseTrackHover(mp);
if (window.debugMousePos === true){
ctx.save();
var mx = mp[0], my = mp[1] - cnvHeight / 2;
if (hover !== false){
mx = hover.region[hover.vert][0];
my = hover.region[hover.vert][1];
}
ctx.setTransform(rscale, 0, 0, rscale, 0, 0);
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, 100, 20);
ctx.fillStyle = '#000';
ctx.textAlign = 'left'
ctx.textBaseline = 'top';
ctx.fillText('(' + mx + ', ' + my + ')', 0, 0);
ctx.restore();
}
});
cnv.addEventListener('mouseup', function(e){
var mp = mousePos(e);
if (dragging){
dragging = false;
mouseTrackHover(mp);
redraw();
}
else
mouseTrackHover(mp);
});
cnv.addEventListener('mouseleave', function(e){
if (dragging){
dragging = false;
hover = false;
redraw();
}
});
cnv.addEventListener('mousedown', function(e){
var mp = mousePos(e);
mouseTrackHover(mp);
if (hover !== false){
dragging = mp; // begin dragging
e.preventDefault();
}
});
document.addEventListener('keydown', function(e){
if (e.keyCode === 37){ // left
buildLogNextWrap(e.shiftKey ? -10 : -1);
e.preventDefault();
}
else if (e.keyCode === 39){ // right
buildLogNextWrap(e.shiftKey ? 10 : 1);
e.preventDefault();
}
else if (e.keyCode === 32){ // space
buildLogPlay();
e.preventDefault();
}
});
}
</script>
<p>
<canvas id="cnv" width="1400" height="1000"></canvas>
</p>
<p>
Drag the polygon nodes to change the shape. Click the buttons below and have fun!
</p>
<p>
Operation:
<button onclick="javascript: setMode('Intersect');">Intersect</button>
<button onclick="javascript: setMode('Union');">Union</button>
<button onclick="javascript: setMode('Red - Blue');">Red - Blue</button>
<button onclick="javascript: setMode('Blue - Red');">Blue - Red</button>
<button onclick="javascript: setMode('Xor');">Xor</button>
</p>
<p>
<input type="checkbox" id="snap" checked="checked" /><label for="snap"> Snap</label>
<button style="margin-left: 1em;" onclick="javascript: poly1.inverted = !poly1.inverted; recalc();">Invert Red</button>
<button onclick="javascript: poly2.inverted = !poly2.inverted; recalc();">Invert Blue</button>
<span style="margin-left: 1em;">Animation:</span>
<button onclick="javascript: buildLogNextWrap(-1);">Prev</button>
<button onclick="javascript: buildLogPlay();" id="bl_play">Play</button>
<button onclick="javascript: buildLogNextWrap(1);">Next</button>
<span style="margin-left: 1em;">Demo:</span>
<button onclick="javascript: nextDemo(-1);">Prev</button>
<button onclick="javascript: nextDemo(1);">Next</button>
</p>
<p>
<a href="https://tools.ietf.org/html/rfc7946">GeoJSON</a> of result (experimental):
</p>
<center><textarea id="geojson" rows="8" style="width: 650px;"></textarea></center>
<p>
Polygon Clipping Demo based somewhat on the F. Martinez et al. algorithm (2008)
</p>
<p>
Coded (painfully) by
<a href="https://twitter.com/voidqk">@voidqk</a> from
<a href="http://syntheti.cc">syntheti.cc</a> &ndash; MIT License
</p>
<p>
<a href="http://syntheti.cc/article/polygon-clipping-pt2/">Read the companion tutorial</a>
<span style="padding: 0 1em; opacity: 0.3;">|</span>
<a href="https://github.com/voidqk/polybooljs">Project home on GitHub</a>
</p>
</body>
</html>