Published
Edited
May 29, 2022
12 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
elevation_chart_container = html``
Insert cell
Insert cell
Insert cell
Insert cell
map = {
let map = new maplibregl.Map({
container,
style: styles[initial_type],
center: [-82.53041, 35.585827],
zoom: 11,
maxBounds: [
[-83, 35.3],
[-82, 35.9]
]
});
map.on("load", async function () {
map.addControl(
new maplibregl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
})
);
if (Object.keys(other_files).indexOf(params.get("gpx")) > -1) {
let pts = get_trkpts(await other_files[params.get("gpx")].text());
add_path(map, pts);
make_elevation_chart(pts, map);
}
});
return map;
}
Insert cell

initial_type = {
if (params.get("type") == "topo") {
return "topo";
} else {
return "streets";
}
}
Insert cell
// The height of the map.
// Should be redefined to "100%" on embed.
height = "720px"
Insert cell
global_markers = ({ start_marker: null, stop_marker: null })
Insert cell
function add_path(map, pts, fit = true) {
// Remove the path, if already there
if (map.getLayer("path")) {
map.removeLayer("path");
}
if (map.getSource("route")) {
map.removeSource("route");
}
// And the start stop markers, too.
if (global_markers.start_marker) {
global_markers.start_marker.remove();
global_markers.start_marker = null;
}
if (global_markers.stop_marker) {
global_markers.stop_marker.remove();
global_markers.stop_marker = null;
}

// Add geoJSON describing the route
map.addSource("route", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "LineString",
coordinates: pts
}
}
]
}
});
// Use the geoJSON to add the layer
map.addLayer({
id: "path",
type: "line",
source: "route",
layout: {
"line-join": "round",
"line-cap": "round"
},
paint: {
"line-color": "#00b",
"line-width": 5
}
});

// Add the markers
global_markers.start_marker = new maplibregl.Marker({ color: "#00dd00" })
.setLngLat(pts[0])
.addTo(map);
global_markers.stop_marker = new maplibregl.Marker({ color: "#dd0000" })
.setLngLat(pts.slice(-1)[0])
.addTo(map); // marker.remove();

// Fit the map to the new path. Note that
// fit=false, when called after a change of map_style
if (fit) {
map.setMaxBounds(null);

let lon_range = pts.max_lon - pts.min_lon;
let lat_range = pts.max_lat - pts.min_lat;

let r = 0.2;
let min_lon = pts.min_lon - lon_range * r;
let max_lon = pts.max_lon + lon_range * r;
let min_lat = pts.min_lat - lat_range * r;
let max_lat = pts.max_lat + lat_range * r;
map.fitBounds(
[
[min_lon, min_lat],
[max_lon, max_lat]
],
{ duration: 1600 }
);

// Well, this is odd. As far as I can tell, though,
// map.fitBounds doesn't return a Promise, nor give
// any indication as to when it completes, so we've
// got to do something to wait to for setMaxBounds.
Promises.delay(2000).then(function () {
r = 4;
min_lon = pts.min_lon - lon_range * r;
max_lon = pts.max_lon + lon_range * r;
min_lat = pts.min_lat - lat_range * r;
max_lat = pts.max_lat + lat_range * r;
map.setMaxBounds([
[min_lon, min_lat],
[max_lon, max_lat]
]);
});
}
}
Insert cell
// Parse the GPX file and return a list of [lat,lon] points.
// Each point optionally has a ele(vation) and time keys.
function get_trkpts(file_string) {
let parsed = parser.parseFromString(file_string, "text/xml");
let trkpts = parsed.getElementsByTagName("trkpt");
let pts = [];
for (let trkpt of trkpts) {
let pt = [
parseFloat(trkpt.getAttribute("lon")),
parseFloat(trkpt.getAttribute("lat"))
];
try {
let ele = trkpt.getElementsByTagName("ele")[0].textContent;
pt.ele = 3.28084 * parseFloat(ele);
} catch (e) {
("pass");
}
try {
let time = trkpt.getElementsByTagName("time")[0].textContent;
pt.time = parseTime(time);
} catch (e) {
("pass");
}
pts.push(pt);
}
let lons = pts.map((pt) => pt[0]);
pts.min_lon = d3.min(lons);
pts.max_lon = d3.max(lons);
let lats = pts.map((pt) => pt[1]);
pts.min_lat = d3.min(lats);
pts.max_lat = d3.max(lats);
return pts;
}
Insert cell
// Constructs the elevation chart

