Skip to content

Instantly share code, notes, and snippets.

@ThomasThoren
Last active January 19, 2018 16:52
Show Gist options
  • Save ThomasThoren/efc950f1d26356afc842 to your computer and use it in GitHub Desktop.
Save ThomasThoren/efc950f1d26356afc842 to your computer and use it in GitHub Desktop.
Texas map
height: 725

Some fun with Texas cities, counties and terrain.

It combines vector data and raster data to display the state's counties and terrain (shaded relief). The vector data comes from the U.S. Census. The raster imagery used for shaded relief is from NASA's Shuttle Radar Topography Mission. Use Derek Watkins' tool for selecting topographic map tiles to select the tiles you'll need.

Geographic data conversion is documented in the Makefile. It uses the GDAL library to convert the shapfiles to TopoJSON and reproject the raster images into the Mercator projection. You can run the entire processing script to see the intermediate files, or use the supplied JSON and PNG files for the final product.

TODO

  • Remove coastline.

Sources

<!DOCTYPE html>
<html>
<head>
<title>Texas terrain</title>
<meta charset="utf-8">
<style>
body {
padding: 0;
margin: 0;
font-family: helvetica, arial, sans-serif;
}
.bold {
font-weight: bold;
}
.raster {
fill: none;
opacity: 1;
}
.neutral-county {
fill: #FFF;
opacity: 0.4;
}
.highlighted-county {
fill: #C00;
opacity: 0.6;
}
.neutral-county,
.highlighted-county {
stroke: #6E6E6E;
stroke-opacity: 0.6;
stroke-width: 0.7px;
}
.counties {
fill: none;
stroke: #6E6E6E;
stroke-opacity: 0.4;
stroke-width: 0.5px;
}
.state-border {
fill: none;
stroke: #6E6E6E;
stroke-opacity: 0.7;
stroke-width: 1px;
}
.city-marker {
fill: none;
opacity: 0.6;
stroke-width: 2px;
stroke: #000;
}
.capital-marker {
fill: goldenrod;
opacity: 1;
stroke-width: 3px;
stroke: #000;
stroke-opacity: 0.6;
}
.text-note {
font-size: 15px;
font-weight: 500;
color: #000;
opacity: 0.6;
line-height: 18px;
margin: 0;
text-shadow: 1px 1px 0 white,
1px -1px 0 white,
-1px 1px 0 white,
-1px -1px 0 white;
}
.city-label {
text-anchor: middle;
margin: 0;
font-size: 15px;
line-height: 14px;
font-weight: 500;
text-align: right;
opacity: 0.6;
color: #000;
text-shadow: 1px 1px 0 white,
1px -1px 0 white,
-1px 1px 0 white,
-1px -1px 0 white;
}
.legend {
font-size: 15px;
line-height: 24px;
font-weight: 500;
color: #333;
}
.label-line {
stroke: #000;
stroke-width: 1.5px;
stroke-opacity: 1;
opacity: 0.8;
fill: none;
}
.state-label {
font-weight: 500;
text-transform: uppercase;
text-anchor: middle;
opacity: 0.3;
color: #000;
font-size: 24px;
line-height: 28px;
letter-spacing: 0.6em;
}
.distance-scale {
font-size: 11px;
line-height: 11px;
position: absolute;
font-weight: 500;
text-transform: uppercase;
color: #000;
}
.distance-scale-line {
stroke: #000;
stroke-width: 1;
stroke-opacity: 1;
opacity: 1;
fill: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<svg id="map"></svg>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var keeper_cities = [
'Abilene',
'Amarillo',
'Austin',
'Beaumont',
'Brownsville',
'Corpus Christi',
'Dallas',
'El Paso',
'Freeport',
'Ft. Worth',
'Galveston',
'Houston',
'Killeen',
'Laredo',
'Lubbock',
'Odessa',
'San Antonio',
'Tyler',
'Victoria',
'Waco',
'Wichita Falls'
];
var keeper_counties = [
'Lubbock'
];
// Make sure at least one dimension is smaller than raster image (705 x 670).
var map_width = 700,
map_height = 700;
var svg = d3.selectAll("#map")
.attr("width", map_width)
.attr("height", map_height);
// Create a unit projection
var map_projection = d3.geo.mercator()
.scale(1)
.translate([0, 0]);
var map_path = d3.geo.path()
.projection(map_projection);
queue()
.defer(d3.json, "texas-counties.json")
.defer(d3.json, "texas-cities.json")
.await(ready);
function ready(error, counties, cities) {
if (error) throw error;
// Scale and center the map to fit into the given dimensions.
var b = map_path.bounds(topojson.feature(counties, counties.objects['texas-counties']));
// Pixels per map-path-degree, for both directions.
// 0.95 is for padding. 1.0 would fill entire bounding box.
var s = 0.95 / Math.max((b[1][0] - b[0][0]) / map_width, (b[1][1] - b[0][1]) / map_height);
var t = [(map_width - s * (b[1][0] + b[0][0])) / 2.0, (map_height - s * (b[1][1] + b[0][1])) / 2.0];
// Scale and center vector
map_projection
.scale(s)
.translate(t);
// Scale and position shaded relief raster image. Assumes already cropped.
var raster_width = (b[1][0] - b[0][0]) * s;
var raster_height = (b[1][1] - b[0][1]) * s;
var rtranslate_x = (map_width - raster_width) / 2.0;
var rtranslate_y = (map_height - raster_height) / 2.0;
// Shaded relief
svg.append("image")
.attr('id', 'Raster')
.attr("clip-path", "url(#texas_clip)")
.attr("xlink:href", "texas-raster.png")
.attr("class", "raster")
.attr("width", raster_width)
.attr("height", raster_height)
.attr("transform", "translate(" + rtranslate_x + ", " + rtranslate_y + ")");
// Draw counties
svg.append("g")
.attr('id', 'Counties')
.selectAll("path")
.data(topojson.feature(counties, counties.objects['texas-counties']).features)
.enter().append("path")
.attr("class", function(d) {
if (keeper_counties.indexOf(d.properties.NAME) > -1) {
return 'highlighted-county';
} else {
return "neutral-county";
}
})
.attr("d", map_path);
// Draw state border
svg.append("g")
.attr('id', 'StateBorder')
.datum(topojson.mesh(counties, counties.objects['texas-counties'], function(a, b) { return a === b; }))
.append("path")
.attr("class", "state-border")
.attr("id", "texas_border") // For shaded relief
.attr("d", map_path);
// Draw city markers
svg.append('g')
.attr('id', 'CityMarkers')
.selectAll("circle")
.data(cities.features)
.enter().append("circle")
.attr('r', '4px')
.attr("class", function(d) {
if (d.properties.NAME === 'Austin') {
return 'capital-marker';
} else {
return "city-marker";
}
})
.attr("transform", function(d) { return "translate(" + map_projection(d.geometry.coordinates) + ")"; })
.filter(function(d) {
return keeper_cities.indexOf(d.properties.NAME) === -1;
}).remove();
// Write city label text
svg.append('g').attr('id', 'CityLabels').selectAll('.city-label')
.data(cities.features)
.enter().append('text')
.attr("class", "city-label")
.each(function(d) {
d3.select(this)
.attr("transform", function(d) {
var t = "translate(" + map_projection(d.geometry.coordinates) + ")";
if (d.properties.NAME === 'Dallas') {
t += "translate(0, -20)";
} else if (d.properties.NAME === 'Beaumont') {
t += "translate(-10, -20)";
} else if (d.properties.NAME === 'Ft. Worth' || d.properties.NAME === 'Killeen') {
t += "translate(-10, 0)";
}
return t;
})
.attr("dx", "5")
.attr("dy", "15")
.style("text-anchor", function(d) {
if (d.properties.NAME === 'Ft. Worth' || d.properties.NAME === 'Killeen' || d.properties.NAME === 'Beaumont') {
return "end";
} else {
return "start";
}
})
.text(function(d) { return d.properties.NAME; });
})
.filter(function(d) {
return keeper_cities.indexOf(d.properties.NAME) === -1;
}).remove();
// State label
svg.append("text")
.attr("class", "state-label")
.attr("x", map_width * 0.55)
.attr("y", map_height * 0.5)
.text('Texas');
// Line path generator
var line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("basis");
// Draw curve between county and text
svg.selectAll(".label-line")
.data(topojson.feature(counties, counties.objects['texas-counties']).features)
.enter().append('path')
.attr("class", "label-line")
.attr("d", function(d) {
var centroid = map_path.centroid(d);
if (d.properties.NAME === 'Lubbock') {
var lineData = [
{"x": centroid[0] - 50, "y": centroid[1] - 59},
{"x": centroid[0] - 2, "y": centroid[1] - 7}
];
} else {
return;
}
var x_diff = lineData[1].x - lineData[0].x;
var y_diff = lineData[1].y - lineData[0].y;
return line([
{"x": lineData[0].x, "y": lineData[0].y},
{"x": lineData[0].x + x_diff * 0.5, "y": lineData[0].y},
{"x": lineData[1].x, "y": lineData[0].y + y_diff * 0.5},
{"x": lineData[1].x, "y": lineData[1].y}
]);
})
.filter(function(d) {
return d.properties.NAME !== 'Lubbock';
}).remove();
// Draw line between city and text
svg.selectAll(".label-line")
.data(cities.features)
.enter().append('path')
.attr("class", "label-line")
.attr("d", function(d) {
var centroid = map_projection(d.geometry.coordinates);
if (d.properties.NAME === 'Austin') {
var lineData = [
{"x": centroid[0] + 3, "y": centroid[1] - 3},
{"x": centroid[0] + 30, "y": centroid[1] - 30}
];
} else {
return;
}
return line(lineData);
})
.filter(function(d) {
return d.properties.NAME !== 'Austin';
}).remove();
// Write counties text note
svg.selectAll('.text-note')
.data(topojson.feature(counties, counties.objects['texas-counties']).features)
.enter().append('text')
.attr("class", "text-note")
.each(function(d) {
if (d.properties.NAME === 'Lubbock') {
d3.select(this)
.attr("transform", function(d) { return "translate(" + map_path.centroid(d) + ")"; })
.append("tspan")
.attr("dx", "-55")
.attr("dy", "-55")
.style("text-anchor", "end")
.text("This is ");
d3.select(this)
.append("tspan")
.attr("class", "bold")
.text("Lubbock County.");
} else {
return;
}
})
.filter(function(d) {
return d.properties.NAME !== 'Lubbock';
}).remove();
// Write text for cities
svg.selectAll('.text-note')
.data(cities.features)
.enter().append('text')
.attr("class", "text-note")
.each(function(d) {
if (d.properties.NAME === 'Austin') {
d3.select(this)
.attr("transform", function(d) { return "translate(" + map_path.centroid(d) + ")"; })
.append("tspan")
.attr("dx", "32")
.attr("dy", "-32")
.style("text-anchor", "start")
.text("This is the ");
d3.select(this)
.append("tspan")
.attr("class", "bold")
.text("capital.");
} else {
return;
}
})
.filter(function(d) {
return d.properties.NAME !== 'Austin';
}).remove();
// Distance scale
function pixelLength(topojson, miles) {
// Calculates the window pixel length for a given map distance.
var actual_map_bounds = d3.geo.bounds(topojson);
var radians = d3.geo.distance(actual_map_bounds[0], actual_map_bounds[1]);
var earth_radius = 3959; // miles
var arc_length = radians * earth_radius; // s = r * theta
var projected_map_bounds = [
map_projection(actual_map_bounds[0]),
map_projection(actual_map_bounds[1])
];
var projected_width = projected_map_bounds[1][0] - projected_map_bounds[0][0];
var projected_height = projected_map_bounds[0][1] - projected_map_bounds[1][1];
var projected_map_hypotenuse = Math.sqrt(
(Math.pow(projected_width, 2)) + (Math.pow(projected_height, 2))
);
var pixels_per_mile = projected_map_hypotenuse / arc_length;
var pixel_distance = pixels_per_mile * miles;
return pixel_distance;
}
var pixels_for_hundred_miles = pixelLength(topojson.feature(counties, counties.objects['texas-counties']), 100);
var distance_scale = svg.selectAll("#distance-scale")
.data([pixels_for_hundred_miles])
.enter().append("g")
.attr("class", "distance-scale")
.attr("width", function(d) { return d; });
distance_scale.append('text')
.attr("x", function(d, i) { return map_width * 0.15; })
.attr("y", function(d, i) { return (map_height * 0.85) + (i * 20); })
.text("100 miles");
distance_scale.append('path')
.attr("class", "distance-scale-line")
.attr("d", function(d, i) {
var lineData = [
{"x": map_width * 0.15, "y": (map_height * 0.85) + (i * 20) + 3},
{"x": map_width * 0.15 + d, "y": (map_height * 0.85) + (i * 20) + 3}
];
return line(lineData);
});
}
</script>
</body>
</html>
# Requirements: gdal, topojson, imagemagick.
.PHONY: all clean
# Eventually, you will want to disable this so intermediate files are removed.
# Many of them are larger than 1 GB.
.SECONDARY:
# Download .zip files.
zip/tl_2015_us_county.zip:
@mkdir -p $(dir $@)
@curl -sS -o $@.download 'ftp://ftp2.census.gov/geo/tiger/TIGER2015/COUNTY/$(notdir $@)'
@mv $@.download $@
zip/ne_10m_populated_places.zip:
@mkdir -p $(dir $@)
@curl -sS -o $@.download 'http://naciscdn.org/naturalearth/10m/cultural/ne_10m_populated_places.zip'
@mv $@.download $@
zip/srtm_%.zip:
@# 90-meter SRTM tiles
@mkdir -p $(dir $@)
@curl -sS -o $@.download 'http://srtm.csi.cgiar.org/SRT-ZIP/SRTM_V41/SRTM_Data_GeoTiff/$(notdir $@)'
@mv $@.download $@
# Unzip
shp/tl_2015_us_county.shp: zip/tl_2015_us_county.zip
@mkdir -p $(dir $@)
@rm -rf tmp && mkdir tmp
@unzip -q -o -d tmp $<
@cp tmp/* $(dir $@)
@rm -rf tmp
shp/ne_10m_populated_places.shp: zip/ne_10m_populated_places.zip
@mkdir -p $(dir $@)
@rm -rf tmp && mkdir tmp
@unzip -q -o -d tmp $<
@cp tmp/* $(dir $@)
@rm -rf tmp
tif/srtm_%.tif: zip/srtm_%.zip
@mkdir -p $(dir $@)
@rm -rf tmp && mkdir tmp
@unzip -q -o -d tmp $<
@cp tmp/* $(dir $@)
@rm -rf tmp
# Extract Texas from U.S.
shp/texas-counties.shp: shp/tl_2015_us_county.shp
@mkdir -p $(dir $@)
@ogr2ogr \
-f 'ESRI Shapefile' \
-t_srs "EPSG:4326" \
$@ $< \
-dialect sqlite \
-sql "SELECT Geometry, STATEFP, COUNTYFP, NAME \
FROM tl_2015_us_county \
WHERE STATEFP = '48'"
# Reduce international cities to only include Texas.
shp/texas-cities.shp: shp/ne_10m_populated_places.shp
@mkdir -p $(dir $@)
@ogr2ogr \
-f 'ESRI Shapefile' \
$@ $< \
-dialect sqlite \
-sql "SELECT Geometry, ADM0NAME, ADM1NAME, NAME, SCALERANK, LABELRANK, NATSCALE \
FROM 'ne_10m_populated_places' \
WHERE ADM1NAME = 'Texas' AND \
SCALERANK <= 7"
# Convert SHP to GeoJSON.
geojson/texas-counties.json: shp/texas-counties.shp
@mkdir -p $(dir $@)
@ogr2ogr \
-f 'GeoJSON' \
$@ $<
texas-cities.json: shp/texas-cities.shp
@mkdir -p $(dir $@)
@ogr2ogr \
-f 'GeoJSON' \
$@ $<
# Convert GeoJSON to TopoJSON.
topojson/texas-counties.json: geojson/texas-counties.json
@mkdir -p $(dir $@)
@topojson \
--no-quantization \
--properties \
-o $@ \
-- $<
# Simplify TopoJSON.
texas-counties.json: topojson/texas-counties.json
@mkdir -p $(dir $@)
@topojson \
--properties \
--spherical \
-q 1e8 \
-s 1e-10 \
-o $@ \
-- $<
# Merge topographic tiles.
# Use http://dwtkns.com/srtm/ to find which tiles are needed.
tif/texas-merged-90m.tif: \
tif/srtm_15_05.tif \
tif/srtm_16_05.tif \
tif/srtm_17_05.tif \
tif/srtm_18_05.tif \
tif/srtm_15_06.tif \
tif/srtm_16_06.tif \
tif/srtm_17_06.tif \
tif/srtm_18_06.tif \
tif/srtm_15_07.tif \
tif/srtm_16_07.tif \
tif/srtm_17_07.tif \
tif/srtm_18_07.tif
@mkdir -p $(dir $@)
@gdal_merge.py \
-o $@ \
-init "255" \
tif/srtm_*.tif
# Convert to Mercator.
tif/texas-reprojected.tif: tif/texas-merged-90m.tif
@# Comes as WGS 84 (EPSG:4326). Want Mercator (EPSG:3857) for D3 projection.
@mkdir -p $(dir $@)
@gdalwarp \
-co "TFW=YES" \
-s_srs "EPSG:4326" \
-t_srs "EPSG:3857" \
$< \
$@
# Crop raster to shape of Texas.
tif/texas-cropped.tif: tif/texas-reprojected.tif shp/texas-counties.shp
@mkdir -p $(dir $@)
@gdalwarp \
-cutline shp/texas-counties.shp \
-crop_to_cutline \
-dstalpha \
tif/texas-reprojected.tif \
$@
# Shade and color.
tif/texas-color-crop.tif: tif/texas-cropped.tif
@rm -rf tmp && mkdir -p tmp
@gdaldem \
hillshade \
$< tmp/hillshade.tmp.tif \
-z 5 \
-az 315 \
-alt 60 \
-compute_edges
@gdal_calc.py \
-A tmp/hillshade.tmp.tif \
--outfile=$@ \
--calc="255*(A>220) + A*(A<=220)"
@gdal_calc.py \
-A tmp/hillshade.tmp.tif \
--outfile=tmp/opacity_crop.tmp.tif \
--calc="1*(A>220) + (256-A)*(A<=220)"
@rm -rf tmp
# Convert to .png.
texas-raster.png: tif/texas-color-crop.tif
@convert \
-resize x670 \
$< $@
all: texas-counties.json \
texas-cities.json \
texas-raster.png
clean:
@rm -rf geojson
@rm -rf hgt
@rm -rf shp
@rm -rf tif
@rm -rf topojson
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