Skip to content

Commit

Permalink
Merge pull request #312 from ropable/master
Browse files Browse the repository at this point in the history
Refine DeviceStream view and detail JS to better maintain connection
  • Loading branch information
ropable authored Jan 7, 2025
2 parents b353fb6 + b9a3a24 commit 00c0428
Show file tree
Hide file tree
Showing 10 changed files with 63 additions and 48 deletions.
2 changes: 1 addition & 1 deletion kustomize/overlays/prod/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion resource_tracking/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
6 changes: 4 additions & 2 deletions tracking/static/js/device_detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -76,3 +77,4 @@ eventSource.onmessage = function (event) {
refreshTrackedDeviceLayer(trackedDeviceLayer, device);
toastRefresh.show();
};
eventSource.onerror = () => toastError.show();
2 changes: 1 addition & 1 deletion tracking/static/js/device_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 18 additions & 15 deletions tracking/static/js/map.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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],
});
Expand Down Expand Up @@ -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);
22 changes: 2 additions & 20 deletions tracking/templates/tracking/device_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,8 @@
integrity="sha512-Tndo4y/YJooD/mGXS9D6F1YyBcSyrWnnSWQ5Z9IcKt6bljicjyka9qcP99qMFbQ5+omfOtwwIapv1DjBCZcTJQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script>
// Constants injected during template rendering.
const geoserver_url = "{{ geoserver_url }}";
const device_list_url = "{% url 'device_list' %}";
const device_map_url = "{% url 'device_map' %}";
const device_geojson_url = "{% url 'device_download' %}";
const car_icon_url = "{% static 'img/car.png' %}";
const ute_icon_url = "{% static 'img/4wd_ute.png' %}";
const light_unit_icon_url = "{% static 'img/light_unit.png' %}";
const gang_truck_icon_url = "{% static 'img/gang_truck.png' %}";
const comms_bus_icon_url = "{% static 'img/comms_bus.png' %}";
const rotary_aircraft_icon_url = "{% static 'img/rotary.png' %}";
const plane_icon_url = "{% static 'img/plane.png' %}";
const dozer_icon_url = "{% static 'img/dozer.png' %}";
const loader_icon_url = "{% static 'img/loader.png' %}";
const float_icon_url = "{% static 'img/float.png' %}";
const fuel_truck_icon_url = "{% static 'img/fuel_truck.png' %}";
const person_icon_url = "{% static 'img/person.png' %}";
const other_icon_url = "{% static 'img/other.png' %}";
</script>
{% comment %}Make additional Javascript variables available from passed-in context.{% endcomment %}
{{ javascript_context|json_script:"javascript_context" }}
{% block extrajs %}{% endblock %}
</body>
</html>
1 change: 0 additions & 1 deletion tracking/templates/tracking/device_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
</div>
{% endblock %}
{% block extrajs %}
<script>const eventSource = new EventSource("{% url 'device_stream' pk=object.pk %}");</script>
<script src="{% static 'js/map.js' %}"></script>
<script src="{% static 'js/device_detail.js' %}"></script>
{% endblock %}
1 change: 1 addition & 0 deletions tracking/templates/tracking/device_map.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
</div>
{% endblock %}
{% block extrajs %}
{% comment %}Additional JavaScript variables are defined in the base template.{% endcomment %}
<script src="{% static 'js/map.js' %}"></script>
<script src="{% static 'js/device_map.js' %}"></script>
{% endblock %}
40 changes: 34 additions & 6 deletions tracking/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 00c0428

Please sign in to comment.