Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
14 changes: 8 additions & 6 deletions gtfs/stop_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ type StopTime struct {
EndPickupDropOffWindow tt.Seconds
PickupBookingRuleID tt.Key `target:"booking_rules.txt"`
DropOffBookingRuleID tt.Key `target:"booking_rules.txt"`
// GTFS-Flex proposed fields (not yet formally adopted, may change)
// See: https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md
MeanDurationFactor tt.Float // proposed gtfs-flex
MeanDurationOffset tt.Float // proposed gtfs-flex
SafeDurationFactor tt.Float // proposed gtfs-flex
SafeDurationOffset tt.Float // proposed gtfs-flex
// Deprecated: safe_duration fields moved to trips.txt per google/transit#598.
// Kept for backward compat with older feeds that still produce these on stop_times.
SafeDurationFactor tt.Float
SafeDurationOffset tt.Float
// Deprecated: mean_duration fields removed from spec per gtfs-flex#73.
// Kept for backward compat with older feeds.
MeanDurationFactor tt.Float
MeanDurationOffset tt.Float
Comment thread
drewda marked this conversation as resolved.
Outdated
tt.MinEntity
tt.ErrorEntity
tt.ExtraEntity
Expand Down
33 changes: 30 additions & 3 deletions gtfs/trip.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package gtfs

import (
"fmt"

"github.com/interline-io/transitland-lib/causes"
"github.com/interline-io/transitland-lib/tt"
)

Expand All @@ -13,9 +16,13 @@ type Trip struct {
TripShortName tt.String
DirectionID tt.Int `enum:"0,1"`
BlockID tt.String
ShapeID tt.Key `target:"shapes.txt"`
WheelchairAccessible tt.Int `enum:"0,1,2"`
BikesAllowed tt.Int `enum:"0,1,2"`
ShapeID tt.Key `target:"shapes.txt"`
WheelchairAccessible tt.Int `enum:"0,1,2"`
BikesAllowed tt.Int `enum:"0,1,2"`
// GTFS-Flex: safe duration fields (google/transit#598)
// See: https://github.com/google/transit/pull/598
SafeDurationFactor tt.Float
SafeDurationOffset tt.Float
Comment thread
drewda marked this conversation as resolved.
Outdated
JourneyPatternID tt.String `csv:"-"`
JourneyPatternOffset tt.Int `csv:"-"`
StopPatternID tt.Int `csv:"-"`
Expand All @@ -42,3 +49,23 @@ func (ent *Trip) Filename() string {
func (ent *Trip) TableName() string {
return "gtfs_trips"
}

// ConditionalErrors validates GTFS-Flex safe duration fields.
func (ent *Trip) ConditionalErrors() (errs []error) {
// safe_duration_factor and safe_duration_offset must both be present or both absent
if ent.SafeDurationFactor.Valid && !ent.SafeDurationOffset.Valid {
errs = append(errs, causes.NewConditionallyRequiredFieldError("safe_duration_offset"))
}
if ent.SafeDurationOffset.Valid && !ent.SafeDurationFactor.Valid {
errs = append(errs, causes.NewConditionallyRequiredFieldError("safe_duration_factor"))
}
// safe_duration_factor must be positive
if ent.SafeDurationFactor.Valid && ent.SafeDurationFactor.Val <= 0 {
errs = append(errs, causes.NewInvalidFieldError(
"safe_duration_factor",
fmt.Sprintf("%f", ent.SafeDurationFactor.Val),
fmt.Errorf("must be positive"),
))
}
return errs
}
39 changes: 39 additions & 0 deletions gtfs/trip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,45 @@ func TestTrip_Errors(t *testing.T) {
}),
expectedErrors: PE("InvalidFieldError:bikes_allowed"),
},
// GTFS-Flex safe duration fields (google/transit#598)
{
name: "Valid: both safe_duration fields present",
trip: newTrip(func(t *Trip) {
t.SafeDurationFactor = tt.NewFloat(1.75)
t.SafeDurationOffset = tt.NewFloat(900)
}),
expectedErrors: nil,
},
{
name: "Invalid: only safe_duration_factor present",
trip: newTrip(func(t *Trip) {
t.SafeDurationFactor = tt.NewFloat(1.5)
}),
expectedErrors: PE("ConditionallyRequiredFieldError:safe_duration_offset"),
},
{
name: "Invalid: only safe_duration_offset present",
trip: newTrip(func(t *Trip) {
t.SafeDurationOffset = tt.NewFloat(300)
}),
expectedErrors: PE("ConditionallyRequiredFieldError:safe_duration_factor"),
},
{
name: "Invalid: safe_duration_factor is negative",
trip: newTrip(func(t *Trip) {
t.SafeDurationFactor = tt.NewFloat(-1.5)
t.SafeDurationOffset = tt.NewFloat(300)
}),
expectedErrors: PE("InvalidFieldError:safe_duration_factor"),
},
{
name: "Valid: safe_duration_factor is zero offset",
trip: newTrip(func(t *Trip) {
t.SafeDurationFactor = tt.NewFloat(1.0)
t.SafeDurationOffset = tt.NewFloat(0)
}),
expectedErrors: nil,
},
}

