Published
Edited
May 17, 2022
1 fork
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map = {
// Setup some base layers
let USGS_USTopo = L.tileLayer(
"https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}",
{
maxZoom: 16,
attribution:
'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>'
}
);
let OSM2 = L.tileLayer(
"https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
{
maxZoom: 19,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>'
}
);
let mapbox_outdoors = L.tileLayer(
`https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=${mb_token}`,
{
attribution:
'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: "mapbox/outdoors-v11",
tileSize: 512,
zoomOffset: -1
}
);

// Set the map itself, centered on Asheville with the USGS Topo baseLayer
let map = L.map(map_container, {
layers: [USGS_USTopo],
center: [35.6, -82.55],
zoom: 12,
minZoom: 10
// maxBounds: L.latLngBounds(
// L.latLng(34.9737, -83.864136),
// L.latLng(36.2021744, -81.243896)
// )
});

// Add controls
let baseLayers = {
"USGS Topo": USGS_USTopo,
"Open Street Map": OSM2
};
// Check to see if we've still got Mapbox credit!
if (
(await fetch(
`https://api.mapbox.com/styles/v1/mapbox/outdoors-v11/tiles/11/554/807?access_token=${mb_token}`
)).status == 200
) {
baseLayers["Mapbox Outdoors"] = mapbox_outdoors;
}

// This Grouped Layer control is not standard, but an easily installed plugin.
// There's only one group but we can tag it as "exclusive" to get radio buttons,
// rather than checkboxes.
L.control
.groupedLayers(baseLayers, groupedOverlays.groups, {
exclusiveGroups: ["Rides"]
})
.addTo(map);
//L.control.scale().addTo(map);

// Try initial display based on query string.
// Useful for sharing one-offs with friends.
{
if (params.get('gpx')) {
try {
let trk_pts = get_trkpts(
await (await fetch(
`https://marksmath.org/maps/GPXFiles/${params.get('gpx')}.gpx`
)).text()
);
let path = create_gpx_path(trk_pts);
path.addTo(map);
map.fitBounds(path.getLayers()[0].getBounds());
make_elevation_chart(trk_pts);
global.gpx_path = path;
} catch {
('pass');
}
}
}

// On overlay, we want to fly to the new path, make the elevation chart,
// and display any comments we might see.
map.on("overlayadd", function(p) {
if (global.gpx_path) {
global.gpx_path.removeFrom(map);
}
global.gpx_path = p.layer;
map.flyToBounds(
groupedOverlays.groups.Rides[p.name].getLayers()[0].getBounds()
);
make_elevation_chart(groupedOverlays.trkpts_list[p.name]);
d3.select(comment).append(() => comments.get(p.name));
});

// Gotta clear some stuff, when we remove the overlay.
map.on("overlayremove", function() {
global.gpx_path = null;
d3.select(elevation_chart_container)
.select("svg")
.remove();
d3.select(comment).html("");
});

// Do I really need to do this?
invalidation.then(() => map.remove());

return map;
}
Insert cell
11975 / 60 ** 2
Insert cell
t2.getTime() - t1.getTime()
Insert cell
t2 = groupedOverlays.trkpts_list["GRVL: Bent Creek WVL"].slice(-1)[0].time
Insert cell
t1 = groupedOverlays.trkpts_list["GRVL: Bent Creek WVL"][0].time
Insert cell
groupedOverlays = {
if(params.get('gpx')) return {};
const files = {
"Road: 276": FileAttachment("276.reduced.gpx"),
"Road: Bakery": FileAttachment("Bakery3.reduced.gpx"),
"Road: Coxes Creek": FileAttachment("CoxesCreek.reduced.gpx"),
"Road: Haw Creek Overlook": FileAttachment("HawcreekTownMtn.reduced.gpx"),
"Road: Hookers Gap to Marshall": FileAttachment("HookersGapAndMarshall.reduced.gpx"),
"Road: Lake Logan": FileAttachment("LakeLogan.reduced.gpx"),
"Road: Up Ox Creek": FileAttachment("OxCreekBiltmoreForrest.reduced@1.gpx"),
"GRVL: Curtis Creek": FileAttachment("FolkArtCurtisCreekParkway.reduced.gpx"),
"GRVL: Bent Creek": FileAttachment("BCG6.reduced.gpx"),
"GRVL: Bent Creek WVL": FileAttachment("BCGfromWVL.reduced.gpx"),
"GRVL: Lynn Cove cut through": FileAttachment("LynnCoveCutThrough.reduced.gpx"),
"GRVL: Three dirt too": FileAttachment("NorthAshevilleGravel.reduced.gpx"),
"GRVL: Pisgah 470s": FileAttachment("PisgahGravel470s.reduced.gpx"),
"GRVL: Stoney Fork": FileAttachment("StoneyFork.reduced.gpx"),
"GRVL: Yellow Gap": FileAttachment("YellowGapTo276.reduced.gpx"),
"MTN: A lotta Bent Creek": FileAttachment("AlottaBentCreek.reduced.gpx"),
"MTN: Big Creek": FileAttachment("BIGBigCreek.reduced.gpx"),
"MTN: Fish Hatch": FileAttachment("CoveCreekDanielButterCat.reduced.gpx"),
"MTN: Jerdon Mountain": FileAttachment("JerdonMountain.reduced.gpx"),
"MTN: Pink Beds to Bennett Gap": FileAttachment("PinkBedsBennettGap.reduced.gpx"),
"MTN: Spencer Fletcher": FileAttachment("SpencerFletcherWithRob.reduced.gpx"),
};
const routes = await Promise.all(Object.entries(files).map(async ([name, file]) => {
const points = get_trkpts(await file.text());
return [name, {points, path: create_gpx_path(points)}];
}));

return {
groups: {
Rides: Object.fromEntries(routes.map(([k, {path}]) => [k, path]))
},
trkpts_list: Object.fromEntries(routes.map(([k, {points}]) => [k, points])),
};
}
Insert cell
comments = {
let comments = new Map();
comments.set(
"GRVL: Curtis Creek",
md`### Curtis Creek
This ride is mostly road but has a serious six mile gravel climb from Old Fort (at 1500 ft) to the Parkway at over 4000 feet. It was 60 degrees in Asheville on this particular January day, but I rode through an inch of snow for several miles on the Parkway.`
);
comments.set(
"Road: 276",
md`### 276
A version from UNCA appears on my old map as well - and I'm still doing it.`
);
comments.set(
"Road: Bakery",
md`### The Bakery Ride
This is a very fast group that that leaves from Fuddruckers on Saturday mornings. Don't get dropped, if you don't know Leicester!
`
);
comments.set(
"Road: Coxes Creek",
md`### Coxes creek
Coxes creek road is an innocent looking road that turns off of 19E as it meanders along the Cane River outside of Burnsville. Innocence turns deadly pretty soon, though as you hit a ramp of 20%. After a mile and a half or so, it bombs back down to Jack's Creek where you can turn right to complete a great little loop.`
);
comments.set(
"Road: Haw Creek Overlook",
md`### Haw Creek Overlook
There's this section of the Parkway between the Folk Art Center and Town Mountain Rd that's right outside of town and just great to go up or down. It opens up at the Haw Creek Overlook to give great views of the valley and mountains in the distance.`
);
comments.set(
"Road: Hookers Gap to Marshall",
md`### Hooker's Gap to Marshall
This is kind of a combination of Hooker's Gap and Rector's Corner, which are both featured on my old map.`
);
comments.set(
"Road: Lake Logan",
md`### Lake Logan
I got a crew I ride this with just about every Summer. Starting near Lake Logan cuts the 15 mile climb up 215 in half and gives you a good spot to swim when you're done.`
);
comments.set(
"Road: Up Ox Creek",
md`### Up Ox Creek
I've got a ride on my old map called The Ox Creek Plunge, which is a very popular ride. Going up Ox Creek is much harder`
);
comments.set(
"GRVL: Bent Creek",
md`### Bent Creek
Bent Creek is about 5 to 10 minute drive from my house, so I ride Gravel and Trail there a lot. This is a nice 20 miler with nothing but gravel`
);
comments.set(
"GRVL: Bent Creek WVL",
md`### Bent Creek WVL
Someties, I just ride the gravel bike to Bent Creek from home. This ride goes straight through Bent Creek to the Parkway and then boms down that to the Arboretum before heading back through Bent Creek to get to the secret Enka Lake exit to get home.`
);
comments.set(
"GRVL: Lynn Cove cut through",
md`### Lynn Cove cut through
So Dave tells me that the Strava heat map says there's a faint path from the back of Lynn Cove to the top of Elk Mountain. Sure - what the hell? :)`
);
comments.set(
"GRVL: Three dirt too",
md`### Three dirt too
There's a ride called Three Dirt on my old page. I think it's awesome that I was riding gravel on my titanium road bike years before everyone was into it. This ride hits the same three dirt roads via an entirely different route.`
);
comments.set(
"GRVL: Pisgah 470s",
md`### Pisgah 470s
I've mountain biked in Pisgah about a thousand times. This ride is the one time I've ever driven down there by myself to gravel ride. Not sure why I don't do it more!`
);
comments.set(
"GRVL: Stoney Fork",
md`### Stoney Fork
A wonderful ride - a lot like Curtis Creek in that it's spectacular in beauty and difficulty.`
);
comments.set(
"GRVL: Yellow Gap",
md`### Yellow Gap
You can turn your Bent Creek gravel ride into a *true* epic by riding up and *over* the Blue Ridge down into Mill River where you catch Yellow Gap Road to ride clear out to 276. You can then take the Parkway back.`
);
comments.set(
"MTN: A lotta Bent Creek",
md`### A lotta Bent Creek
I do a fair amount of 10-12 mile rides in Bent Creek. Almost 23 miles on this day, though!`
);
comments.set(
"MTN: Big Creek",
md`### BIG Big Creek
I would not have done this ride without Rob and Eric, as there's a lot of hike a bike. Big Creek is definitely an fun, fun, fun down hill, though.`
);
comments.set(
"MTN: Fish Hatch",
md`### Fish Hatch
I *love* riding out of the Fish Hatchery! This ride took in Cove Creek, Daniel Ridge, and Butter Gap before ending up coming down the waterfall filled Cat Gap trail behind the Fish Hatch.`
);
comments.set(
"MTN: Jerdon Mountain",
md`### Jerdon Mountain Challenge
I've done a fair amount of organized road races but this was the first time I did a mountain bike race - mainly to escort a friend's 13 year old son. My official time 3:00:18 and Wade's was 3:00:01 and it made me feel pretty good to be comfortably in the first half of the pack.

Wade did it in 2:18 the following year!`
);
comments.set(
"MTN: Pink Beds to Bennett Gap",
md`### Pink Beds and Bennett Gap
This ride was on my old map and it's still one of my favorites.`
);
comments.set(
"MTN: Spencer Fletcher",
md`### Spencer/Fletcher
This is the right way to ride Spencer Gap and Fletcher Creek - from the bottom! Did this with Rob on his visit last Summer; hopefully, we'll do it again next week!`
);
return comments;
}
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('lat')),
parseFloat(trkpt.getAttribute('lon'))
];
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);
}
return pts;
}
Insert cell
// Take a list of [lat,lon] pairs and retun a layerGroup with
// a polyline and start/stop markers.
function create_gpx_path(trkpts, opts = {}) {
let { group_or_fetch = 'group' } = opts;
let polyline = L.polyline(trkpts, { color: 'blue' });
let position_marker = L.marker(trkpts[0], {
icon: L.divIcon({ className: 'markerIconShape positionIconColor' })
});
let start_marker = L.marker(trkpts[0], {
icon: L.divIcon({ className: 'markerIconShape startIconColor' })
});
let end_marker = L.marker(trkpts.slice(-1)[0], {
icon: L.divIcon({ className: 'markerIconShape endIconColor' })
});
let gpx_path = L.layerGroup([
polyline,
position_marker,
start_marker,
end_marker
]);

return gpx_path;
}
Insert cell
// Constructs the elevation chart

