Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split corridor output fields #99

Merged
merged 7 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,21 @@ If you have any trouble using the app, please send an email to Nate Wessel (nate
## Outputs

The app can return results in either CSV or JSON format. The fields in either case are the same. There will be one column for each of the input parameters:
* corridor
* time range
* date range
* days of week
* holiday inclusion

The other fields may require some explanation:

| Field | Description |
| Field | Description |
|----|----|
| `routeStreets` | The name(s) of the streets along the corridor. I.e. the path taken. |
| `startCrossStreets` | The names of any cross-street(s) at the start of the corridor. If the corridor starts mid-block then coordinates of that point will be returned instead. |
| `endCrossStreets` | The names of any cross-street(s) at the end of the corridor. If the corridor ends mid-block then coordinates of that point will be returned instead. |
| `mean_travel_time_minutes` | The mean travel time in minutes is given as a floating point number rounded to two decimal places. Where insufficient data was available to complete the request, the value will be null, and in cases where the request was impossible a value of -999 will be assigned. (See `hoursInRange` below). |
| `URI` | The URI is the API endpoint that corresponds to this exect request. It may be of little use to some end users but may help us to reproduce the request and verify data quality. It can also serve as a unique ID. |
| `mean_travel_time_seconds` | Same as above, but measured in seconds. |
| `URI` | The URI is the API endpoint that corresponds to this exect request. It may be of little use to some end users but can help us to reproduce the request and verify data quality. It can also serve as a unique ID for the record. |
| `hoursInRange` | The total number of hours that are theoretically within the scope of this request. This does not imply that data is/was available at all times. It's possible to construct requests with zero hours in range such as e.g `2023-01-01` to `2023-01-02`, Mondays only (There's only one Sunday in that range). Impossible combinations are included in the output for clarity and completeness but are not actually executed against the API and should return an error. |
| `estimatedVehicleCount` | A very rough estimate of the number of actual vehicles traversing the corridor within the given temporal bounds. Includes partial trips through the corridor and may be subject to additional caveats which we are still exploring. |

Expand Down
3 changes: 1 addition & 2 deletions frontend/src/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { DataContext } from '../Layout'
import FactorContainer from './FactorContainer'
import BigButton from './BigButton'
import FactorList from './FactorList'
import { TravelTimeQuery } from '../travelTimeQuery.js'
import { restoreStateFromFile } from './restoreStateFromFile.js'
import './sidebar.css'

