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.
<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>
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));
}
{
"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"
}
]
}
{ "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 ] ] ] }