Created
June 19, 2025 12:46
-
-
Save youssef22222/86a55b39f0aefc223a7796bac78a97f6 to your computer and use it in GitHub Desktop.
Speed Violation Map - Testing Live Alerts - 1750337202
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> | |
<head> | |
<title>Speed Violation Path - Testing Live Alerts</title> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
<style> | |
html, body { | |
height: 100%; | |
margin: 0; | |
padding: 0; | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; | |
} | |
#map { | |
height: 100%; | |
position: relative; | |
} | |
/* Enhanced marker styles */ | |
.number-icon { | |
background-color: #FF1744; | |
border: 2px solid white; | |
border-radius: 50%; | |
color: white; | |
font-weight: bold; | |
text-align: center; | |
font-size: 12px; | |
line-height: 20px; | |
width: 24px; | |
height: 24px; | |
margin-left: -12px; | |
margin-top: -12px; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.3); | |
} | |
.violation-icon { | |
background-color: #FF1744; | |
border: 3px solid white; | |
width: 32px; | |
height: 32px; | |
line-height: 26px; | |
font-size: 16px; | |
font-weight: bold; | |
margin-left: -16px; | |
margin-top: -16px; | |
position: relative; | |
} | |
.map-matched-icon { | |
background-color: #4CAF50; | |
border: 2px solid white; | |
} | |
/* Pulsing animation for last violation point */ | |
.pulse-ring { | |
border: 3px solid #FF1744; | |
border-radius: 50%; | |
height: 50px; | |
width: 50px; | |
position: absolute; | |
left: 50%; | |
top: 50%; | |
transform: translate(-50%, -50%); | |
animation: pulsate 2s ease-out; | |
animation-iteration-count: infinite; | |
opacity: 0; | |
} | |
@keyframes pulsate { | |
0% { | |
transform: translate(-50%, -50%) scale(0.1, 0.1); | |
opacity: 0; | |
} | |
50% { | |
opacity: 1; | |
} | |
100% { | |
transform: translate(-50%, -50%) scale(1.2, 1.2); | |
opacity: 0; | |
} | |
} | |
/* Enhanced popup styles */ | |
.popup-content { | |
min-width: 300px; | |
font-family: inherit; | |
} | |
.popup-content table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
.popup-content td { | |
padding: 4px 8px; | |
border-bottom: 1px solid #eee; | |
font-size: 13px; | |
} | |
.popup-content td:first-child { | |
font-weight: 600; | |
width: 45%; | |
color: #555; | |
} | |
.popup-content td:last-child { | |
color: #2c3e50; | |
} | |
.popup-title { | |
font-weight: 600; | |
color: #2c3e50; | |
margin-bottom: 8px; | |
padding-bottom: 8px; | |
border-bottom: 2px solid #e9ecef; | |
font-size: 16px; | |
} | |
.speed-violation { | |
color: #e74c3c; | |
font-weight: bold; | |
} | |
.map-matched { | |
color: #4CAF50; | |
font-weight: bold; | |
} | |
/* Enhanced legend styles */ | |
.legend { | |
position: absolute; | |
bottom: 20px; | |
right: 20px; | |
background: white; | |
padding: 15px 20px; | |
border-radius: 12px; | |
box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
font-size: 13px; | |
z-index: 1000; | |
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
transform: translateX(0); | |
min-width: 220px; | |
} | |
.legend.collapsed { | |
transform: translateX(calc(100% + 20px)); | |
} | |
.legend-toggle-btn { | |
position: absolute; | |
left: -35px; | |
top: 50%; | |
width: 35px; | |
height: 50px; | |
background-color: #3498db; | |
color: white; | |
border: none; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 16px; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.15); | |
transition: all 0.3s ease; | |
z-index: 1001; | |
border-radius: 8px 0 0 8px; | |
transform: translateY(-50%); | |
} | |
.legend-toggle-btn:hover { | |
background-color: #2980b9; | |
box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
} | |
.legend-toggle-btn i { | |
transition: transform 0.3s ease; | |
} | |
.legend.collapsed .legend-toggle-btn i { | |
transform: rotate(180deg); | |
} | |
.legend-title { | |
font-weight: 600; | |
margin-bottom: 12px; | |
color: #2c3e50; | |
font-size: 14px; | |
} | |
.legend-item { | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
margin-bottom: 8px; | |
font-size: 12px; | |
} | |
.legend-item:last-child { | |
margin-bottom: 0; | |
} | |
.legend-marker { | |
width: 18px; | |
height: 18px; | |
border-radius: 50%; | |
border: 2px solid white; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.2); | |
flex-shrink: 0; | |
} | |
.legend-line { | |
width: 25px; | |
height: 4px; | |
border-radius: 2px; | |
flex-shrink: 0; | |
} | |
.legend-arrow { | |
width: 25px; | |
height: 4px; | |
background: #3498db; | |
position: relative; | |
flex-shrink: 0; | |
} | |
.legend-arrow::after { | |
content: ''; | |
position: absolute; | |
right: 0; | |
top: -3px; | |
width: 0; | |
height: 0; | |
border-left: 8px solid #3498db; | |
border-top: 5px solid transparent; | |
border-bottom: 5px solid transparent; | |
} | |
/* Info control styles */ | |
.info-control { | |
min-width: 200px; | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
background: rgba(255,255,255,0.95); | |
padding: 15px; | |
border-radius: 10px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
font-size: 14px; | |
z-index: 1000; | |
} | |
.info-control h4 { | |
margin: 0 0 8px 0; | |
color: #2c3e50; | |
font-size: 16px; | |
} | |
.info-control div { | |
margin: 3px 0; | |
} | |
/* Enhanced leaflet popup */ | |
.leaflet-popup-content { | |
margin: 13px 19px; | |
font-size: 14px; | |
line-height: 1.5; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="map"></div> | |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylineDecorator.js"></script> | |
<script> | |
// Initialize the map | |
var map = L.map('map').setView([26.227696375, 50.174768875], 14); | |
// Add OpenStreetMap tile layer | |
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
maxZoom: 19, | |
attribution: '© OpenStreetMap contributors' | |
}).addTo(map); | |
// Define the coordinates and the detailed path data | |
var latlngs = [ | |
[26.255075, 50.087433], | |
[26.213954, 50.168777], | |
[26.213989, 50.173994], | |
[26.215954, 50.183913], | |
[26.226153, 50.195607], | |
[26.230741, 50.195111], | |
[26.244143, 50.197491], | |
[26.221562, 50.195825] | |
]; | |
var pathData = [{"latitude": 26.255075, "longitude": 50.087433, "time": "06/19/25,15:33:15", "speed": 184, "speed_limit": 120, "street_name": "Abu Hadriah Rd", "street_name_ar": "\u0637\u0631\u064a\u0642 \u0623\u0628\u0648\u062d\u062f\u0631\u064a\u0629", "distance_to_segment": 3.04, "vehicle_direction": 144, "segment_bearing": 144.7, "nearest_segment": [[26.2558537, 50.0868564], [26.2549608, 50.0875603]], "closest_point": [26.255091646830767, 50.08745714941299], "map_matched": false}, {"latitude": 26.213954, "longitude": 50.168777, "time": "06/19/25,15:40:59", "speed": 178, "speed_limit": 60, "street_name": null, "street_name_ar": null, "distance_to_segment": 1.85, "vehicle_direction": 96, "segment_bearing": 96.2, "nearest_segment": [[26.2140034, 50.1677301], [26.2137416, 50.170408]], "closest_point": [26.213902047948217, 50.16876681123434], "map_matched": true, "matched_street": "95", "match_distance": 4.1190472050176}, {"latitude": 26.213989, "longitude": 50.173994, "time": "06/19/25,15:41:23", "speed": 178, "speed_limit": 120, "street_name": "Abu Hadriah Rd", "street_name_ar": "\u0637\u0631\u064a\u0642 \u0623\u0628\u0648\u062d\u062f\u0631\u064a\u0629", "distance_to_segment": 0.23, "vehicle_direction": 92, "segment_bearing": 86.5, "nearest_segment": [[26.2138744, 50.1718646], [26.2139922, 50.1740366]], "closest_point": [26.21398998274184, 50.17399571808373], "map_matched": true, "matched_street": "95", "match_distance": 0.1493857018265775}, {"latitude": 26.215954, "longitude": 50.183913, "time": "06/19/25,15:42:11", "speed": 177, "speed_limit": 120, "street_name": "Cooperative Council Road", "street_name_ar": "\u0637\u0631\u064a\u0642 \u0645\u062c\u0644\u0633 \u0627\u0644\u062a\u0639\u0627\u0648\u0646", "distance_to_segment": 5.95, "vehicle_direction": 76, "segment_bearing": 73.6, "nearest_segment": [[26.2150448, 50.1804584], [26.215989, 50.1840445]], "closest_point": [26.215949551725856, 50.183894672830505], "map_matched": true, "matched_street": "95", "match_distance": 5.650290743335173}, {"latitude": 26.226153, "longitude": 50.195607, "time": "06/19/25,15:44:41", "speed": 172, "speed_limit": 110, "street_name": null, "street_name_ar": null, "distance_to_segment": 1.18, "vehicle_direction": 353, "segment_bearing": 358.5, "nearest_segment": [[26.2257516, 50.1956514], [26.2287064, 50.1955624]], "closest_point": [26.226158565256828, 50.19563913435174], "map_matched": true, "matched_street": "613", "match_distance": 2.1064835129410686}, {"latitude": 26.230741, "longitude": 50.195111, "time": "06/19/25,15:45:04", "speed": 173, "speed_limit": 110, "street_name": "King Khaled Road", "street_name_ar": "\u0637\u0631\u064a\u0642 \u0627\u0644\u0645\u0644\u0643 \u062e\u0627\u0644\u062f", "distance_to_segment": 0.34, "vehicle_direction": 354, "segment_bearing": 354.5, "nearest_segment": [[26.2257516, 50.1956514], [26.2323416, 50.1949388]], "closest_point": [26.23074084482978, 50.19511184168362], "map_matched": true, "matched_street": "613", "match_distance": 0.41418217575267635}, {"latitude": 26.244143, "longitude": 50.197491, "time": "06/19/25,15:46:15", "speed": 179, "speed_limit": 110, "street_name": null, "street_name_ar": null, "distance_to_segment": 5.56, "vehicle_direction": 20, "segment_bearing": 20.9, "nearest_segment": [[26.2428935, 50.1969601], [26.244364, 50.1975851]], "closest_point": [26.24412669111161, 50.19748423436298], "map_matched": true, "matched_street": "Unnamed Street", "match_distance": 5.2006820160223866}, {"latitude": 26.221562, "longitude": 50.195825, "time": "06/19/25,15:44:20", "speed": 144, "speed_limit": 110, "street_name": "King Khaled Road", "street_name_ar": "\u0637\u0631\u064a\u0642 \u0627\u0644\u0645\u0644\u0643 \u062e\u0627\u0644\u062f", "distance_to_segment": 0.48, "vehicle_direction": 342, "segment_bearing": 354.3, "nearest_segment": [[26.221408, 50.1961629], [26.2244909, 50.1958212]], "closest_point": [26.22159137354772, 50.19614257183131], "map_matched": true, "matched_street": "613", "match_distance": 32.304604518117976}]; | |
// Add device name to each point's data | |
pathData.forEach(function(data) { | |
data.device_name = "Testing Live Alerts"; | |
}); | |
// Function to create numbered div icons for violations | |
function createNumberedIcon(number, isLast = false, isMapMatched = false) { | |
var className = 'number-icon'; | |
if (isLast) { | |
className += ' violation-icon'; | |
} | |
if (isMapMatched) { | |
className += ' map-matched-icon'; | |
} | |
return L.divIcon({ | |
className: className, | |
html: isLast ? 'V<div class="pulse-ring"></div>' : number.toString(), | |
iconSize: isLast ? [32, 32] : [24, 24] | |
}); | |
} | |
// Function to create popup content | |
function createPopupContent(pointNum, data, isLast) { | |
var lat = data.latitude; | |
var lon = data.longitude; | |
var speedClass = 'speed-violation'; | |
var html = '<div class="popup-content">'; | |
html += '<div class="popup-title">Violation Point ' + pointNum + (isLast ? ' (Latest)' : '') + '</div>'; | |
html += '<table>'; | |
html += '<tr><td>Latitude:</td><td>' + lat.toFixed(6) + '</td></tr>'; | |
html += '<tr><td>Longitude:</td><td>' + lon.toFixed(6) + '</td></tr>'; | |
if (data.map_matched) { | |
html += '<tr><td>Map Matched:</td><td class="map-matched">Yes</td></tr>'; | |
if (data.matched_street) { | |
html += '<tr><td>Matched Street:</td><td>' + data.matched_street + '</td></tr>'; | |
} | |
if (data.match_distance !== undefined) { | |
html += '<tr><td>Match Distance:</td><td>' + data.match_distance.toFixed(2) + ' m</td></tr>'; | |
} | |
} | |
if (data.time) { | |
html += '<tr><td>Time:</td><td>' + data.time + '</td></tr>'; | |
} | |
if (data.device_name) { | |
html += '<tr><td>Vehicle:</td><td>' + data.device_name + '</td></tr>'; | |
} | |
if (data.street_name_ar) { | |
html += '<tr><td>Street (AR):</td><td style="direction: rtl;">' + data.street_name_ar + '</td></tr>'; | |
} | |
if (data.street_name) { | |
html += '<tr><td>Street (EN):</td><td>' + data.street_name + '</td></tr>'; | |
} | |
if (data.distance_to_segment !== undefined) { | |
html += '<tr><td>Dist to street:</td><td>' + data.distance_to_segment.toFixed(2) + ' m</td></tr>'; | |
} | |
if (data.vehicle_direction !== undefined) { | |
html += '<tr><td>Vehicle Dir:</td><td>' + data.vehicle_direction.toFixed(1) + '°</td></tr>'; | |
} | |
if (data.segment_bearing !== undefined) { | |
html += '<tr><td>Street Bearing:</td><td>' + data.segment_bearing.toFixed(1) + '°</td></tr>'; | |
} | |
if (data.speed_limit) { | |
html += '<tr><td>Speed Limit:</td><td>' + data.speed_limit + ' km/h</td></tr>'; | |
} | |
if (data.speed) { | |
html += '<tr><td>Vehicle Speed:</td><td class="' + speedClass + '">' + data.speed + ' km/h</td></tr>'; | |
} | |
if (data.speed && data.speed_limit) { | |
var overSpeed = data.speed - data.speed_limit; | |
html += '<tr><td>Over Speed:</td><td class="speed-violation">+' + overSpeed + ' km/h</td></tr>'; | |
} | |
html += '</table>'; | |
html += '</div>'; | |
return html; | |
} | |
// Function to create direction arrow from point | |
function createDirectionArrow(lat, lon, direction) { | |
if (direction === undefined || direction === null) return null; | |
var arrowLength = 0.0002; // Length of arrow in degrees | |
var radians = direction * Math.PI / 180; | |
var endLat = lat + arrowLength * Math.cos(radians); | |
var endLon = lon + arrowLength * Math.sin(radians); | |
var arrow = L.polyline([[lat, lon], [endLat, endLon]], { | |
color: '#3498db', | |
weight: 3, | |
opacity: 0.8 | |
}); | |
var decorator = L.polylineDecorator(arrow, { | |
patterns: [{ | |
offset: '100%', | |
repeat: 0, | |
symbol: L.Symbol.arrowHead({ | |
pixelSize: 12, | |
polygon: false, | |
pathOptions: { | |
stroke: true, | |
color: '#3498db', | |
weight: 2, | |
opacity: 0.9 | |
} | |
}) | |
}] | |
}); | |
return { arrow: arrow, decorator: decorator }; | |
} | |
// Add markers for all points - all are violation points | |
var allBounds = []; | |
for (var i = 0; i < pathData.length; i++) { | |
var data = pathData[i]; | |
var isLast = (i === latlngs.length - 1); | |
var isMapMatched = data.map_matched || false; | |
var icon = createNumberedIcon(i + 1, isLast, isMapMatched); | |
var popupContent = createPopupContent(i + 1, data, isLast); | |
// Add marker to map | |
var vehicleMarker = L.marker([data.latitude, data.longitude], {icon: icon}) | |
.addTo(map) | |
.bindPopup(popupContent); | |
allBounds.push([data.latitude, data.longitude]); | |
// Add direction arrow for each point | |
if (data.vehicle_direction !== undefined && data.vehicle_direction !== null) { | |
var directionArrowData = createDirectionArrow(data.latitude, data.longitude, data.vehicle_direction); | |
if (directionArrowData) { | |
directionArrowData.arrow.addTo(map); | |
directionArrowData.decorator.addTo(map); | |
} | |
} | |
// Draw the nearest street segment for each point if available | |
if (data.nearest_segment && data.nearest_segment.length === 2) { | |
var segmentPolyline = L.polyline(data.nearest_segment, { | |
color: '#9b59b6', | |
weight: 8, | |
opacity: 0.8 | |
}).addTo(map).bindPopup('Nearest Street Segment for Violation ' + (i + 1)); | |
// Add enhanced start and end circles for the segment | |
L.circleMarker(data.nearest_segment[0], { | |
radius: 6, | |
color: '#8e44ad', | |
fillColor: '#9b59b6', | |
fillOpacity: 1, | |
weight: 2 | |
}).addTo(map); | |
L.circleMarker(data.nearest_segment[1], { | |
radius: 6, | |
color: '#8e44ad', | |
fillColor: '#9b59b6', | |
fillOpacity: 1, | |
weight: 2 | |
}).addTo(map); | |
} | |
// Draw the closest point on the segment and a line to it | |
if (data.closest_point && data.closest_point.length === 2) { | |
// Enhanced closest point marker | |
L.circleMarker(data.closest_point, { | |
radius: 8, | |
color: '#e67e22', | |
fillColor: '#f39c12', | |
fillOpacity: 1, | |
weight: 3 | |
}).addTo(map).bindPopup('Closest Point on Street<br>Distance: ' + (data.distance_to_segment ? data.distance_to_segment.toFixed(2) + ' m' : 'N/A')); | |
// Enhanced dashed line from vehicle to closest point | |
var lineToClosest = [ | |
[data.latitude, data.longitude], | |
data.closest_point | |
]; | |
L.polyline(lineToClosest, { | |
color: '#3498db', | |
weight: 3, | |
opacity: 0.7, | |
dashArray: '8, 8' | |
}).addTo(map); | |
} | |
} | |
// Draw the route line with map-matched corrections | |
if (latlngs.length > 1) { | |
var routeColor = '#FF1744'; | |
var routeWeight = 5; | |
// Check if we have map-matched data | |
var hasMapMatched = pathData.some(p => p.map_matched); | |
if (hasMapMatched) { | |
routeColor = '#4CAF50'; | |
routeWeight = 6; | |
} | |
L.polyline(latlngs, { | |
color: routeColor, | |
weight: routeWeight, | |
opacity: 0.8, | |
smoothFactor: 1 | |
}).addTo(map).bindPopup('Vehicle Route' + (hasMapMatched ? ' (Map Matched)' : '')); | |
} | |
// Fit the map to show all points | |
if (allBounds.length > 0) { | |
var bounds = L.latLngBounds(allBounds); | |
map.fitBounds(bounds.pad(0.1)); | |
} | |
// Add enhanced legend with toggle functionality | |
var legendHtml = '<button class="legend-toggle-btn" onclick="toggleLegend()"><i class="fas fa-chevron-right"></i></button>'; | |
legendHtml += '<div class="legend-title">Map Legend</div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-marker" style="background: #FF1744;"></div><span>Violation Points</span></div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-marker" style="background: #4CAF50;"></div><span>Map Matched Points</span></div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-marker" style="background: #FF1744; width: 24px; height: 24px;"></div><span>Latest Violation</span></div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-arrow"></div><span>Vehicle Direction</span></div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-line" style="background: #9b59b6; height: 6px;"></div><span>Nearest Street Segment</span></div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-marker" style="background: #f39c12;"></div><span>Closest Point</span></div>'; | |
legendHtml += '<div class="legend-item"><div class="legend-line" style="background: #3498db; border: 2px dashed #3498db; height: 2px;"></div><span>Distance Line</span></div>'; | |
var legendControl = L.control({position: 'bottomright'}); | |
legendControl.onAdd = function(map) { | |
var div = L.DomUtil.create('div', 'legend'); | |
div.id = 'mapLegend'; | |
div.innerHTML = legendHtml; | |
return div; | |
}; | |
legendControl.addTo(map); | |
// Toggle legend function | |
function toggleLegend() { | |
var legend = document.getElementById('mapLegend'); | |
legend.classList.toggle('collapsed'); | |
} | |
// Add info control | |
var infoControl = L.control({position: 'topleft'}); | |
infoControl.onAdd = function(map) { | |
var div = L.DomUtil.create('div', 'info-control'); | |
var mapMatchedCount = pathData.filter(p => p.map_matched).length; | |
div.innerHTML = '<h4><i class="fas fa-car" style="margin-right: 8px; color: #3498db;"></i>Vehicle Path Analysis</h4>' + | |
'<div><strong>Vehicle:</strong> Testing Live Alerts</div>' + | |
'<div><strong>Total Violations:</strong> ' + latlngs.length + '</div>' + | |
'<div><strong>Map Matched:</strong> ' + mapMatchedCount + '/' + latlngs.length + '</div>'; | |
return div; | |
}; | |
infoControl.addTo(map); | |
// Add zoom control to top right | |
L.control.zoom({position: 'topright'}).addTo(map); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment