added htlm file

This commit is contained in:
2025-08-16 20:23:44 +01:00
parent 26fabec365
commit d664bfe4b5

395
index.html Normal file
View File

@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watford Traffic Checker with Map</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- TomTom Maps SDK CSS -->
<link rel='stylesheet' type='text/css' href='https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps.css'>
<style>
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(-45deg, #667eea, #853dce, #ed49ff, #3b3ef8, #4facfe, #00f2fe);
background-size: 400% 400%;
animation: gradientFlow 60s ease infinite;
}
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
25% {
background-position: 100% 50%;
}
50% {
background-position: 100% 100%;
}
75% {
background-position: 0% 100%;
}
100% {
background-position: 0% 50%;
}
}
/* Main container sizing for 800x500 constraint */
.main-container {
width: 800px;
height: 500px;
max-width: 800px;
max-height: 500px;
backdrop-filter: blur(20px) saturate(180%);
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
/* Set a height for the map container */
#map {
height: 280px;
width: 100%;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* Map skeleton loader styles */
.map-skeleton {
height: 280px;
width: 100%;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.8);
font-size: 0.875rem;
font-weight: 500;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
}
.map-skeleton::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
/* Traffic dial styling */
.traffic-dial {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 50%;
padding: 8px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
/* Ensure the map container parent has proper dimensions */
.map-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* Force horizontal layout with better spacing */
.side-by-side {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 2rem;
height: 320px;
}
.dial-section {
flex-shrink: 0;
width: 220px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Custom button styling */
.custom-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transform: scale(1);
width: 100%;
max-width: 200px;
min-height: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
}
.custom-button:hover:not(:disabled) {
transform: translateY(-1px) scale(1.02);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
.custom-button:active {
transform: translateY(0) scale(0.98);
transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1);
}
.custom-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: scale(1);
}
/* Status message styling */
.status-text {
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 flex items-center justify-center min-h-screen p-4">
<div class="main-container mx-auto shadow-2xl p-6">
<div class="text-center mb-6">
<h1 class="text-2xl font-bold bg-gradient-to-r from-white to-gray-200 bg-clip-text text-transparent">
Watford Traffic Score
</h1>
<p class="text-white mt-2 opacity-90">Real-time congestion from 0 (clear) to 1 (heavy)</p>
</div>
<!-- Main content container with dial and map side by side -->
<div class="side-by-side">
<!-- Left side: Traffic Dial and Controls -->
<div class="dial-section">
<!-- Traffic Dial -->
<div class="relative w-36 h-36 mx-auto mb-4 traffic-dial">
<svg class="w-full h-full" viewBox="0 0 120 120">
<!-- Background track -->
<circle cx="60" cy="60" r="54" fill="none" stroke-width="12" class="stroke-white opacity-30" />
<!-- Progress bar -->
<circle id="traffic-progress" cx="60" cy="60" r="54" fill="none" stroke-width="12"
class="stroke-green-400"
stroke-dasharray="339.292"
stroke-dashoffset="339.292"
transform="rotate(-90 60 60)" />
</svg>
<div id="traffic-value" class="absolute inset-0 flex items-center justify-center text-3xl font-bold text-white">
--
</div>
</div>
<p id="status-message" class="text-white mb-6 status-text font-medium opacity-90">Ready to load traffic data</p>
<!-- Get Traffic Data Button -->
<button id="refresh-button" class="w-full text-white font-semibold py-3 px-6 rounded-xl custom-button border-0">
<svg id="refresh-icon" class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M20 4l-4 4M4 20l4-4"></path></svg>
<span id="button-text">Get Traffic Data</span>
</button>
</div>
<!-- Right side: Map Container -->
<div class="map-container">
<div id="map"></div>
</div>
</div>
<div class="text-center mt-4">
<p class="text-xs text-white opacity-70">Powered by TomTom Traffic API & Maps SDK</p>
<p id="error-message" class="text-red-300 mt-1 text-sm font-medium"></p>
</div>
</div>
<!-- TomTom Maps SDK JS -->
<script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/6.25.0/maps/maps-web.min.js"></script>
<script>
// --- CONFIGURATION ---
// 1. Get your free API key from https://developer.tomtom.com/
// 2. Paste your API key below.
const apiKey = "MTn7ZkFleT4NHBBdokIPJWffwx2plFcI";
// Coordinates for Watford town centre
const watfordLat = 51.657;
const watfordLon = -0.395;
const watfordCoords = `${watfordLat},${watfordLon}`;
// --- DOM ELEMENTS ---
const trafficValueEl = document.getElementById('traffic-value');
const statusMessageEl = document.getElementById('status-message');
const errorMessageEl = document.getElementById('error-message');
const refreshButton = document.getElementById('refresh-button');
const buttonText = document.getElementById('button-text');
const refreshIcon = document.getElementById('refresh-icon');
const trafficProgress = document.getElementById('traffic-progress');
const mapContainer = document.getElementById('map');
const circleCircumference = 2 * Math.PI * 54; // 2 * pi * radius
let map = null; // Variable to hold the map instance
// --- FUNCTIONS ---
/**
* Shows a skeleton placeholder for the map on initial load.
*/
function showMapSkeleton() {
errorMessageEl.textContent = ''; // Clear previous errors
mapContainer.innerHTML = '<div class="map-skeleton">📍 Click "Get Traffic Data" to load map</div>';
statusMessageEl.textContent = "Ready to load traffic data";
}
/**
* Validates the API key and initializes the map and data fetch.
*/
function startApp() {
errorMessageEl.textContent = ''; // Clear previous errors
if (apiKey === "YOUR_API_KEY_HERE" || !apiKey) {
errorMessageEl.textContent = "ACTION REQUIRED: Please enter a valid TomTom API key in the script.";
statusMessageEl.textContent = "API Key Missing";
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">Map requires an API key</div>';
return;
}
// First initialize the map, then fetch traffic data
initializeMap();
getTrafficData();
}
/**
* Initializes the TomTom map with traffic layers.
*/
function initializeMap() {
if (map) {
map.remove();
}
map = tt.map({
key: apiKey,
container: 'map',
center: [watfordLon, watfordLat],
zoom: 13,
stylesVisibility: {
trafficFlow: true,
trafficIncidents: true
}
});
map.addControl(new tt.NavigationControl());
}
/**
* Updates the UI for the traffic score dial.
* @param {number} score - The traffic score from 0 to 1.
*/
function updateUI(score) {
const dashOffset = circleCircumference * (1 - score);
trafficValueEl.textContent = score.toFixed(2);
trafficProgress.style.strokeDashoffset = dashOffset;
if (score < 0.3) {
statusMessageEl.textContent = "Traffic is flowing freely.";
trafficProgress.classList.remove('stroke-yellow-500', 'stroke-red-500');
trafficProgress.classList.add('stroke-green-500');
} else if (score < 0.7) {
statusMessageEl.textContent = "Moderate congestion.";
trafficProgress.classList.remove('stroke-green-500', 'stroke-red-500');
trafficProgress.classList.add('stroke-yellow-500');
} else {
statusMessageEl.textContent = "Heavy traffic reported.";
trafficProgress.classList.remove('stroke-green-500', 'stroke-yellow-500');
trafficProgress.classList.add('stroke-red-500');
}
}
/**
* Fetches traffic data for the score dial from the TomTom API.
*/
async function getTrafficData() {
statusMessageEl.textContent = 'Fetching data...';
trafficValueEl.textContent = '--';
refreshButton.disabled = true;
buttonText.textContent = 'Loading...';
refreshIcon.classList.add('animate-spin');
const apiUrl = `https://api.tomtom.com/traffic/services/4/flowSegmentData/absolute/10/json?point=${watfordCoords}&unit=KMPH&key=${apiKey}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
let errorDetails = response.statusText;
try {
const errorData = await response.json();
if (errorData && errorData.detailedError && errorData.detailedError.message) {
errorDetails = errorData.detailedError.message;
} else if (errorData && errorData.error && errorData.error.description) {
errorDetails = errorData.error.description;
}
} catch (e) {
console.log("Could not parse error response as JSON.");
}
throw new Error(`API Error: ${response.status} - ${errorDetails}`);
}
const data = await response.json();
if (data && data.flowSegmentData) {
const { currentSpeed, freeFlowSpeed } = data.flowSegmentData;
const trafficScore = freeFlowSpeed > 0 ? 1 - (currentSpeed / freeFlowSpeed) : 0;
const finalScore = Math.max(0, trafficScore);
updateUI(finalScore);
} else {
throw new Error("No traffic data found for this location.");
}
} catch (error) {
console.error("Failed to fetch traffic data:", error);
errorMessageEl.textContent = `Error: ${error.message}`;
statusMessageEl.textContent = 'Could not load data.';
} finally {
resetButtonState();
}
}
/**
* Resets the refresh button to its default state.
*/
function resetButtonState() {
refreshButton.disabled = false;
buttonText.textContent = 'Get Traffic Data';
refreshIcon.classList.remove('animate-spin');
}
// --- EVENT LISTENERS ---
refreshButton.addEventListener('click', startApp);
// Initial load - show skeleton placeholder only, no API calls
window.addEventListener('load', showMapSkeleton);
</script>
</body>
</html>