Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active July 3, 2016 10:39
Show Gist options
  • Save veltman/399cc363de26fee868d6 to your computer and use it in GitHub Desktop.
Save veltman/399cc363de26fee868d6 to your computer and use it in GitHub Desktop.
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>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
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