![[14. Mapty App - OOP, Geolocation, External Libraries, and More!-1664033048247.jpeg]]
![[14. Mapty App - OOP, Geolocation, External Libraries, and More!-1664033068404.jpeg]]
![[14. Mapty App - OOP, Geolocation, External Libraries, and More!-1664033088216.jpeg]]
![[14. Mapty App - OOP, Geolocation, External Libraries, and More!-1664019394459.jpeg]]
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
function (position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
const map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
L.marker(coords)
.addTo(map)
.bindPopup('A pretty CSS3 popup.<br> Easily customizable.')
.openPopup();
},
function () {
alert(`Couldn't get the Location`);
}
);
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
function (position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
const map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
map.on('click', function (mapEvent) {
const { lat, lng } = mapEvent.latlng;
L.marker([lat, lng])
.addTo(map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: 'running-popup',
})
)
.setPopupContent('Workout')
.openPopup();
});
},
function () {
alert(`Couldn't get the Location`);
}
);
let map, mapEvent;
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
function (position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// Handling click on map
map.on('click', function (mapE) {
mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
});
},
function () {
alert(`Couldn't get the Location`);
}
);
form.addEventListener('submit', function (e) {
e.preventDefault();
// clear input fields
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
'';
// Display Marker
const { lat, lng } = mapEvent.latlng;
L.marker([lat, lng])
.addTo(map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: 'running-popup',
})
)
.setPopupContent('Workout')
.openPopup();
});
inputType.addEventListener('change', function () {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
});
![[14. Mapty App - OOP, Geolocation, External Libraries, and More!-1664166813408.jpeg]]
class App {
#map;
#mapEvent;
constructor() {
this._getPosition();
form.addEventListener('submit', this._newWorkout.bind(this));
inputType.addEventListener('change', this._toggleElevationField);
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert(`Couldn't get the Location`);
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
this.#map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling click on map
this.#map.on('click', this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
}
_toggleElevationField() {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
}
_newWorkout(e) {
e.preventDefault();
// clear input fields
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
'';
// Display Marker
const { lat, lng } = this.#mapEvent.latlng;
L.marker([lat, lng])
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: 'running-popup',
})
)
.setPopupContent('Workout')
.openPopup();
}
}
const app = new App();
class Workout {
date = new Date();
id = (Date.now() + '').slice(-10);
constructor(coords, distance, duration) {
this.coords = coords; // [lat, lng]
this.distance = distance; // in Km
this.duration = duration; // in min
}
}
class Running extends Workout {
constructor(coords, distance, duration, cadence) {
super(coords, distance, duration);
this.cadence = cadence;
this.clacPace();
}
clacPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends Workout {
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
this.calcSpeed();
}
calcSpeed() {
// km/hr
this.speed = this.distance / this.duration / 60;
return this.speed;
}
}
console.log(run1, cycling1);
class App {
#map;
#mapEvent;
constructor() {
this._getPosition();
form.addEventListener('submit', this._newWorkout.bind(this));
inputType.addEventListener('change', this._toggleElevationField);
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert(`Couldn't get the Location`);
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
this.#map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling click on map
this.#map.on('click', this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
}
_toggleElevationField() {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
}
_newWorkout(e) {
e.preventDefault();
// clear input fields
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
'';
// Display Marker
const { lat, lng } = this.#mapEvent.latlng;
L.marker([lat, lng])
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: 'running-popup',
})
)
.setPopupContent('Workout')
.openPopup();
}
}
const app = new App();
class Workout {
date = new Date();
id = (Date.now() + '').slice(-10);
constructor(coords, distance, duration) {
this.coords = coords; // [lat, lng]
this.distance = distance; // in Km
this.duration = duration; // in min
}
}
class Running extends Workout {
type = 'running';
constructor(coords, distance, duration, cadence) {
super(coords, distance, duration);
this.cadence = cadence;
this.clacPace();
}
clacPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends Workout {
type = 'cycling';
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
this.calcSpeed();
}
calcSpeed() {
// km/hr
this.speed = this.distance / this.duration / 60;
return this.speed;
}
}
///////////////////////////////////////
// APPLICATION ARCHITECTURE
const form = document.querySelector('.form');
const containerWorkouts = document.querySelector('.workouts');
const inputType = document.querySelector('.form__input--type');
const inputDistance = document.querySelector('.form__input--distance');
const inputDuration = document.querySelector('.form__input--duration');
const inputCadence = document.querySelector('.form__input--cadence');
const inputElevation = document.querySelector('.form__input--elevation');
class App {
#map;
#mapEvent;
#workouts = [];
constructor() {
this._getPosition();
form.addEventListener('submit', this._newWorkout.bind(this));
inputType.addEventListener('change', this._toggleElevationField);
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert(`Couldn't get the Location`);
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
this.#map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling click on map
this.#map.on('click', this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
}
_toggleElevationField() {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
}
_newWorkout(e) {
e.preventDefault();
const validInputs = (...inputs) =>
inputs.every(inp => Number.isFinite(inp));
const allPositives = (...inputs) => inputs.every(inp => inp > 0);
// Get the data from the form
const type = inputType.value;
// + sign is to convert string into Number
const distance = +inputDistance.value;
const duration = +inputDuration.value;
const { lat, lng } = this.#mapEvent.latlng;
let workout;
// If workout running, creacte running object
if (type == 'running') {
const cadence = +inputCadence.value;
console.log(cadence, distance, duration);
// Check if the data is valid
if (
// !Number.isFinite(distance) ||
// !Number.isFinite(duration) ||
// !Number.isFinite(cadence)
!validInputs(distance, duration, cadence) ||
!allPositives(distance, duration, cadence)
)
return alert('Input have to be positive numbers');
workout = new Running([lat, lng], distance, duration, cadence);
}
// If workout cycling, creacte cycling object
if (type == 'cycling') {
const elevation = +inputElevation.value;
if (
!validInputs(distance, duration, elevation) ||
!allPositives(distance, duration)
)
return alert('Input have to be positive numbers');
workout = new Cycling([lat, lng], distance, duration, elevation);
}
// Add new object to workout array
this.#workouts.push(workout);
console.log(workout);
// Render workout on map as marker
this.renderWorkoutMarker(workout);
// clear input fields
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
'';
}
renderWorkoutMarker(workout) {
L.marker(workout.coords)
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: `${workout.type}-popup`,
})
)
.setPopupContent('workout')
.openPopup();
}
}
const app = new App();
'use strict';
class Workout {
date = new Date();
id = (new Date() + '').slice(-10);
constructor(coords, distance, duration) {
this.coords = coords; // [lat, lng]
this.distance = distance; // in Km
this.duration = duration; // in min
}
_setDescription() {
// prettier-ignore
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
months[this.date.getMonth()]
} ${this.date.getDate()}`;
}
}
class Running extends Workout {
type = 'running';
constructor(coords, distance, duration, cadence) {
super(coords, distance, duration);
this.cadence = cadence;
this.clacPace();
this._setDescription();
}
clacPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends Workout {
type = 'cycling';
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
this.calcSpeed();
this._setDescription();
}
calcSpeed() {
// km/hr
this.speed = this.distance / this.duration / 60;
return this.speed;
}
}
///////////////////////////////////////
// APPLICATION ARCHITECTURE
const form = document.querySelector('.form');
const containerWorkouts = document.querySelector('.workouts');
const inputType = document.querySelector('.form__input--type');
const inputDistance = document.querySelector('.form__input--distance');
const inputDuration = document.querySelector('.form__input--duration');
const inputCadence = document.querySelector('.form__input--cadence');
const inputElevation = document.querySelector('.form__input--elevation');
class App {
#map;
#mapEvent;
#workouts = [];
constructor() {
this._getPosition();
form.addEventListener('submit', this._newWorkout.bind(this));
inputType.addEventListener('change', this._toggleElevationField);
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert(`Couldn't get the Location`);
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
this.#map = L.map('map').setView(coords, 15);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling click on map
this.#map.on('click', this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
}
_hideForm() {
// Empty inputs
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
'';
form.style.display = 'none';
form.classList.add('hidden');
setTimeout(() => (form.style.display = 'grid'), 1000);
}
_toggleElevationField() {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
}
_newWorkout(e) {
e.preventDefault();
const validInputs = (...inputs) =>
inputs.every(inp => Number.isFinite(inp));
const allPositives = (...inputs) => inputs.every(inp => inp > 0);
// Get the data from the form
const type = inputType.value;
// + sign is to convert string into Number
const distance = +inputDistance.value;
const duration = +inputDuration.value;
const { lat, lng } = this.#mapEvent.latlng;
let workout;
// If workout running, creacte running object
if (type == 'running') {
const cadence = +inputCadence.value;
console.log(cadence, distance, duration);
// Check if the data is valid
if (
// !Number.isFinite(distance) ||
// !Number.isFinite(duration) ||
// !Number.isFinite(cadence)
!validInputs(distance, duration, cadence) ||
!allPositives(distance, duration, cadence)
)
return alert('Input have to be positive numbers');
workout = new Running([lat, lng], distance, duration, cadence);
}
// If workout cycling, creacte cycling object
if (type == 'cycling') {
const elevation = +inputElevation.value;
if (
!validInputs(distance, duration, elevation) ||
!allPositives(distance, duration)
)
return alert('Input have to be positive numbers');
workout = new Cycling([lat, lng], distance, duration, elevation);
}
// Add new object to workout array
this.#workouts.push(workout);
console.log(workout);
// Render workout on map as marker
this._renderWorkoutMarker(workout);
// Render workout on list
this._renderWorkout(workout);
// clear input fields
this._hideForm();
}
_renderWorkoutMarker(workout) {
L.marker(workout.coords)
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: `${workout.type}-popup`,
})
)
.setPopupContent(
`${workout.type === 'running' ? '🏃♂️' : '🚴🏻♀️'} ${workout.description}`
)
.openPopup();
}
_renderWorkout(workout) {
let html = `
<li class="workout workout--${workout.type}" data-id="${workout.id}">
<h2 class="workout__title">${workout.description}</h2>
<div class="workout__details">
<span class="workout__icon">${
workout.type === 'running' ? '🏃♂️' : '🚴🏻♀️'
}</span>
<span class="workout__value">${workout.distance}</span>
<span class="workout__unit">km</span>
</div>
<div class="workout__details">
<span class="workout__icon">⏱</span>
<span class="workout__value">${workout.duration}</span>
<span class="workout__unit">min</span>
</div>`;
if (workout.type === 'running')
html += `
<div class="workout__details">
<span class="workout__icon">⚡️</span>
<span class="workout__value">${workout.pace.toFixed(1)}</span>
<span class="workout__unit">min/km</span>
</div>
<div class="workout__details">
<span class="workout__icon">🦶🏼</span>
<span class="workout__value">${workout.cadence}</span>
<span class="workout__unit">spm</span>
</div>
</li>`;
if (workout.type === 'cycling')
html += `
<div class="workout__details">
<span class="workout__icon">⚡️</span>
<span class="workout__value">${workout.speed.toFixed(1)}</span>
<span class="workout__unit">km/h</span>
</div>
<div class="workout__details">
<span class="workout__icon">⛰</span>
<span class="workout__value">${workout.elevationGain}</span>
<span class="workout__unit">m</span>
</div>
`;
form.insertAdjacentHTML('afterend', html);
}
}
const app = new App();
'use strict';
class Workout {
date = new Date();
id = (Date.now() + '').slice(-10);
clicks = 0;
constructor(coords, distance, duration) {
this.coords = coords; // [lat, lng]
this.distance = distance; // in Km
this.duration = duration; // in min
}
_setDescription() {
// prettier-ignore
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
months[this.date.getMonth()]
} ${this.date.getDate()}`;
}
click() {
this.clicks++;
}
}
class Running extends Workout {
type = 'running';
constructor(coords, distance, duration, cadence) {
super(coords, distance, duration);
this.cadence = cadence;
this.clacPace();
this._setDescription();
}
clacPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends Workout {
type = 'cycling';
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
this.calcSpeed();
this._setDescription();
}
calcSpeed() {
// km/hr
this.speed = this.distance / this.duration / 60;
return this.speed;
}
}
///////////////////////////////////////
// APPLICATION ARCHITECTURE
const form = document.querySelector('.form');
const containerWorkouts = document.querySelector('.workouts');
const inputType = document.querySelector('.form__input--type');
const inputDistance = document.querySelector('.form__input--distance');
const inputDuration = document.querySelector('.form__input--duration');
const inputCadence = document.querySelector('.form__input--cadence');
const inputElevation = document.querySelector('.form__input--elevation');
class App {
#map;
#mapZoomLevel = 13;
#mapEvent;
#workouts = [];
constructor() {
this._getPosition();
form.addEventListener('submit', this._newWorkout.bind(this));
inputType.addEventListener('change', this._toggleElevationField);
containerWorkouts.addEventListener('click', this._moveToPopup.bind(this));
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert(`Couldn't get the Location`);
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
const coords = [latitude, longitude];
this.#map = L.map('map').setView(coords, this.#mapZoomLevel);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling click on map
this.#map.on('click', this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
}
_hideForm() {
// Empty inputs
inputDistance.value =
inputDuration.value =
inputCadence.value =
inputElevation.value =
'';
form.style.display = 'none';
form.classList.add('hidden');
setTimeout(() => (form.style.display = 'grid'), 1000);
}
_toggleElevationField() {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
}
_newWorkout(e) {
e.preventDefault();
const validInputs = (...inputs) =>
inputs.every(inp => Number.isFinite(inp));
const allPositives = (...inputs) => inputs.every(inp => inp > 0);
// Get the data from the form
const type = inputType.value;
// + sign is to convert string into Number
const distance = +inputDistance.value;
const duration = +inputDuration.value;
const { lat, lng } = this.#mapEvent.latlng;
let workout;
// If workout running, creacte running object
if (type == 'running') {
const cadence = +inputCadence.value;
console.log(cadence, distance, duration);
// Check if the data is valid
if (
// !Number.isFinite(distance) ||
// !Number.isFinite(duration) ||
// !Number.isFinite(cadence)
!validInputs(distance, duration, cadence) ||
!allPositives(distance, duration, cadence)
)
return alert('Input have to be positive numbers');
workout = new Running([lat, lng], distance, duration, cadence);
}
// If workout cycling, creacte cycling object
if (type == 'cycling') {
const elevation = +inputElevation.value;
if (
!validInputs(distance, duration, elevation) ||
!allPositives(distance, duration)
)
return alert('Input have to be positive numbers');
workout = new Cycling([lat, lng], distance, duration, elevation);
}
// Add new object to workout array
this.#workouts.push(workout);
console.log(workout);
// Render workout on map as marker
this._renderWorkoutMarker(workout);
// Render workout on list
this._renderWorkout(workout);
// clear input fields
this._hideForm();
}
_renderWorkoutMarker(workout) {
L.marker(workout.coords)
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: `${workout.type}-popup`,
})
)
.setPopupContent(
`${workout.type === 'running' ? '🏃♂️' : '🚴🏻♀️'} ${workout.description}`
)
.openPopup();
}
_renderWorkout(workout) {
let html = `
<li class="workout workout--${workout.type}" data-id="${workout.id}">
<h2 class="workout__title">${workout.description}</h2>
<div class="workout__details">
<span class="workout__icon">${
workout.type === 'running' ? '🏃♂️' : '🚴🏻♀️'
}</span>
<span class="workout__value">${workout.distance}</span>
<span class="workout__unit">km</span>
</div>
<div class="workout__details">
<span class="workout__icon">⏱</span>
<span class="workout__value">${workout.duration}</span>
<span class="workout__unit">min</span>
</div>`;
if (workout.type === 'running')
html += `
<div class="workout__details">
<span class="workout__icon">⚡️</span>
<span class="workout__value">${workout.pace.toFixed(1)}</span>
<span class="workout__unit">min/km</span>
</div>
<div class="workout__details">
<span class="workout__icon">🦶🏼</span>
<span class="workout__value">${workout.cadence}</span>
<span class="workout__unit">spm</span>
</div>
</li>`;
if (workout.type === 'cycling')
html += `
<div class="workout__details">
<span class="workout__icon">⚡️</span>
<span class="workout__value">${workout.speed.toFixed(1)}</span>
<span class="workout__unit">km/h</span>
</div>
<div class="workout__details">
<span class="workout__icon">⛰</span>
<span class="workout__value">${workout.elevationGain}</span>
<span class="workout__unit">m</span>
</div>
`;
form.insertAdjacentHTML('afterend', html);
}
_moveToPopup(e) {
const workoutEl = e.target.closest('.workout');
if (!workoutEl) return;
const workout = this.#workouts.find(
work => work.id === workoutEl.dataset.id
);
this.#map.setView(workout.coords, this.#mapZoomLevel, {
animate: true,
pan: {
duration: 1,
},
});
// using the public interface
workout.click();
}
}
const app = new App();
'use strict';
class Workout {
date = new Date();
id = (Date.now() + '').slice(-10);
clicks = 0;
constructor(coords, distance, duration) {
// this.date = ...
// this.id = ...
this.coords = coords; // [lat, lng]
this.distance = distance; // in km
this.duration = duration; // in min
}
_setDescription() {
// prettier-ignore
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
months[this.date.getMonth()]
} ${this.date.getDate()}`;
}
click() {
this.clicks++;
}
}
class Running extends Workout {
type = 'running';
constructor(coords, distance, duration, cadence) {
super(coords, distance, duration);
this.cadence = cadence;
this.calcPace();
this._setDescription();
}
calcPace() {
// min/km
this.pace = this.duration / this.distance;
return this.pace;
}
}
class Cycling extends Workout {
type = 'cycling';
constructor(coords, distance, duration, elevationGain) {
super(coords, distance, duration);
this.elevationGain = elevationGain;
// this.type = 'cycling';
this.calcSpeed();
this._setDescription();
}
calcSpeed() {
// km/h
this.speed = this.distance / (this.duration / 60);
return this.speed;
}
}
// const run1 = new Running([39, -12], 5.2, 24, 178);
// const cycling1 = new Cycling([39, -12], 27, 95, 523);
// console.log(run1, cycling1);
///////////////////////////////////////
// APPLICATION ARCHITECTURE
const form = document.querySelector('.form');
const containerWorkouts = document.querySelector('.workouts');
const inputType = document.querySelector('.form__input--type');
const inputDistance = document.querySelector('.form__input--distance');
const inputDuration = document.querySelector('.form__input--duration');
const inputCadence = document.querySelector('.form__input--cadence');
const inputElevation = document.querySelector('.form__input--elevation');
class App {
#map;
#mapZoomLevel = 13;
#mapEvent;
#workouts = [];
constructor() {
// Get user's position
this._getPosition();
// Get data from local storage
this._getLocalStorage();
// Attach event handlers
form.addEventListener('submit', this._newWorkout.bind(this));
inputType.addEventListener('change', this._toggleElevationField);
containerWorkouts.addEventListener('click', this._moveToPopup.bind(this));
}
_getPosition() {
if (navigator.geolocation)
navigator.geolocation.getCurrentPosition(
this._loadMap.bind(this),
function () {
alert('Could not get your position');
}
);
}
_loadMap(position) {
const { latitude } = position.coords;
const { longitude } = position.coords;
// console.log(`https://www.google.pt/maps/@${latitude},${longitude}`);
const coords = [latitude, longitude];
this.#map = L.map('map').setView(coords, this.#mapZoomLevel);
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(this.#map);
// Handling clicks on map
this.#map.on('click', this._showForm.bind(this));
}
_showForm(mapE) {
this.#mapEvent = mapE;
form.classList.remove('hidden');
inputDistance.focus();
}
_hideForm() {
// Empty inputs
inputDistance.value = inputDuration.value = inputCadence.value = inputElevation.value =
'';
form.style.display = 'none';
form.classList.add('hidden');
setTimeout(() => (form.style.display = 'grid'), 1000);
}
_toggleElevationField() {
inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
}
_newWorkout(e) {
const validInputs = (...inputs) =>
inputs.every(inp => Number.isFinite(inp));
const allPositive = (...inputs) => inputs.every(inp => inp > 0);
e.preventDefault();
// Get data from form
const type = inputType.value;
const distance = +inputDistance.value;
const duration = +inputDuration.value;
const { lat, lng } = this.#mapEvent.latlng;
let workout;
// If workout running, create running object
if (type === 'running') {
const cadence = +inputCadence.value;
// Check if data is valid
if (
// !Number.isFinite(distance) ||
// !Number.isFinite(duration) ||
// !Number.isFinite(cadence)
!validInputs(distance, duration, cadence) ||
!allPositive(distance, duration, cadence)
)
return alert('Inputs have to be positive numbers!');
workout = new Running([lat, lng], distance, duration, cadence);
}
// If workout cycling, create cycling object
if (type === 'cycling') {
const elevation = +inputElevation.value;
if (
!validInputs(distance, duration, elevation) ||
!allPositive(distance, duration)
)
return alert('Inputs have to be positive numbers!');
workout = new Cycling([lat, lng], distance, duration, elevation);
}
// Add new object to workout array
this.#workouts.push(workout);
// Render workout on map as marker
this._renderWorkoutMarker(workout);
// Render workout on list
this._renderWorkout(workout);
// Hide form + clear input fields
this._hideForm();
// Set local storage to all workouts
this._setLocalStorage();
}
_renderWorkoutMarker(workout) {
L.marker(workout.coords)
.addTo(this.#map)
.bindPopup(
L.popup({
maxWidth: 250,
minWidth: 100,
autoClose: false,
closeOnClick: false,
className: `${workout.type}-popup`,
})
)
.setPopupContent(
`${workout.type === 'running' ? '🏃♂️' : '🚴♀️'} ${workout.description}`
)
.openPopup();
}
_renderWorkout(workout) {
let html = `
<li class="workout workout--${workout.type}" data-id="${workout.id}">
<h2 class="workout__title">${workout.description}</h2>
<div class="workout__details">
<span class="workout__icon">${
workout.type === 'running' ? '🏃♂️' : '🚴♀️'
}</span>
<span class="workout__value">${workout.distance}</span>
<span class="workout__unit">km</span>
</div>
<div class="workout__details">
<span class="workout__icon">⏱</span>
<span class="workout__value">${workout.duration}</span>
<span class="workout__unit">min</span>
</div>
`;
if (workout.type === 'running')
html += `
<div class="workout__details">
<span class="workout__icon">⚡️</span>
<span class="workout__value">${workout.pace.toFixed(1)}</span>
<span class="workout__unit">min/km</span>
</div>
<div class="workout__details">
<span class="workout__icon">🦶🏼</span>
<span class="workout__value">${workout.cadence}</span>
<span class="workout__unit">spm</span>
</div>
</li>
`;
if (workout.type === 'cycling')
html += `
<div class="workout__details">
<span class="workout__icon">⚡️</span>
<span class="workout__value">${workout.speed.toFixed(1)}</span>
<span class="workout__unit">km/h</span>
</div>
<div class="workout__details">
<span class="workout__icon">⛰</span>
<span class="workout__value">${workout.elevationGain}</span>
<span class="workout__unit">m</span>
</div>
</li>
`;
form.insertAdjacentHTML('afterend', html);
}
_moveToPopup(e) {
// BUGFIX: When we click on a workout before the map has loaded, we get an error. But there is an easy fix:
if (!this.#map) return;
const workoutEl = e.target.closest('.workout');
if (!workoutEl) return;
const workout = this.#workouts.find(
work => work.id === workoutEl.dataset.id
);
this.#map.setView(workout.coords, this.#mapZoomLevel, {
animate: true,
pan: {
duration: 1,
},
});
// using the public interface
// workout.click();
}
_setLocalStorage() {
localStorage.setItem('workouts', JSON.stringify(this.#workouts));
}
_getLocalStorage() {
const data = JSON.parse(localStorage.getItem('workouts'));
if (!data) return;
this.#workouts = data;
this.#workouts.forEach(work => {
this._renderWorkout(work);
});
}
reset() {
localStorage.removeItem('workouts');
location.reload();
}
}
const app = new App();
- 10 Additional Features Ideas: Challenges
- Ability to edit a workout;
- Ability to delete a workout;
- Ability to delete all workouts;
- Ability to sort workouts by a certain field (e.g. distance);
- Re-build Running and Cycling objects coming from Local Storage; More realistic error and confirmation messages;
- Ability to position the map to show all workouts [very hard];
- Ability to draw lines and shapes instead of just points [very hard];
- Geocode location from coordinates ("Run in Faro, Portugal") [only after asynchronous JavaScript section];
- Display weather data for workout time and place [only after asynchronous JavaScript section].