Last active
December 14, 2024 19:01
-
-
Save gterrill/80d3ca4441f75b574a7fc57da8737f88 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Live Vessel Location Map</title> | |
<!-- Leaflet CSS --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" /> | |
<style> | |
#map { | |
height: 500px; | |
width: 100%; | |
} | |
body { | |
margin: 0; | |
font-family: Arial, sans-serif; | |
} | |
.status { | |
margin: 10px 0; | |
padding: 10px; | |
border-radius: 4px; | |
} | |
.error { | |
background-color: #ffebee; | |
color: #c62828; | |
} | |
.success { | |
background-color: #e8f5e9; | |
color: #2e7d32; | |
} | |
</style> | |
<!-- Leaflet JavaScript --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script> | |
</head> | |
<body> | |
<div id="map"></div> | |
<div id="status" class="status"></div> | |
<script> | |
// Initialize the map centered on a default position | |
const map = L.map('map').fitWorld(); | |
const statusElement = document.getElementById('status'); | |
const WEATHER_API_KEY = '896c133431c58b12a20e426a944365ba'; | |
// Add OpenStreetMap tiles | |
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
maxZoom: 19, | |
attribution: '© OpenStreetMap contributors' | |
}); | |
// Add OpenSeaMap tiles | |
const seamapLayer = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', { | |
maxZoom: 18, | |
attribution: '© OpenSeaMap contributors' | |
}); | |
const esriLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { | |
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' | |
}); | |
// Create a base layers object for the layer control | |
const baseLayers = { | |
"OpenStreetMap": osmLayer | |
}; | |
// Create an overlay layers object for the layer control | |
const overlayLayers = { | |
"OpenSeaMap": seamapLayer, | |
"Satellite": esriLayer | |
}; | |
// Add the default layer to the map | |
osmLayer.addTo(map); | |
seamapLayer.addTo(map); | |
// Add layer control | |
L.control.layers(baseLayers, overlayLayers).addTo(map); | |
// Create a marker | |
const marker = L.marker([0, 0]).addTo(map); | |
// Function to convert temperature from Kelvin to Celsius | |
function kelvinToCelsius(kelvin) { | |
return (kelvin - 273.15).toFixed(1); | |
} | |
function degToCompass(num) { | |
var val = Math.floor((num / 22.5) + 0.5); | |
var arr = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]; | |
return arr[(val % 16)]; | |
} | |
function toDegreesMinutesAndSeconds(coordinate) { | |
const absolute = Math.abs(coordinate); | |
const degrees = Math.floor(absolute); | |
const minutesNotTruncated = (absolute - degrees) * 60; | |
const minutes = Math.floor(minutesNotTruncated); | |
const seconds = Math.floor((minutesNotTruncated - minutes) * 60); | |
return `${degrees}°${minutes}'${seconds}"` | |
} | |
function convertDMS(lat, lng) { | |
const latitude = toDegreesMinutesAndSeconds(lat); | |
const latitudeCardinal = lat >= 0 ? "N" : "S"; | |
const longitude = toDegreesMinutesAndSeconds(lng); | |
const longitudeCardinal = lng >= 0 ? "E" : "W"; | |
return `${latitude}${latitudeCardinal}, ${longitude}${longitudeCardinal}` | |
} | |
function fetchWeatherData(lat, lon) { | |
return fetch( | |
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${WEATHER_API_KEY}` | |
).then(response => { | |
if (!response.ok) { | |
throw new Error(`Weather API error! status: ${response.status}`); | |
} | |
return response.json(); | |
}).catch(error => { | |
console.error('Error fetching weather data:', error); | |
return null; | |
}); | |
} | |
function panNearestFeature(targetLat, targetLon) { | |
const radius = 100000; // 100 km | |
// Query Overpass API to find the nearest land (natural=coastline or landuse=residential, etc.) | |
const query = ` | |
[out:json][timeout:25]; | |
( | |
node(around:${radius}, ${targetLat}, ${targetLon})["harbour"]; | |
way(around:${radius}, ${targetLat}, ${targetLon})["harbour"]; | |
relation(around:${radius}, ${targetLat}, ${targetLon})["harbour"]; | |
node(around:${radius}, ${targetLat}, ${targetLon})["seamark"="harbour"]; | |
way(around:${radius}, ${targetLat}, ${targetLon})["seamark"="harbour"]; | |
relation(around:${radius}, ${targetLat}, ${targetLon})["seamark"="harbour"]; | |
); | |
out center; | |
`; | |
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`; | |
// Fetch Overpass data | |
fetch(url).then(response => response.json()) | |
.then(data => { | |
// Find the nearest element | |
const targetPoint = L.latLng(targetLat, targetLon); | |
let nearestFeature = null; | |
let minDistance = Infinity; | |
data.elements.forEach(element => { | |
let featurePoint = null; | |
if (element.center) { | |
featurePoint = L.latLng(element.center.lat, element.center.lon); | |
} else { | |
featurePoint = L.latLng(element.lat, element.lon); | |
} | |
element.latlon = featurePoint; | |
const distance = targetPoint.distanceTo(featurePoint); | |
if (distance < minDistance) { | |
minDistance = distance; | |
nearestFeature = element; | |
} | |
}); | |
if (nearestFeature) { | |
// Pan the map to show nearest feature | |
map.fitBounds([[targetLat, targetLon], [nearestFeature.latlon.lat, nearestFeature.latlon.lng]]); | |
} else { | |
const circle = L.circle([targetLat, targetLon], radius).addTo(map); | |
const result = circle.getBounds(); | |
circle.removeFrom(map); | |
map.fitBounds(result); | |
} | |
}).catch(error => { | |
console.error('Error querying Overpass API:', error); | |
return null; | |
}); | |
} | |
// Function to update marker position | |
function updateMarkerPosition(lat, lon, vesselInfo = {}) { | |
marker.setLatLng([lat, lon]); | |
// map.setView([lat, lon], 10); | |
panNearestFeature(lat, lon); | |
// Fetch weather data | |
let weatherHtml = '<div class="weather-info">Weather data unavailable</div>'; | |
// Fetch weather data | |
fetchWeatherData(lat, lon).then(weatherData => { | |
let weatherHtml = '<div class="weather-info">Weather data unavailable</div>'; | |
if (weatherData) { | |
weatherHtml = ` | |
<div class="weather-info"> | |
<br><strong>Weather Conditions</strong><br> | |
Temperature: ${kelvinToCelsius(weatherData.main.temp)}°C<br> | |
Feels like: ${kelvinToCelsius(weatherData.main.feels_like)}°C<br> | |
Humidity: ${weatherData.main.humidity}%<br> | |
Winds: ${degToCompass(weatherData.wind.deg)} ${(weatherData.wind.speed * 1.943884).toFixed(1)} kts<br> | |
Conditions: ${weatherData.weather[0].description}<br> | |
Pressure: ${weatherData.main.pressure} hPa | |
</div> | |
`; | |
} | |
const popupContent = ` | |
<div class="popup-content"> | |
<strong>MV Pikorua</strong><br> | |
Position: ${convertDMS(lat, lon)}<br> | |
${Object.entries(vesselInfo) | |
.map(([key, value]) => `${key}: ${value}`) | |
.join('<br>')} | |
${weatherHtml} | |
</div> | |
`; | |
marker.bindPopup(popupContent); | |
}); | |
} | |
// Function to fetch vessel data | |
async function fetchVesselData() { | |
try { | |
statusElement.textContent = 'Fetching vessel data...'; | |
statusElement.className = 'status'; | |
const response = await fetch('https://corsproxy.io/?https%3A%2F%2Fwww.marinetraffic.com%2Fmap%2Fgetvesseljson%2Fmmsi%3A518999323', { | |
method: 'GET', | |
headers: { | |
'Accept': 'application/json' | |
} | |
}); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
const dateOptions = { | |
weekday: "short", | |
year: "numeric", | |
month: "2-digit", | |
day: "numeric", | |
hour: 'numeric', | |
minute: 'numeric', | |
second: 'numeric', | |
timeZoneName: 'short' | |
}; | |
// Update marker with the new position | |
updateMarkerPosition(data.LAT, data.LON, { | |
Speed: `${data.SPEED} kts`, | |
Course: `${data.COURSE}°`, | |
Received: new Date(`${data.TIMESTAMP}Z`).toLocaleDateString(undefined, dateOptions) | |
}); | |
statusElement.textContent = 'Vessel position and weather updated successfully'; | |
statusElement.className = 'status success'; | |
} catch (error) { | |
console.error('Error fetching vessel data:', error); | |
statusElement.textContent = `Error fetching vessel data: ${error.message}`; | |
statusElement.className = 'status error'; | |
} | |
} | |
// Fetch data immediately on page load | |
fetchVesselData(); | |
// Update position every 5 minutes | |
setInterval(fetchVesselData, 5 * 60000); | |
// Add a manual refresh button | |
const refreshButton = document.createElement('button'); | |
refreshButton.textContent = 'Refresh Position'; | |
refreshButton.style.margin = '10px 0'; | |
refreshButton.onclick = fetchVesselData; | |
document.body.insertBefore(refreshButton, document.getElementById('map')); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment