<html lang='en'>
<head>
<meta charset='utf-8' />
<title>Munros</title>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />
<script src="https://api.mapbox.com/mapbox.js/plugins/turf/v3.0.11/turf.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<!--Main map-->
<div id="map"></div>
<!--Text for header-->
<div class='header'><span style="font-size: 15pt; padding-left:10px; text-align: left"> Munros by Train </span><span style="font-size: 10pt; padding-right:12px; float:right"> By Keelin Titzer </span></div>
<!--Text for information overlay-->
<div class='map-overlay' id='features'><div id='pd'><p>282 mountains in Scotland over 3000 feet tall are classified as Munros.</p><p>How easily can a hiker reach each Munro by train?</p><h4>Click a Munro to see its nearest train station.</h4>
</p></div></div>
<!--Insert icons and text for a legend-->
<div class='map-overlay' id='legend'>
<div width: 100%>
<p><img src="https://i.ibb.co/yVtxQSc/mountain-green-e.png" class = "logo" alt="Mountain">
Munro</p>
<div width: 100%>
<p><img src="https://i.ibb.co/GnCrk54/national-rail.png" class="logo" alt="Train station"> Train station</p></div>
</div>
<!--Add custom font-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Dosis:wght@300&family=Quicksand:wght@300&family=Rubik:wght@300&display=swap" rel="stylesheet">
<script>
</script>
</body>
</html>
/* Style for the header */
.header {
background: rgba(255, 255, 255, 0.75);
position: fixed;
height: 6%;/* Because page body is 94% */
width: 100%;
top: 0px;
line-height: 40px; /* To make sure text has vertical space */
font-family: 'Quicksand', sans-serif;
}
/* Different header height for short screens such as phones in landscape mode */
@media only screen and (max-height: 480px) {
.header {
height: 12%;
}}
/* Style for body of screen */
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
bottom: 0;
width: 100%;
height: 94%; /* Body and header separated add up to 100%. Navigation control does not overlay header */
}
/* Different map body height for short screens such as phones in landscape mode */
@media only screen and (max-height: 480px) {
#map {
height: 88%; /* Body and header separated add up to 100%. Navigation control does not overlay header */
}}
/* Style for the headings */
h3 {
border-top: 15px blue;
font-family: Quicksand, sans-serif;
}
/* Style for the paragraph in the pop-up window */
p {
font-size: 15px;
font-family: Quicksand, sans-serif;
}
/* Set width for icons in legend */
img {
width: 15px;
}
/* Style for the mountain mouseover pop-up window */
.my-popup .mapboxgl-popup-content {
background: rgba(255, 255, 255, 0.75); /* red, green, blue, and alpha */
border-top: 30px black;
padding: 8px;
color: black;
}
/* Style for the mountain click pop-up window */
.click-popup .mapboxgl-popup-content {
background: rgba(255, 255, 255, 0.75); /* red, green, blue, and alpha */
border-top: 0px black;
padding: 2px;
padding-top: -10px;
color: black;
}
/* General style and position for overlay boxes */
.map-overlay {
position: absolute;
bottom: 0;
right: 0;
background: #fff;
margin-right: 12px;
font-family: 'Quicksand', sans-serif;
overflow: auto;
border-radius: 3px;
}
/* Style specifically for box showing mountain info */
#features {
top: 6%; /* to match with top of body */
min-height: 10%;
max-height: 22%;
width: 250px;
line-height: 85%;
margin-top: 12px; /* 12px lower than header for consistency with right margin */
padding: 0px 10px 0px 10px; /* Extra padding on right and left sides */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
overflow-y: scroll;
}
/* Different box style for small likely landscape screens */
@media only screen and (max-height: 480px) {
#features {
top: 12%;
min-height: 6%;
max-height: 20%;
width: 75%;
}}
/* Different box style for other phone screens */
@media only screen and (max-width: 600px) {
#features {
top: 6%;
max-height: 12%;
width: 250px;
}}
#legend {
padding: 0px 0px 0px 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
height: 70px;
bottom: 40px;
width: 120px;
line-height: 50%;
}
.legend-key {
display: inline;
border-radius: 20%;
width: 20px;
height: 20px;
padding: 5px;
}
mapboxgl.accessToken =
"pk.eyJ1Ijoia3RpdHplciIsImEiOiJja3p5MWl4cTgwMWRqMm5wY3d5YWRreXB1In0.8YKr-h7pmDokMoPaIqbBLA";
const map = new mapboxgl.Map({
container: 'map',
style: "mapbox://styles/ktitzer/cl0sko1ii00kz15qgbhvjsu5h",
center: [-4.35, 57.4],
minZoom: 4,
zoom: 6.6,
attributionControl: false, // So that attribution can be modified to include copyright for the new layers added
});
// Add custom copyright information to the default copyright info
map.addControl(new mapboxgl.AttributionControl({
customAttribution: 'Contains OS data © Crown copyright [and database right] 2022. Contains data © Improvement Service and Database of British & Irish Hills.',
compact: true // Make copyright info only visible on click because it would otherwise cover the mapbox logo due to its length
}));
// URL to station dataset
const stations_url =
"https://api.mapbox.com/datasets/v1/ktitzer/cl0s1n86w001n27pqxu7s5v3i/features?access_token=pk.eyJ1Ijoia3RpdHplciIsImEiOiJja3p5MWl4cTgwMWRqMm5wY3d5YWRreXB1In0.8YKr-h7pmDokMoPaIqbBLA";
// URL to mountain dataset
const mountains_url = "https://api.mapbox.com/datasets/v1/ktitzer/cl0i510650b0027o26pjjtxg9/features?access_token=pk.eyJ1Ijoia3RpdHplciIsImEiOiJja3p5MWl4cTgwMWRqMm5wY3d5YWRreXB1In0.8YKr-h7pmDokMoPaIqbBLA"
// Add a scale bar
const scale = new mapboxgl.ScaleControl({
maxWidth: 80, //size of the scale bar
unit: "metric",
});
map.addControl(scale);
// Add the navigation control to the map to the top left corner.
map.addControl(new mapboxgl.NavigationControl(),'top-left');
map.on("load", () => {
map.addLayer({
id: "stations",
type: "symbol",
source: {
type: "geojson",
data: stations_url // URL to stations dataset
},
layout: {
"icon-image": "national-rail", // custom UK train station icon
"icon-size": 0.8,
"icon-allow-overlap": true,
"text-allow-overlap": false,
'text-field': ['get', 'Name'],
'text-variable-anchor': ['top', 'bottom', 'left', 'right'],
'text-size': 10,
'text-radial-offset': 0.7,
'text-justify': 'auto',
},
paint: {}
});
map.addLayer({
id: "mountains",
type: "symbol",
source: {
type: "geojson",
data: mountains_url
},
layout: {
"icon-image": "mountain-green-e", // Custom mountain icon
"icon-size": 1.0,
"icon-allow-overlap": false
},
paint: {}
});
map.addSource("nearest-station", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
// Create legend
const legend = document.getElementById("legend");
// Create popup
const popup = new mapboxgl.Popup({className: "my-popup", closeButton: false, offset: [0, -5] })
// Events to occur when mouse moves over a mountain
map.on('mousemove', (event) => {
const features = map.queryRenderedFeatures(event.point, {
layers: ['mountains']
});
if (!features.length) {
popup.remove();
return;
}
const feature = features[0];
popup
.setLngLat(feature.geometry.coordinates)
.setHTML(feature.properties.Name)
.addTo(map);
// Change to pointer cursor when over clickable feature
map.getCanvas().style.cursor = 'pointer';
});
// Populate feature information box with name of mountain, nearest station, and linear distance. Fly to mountain when clicked.
map.on("click", (event) => {
const mountain = map.queryRenderedFeatures(event.point, {
layers: ["mountains"]
});
if (!mountain.length) {
return;
}
// This variable feature is the marker clicked.
// Feature has geometry and properties.
// Properties are the columns in the attribute table.
const selectedmountain = mountain[0];
// Fly to the point when clicked.
map.flyTo({
center: selectedmountain.geometry.coordinates, // fly to coordinates of selected mountain
zoom:8.8, //zoom to level 8.8
// flyTo options from https://docs.mapbox.com/mapbox-gl-js/example/flyto-options/
bearing: 0,
// These options control the flight curve, making it move
// slowly and zoom out almost completely before starting
// to pan.
speed: 0.7, // choose flight speed
curve: 1, // change the speed at which it zooms out
// This can be any easing function: it takes a number between 0 and 1 and returns another number between 0 and 1.
easing: (t) => t,
// This animation is considered essential with respect to prefers-reduced-motion
essential: true
});
// Add info to overlay box
document.getElementById("pd").innerHTML = mountain.length
? `<h4>Munro name: ${mountain[0].properties.Name}</h4><p>Closest station: ${mountain[0].properties.NearestStation}</p><p>Distance from station: ${mountain[0].properties.HubDist_2} km</p><p>Elevation: ${mountain[0].properties.Height_ft} feet</p>`
: `<p>Click a mountain to see its details and nearest train station.</p>`;
// Create popup
const popup = new mapboxgl.Popup({className: "my-popup", closeButton: false, offset: [0, -5] })
// Have popup stay visible when a mountain is clicked
popup
.setLngLat(mountain[0].geometry.coordinates)
.setHTML(mountain[0].properties.Name)
.addTo(map);
});
// When pointer leaves mountains layer, change to regular cursor for UI
map.on('mouseleave', 'mountains', (event) => {
map.getCanvas().style.cursor = '';
});
// When pointer leaves stations layer, change to regular cursor for UI
map.on('mouseleave', 'stations', (event) => {
map.getCanvas().style.cursor = '';
});
// Find closest station to each mountain using turf API
map.on("click", (event) => {
const mountainFeatures = map.queryRenderedFeatures(event.point, {
layers: ["mountains"]
});
if (!mountainFeatures.length) {
return;
}
const mountainFeature = mountainFeatures[0];
// Get all features from the station layers
var features = map.querySourceFeatures("stations");
//Create the turf collection to conform with the turf format
const stations = turf.featureCollection(features);
const nearestStation = turf.nearest(mountainFeature, stations);
if (nearestStation === null) return;
map.getSource("nearest-station").setData({
type: "FeatureCollection",
features: [nearestStation]
});
if (map.getLayer("nearest-station")) {
map.removeLayer("nearest-station");
}
// Display circle around the nearest station
map.addLayer(
{
id: "nearest-station",
type: "circle",
source: "nearest-station",
paint: {
"circle-radius": 20,
"circle-opacity": 0.5,
"circle-color": "#006d2c",
"circle-stroke-color": '#ffffff',
"circle-stroke-width": 1
}
},
"stations"
);
});
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.