function make_elevation_chart(trkpts) {
let w = window.innerWidth < 600 ? window.innerWidth : 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('mouseenter', function() {
position_marker.style('opacity', 1);
})
.on('mousemove', 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]);
})
.on('mouseleave', function() {
position_marker.style('opacity', 0);
});

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
parseTime2 = d3.utcParse("%Y-%m-%dT%H:%M:%SZ")
Insert cell
parseTime1 = d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ")
Insert cell
function parseTime(s) {
let t1 = parseTime1(s);
let t2 = parseTime2(s);
if (t1) {
return t1;
} else {
return t2;
}
}
Insert cell
formatTime = d3.timeFormat('%a, %b %e, %Y')
Insert cell
global = ({})
Insert cell
parser = new DOMParser()
Insert cell
L = {
const L = await require("leaflet@1/dist/leaflet.js");

// The following plugin allows the Exclusive radio buttons for the overlays
await require("leaflet-groupedlayercontrol").catch(
() => L.Control.GroupedLayers
);
if (!L._style) {
const href = await require.resolve("leaflet@1/dist/leaflet.css");
document.head.appendChild(
(L._style = html`<link href=${href} rel=stylesheet>`)
);
}
return L;
}
Insert cell
style = html`
<style>
.markerIconShape {
border: 1px solid black;
height: 22px;
width: 22px;
border-radius:50%;
-moz-border-radius:50%;
-webkit-border-radius:50%;
}
.startIconColor {
background-color:#0f0;
}
.positionIconColor {
background-color:#0ff;
}
.wptIconColor {
background-color:#ff0;
}
.endIconColor {
background-color:#f00;
}
</style>
`
Insert cell
h = '550px'
Insert cell
params = new URLSearchParams(location.search)
Insert cell
import { mb_token } from '@mcmcclur/mapbox-token'
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