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