for _, tc := range tests {
Expand Down
76 changes: 75 additions & 1 deletion internal/generated/gqlout/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion schema/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,13 @@ type Trip {

"GTFS `trips.bikes_allowed` [0=no information, 1=vehicle can accommodate at least one bicycle, 2=no bicycles allowed]"
bikes_allowed: Int


"GTFS `trips.safe_duration_factor` (GTFS-Flex, google/transit#598); multiplier applied to driving duration for a safe travel time estimate"
safe_duration_factor: Float

"GTFS `trips.safe_duration_offset` (GTFS-Flex, google/transit#598); fixed offset in seconds added to driving duration for a safe travel time estimate"
safe_duration_offset: Float
Comment thread
drewda marked this conversation as resolved.

"Calculated stop pattern ID; an integer scoped to the feed version"
stop_pattern_id: Int!

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Add GTFS-Flex safe duration fields to trips table
-- See: https://github.com/google/transit/pull/598
ALTER TABLE public.gtfs_trips
ADD COLUMN safe_duration_factor double precision,
ADD COLUMN safe_duration_offset double precision;
2 changes: 2 additions & 0 deletions schema/sqlite/sqlite.sql
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ CREATE TABLE IF NOT EXISTS "gtfs_trips" (
"feed_version_id" integer NOT NULL,
"journey_pattern_id" integer,
"journey_pattern_offset" integer,
"safe_duration_factor" real,
"safe_duration_offset" real,
Comment thread
drewda marked this conversation as resolved.
"created_at" datetime DEFAULT CURRENT_TIMESTAMP,
"updated_at" datetime DEFAULT CURRENT_TIMESTAMP,
foreign key(feed_version_id) REFERENCES feed_versions(id),
Expand Down
2 changes: 2 additions & 0 deletions server/finders/dbfinder/trip.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ func tripSelect(limit *int, after *model.Cursor, ids []int, active bool, permFil
"gtfs_trips.stop_pattern_id",
"gtfs_trips.journey_pattern_id",
"gtfs_trips.journey_pattern_offset",
"gtfs_trips.safe_duration_factor",
"gtfs_trips.safe_duration_offset",
"current_feeds.id AS feed_id",
"current_feeds.onestop_id AS feed_onestop_id",
"feed_versions.sha1 AS feed_version_sha1",
Expand Down
26 changes: 13 additions & 13 deletions server/gql/agency_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestAgencyResolver(t *testing.T) {
name: "basic",
query: `query { agencies {agency_id}}`,
selector: "agencies.#.agency_id",
selectExpect: []string{"caltrain-ca-us", "a8b6ef46-7d4d-45f8-8200-cf4f5ce9d5a6", "BART", "", "1"},
selectExpect: []string{"caltrain-ca-us", "a8b6ef46-7d4d-45f8-8200-cf4f5ce9d5a6", "4982", "BART", "", "1"},
},
{
name: "basic fields",
Expand Down Expand Up @@ -371,7 +371,7 @@ func TestAgencyResolver_Location(t *testing.T) {
}`,
vars: hw{"lat": sanJoseFocus.Lat, "lon": sanJoseFocus.Lon},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpect: []string{"CT", "BA", "HA", "WMATA", "ctran-flex"},
selectExpect: []string{"CT", "BA", "HA", "hopelink-flex", "WMATA", "ctran-flex"},
},
{
name: "focus basic: East coast focus returns FL agency before CA agencies",
Expand All @@ -383,7 +383,7 @@ func TestAgencyResolver_Location(t *testing.T) {
}`,
vars: hw{"lat": floridaFocus.Lat, "lon": floridaFocus.Lon},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpect: []string{"HA", "WMATA", "BA", "CT", "ctran-flex"},
selectExpect: []string{"HA", "WMATA", "BA", "CT", "hopelink-flex", "ctran-flex"},
},
{
name: "focus with pagination: maintains ordering",
Expand Down Expand Up @@ -436,8 +436,8 @@ func TestAgencyResolver_License(t *testing.T) {
query: q,
vars: hw{"lic": hw{"share_alike_optional": "NO"}},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpectUnique: []string{"BA"},
selectExpectCount: 1,
selectExpectUnique: []string{"hopelink-flex", "BA"},
selectExpectCount: 2,
},
{
name: "license filter: share_alike_optional = exclude_no",
Expand All @@ -461,8 +461,8 @@ func TestAgencyResolver_License(t *testing.T) {
query: q,
vars: hw{"lic": hw{"create_derived_product": "NO"}},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpectUnique: []string{"BA"},
selectExpectCount: 1,
selectExpectUnique: []string{"hopelink-flex", "BA"},
selectExpectCount: 2,
},
{
name: "license filter: create_derived_product = exclude_no",
Expand All @@ -486,8 +486,8 @@ func TestAgencyResolver_License(t *testing.T) {
query: q,
vars: hw{"lic": hw{"commercial_use_allowed": "NO"}},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpectUnique: []string{"BA"},
selectExpectCount: 1,
selectExpectUnique: []string{"hopelink-flex", "BA"},
selectExpectCount: 2,
},
{
name: "license filter: commercial_use_allowed = exclude_no",
Expand All @@ -511,8 +511,8 @@ func TestAgencyResolver_License(t *testing.T) {
query: q,
vars: hw{"lic": hw{"redistribution_allowed": "NO"}},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpectUnique: []string{"BA"},
selectExpectCount: 1,
selectExpectUnique: []string{"hopelink-flex", "BA"},
selectExpectCount: 2,
},
{
name: "license filter: redistribution_allowed = exclude_no",
Expand All @@ -536,8 +536,8 @@ func TestAgencyResolver_License(t *testing.T) {
query: q,
vars: hw{"lic": hw{"use_without_attribution": "NO"}},
selector: "agencies.#.feed_version.feed.onestop_id",
selectExpectUnique: []string{"BA"},
selectExpectCount: 1,
selectExpectUnique: []string{"hopelink-flex", "BA"},
selectExpectCount: 2,
},
{
name: "license filter: use_without_attribution = exclude_no",
Expand Down
2 changes: 1 addition & 1 deletion server/gql/booking_rule_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestBookingRuleResolver(t *testing.T) {
}
}`,
vars: hw{"sha1": ctranFlexSha1, "brid": brid},
expect: `{"feed_versions":[{"booking_rules":[{"booking_rule_id":"booking_rule_id__2bc6804f-9e24-4b91-8947-c73a2363e7b6_MTWTFxx_20220107_20320522__053000_190000__053000_190000__m_b3a73dc523608998d850c431bf49b740093fd69415233fb3e74709073b335b6a","booking_type":1,"booking_url":"https://book.ridethecurrent.com","drop_off_message":null,"id":1,"info_url":null,"message":"The Current is an on-demand rideshare service by C-TRAN that provides point-to-point service for just the cost of a local bus ride. Schedule your ride on The Current app, at www.ridethecurrent.com or through our mobile app, or by calling 360-695-0123 then track your driver’s arrival.","phone_number":"360-695-0123","pickup_message":null,"prior_notice_duration_max":null,"prior_notice_duration_min":0,"prior_notice_last_day":null,"prior_notice_last_time":null,"prior_notice_start_day":2,"prior_notice_start_time":"00:00:00"}]}]}`,
expect: `{"feed_versions":[{"booking_rules":[{"booking_rule_id":"booking_rule_id__2bc6804f-9e24-4b91-8947-c73a2363e7b6_MTWTFxx_20220107_20320522__053000_190000__053000_190000__m_b3a73dc523608998d850c431bf49b740093fd69415233fb3e74709073b335b6a","booking_type":1,"booking_url":"https://book.ridethecurrent.com","drop_off_message":null,"id":2,"info_url":null,"message":"The Current is an on-demand rideshare service by C-TRAN that provides point-to-point service for just the cost of a local bus ride. Schedule your ride on The Current app, at www.ridethecurrent.com or through our mobile app, or by calling 360-695-0123 then track your driver’s arrival.","phone_number":"360-695-0123","pickup_message":null,"prior_notice_duration_max":null,"prior_notice_duration_min":0,"prior_notice_last_day":null,"prior_notice_last_time":null,"prior_notice_start_day":2,"prior_notice_start_time":"00:00:00"}]}]}`,
},
}
c, _ := newTestClient(t)
Expand Down
Loading
Loading