|
<!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> |