block by armollica beebf49a0d7df1303d7e63b709ecdfcc

Denali Elevation Profiles

Full Screen

Click the buttons to see different elevation profiles of Denali.

Elevation data are from the Shuttle Radar Topography Mission and were collected using Derek Watkins’s SRTM Tile Grabber. The create-profiles.js Node.js script shows how the profiles were created from the elevation data. Data on the peaks came from peakbagger.com. The shaded relief color palette came from here. By the way, a realistic coloring of the mountain would be almost completely white. I wanted to highlight the elevation change so I chose this color ramp.

index.html

<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700" rel="stylesheet">
<style>

html {
	font-family: 'Source Sans Pro', sans-serif;
}

.hidden {
	display: none;
}

.border {
	fill: none;
	stroke: #000;
}

.peak circle {
	fill: tomato;
	stroke: tomato;
	fill-opacity: 0.25;
	stroke-opacity: 0.5;
}

.peak text {
	font-size: 12px;
	fill: black;
	text-shadow: -1px 0 1px #fff, 
                0 1px 1px #fff,
                1px 0 1px #fff, 
                0 -1px 1px #fff;
}

.overhead-profile {
	stroke: #8A0707;
	stroke-opacity: 0.5;
}

.profile .area {
	fill: #ddd;
}

.profile .line {
	fill: none;
	stroke: #8A0707;
	stroke-opacity: 0.5;
}

.axis--y path {
	stroke: none;
}

.axis--y .tick line {
	stroke: white;
	stroke-width: 1.5px;
	stroke-opacity: 0.2;
}

.profile-selector > button {
	width: 115px;
	display: block;
	font-size: 10px;
}

.south-peak-label > circle {
	fill: tomato;
	stroke: tomato;
	fill-opacity: 0.25;
	stroke-opacity: 0.5;
}

.south-peak-label > text {
	font-size: 12px;
}

</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="proj4.js"></script>
<script>

var wkt = 'PROJCS["Albers Conical Equal Area",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.2572221010042,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4269"]],PROJECTION["Albers_Conic_Equal_Area"],PARAMETER["standard_parallel_1",55],PARAMETER["standard_parallel_2",65],PARAMETER["latitude_of_center",50],PARAMETER["longitude_of_center",-154],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]]]';

var matrix = customProjection()
	.projection(wkt);

var path = d3.geoPath()
	.projection(matrix());

var container = d3.select("body").append("div")
	.attr("class", "container")
	.attr("width", 960)
	.attr("height", 500);

var profileSelector = container.append("div")
	.attr("class", "profile-selector")
	.style("position", "absolute")
	.style("left", "500px")
	.style("top", "35px");

var margin = { top: 30, left: 30, bottom: 30, right: 30 },
		mapWidth = 460 - margin.left - margin.right,
		mapHeight = 460 - margin.bottom - margin.top,
		chartWidth = 460 - margin.left - margin.right,
		chartHeight = 250 - margin.bottom - margin.top;

var mapSvg = container.append("svg")
		.attr("width", mapWidth + margin.left + margin.right)
		.attr("height", mapHeight + margin.bottom + margin.top)
	.append("g")
		.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var chartSvg = container.append("svg")
		.attr("width", chartWidth + margin.left + margin.right)
		.attr("height", chartHeight + margin.bottom + margin.top)
	.append("g")
		.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var x = d3.scaleLinear()
	.range([0, chartWidth]);

var y = d3.scaleLinear()
	.domain([0, 7000])
	.range([chartHeight, 0]);

var area = d3.area()
		.x(function(d) { return x(d.distance); })
		.y0(y(0))
		.y1(function(d) { return y(d.elevation); });
	
var line = d3.line()
	.x(function(d) { return x(d.distance); })
	.y(function(d) { return y(d.elevation); });

d3.queue()
	.defer(d3.json, "denali-peaks.json")
	.defer(d3.json, "denali-profiles.json")
	.defer(d3.json, "ring.json")
	.await(ready);

