1(function initFindSnapshotUrl(root, factory) { 2 const api = factory(); 3 if (typeof module === "object" && module.exports) { 4 module.exports = api; 5 return; 6 } 7 const namespace = root.CrashCountFindCore = root.CrashCountFindCore || {}; 8 Object.assign(namespace, api); 9})(typeof globalThis !== "undefined" ? globalThis : this, function factory() { 10 const SELECTION_SHARE_PARAM_KEYS = [ 11 "q", 12 "label", 13 "layer", 14 "lat", 15 "lon", 16 "street_id", 17 "street_a", 18 "street_b", 19 "bike_lane_id", 20 "crash_id", 21 "collision_id", 22 "radius_m", 23 "radius", 24 "sel", 25 "auto" 26 ]; 27 28 const STATE_SHARE_PARAM_KEYS = [ 29 "compare", 30 "recent_start", 31 "recent_end", 32 "earlier_start", 33 "earlier_end", 34 "modes", 35 "mode" 36 ]; 37 38 const SHARE_PARAM_KEYS = [ 39 ...SELECTION_SHARE_PARAM_KEYS, 40 ...STATE_SHARE_PARAM_KEYS 41 ]; 42 43 const SNAPSHOT_BOOTSTRAP_PARAM_KEYS = [ 44 "version", 45 "window_start", 46 "window_end", 47 "exclude_highways" 48 ]; 49 50 function decodeLabelValue(value) { 51 if (!value) return ""; 52 try { 53 return decodeURIComponent(value); 54 } catch (err) { 55 return value; 56 } 57 } 58 59 function buildFindRedirectUrl(dataset, origin) { 60 const url = new URL("/find/", origin); 61 const label = decodeLabelValue(dataset.label || ""); 62 if (label) url.searchParams.set("q", label); 63 if (label) url.searchParams.set("label", label); 64 url.searchParams.set("auto", "1"); 65 if (dataset.layer) url.searchParams.set("layer", dataset.layer); 66 if (dataset.lat) url.searchParams.set("lat", dataset.lat); 67 if (dataset.lon) url.searchParams.set("lon", dataset.lon); 68 if (dataset.streetId) url.searchParams.set("street_id", dataset.streetId); 69 if (dataset.streetA) url.searchParams.set("street_a", dataset.streetA); 70 if (dataset.streetB) url.searchParams.set("street_b", dataset.streetB); 71 if (dataset.bikeLaneId) url.searchParams.set("bike_lane_id", dataset.bikeLaneId); 72 if (dataset.crashId) url.searchParams.set("crash_id", dataset.crashId); 73 const modes = decodeLabelValue(dataset.modes || ""); 74 const mode = decodeLabelValue(dataset.mode || ""); 75 if (modes) { 76 url.searchParams.set("modes", modes); 77 } else if (mode) { 78 url.searchParams.set("mode", mode); 79 } else if (dataset.layer === "bike_lane") { 80 url.searchParams.set("mode", "cyclist"); 81 } 82 const source = String(dataset.source || "").trim(); 83 if (source) url.searchParams.set("source", source); 84 return url.toString(); 85 } 86 87 function parseSelectionParams(params) { 88 if (!params) return null; 89 const label = decodeLabelValue(params.get("label") || params.get("q") || ""); 90 const layer = params.get("layer") || ""; 91 const latRaw = params.get("lat"); 92 const lonRaw = params.get("lon"); 93 const lat = latRaw ? Number(latRaw) : NaN; 94 const lon = lonRaw ? Number(lonRaw) : NaN; 95 const radiusRaw = params.get("radius_m") || params.get("radius") || ""; 96 const radiusMeters = radiusRaw ? Number(radiusRaw) : NaN; 97 const streetId = params.get("street_id") || ""; 98 const streetA = params.get("street_a") || ""; 99 const streetB = params.get("street_b") || ""; 100 const bikeLaneId = params.get("bike_lane_id") || ""; 101 const crashId = params.get("crash_id") || params.get("collision_id") || ""; 102 const hasLatLon = Number.isFinite(lat) && Number.isFinite(lon); 103 if (!streetId && !bikeLaneId && !(streetA && streetB) && !hasLatLon && !crashId) return null; 104 return { 105 bikeLaneId: bikeLaneId, 106 crashId: crashId, 107 label: label, 108 lat: lat, 109 layer: layer, 110 lon: lon, 111 radiusMeters: Number.isFinite(radiusMeters) && radiusMeters > 0 ? radiusMeters : null, 112 streetA: streetA, 113 streetB: streetB, 114 streetId: streetId 115 }; 116 } 117 118 function clearLegacyShareParams(params) { 119 if (!params) return; 120 SHARE_PARAM_KEYS.forEach(function deleteShareParam(key) { 121 params.delete(key); 122 }); 123 } 124 125 function clearSelectionShareParams(params) { 126 if (!params) return; 127 SELECTION_SHARE_PARAM_KEYS.forEach(function deleteShareParam(key) { 128 params.delete(key); 129 }); 130 } 131 132 function clearSnapshotBootstrapParams(params) { 133 if (!params) return;
134 SNAPSHOT_BOOTSTRAP_PARAM_KEYS.forEach(function deleteBootstrapParam(key) { 135 params.delete(key); 136 }); 137 } 138 139 function finalizeSnapshotSidUrl(params, options) { 140 if (!params) return; 141 const preserveSelectionFallback = !!(options && options.preserveSelectionFallback); 142 const preserveBootstrapFallback = !!(options && options.preserveBootstrapFallback); 143 const syncVersionUrlParam = options && typeof options.syncVersionUrlParam === "function" 144 ? options.syncVersionUrlParam 145 : function noop() {}; 146 if (!preserveSelectionFallback) { 147 clearLegacyShareParams(params); 148 } 149 syncVersionUrlParam(params); 150 if (!preserveBootstrapFallback) { 151 clearSnapshotBootstrapParams(params); 152 } 153 } 154 155 function formatShareCoord(value) { 156 if (!Number.isFinite(value)) return ""; 157 return Number(value).toFixed(6); 158 } 159 160 function serializeShareSelection(selection) { 161 if (!selection) return null; 162 const label = (selection.label || "").trim(); 163 const encodedLabel = label ? encodeURIComponent(label) : ""; 164 const lat = formatShareCoord(selection.lat); 165 const lon = formatShareCoord(selection.lon); 166 const radius = Number.isFinite(selection.radiusMeters) && selection.radiusMeters > 0 167 ? String(Math.round(selection.radiusMeters)) 168 : ""; 169 if (selection.streetId) { 170 return ["street", encodeURIComponent(selection.streetId), lat, lon, encodedLabel].join("|"); 171 } 172 if (selection.bikeLaneId) { 173 return ["bike_lane", encodeURIComponent(selection.bikeLaneId), lat, lon, encodedLabel].join("|"); 174 } 175 if (selection.streetA && selection.streetB) { 176 return [ 177 "intersection", 178 encodeURIComponent(selection.streetA), 179 encodeURIComponent(selection.streetB), 180 lat, 181 lon, 182 encodedLabel 183 ].join("|"); 184 } 185 if (selection.crashId) { 186 const parts = ["crash", encodeURIComponent(String(selection.crashId)), lat, lon, encodedLabel]; 187 if (radius) parts.push(radius); 188 return parts.join("|"); 189 } 190 if (lat && lon) { 191 const parts = ["point", lat, lon, selection.layer || "address", encodedLabel]; 192 if (radius) parts.push(radius); 193 return parts.join("|"); 194 } 195 return null; 196 } 197 198 function parseShareCoord(value) { 199 if (!value) return NaN; 200 const num = Number(value); 201 return Number.isFinite(num) ? num : NaN; 202 } 203 204 function shareKeyForSelection(selection) { 205 if (!selection) return null; 206 if (selection.streetId) return `street:${selection.streetId}`; 207 if (selection.bikeLaneId) return `bike_lane:${selection.bikeLaneId}`; 208 if (selection.streetA && selection.streetB) return `intersection:${selection.streetA}|${selection.streetB}`; 209 if (selection.crashId) return `crash:${selection.crashId}`; 210 if (Number.isFinite(selection.lat) && Number.isFinite(selection.lon)) { 211 const layer = selection.layer || "address"; 212 const radius = Number.isFinite(selection.radiusMeters) && selection.radiusMeters > 0 213 ? Math.round(selection.radiusMeters) 214 : null; 215 const radiusToken = radius ? `:r${radius}` : ""; 216 return `point:${layer}:${formatShareCoord(selection.lat)},${formatShareCoord(selection.lon)}${radiusToken}`; 217 } 218 return null; 219 } 220 221 function parseShareSelection(value) { 222 if (!value) return null; 223 const parts = String(value).split("|"); 224 const type = parts[0]; 225 if (type === "street") { 226 const tailIndex = Math.max(1, parts.length - 3); 227 return { 228 streetId: decodeLabelValue(parts.slice(1, tailIndex).join("|") || parts[1] || ""), 229 layer: "street", 230 lat: parseShareCoord(parts[parts.length - 3]), 231 lon: parseShareCoord(parts[parts.length - 2]), 232 label: decodeLabelValue(parts[parts.length - 1] || "") 233 }; 234 } 235 if (type === "bike_lane") { 236 const tailIndex = Math.max(1, parts.length - 3); 237 return { 238 bikeLaneId: decodeLabelValue(parts.slice(1, tailIndex).join("|") || parts[1] || ""), 239 layer: "bike_lane", 240 lat: parseShareCoord(parts[parts.length - 3]), 241 lon: parseShareCoord(parts[parts.length - 2]), 242 label: decodeLabelValue(parts[parts.length - 1] || "") 243 }; 244 } 245 if (type === "intersection") { 246 const tailIndex = Math.max(1, parts.length - 3); 247 const streetParts = parts.slice(1, tailIndex); 248 let streetA = ""; 249 let streetB = ""; 250 if (streetParts.length === 2) { 251 streetA = decodeLabelValue(streetParts[0] || ""); 252 streetB = decodeLabelValue(streetParts[1] || ""); 253 } else if (streetParts.length > 2) { 254 let splitIndex = -1; 255 for (let i = 2; i < streetParts.length - 1; i += 1) { 256 const token = streetParts[i]; 257 if (token === "all" || token === "0") { 258 splitIndex = i + 1; 259 break; 260 } 261 } 262 if (splitIndex === -1 && streetParts.length % 2 === 0) { 263 splitIndex = streetParts.length / 2; 264 } 265 if (splitIndex === -1) { 266 splitIndex = Math.min(3, streetParts.length - 1); 267 } 268 streetA = decodeLabelValue(streetParts.slice(0, splitIndex).join("|")); 269 streetB = decodeLabelValue(streetParts.slice(splitIndex).join("|")); 270 } 271 return { 272 streetA: streetA, 273 streetB: streetB, 274 layer: "intersection", 275 lat: parseShareCoord(parts[parts.length - 3]), 276 lon: parseShareCoord(parts[parts.length - 2]), 277 label: decodeLabelValue(parts[parts.length - 1] || "") 278 }; 279 } 280 if (type === "crash") { 281 const radiusMeters = parseShareCoord(parts[5]); 282 return { 283 crashId: decodeLabelValue(parts[1] || ""), 284 layer: "crash", 285 lat: parseShareCoord(parts[2]), 286 lon: parseShareCoord(parts[3]), 287 label: decodeLabelValue(parts[4] || ""), 288 radiusMeters: Number.isFinite(radiusMeters) && radiusMeters > 0 ? radiusMeters : null 289 }; 290 } 291 if (type === "point") { 292 const radiusMeters = parseShareCoord(parts[5]); 293 return { 294 layer: parts[3] || "address",
295 lat: parseShareCoord(parts[1]), 296 lon: parseShareCoord(parts[2]), 297 label: decodeLabelValue(parts[4] || ""), 298 radiusMeters: Number.isFinite(radiusMeters) && radiusMeters > 0 ? radiusMeters : null 299 }; 300 } 301 return null; 302 } 303 304 function parseSelectionsParams(params) { 305 if (!params) return []; 306 return params.getAll("sel").map(parseShareSelection).filter(Boolean); 307 } 308 309 function parseSnapshotShareSelections(selection) { 310 const raw = selection && typeof selection === "object" 311 ? (selection.share_selections || selection.shareSelections || []) 312 : []; 313 if (!Array.isArray(raw) || !raw.length) return []; 314 return raw.map(parseShareSelection).filter(Boolean); 315 } 316 317 function snapshotSelectionLabels(selection, normalizeSelectionLabel) { 318 const raw = selection && typeof selection === "object" 319 ? (selection.labels_by_select_cell || selection.labelsBySelectCell || {}) 320 : {}; 321 if (!raw || typeof raw !== "object") return []; 322 const normalize = typeof normalizeSelectionLabel === "function" 323 ? normalizeSelectionLabel 324 : function passthrough(value) { return String(value || "").trim(); }; 325 const seen = new Set(); 326 const labels = [];
327 Object.values(raw).forEach(function collectLabel(value) { 328 const label = normalize(value); 329 if (!label || seen.has(label)) return; 330 seen.add(label); 331 labels.push(label); 332 }); 333 return labels; 334 } 335 336 function hasSelectionDescriptor(selection) { 337 if (!selection) return false; 338 if (selection.streetId) return true; 339 if (selection.bikeLaneId) return true; 340 if (selection.streetA && selection.streetB) return true; 341 if (selection.crashId) return true; 342 return Number.isFinite(selection.lat) && Number.isFinite(selection.lon); 343 } 344 345 function shouldPreserveSnapshotFallbackParams(selections, pendingSelections) { 346 const resolved = Array.isArray(selections) ? selections.filter(Boolean) : []; 347 const pending = Array.isArray(pendingSelections) ? pendingSelections.filter(Boolean) : []; 348 const active = resolved.length ? resolved : pending; 349 if (active.length !== 1) return false; 350 return hasSelectionDescriptor(active[0]); 351 } 352 353 function buildSnapshotStateKey(state, options) { 354 const allModes = Array.isArray(options && options.allModes) ? options.allModes : []; 355 const timelineFilters = Array.isArray(options && options.timelineFilters) ? options.timelineFilters : []; 356 const ready = Boolean(state.series && state.compare && state.seriesSelectionVersion === state.selectionVersion); 357 const severity = state.severityFilter || "crashes"; 358 const modes = state.modeFilters || new Set(allModes); 359 const modesKey = allModes.filter(function activeMode(mode) { 360 return modes.has(mode); 361 }).join(","); 362 const ageKey = state.ageFilter || ""; 363 const hwKey = state.excludeHighways ? "1" : "0"; 364 const timelineKey = timelineFilters 365 .map(function timelineKeyEntry(entry) { return entry.key; }) 366 .filter(function enabledTimelineKey(key) { 367 return state.timelineFilters && state.timelineFilters.has(key); 368 }) 369 .join(","); 370 const compareKey = state.compareMode ? "1" : "0"; 371 const recent = state.compare && state.compare.recent ? state.compare.recent : null; 372 const earlier = state.compare && state.compare.earlier ? state.compare.earlier : null; 373 const recentKey = recent ? `${recent.start}-${recent.end}` : ""; 374 const earlierKey = earlier ? `${earlier.start}-${earlier.end}` : ""; 375 return [ 376 ready ? "1" : "0", 377 String(state.selectionVersion || 0), 378 severity, 379 modesKey, 380 ageKey, 381 hwKey, 382 timelineKey, 383 compareKey, 384 recentKey, 385 earlierKey 386 ].join("|"); 387 } 388 389 function buildSnapshotFromState(state, options) { 390 const allModes = Array.isArray(options && options.allModes) ? options.allModes : []; 391 const timelineFilters = Array.isArray(options && options.timelineFilters) ? options.timelineFilters : []; 392 const serialize = options && typeof options.serializeShareSelection === "function" 393 ? options.serializeShareSelection 394 : serializeShareSelection; 395 const resolvedDataVersion = options && typeof options.resolvedDataVersion === "function" 396 ? options.resolvedDataVersion 397 : function defaultResolvedDataVersion() { return ""; }; 398 const isConcreteVersion = options && typeof options.isConcreteVersion === "function" 399 ? options.isConcreteVersion 400 : function defaultIsConcreteVersion(value) { return !!String(value || "").trim(); }; 401 const selectRes = Number(options && options.selectRes); 402 const dataRes = Number(options && options.dataRes); 403 404 if (!state.selectedData || !state.selectedData.size) return null; 405 if (!state.series || !state.compare || state.seriesSelectionVersion !== state.selectionVersion) return null; 406 const months = state.series.months || []; 407 const recent = state.compare.recent; 408 if (!recent || !months[recent.start] || !months[recent.end]) return null; 409 const earlier = state.compare.earlier; 410 const earlierStart = earlier && months[earlier.start] ? months[earlier.start] : null; 411 const earlierEnd = earlier && months[earlier.end] ? months[earlier.end] : null; 412 const labels = {}; 413 if (state.selectionLabelIndex && typeof state.selectionLabelIndex.forEach === "function") { 414 state.selectionLabelIndex.forEach(function assignLabel(label, cell) { 415 if (!label) return; 416 labels[cell] = label; 417 }); 418 } 419 const modes = state.modeFilters || new Set(allModes); 420 const timeline = timelineFilters 421 .map(function timelineKeyEntry(entry) { return entry.key; }) 422 .filter(function enabledTimelineKey(key) {
423 return state.timelineFilters && state.timelineFilters.has(key); 424 }); 425 const shareSelections = Array.from((state.shareSelections && state.shareSelections.values()) || []) 426 .map(serialize) 427 .filter(Boolean); 428 const selection = { 429 data_cells: Array.from(state.selectedData), 430 labels_by_select_cell: labels 431 }; 432 if (shareSelections.length) selection.share_selections = shareSelections; 433 const snapshot = { 434 v: 1, 435 app: "crashfinder", 436 select_res: Number.isFinite(selectRes) ? selectRes : undefined, 437 data_res: Number.isFinite(dataRes) ? dataRes : undefined, 438 selection: selection, 439 filters: { 440 severity: state.severityFilter || "crashes", 441 modes: allModes.filter(function activeMode(mode) { 442 return modes.has(mode); 443 }), 444 age: state.ageFilter || null, 445 exclude_highways: !!state.excludeHighways, 446 timeline: timeline 447 }, 448 compare: { 449 enabled: !!state.compareMode, 450 recent_start: months[recent.start], 451 recent_end: months[recent.end], 452 earlier_start: earlierStart, 453 earlier_end: earlierEnd 454 } 455 }; 456 if (!Number.isFinite(snapshot.select_res)) delete snapshot.select_res; 457 if (!Number.isFinite(snapshot.data_res)) delete snapshot.data_res; 458 const dataVersion = resolvedDataVersion(); 459 if (!isConcreteVersion(dataVersion)) return null; 460 snapshot.data_version = dataVersion; 461 return snapshot; 462 } 463 464 function applyShareUrlState(params, options) { 465 if (!params) return; 466 const selections = Array.isArray(options && options.selections) ? options.selections.filter(Boolean) : []; 467 const pendingSelections = Array.isArray(options && options.pendingSelections) 468 ? options.pendingSelections.filter(Boolean) 469 : []; 470 const hasSid = !!(options && options.hasSid); 471 const preserveFallbackParamsWithSid = !!(options && options.preserveFallbackParamsWithSid); 472 const syncVersionUrlParam = options && typeof options.syncVersionUrlParam === "function" 473 ? options.syncVersionUrlParam 474 : function noop() {}; 475 const preserveSelectionFallback = shouldPreserveSnapshotFallbackParams(selections, pendingSelections); 476 const allowSelectionFallbackWithSid = hasSid && preserveFallbackParamsWithSid; 477 478 if (!selections.length && pendingSelections.length && !allowSelectionFallbackWithSid) { 479 syncVersionUrlParam(params); 480 return; 481 } 482 483 clearSelectionShareParams(params); 484 if (selections.length && (!hasSid || preserveSelectionFallback || allowSelectionFallbackWithSid)) { 485 applyLegacyShareParams(params, selections); 486 params.set("auto", "1"); 487 } 488 489 STATE_SHARE_PARAM_KEYS.forEach(function deleteShareParam(key) { 490 params.delete(key); 491 }); 492 493 const series = options && options.series; 494 const compare = options && options.compare; 495 if (options && options.compareMode && series && compare && compare.recent) { 496 const recent = compare.recent; 497 const recentStart = series.months && series.months[recent.start]; 498 const recentEnd = series.months && series.months[recent.end]; 499 if (recentStart && recentEnd) { 500 params.set("compare", "1"); 501 params.set("recent_start", recentStart); 502 params.set("recent_end", recentEnd); 503 const earlier = compare.earlier; 504 if (earlier) { 505 const earlierStart = series.months && series.months[earlier.start]; 506 const earlierEnd = series.months && series.months[earlier.end]; 507 if (earlierStart) params.set("earlier_start", earlierStart); 508 if (earlierEnd) params.set("earlier_end", earlierEnd); 509 } 510 } 511 } else if (options && options.pendingCompare && options.pendingCompare.compare) { 512 params.set("compare", "1"); 513 if (options.pendingCompare.recentStart) params.set("recent_start", options.pendingCompare.recentStart); 514 if (options.pendingCompare.recentEnd) params.set("recent_end", options.pendingCompare.recentEnd); 515 if (options.pendingCompare.earlierStart) params.set("earlier_start", options.pendingCompare.earlierStart); 516 if (options.pendingCompare.earlierEnd) params.set("earlier_end", options.pendingCompare.earlierEnd); 517 } 518 519 const modeFilters = Array.isArray(options && options.modeFilters) ? options.modeFilters.filter(
519Boolean) : []; 520 if (modeFilters.length) { 521 params.set("modes", modeFilters.join(",")); 522 } 523 524 syncVersionUrlParam(params); 525 if (hasSid && !preserveFallbackParamsWithSid) { 526 clearSnapshotBootstrapParams(params); 527 } 528 } 529 530 function applyLegacyShareParams(params, selections) { 531 if (!params) return; 532 const activeSelections = Array.isArray(selections) ? selections.filter(Boolean) : []; 533 if (!activeSelections.length) return; 534 if (activeSelections.length === 1) { 535 const selection = activeSelections[0]; 536 const label = (selection.label || "").trim(); 537 if (label) { 538 params.set("q", label); 539 params.set("label", label); 540 } 541 if (selection.layer) params.set("layer", selection.layer); 542 if (Number.isFinite(selection.lat)) params.set("lat", String(selection.lat)); 543 if (Number.isFinite(selection.lon)) params.set("lon", String(selection.lon)); 544 if (selection.streetId) params.set("street_id", selection.streetId); 545 if (selection.bikeLaneId) params.set("bike_lane_id", selection.bikeLaneId); 546 if (selection.streetA) params.set("street_a", selection.streetA); 547 if (selection.streetB) params.set("street_b", selection.streetB); 548 if (selection.crashId) params.set("crash_id", selection.crashId); 549 if (Number.isFinite(selection.radiusMeters) && selection.radiusMeters > 0) { 550 params.set("radius_m", String(Math.round(selection.radiusMeters))); 551 } 552 return; 553 } 554 activeSelections.forEach(function appendSelection(selection) { 555 const encoded = serializeShareSelection(selection); 556 if (encoded) params.append("sel", encoded); 557 }); 558 } 559 560 return { 561 SHARE_PARAM_KEYS: SHARE_PARAM_KEYS, 562 applyLegacyShareParams: applyLegacyShareParams, 563 applyShareUrlState: applyShareUrlState, 564 buildFindRedirectUrl: buildFindRedirectUrl, 565 buildSnapshotFromState: buildSnapshotFromState, 566 buildSnapshotStateKey: buildSnapshotStateKey, 567 clearLegacyShareParams: clearLegacyShareParams, 568 clearSelectionShareParams: clearSelectionShareParams, 569 clearSnapshotBootstrapParams: clearSnapshotBootstrapParams, 570 decodeLabelValue: decodeLabelValue, 571 finalizeSnapshotSidUrl: finalizeSnapshotSidUrl, 572 formatShareCoord: formatShareCoord, 573 hasSelectionDescriptor: hasSelectionDescriptor, 574 parseSelectionParams: parseSelectionParams, 575 parseSnapshotShareSelections: parseSnapshotShareSelections, 576 parseSelectionsParams: parseSelectionsParams, 577 parseShareSelection: parseShareSelection, 578 serializeShareSelection: serializeShareSelection, 579 shareKeyForSelection: shareKeyForSelection, 580 snapshotSelectionLabels: snapshotSelectionLabels, 581 shouldPreserveSnapshotFallbackParams: shouldPreserveSnapshotFallbackParams 582 }; 583});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.