index.html
<!DOCTYPE html>
<html>
<head>
<title>Topological line simplification segments</title>
<script src="//d3js.org/d3.v2.min.js?v2.10.3"></script>
<script src="simplify.js"></script>
<style type="text/css">
#container {
width: 800px;
margin: 1em auto;
}
#map {
height: 450px;
background: #f8f8f8;
margin: 1em auto;
}
#map svg {
border: 1px solid #999;
}
#area {
display: block;
width: 100%;
}
#states .state {
fill: #fff;
stroke: #ccc;
stroke-width: .25;
}
#states .edge path {
fill: none;
stroke-width: 1;
stroke-opacity: .2;
}
#states .circle {
fill: #000;
fill-opacity: .5;
stroke: none;
}
</style>
</head>
<body>
<div id="container">
<div id="map">
</div>
<input id="area" type="range" min="0" max="1000" step="10">
</div>
<script>
var map = d3.select("#map"),
width = map.property("offsetWidth"),
height = map.property("offsetHeight"),
minArea = location.search
? +location.search.substr(1) || 0
: 0,
range = d3.select("#area")
.property("value", minArea)
.on("change", function() {
minArea = +this.value;
update();
});
var zoom = d3.behavior.zoom()
.translate([-1841, -526])
.scale(3.5)
.scaleExtent([0.5, 4.0])
.on("zoom", updateZoom);
function updateZoom() {
var scale = zoom.scale();
g.attr("transform", "translate(" + zoom.translate() + ") scale(" + [scale, scale] + ")");
}
var svg = map.append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var g = svg.append("g")
.attr("id", "states");
updateZoom();
var proj = d3.geo.albers(),
linear = function(c) {
return [c[0], c[1]];
};
var path = d3.geo.path()
.projection(linear);
var simplify = d3.simplify()
.topology(true)
.projection(proj)
.area(minArea);
var collection;
d3.json("us-states.json", function(us) {
var stateNames = [
"Illinois",
"Indiana",
"Ohio",
"Kentucky"
];
us.features = us.features.filter(function(state) {
return stateNames.indexOf(state.properties.name) > -1;
});
collection = us;
update();
});
function update() {
var simplified = simplify
.area(minArea)
.project(copy(collection));
simplified = simplify(simplified);
var states = g.selectAll("path.state")
.data(simplified.features, function(d) {
return d.id;
});
states.enter()
.append("path")
.attr("class", "state")
.attr("id", function(d) {
return d.properties.name;
})
.append("title")
.text(function(d) {
return d.properties.name;
});
states.exit().remove();
states.attr("d", path);
var edges = getEdges(simplified.features, simplified.topology);
console.log("edges:", edges);
var color = d3.scale.linear()
.domain([0, edges.length - 1])
.range(["#00f", "#f00"]);
var eg = g.selectAll("g.edge")
.data(edges, function(d) {
return d.id;
});
var enter = eg.enter()
.append("g")
.attr("class", "edge")
.attr("id", function(d) {
return "edge" + d.id;
});
eg.exit().remove();
enter.append("path");
var dots = eg.selectAll("circle")
.data(function(d) {
return d.coords;
});
dots.enter()
.append("circle")
.attr("class", "point")
.attr("r", .5);
dots.exit().remove();
dots.attr("cx", function(d) {
return d[0];
})
.attr("cy", function(d) {
return d[1];
});
var line = d3.svg.line(),
lines = eg.select("path")
.attr("stroke", function(d, i) {
return color(i);
})
.attr("d", function(d) {
return line(d.coords);
});
}
function getEdges(features, topo) {
var edgesById = {};
console.log("topo:", topo);
features.forEach(function(feature) {
var rings = getRings(feature.geometry);
rings.forEach(function(ring) {
var lastEdge;
ring.forEach(function(coord, i) {
var coord = coord.slice(0, 2),
key = coord.join(","),
id = topo.idByPoint[key];
if (topo.sharedPoints[key] && lastEdge) {
lastEdge.coords.push(coord);
return;
}
var edge;
if (edgesById.hasOwnProperty(id)) {
edge = edgesById[id];
if (edge.features.indexOf(feature) === -1) {
edge.features.push(feature);
}
if (!edge.seen.hasOwnProperty(key)) {
edge.coords.push(coord);
edge.seen[key] = 1;
}
} else {
edge = edgesById[id] = {
features: [feature],
coords: [coord],
seen: {}
};
if (lastEdge && lastEdge.id === id) {
var prev = lastEdge.coords[lastEdge.coords.length - 1],
prevKey = prev.join(",");
if (!edge.seen.hasOwnProperty(prevKey)) {
edge.coords.unshift(prev);
edge.seen[prevKey] = 1;
}
}
edge.seen[key] = 1;
}
lastEdge = edge;
});
});
});
for (var id in edgesById) {
var edge = edgesById[id];
}
return d3.entries(edgesById)
.filter(function(entry) {
return true;
})
.map(function(entry) {
return {
id: entry.key,
features: entry.value.features,
coords: d3.values(entry.value.coords)
};
});
}
function getRings(geometry) {
var rings = [];
switch (geometry.type) {
case "Line":
case "LineString":
return geometry.coordinates;
case "Polygon":
geometry.coordinates.forEach(function(ring) {
rings.push(ring);
});
break;
case "MultiPolygon":
geometry.coordinates.forEach(function(poly) {
poly.forEach(function(ring) {
rings.push(ring);
});
});
break;
}
return rings;
}
function copy(d) {
return d instanceof Array
? d.map(copy)
: (typeof d === "number" || typeof d === "string")
? d
: copyObject(d);
}
function copyObject(d) {
var o = {};
for (var k in d) o[k] = copy(d[k]);
return o;
}
</script>
</body>
</html>
simplify.js
(function() {
d3.simplify = function() {
var projection = d3.geo.albers(),
triangulateLineString = triangulateLineStringSimple,
heap,
minArea = 3,
topology = false,
ringId,
id,
idByRings,
idByPoint,
ringsByPoint,
sharedPoints,
lastRingByPoint,
isShared,
graph;
var projectCoordinates = {
MultiPolygon: projectMultiPolygon,
Polygon: projectPolygon,
MultiLineString: projectPolygon,
LineString: projectLineString
};
var triangulateCoordinates = {
MultiPolygon: triangulateMultiPolygon,
Polygon: triangulatePolygon,
MultiLineString: triangulatePolygon,
LineString: triangulateLineString
};
var simplifyCoordinates = {
MultiPolygon: simplifyMultiPolygon,
Polygon: simplifyPolygon,
MultiLineString: simplifyPolygon,
LineString: simplifyLineString
};
function simplify(object) {
var type = object.type;
if (type === "FeatureCollection") {
object = copy(object);
object.features = object.features.map(simplifyFeature).filter(nonemptyFeature);
return object;
}
return (type === "Feature" ? simplifyFeature : simplifyGeometry)(object);
}
simplify.project = function(feature) {
var maxArea = 0,
maxAreas = {},
triangle;
heap = minHeap();
if (topology) {
id = 0;
idByRings = {};
ringsByPoint = {};
idByPoint = {};
sharedPoints = {};
lastRingByPoint = {};
isShared = {};
graph = {};
triangulateTopology(feature);
} else {
triangulateSimple(feature);
}
while (triangle = heap.pop()) {
if (triangle[1][2] < maxArea) triangle[1][2] = maxArea;
else maxArea = triangle[1][2];
if (triangle.previous) {
triangle.previous.next = triangle.next;
triangle.previous[2] = triangle[2];
update(triangle.previous);
} else if (topology) {
maxAreas[triangle.ring] = triangle[1][2];
} else {
triangle[0][2] = triangle[1][2];
}
if (triangle.next) {
triangle.next.previous = triangle.previous;
triangle.next[0] = triangle[0];
update(triangle.next);
} else if (topology) {
maxAreas[triangle.ring] = triangle[1][2];
} else {
triangle[2][2] = triangle[1][2];
}
}
function update(triangle) {
heap.remove(triangle);
triangle[1][2] = area(triangle);
heap.push(triangle);
}
if (topology) {
var seen = {},
max,
m,
c;
for (var key in graph) {
if (seen.hasOwnProperty(key)) continue;
max = 0;
for (var k in (c = components(graph, key))) if ((m = maxAreas[k]) > max) max = m;
for (var k in c) maxAreas[k] = max, seen[k] = 1;
}
for (var key in sharedPoints) {
maxArea = maxAreas[ringsByPoint[key][0]];
sharedPoints[key].forEach(function(point) { point[2] = maxArea; });
}
feature.topology = {
idByPoint: idByPoint,
idByRings: idByRings,
ringsByPoint: ringsByPoint,
sharedPoints: sharedPoints,
isShared: isShared,
lastRingByPoint: lastRingByPoint,
graph: graph
};
idByPoint = idByRings = ringsByPoint = sharedPoints = isShared = lastRingByPoint = graph = null;
}
heap = null;
return feature;
};
function components(graph, source) {
var seen = {},
nextLevel = {},
thisLevel,
empty;
nextLevel[source] = 1;
while (1) {
empty = true;
for (var k in nextLevel) empty = false;
if (empty) break;
thisLevel = nextLevel;
nextLevel = {};
for (var v in thisLevel) {
if (seen.hasOwnProperty(v)) continue;
seen[v] = 1;
var neighbors = graph[v];
for (var k in neighbors) nextLevel[k] = neighbors[k];
}
}
return seen;
}
function projectFeature(feature) {
projectGeometry(feature.geometry);
}
function projectGeometry(geometry) {
var type = geometry.type;
if (type === "GeometryCollection") geometry.geometries.forEach(projectGeometry);
else geometry.coordinates = projectCoordinates[type](geometry.coordinates);
}
function projectMultiPolygon(multiPolygon) {
return multiPolygon.map(projectPolygon);
}
function projectPolygon(polygon) {
return polygon.map(projectLineString);
}
function projectLineString(lineString) {
++ringId;
return lineString.map(projectPoint);
}
function projectPoint(point) {
var pointKey = (point = projection(point))[0] + "," + point[1],
key = (idByPoint.hasOwnProperty(pointKey) ? idByPoint[pointKey] + ":" : "") + ringId;
idByPoint[pointKey] = idByRings.hasOwnProperty(key)
? idByRings[key]
: idByRings[key] = ++id;
if (lastRingByPoint.hasOwnProperty(pointKey) && lastRingByPoint[pointKey] !== ringId) {
isShared[pointKey] = 1;
}
lastRingByPoint[pointKey] = ringId;
return point;
}
function triangulateLineStringTopology(lineString) {
++ringId;
var n = lineString.length - 1,
triangle0,
triangle,
a = lineString[0],
b = lineString[1],
c,
key0,
key,
idA = idByPoint[a[0] + "," + a[1]],
idB = idByPoint[key0 = b[0] + "," + b[1]],
idC;
lineString[0][2] = lineString[n][2] = 0;
if (n < 2) return lineString;
graph[ringId] = {};
addSharedPoint(a);
for (var i = 2; i <= n; ++i, a = b, b = c, idA = idB, idB = idC, key0 = key) {
c = lineString[i];
idC = idByPoint[key = c[0] + "," + c[1]];
if (idA === idB && idB === idC || !isShared.hasOwnProperty(key0)) {
triangle = [a, b, c];
triangle.ring = ringId;
b[2] = area(triangle);
heap.push(triangle);
if (triangle0) (triangle.previous = triangle0).next = triangle;
triangle0 = triangle;
} else {
addSharedPoint(b);
triangle0 = null;
}
}
addSharedPoint(b);
function addSharedPoint(point) {
var key = point[0] + "," + point[1],
rings = ringsByPoint.hasOwnProperty(key) ? ringsByPoint[key] : (ringsByPoint[key] = []);
rings.forEach(function(ring) {
graph[ring][ringId] = graph[ringId][ring] = 1;
});
rings.push(ringId);
if (sharedPoints.hasOwnProperty(key)) sharedPoints[key].push(point);
else sharedPoints[key] = [point];
}
return lineString;
}
function triangulateLineStringSimple(lineString) {
var points = lineString.map(projection),
n = points.length - 1,
triangle0,
triangle,
a = points[0],
b = points[1],
c;
points[0][2] = points[n][2] = 0;
for (var i = 2; i <= n; ++i) {
triangle = [a, b, c = points[i]];
b[2] = area(triangle);
heap.push(triangle);
if (triangle0) (triangle.previous = triangle0).next = triangle;
triangle0 = triangle;
a = b;
b = c;
}
return points;
}
function triangulateSimple(object) {
var type = object.type;
if (type === "FeatureCollection") object.features.forEach(triangulateFeature);
else (type === "Feature" ? triangulateFeature : triangulateGeometry)(object);
}
function triangulateTopology(object) {
var type = object.type;
ringId = 0;
if (type === "FeatureCollection") {
object.features.forEach(projectFeature);
ringId = 0;
object.features.forEach(triangulateFeature);
} else if (type === "Feature") {
projectFeature(object);
ringId = 0;
triangulateFeature(object);
} else {
projectGeometry(object);
ringId = 0;
triangulateGeometry(object);
}
}
function triangulateFeature(feature) {
triangulateGeometry(feature.geometry);
}
function triangulateGeometry(geometry) {
var type = geometry.type;
if (type === "GeometryCollection") geometry.geometries.forEach(triangulateGeometry);
else geometry.coordinates = triangulateCoordinates[type](geometry.coordinates);
}
function triangulateMultiPolygon(multiPolygon) {
return multiPolygon.map(triangulatePolygon);
}
function triangulatePolygon(polygon) {
return polygon.map(triangulateLineString);
}
function simplifyFeature(feature) {
feature = copy(feature);
feature.geometry = simplifyGeometry(feature.geometry);
return feature;
}
function simplifyGeometry(geometry) {
var type = geometry.type;
geometry = copy(geometry);
if (type === "GeometryCollection") {
geometry.geometries = geometry.geometries.map(simplifyGeometry).filter(nonemptyGeometry);
} else {
geometry.coordinates = simplifyCoordinates[type](geometry.coordinates);
}
return geometry;
}
function simplifyMultiPolygon(multiPolygon) {
return multiPolygon.map(simplifyPolygon).filter(length);
}
function simplifyPolygon(polygon) {
return polygon.map(simplifyLineString).filter(length);
}
function simplifyLineString(lineString) {
return lineString.filter(filterLineString);
}
function filterLineString(point) {
return point[2] >= minArea;
}
simplify.projection = function(_) {
if (!arguments.length) return projection;
projection = _;
return simplify;
};
simplify.area = function(_) {
if (!arguments.length) return minArea;
minArea = +_;
return simplify;
};
simplify.topology = function(_) {
if (!arguments.length) return topology;
triangulateCoordinates.LineString =
triangulateLineString = (topology = !!_)
? triangulateLineStringTopology
: triangulateLineStringSimple;
return simplify;
};
return simplify;
};
function compare(a, b) {
return a[1][2] - b[1][2] || a[1][1] - b[1][1] || a[1][0] - b[1][0];
}
function area(t) {
return Math.abs((t[0][0] - t[2][0]) * (t[1][1] - t[0][1]) - (t[0][0] - t[1][0]) * (t[2][1] - t[0][1]));
}
function minHeap() {
var heap = {},
array = [];
heap.push = function() {
for (var i = 0, n = arguments.length; i < n; ++i) {
var object = arguments[i];
up(object.index = array.push(object) - 1);
}
return array.length;
};
heap.pop = function() {
var removed = array[0],
object = array.pop();
if (array.length) {
array[object.index = 0] = object;
down(0);
}
return removed;
};
heap.remove = function(removed) {
var i = removed.index,
object = array.pop();
if (i !== array.length) {
array[object.index = i] = object;
(compare(object, removed) < 0 ? up : down)(i);
}
return i;
};
function up(i) {
var object = array[i];
while (i > 0) {
var up = ((i + 1) >> 1) - 1,
parent = array[up];
if (compare(object, parent) >= 0) break;
array[parent.index = i] = parent;
array[object.index = i = up] = object;
}
}
function down(i) {
var object = array[i];
while (true) {
var right = (i + 1) << 1,
left = right - 1,
down = i,
child = array[down];
if (left < array.length && compare(array[left], child) < 0) child = array[down = left];
if (right < array.length && compare(array[right], child) < 0) child = array[down = right];
if (down === i) break;
array[child.index = i] = child;
array[object.index = i = down] = object;
}
}
return heap;
}
function nonemptyFeature(d) { return nonemptyGeometry(d.geometry); }
function nonemptyGeometry(d) {
return length(d.type === "GeometryCollection"
? d.geometries : d.coordinates);
}
function length(d) { return d.length; }
function copy(object) {
var o = {};
for (var key in object) o[key] = object[key];
return o;
}
})();