function ready(error, peaks, profiles, ring) {
	if (error) throw error;

	//____________________________________________________________________________
	// Fit map projection to screen

	var b = path.bounds(ring),
      s = .95 / Math.max((b[1][0] - b[0][0]) / mapWidth, (b[1][1] - b[0][1]) / mapHeight),
      t = [(mapWidth - s * (b[1][0] + b[0][0])) / 2, (mapHeight - s * (b[1][1] + b[0][1])) / 2];
	
	path.projection(matrix(s, 0, 0, -s, t[0], t[1]));

	var point = matrix.point(s, 0, 0, -s, t[0], t[1]);
	
	// Adjust raster size and translation
	var rWidth = (b[1][0] - b[0][0]) * s,
			rHeight = (b[1][1] - b[0][1]) * s,
			rTranslateX = (mapWidth - rWidth) / 2,
			rTranslateY = (mapHeight - rHeight) / 2;
	
	//____________________________________________________________________________
	// Draw relief map

	// From Thomas Thoren's example:
	// https://bl.ocks.org/ThomasThoren/550b2ce8b1e2470e75b2

	mapSvg.append("image")
		.attr("xlink:href", "relief.png")
		.attr("class", "raster")
		.attr("width", rWidth)
		.attr("height", rHeight)
		.attr("transform", "translate(" + rTranslateX + "," + rTranslateY + ")");
	
	// ...and the border around it
	mapSvg.append("path").datum(ring)
		.attr("class", "border")
		.attr("d", path);
	
	//____________________________________________________________________________
	// Draw peaks

	var gPeaks = mapSvg.append("g").attr("class", "peaks")
			.selectAll(".peak").data([peaks.peak].concat(peaks.subpeaks))
		.enter().append("g")
			.attr("class", "peak")
			.attr("transform", function(peak) {
				var p = point(peak.lat, peak.lon);
				return "translate(" + p[0] + "," + p[1] + ")";
			});
	
	gPeaks.append("circle")
		.attr("r", 3);
	
	gPeaks.append("text")
		.each(function(peak) {
			d3.select(this).call(orientLabel, peak.labelOrientation || "NE");
		})
		.text(function(peak) { return peak.name; });
	
	//____________________________________________________________________________
	// Draw overhead profile lines on map

	var profileIndex = 0;

	var profileLines = mapSvg.append("g").attr("class", "overhead-profiles")
			.selectAll(".overhead-profile").data(profiles.features)
		.enter().append("path")
			.attr("class", "overhead-profile")
			.attr("d", path)
			.classed("hidden", function(d, i) { return i != profileIndex; });
		
	//____________________________________________________________________________
	// Draw profile as a line chart

	var elevations = profiles.features.map(function(feature) {
		return feature.properties.elevations;
	});

	var elevation = elevations[profileIndex];

	x.domain(d3.extent(elevation, function(d) { return d.distance; }));

	var profile = chartSvg.append("g").attr("class", "profiles")
			.selectAll(".profile").data([elevation])
		.enter().append("g")
			.attr("class", "profile");
	
	profile.append("path").attr("class", "area")
		.attr("d", area);
	
	profile.append("path").attr("class", "line")
		.attr("d", line);
	
	chartSvg.append("g")
		.attr("class", "axis axis--x")
		.attr("transform", "translate(0," + chartHeight + ")")
		.call(d3.axisBottom(x));
	
	var yAxis = chartSvg.append("g")
		.attr("class", "axis axis--y")
		.call(d3.axisLeft(y).tickFormat(d3.format(".2s")))
	
	yAxis.selectAll(".tick line")
		.attr("x1", chartWidth)
	
	yAxis.selectAll(".tick text")
		.attr("dx", "0.5em");
	
	// Y-axis label
	yAxis.append("text")
		.attr("transform", "rotate(-90)")
		.attr("dx", "0.33em")
		.attr("dy", ".66em")
		.style("fill", "#000")
		.text("meters");
	
	// South Peak Label
	var southPeakLabel = chartSvg.append("g")
		.attr("class", "south-peak-label")
		.attr("transform", "translate(200, 23)");
	
	southPeakLabel.append("circle")
		.attr("r", 4);
	
	southPeakLabel.append("text")
		.attr("dx", "0.33em")
		.attr("dy", "-0.66em")
		.text("South Peak (6190 m)");

	//____________________________________________________________________________
	// Add content to profile selector

	profileSelector.selectAll("button").data(peaks.subpeaks)
		.enter().append("button")
			.html(function(d) { return d.name; })
			.on("click", function(d, i) { update(i); });

	function update(index) {

		profileIndex = index;

		// Update overhead profile lines
		profileLines
			.classed("hidden", function(d, i) { return i != profileIndex; });

		// Update the elevation line chart
		elevation = elevations[profileIndex];

		profile.data([elevation]);

		profile.select(".line").attr("d", line);
		profile.select(".area").attr("d", area);
	}
}

