Unrolling a polyline along an arbitrary zig-zag path by simplifying the line and then rotating based on each simplified segment.
See also: Line Simplification
Unrolling a polyline along an arbitrary zig-zag path by simplifying the line and then rotating based on each simplified segment.
See also: Line Simplification
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<style> | |
path { | |
fill: none; | |
stroke-width: 3px; | |
stroke-linejoin: round; | |
} | |
.state { | |
stroke: #777; | |
} | |
.simplified { | |
stroke: #f0f; | |
stroke-dasharray: 4,2; | |
} | |
</style> | |
</head> | |
<body> | |
<div></div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> | |
<script src="simplify.js"></script> | |
<script> | |
var width = 960, | |
height = 500; | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
var projection = d3.geo.conicConformal() | |
.parallels([38 + 18 / 60, 39 + 27 / 60]) | |
.rotate([77, -37 - 40 / 60]) | |
.scale(10185) | |
.translate([520, 500]); | |
var line = d3.svg.line(); | |
// Top point | |
var rotation = -Math.PI / 24; | |
d3.json("md.geojson",function(err,md){ | |
// Project to screen x/y | |
var border = md.features[0].geometry.coordinates[0].map(projection), | |
vertices = simplify(border, 250), | |
groups = getGroups(border, vertices); | |
var state = svg.append("path") | |
.attr("class", "state") | |
.datum(border) | |
.attr("d", line); | |
var simplified = svg.append("path") | |
.attr("class", "simplified"); | |
setTimeout(unroll, 500); | |
function unroll() { | |
var next = groups.shift(), | |
angle = shortest(getAngle(next.anchor, next.end) + rotation); | |
simplified.datum([next.anchor, next.end]) | |
.attr("d", line); | |
// Reset original points for tweening | |
border.forEach(function(p, i){ | |
p.original = i < next.start[2] ? null : p.slice(0,2); | |
}); | |
state.transition() | |
.delay(150) | |
.duration(300) | |
.attrTween("d",function(){ | |
// Update path with partial rotation | |
return function(t) { | |
var rotate = rotator(t * angle, next.anchor); | |
border.forEach(rotate); | |
simplified.attr("d", line); // Update simplified line too | |
return line(border); | |
}; | |
}) | |
.each("end",function(){ | |
// Zig zag | |
if (rotation > -Math.PI / 2 && next.end[0] >= 760) { | |
rotation = -Math.PI - rotation; | |
} else if (rotation < -Math.PI / 2 && next.end[0] <= 150) { | |
rotation = -Math.PI - rotation; | |
} | |
if (groups.length) { | |
unroll(); | |
} else { | |
simplified.remove(); | |
} | |
}); | |
} | |
}); | |
// Turn into groups of anchor point, first point to rotate, last point to rotate | |
function getGroups(border, vertices) { | |
return vertices.slice(1).map(function(v, i){ | |
return { | |
start: i ? border[vertices[i][2] + 1] : border[0], | |
end: v, | |
anchor: vertices[i] | |
}; | |
}); | |
} | |
// Return function ro modify point in place based on rotation from its original x/y | |
// (Using .original for tweening) | |
function rotator(angle, anchor) { | |
var cx = anchor[0], | |
cy = anchor[1], | |
cos = Math.cos(angle), | |
sin = Math.sin(angle); | |
return function(point, i) { | |
var x, | |
y, | |
x2, | |
y2; | |
if (point.original) { | |
x = point.original[0]; | |
y = point.original[1]; | |
x2 = (cos * (x - cx)) + (sin * (y - cy)) + cx; | |
y2 = (cos * (y - cy)) - (sin * (x - cx)) + cy; | |
point[0] = x2; | |
point[1] = y2; | |
} | |
}; | |
} | |
// Take the shorter angle | |
function shortest(angle) { | |
while (angle < -Math.PI) { | |
angle += 2 * Math.PI; | |
} | |
while (angle > Math.PI) { | |
angle -= 2 * Math.PI; | |
} | |
return angle; | |
} | |
function getAngle(a, b) { | |
return Math.atan2(b[1] - a[1], b[0] - a[0]); | |
} | |
</script> | |
</body> | |
</html> |
{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-79.476662,39.721078],[-79.478866,39.531689],[-79.482648,39.521364],[-79.486873,39.205961],[-79.43983,39.217074],[-79.387023,39.26554],[-79.253891,39.337222],[-79.129816,39.419901],[-79.095428,39.462548],[-79.033884,39.467761],[-78.955483,39.442277],[-78.953333,39.463645],[-78.874744,39.522611],[-78.826009,39.588829],[-78.795857,39.606934],[-78.689455,39.54577],[-78.623037,39.539512],[-78.565929,39.519444],[-78.462899,39.52084],[-78.426537,39.559155],[-78.454376,39.574319],[-78.420549,39.624021],[-78.351905,39.640486],[-78.266833,39.618818],[-78.254077,39.640089],[-78.182759,39.69511],[-78.107834,39.682137],[-78.035992,39.63572],[-78.011343,39.604083],[-77.93905,39.587139],[-77.885124,39.615775],[-77.838008,39.606125],[-77.842174,39.564333],[-77.886436,39.551947],[-77.795631,39.489623],[-77.78611,39.447197],[-77.75309,39.423262],[-77.755789,39.333899],[-77.719029,39.321125],[-77.675846,39.324192],[-77.615939,39.302722],[-77.566596,39.306121],[-77.543228,39.266937],[-77.486813,39.247586],[-77.45768,39.22502],[-77.527282,39.146236],[-77.462617,39.076248],[-77.340287,39.062991],[-77.244603,39.020109],[-77.249803,38.985909],[-77.221502,38.97131],[-77.146601,38.96421],[-77.1199,38.934311],[-77.040999,38.99511],[-76.909395,38.892812],[-77.038598,38.791513],[-77.042298,38.718515],[-77.1059,38.696815],[-77.132501,38.673816],[-77.1302,38.635017],[-77.148651,38.6056],[-77.183767,38.600699],[-77.237724,38.55187],[-77.27422,38.48177],[-77.250172,38.382781],[-77.207312,38.359867],[-77.091073,38.407546],[-77.040638,38.444618],[-77.016371,38.445572],[-76.98828,38.394975],[-76.975492,38.347327],[-76.929554,38.321088],[-76.920932,38.291568],[-76.864292,38.268945],[-76.802347,38.280743],[-76.797452,38.236918],[-76.673462,38.234401],[-76.590637,38.214212],[-76.552957,38.187209],[-76.54038,38.152991],[-76.481036,38.115873],[-76.439841,38.138933],[-76.421214,38.106039],[-76.392335,38.102896],[-76.361237,38.059542],[-76.319476,38.043315],[-76.338535,38.119472],[-76.320136,38.138339],[-76.385244,38.217751],[-76.39932,38.259284],[-76.374481,38.296348],[-76.402894,38.311402],[-76.388348,38.387781],[-76.492699,38.482849],[-76.515706,38.528988],[-76.511278,38.615745],[-76.532398,38.678363],[-76.526655,38.72443],[-76.559884,38.767361],[-76.510078,38.801216],[-76.489878,38.838715],[-76.519442,38.863135],[-76.49068,38.884814],[-76.49368,38.910013],[-76.45028,38.941113],[-76.474198,38.972647],[-76.422181,38.989011],[-76.39408,39.011311],[-76.438928,39.052788],[-76.42186,39.081442],[-76.428681,39.131709],[-76.534185,39.190608],[-76.488883,39.202208],[-76.442482,39.195408],[-76.38938,39.235408],[-76.402355,39.258315],[-76.36439,39.31184],[-76.327579,39.314108],[-76.341443,39.354217],[-76.295678,39.350008],[-76.291078,39.318108],[-76.180074,39.377609],[-76.102232,39.435659],[-76.060989,39.447722],[-76.071975,39.475047],[-76.117253,39.496068],[-76.096072,39.536912],[-75.970337,39.557637],[-75.966955,39.53865],[-75.99657,39.476658],[-75.976601,39.447808],[-76.035002,39.401994],[-76.049846,39.370644],[-76.110527,39.372257],[-76.145524,39.334399],[-76.170422,39.332094],[-76.185674,39.285367],[-76.219338,39.261997],[-76.278527,39.145764],[-76.252968,39.133626],[-76.231117,39.061017],[-76.183908,39.096344],[-76.145174,39.092824],[-76.184207,39.046264],[-76.163988,38.999542],[-76.20236,38.973079],[-76.277478,38.982492],[-76.302846,39.025828],[-76.361727,38.939175],[-76.336104,38.905977],[-76.273083,38.941927],[-76.203638,38.928382],[-76.202598,38.862616],[-76.19109,38.82966],[-76.219328,38.812371],[-76.265808,38.847512],[-76.300889,38.82619],[-76.334619,38.772911],[-76.316146,38.729586],[-76.275015,38.712714],[-76.255093,38.736476],[-76.200334,38.670774],[-76.175159,38.673236],[-76.147158,38.63684],[-76.212427,38.606738],[-76.23665,38.628598],[-76.279589,38.60952],[-76.290043,38.569158],[-76.2473,38.523818],[-76.263968,38.503452],[-76.33636,38.492235],[-76.28302,38.413512],[-76.258189,38.318373],[-76.211446,38.302656],[-76.111296,38.286946],[-76.09972,38.253647],[-76.044108,38.241682],[-76.027487,38.280108],[-76.05022,38.304101],[-76.016682,38.332429],[-76.002282,38.374477],[-75.973876,38.36585],[-75.964237,38.324285],[-76.007375,38.304318],[-75.963453,38.251793],[-75.938577,38.272329],[-75.90004,38.232133],[-75.870318,38.243432],[-75.846377,38.210477],[-75.942375,38.187066],[-75.959616,38.137141],[-75.900355,38.14115],[-75.837563,38.113753],[-75.880515,38.075011],[-75.812913,38.058932],[-75.875297,38.011965],[-75.898956,37.974514],[-75.898316,37.925114],[-75.860727,37.91831],[-75.783815,37.972594],[-75.737997,37.963526],[-75.71315,37.976623],[-75.669711,37.950796],[-75.624341,37.994211],[-75.242266,38.027209],[-75.193796,38.096013],[-75.102947,38.311525],[-75.085518,38.32427],[-75.048939,38.451263],[-75.428728,38.452671],[-75.693521,38.460128],[-75.725829,38.869296],[-75.755953,39.245958],[-75.788658,39.658211],[-75.788359,39.721811],[-76.418784,39.721204],[-76.990903,39.7198],[-77.534758,39.720134],[-78.073736,39.722314],[-78.537702,39.72249],[-79.045548,39.722883],[-79.476662,39.721078]]]}}]} |
// Rough Visvalingam simplification | |
// Better: https://bost.ocks.org/mike/simplify/ | |
function simplify(points, threshold) { | |
var heap = binaryHeap(function(a, b){ | |
return a.area < b.area; | |
}), | |
last = 0, | |
check; | |
points.forEach(function(point, i){ | |
point[2] = i; | |
point.prev = points[i - 1]; | |
point.next = points[i + 1]; | |
point.area = getArea(point.prev, point, point.next); | |
heap.insert(point); | |
}); | |
while (check = heap.pop()) { | |
check.area = last = Math.max(check.area, last); | |
if (check.prev) { | |
check.prev.next = check.next; | |
recalc(check.prev); | |
} | |
if (check.next) { | |
check.next.prev = check.prev; | |
recalc(check.next); | |
} | |
} | |
return points.filter(function(p){ | |
return p.area > threshold; | |
}); | |
function recalc(point) { | |
point.area = getArea(point.prev, point, point.next); | |
heap.update(point); | |
} | |
function getArea(a,b,c) { | |
if (!a || !c) { | |
return Infinity; | |
} | |
return Math.abs(a[0] * b[1] - a[0] * c[1] + b[0] * c[1] - b[0] * a[1] + c[0] * a[1] - c[0] * b[1]) / 2; | |
} | |
} | |
function binaryHeap(comparator) { | |
var heap = {}, | |
nodes = []; | |
heap.remove = function(val) { | |
var len = nodes.length, | |
end; | |
for (var i = 0; i < len; i++) { | |
if (nodes[i] === val) { | |
end = nodes.pop(); | |
if (i < len - 1) { | |
nodes[i] = end; | |
this.sink(i); | |
} | |
break; | |
} | |
} | |
return this; | |
}; | |
heap.pop = function() { | |
var top = nodes.shift(); | |
if (nodes.length) { | |
nodes.unshift(nodes.pop()); | |
this.sink(0); | |
} | |
return top; | |
}; | |
heap.bubble = function(i) { | |
var pi = Math.floor((i + 1) / 2) - 1; | |
if (i > 0 && this.compare(i, pi)) { | |
this.swap(i, pi); | |
this.bubble(pi); | |
} | |
return this; | |
}; | |
heap.sink = function(i) { | |
var len = nodes.length, | |
ci = 2 * i + 1; | |
if (ci < len - 1 && this.compare(ci + 1, ci)) { | |
ci++; | |
} | |
if (ci < len && this.compare(ci, i)) { | |
this.swap(i, ci); | |
this.sink(ci); | |
} | |
return this; | |
}; | |
heap.compare = function(i, j) { | |
return comparator(nodes[i], nodes[j]); | |
}; | |
heap.insert = function(d) { | |
this.bubble(nodes.push(d) - 1); | |
}; | |
heap.size = function() { | |
return nodes.length; | |
} | |
heap.swap = function(i, j) { | |
var swap = nodes[i]; | |
nodes[i] = nodes[j]; | |
nodes[j] = swap; | |
}; | |
heap.update = function(d) { | |
this.remove(d); | |
this.insert(d); | |
// bubble / sink instead? | |
} | |
heap.nodes = nodes; | |
return heap; | |
} |