302 lines
11 KiB
HTML
302 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Runner Map</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<link href="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css" rel="stylesheet">
|
|
<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-directions/v4.3.1/mapbox-gl-directions.css" rel="stylesheet">
|
|
<script src="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js"></script>
|
|
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-directions/v4.3.1/mapbox-gl-directions.js"></script>
|
|
<style>
|
|
body, html {
|
|
margin: 0;
|
|
padding: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
#map {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
}
|
|
#header {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 1000;
|
|
}
|
|
#header img {
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
#controls {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: white;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
z-index: 1000;
|
|
max-height: 80%;
|
|
overflow-y: auto;
|
|
}
|
|
#controls.collapsed {
|
|
display: none;
|
|
}
|
|
#toggle-controls {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: white;
|
|
padding: 8px;
|
|
border-radius: 50%;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
z-index: 1000;
|
|
}
|
|
@media (max-width: 768px) {
|
|
#controls {
|
|
width: 80%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100">
|
|
<div id="map"></div>
|
|
<div id="header">
|
|
<img src="https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.cajagranadafundacion.es%2Fwp-content%2Fuploads%2F2017%2F06%2Feventoa-forrest-gump.jpg&f=1&nofb=1&ipt=07c1b21934bbb58f00a8d5c7ac71c72d2a737c2b37857ffddfd3e7686ecebdd7&ipo=images" alt="Forrest Gump">
|
|
</div>
|
|
<div id="controls" class="space-y-4">
|
|
<label class="block">
|
|
<span class="text-gray-700">Use Current Location:</span>
|
|
<input type="checkbox" id="use-current-location" class="mt-1">
|
|
</label>
|
|
<label class="block">
|
|
<span class="text-gray-700">Starting Point (latitude, longitude):</span>
|
|
<input type="text" id="starting-point" placeholder="e.g., 37.7749,-122.4194" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" disabled>
|
|
</label>
|
|
<label class="block">
|
|
<span class="text-gray-700">Distance (km):</span>
|
|
<input type="number" id="distance" value="5" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
|
</label>
|
|
<label class="block">
|
|
<span class="text-gray-700">Round Trip:</span>
|
|
<input type="checkbox" id="round-trip" class="mt-1">
|
|
</label>
|
|
<label class="block">
|
|
<span class="text-gray-700">Hills:</span>
|
|
<input type="checkbox" id="hills" class="mt-1">
|
|
</label>
|
|
<label class="block">
|
|
<span class="text-gray-700">Convenience Stores:</span>
|
|
<input type="checkbox" id="stores" class="mt-1">
|
|
</label>
|
|
<button id="generate-route" class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition duration-300">Generate Route</button>
|
|
<button id="open-google-maps" class="w-full bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-700 transition duration-300">Open in Google Maps</button>
|
|
<button id="open-apple-maps" class="w-full bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-700 transition duration-300">Open in Apple Maps</button>
|
|
</div>
|
|
<button id="toggle-controls" class="bg-gray-800 text-white">≡</button>
|
|
<script>
|
|
mapboxgl.accessToken = 'pk.eyJ1IjoiamVubnlmcm9tdGhhYmxvY2siLCJhIjoiY2x5dWQwa3dwMTI1aTJscHVtOHR1ZmUxdyJ9.NZSYIPeI4A96tF6sWSIRvQ';
|
|
|
|
let startPoint = [];
|
|
let destinationPoint = [];
|
|
let map;
|
|
let directions;
|
|
|
|
document.getElementById('use-current-location').addEventListener('change', (event) => {
|
|
const useCurrentLocation = event.target.checked;
|
|
document.getElementById('starting-point').disabled = useCurrentLocation;
|
|
if (useCurrentLocation) {
|
|
navigator.geolocation.getCurrentPosition((position) => {
|
|
const { latitude, longitude } = position.coords;
|
|
startPoint = [longitude, latitude];
|
|
});
|
|
}
|
|
});
|
|
|
|
document.getElementById('generate-route').addEventListener('click', async () => {
|
|
const useCurrentLocation = document.getElementById('use-current-location').checked;
|
|
if (!useCurrentLocation) {
|
|
const startPointInput = document.getElementById('starting-point').value;
|
|
if (startPointInput) {
|
|
startPoint = startPointInput.split(',').map(Number);
|
|
} else {
|
|
alert("Please enter a valid starting point.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const distance = document.getElementById('distance').value * 1000; // Convert to meters
|
|
const roundTrip = document.getElementById('round-trip').checked;
|
|
const hills = document.getElementById('hills').checked;
|
|
const stores = document.getElementById('stores').checked;
|
|
|
|
if (!map) {
|
|
map = new mapboxgl.Map({
|
|
container: 'map',
|
|
style: 'mapbox://styles/mapbox/outdoors-v11',
|
|
center: startPoint,
|
|
zoom: 14
|
|
});
|
|
|
|
map.on('load', () => {
|
|
map.addLayer({
|
|
'id': 'hillshading',
|
|
'source': {
|
|
'type': 'raster-dem',
|
|
'url': 'mapbox://mapbox.terrain-rgb'
|
|
},
|
|
'type': 'hillshade'
|
|
});
|
|
|
|
new mapboxgl.Marker()
|
|
.setLngLat(startPoint)
|
|
.addTo(map);
|
|
|
|
directions = new MapboxDirections({
|
|
accessToken: mapboxgl.accessToken,
|
|
unit: 'metric',
|
|
profile: 'mapbox/walking'
|
|
});
|
|
|
|
map.addControl(directions, 'top-left');
|
|
});
|
|
}
|
|
|
|
let routeFound = false;
|
|
let attempts = 0;
|
|
const maxAttempts = 10;
|
|
|
|
while (!routeFound && attempts < maxAttempts) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dx = Math.cos(angle) * (roundTrip ? distance / 2 : distance);
|
|
const dy = Math.sin(angle) * (roundTrip ? distance / 2 : distance);
|
|
const waypoint = [startPoint[0] + dx / (111320 * Math.cos(startPoint[1] * Math.PI / 180)), startPoint[1] + dy / 110540];
|
|
|
|
try {
|
|
if (stores) {
|
|
const store = await findNearestStore(startPoint);
|
|
if (store) {
|
|
destinationPoint = store.center;
|
|
console.log(`Nearest store found at: ${destinationPoint}`);
|
|
} else {
|
|
destinationPoint = waypoint;
|
|
console.log(`No store found, using random waypoint: ${destinationPoint}`);
|
|
}
|
|
} else {
|
|
destinationPoint = waypoint;
|
|
}
|
|
|
|
directions.setOrigin(startPoint);
|
|
directions.setDestination(destinationPoint);
|
|
|
|
const route = await new Promise((resolve, reject) => {
|
|
directions.on('route', (e) => {
|
|
if (e.route.length > 0) {
|
|
resolve(e.route[0]);
|
|
} else {
|
|
reject(new Error('No route found'));
|
|
}
|
|
});
|
|
});
|
|
|
|
if (route.distance >= distance * 0.8 && route.distance <= distance * 1.2) {
|
|
routeFound = true;
|
|
drawRoute(route);
|
|
console.log(`Route distance: ${route.distance} meters`);
|
|
} else {
|
|
console.log("Route distance does not meet the requirement. Adjusting...");
|
|
attempts++;
|
|
}
|
|
} catch (error) {
|
|
console.log("Error finding route: ", error);
|
|
attempts++;
|
|
}
|
|
}
|
|
|
|
if (!routeFound) {
|
|
alert("Unable to find a suitable route. Please try again.");
|
|
}
|
|
});
|
|
|
|
function drawRoute(route) {
|
|
const coordinates = route.geometry.coordinates;
|
|
if (map.getLayer('route')) {
|
|
map.getSource('route').setData({
|
|
'type': 'Feature',
|
|
'geometry': {
|
|
'type': 'LineString',
|
|
'coordinates': coordinates
|
|
}
|
|
});
|
|
} else {
|
|
map.addLayer({
|
|
'id': 'route',
|
|
'type': 'line',
|
|
'source': {
|
|
'type': 'geojson',
|
|
'data': {
|
|
'type': 'Feature',
|
|
'geometry': {
|
|
'type': 'LineString',
|
|
'coordinates': coordinates
|
|
}
|
|
}
|
|
},
|
|
'layout': {
|
|
'line-join': 'round',
|
|
'line-cap': 'round'
|
|
},
|
|
'paint': {
|
|
'line-color': '#3887be',
|
|
'line-width': 5,
|
|
'line-opacity': 0.75
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async function findNearestStore(startPoint) {
|
|
const response = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/convenience%20store.json?proximity=${startPoint[0]},${startPoint[1]}&access_token=${mapboxgl.accessToken}`);
|
|
const data = await response.json();
|
|
return data.features[0];
|
|
}
|
|
|
|
document.getElementById('open-google-maps').addEventListener('click', () => {
|
|
if (startPoint.length === 0 || destinationPoint.length === 0) {
|
|
alert("Please generate a route first.");
|
|
return;
|
|
}
|
|
const googleMapsUrl = `https://www.google.com/maps/dir/?api=1&origin=${startPoint[1]},${startPoint[0]}&destination=${destinationPoint[1]},${destinationPoint[0]}&travelmode=walking`;
|
|
window.open(googleMapsUrl, '_blank');
|
|
});
|
|
|
|
document.getElementById('open-apple-maps').addEventListener('click', () => {
|
|
if (startPoint.length === 0 || destinationPoint.length === 0) {
|
|
alert("Please generate a route first.");
|
|
return;
|
|
}
|
|
const appleMapsUrl = `http://maps.apple.com/?saddr=${startPoint[1]},${startPoint[0]}&daddr=${destinationPoint[1]},${destinationPoint[0]}&dirflg=w`;
|
|
window.open(appleMapsUrl, '_blank');
|
|
});
|
|
|
|
document.getElementById('toggle-controls').addEventListener('click', () => {
|
|
const controls = document.getElementById('controls');
|
|
if (controls.classList.contains('collapsed')) {
|
|
controls.classList.remove('collapsed');
|
|
} else {
|
|
controls.classList.add('collapsed');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|