From fe35664efdc222c332a82fb6d982ba9a30c026ac Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 13:31:23 +0000 Subject: [PATCH 1/9] add holidays API endpoint --- backend/app/routes.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/app/routes.py b/backend/app/routes.py index fd57cca..cb38eb3 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -314,9 +314,7 @@ def aggregate_travel_times(start_node, end_node, start_time, end_time, start_dat # test URL /date-bounds @app.route('/date-bounds', methods=['GET']) def get_date_bounds(): - """ - Get the earliest date and latest data in the travel database. - """ + "Get the earliest date and latest data in the travel database." connection = getConnection() with connection: with connection.cursor() as cursor: @@ -327,3 +325,15 @@ def get_date_bounds(): "start_time": min_date.strftime('%Y-%m-%d'), "end_time": max_date.strftime('%Y-%m-%d') } + +# test URL /holidays +@app.route('/holidays', methods=['GET']) +def get_holidays(): + "Return dates of all known holidays in ascending order" + connection = getConnection() + with connection: + with connection.cursor() as cursor: + cursor.execute('SELECT dt FROM ref.holiday ORDER BY dt;') + dates = [str(x) for (x,) in cursor.fetchall()] + connection.close() + return dates \ No newline at end of file From de790482dc8934bf6881dac6f8ea8806d2ebe01d Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 14:00:44 +0000 Subject: [PATCH 2/9] create placeholder HolidayOptions class --- frontend/src/Sidebar/index.jsx | 18 ++++++++++++++++++ frontend/src/holidayOption.js | 17 +++++++++++++++++ frontend/src/spatialData.js | 3 +++ 3 files changed, 38 insertions(+) create mode 100644 frontend/src/holidayOption.js diff --git a/frontend/src/Sidebar/index.jsx b/frontend/src/Sidebar/index.jsx index 0a5b9fe..ca0bda3 100644 --- a/frontend/src/Sidebar/index.jsx +++ b/frontend/src/Sidebar/index.jsx @@ -18,6 +18,8 @@ export default function SidebarContent(){
×
+
×
+
=
@@ -132,6 +134,22 @@ function DaysContainer(){ ) } +function HolidaysContainer(){ + const { logActivity, data } = useContext(DataContext) + function changeSomething(){ + data.yaddaYadda() + logActivity('holiday options modified') + } + return ( + + + Opt to include/exclude holidays + + + + ) +} + function Welcome(){ return ( <>

Toronto Historic Travel Times

diff --git a/frontend/src/holidayOption.js b/frontend/src/holidayOption.js new file mode 100644 index 0000000..aa0078a --- /dev/null +++ b/frontend/src/holidayOption.js @@ -0,0 +1,17 @@ +import { Factor } from './factor.js' +import { domain } from './domain.js' +//import { useContext, useState, useEffect } from 'react' +import { DataContext } from './Layout' + +export class HolidayOption extends Factor { + #includeHolidays = true + #holidays = [] + constructor(dataContext){ + super(dataContext) + fetch(`${domain}/holidays`) + .then( holidays => this.#holidays = holidays ) + } + render(){ + return

hello world

+ } +} \ No newline at end of file diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 77c8e00..1071c76 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -2,6 +2,7 @@ import { Corridor } from './corridor.js' import { TimeRange } from './timeRange.js' import { DateRange } from './dateRange.js' import { Days } from './days.js' +import { HolidayOption } from './holidayOption.js' import { TravelTimeQuery } from './travelTimeQuery.js' // instantiated once, this is the data store for all spatial and temporal data @@ -10,11 +11,13 @@ export class SpatialData { #queries = new Map() // store/cache for travelTimeQueries, letting them remember their results if any constructor(){ this.#factors.push(new Days(this)) + this.#factors.push(new HolidayOption(this)) } get corridors(){ return this.#factors.filter( f => f instanceof Corridor ) } get timeRanges(){ return this.#factors.filter( f => f instanceof TimeRange ) } get dateRanges(){ return this.#factors.filter( f => f instanceof DateRange ) } get days(){ return this.#factors.filter( f => f instanceof Days ) } + get holidayOptions(){ return this.#factors.filter( f => f instanceof HolidayOption ) } get activeCorridor(){ return this.corridors.find( cor => cor.isActive ) } From 5f206569d5b0b5ada46603c1ff57749d81479d49 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 17:25:45 +0000 Subject: [PATCH 3/9] send names too --- backend/app/routes.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/app/routes.py b/backend/app/routes.py index cb38eb3..e05a744 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -333,7 +333,13 @@ def get_holidays(): connection = getConnection() with connection: with connection.cursor() as cursor: - cursor.execute('SELECT dt FROM ref.holiday ORDER BY dt;') - dates = [str(x) for (x,) in cursor.fetchall()] + cursor.execute(""" + SELECT + dt::text, + holiday + FROM ref.holiday + ORDER BY dt; + """) + dates = [ {'date': dt, 'name': nm} for (dt, nm) in cursor.fetchall()] connection.close() return dates \ No newline at end of file From 29cca8ff646374356c97c8893986b93e9d26fdce Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 17:27:08 +0000 Subject: [PATCH 4/9] fetch holidays once from data store --- frontend/src/spatialData.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 1071c76..69752e7 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -4,20 +4,27 @@ import { DateRange } from './dateRange.js' import { Days } from './days.js' import { HolidayOption } from './holidayOption.js' import { TravelTimeQuery } from './travelTimeQuery.js' +import { domain } from './domain.js' // instantiated once, this is the data store for all spatial and temporal data export class SpatialData { #factors = [] #queries = new Map() // store/cache for travelTimeQueries, letting them remember their results if any + #knownHolidays = [] constructor(){ this.#factors.push(new Days(this)) this.#factors.push(new HolidayOption(this)) + fetch(`${domain}/holidays`) + .then( response => response.json() ) + .then( holidayList => this.#knownHolidays = holidayList ) } get corridors(){ return this.#factors.filter( f => f instanceof Corridor ) } get timeRanges(){ return this.#factors.filter( f => f instanceof TimeRange ) } get dateRanges(){ return this.#factors.filter( f => f instanceof DateRange ) } get days(){ return this.#factors.filter( f => f instanceof Days ) } - get holidayOptions(){ return this.#factors.filter( f => f instanceof HolidayOption ) } + get holidayOptions(){ + return this.#factors.filter( f => f instanceof HolidayOption ) + } get activeCorridor(){ return this.corridors.find( cor => cor.isActive ) } From 273df056eef266f8ec22c83d4a963e92bcd7947b Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 18:20:11 +0000 Subject: [PATCH 5/9] create three-way holiday logic + form --- frontend/src/Sidebar/index.jsx | 49 +++++++++++++++++++++++++++++----- frontend/src/holidayOption.js | 13 +++------ frontend/src/spatialData.js | 15 ++++++++++- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/frontend/src/Sidebar/index.jsx b/frontend/src/Sidebar/index.jsx index ca0bda3..e393f26 100644 --- a/frontend/src/Sidebar/index.jsx +++ b/frontend/src/Sidebar/index.jsx @@ -136,16 +136,51 @@ function DaysContainer(){ function HolidaysContainer(){ const { logActivity, data } = useContext(DataContext) - function changeSomething(){ - data.yaddaYadda() - logActivity('holiday options modified') + let options = data.holidayOptions + let included, excluded + if(options.length == 1){ + included = options[0].holidaysIncluded + excluded = ! included + }else{ + included = true + excluded = true + } + function handleChange(option){ + if(option=='no'){ + data.excludeHolidays() + }else if(option=='yeah'){ + data.includeHolidays() + }else{ + data.includeAndExcludeHolidays() + } + logActivity(`include holidays? ${option}`) } return ( - - Opt to include/exclude holidays - - +
Include holidays?
+ +
+ +
+
) } diff --git a/frontend/src/holidayOption.js b/frontend/src/holidayOption.js index aa0078a..6be04bb 100644 --- a/frontend/src/holidayOption.js +++ b/frontend/src/holidayOption.js @@ -1,17 +1,12 @@ import { Factor } from './factor.js' -import { domain } from './domain.js' //import { useContext, useState, useEffect } from 'react' import { DataContext } from './Layout' export class HolidayOption extends Factor { - #includeHolidays = true - #holidays = [] - constructor(dataContext){ + #includeHolidays + constructor(dataContext,includeHolidays){ super(dataContext) - fetch(`${domain}/holidays`) - .then( holidays => this.#holidays = holidays ) - } - render(){ - return

hello world

+ this.#includeHolidays = includeHolidays } + get holidaysIncluded(){ return this.#includeHolidays } } \ No newline at end of file diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 69752e7..66b9e1d 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -13,7 +13,7 @@ export class SpatialData { #knownHolidays = [] constructor(){ this.#factors.push(new Days(this)) - this.#factors.push(new HolidayOption(this)) + this.#factors.push(new HolidayOption(this,true)) fetch(`${domain}/holidays`) .then( response => response.json() ) .then( holidayList => this.#knownHolidays = holidayList ) @@ -62,6 +62,19 @@ export class SpatialData { if(f != factor) f.deactivate() } ) } + includeHolidays(){ + this.holidayOptions.forEach(f => this.dropFactor(f)) + this.#factors.push(new HolidayOption(this,true)) + } + excludeHolidays(){ + this.holidayOptions.forEach(f => this.dropFactor(f)) + this.#factors.push(new HolidayOption(this,false)) + } + includeAndExcludeHolidays(){ + this.holidayOptions.forEach(f => this.dropFactor(f)) + this.#factors.push(new HolidayOption(this,true)) + this.#factors.push(new HolidayOption(this,false)) + } get travelTimeQueries(){ // is the crossproduct of all complete/valid factors const crossProduct = [] From 3582db59fe7d44b4047877d2c29bf51c77d8c049 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 18:37:01 +0000 Subject: [PATCH 6/9] pass options through to query and results --- frontend/src/holidayOption.js | 1 + frontend/src/spatialData.js | 14 +++++++++++--- frontend/src/travelTimeQuery.js | 14 +++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/frontend/src/holidayOption.js b/frontend/src/holidayOption.js index 6be04bb..85938b3 100644 --- a/frontend/src/holidayOption.js +++ b/frontend/src/holidayOption.js @@ -9,4 +9,5 @@ export class HolidayOption extends Factor { this.#includeHolidays = includeHolidays } get holidaysIncluded(){ return this.#includeHolidays } + get name(){ return `holidays included = ${this.holidaysIncluded}` } } \ No newline at end of file diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 66b9e1d..0c968a5 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -82,9 +82,17 @@ export class SpatialData { this.timeRanges.filter(tr=>tr.isComplete).forEach( timeRange => { this.dateRanges.filter(dr=>dr.isComplete).forEach( dateRange => { this.days.filter(d=>d.isComplete).forEach( days => { - crossProduct.push( - new TravelTimeQuery({corridor,timeRange,dateRange,days}) - ) + this.holidayOptions.forEach( holidayOption => { + crossProduct.push( + new TravelTimeQuery({ + corridor, + timeRange, + dateRange, + days, + holidayOption + }) + ) + } ) } ) } ) } ) diff --git a/frontend/src/travelTimeQuery.js b/frontend/src/travelTimeQuery.js index bc40f65..68713f8 100644 --- a/frontend/src/travelTimeQuery.js +++ b/frontend/src/travelTimeQuery.js @@ -5,12 +5,14 @@ export class TravelTimeQuery { #timeRange #dateRange #days + #holidayOption #travelTime - constructor({corridor,timeRange,dateRange,days}){ + constructor({corridor,timeRange,dateRange,days,holidayOption}){ this.#corridor = corridor this.#timeRange = timeRange this.#dateRange = dateRange this.#days = days + this.#holidayOption = holidayOption } get URI(){ let path = `aggregate-travel-times` @@ -20,8 +22,10 @@ export class TravelTimeQuery { path += `/${this.#timeRange.startHour}/${this.#timeRange.endHour}` // start and end dates path += `/${this.#dateRange.startDateFormatted}/${this.#dateRange.endDateFormatted}` - // options not yet supported: holidays and days of week - path += `/true/${this.#days.apiString}` + // holiday inclusion + path += `/${this.#holidayOption.holidaysIncluded}` + // days of week + path += `/${this.#days.apiString}` return path } get corridor(){ return this.#corridor } @@ -29,7 +33,6 @@ export class TravelTimeQuery { get dateRange(){ return this.#dateRange } get days(){ return this.#days } async fetchData(){ - console.log(this.hoursInRange) if( this.hoursInRange < 1 ){ return this.#travelTime = -999 } @@ -52,6 +55,7 @@ export class TravelTimeQuery { timeRange: this.timeRange.name, dateRange: this.dateRange.name, daysOfWeek: this.days.name, + holidaysIncluded: this.#holidayOption.holidaysIncluded, hoursInRange: this.hoursInRange, mean_travel_time_minutes: this.#travelTime } @@ -71,6 +75,6 @@ export class TravelTimeQuery { return 'invalid type requested' } static csvHeader(){ - return 'URI,corridor,timeRange,dateRange,daysOfWeek,hoursPossible,mean_travel_time_minutes' + return 'URI,corridor,timeRange,dateRange,daysOfWeek,holidaysIncluded,hoursPossible,mean_travel_time_minutes' } } \ No newline at end of file From 59613db71d65460a814335a2fd14997cf7f15f09 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Fri, 6 Oct 2023 19:40:32 +0000 Subject: [PATCH 7/9] use holiday dates in hoursPossible counts --- frontend/src/dateRange.js | 23 ++++++++++++++++++----- frontend/src/holidayOption.js | 10 +++++++--- frontend/src/spatialData.js | 1 + frontend/src/travelTimeQuery.js | 4 +++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/dateRange.js b/frontend/src/dateRange.js index 3c726d2..9da131a 100644 --- a/frontend/src/dateRange.js +++ b/frontend/src/dateRange.js @@ -48,27 +48,40 @@ export class DateRange extends Factor { get endDateFormatted(){ return DateRange.dateFormatted(this.#endDate) } - daysInRange(daysOptions){ // number of days covered by this dateRange - // TODO this will need to be revisited with the holiday options enabled + // number of days covered by this dateRange, considering DoW and holidays + daysInRange(daysOptions,holidayOptions){ + // TODO: this logic is pretty convoluted - clean it up!! if( ! (this.isComplete && daysOptions.isComplete) ){ return undefined } + let holidayDates = new Set(holidayOptions.holidays.map(h=>h.date)) // iterate each day in the range let d = new Date(this.#startDate.valueOf()) let dayCount = 0 + const holidaysExcluded = ! holidayOptions.holidaysIncluded while(d < this.#endDate){ - let dow = d.getUTCDay() + let dow = d.getUTCDay() let isodow = dow == 0 ? 7 : dow if( daysOptions.hasDay(isodow) ){ - dayCount ++ + // if holidays are NOT included, check the date isn't a holiday + if( ! ( holidaysExcluded && holidayDates.has(formatISODate(d)) ) ){ + dayCount ++ + } } - // incrememnt, modified in-place + // incrememnt one day, modified in-place d.setUTCDate(d.getUTCDate() + 1) } return dayCount } } +function formatISODate(dt){ // this is waaay too complicated... alas + let year = dt.getUTCFullYear() // should be good + let month = 1 + dt.getUTCMonth() // 0 - 11 -> 1 - 12 + let day = dt.getUTCDate() // 1 - 31 + return `${year}-${('0'+month).slice(-2)}-${('0'+day).slice(-2)}` +} + function DateRangeElement({dateRange}){ const [ startInput, setStartInput ] = useState(dateRange.startDateFormatted) const [ endInput, setEndInput ] = useState(dateRange.endDateFormatted) diff --git a/frontend/src/holidayOption.js b/frontend/src/holidayOption.js index 85938b3..f9c9d35 100644 --- a/frontend/src/holidayOption.js +++ b/frontend/src/holidayOption.js @@ -1,13 +1,17 @@ import { Factor } from './factor.js' -//import { useContext, useState, useEffect } from 'react' -import { DataContext } from './Layout' export class HolidayOption extends Factor { #includeHolidays + #dataContext constructor(dataContext,includeHolidays){ super(dataContext) + // store this here too to actually access the holiday data + this.#dataContext = dataContext + // selection for whether to include holidays this.#includeHolidays = includeHolidays } get holidaysIncluded(){ return this.#includeHolidays } - get name(){ return `holidays included = ${this.holidaysIncluded}` } + get holidays(){ + return this.#dataContext.holidays + } } \ No newline at end of file diff --git a/frontend/src/spatialData.js b/frontend/src/spatialData.js index 0c968a5..359cc5c 100644 --- a/frontend/src/spatialData.js +++ b/frontend/src/spatialData.js @@ -28,6 +28,7 @@ export class SpatialData { get activeCorridor(){ return this.corridors.find( cor => cor.isActive ) } + get holidays(){ return this.#knownHolidays } createCorridor(){ let corridor = new Corridor(this) this.#factors.push(corridor) diff --git a/frontend/src/travelTimeQuery.js b/frontend/src/travelTimeQuery.js index 68713f8..7f3fc89 100644 --- a/frontend/src/travelTimeQuery.js +++ b/frontend/src/travelTimeQuery.js @@ -46,7 +46,9 @@ export class TravelTimeQuery { return Boolean(this.#travelTime) } get hoursInRange(){ // number of hours covered by query options - return this.timeRange.hoursInRange * this.dateRange.daysInRange(this.days) + let hoursPerDay = this.timeRange.hoursInRange + let numDays = this.dateRange.daysInRange(this.days,this.#holidayOption) + return hoursPerDay * numDays } resultsRecord(type='json'){ const record = { From 766dc4d3f6338a9f577ccf6fcd30d3d53a20f3f8 Mon Sep 17 00:00:00 2001 From: Nate Wessel Date: Mon, 16 Oct 2023 11:23:32 -0400 Subject: [PATCH 8/9] field has been added --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 7fb9d26..698610f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The app can return results in either CSV or JSON format. The fields in either ca * time range * date range * days of week -* holiday inclusion (will be added shortly) +* holiday inclusion The other fields may require some explanation: @@ -39,7 +39,6 @@ The other fields may require some explanation: | `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. | - ## Methodology Data for travel time estimation through the app are sourced from [HERE](https://github.com/CityofToronto/bdit_data-sources/tree/master/here)'s [traffic API](https://developer.here.com/documentation/traffic-api/api-reference.html) and are available back to about 2012. HERE collects data from motor vehicles that report their speed and position to HERE, most likely as a by-poduct of the driver making use of an in-car navigation system connected to the Internet. From a2cb37990d9cba28ef3cab931f0a35521e891fa5 Mon Sep 17 00:00:00 2001 From: Nate-Wessel Date: Mon, 16 Oct 2023 15:38:32 +0000 Subject: [PATCH 9/9] note change in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 698610f..7c12fc6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ When you [visit the app](https://trans-bdit.intra.prod-toronto.ca/traveltime-req * a time range, given in hours of the day, 00 - 23 * a date range (note that the end of the date range is exclusive) * a day of week selection -* _coming soon_! a selection of whether or not to include statutory holidays +* a selection of whether or not to include statutory holidays The app will combine these factors together to request travel times for all possible combinations. If one of each type of factor is selected, only a single travel time will be estimated with the given parameters.