// Orient text label relative to it's current position given a cardinal or
// intermediate direction (i.e., N, S, E, W, NE, SE, SW, NW)
function orientLabel(selection, orientation) {
	var dx, dy, textAnchor;
	
	// Determine `dx` (x-offset) and the `text-anchor` 
	if (orientation === "N" || orientation == "S") {
		dx = 0; 
		textAnchor = "middle";
	}
	if (contains(orientation, "E")) {
		dx = ".33em";
		textAnchor = "start";
	}
	if (contains(orientation, "W")) {
		dx = "-.33em";
		textAnchor = "end";
	}

	// Determine `dy` (y-offset)
	dy = contains(orientation, "N") ? "-.33em" :
			 contains(orientation, "S") ? "1em" : 0;
	
	return selection
		.attr("dx", dx)
		.attr("dy", dy)
		.style("text-anchor", textAnchor);

	function contains(str, x) { return str.indexOf(x) !== -1; }
}


// Create custom projection using Proj4.js 
function customProjection() {

	var projection = function(d) { return d; };

	// TODO: Think about moving these matrix parameters into 
	//       setter-functions like projection.translate() and
	//       projection.scale().

	function matrix(a, b, c, d, tx, ty) {
		if (!arguments.length) {
			a = 1; b = 0; c = 0; d = -1; tx = 0; ty = 0;
		}

		return d3.geoTransform({
			point: function(x, y) {
				var p = projection.forward([x, y]);
				this.stream.point(a * p[0] + b * p[1] + tx, 
													c * p[0] + d * p[1] + ty)
			}
		});
	}

	matrix.projection = function(_) {
		if (!arguments.length) return projection;

		// Pass a proj or wkt string defining a projection. This will convert from
		// WGS84 to the specified projection.
		if (typeof _ === "string") {
			projection = proj4(_);
		}

		// Pass a pair of proj or wkt strings defining the source and destination 
		// projections. First element is the source, second is the destination.
		if (_ instanceof Array) {
			if (_.length !== 2) { 
				throw new Error("Array passed to customProjection.projection() must " +
												"be of length 2: [srcProj, dstProj]");
			}
			projection = proj4(_[0], _[1]);
		}

		// Pass a Proj4.js function directly
		if (typeof _ === "function") {
			projection = _;
		}

		return matrix;
	};

	// Point transformation function
	matrix.point = function(a, b, c, d, tx, ty) {
		return function(x, y) {
			var p = projection.forward([x, y]);
			return [a * p[0] + b * p[1] + tx, c * p[0] + d * p[1] + ty];
		};
	};

	return matrix;
}

</script>
</body>
</html>

create-profiles.js

var fs = require("fs"),
		gdal = require("gdal"),
		d3 = require("d3");

// Meters from peak
var radius = 7900;

// How many points should be sampled to create the profile?
var granularity = 500;

var srs_WGS84 = gdal.SpatialReference.fromEPSG(4326);

// DEM dataset
var dataset = gdal.open("dem/merged/merged.tif"),
		band = dataset.bands.get(1);