Expand Down Expand Up @@ -67,7 +66,7 @@ function Results(){
</BigButton>
</a>
<a download='results.csv'
href={`data:text/plain;charset=utf-8,${encodeURIComponent(TravelTimeQuery.csvHeader() + '\n' + results.map(r=>r.resultsRecord('csv')).join('\n'))}`}
href={`data:text/plain;charset=utf-8,${encodeURIComponent([...results[0].resultsRecord('').keys()].join(',') + '\n' + results.map(r=>r.resultsRecord('csv')).join('\n'))}`}
>
<BigButton>
Download results as CSV
Expand Down
49 changes: 35 additions & 14 deletions frontend/src/corridor.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,40 @@ export class Corridor extends Factor {
get viaStreets(){
return new Set( this.links.map( link => link.name ) )
}
get viaStreetsString(){
return [...this.viaStreets].join(' & ')
}
get startCrossStreets(){
try { return difference(this.#intersections[0].streetNames,this.viaStreets) }
catch (e) { return new Set() }
}
get startCrossStreetsString(){
if(this.startCrossStreets.size > 0){
return [...this.startCrossStreets].join(' & ')
}else if(this.#intersections.length > 0){
return this.#intersections[0].displayCoords
}
return ''
}
get endCrossStreets(){
try { return difference(this.#intersections[1].streetNames,this.viaStreets) }
catch (e) { return new Set() }
}
get endCrossStreetsString(){
if(this.endCrossStreets.size > 0){
return [...this.endCrossStreets].join(' & ')
}else if(this.#intersections.length > 1){
return this.#intersections[1].displayCoords
}
return ''
}
get name(){
if(this.#intersections.length == 1){
return `Incomplete corridor starting from ${this.intersections[0].description}`
return `Incomplete corridor starting from ${this.startCrossStreetsString}`
}else if(this.#intersections.length == 2 && this.viaStreets.size > 0){
// routing should be done
let start = difference(this.intersections[0].streetNames,this.viaStreets)
let end = difference(this.intersections[1].streetNames,this.viaStreets)
// no cross-street of a different name
if(start.size == 0){ start.add(this.intersections[0].displayCoords) }
if(end.size == 0){ end.add(this.intersections[1].displayCoords) }
return `${[...this.viaStreets].join(' & ')} from ${[...start].join(' & ')} to ${[...end].join(' & ')}`
return `${this.viaStreetsString} from ${this.startCrossStreetsString} to ${this.endCrossStreetsString}`
}else if(this.#intersections.length == 2){ // but no via streets (yet?)
let start = [...this.intersections[0].streetNames].join(' & ')
let end = [...this.intersections[1].streetNames].join(' & ')
return `from ${start} to ${end}`
return `from ${this.startCrossStreetsString} to ${this.endCrossStreetsString}`
}
return 'New Corridor'
}
Expand All @@ -68,14 +87,16 @@ export class Corridor extends Factor {
function CorridorElement({corridor}){
return (
<div>
<div className='corridorName'>{corridor.name}</div>
<div className='corridorName'>
{corridor.name}
</div>
{corridor.isActive && <>
<div className='instructions'>
{corridor.intersections.length == 0 &&
<>Click on the map to identify the starting point</>
'Click on the map to identify the starting point'
}
{corridor.intersections.length == 1 &&
<>Click on the map to identify the end point</>
'Click on the map to identify the end point'
}
</div>
</> }
Expand Down
45 changes: 21 additions & 24 deletions frontend/src/travelTimeQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,33 +53,30 @@ export class TravelTimeQuery {
return hoursPerDay * numDays
}
resultsRecord(type='json'){
const record = {
URI: this.URI,
corridor: this.corridor.name,
timeRange: this.timeRange.name,
dateRange: this.dateRange.name,
daysOfWeek: this.days.name,
holidaysIncluded: this.#holidayOption.holidaysIncluded,
hoursInRange: this.hoursInRange,
estimatedVehicleCount: this.#estimatedSample,
mean_travel_time_minutes: this.#travelTime
}
// map used instead of object to preserve insertion order
const record = new Map()
record.set('URI',this.URI)
record.set('routeStreets',this.corridor.viaStreetsString)
record.set('startCrossStreets',this.corridor.startCrossStreetsString)
record.set('endCrossStreets',this.corridor.endCrossStreetsString)
record.set('timeRange',this.timeRange.name)
record.set('dateRange',this.dateRange.name)
record.set('daysOfWeek', this.days.name)
record.set('holidaysIncluded', this.#holidayOption.holidaysIncluded)
record.set('hoursInRange', this.hoursInRange)
record.set('estimatedVehicleCount', this.#estimatedSample)
record.set('mean_travel_time_minutes', this.#travelTime)
record.set('mean_travel_time_seconds', 60* this.#travelTime)

if(type=='json'){
return record
return Object.fromEntries(record) // can't JSONify maps
}else if(type=='csv'){
return Object.values(record)
.map( value => {

if(typeof value == 'string'){
return `"${value}"`
}
return value
} )
// add double quotes to strings and concatenate
return [...record.values()]
.map( value => typeof value == 'string' ? `"${value}"` : value )
.join(',')
}
return 'invalid type requested'
}
static csvHeader(){
return 'URI,corridor,timeRange,dateRange,daysOfWeek,holidaysIncluded,hoursPossible,estimatedSample,mean_travel_time_minutes'
// the keys of a map record are used to create the CSV header
return record
}
}