Django and LeafletJS - Dynamic Map Updates with AJAX Polling Requests

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
let's explore using Django and leaflet JS to display the positions dynamically of vehicles that we have in our database so in this video we're going to create a map using leaflet and we're going to plot with markers the positions of these vehicles and we're going to grip these markers together using a leaflet JS layered grip after we've done that what we're going to do is create a simulator that's going to dynamically update the positions of these vehicles in the database and then we're going to pull the back end every five seconds to get the updated positions and then update the markers on the leaflet map dynamically so we're going to have a map on our page that's displaying in real time the positions of these vehicles with a 5 Second polling interval it's not quite real time but it's going to be fairly close so let's get started I have here a GitHub repository that contains starter code and I'm going to push the final code to another Branch at the end of this video so you can check out this with the get clone command I'll link this repository below the video and you can clone that code now I have this open in vs code and we're going to explore the startle chord a little bit we have a Django model here it's called vehicle and it contains two Fields storing the latitude and the longitude of the vehicle at a given time so the starter code will have this model and we also have two management commands the first one here is called create vehicles and this is a simple command within the handle method what we're doing is we're generating five vehicle objects and storing those in our Django database so we use the vehicle.objects.createfunction and we're adding a latitude in our longitude and those are selected using the random dot uniform function and we're selecting any latitude between 44 and 47 and any longitude between minus 100 and minus 80. now I'm choosing these positions randomly but they will be in the center to the north of the USA and I think that's going to be in states such as North Dakota Minnesota and Illinois that kind of region so that's going to give you some random locations in those areas when we run this command I will add them to the database and that will print a message at the end so once you've cloned the report three what you can do is you can install Django if you don't already have it and we can run the python manage.pi migrate command and that's going to create a local SQL Lite database and it's that database that's going to store our vehicle positions once we've done that we can run this custom management command it's called create Vehicles you can do that with python manage.pi and then just the name of the command here create Vehicles as you can see that creates five vehicles and stores them in our database so we have access to these vehicles now so that handles the database what I'm going to do is go to the templates directory and we're going to go to base.html and I've got some code here that's loading up certain libraries in the head tag we've got bootstrap for styling we've got htmx we're actually not going to use these in this video but they're there anyway what we also have is leaflet JS and we have a link tag loading the leaflet Styles and also a script tag below that that contains the JavaScript code for the leaflet module finally just below that we're using Django's static template tag here and we're loading up a custom Javascript file called script.js and you can find that within the static directory and it's this file here at the moment all it's going to do is console.log the message hello to the browser we're going to change that throughout this video to add some code for leaflet.js mapping but what I want to do to begin with is we're going to close the script.js file and we're going to go to views.pi now within this file we already have a very basic view called index which is rendering this template what I'm going to do is import the vehicle model from the models.pi file and what we want to do is we want to take our vehicles all of them in the database and we want to convert that to a list of dictionaries that we can then add to our templates as context and use the Json script template filter in order to generate a script tag containing all of the lassitudes and longitudes so what we're going to do in the context dictionary here we're going to add a key here called vehicles and that's going to map to a statement here we're going to use the vehicle model here and we're going to get the values from the database and the two values we want to get are the latitude and of course the longitude as well so that's going to affect those two particular fields from our model you can see that's the only two fields we have we also have an implicit ID here as well but we don't need that so that's going to return all of the vehicles in the database and it's going to return the latitude and longitude Fields as a query set we want to convert that query set to a python list so we can use the list function to do that so the effect of this statement is rather than a query set of Django models what we instead have is a list of python dictionaries and that structure can then be used in our template if we go to the index.html and it can be used with the Json script template tag so what we're going to do is reference that bit of context that we've just added called vehicles and we can use the jsonscript template filter to convert that to a script tag of type application Json and we can also give that an ID in our case we'll get an ID of vehicle Json once we've done that we can save the template file and I'm going to run the Django run server command here at the bottom I'm going to run the Django development server and we're going to go to the page now on the page just now as you can see it just says hello here but if we inspect the Dom here by going to the developer tools you can see as well as the div that contains an ID of map underneath that we have a script with the ID of vehicle Json if we expand that you can see we have a list or a JavaScript array in this case of latitude and longitude pairs so what we're going to do a little bit later in the video is we're going to get a reference to the script tag with this ID and we're going to load these latitudes and longitudes into our Javascript file and we're going to then use them to render markers on the map and after that later on we're going to update those markers dynamically so that the map that we have on the page is updating in real time and positions of these vehicles are also updating so that's the important first step of converting the database data to a structure that can then be used within the template and then referenced by our JavaScript so let's close off the developer tools and what we're going to do now is go to leaflet's quick start guide here and what I'm going to do is we're going to scroll down and this contains some setup code for leaflet Js I'm going to link this page below the video you can see it has the link and the script tag that we have in our base template from the starter code so we don't need to add these they're already there and we also already have a div with the ID of map if we go back to our template here and look at this div tag you can see we've also added some Styles a height of 500 pixels and a width of a hundred percent and we also have this message of hello within the div I'm actually going to remove that for now now let's go back to the quick start guide here what we're going to do is reference this code here for setting up the map I'm going to make the text a little bit bigger so we can see that better now this is how you actually access or rather create a map in leaflet JS we have an object called L and that has a function called map that's used to actually generate the map and that function takes as an argument the ID of the particular tag in the Dom that you want to add the map to in our case it's just a div with the ideal map which is fairly standard and then once that map is created there's a function called set view which takes some coordinates that you want Center the map on as a first argument and also a zoom level as the second argument so what we're going to do is go back to vs code and I'm going to go to this script that we have here it's called script.js and let's remove the console log statement I'm going to write a JavaScript function here and we're going to call it create map and that's going to take two arguments the center location and also the zoom level so let's create the map variable here and we're going to reference the l dot map function this takes the ID of the element that we want to add the map to and then we can call that set view function and pass the center and the zoom level to that and this code here on line two this will create our map object there is one more step that we need to do in this function if we go back to the documentation here for the quick start we have this code here that adds a tile layer we're just going to copy that and paste it into vs code just below the map we can paste that there and I'm going to tab this over so we create what's called a tile layer and we are adding that to the map and the final thing I want to do because this is a function that's used to create the map I'm actually just going to return on the map to the caller so that we have access to that map when we call this function so let's now try and actually call the function I'm going to copy the name of this function and we're going to go to the base or rather the index.html and we have a block here called Script and that extends the base template here where we've defined that block at the bottom so this will just load any script that we have for a template right before the end of the body tag so let's go back to index.html and within this block we're going to create a script tag and we're going to reference some JavaScript to actually load up the map here so we want to create this map when the page loads so I'm going to add an event listener to the document with the document.add event listener function and the event that we want to listen for it's the Dom content loaded event basically that's the event that fires when the page first loads and that event listener takes a second argument here it's a callback function that is called when this event occurs in the document so when the page first loads the body of this function is going to execute so what we're going to do here is we're going to create a variable called map and we're going to call that that function create map that we just defined in our script.js and we're going to pass our center location and a zoom level to that function now for the center location I'm going to pass a JavaScript array with the latitude of 45.5 and a longitude of -90 and that's roughly halfway between the latitudes and longitudes that we defined here in this vehicle.objects.create statement that creates the random data so we will have vehicles around about these latitudes and longitudes and this is roughly the center point of those and the second argument to our function was the zoom level and I'm going to make the zoom level 5 in this case so that creates the map object and the second thing we want to do is get a reference to all the vehicle positions so I'm going to create another variable called vehicles and to get those we need the ID of this Json script output and what we're going to do is use the document.getelement by ID function to get that element with that ID we then grab the text content which is just the text within that element and then finally we're going to call the json.pass function and that is going to tick that Json data and bring it into this JavaScript as JavaScript objects and those objects can then be referenced with DOT notation as we're about to see so before we go on let's console.log these vehicles and we're going to go back to the browser back to our page and we're going to refresh this page now you can see when we refresh the page the map is appearing and that's because we created the map and we've added it to that div with the ID of map if we go to the development console you can see we have an array of five objects within the console here and that's the latitors and longitudes that have been loaded in from that Json script output and their console logged here in our JavaScript code so all good so far the next step I'm going to actually create something called a layer group in leaflet JS let's go to the leaflet documentation again I'll link this page below the video we have the API reference now you can see that in leaflet JS we have different types of layers in the code we have UI layers we have raster and Vector layers and some of these are familiar things such as markers which we used to denote a point daughter position on the map and we also have in the vector layers things like circles and rectangles and also poly lines that can be used to draw lines on the map now in leaflet JS you can actually grip different types of layers different types of objects on the map into grips you can see here we have a section for other layers and we have a layer group and also a feature group what we're going to do in this video is we're going to use a feature group if we click this link we're taken to the documentation for that and this extends the layer grip and it makes it easy to do the same thing to all of the objects or the layers within that group for example you can bind a pop-up to all of the layers at once and so on now in order to create a feature group all we need to do is use this L object from leaflet and call the feature group function and you can pass different layers or different objects that you want to add to that group as a parameter so let's go back to vs code and just below this console.log statement I'm going to create a new variable here and let's call this variable marker feature group and we're going to set that equal to l dot feature group and we're not going to pass any objects to that right away but what we are going to do is add it to our map that we have received back from the create map function so we're creating a feature group and we're adding it to the map and once we've done that what we can do is we can create a for Loop in JavaScript and we're going to see lit vehicle of vehicles so we're iterating over the vehicles that we received back from that Json script output and we're going to add as a marker each vehicle's position to that feature group that we've just created Now to create a marker we can use the l dot marker function and we can pass through that the vehicle's latitude and longitude so let's create a JavaScript array here and reference the vehicle dot latitude and the second parameter is the vehicle dot longitude so that's going to create the marker at that position and then we can add that marker to our feature group called marker feature group here so let's save that file and we can go back to the page if we refresh this page you can see we now get these markers appearing on the map and these markers are placed on the map as part of this feature group that we've created here and when we create the marker object we are adding it to that feature group there rather than the entire map so we can grip these markers together into a feature group and this is the initial map that we see here now what we want to achieve in this video is we want to update the positions of these markers to reflect changing positions of the vehicles in the database so we're going to write a simulator that is going to update the position of these vehicles every 5 seconds and update the database coordinates for the latitude and the longitude that we see in our Django vehicle model now we have the scale lesson of another management command in this starter code this command is called simulate vehicle movements and we have the handle function here that currently raises and not implemented error so let's remove that error and we're going to Define a variable here and we're going to call it lat long variation and we're going to set that equal to 0.1 now I'm going to add a comment after this variable and it's going to explain why we're setting it to 0.1 and that's because 0.1 degrees is approximately 11 kilometers so we're going to use that a little bit later in the code let's create an infinite Loop here by using the whale true statement and each time this will look runs we're going to print that the vehicle positions are updating and then we're going to use the time.sleep call I'm going to sleep for five seconds and in between these two statements we're going to Define code that's going to update each vehicle so now we need to update the positions of these vehicles we're going to do that within a for Loop here and we're going to see four vehicle in vehicle.objects.org so for each vehicle we're going to change its latitude and longitude let's paste a statement in here we are setting the vehicle.latitude equal to what it was before and we're adding again using the random.uniform function a value somewhere between minus 0.1 and positive 0.1 to the previous value and we can get a value between those using the random.uniform function from the random module and we're going to do the exact same thing below that for the longitude we're just setting equal to the previous longitude plus some random value between minus 0.1 and positive of 0.1 once we've done that we can use the vehicle.save function and that's going to save the new latitude and longitude to the database now the goal that we have here is to be able to run this command here which is simulating some updates to the positions of vehicles and we want the leaflet map that we have on the front end here to also update to reflect the new positions of these vehicles so what we're going to need to do from our client-side code from the JavaScript is we're going to have to send an Ajax request to our Django backend and that Ajax request will have to fetch the new positions every 5 seconds and return them as Json data to the front end and once the front end gets that data it can use it to actually update the positions of these markers and I can do that by clearing out the existing markers from the layer group that we've created or the feature group that we've created and then adding the new positions to that feature group so we're going to start doing that in a second but one thing to note is we are going to update the map but we don't want to re-render the entire map every five seconds that would be quite a waste of resources instead said we just want to update the positions of the markers on the map and we're using the layer group or the feature group to grip them together so that we can easily get rid of those and bring in the new markers so let's go back to vs code now and I'm going to go to the viewers.pi file and what we're going to do in this file is Define a new view in Django and this new view is going to be responsible for returning the possessions of all of the vehicles in our database as Json data to the client-side application now at the top of this file what I'm going to do is I'm going to bring in an import from Django's HTTP module we're going to bring in the Json response because in this new view we're going to return Json data so let's define the view here and we're going to call it vehicle positions and this viewer will take the Django request as an argument and the code is going to be very similar to what we had up here for the context so what I'm going to do is I'm actually going to copy this statement here which converts the query set of vehicles to a list of python dictionaries we're going to bring that down here and we're just going to return a Json response and to that Json response actually I'm just going to copy this entire dictionary here and we're going to paste that in below so we have a key of vehicles and the values are going to be very similar to what we had in the Json script output so this view when it's called is just going to fetch all of the vehicle's latitudes and longitudes and return those as Json data essentially a list of the latitudes and longitudes that we have in the database so that's the new view that's responsible for returning that data we now need a URL that's going to call the view so let's Create A New Path within urls.pi I'm creating a path with the string of vehicle positions and that path will then call out to that view that we've just created which will then return the positions of those Vehicles so that's the path we now need to call that endpoint using our client-side JavaScript code so let's go back to the index.html template here and in a second we're going to create a function that's going to be called from this template and we're going to do that in the script.js so we're going to call that function here but first of all we need to actually create the function so let's go over to script.js and I'm I'm going to create a new function here so let's create a new function here and we're going to call it update markers and it's going to take in that marker feature group that we created within the index.html file and that's this feature group here it's the feature group that we added to the map and then we added all the markers to that feature group so we're going to reference that by passing it as a parameter to this function that we're creating here now step one of this function we want to send a request to the server and get the new latitude and longitude data so we're going to send what's called a fetch request and this is just an HTTP request to our backend and we need to specify the end point that we want to use so let's go back to urls.pi we had an endpoint here called vehicle positions let's go back to our script here and we're going to reference that string here and that's going to then call the URL that's going to be matched up with the correct Django view now the fetch request returns something called a promise we can then use the dot then function to get the response and then we can convert that response to Json data with the response.json function action once again that returns a promise so we then use the dot then function again and finally after the response.json function has completed that will have returned our data we can use that within a callback function here so before we go on let me just quickly explain this statement again we send a fetch request to Django that will then return a response but that will not happen immediately it's an asynchronous call so we use a promise to get the response when it arrives back to our application and then we convert that response from Json data to JavaScript objects something that can be used within our JavaScript code again that returns a promise so we use dot then and we get the data after that promise resolves and we can then use that data within the body of this function here now when we get new positions what we're going to do is grab the feature group here and we're going to get rid of all of the markers that existed in that feature group before now to get rid of all of the items or all of the layers within the marker feature group it has a function called clear layers that we can call and by default what that's going to do is just remove of all of the items from the map and that's just going to remove all of the features that we have within that feature group once we've done that we can take our data and that has our key called vehicles and that comes from our backend response we had a key within the dictionary of the Json response called vehicle so let's copy that and go back to our script so it's going to be data.vehicles which is a list of JavaScript objects containing a latitude and a longitude for each one of those we want to actually create a marker for that particular latitude and longitude so we can use the for each function and we pass to that another callback function for each vehicle in the data we are going to create a new function here and plot the marker as part of that feature group so for each vehicle we're going to create a variable called new latitude longitude and that's going to be a JavaScript array with the vehicle's new latitude and the new longitude from the back-end server once we have that we're going to use the leaflet dot marker function again and we can add that new latitude and longitude as a marker and we're going to add that to a feature group so it's the marker feature group here so let's save this and we're going to go to the front end I'm going to see if this works let's refresh the page and we have these markers on the map let's see if we can see these dynamically updating now I've zoomed into this particular marker here but there are no updates coming through and that's because we haven't actually called the function so once we have this let's save the file and we're going to copy the name of this function that we've defined here we need to actually call that every five seconds we're going to do that from our index.html within this event listener here now in JavaScript we can call a function every five seconds or any time interval using this set interval function here now the first argument to set interval is a callback function and this is what executes at the given time period for example every five seconds now what we want to do is call this update markers function so we're going to actually call that here within the Callback function and remember that takes the marker feature group as an argument and the second parameter to set interval is the time period that we want to actually call that function for us it's going to be 5 000 milliseconds so 5 Seconds every five seconds we're going to call update markers and that function is then going to send an Ajax request to a Django and get the new positions for each of these vehicles in our database and it's then going to add a marker for the new position to this feature group here and hopefully we're going to see that reflected on the map that we have here in the front end so once you've saved all that code you can refresh this page and we're going to zoom into one marker in particular and it's going to be this one here remember you will have a random set of markers if you're following along with this video now you can see at the moment that this marker is not moving it's not updating its position what we need to do in order to actually update the positions is actually run this simulator here this management command and this command is going to actually update the vehicle's latitude and longitude every five seconds now we need to run this command along with our Django server the Django server needs to be running so that the Ajax request from script.js actually finds the vehicle position's endpoint and Returns the data so we're going to keep the server running in vs code and I'm going to bring a terminal in here and we're going to run the python manage.pi simulate vehicle movement command so let's actually start executing this command once we're doing that we can see that the positions of these markers they should update every five seconds with the new position of the vehicle so we can imagine this being a real-time scenario where we have every five seconds an update to a given vehicle's position in the database and if we scroll out here you can see that these small updates are occurring for all of the markers in our data they're all being updated every five seconds based on this new setup that we have so let's summarize what's going on here if we go back to our index.html file when the page first loads we create the map at that given Center Point and zoom live we then load in the vehicle positions from this script tag that we've defined using the Json script template filter and that will then bring that data into this JavaScript application with then add a feature group to the map and for each vehicle and the data that we've loaded we add a marker to that feature group at the given position for that particular vehicle finally we are using the set interval function here to run a bit of code every five seconds and that code is going to update the markers and that's done using this function within a script.js it sends a fetch request to our Django view which then Returns the data for the vehicles the updated vehicle positions when we get that updated data we clear out the layers that exist within the feature group and then for each vehicle we create a new marker at its given position that's coming back from the Django server and the positions are updating because we're running this particular command here the simulate movement command and every five seconds that's going to update the position of all vehicles in our database so that's the Crux of this video we're going to show one more thing in the video and that's how to draw a line between the vehicle's previous position and its new position now to do that we're going to amend the code within this up the markers function what I'm going to do to start with is create an empty JavaScript array called the previous lat lungs and then what we're going to do is we're going to get the previous positions of the markers that we see on the map and we're going to do that using the marker feature group here so let's copy the name of that variable and below this empty array we're going to reference that feature group and it has a function called each layer and this is a function that's similar to the JavaScript for each function basically this function will look at all of the layers that belong to a feature group and that can be markers it can be polylines it can be any kind of object on the map it's going to iterate over all of them and it allows you to perform some sort of logic in that particular layer now what I'm going to do to start with is we're just going to console.log the layer that we're iterating over and we're going to save that and go back to the page and refresh this page if we go to the developer tools you should see after five seconds here that we are getting some statements printed to the console and we're getting five objects every five seconds and each one of these represents one of the layers on this clear group this feature group and each layer represents one of the markers on the map and you can see that they have a property called underscore lat long which contains the latitude and the longitude for that marker on the map now what we're going to do is we're going to go back to the code here and we're going to fetch that Latitude in longitude and we're going to push it to this array here that we created on line 13 so let's remove the console.log statement and we're going to add some new code here let's say previouslat loans dot push and we're going to push the layers dot underscore lat long property so after this code in line 14 runs this particular array above will be populated with the latitude and longitude of the previous positions for the given markers on the map after we've done that we can go down to this for each Loop here where we are adding the new vehicle positions to this marker feature grip and what I'm going to do here is we're going to not only get a reference to the vehicle in the data but we're going to get the index of iteration as well and we can do that in the for each statement by having the data as the first argument and the index as the second argument so for each vehicle in the data we get a reference to the vehicle at each stage as well as the index that we are iterating over and we're going to use that index to actually index into this array here and get the corresponding previous position for that vehicle so let's now go to the body of this for each function and we're going to create a variable called previous latitude longitude and that's going to be equal to another JavaScript array here and the latitude and longitude are going to come from this array that we built up above we're going to index in at the given index and we're going to get the latitude here I'm going to do the same thing for the longitude so let's copy that and paste it here and replace the name of the field with launch it so now we have the previous latitude and longitude for a given marker as well as its new position so these vehicles we have the previous and their new positions what we're going to do is add another object another layer to this feature group and that's the layer called polylink and the polyline layer takes a list and each element within that list is another list of latitude longitude pills so we're going to reference the previous latitude and longitudes as well as the new one here and that's going to create a line between that previous point and the new point and actually this poorly line layer can take as many different positions as you want to give it so we could give it more possessions here and it would create a line between each consecutive pair of points within that JavaScript array for us it's easy we only have two points so it's going to create a line from the first point to the second point and of course we need to add that to our feature group so we'll use the add to function and we can reference the marker feature group here so let's save that and go back to our page and refresh this page we're going to see that this doesn't actually work and we're going to get to the reason for that in a second we are only left with one marker and if we inspect the console here you can see that it cannot read properties of undefined now that error doesn't tell us a lot about what's going on but let's go back to the code and the problem lies within this statement here that where we iterate over each layer in the feature group and we're trying to push the layers latitude and longitude to this array above but the problem is we have now added poly lines to this feature group and polylines they are different from markers they don't have a specified latitude and longitude this is a list of different points and it's creating lines between those points so this property doesn't actually exist on the polyline layer so we're going to actually check for the existence of that property before we try and push the latitude and longitude to this list Above So before we push that we're going to write some JavaScript here and we're going to check that that property exists we can do that with the layer dot has own property function in JavaScript and of course the property we're checking for is the underscore lat long property and then we can use the logical and in JavaScript to say that if that property does exist we will then push the property or the value of the property to this array that's defined above and when you write these statements here in JavaScript with the logical and this is actually a short circuit operator so if this evaluates to false the code after it will not actually be run so we're not going to try and push something that doesn't exist to that particular array above because it will short circuit if this evaluates the false and it will not run the code after the and statement so the effect of this statement is it's going to check for the underscore lat long property if it does exist it's then going to push the value of that to the array above so let's now save this and go back to the page if we refresh this page we now see the markers but we're actually going to see we still have an issue on this page we're still getting this error here and the reason for that is something I'm going to show you now we're going to comment this code out and I'm actually going to copy that code and print it below and I'm going to remove the body of this callback function and again I'm just going to console.log the layer at each iteration of this feature group dot each layer function let's go back to the page and we're going to see the console.log statements I'm going to show you where I've went wrong in this code it's going to print out the five marker positions and if we look at the layers underscore long field you can see it's a JavaScript object and it has the key names of lat and long here so what I've done in the code here when we created this previous latitude and longitude we were referencing dot latitude and Dot longitude we need to change that to match what's in the JavaScript object so let's change that to dot lat and we'll change the second one to dot long here we can now get rid of this statement with the console log and bring back the code that we had above that and we can then go back to the page and refresh this page now hopefully if we zoom in here to a marker we should see its position as updating and it's drawing a line between the previous position and the new position and that's going to keep happening for every movement of the vehicle it's going to take the previous position and it's going to update the position to the new position and draw a line between those using the leaflet polyline function and this is just updating randomly this vehicle is not a real vehicle of course so it's jittering about the map randomly it's not particularly coherent simulation but we're just demonstrating here how we can update positions on a map if I close the developer tool so you can see this is happening for are all five of our vehicles in the database every five seconds it's getting a new position and these are being updated on the page now you could extend this in all sorts of different ways you can store a historical list of every position that a given vehicle has been in if you do that of course your database is very rapidly going to grow in size so you might need some data engineering strategies to manage such large volumes of data for our purposes we just have a map where updating the map every five seconds without any refreshing of the page and we're doing that by using an Ajax function and javascript's set interval function to send a request to Django which responds with the data we need every five seconds so that's all for this video thank you for watching we've seen in this video how to use leaflet JS to plot the points of vehicles on a map we've seen how to add markers and polylines to feature grips within a leaflet map and how we can clear all of the elements within that feature group using the clear layers function and then add other elements after that and we've seen how to send an Ajax request every five seconds to get get the updated positions of these vehicles and then add them to the map and if you have any ideas about how we could extend this example please let me know in the comments I'm happy to try and make more videos on Django and leaflet JS but for now thank you for watching hope you've enjoyed the video please like And subscribe to the channel and we'll see you in the next video
Info
Channel: BugBytes
Views: 4,513
Rating: undefined out of 5
Keywords:
Id: vBd9Yy3tQPo
Channel Id: undefined
Length: 33min 17sec (1997 seconds)
Published: Mon Apr 17 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.