function make_elevation_chart(trkpts, map) {
let size = d3.min([window.innerWidth, window.innerHeight]);
let w = size < 600 ? 0.98 * size : 600;
let h = w / 3;

let pad_left = 40;
let pad_bottom = 20;
let svg = d3
.create("svg")
.attr("class", "elevation_chart")
.attr("width", w)
.attr("height", h);
svg
.append("rect")
.attr("width", w)
.attr("height", h)
.attr("fill", "white")
.attr("opacity", 0.5);

let lngLats = trkpts; //.map((pt) => [pt[1], pt[0]]);
let elevation_path = [];
let cummulative_length = 0;
let R = 3;

let distance_to_point_map = new Map();
for (let i = R; i < trkpts.length - R; i++) {
cummulative_length =
cummulative_length + d3.geoDistance(lngLats[i - 1], lngLats[i]) * 3958.8;
let elevation = d3.mean(trkpts.slice(i - R, i + R).map((pt) => pt.ele));
elevation_path.push([cummulative_length, elevation]);
distance_to_point_map.set(cummulative_length, lngLats[i]);
}

let path_length = elevation_path.slice(-1)[0][0];
let elevations = trkpts.map((o) => o.ele);
let min_elevation = d3.min(elevations);
let max_elevation = d3.max(elevations);
let elevation_pad = 500;

let elevation_path2 = [[0, min_elevation - elevation_pad]]
.concat(elevation_path)
.concat([[path_length, min_elevation - elevation_pad]]);

let x_scale = d3.scaleLinear().domain([0, path_length]).range([pad_left, w]);
let y_scale = d3
.scaleLinear()
.domain([min_elevation - elevation_pad, max_elevation + elevation_pad])
.range([h - pad_bottom, 0]);
let pts_to_path = d3
.line()
.x((d) => x_scale(d[0]))
.y((d) => y_scale(d[1]));

svg
.append("path")
.attr("d", pts_to_path(elevation_path2))
.style("stroke", "black")
.style("stroke-width", "0px")
.style("stroke-linejoin", "round")
.style("opacity", 0.7)
.style("fill", "#eee");
svg
.append("path")
.attr("d", pts_to_path(elevation_path))
.style("stroke", "black")
.style("stroke-width", "3px")
.style("stroke-linejoin", "round")
.style("fill", "none");
svg
.append("g")
.attr("transform", `translate(0, ${h - pad_bottom})`)
.call(d3.axisBottom(x_scale));
svg
.append("g")
.attr("transform", `translate(${pad_left})`)
.call(d3.axisLeft(y_scale));
let position_marker = svg
.append("g")
.attr("class", "position_marker")
.style("opacity", 0);
position_marker
.append("line")
.attr("stroke-width", "1px")
.attr("stroke", "black")
.attr("y1", 0)
.attr("y2", h)
.attr("x1", w / 2)
.attr("x2", w / 2);
position_marker
.append("circle")
.attr("r", "5px")
.attr("cx", w / 2)
.attr("cy", h / 2)
.attr("fill", "#0ff")
.attr("stroke", "black");

let lengths = elevation_path.map((pt) => pt[0]);
svg
.on("touchmove", (e) => e.preventDefault()) // prevent scrolling
.on("pointerenter", function () {
position_marker.style("opacity", 1);
map.addSource("point", {
type: "geojson",
data: {
type: "Feature",
geometry: {
type: "Point",
coordinates: trkpts[20]
}
}
});
map.addLayer({
id: "point",
type: "circle",
source: "point",
paint: {
"circle-radius": 6,
"circle-color": "cyan"
// "stroke-color": "black"
}
});
})
.on("pointermove", function (evt) {
evt.preventDefault();
let distance = x_scale.invert(d3.pointer(evt)[0]);
let i = binarySearch(lengths, distance);
let x = x_scale(distance);
let elevation = elevations[i];
let y = y_scale(elevation);
position_marker.select("line").attr("x1", x).attr("x2", x);
position_marker.select("circle").attr("cx", x).attr("cy", y);
// global.gpx_path.getLayers()[1].setLatLng(trkpts[i]);
map.getSource("point").setData({
type: "Feature",
geometry: {
type: "Point",
coordinates: trkpts[i]
}
});
})
.on("pointerleave", function () {
position_marker.style("opacity", 0);
map.removeLayer("point");
map.removeSource("point");
});

svg
.append("text")
.attr("x", 50)
.attr("y", 20)
.text(formatTime(trkpts[0].time));
svg
.append("text")
.attr("x", 50)
.attr("y", 40)
.text(`${d3.format("0.1f")(path_length)} miles`);

d3.select(elevation_chart_container).selectAll(".elevation_chart").remove();
d3.select(elevation_chart_container).append(() => svg.node());
}
Insert cell
// Given an *ordered* array arr and a value t, this finds the index
// of the laregest element of arr that is less than t.
// Used when we hover over the elevation chart to quickly find the
// corresponding point on the path.

function binarySearch(arr, t, bail = 100) {
let cnt = 0;
let m = 0;
let n = arr.length;
let a = arr[0];
let b = arr[n - 1];
if (t <= a) {
return 0;
} else if (t >= b) {
return n - 1;
} else {
let k;
while (n - m > 1 && cnt++ < bail) {
k = Math.floor((m + n) / 2);
let c = arr[k];
if (t <= c) {
b = c;
n = k;
} else {
a = c;
m = k;
}
}
return k;
}
}
Insert cell
Insert cell
formatTime = d3.timeFormat("%a, %b %e, %Y")
Insert cell
function parseTime(s) {
let t1 = parseTime1(s);
let t2 = parseTime2(s);
if (t1) {
return t1;
} else {
return t2;
}
}
Insert cell
parseTime2 = d3.utcParse("%Y-%m-%dT%H:%M:%SZ")
Insert cell
parseTime1 = d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ")
Insert cell
parser = new DOMParser()
Insert cell
params = new URLSearchParams(location.search)
Insert cell
Insert cell
Insert cell
// These can files can be accessed via a query string like ?gpx=CadillacMountain
// Useful for sharing rides with friends.
other_files = ({
CadillacMountain: await FileAttachment("CadillacMountain.gpx"),
Towpath: await FileAttachment("Towpath.05.28.22-reduced.gpx"),
Towpath2: await FileAttachment("Towpath.05.29.22.gpx")
})
Insert cell
Insert cell
Insert cell
// Let's download and parse the streets and topo style files to JSON right at the outset.
// That makes the switching between the two styles more efficient.
styles = ({
streets: await fetch(
"https://api.maptiler.com/maps/streets/style.json?key=W5IvZO3kQ9M5HwKinY7E"
).then(async function (r) {
return JSON.parse(await r.text());
}),
topo: await fetch(
"https://api.maptiler.com/maps/topo/style.json?key=W5IvZO3kQ9M5HwKinY7E"
).then(async function (r) {
return JSON.parse(await r.text());
})
})
Insert cell
// MapLibre-gl library
maplibregl = require("maplibre-gl@2.1.9")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more