// Objects used to transform between coordinate systems
var transformToProjection = new gdal.CoordinateTransformation(srs_WGS84, dataset.srs);
var transformToPixel = new gdal.CoordinateTransformation(dataset.srs, dataset);
var transformToWGS84 = new gdal.CoordinateTransformation(dataset.srs, srs_WGS84);

// Peaks of Denali
var denali = JSON.parse(fs.readFileSync("denali-peaks.json"));

var profiles = denali.subpeaks.map(function(subpeak) {

	// Vector for peak and subpeak
	var v0 = toVector(transformToProjection, denali.peak.lat, denali.peak.lon),
			v1 = toVector(transformToProjection, subpeak.lat, subpeak.lon);

	// Unit vector pointing from peak to subpeak 
	var u = toUnit(subtract(v0, v1));  

	// Extend this line out to length `radius` on either side, center at peak
	var l0 = add(multiply(u, radius), v0),
			l1 = add(multiply(u, -radius), v0);
	
	// Get elevations along line connecting these two points
	var elevations = d3.ticks(0, 1, granularity)
		.map(function(t) {
			var l = lerp(l0, l1, t);

			var p = toVector(transformToPixel, l[0], l[1])
				.map(Math.floor);

			var elevation = band.pixels
				.get(p[0], p[1]);

			var v = toVector(transformToWGS84, l[0], l[1]);

			return {
				lat: v[0],
				lon: v[1],
				elevation: elevation
			};
		});

	return {
		subpeak: subpeak.name,
		elevations: elevations
	};
});

// Get distance between "ticks"
var d0 = profiles[0].elevations[0],
		d1 = profiles[0].elevations[1]

var p0 = new gdal.Point(d0.lat, d0.lon),
		p1 = new gdal.Point(d1.lat, d1.lon);

p0.transform(transformToProjection);
p1.transform(transformToProjection);

var tickDistance = p0.distance(p1);

profilesFeatures = profiles.map(function(profile) {

	var properties = {
		subpeak: profile.subpeak,
		elevations: profile.elevations.map(function(d, i) {
			d.distance = i * tickDistance;
			return d;
		})
	};

	var d0 = profile.elevations[0],
			d1 = profile.elevations[profile.elevations.length - 1];

	var geometry = {
		type: "LineString",
		coordinates: [[d0.lat, d0.lon], [d1.lat, d1.lon]]
	};

	return {
		type: "Feature",
		properties: properties,
		geometry: geometry
	};
});

var profilesFeatureCollection = {
	type: "FeatureCollection",
	features: profilesFeatures
}

fs.writeFileSync("denali-profiles.json", 
								 JSON.stringify(profilesFeatureCollection));

// Convert (lat, lon) to vector using coordinate system transformation
function toVector(transform, lat, lon) {
	var d = transform.transformPoint(lat, lon);
	return [d.x, d.y];
}

// Multiply a vector with a constant
function multiply(v, c) {
	return v.map(function(d) {
		return d * c;
	});
}

// Add two vectors together
function add(v0, v1) {
	return v0.map(function(d0, i) {
		var d1 = v1[i];
		return d0 + d1;
	});
}

// Subtract two vectors together
function subtract(v0, v1) {
	return v0.map(function(d0, i) {
		var d1 = v1[i];
		return d0 - d1;
	});
}

// Convert vector to unit vector
function toUnit(v) {
	var length = norm(v);
	return v.map(function(d) { return d / length; });
}

// Get the length (i.e., norm) of a vector
function norm(v) {
	return Math.sqrt(
		v.reduce(function(total, d) {
			return total + Math.pow(d, 2)
		}, 0)
	);
}

// Linearly interpolate between two vectors
function lerp(v0, v1, t) {
	return add(multiply(v0, 1 - t), multiply(v1, t));
}

denali-peaks.json

