From b3f3067ec5b8341db3e5ae7767facfc718135333 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sat, 7 Mar 2026 20:12:10 +1100 Subject: [PATCH 01/11] frao redraw timer to 10min --- src/pages/tasking/mapLayers/frao.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tasking/mapLayers/frao.js b/src/pages/tasking/mapLayers/frao.js index 83ee9c94..59a86740 100644 --- a/src/pages/tasking/mapLayers/frao.js +++ b/src/pages/tasking/mapLayers/frao.js @@ -76,7 +76,7 @@ export function renderFRAOSLayer(vm, map, getToken, apiHost, params) { vm.mapVM.registerPollingLayer("fraos", { label: "Active FRAOs", menuGroup: "NSW SES Geoservices", - refreshMs: 0, // 0 refresh. they dont change. only redraw on filter change + refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.fraos`) || false, fetchFn: async () => { const start = new Date(); From ce526ee541a264518714338a4503fdfdc8d8054a Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sat, 7 Mar 2026 20:12:16 +1100 Subject: [PATCH 02/11] unused file delete --- src/pages/tasking/mapLayers/common.js | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/pages/tasking/mapLayers/common.js diff --git a/src/pages/tasking/mapLayers/common.js b/src/pages/tasking/mapLayers/common.js deleted file mode 100644 index 07012419..00000000 --- a/src/pages/tasking/mapLayers/common.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import BeaconClient from '../../../shared/BeaconClient.js'; - -function getTransportApiKeyOpsLog(origin) { - switch (origin) { - case 'https://previewbeacon.ses.nsw.gov.au': - return '46273'; - case 'https://beacon.ses.nsw.gov.au': - return '515514'; - case 'https://trainbeacon.ses.nsw.gov.au': - return '36753'; - default: - return '0'; - } -} - From 3e55262eb26724c145240956c23757f178ab64fa Mon Sep 17 00:00:00 2001 From: Rowantrek <12322201+Rowantrek@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:13:31 +1100 Subject: [PATCH 03/11] Minor Feature Additions & Fixes (#334) * ICEMS Dictionary ICEMS Dictionary added for Timeline entries. * Alert + Page Config Touchups - Renamed 'Rescue jobs' to 'Rescue incidents' - Added an underline when you hover over an alert item. * Additional Address Info - Additional Address Info now included in Job Model. (is included in updateFromJSON function too - confirmed it updates with refresh) - Additional Address Info displayed under 'Incident Location' on expanded incidents. - Additional Address Info displayed under the address * Fixing my poor formatting * Making this the same as Address model --- src/pages/tasking/components/alerts.js | 2 + src/pages/tasking/components/job_popup.js | 3 + src/pages/tasking/main.js | 103 +++++++++++++++++++++- src/pages/tasking/models/Address.js | 1 + src/pages/tasking/models/Job.js | 1 + static/pages/tasking.html | 20 ++++- 6 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/pages/tasking/components/alerts.js b/src/pages/tasking/components/alerts.js index 9c90ead1..d7945c9c 100644 --- a/src/pages/tasking/components/alerts.js +++ b/src/pages/tasking/components/alerts.js @@ -125,6 +125,8 @@ function renderRules(container, rules) { if (typeof rule.onClick === 'function') { div.querySelectorAll('li[data-id]').forEach(li => { li.style.cursor = 'pointer'; + li.addEventListener('mouseenter', () => li.style.textDecoration = 'underline'); + li.addEventListener('mouseleave', () => li.style.textDecoration = ''); li.addEventListener('click', () => rule.onClick(li.getAttribute('data-id'))); }); } diff --git a/src/pages/tasking/components/job_popup.js b/src/pages/tasking/components/job_popup.js index e36f9ab7..71c30060 100644 --- a/src/pages/tasking/components/job_popup.js +++ b/src/pages/tasking/components/job_popup.js @@ -107,6 +107,9 @@ export function buildJobPopupKO() {
+ +
+
diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js index 6a1b09ac..150a8bc9 100644 --- a/src/pages/tasking/main.js +++ b/src/pages/tasking/main.js @@ -2946,7 +2946,108 @@ function VM() { }); })(); - +self.acronymDictionary = [ + { regex: 'AA', meaning: 'As Above' }, + { regex: 'ACK', meaning: 'Acknowledge' }, + { regex: 'ASNSW', meaning: 'NSW Ambulance' }, + { regex: 'ATTD', meaning: 'Attend' }, + { regex: 'B4', meaning: 'Before' }, + { regex: 'BET', meaning: 'Between' }, + { regex: 'BTW', meaning: 'Between OR By the Way' }, + { regex: 'C[4F]W', meaning: 'Concern for Welfare' }, + { regex: 'CNCLD', meaning: 'Cancelled' }, + { regex: 'CNR', meaning: 'Corner' }, + { regex: 'DEC', meaning: 'Deceased' }, + { regex: 'ETA', meaning: 'Estimated Time of Arrival' }, + { regex: 'ETC', meaning: 'Estimated Time of Completion' }, + { regex: 'FRNSW', meaning: 'NSW Fire & Rescue' }, + { regex: 'FB', meaning: 'Fire Brigade' }, + { regex: 'ICEMS', meaning: 'Inter CAD Electronic Messaging System' }, + { regex: 'LOC', meaning: 'Location' }, + { regex: 'LS', meaning: 'Last Seen' }, + { regex: 'MP', meaning: 'Missing Person(s)' }, + { regex: 'NESB', meaning: 'Non-English Speaking Background' }, + { regex: 'NFA', meaning: 'No Further Action' }, + { regex: 'NK', meaning: "Not Known" }, + { regex: 'NPI', meaning: 'No Person(s) Injured' }, + { regex: 'NPT', meaning: 'No Person(s) Trapped' }, + { regex: 'NFI', meaning: 'No Further Information' }, + { regex: 'NN[2T]A', meaning: 'No Need to Attend' }, + { regex: 'NSWPF', meaning: 'NSW Police Force' }, + { regex: 'POL', meaning: 'Police' }, + { regex: 'NVS', meaning: 'No Vehicle(s) Sighted' }, + { regex: 'OPP', meaning: 'Opposite' }, + { regex: 'OTW', meaning: 'On the Way' }, + { regex: 'P[2T]P', meaning: 'Pole to Pole (Powerlines)' }, + { regex: 'P[2T]H', meaning: 'Pole to House (Powerlines)' }, + { regex: '(\\d+)?PAX', meaning: 'Passenger' }, + { regex: 'PBY', meaning: 'Passer By' }, + { regex: 'POIS?', meaning: 'Person(s) Of Interest' }, + { regex: 'RCO', meaning: 'Police Radio Rescue Coordinator' }, + { regex: 'REQ', meaning: 'Require' }, + { regex: 'VEHS?', meaning: 'Vehicle(s)' }, + { regex: 'VOIS?', meaning: 'Vehicle(s) Of Interest' }, + { regex: 'TMC', meaning: 'Transport Management Center' }, + { regex: 'NSWTMC', meaning: 'NSW Transport Management Center' }, + { regex: 'KLO4', meaning: 'Keep a Look Out For' }, + { regex: 'INFTS?', meaning: 'Informant/Caller(s)' }, + { regex: '(\\d+)?POBS?', meaning: 'Person(s) On Board' }, + { regex: 'POSS', meaning: 'Possible' }, + { regex: 'PTS?', meaning: 'Patient(s) OR Person(s) Trapped' }, + { regex: 'VICT?', meaning: 'Victim(s)' }, + { regex: 'YR', meaning: 'Years Old' }, + { regex: 'YO', meaning: 'Years Old' }, + { regex: 'YOM', meaning: 'Year Old Male' }, + { regex: 'YOF', meaning: 'Year Old Female' }, + { regex: 'THX', meaning: 'Thanks' }, +]; + +// Custom Knockout Binding Handler (only define once) +if (!ko.bindingHandlers.acronymText) { + ko.bindingHandlers.acronymText = { + init: function(element, valueAccessor) { + var value = ko.unwrap(valueAccessor()); + var $element = $(element); + + var contentOrig = $element.html(); + var contentRepl = contentOrig; + + // Apply acronym dictionary + self.acronymDictionary.forEach(function(item) { + contentRepl = contentRepl.replace( + new RegExp('\\b(' + item.regex + ')\\b', 'gi'), + '$1' + ); + }); + + if (contentRepl != contentOrig) { + $element.html(contentRepl); + } + }, + update: function(element, valueAccessor) { + var value = ko.unwrap(valueAccessor()); + var $element = $(element); + + // Reset to plain text first + $element.text(value); + + var contentOrig = $element.html(); + var contentRepl = contentOrig; + + // Apply acronym dictionary + self.acronymDictionary.forEach(function(item) { + contentRepl = contentRepl.replace( + new RegExp('\\b(' + item.regex + ')\\b', 'gi'), + '$1' + ); + }); + + if (contentRepl != contentOrig) { + $element.html(contentRepl); + } + } + }; +} } diff --git a/src/pages/tasking/models/Address.js b/src/pages/tasking/models/Address.js index 82f3a5a2..c479ce8a 100644 --- a/src/pages/tasking/models/Address.js +++ b/src/pages/tasking/models/Address.js @@ -10,6 +10,7 @@ export function Address(data = {}) { this.locality = ko.observable(data.Locality ?? ""); this.postCode = ko.observable(data.PostCode ?? ""); this.prettyAddress = ko.observable(data.PrettyAddress ?? ""); + this.additionalAddressInfo = ko.observable(data.AdditionalAddressInfo ?? null); this.latLng = ko.pureComputed(() => { const lat = +this.latitude(), lng = +this.longitude(); diff --git a/src/pages/tasking/models/Job.js b/src/pages/tasking/models/Job.js index 56537c0e..456e659e 100644 --- a/src/pages/tasking/models/Job.js +++ b/src/pages/tasking/models/Job.js @@ -602,6 +602,7 @@ export function Job(data = {}, deps = {}) { this.address.locality(d.Address?.Locality ?? ""); this.address.postCode(d.Address?.PostCode ?? ""); this.address.prettyAddress(d.Address?.PrettyAddress ?? ""); + this.address.additionalAddressInfo(d.Address?.AdditionalAddressInfo ?? null); } if (Array.isArray(d.Tags)) { diff --git a/static/pages/tasking.html b/static/pages/tasking.html index 28cb0a8a..aed108b2 100644 --- a/static/pages/tasking.html +++ b/static/pages/tasking.html @@ -1316,6 +1316,20 @@
+ + +
+ +
+

