diff --git a/src/App.svelte b/src/App.svelte index 85f6cd8..7e1b2e9 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -57,11 +57,15 @@ {row.delay[0]} ({Math.round(parseFloat(row.delay[1]))}) {#each row.vc as vc, index} - {#if parseFloat(vc) >= criticaVC} + {#if row.type != "hcm-unsignalized"} + {#if parseFloat(vc) >= criticaVC} + {row.movements[index]} ({vc.padEnd(4, "0")})
+ {/if} + {:else} {row.movements[index]} ({vc.padEnd(4, "0")})
{/if} {:else} - Error + -- {/each} diff --git a/src/parse.ts b/src/parse.ts index 2795595..0e38075 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -5,9 +5,11 @@ type rowGroup = row[]; export interface intersectionData { name: string; type: string; + control: string; delay: string[]; movements: string[]; vc: string[]; + [key: string]: any; } export type results = { [key: string]: intersectionData; @@ -43,10 +45,10 @@ export function parseResults(results: rowGroup[]) { let table: results = {}; results.forEach((intersection) => { - let rowData: any = { + let rowData: Partial = { delay: ["Error", "0"], vc: [], - } satisfies Partial; + }; let name = intersection[1][1]; rowData.name = name; @@ -54,21 +56,32 @@ export function parseResults(results: rowGroup[]) { let [label, data] = parseRow(line); if (label) rowData[label as string] = data; }); + rowData.movements = parseLaneConfig(rowData); // skip synchro unsignalized - if (rowData.type == "synchro" && rowData.signal == "Unsignalized") return; + if (rowData.type == "synchro" && rowData.control == "Unsignalized") return; // skip queues if (rowData.type == "synchro-queues") return; - // identify side street delay + // identify unsignalized delay and v/c if (rowData.type == "hcm-unsignalized") { - let control = rowData.sign; - let delay = rowData.movementDelay; + if (rowData.los) { + // all-way stop + rowData.control = "All-way Stop"; + rowData.delay = [rowData.los, rowData.delay]; + } else { + // side street stop + rowData.control = "Side-street Stop"; + let delays: number[] = rowData.movementDelay.map((d: string) => parseFloat(d)); + let i = delays.indexOf(Math.max(...delays)); + rowData.delay = [rowData.movementLOS[i], rowData.movementDelay[i]]; + rowData.vc = [rowData.movementVC[i]]; + rowData.movements = [rowData.movements[i]]; + } } - rowData.movements = parseLaneConfig(rowData.laneGroup, rowData.laneConfig); - table[name] = rowData; + table[name] = rowData as intersectionData; }); return table; @@ -91,8 +104,10 @@ export function parseRow(line: row) { case "Lane Configurations": // synchro case "Lanes": // HCM2000 unsignalized return ["laneConfig", line.slice(2)]; - case "Control Type": // HCM2000 signalized - return ["signal", line[1]]; + case "Control Type": // synchro + return ["control", line[1]]; + case "Direction, Lane #": // HCM2000 unsignalized + return ["lanes", line.slice(2)]; case "Sign Control": // HCM2000 unsignalized return ["sign", line.slice(2)]; @@ -100,20 +115,29 @@ export function parseRow(line: row) { return ["delay", [line[7], line[1]]]; case "HCM 2000 Control Delay": // HCM2000 signalized return ["delay", [line[10], line[4]]]; + case "v/c Ratio": // synchro and HCM2000 signalized + return ["vc", line.slice(2)]; + + case "Level of Service": // HCM2000 unsignalized, all-way stop + return ["los", line[4]]; + case "Delay": // HCM2000 unsignalized, all-way stop + return ["delay", line[4]]; case "Control Delay (s)": // HCM2000 unsignalized, per movement return ["movementDelay", line.slice(2)]; - // TODO: need LOS - // case "Average Delay": // HCM2000 unsignalized, intersection average - // return ["delay", line[4]]; - case "v/c Ratio": - return ["vc", line.slice(2)]; + case "Lane LOS": // HCM2000 unsignalized, per movement + return ["movementLOS", line.slice(2)]; + case "Volume to Capacity": // HCM2000 unsignalized, per movement + return ["movementVC", line.slice(2)]; } return []; } -export function parseLaneConfig(group: string[], config: string[]) { +export function parseLaneConfig(rowData: Partial) { let movements: string[] = []; - group.forEach((lane, index) => { + + let group: string[] = rowData.laneGroup; + let config: string[] = rowData.laneConfig; + group.forEach((lane, index: number) => { if (config[index] == "0" || config[index] == "") { movements.push("n/a"); return; @@ -130,5 +154,11 @@ export function parseLaneConfig(group: string[], config: string[]) { movements.push(movement); }); + if (rowData.type == "hcm-unsignalized") { + let lanes: string[] = rowData.lanes; + + movements = lanes; + } + return movements; } diff --git a/tests/HCM2000-Signalized.test.ts b/tests/HCM2000-Signalized.test.ts index 4267e7c..b678d80 100644 --- a/tests/HCM2000-Signalized.test.ts +++ b/tests/HCM2000-Signalized.test.ts @@ -18,10 +18,20 @@ describe("parse HCM2000 signalized results", () => { let groups = groupByIntersection(data); let results = parseResults(groups); - test("names", () => { + test("name", () => { expect(Object.keys(results)).toEqual(["Side1 & Main", "Side2 & Main", "Main & Side3"]); }); + test("type", () => { + let type = []; + let control = []; + for (const intersection of Object.values(results)) { + type.push(intersection.type); + control.push(intersection.control); + } + expect(type).toEqual(["hcm-signalized", "hcm-signalized", "hcm-signalized"]); + }); + test("lane configuration", () => { let config = []; for (const intersection of Object.values(results)) { diff --git a/tests/HCM2000-Unsignalized.test.ts b/tests/HCM2000-Unsignalized.test.ts new file mode 100644 index 0000000..478171b --- /dev/null +++ b/tests/HCM2000-Unsignalized.test.ts @@ -0,0 +1,88 @@ +import { readFileSync } from "fs"; +import { describe, expect, test } from "vitest"; + +import { groupByIntersection, parseCSV, parseResults } from "src/parse"; + +const input = readFileSync("tests/sample-output/HCM2000_Unsignalized.txt", "utf-8").replaceAll(":", " "); + +test("read file into intersections", () => { + let data = parseCSV(input); + expect(data).toHaveLength(119); + + let groups = groupByIntersection(data); + expect(groups).toHaveLength(3); +}); + +describe("parse HCM2000 unsignalized results", () => { + let data = parseCSV(input); + let groups = groupByIntersection(data); + let results = parseResults(groups); + + test("names", () => { + expect(Object.keys(results)).toEqual(["Side4 & Main", "Side5 & Main", "Side6 & Main"]); + }); + + test("type", () => { + let type = []; + let control = []; + for (const intersection of Object.values(results)) { + type.push(intersection.type); + control.push(intersection.control); + } + expect(type).toEqual(["hcm-unsignalized", "hcm-unsignalized", "hcm-unsignalized"]); + expect(control).toEqual(["Side-street Stop", "All-way Stop", "Side-street Stop"]); + }); + + test("lane configuration", () => { + let config = []; + for (const intersection of Object.values(results)) { + config.push(intersection.movements); + } + expect(config).toMatchInlineSnapshot(` + [ + [ + "SB 1", + ], + [ + "EB 1", + "WB 1", + "NB 1", + "SB 1", + ], + [ + "NB 1", + ], + ] + `); + }); + + test("delay", () => { + let delays = []; + for (const intersection of Object.values(results)) { + delays.push(intersection.delay); + } + expect(delays).toEqual([ + ["F", "225.3"], + ["C", "20.9"], + ["B", "11.5"], + ]); + }); + + test("v/c", () => { + let vc = []; + for (const intersection of Object.values(results)) { + vc.push(intersection.vc); + } + expect(vc).toMatchInlineSnapshot(` + [ + [ + "1.32", + ], + [], + [ + "0.30", + ], + ] + `); + }); +}); diff --git a/tests/sample-output/HCM2000_Unsignalized.txt b/tests/sample-output/HCM2000_Unsignalized.txt new file mode 100644 index 0000000..131a6b8 --- /dev/null +++ b/tests/sample-output/HCM2000_Unsignalized.txt @@ -0,0 +1,140 @@ +HCM Unsignalized Intersection Capacity Analysis +11: Side4 & Main 02-27-2024 + + +Movement EBL EBT EBR WBL WBT WBR NBL NBT NBR SBL SBT SBR +Lanes 1 2> 0 0 <2 1 0 1> 0 0 <1 0 +Traffic Volume (veh/h) 101 102 103 104 105 106 0 108 109 110 111 0 +Future Volume (Veh/h) 101 102 103 104 105 106 0 108 109 110 111 0 +Sign Control Free Free Stop Stop +Grade 0% 0% 0% 0% +Peak Hour Factor 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 +Hourly flow rate (vph) 110 111 112 113 114 115 0 117 118 120 121 0 +Pedestrians +Lane Width (m) +Walking Speed (m/s) +Percent Blockage +Right turn flare (veh) +Median type None None +Median storage veh) +Upstream signal (m) +pX, platoon unblocked +vC, conflicting volume 229 223 730 842 112 792 783 57 +vC1, stage 1 conf vol +vC2, stage 2 conf vol +vCu, unblocked vol 229 223 730 842 112 792 783 57 +tC, single (s) 4.1 4.1 7.5 6.5 6.9 7.5 6.5 6.9 +tC, 2 stage (s) +tF (s) 2.2 2.2 3.5 4.0 3.3 3.5 4.0 3.3 +p0 queue free % 92 92 100 53 87 13 56 100 +cM capacity (veh/h) 1336 1343 179 252 920 138 272 997 + +Direction, Lane # EB 1 EB 2 EB 3 WB 1 WB 2 WB 3 NB 1 SB 1 +Volume Total 110 74 149 151 76 115 235 241 +Volume Left 110 0 0 113 0 0 0 120 +Volume Right 0 0 112 0 0 115 118 0 +cSH 1336 1700 1700 1343 1700 1700 396 183 +Volume to Capacity 0.08 0.04 0.09 0.08 0.04 0.07 0.59 1.32 +Queue Length 95th (m) 2.1 0.0 0.0 2.2 0.0 0.0 29.6 110.4 +Control Delay (s) 7.9 0.0 0.0 6.1 0.0 0.0 26.5 225.3 +Lane LOS A A D F +Approach Delay (s) 2.6 2.7 26.5 225.3 +Approach LOS D F + +Intersection Summary +Average Delay 54.1 +Intersection Capacity Utilization 49.7% ICU Level of Service A +Analysis Period (min) 15 + + + +Scenario 1 5:08 pm 10-27-2022 Baseline Synchro 11 Report + Page 0 + +HCM Unsignalized Intersection Capacity Analysis +12: Side5 & Main 02-27-2024 + + +Movement EBL EBT EBR WBL WBT WBR NBL NBT NBR SBL SBT SBR +Lanes 0 <1> 0 0 <1> 0 0 1> 0 0 <1> 0 +Sign Control Stop Stop Stop Stop +Traffic Volume (vph) 101 102 103 104 105 106 0 108 109 110 111 112 +Future Volume (vph) 101 102 103 104 105 106 0 108 109 110 111 112 +Peak Hour Factor 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 0.92 +Hourly flow rate (vph) 110 111 112 113 114 115 0 117 118 120 121 122 + +Direction, Lane # EB 1 WB 1 NB 1 SB 1 +Volume Total (vph) 333 342 235 363 +Volume Left (vph) 110 113 0 120 +Volume Right (vph) 112 115 118 122 +Hadj (s) -0.10 -0.10 -0.27 -0.10 +Departure Headway (s) 6.8 6.8 7.0 6.8 +Degree Utilization, x 0.63 0.65 0.46 0.69 +Capacity (veh/h) 481 485 442 491 +Control Delay (s) 20.9 21.5 15.9 23.4 +Approach Delay (s) 20.9 21.5 15.9 23.4 +Approach LOS C C C C + +Intersection Summary +Delay 20.9 +Level of Service C +Intersection Capacity Utilization 64.5% ICU Level of Service C +Analysis Period (min) 15 + + + +Scenario 1 5:08 pm 10-27-2022 Baseline Synchro 11 Report + Page 0 + +HCM Unsignalized Intersection Capacity Analysis +13: Side6 & Main 02-27-2024 + + +Movement EBL EBT EBR WBL WBT WBR NBL NBT NBR SBL SBT SBR +Lanes 1> 0 0 1 1> 0 +Traffic Volume (veh/h) 102 103 0 105 107 109 +Future Volume (Veh/h) 102 103 0 105 107 109 +Sign Control Free Free Stop +Grade 0% 0% 0% +Peak Hour Factor 0.92 0.92 0.92 0.92 0.92 0.92 +Hourly flow rate (vph) 111 112 0 114 116 118 +Pedestrians +Lane Width (m) +Walking Speed (m/s) +Percent Blockage +Right turn flare (veh) +Median type None None +Median storage veh) +Upstream signal (m) +pX, platoon unblocked +vC, conflicting volume 223 281 167 +vC1, stage 1 conf vol +vC2, stage 2 conf vol +vCu, unblocked vol 223 281 167 +tC, single (s) 4.1 6.4 6.2 +tC, 2 stage (s) +tF (s) 2.2 3.5 3.3 +p0 queue free % 100 84 87 +cM capacity (veh/h) 1346 709 877 + +Direction, Lane # EB 1 WB 1 NB 1 +Volume Total 223 114 234 +Volume Left 0 0 116 +Volume Right 112 0 118 +cSH 1700 1700 785 +Volume to Capacity 0.13 0.07 0.30 +Queue Length 95th (m) 0.0 0.0 10.0 +Control Delay (s) 0.0 0.0 11.5 +Lane LOS B +Approach Delay (s) 0.0 0.0 11.5 +Approach LOS B + +Intersection Summary +Average Delay 4.7 +Intersection Capacity Utilization 30.9% ICU Level of Service A +Analysis Period (min) 15 + + + +Scenario 1 5:08 pm 10-27-2022 Baseline Synchro 11 Report + Page 0 diff --git a/tests/synchro.test.ts b/tests/synchro.test.ts index 0c53be9..244bcc8 100644 --- a/tests/synchro.test.ts +++ b/tests/synchro.test.ts @@ -22,6 +22,17 @@ describe("parse Synchro results", () => { expect(Object.keys(results)).toEqual(["Side1 & Main", "Side2 & Main", "Main & Side3"]); }); + test("type", () => { + let type = []; + let control = []; + for (const intersection of Object.values(results)) { + type.push(intersection.type); + control.push(intersection.control); + } + expect(type).toEqual(["synchro", "synchro", "synchro"]); + expect(control).toEqual(["Pretimed", "Actuated-Coordinated", "Actuated-Uncoordinated"]); + }); + test("lane configuration", () => { let config = []; for (const intersection of Object.values(results)) {