Skip to content

Instantly share code, notes, and snippets.

@gterrill
Last active December 14, 2024 19:01
Show Gist options
  • Save gterrill/80d3ca4441f75b574a7fc57da8737f88 to your computer and use it in GitHub Desktop.
Save gterrill/80d3ca4441f75b574a7fc57da8737f88 to your computer and use it in GitHub Desktop.
<!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 &copy; Esri &mdash; 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