Automatically adding some extra context (major cities and interstates) to a county map with MapZen vector tiles.
Downloads the tiles to cover the entire bounding box at a medium zoom level, stitches together the road segments, and filters down to major interstates (e.g. I-5, I-80) and large cities with some minimum spacing between them.
This would probably never be practical at all! Downloads a ton of unused data and the results for any random place aren't totally predictable.
Potential improvements: dynamic label placement, adding rivers
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
font: 13px Helvetica, Arial, sans-serif;
}
path {
stroke-linejoin: round;
fill: none;
stroke: #000;
}
#boundary {
stroke-width: 4px;
stroke: #ccc;
}
.counties path {
stroke-width: 1px;
stroke: #ccc;
fill: #fff;
}
.outer path {
stroke-width: 6px;
stroke: #ababab;
stroke-linecap: butt;
}
.inner path {
stroke-width: 4px;
stroke: #fff;
stroke-linecap: round;
}
circle {
fill: #444;
}
.clipped {
clip-path: url(#clip);
}
text {
fill: #444;
}
.shields text {
fill: #666;
font-size: 12px;
letter-spacing: 1px;
text-anchor: middle;
vertical-align: top;
font-weight: 500;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.14/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script>
var width = 960,
height = 500;
var projection = d3.geo.transverseMercator()
.rotate([106.25, -31])
.scale(4778)
.translate([467, 517]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var clipped = svg.append("g")
.attr("class","clipped");
d3.json("NM.topo.json",function(err,topo){
var state = topojson.feature(topo,d3.values(topo.objects)[0]),
outer = topojson.merge(topo,d3.values(topo.objects)[0].geometries),
inBounds = pipper(outer),
q = queue();
clipped.append("g")
.attr("class","counties")
.selectAll("path")
.data(state.features)
.enter()
.append("path")
.attr("d",path);
xyz(d3.geo.bounds(state),8).forEach(function(tile){
q.defer(d3.json,"https://vector.mapzen.com/osm/roads,places/" + tile.z + "/" + tile.x + "/" + tile.y + ".topojson");
});
q.awaitAll(function(err,tiles){
var roads = [],
cities = [];
tiles.forEach(function(tile){
topojson.feature(tile,tile.objects.roads).features.forEach(function(d){
d.properties.highway = getHighway(d.properties);
// Only get two-digit interstates
if (d.properties.highway && inBounds(d.geometry)) {
roads.push(d);
}
});
topojson.feature(tile,tile.objects.places).features.forEach(function(d){
if (d.properties.kind === "city" && inBounds(d.geometry)) {
cities.push(d);
}
});
cities.sort(function(a,b){
return a.properties.scalerank === b.properties.scalerank ?
b.properties.population - a.properties.population :
a.properties.scalerank - b.properties.scalerank;
});
});
// Prevent closely packed cities
cities = cities.reduce(function(arr,city){
if (arr.every(farFrom(city))) {
arr.push(city);
}
return arr;
},[]);
roads = d3.nest()
.key(function(d){ return d.properties.highway; })
.rollup(merge)
.entries(roads);
clipped.selectAll(".highways")
.data(["outer","inner"])
.enter()
.append("g")
.attr("class",function(d){
return d + " highways";
})
.selectAll("path")
.data(roads.map(function(d){
return d.values;
}))
.enter()
.append("path")
.attr("d",path);
var shields = clipped.selectAll("g.shields")
.data(Array.prototype.slice.call(document.querySelectorAll(".inner path")))
.enter()
.append("g")
.attr("class","shields")
.attr("transform",function(d){
var mid = d.getPointAtLength(d.getTotalLength() * 0.8);
return "translate(" + (mid.x - 15) + " " + (mid.y - 15) + ")";
});
shields.append("image")
.attr("xlink:href","shield.svg")
.attr("width",30)
.attr("height",30)
.append("text")
.text(function(d){
return d3.select(d).datum().properties.highway;
})
.attr("dx",15)
.attr("dy",21)
.each(function(){
this.parentNode.parentNode.appendChild(this);
});
var places = svg.append("g")
.attr("class","places")
.selectAll("g")
.data(cities)
.enter()
.append("g")
.attr("transform",function(d){
return "translate(" + projection(d.geometry.coordinates) + ")";
});
places.append("circle")
.attr("r",3);
places.append("text")
.attr("dy",-5)
.attr("dx",5)
.text(function(d){
return d.properties.name;
});
d3.select("g").append("clipPath")
.attr("id","clip")
.append("path")
.attr("id","boundary")
.datum(outer)
.attr("d",path);
clipped.append("use")
.attr("xlink:href","#boundary");
});
});
function getHighway(properties) {
if (!properties.ref || properties.kind !== "highway") {
return false;
}
// Filter on two-digit multiples of five and ten for major interstates
var match = properties.ref.match(/I[- ]?[0-9][05](?![0-9])/);
return match ? match[0].replace(/[^0-9]/g,""): null;
}
function xyz(bounds,z) {
var tiles = [],
tileBounds = bounds.map(pointToTile(z));
d3.range(tileBounds[0][0],tileBounds[1][0] + 1).forEach(function(x){
d3.range(tileBounds[1][1],tileBounds[0][1] + 1).forEach(function(y){
tiles.push({
x: x,
y: y,
z: z
});
});
});
return tiles;
}
// Modified from https://github.com/mapbox/tilebelt/blob/master/index.js
function pointToTile(z) {
return function(p){
var sin = Math.sin(p[1] * Math.PI / 180),
z2 = Math.pow(2, z),
x = z2 * (p[0] / 360 + 0.5),
y = z2 * (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI);
return [
Math.floor(x),
Math.floor(y)
];
};
}
function merge(features) {
var merged = {
type: "Feature",
properties: {
highway: features[0].properties.highway
},
geometry: {
type: "MultiLineString",
coordinates: []
}
};
features.forEach(function(feature){
var coords = feature.geometry.coordinates;
if (feature.geometry.type === "LineString") {
coords = [coords];
}
coords.forEach(function(lineString){
merged.geometry.coordinates.push(lineString);
});
});
return merged;
}
function pipper(geo) {
var vs = geo.coordinates[0][0];
return function(search) {
if (search.type === "Point") {
return pip(search.coordinates,vs);
}
if (search.type === "LineString") {
return search.coordinates.some(function(point){
return pip(point,vs);
});
}
return search.coordinates.some(function(ls){
return ls.some(function(point){
return pip(point,vs);
});
});
};
}
// https://github.com/substack/point-in-polygon/
function pip(point, vs){
var x = point[0], y = point[1];
var 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 farFrom(a) {
var pa = projection(a.geometry.coordinates);
return function(b){
var pb = projection(b.geometry.coordinates);
return Math.sqrt(Math.pow(pb[0] - pa[0],2) + Math.pow(pb[1] - pa[1],2)) > 50;
};
}
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.14/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js
https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js