Skip to content

Commit 9efc5d1

Browse files
committed
Create filter worker for mobile performance
1 parent 1843922 commit 9efc5d1

4 files changed

Lines changed: 444 additions & 12 deletions

File tree

service-worker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const STATIC_ASSETS = [
2929
'./src/js/config.js',
3030
'./src/js/data-loader.js',
3131
'./src/js/filters.js',
32+
'./src/js/filter-worker.js',
3233
'./src/js/map-renderer.js',
3334
'./src/js/state.js',
3435
'./src/js/ui.js',

src/js/data-loader.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,16 @@ export async function loadData() {
403403
linkCrashData();
404404

405405
// After linking, populate filter options and load boundaries
406-
const { populateFilterOptions } = await import('./filters.js');
406+
const { populateFilterOptions, initFilterWorker } = await import('./filters.js');
407407
const { updateMarkerColorLegend } = await import('./map-renderer.js');
408408

409409
populateFilterOptions();
410410
updateMarkerColorLegend();
411411

412+
// Initialise the filter worker now that crashData is fully linked.
413+
// The worker receives the data once; subsequent filter runs only send filter values.
414+
initFilterWorker();
415+
412416
// Load LGA boundaries (which will trigger initial filter application)
413417
await loadLGABoundaries();
414418

src/js/filter-worker.js

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/**
2+
* Filter Worker
3+
* Runs crash record matching off the main thread to keep the UI responsive.
4+
*
5+
* Protocol:
6+
* INIT { type:'INIT', crashData, heavyVehicleTypes } → READY
7+
* FILTER { type:'FILTER', id, filters } → RESULT { type:'RESULT', id, indicesBuffer }
8+
*
9+
* indicesBuffer is a transferred Uint32Array.buffer (zero-copy) holding the
10+
* indices into the original crashData array that passed every filter.
11+
*
12+
* NOTE: The drawn-area polygon filter is NOT handled here because it requires
13+
* turf.js, which is loaded as a CDN global on the main thread. The main thread
14+
* applies that filter as a post-step on the indices returned here.
15+
*/
16+
17+
let crashData = null;
18+
let HEAVY_VEHICLE_TYPES = [];
19+
20+
self.onmessage = function (e) {
21+
const { type } = e.data;
22+
23+
if (type === 'INIT') {
24+
crashData = e.data.crashData;
25+
HEAVY_VEHICLE_TYPES = e.data.heavyVehicleTypes;
26+
self.postMessage({ type: 'READY' });
27+
return;
28+
}
29+
30+
if (type === 'FILTER') {
31+
const { id, filters } = e.data;
32+
33+
if (!crashData) {
34+
const empty = new Uint32Array(0);
35+
self.postMessage({ type: 'RESULT', id, indicesBuffer: empty.buffer }, [empty.buffer]);
36+
return;
37+
}
38+
39+
const matching = [];
40+
for (let i = 0; i < crashData.length; i++) {
41+
const row = crashData[i];
42+
if (
43+
matchesBasicFilters(row, filters) &&
44+
matchesDateTimeFilters(row, filters) &&
45+
matchesCasualtyFilters(row, filters) &&
46+
matchesUnitsFilters(row, filters)
47+
) {
48+
matching.push(i);
49+
}
50+
}
51+
52+
const indicesBuffer = new Uint32Array(matching).buffer;
53+
self.postMessage({ type: 'RESULT', id, indicesBuffer }, [indicesBuffer]);
54+
return;
55+
}
56+
};
57+
58+
// ─── Filter functions ────────────────────────────────────────────────────────
59+
// These are intentionally self-contained copies of the functions in filters.js.
60+
// The draw-area filter block is omitted; it is applied on the main thread.
61+
62+
function matchesBasicFilters(row, filters) {
63+
const year = parseInt(row.Year);
64+
const severity = row['CSEF Severity'];
65+
66+
if (year < filters.yearFrom || year > filters.yearTo) return false;
67+
68+
if (!filters.selectedSeverities.includes('all') && !filters.selectedSeverities.includes(severity)) {
69+
return false;
70+
}
71+
72+
if (!filters.selectedCrashTypes.includes('all') && !filters.selectedCrashTypes.includes(row['Crash Type'])) {
73+
return false;
74+
}
75+
76+
if (filters.weather !== 'all' && row['Weather Cond'] !== filters.weather) return false;
77+
if (filters.dayNight !== 'all' && row.DayNight !== filters.dayNight) return false;
78+
79+
if (filters.duiInvolved !== 'all') {
80+
const duiValue = row['DUI Involved'] ? row['DUI Involved'].trim() : '';
81+
if (filters.duiInvolved === 'Yes' && duiValue !== 'Y') return false;
82+
if (filters.duiInvolved === 'No' && duiValue === 'Y') return false;
83+
}
84+
85+
if (filters.drugsInvolved !== 'all') {
86+
const drugsValue = row['Drugs Involved'] ? row['Drugs Involved'].trim() : '';
87+
if (filters.drugsInvolved === 'Yes' && drugsValue !== 'Y') return false;
88+
if (filters.drugsInvolved === 'No' && drugsValue === 'Y') return false;
89+
}
90+
91+
if (!filters.selectedAreas.includes('all') && !filters.selectedAreas.includes(row['LGA'])) {
92+
return false;
93+
}
94+
95+
if (!filters.selectedSuburbs.includes('all') && !filters.selectedSuburbs.includes(row.Suburb)) {
96+
return false;
97+
}
98+
99+
if (!filters.selectedRoadSurfaces.includes('all')) {
100+
if (!filters.selectedRoadSurfaces.includes(row['Road Surface'])) return false;
101+
}
102+
103+
if (!filters.selectedMoistureConds.includes('all')) {
104+
if (!filters.selectedMoistureConds.includes(row['Moisture Cond'])) return false;
105+
}
106+
107+
if (filters.selectedSpeedZones && !filters.selectedSpeedZones.includes('all')) {
108+
const speed = (row['Area Speed'] || '').trim();
109+
if (!filters.selectedSpeedZones.includes(speed)) return false;
110+
}
111+
112+
if (filters.selectedMonths && !filters.selectedMonths.includes('all')) {
113+
const dt = row['Crash Date Time'];
114+
if (!dt) return false;
115+
const datePart = dt.split(' ')[0];
116+
const dp = datePart ? datePart.split('/') : [];
117+
if (dp.length < 2) return false;
118+
const month = String(parseInt(dp[1]));
119+
if (!filters.selectedMonths.includes(month)) return false;
120+
}
121+
122+
return true;
123+
}
124+
125+
function matchesDateTimeFilters(row, filters) {
126+
const crashDateTime = row['Crash Date Time'];
127+
if (!crashDateTime) return filters.dateFrom || filters.dateTo || filters.timeFrom || filters.timeTo ? false : true;
128+
129+
const parts = crashDateTime.split(' ');
130+
131+
if (filters.dateFrom || filters.dateTo) {
132+
if (parts.length >= 1) {
133+
const dateParts = parts[0].split('/');
134+
if (dateParts.length === 3) {
135+
const crashDate = `${dateParts[2]}-${dateParts[1].padStart(2, '0')}-${dateParts[0].padStart(2, '0')}`;
136+
if (filters.dateFrom && crashDate < filters.dateFrom) return false;
137+
if (filters.dateTo && crashDate > filters.dateTo) return false;
138+
} else {
139+
return false;
140+
}
141+
} else {
142+
return false;
143+
}
144+
}
145+
146+
if (filters.timeFrom || filters.timeTo) {
147+
if (parts.length >= 2) {
148+
const crashTime = parts[1];
149+
if (filters.timeFrom && filters.timeTo) {
150+
if (filters.timeFrom <= filters.timeTo) {
151+
if (crashTime < filters.timeFrom || crashTime > filters.timeTo) return false;
152+
} else {
153+
// Crosses midnight (e.g. 22:00 to 02:00)
154+
if (crashTime < filters.timeFrom && crashTime > filters.timeTo) return false;
155+
}
156+
} else if (filters.timeFrom) {
157+
if (crashTime < filters.timeFrom) return false;
158+
} else if (filters.timeTo) {
159+
if (crashTime > filters.timeTo) return false;
160+
}
161+
} else {
162+
return false;
163+
}
164+
}
165+
166+
return true;
167+
}
168+
169+
function matchesCasualtyFilters(row, filters) {
170+
const casualties = row._casualties;
171+
if (!casualties || casualties.length === 0) {
172+
return filters.selectedRoadUsers.includes('all') &&
173+
filters.selectedAgeGroups.includes('all') &&
174+
filters.selectedSexes.includes('all') &&
175+
filters.selectedInjuries.includes('all') &&
176+
filters.selectedSeatBelts.includes('all') &&
177+
filters.selectedHelmets.includes('all');
178+
}
179+
180+
const hasRoadUserFilter = !filters.selectedRoadUsers.includes('all');
181+
const hasAgeFilter = !filters.selectedAgeGroups.includes('all');
182+
const hasSexFilter = !filters.selectedSexes.includes('all');
183+
const hasInjuryFilter = !filters.selectedInjuries.includes('all');
184+
const hasSeatBeltFilter = !filters.selectedSeatBelts.includes('all');
185+
const hasHelmetFilter = !filters.selectedHelmets.includes('all');
186+
187+
const hasAnyCasualtyFilter = hasRoadUserFilter || hasAgeFilter || hasSexFilter ||
188+
hasInjuryFilter || hasSeatBeltFilter || hasHelmetFilter;
189+
190+
if (!hasAnyCasualtyFilter) return true;
191+
192+
const roadUserSet = hasRoadUserFilter ? new Set(filters.selectedRoadUsers) : null;
193+
const sexSet = hasSexFilter ? new Set(filters.selectedSexes) : null;
194+
const injurySet = hasInjuryFilter ? new Set(filters.selectedInjuries) : null;
195+
const seatBeltSet = hasSeatBeltFilter ? new Set(filters.selectedSeatBelts) : null;
196+
const helmetSet = hasHelmetFilter ? new Set(filters.selectedHelmets) : null;
197+
198+
return casualties.some(casualty => {
199+
if (hasRoadUserFilter && !roadUserSet.has(casualty['Casualty Type'])) return false;
200+
201+
if (hasAgeFilter) {
202+
const age = parseInt(casualty.AGE);
203+
if (isNaN(age)) return false;
204+
const matchesAnyAgeGroup = filters.selectedAgeGroups.some(group => {
205+
if (group === '0-17') return age >= 0 && age <= 17;
206+
if (group === '18-25') return age >= 18 && age <= 25;
207+
if (group === '26-35') return age >= 26 && age <= 35;
208+
if (group === '36-50') return age >= 36 && age <= 50;
209+
if (group === '51-65') return age >= 51 && age <= 65;
210+
if (group === '66+') return age >= 66;
211+
return false;
212+
});
213+
if (!matchesAnyAgeGroup) return false;
214+
}
215+
216+
if (hasSexFilter && !sexSet.has(casualty.Sex)) return false;
217+
if (hasInjuryFilter && !injurySet.has(casualty['Injury Extent'])) return false;
218+
if (hasSeatBeltFilter && !seatBeltSet.has(casualty['Seat Belt'])) return false;
219+
if (hasHelmetFilter && !helmetSet.has(casualty.Helmet)) return false;
220+
221+
return true;
222+
});
223+
}
224+
225+
function matchesUnitsFilters(row, filters) {
226+
const units = row._units || [];
227+
228+
const hasVehicleTypeFilter = !filters.selectedVehicles.includes('all');
229+
const hasVehicleYearFilter = !filters.selectedVehicleYears.includes('all');
230+
const hasOccupantsFilter = !filters.selectedOccupants.includes('all');
231+
const hasLicenseTypeFilter = !filters.selectedLicenseTypes.includes('all');
232+
const hasRegStateFilter = !filters.selectedRegStates.includes('all');
233+
const hasDirectionFilter = !filters.selectedDirections.includes('all');
234+
const hasMovementFilter = !filters.selectedMovements.includes('all');
235+
const hasHeavyVehicleFilter = filters.heavyVehicle !== 'all';
236+
const hasTowingFilter = filters.towing !== 'all';
237+
const hasRolloverFilter = filters.rollover !== 'all';
238+
const hasFireFilter = filters.fire !== 'all';
239+
240+
const hasAnyUnitFilter = hasVehicleTypeFilter || hasVehicleYearFilter ||
241+
hasOccupantsFilter || hasLicenseTypeFilter ||
242+
hasRegStateFilter || hasDirectionFilter || hasMovementFilter ||
243+
hasHeavyVehicleFilter || hasTowingFilter || hasRolloverFilter || hasFireFilter;
244+
245+
if (!hasAnyUnitFilter) return true;
246+
if (units.length === 0) return false;
247+
248+
const vehicleTypeSet = hasVehicleTypeFilter ? new Set(filters.selectedVehicles) : null;
249+
const vehicleYearSet = hasVehicleYearFilter ? new Set(filters.selectedVehicleYears) : null;
250+
const occupantsSet = hasOccupantsFilter ? new Set(filters.selectedOccupants) : null;
251+
const licenseTypeSet = hasLicenseTypeFilter ? new Set(filters.selectedLicenseTypes) : null;
252+
const regStateSet = hasRegStateFilter ? new Set(filters.selectedRegStates) : null;
253+
const directionSet = hasDirectionFilter ? new Set(filters.selectedDirections) : null;
254+
const movementSet = hasMovementFilter ? new Set(filters.selectedMovements) : null;
255+
256+
return units.some(unit => {
257+
if (hasHeavyVehicleFilter) {
258+
const isHeavy = HEAVY_VEHICLE_TYPES.includes(unit['Unit Type']);
259+
if (filters.heavyVehicle === 'yes' && !isHeavy) return false;
260+
if (filters.heavyVehicle === 'no' && isHeavy) return false;
261+
}
262+
263+
if (hasTowingFilter) {
264+
const val = (unit.Towing || '').trim();
265+
const hasTowing = val !== '' && val !== 'Not Towing' && val !== 'Unknown';
266+
if (filters.towing === 'Yes' && !hasTowing) return false;
267+
if (filters.towing === 'No' && hasTowing) return false;
268+
}
269+
270+
if (hasRolloverFilter) {
271+
const hasRollover = unit.Rollover && unit.Rollover.trim() !== '';
272+
if (filters.rollover === 'Yes' && !hasRollover) return false;
273+
if (filters.rollover === 'No' && hasRollover) return false;
274+
}
275+
276+
if (hasFireFilter) {
277+
const hasFire = unit.Fire && unit.Fire.trim() !== '';
278+
if (filters.fire === 'Yes' && !hasFire) return false;
279+
if (filters.fire === 'No' && hasFire) return false;
280+
}
281+
282+
if (hasVehicleTypeFilter && !vehicleTypeSet.has(unit['Unit Type'])) return false;
283+
284+
if (hasVehicleYearFilter) {
285+
const year = parseInt(unit['Veh Year']);
286+
if (isNaN(year)) return false;
287+
const matches = filters.selectedVehicleYears.some(range => {
288+
if (range === 'pre-2000') return year < 2000;
289+
if (range === '2000-2010') return year >= 2000 && year <= 2010;
290+
if (range === '2011-2020') return year >= 2011 && year <= 2020;
291+
if (range === '2021+') return year >= 2021;
292+
return false;
293+
});
294+
if (!matches) return false;
295+
}
296+
297+
if (hasOccupantsFilter) {
298+
const occupantsStr = unit['Number Occupants'];
299+
if (occupantsStr !== undefined && occupantsStr !== null) {
300+
const occupants = parseInt(occupantsStr);
301+
if (!isNaN(occupants)) {
302+
if (occupants === 0) return false;
303+
const matches = filters.selectedOccupants.some(value => {
304+
if (value === '1') return occupants === 1;
305+
if (value === '2') return occupants === 2;
306+
if (value === '3') return occupants === 3;
307+
if (value === '4') return occupants === 4;
308+
if (value === '5+') return occupants >= 5;
309+
return false;
310+
});
311+
if (!matches) return false;
312+
} else {
313+
return false;
314+
}
315+
} else {
316+
return false;
317+
}
318+
}
319+
320+
if (hasLicenseTypeFilter && !licenseTypeSet.has(unit['Licence Type'])) return false;
321+
if (hasRegStateFilter && !regStateSet.has(unit['Veh Reg State'])) return false;
322+
if (hasDirectionFilter && !directionSet.has(unit['Direction Of Travel'])) return false;
323+
if (hasMovementFilter && !movementSet.has(unit['Unit Movement'])) return false;
324+
325+
return true;
326+
});
327+
}

0 commit comments

Comments
 (0)