added htlm file
This commit is contained in:
395
index.html
Normal file
395
index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user