Skip to content

Instantly share code, notes, and snippets.

@burritojustice
Last active May 28, 2019 07:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save burritojustice/5938a5bee73792f7db101d4be87f056c to your computer and use it in GitHub Desktop.
Save burritojustice/5938a5bee73792f7db101d4be87f056c to your computer and use it in GitHub Desktop.
SF strip map
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
path {
fill: none;
stroke-width: 2px;
stroke-linejoin: round;
}
text {
font: 14px Helvetica, Arial, sans-serif;
text-anchor: end;
}
.state {
stroke: #999;
stroke-width: 1px;
fill: papayawhip;
}
.simplified {
stroke: #de1e3d;
stroke-width: 2px;
stroke-dasharray: 8,8;
}
.zone {
stroke: #0eb8ba;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="warper.js"></script>
<script src="simplify.js"></script>
<script>
var stripWidth = 100;
var points = [
{ name: "Ft. Funston", coordinates: [-122.501464,37.709899] },
{ name: "SF Zoo", coordinates: [-122.506528,37.732643] },
{ name: "The Sunset", coordinates: [-122.509789,37.753140] },
{ name: "Golden Gate Park", coordinates: [-122.510648,37.765897] },
{ name: "The Richmond", coordinates: [-122.511506,37.774989] },
{ name: "Lands End", coordinates: [-122.506356,37.787471] },
{ name: "Sea Cliff", coordinates: [-122.487216,37.789574] },
{ name: "GGB", coordinates: [-122.477174,37.810733] },
{ name: "Crissy Field", coordinates: [-122.453570,37.806258] },
{ name: "Fort Mason", coordinates: [-122.430997,37.807207] },
{ name: "Fisherman's Wharf", coordinates: [-122.416406,37.809445] },
{ name: "Exploratorium", coordinates: [-122.398859,37.801036] },
{ name: "Ferry Building", coordinates: [-122.393135,37.795508] },
{ name: "Bay Bridge", coordinates: [-122.387545,37.789302] },
{ name: "AT&T Park", coordinates: [-122.387846,37.778449] },
{ name: "The Ramp", coordinates: [-122.386783,37.764557] },
{ name: "Pier 70", coordinates: [-122.381607,37.756457] },
{ name: "Islais Creek", coordinates: [-122.394562,37.748254] },
{ name: "Heron's Head Park", coordinates: [-122.374885,37.738515] },
{ name: "Hunters Point", coordinates: [-122.365497,37.727467] },
{ name: "Burrito Railgun", coordinates: [-122.361013,37.719982] },
{ name: "Candlestick Point", coordinates: [-122.380357,37.709356] }
];
var projection = d3.geo.conicConformal()
.parallels([37 + 4 / 60, 38 + 26 / 60])
.rotate([120 + 30 / 60, -36 - 30 / 60])
.scale(150000)
.translate([5300, 4400]);
var line = d3.svg.line();
// Top point
var origin = [50, 100];
d3.json("ca.geojson",function(err,ca){
// Preproject to screen coords
ca.coordinates[0] = ca.coordinates[0].map(projection);
points.forEach(function(point){
point.coordinates = projection(point.coordinates);
});
// Move the starting point 54 points earlier (Ft. Funston-ish)
windBackwards(ca.coordinates[0], 54);
// Get coastline (54 points longer than before)
var ls = ca.coordinates[0].slice(0, 1034);
// Get simplified vertices
var simplified = simplify(ls, 1500);
var zones = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 720)
.selectAll("g")
.data(getZones(simplified))
.enter()
.append("g");
zones.append("defs")
.append("clipPath")
.attr("id",function(d, i){
return "clip" + i;
})
.append("path");
var inner = zones.append("g")
.attr("class",function(d, i) {
return i ? "hidden" : null;
});
inner.append("path")
.attr("class", "state");
inner.append("line")
.attr("class", "simplified fade hidden");
// Put boundary outside so it isn't clipped
zones.append("path")
.attr("class", "zone fade hidden");
// Only put cities in zones they actually fall in
var cities = zones.selectAll(".city")
.data(function(d, i){
return points.filter(function(point){
if (pip(point.coordinates, d.boundary)) {
return point.zone = d;
}
});
})
.enter()
.append("g")
.attr("class", "city");
cities.append("circle")
.attr("r", 3);
cities.append("text")
.text(function(d){
return d.name;
})
.attr("dx", "-0.5em")
.attr("dy", "0.35em")
.attr("transform", function(d) {
return "rotate(-23)"
});
zones.call(update);
// Step-by-step for demo purposes
d3.select("body")
.transition()
.duration(2000)
.each("end", clipState)
.transition()
.each("end", showLine)
.transition()
.each("end", showZones)
.transition()
.each("end", move);
// 1. Clip out the rest of CA
function clipState() {
inner.classed("hidden", false)
.attr("clip-path",function(d, i){
return "url(#clip" + i + ")";
});
}
// 2. Show the simplified line
function showLine() {
inner.select(".simplified")
.classed("hidden", false);
}
// 3. Show the zone boundaries
function showZones() {
zones.select(".zone")
.classed("hidden", false);
}
// 4. Rotate/translate all the zones
function move() {
warpZones(zones.data());
// Flip text orientation
zones.transition()
.duration(2000)
.each("end",align)
.call(update);
}
// 5. Warp the zones to rectangles
function align(z) {
z.project = function(d){
return z.warp(z.translate(d));
};
z.boundary = z.corners;
d3.select(this)
.transition()
.duration(750)
.call(update)
.each("end",fade);
d3.selectAll("text").transition()
.duration(1000)
.each("end",function(){
d3.select(this).transition().duration(500).style("text-anchor", "left")
.attr("transform","rotate(-90)")
.attr("dx", "-1.0em")
.attr("dy", "0.28em");
});
}
// 6. Fade out
function fade() {
d3.select(this).selectAll(".fade")
.transition()
.duration(500)
.style("opacity", 0);
}
// Redraw
function update(sel) {
sel.select(".zone")
.attr("d",function(d){
return line(d.boundary.slice(0,4)) + "Z";
});
sel.select(".state")
.attr("d",function(d){
return d.path(ca);
});
sel.select(".simplified")
.attr("x1",function(d){
return d.ends[0][0];
})
.attr("x2",function(d){
return d.ends[1][0];
})
.attr("y1",function(d){
return d.ends[0][1];
})
.attr("y2",function(d){
return d.ends[1][1];
});
sel.select("clipPath path")
.attr("d",function(d){
return line(d.boundary.slice(0,4)) + "Z";
});
sel.selectAll(".city")
.attr("transform",function(d){
return "translate(" + d.zone.project(d.coordinates) + ")";
});
}
});
// Turn a simplified LineString into one group per segment
function getZones(simp) {
return simp.slice(1).map(function(p, i){
return {
boundary: getBoundary(simp[i - 1], simp[i], p, simp[i + 2]),
ends: [simp[i], p],
project: id,
path: d3.geo.path().projection(null)
};
});
}
function warpZones(zones) {
zones.forEach(function(z,i){
var angle = getAngle(z.ends[0], z.ends[1]),
anchor = i ? zones[i - 1].ends[1] : origin;
// Anchor points to end of prev segment
var translate = [
anchor[0] - z.ends[0][0],
anchor[1] - z.ends[0][1]
];
// Get translation/rotation function
z.translate = translateAndRotate(translate, z.ends[0], angle);
// Warp the boundary line and the simplified segment
z.ends = z.ends.map(z.translate);
z.boundary = z.boundary.map(z.translate);
var top = bisect(null, z.ends[0], z.ends[1]),
bottom = bisect(z.ends[0], z.ends[1], null);
z.corners = [top[0], top[1], bottom[1], bottom[0], top[0]];
z.corners.push(z.corners[0]);
// See: http://bl.ocks.org/veltman/8f5a157276b1dc18ce2fba1bc06dfb48
z.warp = warper(z.boundary, z.corners);
z.project = function(d){
return z.translate(d);
};
z.path.projection(d3.geo.transform({
point: function(x, y) {
var p = z.project([x, y]);
this.stream.point(p[0], p[1]);
}
}));
});
}
function getBoundary(prev, first, second, next) {
// if prev is undefined, top is perpendicular through first
// otherwise top bisects the prev-first-second angle
// if next is undefined, bottom is perpendicular through second
// otherwise bottom bisects the first-second-next angle
var top = bisect(prev, first, second),
bottom = bisect(first, second, next);
return [top[0], top[1], bottom[1], bottom[0], top[0]];
}
function getAngle(a, b) {
return Math.atan2(b[1] - a[1], b[0] - a[0]);
}
// Given an anchor point, initial translate, and angle rotation
// Return a function to translate+rotate a point
function translateAndRotate(translate, anchor, angle) {
var cos = Math.cos(angle),
sin = Math.sin(angle);
return function(point) {
return [
translate[0] + anchor[0] + ( cos * (point[0] - anchor[0]) + sin * (point[1] - anchor[1])),
translate[1] + anchor[1] + ( -sin * (point[0] - anchor[0]) + cos * (point[1] - anchor[1]))
];
};
}
// Hacky angle bisector
function bisect(start, vertex, end) {
var at,
bt,
adjusted,
right,
left;
if (start) {
at = getAngle(start, vertex);
}
if (end) {
bt = getAngle(vertex, end);
}
if (!start) {
at = bt;
}
if (!end) {
bt = at;
}
adjusted = bt - at;
if (adjusted <= -Math.PI) {
adjusted = 2 * Math.PI + adjusted;
} else if (adjusted > Math.PI) {
adjusted = adjusted - 2 * Math.PI;
}
right = (adjusted - Math.PI) / 2;
left = Math.PI + right;
left += at;
right += at;
return [
[vertex[0] + stripWidth * Math.cos(left) / 2, vertex[1] + stripWidth * Math.sin(left) / 2],
[vertex[0] + stripWidth * Math.cos(right) / 2, vertex[1] + stripWidth * Math.sin(right) / 2]
];
}
// https://github.com/substack/point-in-polygon
// based on http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
function pip(point, vs) {
var x = point[0],
y = point[1],
inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0], yi = vs[i][1];
var xj = vs[j][0], yj = vs[j][1];
var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
}
}
return inside;
}
function id(d) {
return d;
}
function windBackwards(arr, num) {
arr.pop();
for (var i = 0; i < num; i++) {
arr.unshift(arr.pop());
}
arr.push(arr[0]);
}
d3.select(self.frameElement).style("height", "720px");
</script>
</body>
</html>
// 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;
}
function warper(start,end) {
var u0 = start[0][0],
v0 = start[0][1],
u1 = start[1][0],
v1 = start[1][1],
u2 = start[2][0],
v2 = start[2][1],
u3 = start[3][0],
v3 = start[3][1],
x0 = end[0][0],
y0 = end[0][1],
x1 = end[1][0],
y1 = end[1][1],
x2 = end[2][0],
y2 = end[2][1],
x3 = end[3][0],
y3 = end[3][1];
var square = [
[1,u0,v0,u0 * v0,0,0,0,0],
[1,u1,v1,u1 * v1,0,0,0,0],
[1,u2,v2,u2 * v2,0,0,0,0],
[1,u3,v3,u3 * v3,0,0,0,0],
[0,0,0,0,1,u0,v0,u0 * v0],
[0,0,0,0,1,u1,v1,u1 * v1],
[0,0,0,0,1,u2,v2,u2 * v2],
[0,0,0,0,1,u3,v3,u3 * v3]
];
// Prevent float precision problems in FF/Safari
square.forEach(function(row){
row.forEach(function(cell, i){
row[i] = cell.toFixed(6);
});
});
var inverted = invert(square);
var s = multiply(inverted,[x0,x1,x2,x3,y0,y1,y2,y3]);
return function(p) {
return [
s[0] + s[1] * p[0] + s[2] * p[1] + s[3] * p[0] * p[1],
s[4] + s[5] * p[0] + s[6] * p[1] + s[7] * p[0] * p[1],
];
};
}
function multiply(matrix,vector) {
return matrix.map(function(row){
var sum = 0;
row.forEach(function(c,i){
sum += c * vector[i];
});
return sum;
});
}
function invert(matrix) {
var size = matrix.length,
base,
swap,
augmented;
// Augment w/ identity matrix
augmented = matrix.map(function(row,i){
return row.slice(0).concat(row.slice(0).map(function(d,j){
return j === i ? 1 : 0;
}));
});
// Process each row
for (var r = 0; r < size; r++) {
base = augmented[r][r];
// Zero on diagonal, swap with a lower row
if (!base) {
for (var rr = r + 1; rr < size; rr++) {
if (augmented[rr][r]) {
// swap
swap = augmented[rr];
augmented[rr] = augmented[r];
augmented[r] = swap;
base = augmented[r][r];
break;
}
}
if (!base) {
throw new Error("Not invertable :(");
}
}
// 1 on the diagonal
for (var c = 0; c < size * 2; c++) {
augmented[r][c] = augmented[r][c] / base;
}
// Zeroes elsewhere
for (var q = 0; q < size; q++) {
if (q !== r) {
base = augmented[q][r];
for (var p = 0; p < size * 2; p++) {
augmented[q][p] -= base * augmented[r][p];
}
}
}
}
return augmented.map(function(row){
return row.slice(size);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment