diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml index 823e17d..905729c 100644 --- a/kustomize/overlays/prod/kustomization.yaml +++ b/kustomize/overlays/prod/kustomization.yaml @@ -29,4 +29,4 @@ patches: - path: service_patch.yaml images: - name: ghcr.io/dbca-wa/resource_tracking - newTag: 1.4.23 + newTag: 1.4.24 diff --git a/pyproject.toml b/pyproject.toml index bc74b07..f9fd8da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "resource_tracking" -version = "1.4.23" +version = "1.4.24" description = "DBCA internal corporate application to download and serve data from remote tracking devices." authors = ["DBCA OIM "] license = "Apache-2.0" diff --git a/resource_tracking/workers.py b/resource_tracking/workers.py index 912b92e..4dab7b8 100644 --- a/resource_tracking/workers.py +++ b/resource_tracking/workers.py @@ -6,4 +6,4 @@ class UvicornWorker(BaseUvicornWorker): # UvicornWorker doesn't support the lifespan protocol. # Reference: https://stackoverflow.com/a/75996092/14508 - CONFIG_KWARGS: Dict[str, Any] = {"loop": "auto", "http": "auto", "lifespan": "off"} + CONFIG_KWARGS: Dict[str, Any] = {"loop": "auto", "http": "auto", "lifespan": "off", "timeout_keep_alive": 30} diff --git a/tracking/static/js/device_detail.js b/tracking/static/js/device_detail.js index 2c6e22b..e274e51 100644 --- a/tracking/static/js/device_detail.js +++ b/tracking/static/js/device_detail.js @@ -39,7 +39,7 @@ const trackedDeviceLayer = L.geoJSON(null, {}).addTo(map); // Layers control. L.control.layers(baseMaps, overlayMaps).addTo(map); // Link to device map view. -L.easyButton("fa-solid fa-map", () => window.open(device_map_url, "_self"), "Device map", "idDeviceMapControl").addTo(map); +L.easyButton("fa-solid fa-map", () => window.open(context.device_map_url, "_self"), "Device map", "idDeviceMapControl").addTo(map); // Function to consume streamed device data and repopulate the layer. function refreshTrackedDeviceLayer(trackedDeviceLayer, device) { @@ -57,7 +57,8 @@ function refreshTrackedDeviceLayer(trackedDeviceLayer, device) { map.flyTo([point.lat, point.lon], map.getZoom()); } -// The EventSource object is defined on the HTML template. +// The EventSource URL is defined on the HTML template. +let eventSource = new EventSource(context.event_source_url); // Ping event, to help maintain the connection. let ping = 0; eventSource.addEventListener("ping", function (event) { @@ -76,3 +77,4 @@ eventSource.onmessage = function (event) { refreshTrackedDeviceLayer(trackedDeviceLayer, device); toastRefresh.show(); }; +eventSource.onerror = () => toastError.show(); diff --git a/tracking/static/js/device_map.js b/tracking/static/js/device_map.js index 5cd6fcd..8cc32b3 100644 --- a/tracking/static/js/device_map.js +++ b/tracking/static/js/device_map.js @@ -58,7 +58,7 @@ function refreshTrackedDevicesLayer(trackedDevicesLayer) { // Remove any existing data from the layer. trackedDevicesLayer.clearLayers(); // Query the API endpoint for device data. - fetch(device_geojson_url) + fetch(context.device_geojson_url) // Parse the response as JSON. .then((resp) => resp.json()) // Replace the data in the tracked devices layer. diff --git a/tracking/static/js/map.js b/tracking/static/js/map.js index 5c5b8e0..116a410 100644 --- a/tracking/static/js/map.js +++ b/tracking/static/js/map.js @@ -1,7 +1,10 @@ "use strict"; +// Parse additional variables from the DOM element. +const context = JSON.parse(document.getElementById("javascript_context").textContent); + const geoserver_wmts_url = - geoserver_url + + context.geoserver_url + "/gwc/service/wmts?service=WMTS&request=GetTile&version=1.0.0&tilematrixset=mercator&tilematrix=mercator:{z}&tilecol={x}&tilerow={y}"; const geoserver_wmts_url_base = geoserver_wmts_url + "&format=image/jpeg"; const geoserver_wmts_url_overlay = geoserver_wmts_url + "&format=image/png"; @@ -26,67 +29,67 @@ const lgaBoundaries = L.tileLayer(geoserver_wmts_url_overlay + "&layer=cddp:loca // Icon classes (note that URLs are injected into the base template.) const iconCar = L.icon({ - iconUrl: car_icon_url, + iconUrl: context.car_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconUte = L.icon({ - iconUrl: ute_icon_url, + iconUrl: context.ute_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconLightUnit = L.icon({ - iconUrl: light_unit_icon_url, + iconUrl: context.light_unit_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconGangTruck = L.icon({ - iconUrl: gang_truck_icon_url, + iconUrl: context.gang_truck_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconCommsBus = L.icon({ - iconUrl: comms_bus_icon_url, + iconUrl: context.comms_bus_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconRotary = L.icon({ - iconUrl: rotary_aircraft_icon_url, + iconUrl: context.rotary_aircraft_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconPlane = L.icon({ - iconUrl: plane_icon_url, + iconUrl: context.plane_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconDozer = L.icon({ - iconUrl: dozer_icon_url, + iconUrl: context.dozer_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconLoader = L.icon({ - iconUrl: loader_icon_url, + iconUrl: context.loader_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconFloat = L.icon({ - iconUrl: float_icon_url, + iconUrl: context.float_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconFuelTruck = L.icon({ - iconUrl: fuel_truck_icon_url, + iconUrl: context.fuel_truck_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconPerson = L.icon({ - iconUrl: person_icon_url, + iconUrl: context.person_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); const iconOther = L.icon({ - iconUrl: other_icon_url, + iconUrl: context.other_icon_url, iconSize: [32, 32], iconAnchor: [16, 16], }); @@ -120,4 +123,4 @@ L.control.scale({ maxWidth: 500, imperial: false }).addTo(map); // Fullscreen control L.control.fullscreen().addTo(map); // Link to device list view. -L.easyButton("fa-solid fa-list", () => window.open(device_list_url, "_self"), "Device list", "idDeviceListControl").addTo(map); +L.easyButton("fa-solid fa-list", () => window.open(context.device_list_url, "_self"), "Device list", "idDeviceListControl").addTo(map); diff --git a/tracking/templates/tracking/device_base.html b/tracking/templates/tracking/device_base.html index 66b394f..7c567f9 100644 --- a/tracking/templates/tracking/device_base.html +++ b/tracking/templates/tracking/device_base.html @@ -66,26 +66,8 @@ integrity="sha512-Tndo4y/YJooD/mGXS9D6F1YyBcSyrWnnSWQ5Z9IcKt6bljicjyka9qcP99qMFbQ5+omfOtwwIapv1DjBCZcTJQ==" crossorigin="anonymous" referrerpolicy="no-referrer"> - + {% comment %}Make additional Javascript variables available from passed-in context.{% endcomment %} + {{ javascript_context|json_script:"javascript_context" }} {% block extrajs %}{% endblock %} diff --git a/tracking/templates/tracking/device_detail.html b/tracking/templates/tracking/device_detail.html index 9dcff8b..5394503 100644 --- a/tracking/templates/tracking/device_detail.html +++ b/tracking/templates/tracking/device_detail.html @@ -49,7 +49,6 @@ {% endblock %} {% block extrajs %} - {% endblock %} diff --git a/tracking/templates/tracking/device_map.html b/tracking/templates/tracking/device_map.html index 715a908..f00821d 100644 --- a/tracking/templates/tracking/device_map.html +++ b/tracking/templates/tracking/device_map.html @@ -53,6 +53,7 @@ {% endblock %} {% block extrajs %} + {% comment %}Additional JavaScript variables are defined in the base template.{% endcomment %} {% endblock %} diff --git a/tracking/views.py b/tracking/views.py index a8e78fc..61a8d7a 100644 --- a/tracking/views.py +++ b/tracking/views.py @@ -7,12 +7,32 @@ from django.core.serializers import serialize from django.db.models import Q from django.http import HttpResponse, HttpResponseBadRequest, StreamingHttpResponse +from django.urls import reverse from django.utils import timezone from django.views.generic import DetailView, ListView, TemplateView, View from tracking.api import CSVSerializer from tracking.models import Device, LoggedPoint +# Define a dictionary of context variables to supply to JavaScript in view templates. +# NOTE: we can't include values needing `reverse` in the dict below due to circular imports. +JAVASCRIPT_CONTEXT = { + "geoserver_url": settings.GEOSERVER_URL, + "car_icon_url": f"{settings.STATIC_URL}img/car.png", + "ute_icon_url": f"{settings.STATIC_URL}img/4wd_ute.png", + "light_unit_icon_url": f"{settings.STATIC_URL}img/light_unit.png", + "gang_truck_icon_url": f"{settings.STATIC_URL}img/gang_truck.png", + "comms_bus_icon_url": f"{settings.STATIC_URL}img/comms_bus.png", + "rotary_aircraft_icon_url": f"{settings.STATIC_URL}img/rotary.png", + "plane_icon_url": f"{settings.STATIC_URL}img/plane.png", + "dozer_icon_url": f"{settings.STATIC_URL}img/dozer.png", + "loader_icon_url": f"{settings.STATIC_URL}img/loader.png", + "float_icon_url": f"{settings.STATIC_URL}img/float.png", + "fuel_truck_icon_url": f"{settings.STATIC_URL}img/fuel_truck.png", + "person_icon_url": f"{settings.STATIC_URL}img/person.png", + "other_icon_url": f"{settings.STATIC_URL}img/other.png", +} + class DeviceMap(TemplateView): """A map view displaying all device locations.""" @@ -23,7 +43,10 @@ class DeviceMap(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_title"] = "DBCA Resource Tracking device map" - context["geoserver_url"] = settings.GEOSERVER_URL + context["javascript_context"] = JAVASCRIPT_CONTEXT + context["javascript_context"]["device_list_url"] = reverse("device_list") + context["javascript_context"]["device_map_url"] = reverse("device_map") + context["javascript_context"]["device_geojson_url"] = reverse("device_download") return context @@ -76,7 +99,11 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) obj = self.get_object() context["page_title"] = f"DBCA Resource Tracking device {obj.deviceid}" - context["geoserver_url"] = settings.GEOSERVER_URL + context["javascript_context"] = JAVASCRIPT_CONTEXT + context["javascript_context"]["device_list_url"] = reverse("device_list") + context["javascript_context"]["device_map_url"] = reverse("device_map") + context["javascript_context"]["device_geojson_url"] = reverse("device_download") + context["javascript_context"]["event_source_url"] = reverse("device_stream", kwargs={"pk": obj.pk}) return context @@ -327,17 +354,18 @@ async def stream(self, *args, **kwargs): except: data = {} - # # Only send a message event if the device location has changed. + # Include a recommended retry delay for reconnections of 15000 ms. + # Reference: https://javascript.info/server-sent-events if device and device.point.ewkt != last_location: last_location = device.point.ewkt - yield f"data: {data}\n\n" + yield f"event: message\nretry: 15000\ndata: {data}\n\n" else: # Always send a ping to keep the connection open. - yield "event: ping\ndata: {}\n\n" + yield "event: ping\nretry: 15000\ndata: {}\n\n" # Sleep for a period before repeating. - await asyncio.sleep(30) + await asyncio.sleep(10) async def get(self, request, *args, **kwargs): return StreamingHttpResponse(