{
	"peak": {
		"name": "South Peak",
		"lat": -151.0074, 
		"lon": 63.0695 
	},
	"subpeaks": [
		{
			"name": "North Peak",
			"lat": -151.006322,
			"lon": 63.097617
		},
		{
			"name": "Archdeacons Tower",
			"lat": -151.020664,
			"lon": 63.073338,
			"labelOrientation": "SW"
		},
		{
			"name": "Peak 18735",
			"lat": -151.04142,
			"lon": 63.097854,
			"labelOrientation": "NW"
		},
		{
			"name": "Peak 17400",
			"lat": -150.954406,
			"lon": 63.086656
		},
		{
			"name": "West Buttress",
			"lat": -151.093556,
			"lon": 63.076935
		},
		{
			"name": "South Buttress",
			"lat": -150.976762,
			"lon": 63.034893
		},
		{
			"name": "East Buttress",
			"lat": -150.929597,
			"lon": 63.060288
		},
		{
			"name": "Browne Tower",
			"lat": -150.931684,
			"lon": 63.102118,
			"labelOrientation": "N"
		},
		{
			"name": "Southeast Spur",
			"lat": -150.918443,
			"lon": 63.023001,
			"labelOrientation": "N"
		}
	]
}

ring.json

{ "type": "Polygon", "coordinates": [ [ [ -150.85089444654758, 63.066232398456513 ], [ -150.85149748052817, 63.062541470070144 ], [ -150.8525275324119, 63.058869846642921 ], [ -150.85398162516097, 63.055227582677986 ], [ -150.85585562452084, 63.051624648548703 ], [ -150.85814425197418, 63.048070903313032 ], [ -150.86084110075603, 63.044576067871738 ], [ -150.86393865486994, 63.04114969854129 ], [ -150.86742831103729, 63.037801161111368 ], [ -150.87130040350479, 63.034539605456523 ], [ -150.87554423162956, 63.0313739407664 ], [ -150.88014809015388, 63.028312811461213 ], [ -150.88509930207726, 63.025364573853061 ], [ -150.89038425402705, 63.022537273614418 ], [ -150.89598843402413, 63.019838624110008 ], [ -150.90189647153571, 63.017275985649633 ], [ -150.90809217970249, 63.014856345711856 ], [ -150.91455859962386, 63.012586300190016 ], [ -150.92127804658071, 63.010472035707942 ], [ -150.92823215807201, 63.008519313048112 ], [ -150.93540194353906, 63.006733451734412 ], [ -150.94276783564695, 63.005119315808479 ], [ -150.95030974299181, 63.003681300833051 ], [ -150.95800710409924, 63.002423322155749 ], [ -150.96583894257765, 63.001348804461315 ], [ -150.97378392328815, 63.000460672638646 ], [ -150.98182040939199, 62.999761343983636 ], [ -150.98992652013391, 62.999252721758786 ], [ -150.99808018921993, 62.998936190123125 ], [ -151.00625922364659, 62.99881261044623 ], [ -151.01444136283843, 62.998882319014889 ], [ -151.02260433794945, 62.999145126137641 ], [ -151.03072593118472, 62.999600316649975 ], [ -151.038784034998, 63.000246651818159 ], [ -151.04675671102135, 63.001082372638329 ], [ -151.0546222485832, 63.002105204520888 ], [ -151.06235922267174, 63.003312363351235 ], [ -151.06994655120161, 63.004700562909761 ], [ -151.07736355144266, 63.006266023635135 ], [ -151.08458999547045, 63.008004482707662 ], [ -151.0916061645006, 63.009911205429127 ], [ -151.09839290196948, 63.011980997871042 ], [ -151.1049316652265, 63.014208220759812 ], [ -151.11120457570564, 63.016586804565193 ], [ -151.11719446744533, 63.019110265753525 ], [ -151.12288493382945, 63.021771724166932 ], [ -151.12826037242502, 63.024563921483733 ], [ -151.1333060277947, 63.027479240714072 ], [ -151.13800803216756, 63.030509726682034 ], [ -151.1423534438531, 63.033647107442754 ], [ -151.14633028329038, 63.036882816578235 ], [ -151.14992756662647, 63.040208016317358 ], [ -151.15313533672511, 63.043613621418864 ], [ -151.15594469151043, 63.04709032375608 ], [ -151.15834780955709, 63.050628617539481 ], [ -151.1603379728445, 63.054218825112159 ], [ -151.16190958659726, 63.057851123247708 ], [ -151.16305819614374, 63.061515569884364 ], [ -151.16378050072888, 63.065202131221696 ], [ -151.16407436422639, 63.068900709108682 ], [ -151.1639388227031, 63.072601168648298 ], [ -151.16337408879582, 63.076293365945531 ], [ -151.16238155286968, 63.079967175921048 ], [ -151.16096378093604, 63.083612520115956 ], [ -151.15912450931631, 63.087219394410255 ], [ -151.15686863604779, 63.090777896577407 ], [ -151.15420220903675, 63.094278253600052 ], [ -151.15113241097407, 63.097710848667184 ], [ -151.14766754103744, 63.101066247780039 ], [ -151.1438169934157, 63.104335225887148 ], [ -151.1395912326993, 63.107508792478043 ], [ -151.13500176619161, 63.110578216558721 ], [ -151.13006111320612, 63.113535050938602 ], [ -151.12478277142358, 63.116371155757989 ], [ -151.11918118039407, 63.119078721187613 ], [ -151.11327168227811, 63.121650289233088 ], [ -151.10707047993071, 63.124078774581456 ], [ -151.10059459244107, 63.126357484426606 ], [ -151.09386180825081, 63.128480137216151 ], [ -151.08689063598101, 63.130440880262881 ], [ -151.07970025310763, 63.132234306168712 ], [ -151.072310452632, 63.133855468011689 ], [ -151.06474158790138, 63.135299893250448 ], [ -151.05701451574072, 63.136563596303994 ], [ -151.04915053806346, 63.137643089768773 ], [ -151.04117134213482, 63.138535394239327 ], [ -151.0330989396667, 63.139238046702125 ], [ -151.02495560492667, 63.139749107477662 ], [ -151.01676381204862, 63.140067165688805 ], [ -151.00854617173482, 63.140191343240197 ], [ -151.00032536754193, 63.14012129729457 ], [ -150.99212409194485, 63.139857221239531 ], [ -150.98396498237335, 63.139399844142893 ], [ -150.97587055741673, 63.138750428695523 ], [ -150.96786315339031, 63.137910767650389 ], [ -150.95996486145731, 63.136883178767839 ], [ -150.95219746549625, 63.135670498282444 ], [ -150.94458238090263, 63.134276072911327 ], [ -150.93714059450801, 63.132703750428355 ], [ -150.92989260579785, 63.130957868833391 ], [ -150.9228583696019, 63.12904324414837 ], [ -150.91605724042748, 63.126965156878583 ], [ -150.90950791859865, 63.124729337178145 ], [ -150.9032283983573, 63.122341948766497 ], [ -150.89723591807635, 63.119809571642911 ], [ -150.89154691272552, 63.117139183650046 ], [ -150.88617696872339, 63.114338140944113 ], [ -150.88114078130059, 63.111414157426474 ], [ -150.87645211448913, 63.108375283199976 ], [ -150.87212376384491, 63.105229882112639 ], [ -150.86816752200031, 63.101986608453423 ], [ -150.86459414713363, 63.09865438287104 ], [ -150.86141333443331, 63.095242367582053 ], [ -150.85863369062412, 63.091759940943284 ], [ -150.85626271161289, 63.088216671459698 ], [ -150.85430676330105, 63.084622291301898 ], [ -150.85277106560144, 63.080986669410947 ], [ -150.85165967968697, 63.07731978426358 ], [ -150.85097549848885, 63.073631696376161 ], [ -150.85072024045218, 63.069932520624761 ], [ -150.85089444654758, 63.066232398456513 ] ] ] }