Create a gist now

Instantly share code, notes, and snippets.

Locator map + vector tiles

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

<!DOCTYPE html>
<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,"http://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>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment