|
| 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