+

+
+
+ +
@@ -2832,7 +2846,7 @@
- +
@@ -2847,7 +2861,7 @@
-
+
From 18720f852ef730f93062429c212ba8e55798bd30 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sat, 7 Mar 2026 20:16:26 +1100 Subject: [PATCH 04/11] another job vs incident word slip --- static/pages/tasking.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/pages/tasking.html b/static/pages/tasking.html index aed108b2..aa2468df 100644 --- a/static/pages/tasking.html +++ b/static/pages/tasking.html @@ -2565,7 +2565,7 @@

- When enabled, Rescue-priority jobs cluster with + When enabled, Rescue priority incidents cluster with other markers.
From 8a1970be6e113ab6d84ca9eec8e73de7f5a65d86 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sat, 7 Mar 2026 20:42:19 +1100 Subject: [PATCH 05/11] refactor (#335) Moved it out of main, added a snappier mouse over --- src/pages/tasking/components/acronymText.js | 163 ++++++++++++++++++++ src/pages/tasking/main.js | 105 +------------ 2 files changed, 165 insertions(+), 103 deletions(-) create mode 100644 src/pages/tasking/components/acronymText.js diff --git a/src/pages/tasking/components/acronymText.js b/src/pages/tasking/components/acronymText.js new file mode 100644 index 00000000..34fdbc76 --- /dev/null +++ b/src/pages/tasking/components/acronymText.js @@ -0,0 +1,163 @@ +import ko from 'knockout'; +import $ from 'jquery'; + +/** + * Acronym dictionary for emergency services and common abbreviations + */ +export const acronymDictionary = [ + { regex: 'AA', meaning: 'As Above' }, + { regex: 'ACK', meaning: 'Acknowledge' }, + { regex: 'ASNSW', meaning: 'NSW Ambulance' }, + { regex: 'ATTD', meaning: 'Attend' }, + { regex: 'B4', meaning: 'Before' }, + { regex: 'BET', meaning: 'Between' }, + { regex: 'BTW', meaning: 'Between OR By the Way' }, + { regex: 'C[4F]W', meaning: 'Concern for Welfare' }, + { regex: 'CNCLD', meaning: 'Cancelled' }, + { regex: 'CNR', meaning: 'Corner' }, + { regex: 'DEC', meaning: 'Deceased' }, + { regex: 'ETA', meaning: 'Estimated Time of Arrival' }, + { regex: 'ETC', meaning: 'Estimated Time of Completion' }, + { regex: 'FRNSW', meaning: 'NSW Fire & Rescue' }, + { regex: 'FB', meaning: 'Fire Brigade' }, + { regex: 'ICEMS', meaning: 'Inter CAD Electronic Messaging System' }, + { regex: 'LOC', meaning: 'Location' }, + { regex: 'LS', meaning: 'Last Seen' }, + { regex: 'MP', meaning: 'Missing Person(s)' }, + { regex: 'NESB', meaning: 'Non-English Speaking Background' }, + { regex: 'NFA', meaning: 'No Further Action' }, + { regex: 'NK', meaning: "Not Known" }, + { regex: 'NPI', meaning: 'No Person(s) Injured' }, + { regex: 'NPT', meaning: 'No Person(s) Trapped' }, + { regex: 'NFI', meaning: 'No Further Information' }, + { regex: 'NN[2T]A', meaning: 'No Need to Attend' }, + { regex: 'NSWPF', meaning: 'NSW Police Force' }, + { regex: 'POL', meaning: 'Police' }, + { regex: 'NVS', meaning: 'No Vehicle(s) Sighted' }, + { regex: 'OPP', meaning: 'Opposite' }, + { regex: 'OTW', meaning: 'On the Way' }, + { regex: 'P[2T]P', meaning: 'Pole to Pole (Powerlines)' }, + { regex: 'P[2T]H', meaning: 'Pole to House (Powerlines)' }, + { regex: '(\\d+)?PAX', meaning: 'Passenger' }, + { regex: 'PBY', meaning: 'Passer By' }, + { regex: 'POIS?', meaning: 'Person(s) Of Interest' }, + { regex: 'RCO', meaning: 'Police Radio Rescue Coordinator' }, + { regex: 'REQ', meaning: 'Require' }, + { regex: 'VEHS?', meaning: 'Vehicle(s)' }, + { regex: 'VOIS?', meaning: 'Vehicle(s) Of Interest' }, + { regex: 'TMC', meaning: 'Transport Management Center' }, + { regex: 'NSWTMC', meaning: 'NSW Transport Management Center' }, + { regex: 'KLO4', meaning: 'Keep a Look Out For' }, + { regex: 'INFTS?', meaning: 'Informant/Caller(s)' }, + { regex: '(\\d+)?POBS?', meaning: 'Person(s) On Board' }, + { regex: 'POSS', meaning: 'Possible' }, + { regex: 'PTS?', meaning: 'Patient(s) OR Person(s) Trapped' }, + { regex: 'VICT?', meaning: 'Victim(s)' }, + { regex: 'YR', meaning: 'Years Old' }, + { regex: 'YO', meaning: 'Years Old' }, + { regex: 'YOM', meaning: 'Year Old Male' }, + { regex: 'YOF', meaning: 'Year Old Female' }, + { regex: 'THX', meaning: 'Thanks' }, +]; + +/** + * Register the acronymText Knockout binding handler + * Replaces matching acronyms with tags showing their meanings on hover + */ +export function registerAcronymTextBinding() { + if (ko.bindingHandlers.acronymText) { + return; // Already registered + } + + /** + * Attach custom tooltips to abbr elements (similar to rainviewer legend) + */ + function attachAbbreviationTooltips(element) { + const abbrs = element.querySelectorAll('abbr[data-meaning]'); + abbrs.forEach((abbr) => { + const meaning = abbr.getAttribute('data-meaning'); + if (!meaning) return; + + // Position: relative is needed for absolute tooltip positioning + abbr.style.position = 'relative'; + abbr.style.cursor = 'help'; + abbr.style.textDecoration = 'underline'; + abbr.style.textDecorationStyle = 'dotted'; + + // Create tooltip div + const tooltip = document.createElement('div'); + tooltip.style.position = 'absolute'; + tooltip.style.bottom = '100%'; + tooltip.style.left = '50%'; + tooltip.style.transform = 'translateX(-50%)'; + tooltip.style.marginBottom = '6px'; + tooltip.style.padding = '6px 8px'; + tooltip.style.backgroundColor = '#333'; + tooltip.style.color = '#fff'; + tooltip.style.fontSize = '11px'; + tooltip.style.fontWeight = '500'; + tooltip.style.whiteSpace = 'nowrap'; + tooltip.style.borderRadius = '3px'; + tooltip.style.pointerEvents = 'none'; + tooltip.style.zIndex = '1001'; + tooltip.style.opacity = '0'; + tooltip.style.transition = 'opacity 0.1s ease'; + tooltip.textContent = meaning; + abbr.appendChild(tooltip); + + // Hover handlers + abbr.addEventListener('mouseenter', () => { + tooltip.style.opacity = '1'; + }); + abbr.addEventListener('mouseleave', () => { + tooltip.style.opacity = '0'; + }); + }); + } + + ko.bindingHandlers.acronymText = { + init: function(element, valueAccessor) { + ko.unwrap(valueAccessor()); // Trigger dependency tracking + var $element = $(element); + + var contentOrig = $element.html(); + var contentRepl = contentOrig; + + // Apply acronym dictionary + acronymDictionary.forEach(function(item) { + contentRepl = contentRepl.replace( + new RegExp('\\b(' + item.regex + ')\\b', 'gi'), + '$1' + ); + }); + + if (contentRepl != contentOrig) { + $element.html(contentRepl); + attachAbbreviationTooltips(element); + } + }, + update: function(element, valueAccessor) { + var value = ko.unwrap(valueAccessor()); + var $element = $(element); + + // Reset to plain text first + $element.text(value); + + var contentOrig = $element.html(); + var contentRepl = contentOrig; + + // Apply acronym dictionary + acronymDictionary.forEach(function(item) { + contentRepl = contentRepl.replace( + new RegExp('\\b(' + item.regex + ')\\b', 'gi'), + '$1' + ); + }); + + if (contentRepl != contentOrig) { + $element.html(contentRepl); + attachAbbreviationTooltips(element); + } + } + }; +} diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js index 150a8bc9..ecca7c41 100644 --- a/src/pages/tasking/main.js +++ b/src/pages/tasking/main.js @@ -28,6 +28,7 @@ import IncidentImagesModalVM from "./viewmodels/IncidentImagesModalVM"; import { installAlerts } from './components/alerts.js'; import { LegendControl } from './components/legend.js'; import { SpotlightSearchVM } from "./components/spotlightSearch.js"; +import { registerAcronymTextBinding } from "./components/acronymText.js"; import { Asset } from './models/Asset.js'; @@ -2946,109 +2947,6 @@ function VM() { }); })(); -self.acronymDictionary = [ - { regex: 'AA', meaning: 'As Above' }, - { regex: 'ACK', meaning: 'Acknowledge' }, - { regex: 'ASNSW', meaning: 'NSW Ambulance' }, - { regex: 'ATTD', meaning: 'Attend' }, - { regex: 'B4', meaning: 'Before' }, - { regex: 'BET', meaning: 'Between' }, - { regex: 'BTW', meaning: 'Between OR By the Way' }, - { regex: 'C[4F]W', meaning: 'Concern for Welfare' }, - { regex: 'CNCLD', meaning: 'Cancelled' }, - { regex: 'CNR', meaning: 'Corner' }, - { regex: 'DEC', meaning: 'Deceased' }, - { regex: 'ETA', meaning: 'Estimated Time of Arrival' }, - { regex: 'ETC', meaning: 'Estimated Time of Completion' }, - { regex: 'FRNSW', meaning: 'NSW Fire & Rescue' }, - { regex: 'FB', meaning: 'Fire Brigade' }, - { regex: 'ICEMS', meaning: 'Inter CAD Electronic Messaging System' }, - { regex: 'LOC', meaning: 'Location' }, - { regex: 'LS', meaning: 'Last Seen' }, - { regex: 'MP', meaning: 'Missing Person(s)' }, - { regex: 'NESB', meaning: 'Non-English Speaking Background' }, - { regex: 'NFA', meaning: 'No Further Action' }, - { regex: 'NK', meaning: "Not Known" }, - { regex: 'NPI', meaning: 'No Person(s) Injured' }, - { regex: 'NPT', meaning: 'No Person(s) Trapped' }, - { regex: 'NFI', meaning: 'No Further Information' }, - { regex: 'NN[2T]A', meaning: 'No Need to Attend' }, - { regex: 'NSWPF', meaning: 'NSW Police Force' }, - { regex: 'POL', meaning: 'Police' }, - { regex: 'NVS', meaning: 'No Vehicle(s) Sighted' }, - { regex: 'OPP', meaning: 'Opposite' }, - { regex: 'OTW', meaning: 'On the Way' }, - { regex: 'P[2T]P', meaning: 'Pole to Pole (Powerlines)' }, - { regex: 'P[2T]H', meaning: 'Pole to House (Powerlines)' }, - { regex: '(\\d+)?PAX', meaning: 'Passenger' }, - { regex: 'PBY', meaning: 'Passer By' }, - { regex: 'POIS?', meaning: 'Person(s) Of Interest' }, - { regex: 'RCO', meaning: 'Police Radio Rescue Coordinator' }, - { regex: 'REQ', meaning: 'Require' }, - { regex: 'VEHS?', meaning: 'Vehicle(s)' }, - { regex: 'VOIS?', meaning: 'Vehicle(s) Of Interest' }, - { regex: 'TMC', meaning: 'Transport Management Center' }, - { regex: 'NSWTMC', meaning: 'NSW Transport Management Center' }, - { regex: 'KLO4', meaning: 'Keep a Look Out For' }, - { regex: 'INFTS?', meaning: 'Informant/Caller(s)' }, - { regex: '(\\d+)?POBS?', meaning: 'Person(s) On Board' }, - { regex: 'POSS', meaning: 'Possible' }, - { regex: 'PTS?', meaning: 'Patient(s) OR Person(s) Trapped' }, - { regex: 'VICT?', meaning: 'Victim(s)' }, - { regex: 'YR', meaning: 'Years Old' }, - { regex: 'YO', meaning: 'Years Old' }, - { regex: 'YOM', meaning: 'Year Old Male' }, - { regex: 'YOF', meaning: 'Year Old Female' }, - { regex: 'THX', meaning: 'Thanks' }, -]; - -// Custom Knockout Binding Handler (only define once) -if (!ko.bindingHandlers.acronymText) { - ko.bindingHandlers.acronymText = { - init: function(element, valueAccessor) { - var value = ko.unwrap(valueAccessor()); - var $element = $(element); - - var contentOrig = $element.html(); - var contentRepl = contentOrig; - - // Apply acronym dictionary - self.acronymDictionary.forEach(function(item) { - contentRepl = contentRepl.replace( - new RegExp('\\b(' + item.regex + ')\\b', 'gi'), - '$1' - ); - }); - - if (contentRepl != contentOrig) { - $element.html(contentRepl); - } - }, - update: function(element, valueAccessor) { - var value = ko.unwrap(valueAccessor()); - var $element = $(element); - - // Reset to plain text first - $element.text(value); - - var contentOrig = $element.html(); - var contentRepl = contentOrig; - - // Apply acronym dictionary - self.acronymDictionary.forEach(function(item) { - contentRepl = contentRepl.replace( - new RegExp('\\b(' + item.regex + ')\\b', 'gi'), - '$1' - ); - }); - - if (contentRepl != contentOrig) { - $element.html(contentRepl); - } - } - }; -} - } window.addEventListener('resize', () => map.invalidateSize()); @@ -3104,6 +3002,7 @@ document.addEventListener('DOMContentLoaded', function () { installDragDropRowBindings(); noBubbleFromDisabledButtonsBindings(); installSortableArrayBindings(); + registerAcronymTextBinding(); ko.bindingProvider.instance = new ksb(options); window.ko = ko; From 760bb27bcd507f5fc9c3d3f563741c3872be1156 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sat, 7 Mar 2026 21:55:20 +1100 Subject: [PATCH 06/11] map layer name updates --- src/pages/tasking/viewmodels/Config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tasking/viewmodels/Config.js b/src/pages/tasking/viewmodels/Config.js index ed7b86bd..d4a60a97 100644 --- a/src/pages/tasking/viewmodels/Config.js +++ b/src/pages/tasking/viewmodels/Config.js @@ -46,8 +46,8 @@ export function ConfigVM(root, deps) { self.paneDefs = [ { id: 'pane-tippy-top', name: 'Incident markers' }, { id: 'pane-top', name: 'Asset markers' }, - { id: 'pane-middle', name: 'Map overlays icons' }, - { id: 'pane-lowest', name: 'Map overlay polygons' } + { id: 'pane-middle', name: 'Map overlays icons & labels' }, + { id: 'pane-lowest', name: 'Map overlay polygons & drawings' } ]; // UI list (objects so bindings are property-only) From 112ef18e5cf38dd7195435836bff1e8d4debd332 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sat, 7 Mar 2026 23:30:40 +1100 Subject: [PATCH 07/11] correct format --- src/pages/tasking/mapLayers/weather.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tasking/mapLayers/weather.js b/src/pages/tasking/mapLayers/weather.js index 0189459d..097d881b 100644 --- a/src/pages/tasking/mapLayers/weather.js +++ b/src/pages/tasking/mapLayers/weather.js @@ -135,7 +135,7 @@ export function registerBOMThunderstormTrackingLayer(vm, sourceUrl) { }, drawFn: (layerGroup, _data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { - layers: ["IDR00011","IDR00011_track"], + layers: "IDR00011,IDR00011_track", styles: "default", format: "image/png", transparent: true, From a2624f31821d037eb4195ad4761b0905dab844a3 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sun, 8 Mar 2026 00:01:52 +1100 Subject: [PATCH 08/11] map layers messing with zoom limits crashing clusters Root cause is likely cluster grid mismatch when extra overlays change map zoom limits (zoomlevelschange). leaflet.markercluster keeps zoom-indexed internal grids; when those become stale, addLayer can hit an undefined grid and throw at getNearObject. --- src/pages/tasking/markers/jobMarker.js | 7 ++- src/pages/tasking/viewmodels/Map.js | 64 +++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/pages/tasking/markers/jobMarker.js b/src/pages/tasking/markers/jobMarker.js index cc764e3c..8d37f3e8 100644 --- a/src/pages/tasking/markers/jobMarker.js +++ b/src/pages/tasking/markers/jobMarker.js @@ -90,7 +90,12 @@ export function addOrUpdateJobMarker(ko, map, vm, job) { marker._isRescue = isRescue; marker._isNew = (job.statusName?.() || '').toLowerCase() === 'new'; marker._priorityColor = style.fill || '#6b7280'; - marker.addTo(targetLayer); + if (targetLayer === vm.mapVM.jobClusterGroup) { + marker.addTo(targetLayer); + //vm.mapVM.safeAddToClusterGroup?.(marker); + } else { + marker.addTo(targetLayer); + } markers.set(id, marker); job.marker = marker; diff --git a/src/pages/tasking/viewmodels/Map.js b/src/pages/tasking/viewmodels/Map.js index 7234d81a..e9dbf27b 100644 --- a/src/pages/tasking/viewmodels/Map.js +++ b/src/pages/tasking/viewmodels/Map.js @@ -359,6 +359,58 @@ export function MapVM(Lmap, root) { self._syncPulseRings(); }; + /** + * Ensure markercluster internal distance grids are compatible with the + * map's current min/max zoom constraints. + * + * Some overlay layers can change map zoom levels (`zoomlevelschange`). + * markercluster keeps prebuilt grids keyed by zoom level; if map minZoom + * drops below those keys, `addLayer` can hit an undefined grid entry and + * throw in `_addLayer`. + */ + self.ensureClusterGridCompatibility = function () { + const cg = self.jobClusterGroup; + if (!cg || !cg._map) return; + + const minZoom = Math.floor(self.map.getMinZoom()); + const expectedTopZoom = minZoom - 1; + const disableAt = cg.options?.disableClusteringAtZoom; + const expectedMaxZoom = Number.isFinite(disableAt) + ? (disableAt - 1) + : Math.ceil(self.map.getMaxZoom()); + + const grids = cg._gridClusters; + const unclustered = cg._gridUnclustered; + const gridsMissingForMinZoom = !grids || !unclustered || !grids[minZoom] || !unclustered[minZoom]; + const maxZoomMismatch = !Number.isFinite(cg._maxZoom) || cg._maxZoom !== expectedMaxZoom; + const topZoomMismatch = !cg._topClusterLevel || !Number.isFinite(cg._topClusterLevel._zoom) + || cg._topClusterLevel._zoom !== expectedTopZoom; + + const safeCurrentZoom = Number.isFinite(self.map.getZoom()) + ? Math.round(self.map.getZoom()) + : (expectedTopZoom + 1); + + if (!gridsMissingForMinZoom && !maxZoomMismatch && !topZoomMismatch) { + const minClusterZoom = (cg._topClusterLevel?._zoom ?? expectedTopZoom) + 1; + if (!Number.isFinite(cg._zoom) || cg._zoom < minClusterZoom) { + cg._zoom = Math.max(safeCurrentZoom, minClusterZoom); + } + return; + } + + const markers = []; + cg.eachLayer((m) => markers.push(m)); + + // clearLayers() on an attached cluster group rebuilds internal grids. + cg.clearLayers(); + const minClusterZoom = (cg._topClusterLevel?._zoom ?? expectedTopZoom) + 1; + cg._zoom = Math.max(safeCurrentZoom, minClusterZoom); + if (markers.length) cg.addLayers(markers); + }; + + // Reconcile cluster grids whenever any layer changes map min/max zooms. + self.map.on('zoomlevelschange', self.ensureClusterGridCompatibility); + self.applyPaneOrder = function (paneOrderTopToBottom) { if (!Array.isArray(paneOrderTopToBottom) || paneOrderTopToBottom.length === 0) return; @@ -919,7 +971,17 @@ export function MapVM(Lmap, root) { return; } - const visibleParent = self.jobClusterGroup.getVisibleParent(marker); + let visibleParent; + try { + visibleParent = self.jobClusterGroup.getVisibleParent(marker); + } catch (err) { + // On error, hide the pulse ring to be safe + if (self.jobPulseLayer.hasLayer(marker._pulseRing)) { + self.jobPulseLayer.removeLayer(marker._pulseRing); + } + return; + } + if (visibleParent === marker) { // Marker is individually visible – show pulse ring if (!self.jobPulseLayer.hasLayer(marker._pulseRing)) { From 08b89bf168bdbff80dbb519c27cda6a34d10c152 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sun, 8 Mar 2026 00:07:51 +1100 Subject: [PATCH 09/11] cache busting on WMS Layers --- src/pages/tasking/mapLayers/weather.js | 134 +++++++++++++------------ 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/src/pages/tasking/mapLayers/weather.js b/src/pages/tasking/mapLayers/weather.js index 097d881b..c7938058 100644 --- a/src/pages/tasking/mapLayers/weather.js +++ b/src/pages/tasking/mapLayers/weather.js @@ -10,10 +10,8 @@ export function registerBOMAllFloodLevelsLayer(vm, sourceUrl) { menuGroup: "BOM Observations", refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomAllFlood`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(allUrl, { layers: "IDN62011_all", styles: "default", @@ -23,6 +21,7 @@ export function registerBOMAllFloodLevelsLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }); layerGroup.addLayer(wmsLayer); }, @@ -39,10 +38,8 @@ export function registerBOMRainfallLayer(vm, sourceUrl) { menuGroup: "BOM Observations", refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomRainfall`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(rainfallUrl, { layers: "IDZ20010_rainfall_9am", styles: "default", @@ -52,6 +49,7 @@ export function registerBOMRainfallLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, // Default NSW bounding box, but map will handle view }); layerGroup.addLayer(wmsLayer); @@ -70,10 +68,8 @@ export function registerBOMRadarLayer(vm, sourceUrl) { menuGroup: "BOM Radar & Satellite", refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomRadar`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { layers: "IDR00010", styles: "default", @@ -83,6 +79,7 @@ export function registerBOMRadarLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }); layerGroup.addLayer(wmsLayer); }, @@ -100,10 +97,8 @@ export function registerBOMSatTrueColorLayer(vm, sourceUrl) { menuGroup: "BOM Radar & Satellite", refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomSatTrueColor`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { layers: "IDE00435", styles: "default", @@ -112,7 +107,8 @@ export function registerBOMSatTrueColorLayer(vm, sourceUrl) { bgcolor: "0xFFFFFF", version: "1.3.0", crs: L.CRS.EPSG4326, - attribution: "Japan Meteorological Agency via Bureau of Meteorology" + attribution: "Japan Meteorological Agency via Bureau of Meteorology", + _cb: data?.cacheBuster, }); layerGroup.addLayer(wmsLayer); }, @@ -131,9 +127,11 @@ export function registerBOMThunderstormTrackingLayer(vm, sourceUrl) { refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomThunderstormTracking`) || false, fetchFn: async () => { - return {}; + // Force a unique tile URL each refresh so browser/proxy caches don't + // keep serving old thunderstorm tiles. + return { cacheBuster: Date.now() }; }, - drawFn: (layerGroup, _data) => { + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { layers: "IDR00011,IDR00011_track", styles: "default", @@ -143,7 +141,8 @@ export function registerBOMThunderstormTrackingLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", - opacity: 0.7 + opacity: 0.7, + _cb: data?.cacheBuster }); layerGroup.addLayer(wmsLayer); }, @@ -161,10 +160,8 @@ export function registerBOMWindLayer(vm, sourceUrl) { refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomWind`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { layers: ["IDY25026_windpt","IDY25026_windrast"], styles: "default", @@ -174,7 +171,8 @@ export function registerBOMWindLayer(vm, sourceUrl) { opacity: 0.5, // Adjust opacity for more transparency version: "1.3.0", crs: L.CRS.EPSG4326, - attribution: "Bureau of Meteorology" + attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }); layerGroup.addLayer(wmsLayer); }, @@ -191,10 +189,8 @@ export function registerBOMMSLPLayer(vm, sourceUrl) { menuGroup: "BOM Forecasts", refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomMSLP`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { layers: ["IDY25026_mslp"], styles: "default", @@ -203,7 +199,8 @@ export function registerBOMMSLPLayer(vm, sourceUrl) { bgcolor: "0xFFFFFF", version: "1.3.0", crs: L.CRS.EPSG4326, - attribution: "Bureau of Meteorology" + attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }); layerGroup.addLayer(wmsLayer); }, @@ -221,10 +218,8 @@ export function registerBOMLightningLayer(vm, sourceUrl) { menuGroup: "BOM Radar & Satellite", refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomLightning`) || false, - fetchFn: async () => { - return {}; - }, - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { const wmsLayer = L.tileLayer.wms(radarUrl, { layers: ["IDZ20019_c2g_2h"], styles: "default", @@ -234,6 +229,7 @@ export function registerBOMLightningLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }); layerGroup.addLayer(wmsLayer); }, @@ -251,8 +247,8 @@ export function registerBOMLightning24hLayer(vm, sourceUrl) { menuGroup: "BOM Radar & Satellite", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomLightning24h`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20019_c2g_2-24h", @@ -262,6 +258,7 @@ export function registerBOMLightning24hLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -279,8 +276,8 @@ export function registerBOMTsunamiLayer(vm, sourceUrl) { menuGroup: "BOM Warnings", refreshMs: 300000, // 5 min – critical warning visibleByDefault: localStorage.getItem(`ov.bomTsunami`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: ["IDZ20002", "IDZ20002_info"], @@ -290,6 +287,7 @@ export function registerBOMTsunamiLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -307,8 +305,8 @@ export function registerBOMTropicalCycloneLayer(vm, sourceUrl) { menuGroup: "BOM Warnings", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomTropicalCyclone`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: [ @@ -325,6 +323,7 @@ export function registerBOMTropicalCycloneLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -342,8 +341,8 @@ export function registerBOMFireDangerRatingLayer(vm, sourceUrl) { menuGroup: "BOM Forecasts", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomFireDangerRating`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20022000", @@ -354,6 +353,7 @@ export function registerBOMFireDangerRatingLayer(vm, sourceUrl) { crs: L.CRS.EPSG4326, opacity: 0.6, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -371,8 +371,8 @@ export function registerBOMHeatwaveLayer(vm, sourceUrl) { menuGroup: "BOM Forecasts", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomHeatwave`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDY10012_day0", @@ -383,6 +383,7 @@ export function registerBOMHeatwaveLayer(vm, sourceUrl) { crs: L.CRS.EPSG4326, opacity: 0.6, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -400,8 +401,8 @@ export function registerBOMHazardousSurfLayer(vm, sourceUrl) { menuGroup: "BOM Warnings", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomHazardousSurf`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20017000", @@ -411,6 +412,7 @@ export function registerBOMHazardousSurfLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -428,8 +430,8 @@ export function registerBOMCoastalHazardLayer(vm, sourceUrl) { menuGroup: "BOM Warnings", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomCoastalHazard`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20023", @@ -439,6 +441,7 @@ export function registerBOMCoastalHazardLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -456,8 +459,8 @@ export function registerBOMRoadWeatherLayer(vm, sourceUrl) { menuGroup: "BOM Warnings", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomRoadWeather`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20014", @@ -467,6 +470,7 @@ export function registerBOMRoadWeatherLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -484,8 +488,8 @@ export function registerBOMSurfaceGustLayer(vm, sourceUrl) { menuGroup: "BOM Observations", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomSurfaceGust`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20010_gustkmh", @@ -495,6 +499,7 @@ export function registerBOMSurfaceGustLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -512,8 +517,8 @@ export function registerBOMSurfaceTempLayer(vm, sourceUrl) { menuGroup: "BOM Observations", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomSurfaceTemp`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ20010_air_temperature", @@ -523,6 +528,7 @@ export function registerBOMSurfaceTempLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -540,8 +546,8 @@ export function registerBOMFireBehaviourIndexLayer(vm, sourceUrl) { menuGroup: "BOM Forecasts", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomFireBehaviourIndex`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ10135", @@ -552,6 +558,7 @@ export function registerBOMFireBehaviourIndexLayer(vm, sourceUrl) { crs: L.CRS.EPSG4326, opacity: 0.6, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -569,8 +576,8 @@ export function registerBOMHazardousWindLayer(vm, sourceUrl) { menuGroup: "BOM Forecasts", refreshMs: 600000, visibleByDefault: localStorage.getItem(`ov.bomHazardousWind`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDZ71153", @@ -581,6 +588,7 @@ export function registerBOMHazardousWindLayer(vm, sourceUrl) { crs: L.CRS.EPSG4326, opacity: 0.6, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -598,8 +606,8 @@ export function registerBOMFloodWarningBoundariesLayer(vm, sourceUrl) { menuGroup: "BOM Observations", refreshMs: 3600000, // 1 hr – reference data visibleByDefault: localStorage.getItem(`ov.bomFloodWarningBoundaries`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: "IDM00017", @@ -609,6 +617,7 @@ export function registerBOMFloodWarningBoundariesLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, @@ -626,8 +635,8 @@ export function registerBOMFireWeatherDistrictsLayer(vm, sourceUrl) { menuGroup: "BOM Observations", refreshMs: 3600000, // 1 hr – reference data visibleByDefault: localStorage.getItem(`ov.bomFireWeatherDistricts`) || false, - fetchFn: async () => ({}), - drawFn: (layerGroup, _data) => { + fetchFn: async () => ({ cacheBuster: Date.now() }), + drawFn: (layerGroup, data) => { layerGroup.addLayer( L.tileLayer.wms(wmsUrl, { layers: ["IDM00007", "IDM00021"], @@ -637,6 +646,7 @@ export function registerBOMFireWeatherDistrictsLayer(vm, sourceUrl) { version: "1.3.0", crs: L.CRS.EPSG4326, attribution: "Bureau of Meteorology", + _cb: data?.cacheBuster, }) ); }, From db0a84290acec5b1e9eabbec4008ea85b6582f04 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sun, 8 Mar 2026 00:09:42 +1100 Subject: [PATCH 10/11] formatting fix --- src/pages/tasking/mapLayers/weather.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/tasking/mapLayers/weather.js b/src/pages/tasking/mapLayers/weather.js index c7938058..1d5fd37a 100644 --- a/src/pages/tasking/mapLayers/weather.js +++ b/src/pages/tasking/mapLayers/weather.js @@ -127,8 +127,6 @@ export function registerBOMThunderstormTrackingLayer(vm, sourceUrl) { refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomThunderstormTracking`) || false, fetchFn: async () => { - // Force a unique tile URL each refresh so browser/proxy caches don't - // keep serving old thunderstorm tiles. return { cacheBuster: Date.now() }; }, drawFn: (layerGroup, data) => { @@ -157,7 +155,6 @@ export function registerBOMWindLayer(vm, sourceUrl) { vm.mapVM.registerPollingLayer("bomWind", { label: "BOM Wind Barbs & Raster", menuGroup: "BOM Forecasts", - refreshMs: 600000, // 10 min visibleByDefault: localStorage.getItem(`ov.bomWind`) || false, fetchFn: async () => ({ cacheBuster: Date.now() }), From bae478916f9c30eba77cd78162c405fa5ba16703 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Sun, 8 Mar 2026 00:12:40 +1100 Subject: [PATCH 11/11] rainviewer cache bustering --- src/pages/tasking/mapLayers/rainviewer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/tasking/mapLayers/rainviewer.js b/src/pages/tasking/mapLayers/rainviewer.js index aba015b6..ed595099 100644 --- a/src/pages/tasking/mapLayers/rainviewer.js +++ b/src/pages/tasking/mapLayers/rainviewer.js @@ -150,7 +150,9 @@ export function registerRainRadarLayer(vm, map) { /* ── data loading ──────────────────────────────────────────── */ async function loadFrames(layerGroup) { try { - const res = await fetch(RAINVIEWER_API); + const res = await fetch(`${RAINVIEWER_API}?_cb=${Date.now()}`, { + cache: "no-store", + }); if (!res.ok) throw new Error(`RainViewer API ${res.status}`); const json = await res.json(); const past = json.radar?.past;