diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1dadf85..dcaa3a06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # CHANGELOG ----------------------- + +--- + ## Next - * Support German language and add initial translations. + +- Support German language and add initial translations. + ## Former - * Fix timetable previous stop time checks from checking text columns. - * Add feature allowing routing to avoid highways. - * Support Polish language and add initial translations. - * Fix bug displaying null continuous_pickup as '0'. - * Add route type selector with new extended GTFS route types. - * Fix bug to update continuous_pickup/dropoff values correctly. \ No newline at end of file + +- Fix timetable previous stop time checks from checking text columns. +- Add feature allowing routing to avoid highways. +- ~Support Polish language and add initial translations.~ +- Fix bug displaying null continuous_pickup as '0'. +- Add route type selector with new extended GTFS route types. +- Fix bug to update continuous_pickup/dropoff values correctly. diff --git a/__tests__/end-to-end.js b/__tests__/end-to-end.js index 1178763fa..4b1fa32a7 100644 --- a/__tests__/end-to-end.js +++ b/__tests__/end-to-end.js @@ -1099,40 +1099,43 @@ describe('end-to-end', () => { }, defaultTestTimeout, 'should create a project') if (doNonEssentialSteps) { - makeTestPostLogin('should delete a project', async () => { - const testProjectToDeleteName = `test-project-that-will-get-deleted-${testTime}` - - // navigate to home project view - await goto( - `https://datatools-ui-proxy/home/${testProjectId}`, - { - waitUntil: 'networkidle0' - } - ) - await waitForSelector('#context-dropdown') - - // create a new project - await createProject(testProjectToDeleteName) - - // get the created project id - // go into the project page and verify that it looks ok-ish - const projectEls = await getAllElements('.project-name-editable a') - - let projectFound = false - let projectToDeleteId = '' - for (const projectEl of projectEls) { - const innerHtml = await getInnerHTML(projectEl) - if (innerHtml.indexOf(testProjectToDeleteName) > -1) { - const href = await getHref(projectEl) - projectToDeleteId = href.match(/\/project\/([\w-]*)/)[1] - projectFound = true - break - } - } - if (!projectFound) throw new Error('Created project not found') - - await deleteProject(projectToDeleteId) - }, defaultTestTimeout, 'should create a project') + // makeTestPostLogin('should delete a project', async () => { + // const testProjectToDeleteName = `test-project-that-will-get-deleted-${testTime}` + + // // navigate to home project view + // await goto( + // `https://datatools-ui-proxy/home/${testProjectId}`, + // { + // waitUntil: 'networkidle0' + // } + // ) + // await waitForSelector('#context-dropdown') + + // // create a new project + // await createProject(testProjectToDeleteName) + + // // go back to project list + // await goto('http://localhost:9966/project', {waitUntil: 'networkidle0'}) + + // // get the created project id + // // go into the project page and verify that it looks ok-ish + // const projectEls = await getAllElements('.project-name-editable a') + + // let projectFound = false + // let projectToDeleteId = '' + // for (const projectEl of projectEls) { + // const innerHtml = await getInnerHTML(projectEl) + // if (innerHtml.indexOf(testProjectToDeleteName) > -1) { + // const href = await getHref(projectEl) + // projectToDeleteId = href.match(/\/project\/([\w-]*)/)[1] + // projectFound = true + // break + // } + // } + // if (!projectFound) throw new Error('Created project not found') + + // await deleteProject(projectToDeleteId) + // }, defaultTestTimeout, 'should create a project') } }) diff --git a/__tests__/test-utils/mock-data/store.js b/__tests__/test-utils/mock-data/store.js index e59464c12..09a390cdf 100644 --- a/__tests__/test-utils/mock-data/store.js +++ b/__tests__/test-utils/mock-data/store.js @@ -29,6 +29,7 @@ import {defaultState as defaultEditorSettingsState} from '../../../lib/editor/re import {defaultState as defaultEditorTimetableState} from '../../../lib/editor/reducers/timetable' import gtfs from '../../../lib/gtfs/reducers' import {defaultState as defaultGtfsFilterState} from '../../../lib/gtfs/reducers/filter' +import {defaultState as defaultGtfsLocationsState} from '../../../lib/gtfs/reducers/locations' import {defaultState as defaultGtfsPatternState} from '../../../lib/gtfs/reducers/patterns' import {defaultState as defaultGtfsRoutesState} from '../../../lib/gtfs/reducers/routes' import {defaultState as defaultGtfsShapesState} from '../../../lib/gtfs/reducers/shapes' @@ -97,6 +98,7 @@ export function getMockInitialState (): AppState { }, gtfs: { filter: defaultGtfsFilterState, + locations: defaultGtfsLocationsState, patterns: defaultGtfsPatternState, routes: defaultGtfsRoutesState, shapes: defaultGtfsShapesState, diff --git a/gtfs.yml b/gtfs.yml index 120b7e49b..400bd7c18 100644 --- a/gtfs.yml +++ b/gtfs.yml @@ -3,141 +3,141 @@ name: feed_info.txt helpContent: The file contains information about the feed itself, rather than the services that the feed describes. GTFS currently has an agency.txt file to provide information about the agencies that operate the services described by the feed. However, the publisher of the feed is sometimes a different entity than any of the agencies (in the case of regional aggregators). In addition, there are some fields that are really feed-wide settings, rather than agency-wide. fields: - - name: "feed_id" - required: false - inputType: TEXT - columnWidth: 12 - helpContent: "The feed_id field contains the feed_id which should uniquely identify the GTFS feed within trip planning applications and ideally be universally unique among public transit operators." - - name: "feed_publisher_name" - required: true - inputType: TEXT - columnWidth: 12 - helpContent: "The feed_publisher_name field contains the full name of the organization that publishes the feed. (This may be the same as one of the agency_name values in agency.txt.) GTFS-consuming applications can display this name when giving attribution for a particular feed's data." - - name: "feed_publisher_url" - required: true - inputType: URL - columnWidth: 12 - helpContent: "The feed_publisher_url field contains the URL of the feed publishing organization's website. (This may be the same as one of the agency_url values in agency.txt.) The value must be a fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped. See http://www.w3.org/Addressing/URL/4_URI_Recommentations.html for a description of how to create fully qualified URL values." - - name: "feed_lang" - required: true - inputType: LANGUAGE - columnWidth: 12 - helpContent: "The feed_lang field contains a IETF BCP 47 language code specifying the default language used for the text in this feed. This setting helps GTFS consumers choose capitalization rules and other language-specific settings for the feed. For an introduction to IETF BCP 47, please refer to http://www.rfc-editor.org/rfc/bcp/bcp47.txt and http://www.w3.org/International/articles/language-tags/." - - name: "default_lang" - required: false - inputType: LANGUAGE - columnWidth: 12 - helpContent: "Defines the language used when the data consumer doesn’t know the language of the rider. It's often defined as en, English." - - name: "feed_start_date" - required: false - inputType: DATE - columnWidth: 12 - helpContent: "The feed provides complete and reliable schedule information for service in the period from the beginning of the feed_start_date day to the end of the feed_end_date day. Both days are given as dates in YYYYMMDD format as for calendar.txt, or left empty if unavailable. The feed_end_date date must not precede the feed_start_date date if both are given. Feed providers are encouraged to give schedule data outside this period to advise of likely future service, but feed consumers should treat it mindful of its non-authoritative status. If feed_start_date or feed_end_date extend beyond the active calendar dates defined in calendar.txt and calendar_dates.txt, the feed is making an explicit assertion that there is no service for dates within the feed_start_date or feed_end_date range but not included in the active calendar dates." - - name: "feed_end_date" - required: false - inputType: DATE - columnWidth: 12 - helpContent: "The feed provides complete and reliable schedule information for service in the period from the beginning of the feed_start_date day to the end of the feed_end_date day. Both days are given as dates in YYYYMMDD format as for calendar.txt, or left empty if unavailable. The feed_end_date date must not precede the feed_start_date date if both are given. Feed providers are encouraged to give schedule data outside this period to advise of likely future service, but feed consumers should treat it mindful of its non-authoritative status. If feed_start_date or feed_end_date extend beyond the active calendar dates defined in calendar.txt and calendar_dates.txt, the feed is making an explicit assertion that there is no service for dates within the feed_start_date or feed_end_date range but not included in the active calendar dates." - - name: default_route_color - displayName: Default color - datatools: true - required: false - inputType: COLOR - columnWidth: 6 - - name: default_route_type - displayName: Default route type - datatools: true - columnWidth: 6 - required: false - inputType: DROPDOWN - options: - - value: 3 - text: Bus - - value: 0 - text: Tram - - value: 1 - text: Subway - - value: 2 - text: Rail - - value: 4 - text: Ferry - - value: 5 - text: Cable car - - value: 6 - text: Gondola - - value: 7 - text: Funicular - - name: "feed_version" - required: false - inputType: TEXT - columnWidth: 12 - helpContent: "The feed publisher can specify a string here that indicates the current version of their GTFS feed. GTFS-consuming applications can display this value to help feed publishers determine whether the latest version of their feed has been incorporated." - - name: "feed_contact_email" - required: false - inputType: EMAIL - columnWidth: 12 - helpContent: "Email address for communication regarding the GTFS dataset and data publishing practices." - - name: "feed_contact_url" - required: false - inputType: URL - columnWidth: 12 - helpContent: "URL for contact information, a web-form, support desk, or other tools for communication regarding the GTFS dataset and data publishing practices." + - name: "feed_id" + required: false + inputType: TEXT + columnWidth: 12 + helpContent: "The feed_id field contains the feed_id which should uniquely identify the GTFS feed within trip planning applications and ideally be universally unique among public transit operators." + - name: "feed_publisher_name" + required: true + inputType: TEXT + columnWidth: 12 + helpContent: "The feed_publisher_name field contains the full name of the organization that publishes the feed. (This may be the same as one of the agency_name values in agency.txt.) GTFS-consuming applications can display this name when giving attribution for a particular feed's data." + - name: "feed_publisher_url" + required: true + inputType: URL + columnWidth: 12 + helpContent: "The feed_publisher_url field contains the URL of the feed publishing organization's website. (This may be the same as one of the agency_url values in agency.txt.) The value must be a fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped. See http://www.w3.org/Addressing/URL/4_URI_Recommentations.html for a description of how to create fully qualified URL values." + - name: "feed_lang" + required: true + inputType: LANGUAGE + columnWidth: 12 + helpContent: "The feed_lang field contains a IETF BCP 47 language code specifying the default language used for the text in this feed. This setting helps GTFS consumers choose capitalization rules and other language-specific settings for the feed. For an introduction to IETF BCP 47, please refer to http://www.rfc-editor.org/rfc/bcp/bcp47.txt and http://www.w3.org/International/articles/language-tags/." + - name: "default_lang" + required: false + inputType: LANGUAGE + columnWidth: 12 + helpContent: "Defines the language used when the data consumer doesn’t know the language of the rider. It's often defined as en, English." + - name: "feed_start_date" + required: false + inputType: DATE + columnWidth: 12 + helpContent: "The feed provides complete and reliable schedule information for service in the period from the beginning of the feed_start_date day to the end of the feed_end_date day. Both days are given as dates in YYYYMMDD format as for calendar.txt, or left empty if unavailable. The feed_end_date date must not precede the feed_start_date date if both are given. Feed providers are encouraged to give schedule data outside this period to advise of likely future service, but feed consumers should treat it mindful of its non-authoritative status. If feed_start_date or feed_end_date extend beyond the active calendar dates defined in calendar.txt and calendar_dates.txt, the feed is making an explicit assertion that there is no service for dates within the feed_start_date or feed_end_date range but not included in the active calendar dates." + - name: "feed_end_date" + required: false + inputType: DATE + columnWidth: 12 + helpContent: "The feed provides complete and reliable schedule information for service in the period from the beginning of the feed_start_date day to the end of the feed_end_date day. Both days are given as dates in YYYYMMDD format as for calendar.txt, or left empty if unavailable. The feed_end_date date must not precede the feed_start_date date if both are given. Feed providers are encouraged to give schedule data outside this period to advise of likely future service, but feed consumers should treat it mindful of its non-authoritative status. If feed_start_date or feed_end_date extend beyond the active calendar dates defined in calendar.txt and calendar_dates.txt, the feed is making an explicit assertion that there is no service for dates within the feed_start_date or feed_end_date range but not included in the active calendar dates." + - name: default_route_color + displayName: Default color + datatools: true + required: false + inputType: COLOR + columnWidth: 6 + - name: default_route_type + displayName: Default route type + datatools: true + columnWidth: 6 + required: false + inputType: DROPDOWN + options: + - value: 3 + text: Bus + - value: 0 + text: Tram + - value: 1 + text: Subway + - value: 2 + text: Rail + - value: 4 + text: Ferry + - value: 5 + text: Cable car + - value: 6 + text: Gondola + - value: 7 + text: Funicular + - name: "feed_version" + required: false + inputType: TEXT + columnWidth: 12 + helpContent: "The feed publisher can specify a string here that indicates the current version of their GTFS feed. GTFS-consuming applications can display this value to help feed publishers determine whether the latest version of their feed has been incorporated." + - name: "feed_contact_email" + required: false + inputType: EMAIL + columnWidth: 12 + helpContent: "Email address for communication regarding the GTFS dataset and data publishing practices." + - name: "feed_contact_url" + required: false + inputType: URL + columnWidth: 12 + helpContent: "URL for contact information, a web-form, support desk, or other tools for communication regarding the GTFS dataset and data publishing practices." - id: agency name: agency.txt helpContent: One or more transit agencies that provide the data in this feed. fields: - - name: "agency_id" - required: false - inputType: GTFS_ID - columnWidth: 12 - helpContent: "The agency_id field is an ID that uniquely identifies a transit agency. A transit feed may represent data from more than one agency. The agency_id is dataset unique. This field is optional for transit feeds that only contain data for a single agency." - - name: "agency_name" - required: true - inputType: TEXT - columnWidth: 12 - helpContent: "The agency_name field contains the full name of the transit agency. Google Maps will display this name." - - name: "agency_url" - required: true - inputType: URL - placeholder: https://agency.org - columnWidth: 12 - helpContent: "The agency_url field contains the URL of the transit agency. The value must be a fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped." - - name: "agency_timezone" - required: true - inputType: TIMEZONE - columnWidth: 12 - helpContent: "The agency_timezone field contains the timezone where the transit agency is located. Timezone names never contain the space character but may contain an underscore. If multiple agencies are specified in the feed, each must have the same agency_timezone." - - name: "agency_lang" - required: false - inputType: LANGUAGE - columnWidth: 12 - helpContent: "The agency_lang field contains a two-letter ISO 639-1 code for the primary language used by this transit agency. The language code is case-insensitive (both en and EN are accepted). This setting defines capitalization rules and other language-specific settings for all text contained in this transit agency's feed. Please refer to http://www.loc.gov/standards/iso639-2/php/code_list.php for a list of valid values." - - name: "agency_phone" - required: false - inputType: TEXT - columnWidth: 12 - helpContent: "The agency_phone field contains a single voice telephone number for the specified agency. This field is a string value that presents the telephone number as typical for the agency's service area. It can and should contain punctuation marks to group the digits of the number. Dialable text (for example, TriMet's \"503-238-RIDE\") is permitted, but the field must not contain any other descriptive text." - - name: "agency_fare_url" - required: false - inputType: URL - placeholder: https://agency.org/fares - columnWidth: 12 - helpContent: "The agency_fare_url specifies the URL of a web page that allows a rider to purchase tickets or other fare instruments for that agency online. The value must be a fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped. Seehttp://www.w3.org/Addressing/URL/4_URI_Recommentations.html for a description of how to create fully qualified URL values." - - name: "agency_email" - required: false - inputType: EMAIL - placeholder: contact@agency.org - columnWidth: 12 - helpContent: "The agency_email field contains a single valid email address actively monitored by the agency’s customer service department. This email address will be considered a direct contact point where transit riders can reach a customer service representative at the agency." - - name: agency_branding_url - required: false - displayName: Agency branding URL - datatools: false - placeholder: https://agency.org/assets/agency/XYZ - inputType: URL - columnWidth: 12 - helpContent: + - name: "agency_id" + required: false + inputType: GTFS_ID + columnWidth: 12 + helpContent: "The agency_id field is an ID that uniquely identifies a transit agency. A transit feed may represent data from more than one agency. The agency_id is dataset unique. This field is optional for transit feeds that only contain data for a single agency." + - name: "agency_name" + required: true + inputType: TEXT + columnWidth: 12 + helpContent: "The agency_name field contains the full name of the transit agency. Google Maps will display this name." + - name: "agency_url" + required: true + inputType: URL + placeholder: https://agency.org + columnWidth: 12 + helpContent: "The agency_url field contains the URL of the transit agency. The value must be a fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped." + - name: "agency_timezone" + required: true + inputType: TIMEZONE + columnWidth: 12 + helpContent: "The agency_timezone field contains the timezone where the transit agency is located. Timezone names never contain the space character but may contain an underscore. If multiple agencies are specified in the feed, each must have the same agency_timezone." + - name: "agency_lang" + required: false + inputType: LANGUAGE + columnWidth: 12 + helpContent: "The agency_lang field contains a two-letter ISO 639-1 code for the primary language used by this transit agency. The language code is case-insensitive (both en and EN are accepted). This setting defines capitalization rules and other language-specific settings for all text contained in this transit agency's feed. Please refer to http://www.loc.gov/standards/iso639-2/php/code_list.php for a list of valid values." + - name: "agency_phone" + required: false + inputType: TEXT + columnWidth: 12 + helpContent: 'The agency_phone field contains a single voice telephone number for the specified agency. This field is a string value that presents the telephone number as typical for the agency''s service area. It can and should contain punctuation marks to group the digits of the number. Dialable text (for example, TriMet''s "503-238-RIDE") is permitted, but the field must not contain any other descriptive text.' + - name: "agency_fare_url" + required: false + inputType: URL + placeholder: https://agency.org/fares + columnWidth: 12 + helpContent: "The agency_fare_url specifies the URL of a web page that allows a rider to purchase tickets or other fare instruments for that agency online. The value must be a fully qualified URL that includes http:// or https://, and any special characters in the URL must be correctly escaped. Seehttp://www.w3.org/Addressing/URL/4_URI_Recommentations.html for a description of how to create fully qualified URL values." + - name: "agency_email" + required: false + inputType: EMAIL + placeholder: contact@agency.org + columnWidth: 12 + helpContent: "The agency_email field contains a single valid email address actively monitored by the agency’s customer service department. This email address will be considered a direct contact point where transit riders can reach a customer service representative at the agency." + - name: agency_branding_url + required: false + displayName: Agency branding URL + datatools: false + placeholder: https://agency.org/assets/agency/XYZ + inputType: URL + columnWidth: 12 + helpContent: - id: stop name: stops.txt @@ -571,469 +571,658 @@ name: trips.txt helpContent: Trips for each route. A trip is a sequence of two or more stops that occurs at specific time. fields: - - name: "route_id" - required: true - inputType: GTFS_ROUTE - columnWidth: 6 - helpContent: "The route_id field contains an ID that uniquely identifies a route. This value is referenced from the routes.txt file." - - name: "service_id" - required: true - inputType: GTFS_SERVICE - columnWidth: 6 - helpContent: "The service_id contains an ID that uniquely identifies a set of dates when service is available for one or more routes. This value is referenced from thecalendar.txt or calendar_dates.txt file." - - name: "trip_id" - required: true - inputType: GTFS_ID - columnWidth: 6 - helpContent: "The trip_id field contains an ID that identifies a trip. The trip_id is dataset unique." - - name: "trip_headsign" - required: false - inputType: TEXT - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The trip_headsign field contains the text that appears on a sign that identifies the trip's destination to passengers. Use this field to distinguish between different patterns of service in the same route. If the headsign changes during a trip, you can override the trip_headsign by specifying values for the the stop_headsign field in stop_times.txt." - - name: "trip_short_name" - required: false - inputType: TEXT - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The trip_short_name field contains the text that appears in schedules and sign boards to identify the trip to passengers, for example, to identify train numbers for commuter rail trips. If riders do not commonly rely on trip names, please leave this field blank." - - name: "direction_id" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The direction_id field contains a binary value that indicates the direction of travel for a trip. Use this field to distinguish between bi-directional trips with the same route_id. This field is not used in routing; it provides a way to separate trips by direction when publishing time tables. You can specify names for each direction with the trip_headsign field." - - name: "block_id" - required: false - inputType: GTFS_BLOCK - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The block_id field identifies the block to which the trip belongs. A block consists of two or more sequential trips made using the same vehicle, where a passenger can transfer from one trip to the next just by staying in the vehicle. The block_id must be referenced by two or more trips in trips.txt." - - name: "shape_id" - required: false - inputType: GTFS_SHAPE - columnWidth: 6 - helpContent: "The shape_id field contains an ID that defines a shape for the trip. This value is referenced from the shapes.txt file. The shapes.txt file allows you to define how a line should be drawn on the map to represent a trip." - - name: "wheelchair_accessible" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - - value: '2' - columnWidth: 6 - helpContent: "0 (or empty) - indicates that there is no accessibility information for the trip" - - name: "bikes_allowed" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - - value: '2' - columnWidth: 6 - helpContent: "0 (or empty) - indicates that there is no bike information for the trip" - + - name: "route_id" + required: true + inputType: GTFS_ROUTE + columnWidth: 6 + helpContent: "The route_id field contains an ID that uniquely identifies a route. This value is referenced from the routes.txt file." + - name: "service_id" + required: true + inputType: GTFS_SERVICE + columnWidth: 6 + helpContent: "The service_id contains an ID that uniquely identifies a set of dates when service is available for one or more routes. This value is referenced from thecalendar.txt or calendar_dates.txt file." + - name: "trip_id" + required: true + inputType: GTFS_ID + columnWidth: 6 + helpContent: "The trip_id field contains an ID that identifies a trip. The trip_id is dataset unique." + - name: "trip_headsign" + required: false + inputType: TEXT + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The trip_headsign field contains the text that appears on a sign that identifies the trip's destination to passengers. Use this field to distinguish between different patterns of service in the same route. If the headsign changes during a trip, you can override the trip_headsign by specifying values for the the stop_headsign field in stop_times.txt." + - name: "trip_short_name" + required: false + inputType: TEXT + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The trip_short_name field contains the text that appears in schedules and sign boards to identify the trip to passengers, for example, to identify train numbers for commuter rail trips. If riders do not commonly rely on trip names, please leave this field blank." + - name: "direction_id" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The direction_id field contains a binary value that indicates the direction of travel for a trip. Use this field to distinguish between bi-directional trips with the same route_id. This field is not used in routing; it provides a way to separate trips by direction when publishing time tables. You can specify names for each direction with the trip_headsign field." + - name: "block_id" + required: false + inputType: GTFS_BLOCK + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The block_id field identifies the block to which the trip belongs. A block consists of two or more sequential trips made using the same vehicle, where a passenger can transfer from one trip to the next just by staying in the vehicle. The block_id must be referenced by two or more trips in trips.txt." + - name: "shape_id" + required: false + inputType: GTFS_SHAPE + columnWidth: 6 + helpContent: "The shape_id field contains an ID that defines a shape for the trip. This value is referenced from the shapes.txt file. The shapes.txt file allows you to define how a line should be drawn on the map to represent a trip." + - name: "wheelchair_accessible" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + - value: "2" + columnWidth: 6 + helpContent: "0 (or empty) - indicates that there is no accessibility information for the trip" + - name: "bikes_allowed" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + - value: "2" + columnWidth: 6 + helpContent: "0 (or empty) - indicates that there is no bike information for the trip" - id: stop_time name: stop_times.txt helpContent: Times that a vehicle arrives at and departs from individual stops for each trip. fields: - - name: "trip_id" - required: true - inputType: GTFS_TRIP - columnWidth: 6 - helpContent: "The trip_id field contains an ID that identifies a trip. This value is referenced from the trips.txt file." - - name: "arrival_time" - required: true - inputType: TIME - columnWidth: 6 - helpContent: "The arrival_time specifies the arrival time at a specific stop for a specific trip on a route. The time is measured from " - - name: "departure_time" - required: true - inputType: TIME - columnWidth: 6 - helpContent: "The departure_time specifies the departure time from a specific stop for a specific trip on a route. The time is measured from " - - name: "stop_id" - required: true - inputType: GTFS_STOP - columnWidth: 6 - helpContent: "The stop_id field contains an ID that uniquely identifies a stop. Multiple routes may use the same stop. The stop_id is referenced from the stops.txt file. If location_type is used in stops.txt, all stops referenced in stop_times.txt must have location_type of 0." - - name: "stop_sequence" - required: true - inputType: POSITIVE_INT - columnWidth: 6 - helpContent: "The stop_sequence field identifies the order of the stops for a particular trip. The values for stop_sequence must be non-negative integers, and they must increase along the trip." - - name: "stop_headsign" - required: false - inputType: TEXT - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The stop_headsign field contains the text that appears on a sign that identifies the trip's destination to passengers. Use this field to override the defaulttrip_headsign when the headsign changes between stops. If this headsign is associated with an entire trip, use trip_headsign instead." - - name: "pickup_type" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - - value: '2' - - value: '3' - columnWidth: 6 - helpContent: "The pickup_type field indicates whether passengers are picked up at a stop as part of the normal schedule or whether a pickup at the stop is not available. This field also allows the transit agency to indicate that passengers must call the agency or notify the driver to arrange a pickup at a particular stop. Valid values for this field are:" - - name: "drop_off_type" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - - value: '2' - - value: '3' - columnWidth: 6 - helpContent: "The drop_off_type field indicates whether passengers are dropped off at a stop as part of the normal schedule or whether a drop off at the stop is not available. This field also allows the transit agency to indicate that passengers must call the agency or notify the driver to arrange a drop off at a particular stop. Valid values for this field are:" - - name: "shape_dist_traveled" - required: false - inputType: POSITIVE_NUM - columnWidth: 6 - helpContent: "When used in the stop_times.txt file, the shape_dist_traveled field positions a stop as a distance from the first shape point. The shape_dist_traveled field represents a real distance traveled along the route in units such as feet or kilometers. For example, if a bus travels a distance of 5.25 kilometers from the start of the shape to the stop, the shape_dist_traveled for the stop ID would be entered as " - - name: "timepoint" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '' - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The timepoint field can be used to indicate if the specified arrival and departure times for a stop are strictly adhered to by the transit vehicle or if they are instead approximate and/or interpolated times. The field allows a GTFS producer to provide interpolated stop times that potentially incorporate local knowledge, but still indicate if the times are approximate. For stop-time entries with specified arrival and departure times, valid values for this field are:" - -- id: calendar - name: calendar.txt - helpContent: Dates for service IDs using a weekly schedule. Specify when service starts and ends, as well as days of the week where service is available. - fields: - - name: "service_id" - required: true - inputType: GTFS_ID - columnWidth: 6 - helpContent: "The service_id contains an ID that uniquely identifies a set of dates when service is available for one or more routes. Each service_id value can appear at most once in a calendar.txt file. This value is dataset unique. It is referenced by the trips.txt file." - - name: "description" - required: false - datatools: true - inputType: TEXT - bulkEditEnabled: true - columnWidth: 6 - helpContent: - - name: "monday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The monday field contains a binary value that indicates whether the service is valid for all Mondays." - - name: "tuesday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The tuesday field contains a binary value that indicates whether the service is valid for all Tuesdays." - - name: "wednesday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The wednesday field contains a binary value that indicates whether the service is valid for all Wednesdays." - - name: "thursday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The thursday field contains a binary value that indicates whether the service is valid for all Thursdays." - - name: "friday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The friday field contains a binary value that indicates whether the service is valid for all Fridays." - - name: "saturday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The saturday field contains a binary value that indicates whether the service is valid for all Saturdays." - - name: "sunday" - required: true - inputType: DAY_OF_WEEK_BOOLEAN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The sunday field contains a binary value that indicates whether the service is valid for all Sundays." - - name: "start_date" - required: true - inputType: DATE - bulkEditEnabled: true - columnWidth: 12 - helpContent: "The start_date field contains the start date for the service." - - name: "end_date" - required: true - inputType: DATE - bulkEditEnabled: true - columnWidth: 12 - helpContent: "The end_date field contains the end date for the service. This date is included in the service interval." + - name: "trip_id" + required: true + inputType: GTFS_TRIP + columnWidth: 6 + helpContent: "The trip_id field contains an ID that identifies a trip. This value is referenced from the trips.txt file." + - name: "arrival_time" + required: true + inputType: TIME + columnWidth: 6 + helpContent: "The arrival_time specifies the arrival time at a specific stop for a specific trip on a route. The time is measured from " + - name: "departure_time" + required: true + inputType: TIME + columnWidth: 6 + helpContent: "The departure_time specifies the departure time from a specific stop for a specific trip on a route. The time is measured from " + - name: "start_pickup_drop_off_window" + required: false + inputType: TIME + columnWidth: 6 + - name: "end_pickup_drop_off_window" + required: false + inputType: TIME + columnWidth: 6 + - name: "stop_id" + required: false + inputType: GTFS_STOP + columnWidth: 6 + helpContent: "The stop_id field contains an ID that uniquely identifies a stop. Multiple routes may use the same stop. The stop_id is referenced from the stops.txt file. If location_type is used in stops.txt, all stops referenced in stop_times.txt must have location_type of 0." + - name: "location_id" + required: false + inputType: GTFS_ID + columnWidth: 6 + helpContent: "The location_id field contains an ID that uniquely identifies a location. Multiple routes may use the same stop. The stop_id is referenced from the stops.txt file. If location_type is used in stops.txt, all stops referenced in stop_times.txt must have location_type of 0." + - name: "location_group_id" + required: false + inputType: TEXT + columnWidth: 6 + helpContent: "The location_group_id field contains an ID that uniquely identifies a location group. Multiple routes may use the same stop. The stop_id is referenced from the stops.txt file. If location_type is used in stops.txt, all stops referenced in stop_times.txt must have location_type of 0." + - name: "stop_sequence" + required: true + inputType: POSITIVE_INT + columnWidth: 6 + helpContent: "The stop_sequence field identifies the order of the stops for a particular trip. The values for stop_sequence must be non-negative integers, and they must increase along the trip." + - name: "stop_headsign" + required: false + inputType: TEXT + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The stop_headsign field contains the text that appears on a sign that identifies the trip's destination to passengers. Use this field to override the defaulttrip_headsign when the headsign changes between stops. If this headsign is associated with an entire trip, use trip_headsign instead." + - name: "pickup_type" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + - value: "2" + - value: "3" + columnWidth: 6 + helpContent: "The pickup_type field indicates whether passengers are picked up at a stop as part of the normal schedule or whether a pickup at the stop is not available. This field also allows the transit agency to indicate that passengers must call the agency or notify the driver to arrange a pickup at a particular stop. Valid values for this field are:" + - name: "drop_off_type" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + - value: "2" + - value: "3" + columnWidth: 6 + helpContent: "The drop_off_type field indicates whether passengers are dropped off at a stop as part of the normal schedule or whether a drop off at the stop is not available. This field also allows the transit agency to indicate that passengers must call the agency or notify the driver to arrange a drop off at a particular stop. Valid values for this field are:" + - name: "pickup_booking_rule_id" + required: false + displayName: Pickup Booking Rule + # TODO: make this a real type + inputType: GTFSFLEX_BOOKING_RULE + bulkEditEnabled: true + helpContent: "Identifies the boarding booking rule at this stop time." + - name: "drop_off_booking_rule_id" + required: false + displayName: Dropoff Booking Rule + # TODO: make this a real type + inputType: GTFSFLEX_BOOKING_RULE + bulkEditEnabled: true + helpContent: "Identifies the alighting booking rule at this stop time." + - name: "shape_dist_traveled" + required: false + inputType: POSITIVE_NUM + columnWidth: 6 + helpContent: "When used in the stop_times.txt file, the shape_dist_traveled field positions a stop as a distance from the first shape point. The shape_dist_traveled field represents a real distance traveled along the route in units such as feet or kilometers. For example, if a bus travels a distance of 5.25 kilometers from the start of the shape to the stop, the shape_dist_traveled for the stop ID would be entered as " + - name: "timepoint" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "" + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The timepoint field can be used to indicate if the specified arrival and departure times for a stop are strictly adhered to by the transit vehicle or if they are instead approximate and/or interpolated times. The field allows a GTFS producer to provide interpolated stop times that potentially incorporate local knowledge, but still indicate if the times are approximate. For stop-time entries with specified arrival and departure times, valid values for this field are:" +- id: calendar + name: calendar.txt + helpContent: Dates for service IDs using a weekly schedule. Specify when service starts and ends, as well as days of the week where service is available. + fields: + - name: "service_id" + required: true + inputType: GTFS_ID + columnWidth: 6 + helpContent: "The service_id contains an ID that uniquely identifies a set of dates when service is available for one or more routes. Each service_id value can appear at most once in a calendar.txt file. This value is dataset unique. It is referenced by the trips.txt file." + - name: "description" + required: false + datatools: true + inputType: TEXT + bulkEditEnabled: true + columnWidth: 6 + helpContent: + - name: "monday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The monday field contains a binary value that indicates whether the service is valid for all Mondays." + - name: "tuesday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The tuesday field contains a binary value that indicates whether the service is valid for all Tuesdays." + - name: "wednesday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The wednesday field contains a binary value that indicates whether the service is valid for all Wednesdays." + - name: "thursday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The thursday field contains a binary value that indicates whether the service is valid for all Thursdays." + - name: "friday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The friday field contains a binary value that indicates whether the service is valid for all Fridays." + - name: "saturday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The saturday field contains a binary value that indicates whether the service is valid for all Saturdays." + - name: "sunday" + required: true + inputType: DAY_OF_WEEK_BOOLEAN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The sunday field contains a binary value that indicates whether the service is valid for all Sundays." + - name: "start_date" + required: true + inputType: DATE + bulkEditEnabled: true + columnWidth: 12 + helpContent: "The start_date field contains the start date for the service." + - name: "end_date" + required: true + inputType: DATE + bulkEditEnabled: true + columnWidth: 12 + helpContent: "The end_date field contains the end date for the service. This date is included in the service interval." - id: calendar_dates name: calendar_dates.txt helpContent: Exceptions for the service IDs defined in the calendar.txt file. If calendar_dates.txt includes ALL dates of service, this file may be specified instead of calendar.txt. fields: - - name: "service_id" - required: true - inputType: GTFS_SERVICE - columnWidth: 6 - helpContent: "The service_id contains an ID that uniquely identifies a set of dates when a service exception is available for one or more routes. Each (service_id, date) pair can only appear once in calendar_dates.txt. If the a service_id value appears in both the calendar.txt and calendar_dates.txt files, the information in calendar_dates.txt modifies the service information specified in calendar.txt. This field is referenced by the trips.txt file." - - name: "date" - required: true - inputType: DATE - bulkEditEnabled: true - columnWidth: 12 - helpContent: "The date field specifies a particular date when service availability is different than the norm. You can use the exception_type field to indicate whether service is available on the specified date." - - name: "exception_type" - required: true - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '1' - - value: '2' - columnWidth: 6 - helpContent: "The exception_type indicates whether service is available on the date specified in thedate field." - + - name: "service_id" + required: true + inputType: GTFS_SERVICE + columnWidth: 6 + helpContent: "The service_id contains an ID that uniquely identifies a set of dates when a service exception is available for one or more routes. Each (service_id, date) pair can only appear once in calendar_dates.txt. If the a service_id value appears in both the calendar.txt and calendar_dates.txt files, the information in calendar_dates.txt modifies the service information specified in calendar.txt. This field is referenced by the trips.txt file." + - name: "date" + required: true + inputType: DATE + bulkEditEnabled: true + columnWidth: 12 + helpContent: "The date field specifies a particular date when service availability is different than the norm. You can use the exception_type field to indicate whether service is available on the specified date." + - name: "exception_type" + required: true + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "1" + - value: "2" + columnWidth: 6 + helpContent: "The exception_type indicates whether service is available on the date specified in thedate field." - id: fare_attributes name: fare_attributes.txt helpContent: Fare information for a transit organization's routes. fields: - - name: "fare_id" - required: true - inputType: GTFS_ID - columnWidth: 6 - helpContent: "The fare_id field contains an ID that uniquely identifies a fare class. The fare_id is dataset unique." - - name: "price" - required: true - inputType: NUMBER - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The price field contains the fare price, in the unit specified by currency_type." - - name: "currency_type" - required: true - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: USD - text: US dollar (USD) - - value: AUD - text: Australian dollar (AUD) - - value: CAD - text: Canadian dollar (CAD) - - value: CHF - text: Swiss franc (CHF) - - value: CNH - text: Chinese renminbi (CNH) - - value: EUR - text: Euro (EUR) - - value: GBP - text: Pound sterling (GBP) - - value: JPY - text: Japanese yen (JPY) - - value: MXN - text: Mexican peso (MXN) - - value: NZD - text: New Zealand dollar (NZD) - - value: SEK - text: Swedish krona (SEK) - columnWidth: 12 - helpContent: "The currency_type field defines the currency used to pay the fare. Please use the ISO 4217 alphabetical currency codes which can be found at the following URL:http://en.wikipedia.org/wiki/ISO_4217." - - name: "payment_method" - required: true - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - text: Paid on board (0) - - value: '1' - text: Paid before boarding (1) - columnWidth: 12 - helpContent: "The payment_method field indicates when the fare must be paid. Valid values for this field are:" - - name: "transfers" - required: true - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - text: No transfers permitted (0) - - value: '1' - text: One transfers permitted (1) - - value: '2' - text: Two transfers permitted (2) - - value: '' - text: Unlimited transfers (empty) - columnWidth: 6 - helpContent: "The transfers field specifies the number of transfers permitted on this fare. Valid values for this field are:" - - name: "transfer_duration" - required: false - inputType: POSITIVE_INT - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The transfer_duration field specifies the length of time in seconds before a transfer expires." - - name: agency_id - required: false - inputType: GTFS_AGENCY - bulkEditEnabled: true - columnWidth: 12 - helpContent: "Required for feeds with multiple agencies defined in the agency.txt file. Each fare attribute must specify an agency_id value to indicate which agency the fare applies to." - + - name: "fare_id" + required: true + inputType: GTFS_ID + columnWidth: 6 + helpContent: "The fare_id field contains an ID that uniquely identifies a fare class. The fare_id is dataset unique." + - name: "price" + required: true + inputType: NUMBER + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The price field contains the fare price, in the unit specified by currency_type." + - name: "currency_type" + required: true + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: USD + text: US dollar (USD) + - value: AUD + text: Australian dollar (AUD) + - value: CAD + text: Canadian dollar (CAD) + - value: CHF + text: Swiss franc (CHF) + - value: CNH + text: Chinese renminbi (CNH) + - value: EUR + text: Euro (EUR) + - value: GBP + text: Pound sterling (GBP) + - value: JPY + text: Japanese yen (JPY) + - value: MXN + text: Mexican peso (MXN) + - value: NZD + text: New Zealand dollar (NZD) + - value: SEK + text: Swedish krona (SEK) + columnWidth: 12 + helpContent: "The currency_type field defines the currency used to pay the fare. Please use the ISO 4217 alphabetical currency codes which can be found at the following URL:http://en.wikipedia.org/wiki/ISO_4217." + - name: "payment_method" + required: true + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + text: Paid on board (0) + - value: "1" + text: Paid before boarding (1) + columnWidth: 12 + helpContent: "The payment_method field indicates when the fare must be paid. Valid values for this field are:" + - name: "transfers" + required: true + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + text: No transfers permitted (0) + - value: "1" + text: One transfers permitted (1) + - value: "2" + text: Two transfers permitted (2) + - value: "" + text: Unlimited transfers (empty) + columnWidth: 6 + helpContent: "The transfers field specifies the number of transfers permitted on this fare. Valid values for this field are:" + - name: "transfer_duration" + required: false + inputType: POSITIVE_INT + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The transfer_duration field specifies the length of time in seconds before a transfer expires." + - name: agency_id + required: false + inputType: GTFS_AGENCY + bulkEditEnabled: true + columnWidth: 12 + helpContent: "Required for feeds with multiple agencies defined in the agency.txt file. Each fare attribute must specify an agency_id value to indicate which agency the fare applies to." - id: fare_rules name: fare_rules.txt helpContent: Rules for applying fare information for a transit organization's routes. fields: - - name: "fare_id" - required: true - inputType: GTFS_FARE - columnWidth: 6 - helpContent: "The fare_id field contains an ID that uniquely identifies a fare class. This value is referenced from the fare_attributes.txt file." - - name: "route_id" - required: false - inputType: GTFS_ROUTE - columnWidth: 12 - helpContent: "The route_id field associates the fare ID with a route. Route IDs are referenced from the routes.txt file. If you have several routes with the same fare attributes, create a row in fare_rules.txt for each route." - - name: "origin_id" - required: false - inputType: GTFS_ZONE - columnWidth: 6 - helpContent: "The origin_id field associates the fare ID with an origin zone ID. Zone IDs are referenced from the stops.txt file. If you have several origin IDs with the same fare attributes, create a row in fare_rules.txt for each origin ID." - - name: "destination_id" - required: false - inputType: GTFS_ZONE - columnWidth: 6 - helpContent: "The destination_id field associates the fare ID with a destination zone ID. Zone IDs are referenced from the stops.txt file. If you have several destination IDs with the same fare attributes, create a row in fare_rules.txt for each destination ID." - - name: "contains_id" - required: false - inputType: GTFS_ZONE - columnWidth: 6 - helpContent: "The contains_id field associates the fare ID with a zone ID, referenced from thestops.txt file. The fare ID is then associated with itineraries that pass through every contains_id zone." - + - name: "fare_id" + required: true + inputType: GTFS_FARE + columnWidth: 6 + helpContent: "The fare_id field contains an ID that uniquely identifies a fare class. This value is referenced from the fare_attributes.txt file." + - name: "route_id" + required: false + inputType: GTFS_ROUTE + columnWidth: 12 + helpContent: "The route_id field associates the fare ID with a route. Route IDs are referenced from the routes.txt file. If you have several routes with the same fare attributes, create a row in fare_rules.txt for each route." + - name: "origin_id" + required: false + inputType: GTFS_ZONE + columnWidth: 6 + helpContent: "The origin_id field associates the fare ID with an origin zone ID. Zone IDs are referenced from the stops.txt file. If you have several origin IDs with the same fare attributes, create a row in fare_rules.txt for each origin ID." + - name: "destination_id" + required: false + inputType: GTFS_ZONE + columnWidth: 6 + helpContent: "The destination_id field associates the fare ID with a destination zone ID. Zone IDs are referenced from the stops.txt file. If you have several destination IDs with the same fare attributes, create a row in fare_rules.txt for each destination ID." + - name: "contains_id" + required: false + inputType: GTFS_ZONE + columnWidth: 6 + helpContent: "The contains_id field associates the fare ID with a zone ID, referenced from thestops.txt file. The fare ID is then associated with itineraries that pass through every contains_id zone." - id: frequencies name: frequencies.txt helpContent: Headway (time between trips) for routes with variable frequency of service. fields: - - name: "trip_id" - required: true - inputType: GTFS_TRIP - columnWidth: 6 - helpContent: "The trip_id contains an ID that identifies a trip on which the specified frequency of service applies. Trip IDs are referenced from the trips.txt file." - - name: "start_time" - required: true - inputType: TIME - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The start_time field specifies the time at which service begins with the specified frequency. The time is measured from " - - name: "end_time" - required: true - inputType: TIME - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The end_time field indicates the time at which service changes to a different frequency (or ceases) at the first stop in the trip. The time is measured from " - - name: "headway_secs" - required: true - inputType: POSITIVE_INT - bulkEditEnabled: true - columnWidth: 6 - helpContent: "The headway_secs field indicates the time between departures from the same stop (headway) for this trip type, during the time interval specified by start_time andend_time. The headway value must be entered in seconds." - - name: "exact_times" - required: false - inputType: DROPDOWN - bulkEditEnabled: true - options: - - value: '0' - - value: '1' - columnWidth: 6 - helpContent: "The exact_times field determines if frequency-based trips should be exactly scheduled based on the specified headway information. Valid values for this field are:" - + - name: "trip_id" + required: true + inputType: GTFS_TRIP + columnWidth: 6 + helpContent: "The trip_id contains an ID that identifies a trip on which the specified frequency of service applies. Trip IDs are referenced from the trips.txt file." + - name: "start_time" + required: true + inputType: TIME + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The start_time field specifies the time at which service begins with the specified frequency. The time is measured from " + - name: "end_time" + required: true + inputType: TIME + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The end_time field indicates the time at which service changes to a different frequency (or ceases) at the first stop in the trip. The time is measured from " + - name: "headway_secs" + required: true + inputType: POSITIVE_INT + bulkEditEnabled: true + columnWidth: 6 + helpContent: "The headway_secs field indicates the time between departures from the same stop (headway) for this trip type, during the time interval specified by start_time andend_time. The headway value must be entered in seconds." + - name: "exact_times" + required: false + inputType: DROPDOWN + bulkEditEnabled: true + options: + - value: "0" + - value: "1" + columnWidth: 6 + helpContent: "The exact_times field determines if frequency-based trips should be exactly scheduled based on the specified headway information. Valid values for this field are:" - id: transfers name: transfers.txt helpContent: Rules for making connections at transfer points between routes. fields: - - name: "from_stop_id" - required: true - inputType: GTFS_STOP - columnWidth: 12 - helpContent: "The from_stop_id field contains a stop ID that identifies a stop or station where a connection between routes begins. Stop IDs are referenced from the stops.txt file. If the stop ID refers to a station that contains multiple stops, this transfer rule applies to all stops in that station." - - name: "to_stop_id" - required: true - inputType: GTFS_STOP - columnWidth: 12 - helpContent: "The to_stop_id field contains a stop ID that identifies a stop or station where a connection between routes ends. Stop IDs are referenced from the stops.txt file. If the stop ID refers to a station that contains multiple stops, this transfer rule applies to all stops in that station." - - name: "transfer_type" - required: true - inputType: DROPDOWN - options: - - value: '0' - - value: '1' - - value: '2' - - value: '3' - columnWidth: 6 - helpContent: "The transfer_type field specifies the type of connection for the specified (from_stop_id, to_stop_id) pair. Valid values for this field are:" - - name: "min_transfer_time" - required: false - inputType: POSITIVE_INT - columnWidth: 6 - helpContent: "When a connection between routes requires an amount of time between arrival and departure (transfer_type=2), the min_transfer_time field defines the amount of time that must be available in an itinerary to permit a transfer between routes at these stops. The min_transfer_time must be sufficient to permit a typical rider to move between the two stops, including buffer time to allow for schedule variance on each route." - + - name: "from_stop_id" + required: true + inputType: GTFS_STOP + columnWidth: 12 + helpContent: "The from_stop_id field contains a stop ID that identifies a stop or station where a connection between routes begins. Stop IDs are referenced from the stops.txt file. If the stop ID refers to a station that contains multiple stops, this transfer rule applies to all stops in that station." + - name: "to_stop_id" + required: true + inputType: GTFS_STOP + columnWidth: 12 + helpContent: "The to_stop_id field contains a stop ID that identifies a stop or station where a connection between routes ends. Stop IDs are referenced from the stops.txt file. If the stop ID refers to a station that contains multiple stops, this transfer rule applies to all stops in that station." + - name: "transfer_type" + required: true + inputType: DROPDOWN + options: + - value: "0" + - value: "1" + - value: "2" + - value: "3" + columnWidth: 6 + helpContent: "The transfer_type field specifies the type of connection for the specified (from_stop_id, to_stop_id) pair. Valid values for this field are:" + - name: "min_transfer_time" + required: false + inputType: POSITIVE_INT + columnWidth: 6 + helpContent: "When a connection between routes requires an amount of time between arrival and departure (transfer_type=2), the min_transfer_time field defines the amount of time that must be available in an itinerary to permit a transfer between routes at these stops. The min_transfer_time must be sufficient to permit a typical rider to move between the two stops, including buffer time to allow for schedule variance on each route." - id: scheduleexception name: (none) datatools: true helpContent: Conveyal-specific table for classifying schedule exceptions. fields: - - name: name - required: true - - name: dates - required: true - inputType: EXCEPTION_DATE - - name: exemplar + - name: name + required: true + - name: dates + required: true + inputType: EXCEPTION_DATE + - name: exemplar + required: true + - name: custom_schedule + required: false + - name: removed_service + required: false + - name: added_service + required: false + +# FIXME: helpContent is lifted from https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md +- id: bookingrule + name: booking_rules.txt + helpContent: Booking information for rider-requested services using GTFS-FlexibleTrips, such as how far in advance booking should occur or a phone number that should be called. + fields: + - name: booking_rule_id + required: true + inputType: GTFS_ID + helpContent: ID which identifies the booking rule + columnWidth: 12 + - name: booking_type + required: true + inputType: DROPDOWN + helpContent: Indicates how far in advance booking can be made + columnWidth: 12 + bulkEditEnabled: true + options: + - value: "0" + text: Real time booking + - value: "1" + text: Up to same-day booking with advance notice + - value: "2" + text: Up to prior day(s) booking + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_duration_min + inputType: HOUR_MINUTE_INPUT + helpContent: Minimum number of minutes before travel to make the request + bulkEditEnabled: true + columnWidth: 6 + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_duration_max + inputType: HOUR_MINUTE_INPUT + helpContent: Maximum number of minutes before travel to make the request + bulkEditEnabled: true + columnWidth: 6 + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_last_time + inputType: TIME + helpContent: Last time on the last day before travel to make the booking request + bulkEditEnabled: true + columnWidth: 6 + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_start_time + inputType: TIME + helpContent: Earliest time on the earliest day before travel to make the booking request + bulkEditEnabled: true + columnWidth: 6 + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_start_day + inputType: POSITIVE_NUM + helpContent: Earliest day before travel to make the booking request + columnWidth: 6 + bulkEditEnabled: true + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_last_day + inputType: POSITIVE_NUM + helpContent: Earliest day before travel to make the booking request + columnWidth: 6 + bulkEditEnabled: true + # TODO: enable validation to match spec (only appear when appropriate) + - name: prior_notice_service_id + inputType: GTFS_SERVICE_DROPDOWN + helpContent: Indicates the service days on which prior_notice_last_day or prior_notice_start_day are counted. + columnWidth: 12 + bulkEditEnabled: true + - name: message + inputType: TEXT + helpContent: Message to riders utilizing service at a stop_time when booking on-demand pickup and drop off. Meant to provide minimal information to be transmitted within a user interface about the action a rider must take in order to utilize the service + bulkEditEnabled: true + columnWidth: 12 + - name: pickup_message + inputType: TEXT + helpContent: Functions in the same way as message but used when riders have on-demand pickup only + bulkEditEnabled: true + columnWidth: 12 + - name: drop_off_message + inputType: TEXT + helpContent: Functions in the same way as message but used when riders have on-demand drop off only + columnWidth: 12 + - name: phone_number + inputType: TEXT # TODO: VALIDATION + helpContent: Phone number to call to make the booking request + columnWidth: 12 + - name: info_url + inputType: URL + helpContent: URL providing information about the booking rule + columnWidth: 12 + - name: booking_url + inputType: URL + helpContent: URL to an online interface or app where the booking request can be made + columnWidth: 12 + + # FIXME: helpContent is lifted from https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md +- id: location + name: locations.geojson + flex: true + helpContent: Adds features that indicate areas where riders can request either pickup or drop off. + fields: + # TODO: enable validation to match spec (only appear when appropriate) + - name: "location_id" required: true - - name: custom_schedule + inputType: GTFS_ID + columnWidth: 6 + helpContent: "The location_id contains an ID that identifies a location where riders can request either pickup or drop off." + # TODO: enable validation to match spec (only appear when appropriate) + - name: "stop_name" required: false - - name: removed_service + inputType: TEXT + columnWidth: 12 + helpContent: "The stop_name field contains the name of the location as displayed to riders." + - name: "stop_desc" required: false - - name: added_service + inputType: TEXT + columnWidth: 12 + helpContent: "The stop_desc field contains a meaningful description of the location to help orient riders." + # TODO: enable validation to match spec (only appear when appropriate) + - name: "zone_id" + required: false + inputType: GTFS_ZONE + columnWidth: 12 + helpContent: "The zone_id field defines the fare zone for a location ID. Zone IDs are required if you want to provide fare information using fare_rules.txt." + # TODO: enable validation to match spec (only appear when appropriate) + - name: "stop_url" + required: false + inputType: URL + columnWidth: 12 + helpContent: "The stop_url field contains the URL of a web page about a particular location. This should be different from the agency_url and the route_url fields." + + # FIXME: helpContent is lifted from https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md +- id: locationgroupstop + flex: true + name: location_group_stops.txt + fields: + - name: "location_group_id" + required: true + inputType: TEXT + - name: "stop_id" + required: true + inputType: GTFS_ID +- id: locationgroup + flex: true + name: location_groups.txt + helpContent: Adds location groups. Location groups are groups of stops and GeoJSON locations, which allow predetermined groups of these features to be specified on individual rows of stop_times.txt. + fields: + # TODO: enable validation to match spec (only appear when appropriate) + - name: "location_group_id" + required: true + inputType: TEXT + columnWidth: 12 + helpContent: "Identifies a location group. A location group is a group of stops or GeoJSON locations that together indicate locations where a rider may request pickup or drop off. " + - name: "location_group_name" required: false + inputType: TEXT + columnWidth: 12 + helpContent: "Name of the location group. Must be defined either once, or exhaustively for a single location_group_id." + extraFields: + - name: "stop_id" + required: false + inputType: GTFS_STOP_OR_LOCATION_LIST + columnWidth: 12 + helpContent: "Identifies a stop or location belonging to the location group." + activeComponentOverride: "locationgroupstop" + # Fares v2 @@ -1286,4 +1475,4 @@ - name: to_stop_id required: false inputType: GTFS_STOP - columnWidth: 12 \ No newline at end of file + columnWidth: 12 diff --git a/i18n/english.yml b/i18n/english.yml index c0fd7343a..6c14de0f6 100644 --- a/i18n/english.yml +++ b/i18n/english.yml @@ -52,6 +52,13 @@ components: routeCount: 'Latest Version: Number of Routes' sortBy: Sort By url: 'Feed download URL' + BookingRuleValidation: + forbiddenForBookingType: Forbidden for booking_type %bookingType% + forbiddenForBookingTypeIfDefined: Forbidden for booking_type %bookingType% if %field% is defined. + forbiddenForBookingTypeOtherThan: Forbidden for booking_types other than %bookingType% + forbiddenIfFieldNotDefined: Forbidden if %field% is not defined + requiredForBookingType: Required for booking_type %bookingType% + requiredIfFieldIsDefined: Required if %field% is defined Brand: dataTools: Data Tools Breadcrumbs: @@ -268,6 +275,8 @@ components: daysOfService: Days of service dropImage: Drop %activeComponent% image here, or click to select image to upload. optional: (optional) + realtimeBookingTypeErrorToast: ⓘ Input Error! You must clear all prior notice fields before changing booking_type to realtime! + realtimeBookingTypeErrorTooltip: Prior notice information must be cleared before setting type to realtime selectAgency: Select agency... selectDate: Please select a date selectOption: -- select an option -- @@ -322,6 +331,7 @@ components: bundleFilename: Bundle Filename dateFormat: MMM D, YYYY deployable: Deployable + flex: Flexible service edit: Edit lastUpdatedDate: Last updated %date% noUpdates: No updates @@ -662,6 +672,10 @@ components: updates: autoFetchFeeds: Auto fetch feed sources? title: Updates + flex: + title: Flex Service + checkbox: Feed includes flex service + description: Enable this feed source to include flex service. Checking this checkbox will enable flex tools in the editor. Flex service describes operations that follow a schedule, but also include flexible features such as dial-a-ride service, route deviation, continuous stops, or point to zone service. labels: title: Labels bundleFilename: Override filename for GTFS bundle @@ -705,6 +719,8 @@ components: stop: title: Edit stops label: Stops + GtfsServiceSelect: + placeholder: Select existing service ID GtfsValidationExplorer: accessibilityValidation: Accessibility Explorer table: @@ -726,16 +742,9 @@ components: trips: Trip issues noResults: No validation results to show. tips: - DATE_NO_SERVICE: "If the transit service does not operate on weekends, - some or all of these validation issue may be ignored. Similarly, holidays - for which there is no transit service running may appear in this list." - FEED_TRAVEL_TIMES_ROUNDED: "This is a common feature of GTFS feeds that do - not use down-to-the-second precision for arrival/departure times. However, - if this precision is expected, there may be an issue occurring during feed - export." - MISSING_TABLE: "Missing a required table is a major issue that must be - resolved before most GTFS consumers can make use of the data for trip - planning or in other applications." + DATE_NO_SERVICE: "If the transit service does not operate on weekends, some or all of these validation issue may be ignored. Similarly, holidays for which there is no transit service running may appear in this list." + FEED_TRAVEL_TIMES_ROUNDED: "This is a common feature of GTFS feeds that do not use down-to-the-second precision for arrival/departure times. However, if this precision is expected, there may be an issue occurring during feed export." + MISSING_TABLE: "Missing a required table is a major issue that must be resolved before most GTFS consumers can make use of the data for trip planning or in other applications." title: Validation issues HomeProjectDropdown: new: New Project @@ -824,9 +833,7 @@ components: This feature ignores all travel/dwell times except for timepoints! usageNotes: " Usage notes" NormalizeStopTimesTip: - info: "Tip: when changing travel times, consider - using the 'Normalize stop times' button above to automatically update - all stop times to the updated travel time." + info: "Tip: when changing travel times, consider using the 'Normalize stop times' button above to automatically update all stop times to the updated travel time." NoteForm: adminOnly: 'Admin only?' new: Post @@ -868,8 +875,13 @@ components: PatternStopCard: PatternStopContents: defaultDwellTime: Default dwell time + defaultTimeInLocation: Default time in location defaultTravelTime: Default travel time firstStopTravelTime: Travel time for first stop must be zero + meanDurationFactor: Mean Duration Factor + meanDurationOffset: Mean Duration Offset + safeDurationFactor: Safe Duration Factor + safeDurationOffset: Safe Duration Offset stopHeadsignPlaceholder: Outbound stopHeadsignText: Stop headsign stopHeadsignTitle: Headsign that overrides trip headsign between stops. @@ -1455,6 +1467,7 @@ components: mustBePositiveInteger: Field must be a positive integer. mustBePositiveNumber: Field must be a positive number. mustBeValidNumber: Field must be a valid number. + mustBeValidTime: Field must be a valid time. nameAlreadyUsed: "%name% is already used in another exception," serviceRequired: Calendar must have service for at least one day. stopNameRequired: Stop name is required for stop, station, and entrance location types. diff --git a/i18n/german.yml b/i18n/german.yml index 6e024b42d..8624f4a20 100644 --- a/i18n/german.yml +++ b/i18n/german.yml @@ -52,6 +52,13 @@ components: url: Feed-Abruf-URL userLogs: Nutzer-Authentisierungs-Logs viewUserLogs: Nutzenden-Logs auf Auth0.com ansehen + BookingRuleValidation: + forbiddenForBookingType: Forbidden for booking_type %bookingType% + forbiddenForBookingTypeIfDefined: Forbidden for booking_type %bookingType% if %field% is defined. + forbiddenForBookingTypeOtherThan: Forbidden for booking_types other than %bookingType% + forbiddenIfFieldNotDefined: Forbidden if %field% is not defined + requiredForBookingType: Required for booking_type %bookingType% + requiredIfFieldIsDefined: Required if %field% is defined Brand: dataTools: Data Tools Breadcrumbs: @@ -279,6 +286,8 @@ components: dropImage: Ziehen Sie ein %activeComponent%-Bild hier hin, oder klicken Sie um ein Bild hochzuladen. optional: (optional) + realtimeBookingTypeErrorToast: ⓘ Input Error! You must clear all prior notice fields before changing booking_type to realtime! + realtimeBookingTypeErrorTooltip: Prior notice information must be cleared before setting type to realtime selectAgency: Betreiber auswählen... selectDate: Bitte wählen Sie ein Datum selectOption: -- Option auswählen -- @@ -340,6 +349,7 @@ components: MINUTES: Minuten fetchFeedEvery: Feed-Abruf alle FeedInfo: + flex: Flexible service autoFetch: Automatischer Abruf autoPublish: Automatische Veröffentlichung bundleFilename: Bundle Filename @@ -605,6 +615,10 @@ components: title: Real-time Updater walkSpeed: Geh-Geschwindigkeit GeneralSettings: + flex: + title: Flexible Routen + checkbox: Feed-Quelle enthält flexible Routen. + description: Erlauben Sie dieser Feed-Quelle, den Flex-Service einzuschließen. Wenn Sie dieses Kontrollkästchen markieren, werden die Flex-Tools im Editor aktiviert. Flex-Service beschreibt Routen, die einem Fahrplan folgen, aber auch flexible Routen wie Dial-a-Ride-Service, Routenabweichung, kontinuierliche Haltestellen oder Punkt-zu-Zone-Service. autoFetch: checkbox: Feed-Quelle automatisch abrufen hint: Aktiviert den automatischen Feed-Abruf. (Feed-Quelle-URL must angegeben @@ -723,6 +737,8 @@ components: stop: label: Haltestellen title: Haltestellen bearbeiten + GtfsServiceSelect: + placeholder: Select existing service ID GtfsValidationExplorer: accessibilityValidation: Erreichbarkeits-Explorer table: @@ -1462,6 +1478,7 @@ components: mustBePositiveInteger: Field must be a positive integer. mustBePositiveNumber: Field must be a positive number. mustBeValidNumber: Field must be a valid number. + mustBeValidTime: Feld muss eine gültige Zeit beinhalten. nameAlreadyUsed: "%name% is already used in another exception." serviceRequired: Calendar must have service for at least one day. stopNameRequired: Stop name is required for stop, station, and entrance location types. diff --git a/i18n/polish.yml b/i18n/polish.yml index 0b14e2d03..c5bca2bc2 100644 --- a/i18n/polish.yml +++ b/i18n/polish.yml @@ -342,6 +342,7 @@ components: bundleFilename: Bundle Filename dateFormat: MMM D, YYYY deployable: Rozmieszczany + flex: Flexible service edit: Edytować lastUpdatedDate: Last updated %date% noUpdates: No updates @@ -599,6 +600,10 @@ components: title: Real-time updaters walkSpeed: Walk Speed GeneralSettings: + flex: + title: Flex Service + checkbox: Feed includes flex service + description: Enable this feed source to include flex service. Checking this checkbox will enable flex tools in the editor. Flex service describes operations that follow a schedule, but also include flexible features such as dial-a-ride service, route deviation, continuous stops, or point to zone service. autoFetch: checkbox: Auto fetch feed source hint: Set this feed source to fetch automatically. (Feed source URL must be @@ -715,6 +720,8 @@ components: stop: label: Stops title: Edit stops + GtfsServiceSelect: + placeholder: Select existing service ID GtfsValidationExplorer: accessibilityValidation: Accessibility Explorer table: @@ -1441,6 +1448,7 @@ components: mustBePositiveInteger: Field must be a positive integer. mustBePositiveNumber: Field must be a positive number. mustBeValidNumber: Field must be a valid number. + mustBeValidTime: Field must be a valid time. nameAlreadyUsed: "%name% is already used in another exception." serviceRequired: Calendar must have service for at least one day. stopNameRequired: Stop name is required for stop, station, and entrance location types. diff --git a/lib/common/components/GtfsEntitySelect.js b/lib/common/components/GtfsEntitySelect.js new file mode 100644 index 000000000..6163c74e0 --- /dev/null +++ b/lib/common/components/GtfsEntitySelect.js @@ -0,0 +1,54 @@ +// @flow + +import React, {Component} from 'react' +import { shallowEqual } from 'react-pure-render' +import Select from 'react-select' + +import type { ReactSelectOption } from '../../types' + +type Props = { + clearable?: boolean, + onChange: ReactSelectOption => void, + options: Array, + placeholder: string, + refString: string, + value: ?string +} + +type State = {value: ?(string | ReactSelectOption)} + +export default class GtfsEntitySelect extends Component { + componentWillMount () { + this.setState({ + value: this.props.value + }) + } + + componentWillReceiveProps (nextProps: Props) { + if (!shallowEqual(nextProps.value, this.props.value)) { + this.setState({value: nextProps.value}) + } + } + + onChange = (value: ReactSelectOption) => { + const {onChange} = this.props + this.setState({value}) + onChange && onChange(value) + } + + render () { + const {clearable, options, placeholder, refString} = this.props + return ( + + placeholder={this.messages('placeholder')} + refString='tzSelect' + {...this.props} + /> ) } } diff --git a/lib/common/util/config.js b/lib/common/util/config.js index 1f54c19db..93390e73f 100644 --- a/lib/common/util/config.js +++ b/lib/common/util/config.js @@ -36,6 +36,14 @@ export function getGtfsSpec (): Array { if (!GTFS_SPEC) throw new Error('GTFS yml configuration file is not defined!') return GTFS_SPEC } +export function getGtfsSpecField (tableId: string, fieldName: string): any { + const specTable = getGtfsSpec().find(t => t.id === tableId) + if (!specTable) { + throw new Error("Tried to access field of table that doesn't exist!") + } + + return specTable.fields.find(field => field.name === fieldName) +} export function getGtfsPlusSpec (): Array { const CONFIG: DataToolsConfig = window.DT_CONFIG diff --git a/lib/editor/actions/active.js b/lib/editor/actions/active.js index cb67edfd4..ed2437653 100644 --- a/lib/editor/actions/active.js +++ b/lib/editor/actions/active.js @@ -11,12 +11,13 @@ import {fetchGTFSEntities} from '../../manager/actions/versions' import { getEditorNamespace, getTableById, + getTripPatterns, isValidComponent, subComponentList, subSubComponentList } from '../util/gtfs' import {getMapFromGtfsStrategy, entityIsNew} from '../util/objects' -import type {Entity, Feed} from '../../types' +import type {Entity, Feed, GeometryType, LocationShape} from '../../types' import type {dispatchFn, getStateFn, AppState} from '../../types/reducers' import {getEntityGraphQLRoot} from '../../gtfs/util' @@ -29,9 +30,10 @@ import { removeEditorLock } from './editor' import {saveTripPattern} from './tripPattern' +import { saveLocation, saveLocationGroup } from './location' export const clearGtfsContent = createVoidPayloadAction('CLEAR_GTFSEDITOR_CONTENT') -const receivedNewEntity = createAction( +export const receivedNewEntity = createAction( 'RECEIVE_NEW_ENTITY', (payload: { component: string, @@ -76,7 +78,13 @@ export const updateEditSetting = createAction( value: any }) => payload ) - +export const updatingLocationShape = createAction( + 'UPDATING_LOCATION_SHAPE', + (payload: { + geometryType: GeometryType, + shapes: LocationShape + }) => payload +) export type EditorActiveActions = ActionType | ActionType | ActionType | @@ -145,9 +153,10 @@ function getActiveEntity ( } function getActiveSubEntity (activeEntity: ?Entity, subEntityId: ?number): any { - return activeEntity && activeEntity.tripPatterns && Array.isArray(activeEntity.tripPatterns) - ? clone(activeEntity.tripPatterns.find(p => p.id === subEntityId)) - : null + if (activeEntity) { + return clone(getTripPatterns(activeEntity).find(p => p.id === subEntityId)) + } + return null } function updateUrl ( @@ -349,7 +358,7 @@ export function saveActiveGtfsEntity ( return function (dispatch: dispatchFn, getState: getStateFn) { const {active} = getState().editor.data let entity = optionalEntity || active.entity - const feedId = (entity && entity.feedId) || active.feedSourceId + const feedId = entity && typeof entity.feedId === 'string' ? entity.feedId : active.feedSourceId switch (component) { // FIXME case 'trippattern': @@ -362,6 +371,10 @@ export function saveActiveGtfsEntity ( } // cast entity to make flow happy return dispatch(saveTripPattern(feedId, (entity: any))) + case 'location': + return dispatch(saveLocation(feedId, (entity: any), refetch)) + case 'locationgroup': + return dispatch(saveLocationGroup(feedId, (entity: any), refetch)) default: // Default method for agencies, stops, routes, fares, calendars. // Trip patterns and feed info are handled above. Trips are handled in @@ -394,8 +407,8 @@ export function saveEntity ( // Add default vals for component const defaults = {} if (component === 'route') { - defaults.continuous_pickup = 1 // Default value for no continuous pickup - defaults.continuous_drop_off = 1 // Default value for no continuous drop off + defaults.continuous_pickup = null // Default value for no continuous pickup + defaults.continuous_drop_off = null // Default value for no continuous drop off } else if (component === 'feedinfo') { defaults.default_lang = '' defaults.feed_contact_url = '' @@ -410,6 +423,15 @@ export function saveEntity ( const url = `/api/editor/secure/${route}${idParam}?feedId=${feedId}&sessionId=${sessionId || ''}` const mappingStrategy = getMapFromGtfsStrategy(component) const data = mappingStrategy(entityWithDefaults) + + if (data.location_group_id && data.location_id) { + data.location_id = data.location_id.join(',') + if (data.is_creating) { + delete data.is_creating + delete data.id + } + } + return dispatch(secureFetch(url, method, data)) .then(res => res.json()) .then(savedEntity => { @@ -457,7 +479,9 @@ export function deleteGtfsEntity ( const {sessionId} = getState().editor.data.lock const entityPath = component === 'trippattern' ? 'pattern' - : component === 'fare' ? 'fareattribute' : component + : component === 'fare' + ? 'fareattribute' + : component if (!sessionId) { console.warn('No sessionId defined, unable to delete!') return diff --git a/lib/editor/actions/editor.js b/lib/editor/actions/editor.js index 1b50c87ca..5948fde98 100644 --- a/lib/editor/actions/editor.js +++ b/lib/editor/actions/editor.js @@ -71,7 +71,7 @@ function getCloneProps (entityId: number, component: string, state: AppState) { } const pattern = active.entity.tripPatterns.find(tp => tp.id === entityId) const newPatternId = generateUID() - const newShapeId = generateUID() + const newShapeId = pattern.patternStops.length > 0 ? generateUID() : null return { ...pattern, // Overwrite existing name 'X' with 'X copy' @@ -87,6 +87,7 @@ function getCloneProps (entityId: number, component: string, state: AppState) { // FIXME: Cloning a route should also clone its patterns. // See https://github.com/catalogueglobal/datatools-ui/issues/153 delete route.tripPatterns + delete route.tripLocations return route case 'scheduleexception': return { @@ -479,6 +480,20 @@ export function fetchBaseGtfs ({ count } } + booking_rules (limit: -1) { + id + booking_rule_id + booking_type + } + location_groups { + id + location_group_id + location_group_name + } + location_group_stops { + location_group_id + stop_id + } feed_info { id feed_id @@ -513,6 +528,57 @@ export function fetchBaseGtfs ({ id fare_id } + locations (limit: -1) { + id + geometry_type + location_id + stop_desc + stop_name + stop_url + zone_id + location_shapes (limit: -1) { + id + location_id + geometry_id + geometry_pt_lat + geometry_pt_lon + } + } + fare_product (limit: -1) { + id + fare_product_id + fare_media_id + } + fare_media (limit: -1) { + id + fare_media_id + fare_media_name + } + fare_transfer_rule (limit: -1) { + id + from_leg_group_id + to_leg_group_id + fare_product_id + } + fare_leg_rule (limit: -1) { + id + fare_product_id + leg_group_id + } + fare_leg_join_rule (limit: -1) { + id + from_network_id + to_network_id + } + network (limit: -1) { + id + network_id + network_name + } + rider_category (limit: -1) { + id + rider_category_id + } ${getFaresV2Query('fareproduct', 'fare_product')} ${getFaresV2Query('faremedia', 'fare_media')} ${getFaresV2Query('faretransferrule', 'fare_transfer_rule')} diff --git a/lib/editor/actions/location.js b/lib/editor/actions/location.js new file mode 100644 index 000000000..de4abb8d8 --- /dev/null +++ b/lib/editor/actions/location.js @@ -0,0 +1,119 @@ +// @flow +import {secureFetch} from '../../common/actions' +import {getMapFromGtfsStrategy, entityIsNew} from '../util/objects' +import { getEditorNamespace } from '../util/gtfs' +import {fetchGTFSEntities} from '../../manager/actions/versions' +import type {dispatchFn, getStateFn} from '../../types/reducers' +import type {GtfsLocation} from '../../types' + +import { receivedNewEntity, savedGtfsEntity } from './active' + +// FLEX TODO: The two big dispatch blocks are almost the same... Would it be possible to extract them (such as saveEntity('locationgroup', locationGroupUrl) and same for locationgroupstop)? +export function saveLocationGroup ( + feedId: ?string, + locationGroup: GtfsLocation, + refetch: ?boolean = true +) { + return function (dispatch: dispatchFn, getState: getStateFn) { + if (!feedId || !locationGroup) { + return + } + // dispatch(savingActiveLocation()) //Update this? + + const notNew = !entityIsNew(locationGroup) // Checks if id is -2 or undefined + const method = notNew ? 'put' : 'post' + const idParam = notNew ? `/${locationGroup.id || ''}` : '' + const {sessionId} = getState().editor.data.lock + + const mappingStrategy = getMapFromGtfsStrategy('locationGroup') + const data = mappingStrategy(locationGroup) + + const locationGroupUrl = `/api/editor/secure/locationgroup${idParam}?feedId=${feedId}&sessionId=${sessionId || ''}` + const locationGroupStopsUrl = `/api/editor/secure/locationgroupstop${idParam}?feedId=${feedId}&sessionId=${sessionId || ''}` + + dispatch(secureFetch(locationGroupStopsUrl, method, data)) + .then(res => res.json()) + .then(savedEntity => { + dispatch(savedGtfsEntity()) + const namespace = getEditorNamespace(feedId, getState()) + // Refetch entity and replace in store + if (refetch) { + dispatch(fetchGTFSEntities({ + editor: true, + id: savedEntity.id, + namespace, + replaceNew: !notNew, + type: 'locationgroupstop' + })) + } else { + // Push new entity into store. + dispatch(receivedNewEntity({component: 'locationgroupstop', entity: savedEntity})) + } + }) + return dispatch(secureFetch(locationGroupUrl, method, data)) + .then(res => res.json()) + .then(savedEntity => { + dispatch(savedGtfsEntity()) + const namespace = getEditorNamespace(feedId, getState()) + // Refetch entity and replace in store + if (refetch) { + dispatch(fetchGTFSEntities({ + editor: true, + id: savedEntity.id, + namespace, + replaceNew: !notNew, + type: 'locationgroup' + })) + } else { + // Push new entity into store. + dispatch(receivedNewEntity({component: 'locationgroup', entity: savedEntity})) + Promise.resolve(savedEntity) + } + }) + } +} + +export function saveLocation ( + feedId: ?string, + location: GtfsLocation, + refetch: ?boolean = true +) { + return function (dispatch: dispatchFn, getState: getStateFn) { + // TODO: Handle this w/in the regular saveEntity fn? + if (!feedId || !location) { + return + } + // dispatch(savingActiveLocation()) //Update this? + + const notNew = !entityIsNew(location) // Checks if id is -2 or undefined + const method = notNew ? 'put' : 'post' + const idParam = notNew ? `/${location.id || ''}` : '' + const {sessionId} = getState().editor.data.lock + + const mappingStrategy = getMapFromGtfsStrategy('location') + const data = mappingStrategy(location) + + const locationUrl = `/api/editor/secure/location${idParam}?feedId=${feedId}&sessionId=${sessionId || ''}` + + return dispatch(secureFetch(locationUrl, method, data)) + .then(res => res.json()) + .then(savedEntity => { + dispatch(savedGtfsEntity()) + const namespace = getEditorNamespace(feedId, getState()) + // Refetch entity and replace in store + if (refetch) { + dispatch(fetchGTFSEntities({ + namespace, + id: savedEntity.id, + type: 'location', + editor: true, + replaceNew: !notNew + })) + } else { + // Push new entity into store. + dispatch(receivedNewEntity({component: 'location', entity: savedEntity})) + Promise.resolve(savedEntity) + } + }) + } +} diff --git a/lib/editor/actions/map/stopStrategies.js b/lib/editor/actions/map/stopStrategies.js index 08da8dce1..cf27e08d9 100644 --- a/lib/editor/actions/map/stopStrategies.js +++ b/lib/editor/actions/map/stopStrategies.js @@ -8,6 +8,9 @@ import lineSliceAlong from '@turf/line-slice-along' import lineSlice from 'turf-line-slice' import lineString from 'turf-linestring' import point from 'turf-point' +import turfPolygon from 'turf-polygon' +// $FlowFixMe flow doesn't understand new packages +import centerOfMass from '@turf/center-of-mass' import {updateActiveGtfsEntity, saveActiveGtfsEntity} from '../active' import {updatePatternStops} from '../tripPattern' @@ -31,7 +34,7 @@ import {stopToGeoJSONPoint, stopToPoint, constructPoint } from '../../util/map' -import type {ControlPoint, Coordinates, GtfsStop, LatLng, Pattern, PatternStop, StopControlPoint} from '../../../types' +import type {ControlPoint, Coordinates, GtfsLocation, GtfsStop, LatLng, Pattern, PatternStop, StopControlPoint} from '../../../types' import type {dispatchFn, getStateFn} from '../../../types/reducers' /** @@ -205,7 +208,9 @@ export function addStopAtInterval (latlng: LatLng, activePattern: Pattern, contr const stopControlPoint = result.controlPoints[controlPoints.length + index] const patternStop = stopToPatternStop(s) // Set pattern stop's shape dist traveled. - patternStop.shapeDistTraveled = stopControlPoint.distance + if (patternStop.stopId !== null || patternStop.locationId !== null || patternStop.locationGroupId !== null) { + patternStop.shapeDistTraveled = stopControlPoint.distance + } patternStops.push(patternStop) // Update stop properties on new control points. stopControlPoint.pointType = POINT_TYPE.STOP @@ -226,7 +231,8 @@ export function addStopAtInterval (latlng: LatLng, activePattern: Pattern, contr } } -export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: ?number) { +export function addStopToPattern (pattern: Pattern, origStop: GtfsStop | GtfsLocation, index?: ?number) { + // eslint-disable-next-line complexity return async function (dispatch: dispatchFn, getState: getStateFn) { const {data, editSettings} = getState().editor const {avoidMotorways, followStreets, followRail} = editSettings.present @@ -234,22 +240,48 @@ export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: ?num const patternStops = clone(currentPatternStops) const {controlPoints, patternSegments} = getControlPoints(getState()) const hasShapePoints = shapePoints && shapePoints.length > 1 + + // $FlowFixMe this is a bit of a hack to get locations to work with methods built for stops. We do the work required to have all methods work. + const stop: GtfsStop = clone(origStop) + + if (stop.stop_id === undefined || stop.stop_id === null) { + // convert stop to turf polygon + const polygon = stop.location_shapes.map(ls => [ls.geometry_pt_lat, ls.geometry_pt_lon]) + + // find center to simulate stop + const center = centerOfMass(turfPolygon([polygon])) + const {coordinates} = center.geometry ? center.geometry : {coordinates: false} + if (coordinates) { + stop.stop_lon = coordinates[1] + stop.stop_lat = coordinates[0] + } + } + + const {stop_lon: lng, stop_lat: lat} = stop + const newStop = stopToPatternStop( stop, (typeof index === 'undefined' || index === null) ? patternStops.length : index ) - if (typeof index === 'undefined' || index === null || index === patternStops.length) { - // Push pattern stop to cloned list. + + if (index === undefined) { patternStops.push(newStop) + } + pattern.shapeId = generateUID() + + // FLEX TODO: is this stuff all messed up by moving away from pattern halts? + + if (typeof index === 'undefined' || index === null || index === patternStops.length) { if (hasShapePoints) { + // Push pattern stop to cloned list. // console.log('extending pattern to new stop', stop) // If a pattern shape already exists, extend it from the current end // point to the new stop. const {shapePtLon, shapePtLat} = shapePoints[shapePoints.length - 1] const currentEndPoint = ll.toLeaflet([shapePtLon, shapePtLat]) - const {stop_lon: lng, stop_lat: lat} = stop + // Extend pattern to the new point. return dispatch(extendPatternToPoint(pattern, currentEndPoint, {lng, lat}, stop)) .then(result => { @@ -264,16 +296,20 @@ export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: ?num }) } else { dispatch(updatePatternStops(pattern, patternStops)) + // Only a stop should be checked, not a location // Otherwise, check if a shape ought to be created. Then, save. + // FLEX TODO: Is this working? Why 2? if (patternStops.length === 2 && (followStreets || followRail)) { // Create shape between stops the added stop is the second one and // followStreets is enabled. Otherwise, there is no need to create a // new shape because it would just be a straight line segment anyways. - const previousStopId = patternStops[patternStops.length - 2].stopId + const previousStopId = patternStops[patternStops.length - 2].stopId || patternStops[patternStops.length - 2].locationId || patternStops[patternStops.length - 2].locationGroupId const stops = getTableById(data.tables, 'stop') - const previousStop = stops.find(s => s.stop_id === previousStopId) + const locations = getTableById(data.tables, 'location') + const locationgroups = getTableById(data.tables, 'locationgroup') + const previousStop = stops.find(s => s.stop_id === previousStopId) || locations.find(s => s.location_id === previousStopId) || locationgroups.find(s => s.location_group_id === previousStopId) if (!previousStop) { - throw new Error(`Stop not found for stop_id ${previousStopId}.`) + throw new Error(`Stop not found for stop_id ${previousStopId || '(no previous stop id)'}.`) } const points = [previousStop, stop] .map((stop, index) => ({lng: stop.stop_lon, lat: stop.stop_lat})) @@ -285,10 +321,11 @@ export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: ?num // Finally, save the updated pattern. return dispatch(saveActiveGtfsEntity('trippattern')) } - } else if (index > 0) { + } else if (index && index > 0) { // If adding stop in middle, splice the stop into the array. patternStops.splice(index, 0, newStop) dispatch(updatePatternStops(pattern, patternStops)) + // Checking for stop_id ensures we are dealing with a stop and not a location if (hasShapePoints) { // Update shape if it exists. No need to update anything besides pattern // stops (which already occurred above) if there is no shape. NOTE: the @@ -337,6 +374,8 @@ export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: ?num insertPoint, { stopId: stop.stop_id, + locationGroupId: stop.location_group_id, + locationId: stop.location_id, pointType: POINT_TYPE.STOP // 2 }) // Instead of splicing at the previousStopControlPoint, splice immediately after. @@ -381,11 +420,14 @@ export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: ?num // Handle adding stop to beginning of pattern. patternStops.splice(0, 0, newStop) dispatch(updatePatternStops(pattern, patternStops)) + // Checking for stop_id ensures we don't process locations if (hasShapePoints) { // Update shape if coordinates already exist. const clonedControlPoints = clone(controlPoints) const controlPoint = newControlPoint(0, stopToPoint(stop), { stopId: stop.stop_id, + locationId: stop.location_id, + locationGroupId: stop.location_group_id, pointType: POINT_TYPE.STOP // 2 }) clonedControlPoints.splice(index, 0, controlPoint) @@ -508,13 +550,14 @@ function extendPatternToPoint (pattern, endPoint, newEndPoint, stop = null, spli } } -export function removeStopFromPattern (pattern: Pattern, stop: GtfsStop, index: number) { +export function removeStopFromPattern (pattern: Pattern, stop: GtfsLocation | GtfsStop, index: number) { return async function (dispatch: dispatchFn, getState: getStateFn) { const {controlPoints, patternSegments} = getControlPoints(getState()) const clonedControlPoints = clone(controlPoints) const clonedPatternSegments = clone(patternSegments) - const {shapePoints} = pattern - const patternStops = [...pattern.patternStops] + const {shapePoints, patternStops} = pattern + // $FlowFixMe Flow doesn't understand this property check + const stopIsStop = stop.hasOwnProperty('stopId') && stop.stopId !== null // Use control points to determine control point index for removed stop. // This is distinct from the index arg supplied because there may be more // control points than pattern stops, causing a mismatch in the indexes. @@ -522,13 +565,12 @@ export function removeStopFromPattern (pattern: Pattern, stop: GtfsStop, index: // TODO: refactor into shared function (see pattern-debug-lines.js) .map((cp, index) => ({...cp, cpIndex: index})) .filter(cp => cp.pointType === POINT_TYPE.STOP) - const {cpIndex} = stopControlPoints[index] - console.log(`deleting control point at index ${cpIndex}`) + const {cpIndex} = stopIsStop ? stopControlPoints[index] : {cpIndex: -1} + stopIsStop && console.log(`deleting control point at index ${cpIndex}`) if (!shapePoints || shapePoints.length === 0) { // If pattern has no shape points, don't attempt to refactor pattern shape console.log('pattern coordinates do not exist') - patternStops.splice(index, 1) - } else { + } else if (stopIsStop) { const {avoidMotorways, followStreets} = getState().editor.editSettings.present let result try { @@ -562,6 +604,8 @@ export function removeStopFromPattern (pattern: Pattern, stop: GtfsStop, index: patternSegments: result.coordinates })) } + // Update pattern stops (whether or not geometry exists) + patternStops.splice(index, 1) dispatch(updatePatternStops(pattern, patternStops)) dispatch(saveActiveGtfsEntity('trippattern')) } diff --git a/lib/editor/actions/trip.js b/lib/editor/actions/trip.js index 8576b463a..06dd9a2be 100644 --- a/lib/editor/actions/trip.js +++ b/lib/editor/actions/trip.js @@ -63,7 +63,7 @@ export const updateCellValue = createAction( (payload: { key: string, rowIndex: number, - value: ?(number | string | { stopId: string }) + value: ?(number | string | { stopId: ?string }) }) => payload ) @@ -120,14 +120,21 @@ export function fetchTripsForCalendar ( service_id stopTimes: stop_times (limit: -1) { stopId: stop_id + locationId: location_id + locationGroupId: location_group_id stopSequence: stop_sequence arrivalTime: arrival_time departureTime: departure_time + startPickupDropOffWindow: start_pickup_drop_off_window + endPickupDropOffWindow: end_pickup_drop_off_window stopHeadsign: stop_headsign shape_dist_traveled: shape_dist_traveled pickup_type drop_off_type timepoint + + pickupBookingRuleId: pickup_booking_rule_id + dropOffBookingRuleId: drop_off_booking_rule_id } } } diff --git a/lib/editor/actions/tripPattern.js b/lib/editor/actions/tripPattern.js index 3ef3507f2..f99cbb702 100644 --- a/lib/editor/actions/tripPattern.js +++ b/lib/editor/actions/tripPattern.js @@ -10,9 +10,9 @@ import {generateUID} from '../../common/util/util' import {shapes} from '../../gtfs/util/graphql' import {fetchGTFSEntities, receiveGTFSEntities} from '../../manager/actions/versions' import {getEditorNamespace} from '../util/gtfs' -import {resequenceStops, resequenceShapePoints} from '../util/map' +import {resequenceShapePoints, resequenceStops} from '../util/map' import {entityIsNew} from '../util/objects' -import type {ControlPoint, Pattern, PatternStop} from '../../types' +import type { ControlPoint, Pattern, PatternStop } from '../../types' import type {dispatchFn, getStateFn} from '../../types/reducers' import {fetchTripCounts} from './trip' @@ -80,12 +80,16 @@ export function updatePatternStops ( patternStops: Array ) { return function (dispatch: dispatchFn, getState: getStateFn) { - const sortedPatternStops = patternStops.map(resequenceStops) - dispatch(updateActiveGtfsEntity({ - component: 'trippattern', - entity: pattern, - props: {patternStops: sortedPatternStops} - })) + const sortedPatternStops = patternStops && patternStops.map(resequenceStops) + dispatch( + updateActiveGtfsEntity( + { + component: 'trippattern', + entity: pattern, + props: { patternStops: sortedPatternStops } + } + ) + ) } } @@ -144,6 +148,7 @@ export function fetchTripPatterns (feedId: string) { } export function saveTripPattern (feedId: ?string, tripPattern: Pattern) { + // eslint-disable-next-line complexity return function (dispatch: dispatchFn, getState: getStateFn) { if (!feedId) { console.warn('feedId not defined, unable to save trip pattern') @@ -193,7 +198,8 @@ export function saveTripPattern (feedId: ?string, tripPattern: Pattern) { // If trip pattern has no shape ID (e.g., if the pattern was imported // without shapes) but it does have shape points, generate a new shape ID // and assign shape points to the new ID. - const shapeId = generateUID() + // This should only happen if stops are present. A location-only pattern should have no shape id + const shapeId = tripPattern.patternStops.length > 0 ? generateUID() : null tripPattern.shapeId = shapeId tripPattern.shapePoints = tripPattern.shapePoints.map(sp => ({...sp, shapeId})) } diff --git a/lib/editor/components/BookingRulesForm.js b/lib/editor/components/BookingRulesForm.js new file mode 100644 index 000000000..63f7b9cd5 --- /dev/null +++ b/lib/editor/components/BookingRulesForm.js @@ -0,0 +1,42 @@ +// @flow + +import React from 'react' + +import type { Entity, GtfsSpecField } from '../../types' + +import EditorInput from './EditorInput' + +type Props = { + activeEntity: Entity, + approveGtfsDisabled: boolean, + fields: Array, + isNotValidMethod: (GtfsSpecField) => any, + serviceIds: Array +} + +const BookingRulesForm = (props: Props) => { + const {activeEntity, approveGtfsDisabled, fields, isNotValidMethod, serviceIds} = props + + return ( +
+ {fields.map((field, index) => ( + + ))} +
+ ) +} + +export default BookingRulesForm diff --git a/lib/editor/components/BulkEditorModal.js b/lib/editor/components/BulkEditorModal.js index 29494345e..1142cd628 100644 --- a/lib/editor/components/BulkEditorModal.js +++ b/lib/editor/components/BulkEditorModal.js @@ -6,15 +6,15 @@ import { Alert, Button, ControlLabel, FormControl, Modal } from 'react-bootstrap import * as editorActions from '../actions/editor' import * as snapshotActions from '../actions/snapshots' -import EditorInput from './EditorInput' import { getZones, getEditorTable } from '../util' import { getTableById } from '../util/gtfs' -import { validate } from '../util/validation' +import { validate } from '../util/validation/validation' import { formatTimestamp } from '../../common/util/date-time' - import type { Entity, Feed, GtfsSpecField } from '../../types' import type {EditorTables} from '../../types/reducers' +import EditorInput from './EditorInput' + const OPERATORS = { eq: '=', gte: '>=', diff --git a/lib/editor/components/EditorInput.js b/lib/editor/components/EditorInput.js index a845eff0f..af52b0a0c 100644 --- a/lib/editor/components/EditorInput.js +++ b/lib/editor/components/EditorInput.js @@ -11,15 +11,18 @@ import * as activeActions from '../actions/active' import * as editorActions from '../actions/editor' import {getEntityName, getTableById} from '../util/gtfs' import {FIELD_PROPS} from '../util/types' -import {doesNotExist} from '../util/validation' +import {doesNotExist} from '../util/validation/validation' +import GtfsServiceSelect from '../../common/components/GtfsServiceSelect' import TimezoneSelect from '../../common/components/TimezoneSelect' import LanguageSelect from '../../common/components/LanguageSelect' import {getComponentMessages} from '../../common/util/config' import toSentenceCase from '../../common/util/text' -import type {Entity, Feed, GtfsAgency, GtfsRoute, GtfsSpecField, GtfsStop} from '../../types' +import type {Entity, Feed, GtfsAgency, GtfsLocation, GtfsRoute, GtfsSpecField, GtfsStop} from '../../types' import type {EditorTables} from '../../types/reducers' import ColorField from './ColorField' +import HourMinuteInput from './HourMinuteInput' +import LocationGroupsEditor from './LocationGroupsEditor' import GenericEntitySelector from './EntitiySelector' import RouteTypeSelect from './RouteTypeSelect' import VirtualizedEntitySelect from './VirtualizedEntitySelect' @@ -33,11 +36,13 @@ type Props = { feedSource?: Feed, // Optional, but needed for uploadBrandingAsset. field: GtfsSpecField, isNotValid: boolean, + // locationGroupOptions: Array, onChange?: any => void, - tableData: EditorTables, + serviceIds?: ?Array, + tableData?: EditorTables, updateActiveGtfsEntity?: typeof activeActions.updateActiveGtfsEntity, uploadBrandingAsset?: typeof editorActions.uploadBrandingAsset, - zoneOptions: Array + zoneOptions?: Array } const agencyToOption = (agency: GtfsAgency) => ({ @@ -63,13 +68,14 @@ export default class EditorInput extends React.Component { */ _processFieldChange = (val: any) => { const { - activeComponent, activeEntity, field, onChange, updateActiveGtfsEntity } = this.props onChange && onChange(val) + const activeComponent = field.activeComponentOverride || this.props.activeComponent + updateActiveGtfsEntity && activeEntity && updateActiveGtfsEntity({ component: activeComponent, entity: activeEntity, @@ -93,6 +99,10 @@ export default class EditorInput extends React.Component { this._processFieldChange(evt.target.value) } + _onHourMinuteInputChange = (seconds: number) => { + this._processFieldChange(seconds / 60) // Maintain the state in minutes, since that is what the field type is. + } + _onRouteTypeChange = (currentNode: any) => { this._processFieldChange(currentNode.value) } @@ -110,17 +120,17 @@ export default class EditorInput extends React.Component { } } - /* eslint-disable complexity */ + // eslint-disable-next-line complexity render () { const { activeEntity, activeComponent, - tableData, - field, - currentValue, approveGtfsDisabled, - zoneOptions, - isNotValid + currentValue, + field, + isNotValid, + tableData, + zoneOptions } = this.props const editorField = field.displayName || field.name const formProps = {} @@ -179,6 +189,21 @@ export default class EditorInput extends React.Component { ) } + case 'GTFS_SERVICE_DROPDOWN': { + const { serviceIds } = this.props + if (!serviceIds) return null + return ( + + {basicLabel} + + + ) + } case 'URL': { // Ensure that an empty string is passed to value prop, so that on 'undo' // the input value will revert back to a blank field. @@ -213,10 +238,12 @@ export default class EditorInput extends React.Component { ) + return {elements} } - return {elements} + return elements } case 'GTFS_ZONE': + if (!zoneOptions) return null // Explicit check for zone options to make flow happy return ( {basicLabel} @@ -228,6 +255,18 @@ export default class EditorInput extends React.Component { /> ) + case 'GTFSFLEX_LOCATION_GROUP': + return ( + + {basicLabel} + {/* */} + + ) case 'TIMEZONE': return ( @@ -239,14 +278,27 @@ export default class EditorInput extends React.Component { /> ) + case 'HOUR_MINUTE_INPUT' : + return ( + + {basicLabel} + + + ) case 'LANGUAGE': return ( {basicLabel} + onChange={this._onSelectChange} + value={currentValue} + /> ) case 'DATE': @@ -390,6 +442,22 @@ export default class EditorInput extends React.Component { ) } + case 'GTFS_STOP_OR_LOCATION_LIST': { + const stops: Array = getTableById(tableData, 'stop') + const locations: Array = getTableById(tableData, 'location') + + return ( + + Locations + + + ) + } // TODO: fare_product and fare_media could likely be refactored to use the GenericEntitySelector component. case 'GTFS_FARE_PRODUCT': const fareProducts = getTableById(tableData, 'fareproduct').map(fareProduct => ({ diff --git a/lib/editor/components/EditorSidebar.js b/lib/editor/components/EditorSidebar.js index 9443f62df..86cdcb9a6 100644 --- a/lib/editor/components/EditorSidebar.js +++ b/lib/editor/components/EditorSidebar.js @@ -20,9 +20,10 @@ export default class EditorSidebar extends Component { messages = getComponentMessages('EditorSidebar') isActive (item: GtfsIcon, component: string) { - return component === item.id || - (component === 'scheduleexception' && item.id === 'calendar') || - (item.id === 'fareproduct' && isComponentFaresv2(component)) + return (component === item.id) || + (component === 'scheduleexception' && item.id === 'calendar') || + (component === 'location' && item.id === 'stop') || + (item.id === 'fareproduct' && isComponentFaresv2(component)) } render () { @@ -37,7 +38,8 @@ export default class EditorSidebar extends Component { link={feedSource ? `/feed/${feedSource.id}` : `/home`} ref='backNav' /> - {GTFS_ICONS.map(item => { + {/* FLEX TODO: what edge case is this for? Can the condition be simplified? */} + {GTFS_ICONS.filter(item => !item.flexOnly || (item.flexOnly && feedSource && feedSource.flexUIFeaturesEnabled)).map(item => { return item.hideSidebar ? null : { disabled={editingIsDisabled} icon={item.icon} key={item.id} - label={item.label} + // Hack?: Override label for stops if flex locations are included. + label={item.id === 'stop' && feedSource && feedSource.flexUIFeaturesEnabled ? 'Stops & Locations' : item.label} link={!feedSource ? '/home' : activeComponent === item.id diff --git a/lib/editor/components/EntityDetails.js b/lib/editor/components/EntityDetails.js index 52157b7d2..650eb49b4 100644 --- a/lib/editor/components/EntityDetails.js +++ b/lib/editor/components/EntityDetails.js @@ -8,22 +8,25 @@ import * as activeActions from '../actions/active' import * as editorActions from '../actions/editor' import * as mapActions from '../actions/map' import ActiveTripPatternList from '../containers/ActiveTripPatternList' -import EditorInput from './EditorInput' -import FareRulesForm from './FareRulesForm' -import EntityDetailsHeader from './EntityDetailsHeader' -import ScheduleExceptionForm from './ScheduleExceptionForm' import { getZones, getEditorTable, canApproveGtfs } from '../util' import { getTableById } from '../util/gtfs' - -import type {Entity, Feed, GtfsSpecField, GtfsStop, Pattern, Project} from '../../types' +import type {Entity, Feed, GtfsSpecField, GtfsStop, GtfsLocation, Pattern, Project} from '../../types' import type {EditorTables, ManagerUserState, MapState} from '../../types/reducers' -import type {EditorValidationIssue} from '../util/validation' +import type {EditorValidationIssue} from '../util/validation/common' + +import ScheduleExceptionForm from './ScheduleExceptionForm' +import EntityDetailsHeader from './EntityDetailsHeader' +import FareRulesForm from './FareRulesForm' +import EditorInput from './EditorInput' +import BookingRulesForm from './BookingRulesForm' type Props = { activeComponent: string, activeEntity: Entity, activeEntityId: number, activePattern: Pattern, + activePatternLocationGroups: Array, + activePatternLocations: Array, activePatternStops: Array, deleteEntity: typeof activeActions.deleteGtfsEntity, entities: Array, @@ -62,6 +65,7 @@ export default class EntityDetails extends Component { _toggleFareRules = (editFareRules: boolean) => this.setState({ editFareRules }) + // eslint-disable-next-line complexity render () { const { activeComponent, @@ -104,6 +108,8 @@ export default class EntityDetails extends Component { } const stop = ((activeEntity: any): GtfsStop) const {zones, zoneOptions} = getZones(getTableById(tableData, 'stop'), stop) + // TODO: Destructure locationGroups and pass? + // const {locationGroupOptions} = getLocationGroups(getTableById(tableData, 'stop'), stop) const currentTable = getEditorTable(activeComponent) if (!currentTable) { throw new Error(`No table found in GTFS spec for activeComponent: ${activeComponent}`) @@ -121,6 +127,12 @@ export default class EntityDetails extends Component { editFareRules={this.state.editFareRules} toggleEditFareRules={this._toggleFareRules} {...this.props} /> + { /* Render editor instructions for locations */ } + {activeComponent === 'location' && +
+ Use the polygon editor at the top left of the map to create flex areas +
+ } {/* Entity Details Body */}
{/* Render relevant form based on entity type */} @@ -136,26 +148,40 @@ export default class EntityDetails extends Component { : activeComponent === 'scheduleexception' // $FlowFixMe ? - :
-
- {/* Editor Inputs */} - {renderDefault && currentTable.fields - .map((field, i) => ( -
- -
- )) - } -
-
+ : activeComponent === 'bookingrule' + ? ( + c.service_id)} + {...this.props} + /> + ) + : ( +
+
+ {/* Editor Inputs */} + {renderDefault && [...currentTable.fields, ...(currentTable.extraFields || [])] + .map((field, i) => ( +
+ +
+ )) + } +
+
+ ) + }
diff --git a/lib/editor/components/EntityDetailsHeader.js b/lib/editor/components/EntityDetailsHeader.js index 6b6d9bfa3..b43fa3658 100644 --- a/lib/editor/components/EntityDetailsHeader.js +++ b/lib/editor/components/EntityDetailsHeader.js @@ -2,17 +2,19 @@ import Icon from '@conveyal/woonerf/components/icon' import React, {Component} from 'react' +import {connect} from 'react-redux' import {Badge, Button, ButtonGroup, Tooltip, OverlayTrigger, Nav, NavItem} from 'react-bootstrap' import {getComponentMessages} from '../../common/util/config' +import {getLocations} from '../selectors' import * as activeActions from '../actions/active' import * as mapActions from '../actions/map' -import {getEntityBounds, getEntityName} from '../util/gtfs' +import {getEntityBounds, getEntityName, getTripPatterns} from '../util/gtfs' import {entityIsNew} from '../util/objects' import {getGtfsIconName} from '../util/ui' -import type {Entity, Feed, GtfsRoute, GtfsStop, Pattern} from '../../types' -import type {MapState} from '../../types/reducers' -import type {EditorValidationIssue} from '../util/validation' +import type {Entity, Feed, GtfsRoute, GtfsLocation, GtfsStop, Pattern} from '../../types' +import type {MapState, AppState, RouteParams} from '../../types/reducers' +import type {EditorValidationIssue} from '../util/validation/common' type RouteWithPatterns = {tripPatterns: Array} & GtfsRoute @@ -24,7 +26,9 @@ type Props = { editFareRules: boolean, entityEdited: boolean, feedSource: Feed, + locations?: Array, mapState: MapState, + params?: RouteParams, resetActiveGtfsEntity: typeof activeActions.resetActiveGtfsEntity, saveActiveGtfsEntity: typeof activeActions.saveActiveGtfsEntity, setActiveEntity: typeof activeActions.setActiveEntity, @@ -35,7 +39,7 @@ type Props = { validationErrors: Array } -export default class EntityDetailsHeader extends Component { +class EntityDetailsHeader extends Component { messages = getComponentMessages('EntityDetailsHeader') _onClickSave = () => { @@ -65,14 +69,16 @@ export default class EntityDetailsHeader extends Component { } _onClickZoomTo = () => { - const { activeEntity, activePatternStops, subEntity, updateMapSetting } = this.props + const { activeEntity, activePatternStops, locations, subEntity, updateMapSetting } = this.props let props if (subEntity) { const castedRoute = ((activeEntity: any): RouteWithPatterns) const pattern = castedRoute.tripPatterns.find(p => p.id === subEntity) - props = { bounds: getEntityBounds(pattern, activePatternStops), target: subEntity } + // Locations are needed to find bounds of pattern locations since shapes don't provide them + props = { bounds: getEntityBounds(pattern, activePatternStops, locations), target: subEntity } } else { - props = { bounds: getEntityBounds(activeEntity), target: +activeEntity.id } + // We need locations & pattern stops to check against trip patterns + props = { bounds: getEntityBounds(activeEntity, activePatternStops, locations), target: +activeEntity.id } } updateMapSetting(props) } @@ -126,14 +132,25 @@ export default class EntityDetailsHeader extends Component { ) const hasErrors = validationErrors.length > 0 + const noLocationShapes = + activeEntity.location_shapes && + typeof activeEntity.location_shapes === 'object' && + !activeEntity.location_shapes.length const entityName = activeComponent === 'feedinfo' ? this.messages('feedInfo') : getEntityName(activeEntity) const zoomDisabled = activeEntity && !subComponent ? mapState.target === activeEntity.id : mapState.target === subEntity + + // prevent zooming to empty trip pattern + const patterns = getTripPatterns(activeEntity) + const emptyTripPattern = patterns.some(pattern => pattern.patternStops.length === 0 && pattern.patternStops.length === 0) + // flex zoom available only when a trip pattern is actually rendered on the map + const tripPatternRendered = activeEntity.tripPatterns && (this.props.params ? !this.props.params.subEntityId : true) + const iconName = getGtfsIconName(activeComponent) - const nameWidth = activeComponent === 'stop' || activeComponent === 'route' + const nameWidth = activeComponent === 'stop' || activeComponent === 'route' || activeComponent === 'location' ? '176px' : '210px' return ( @@ -141,14 +158,14 @@ export default class EntityDetailsHeader extends Component {
{/* Zoom, undo, and save buttons */} - {activeComponent === 'stop' || activeComponent === 'route' + {activeComponent === 'stop' || activeComponent === 'route' || activeComponent === 'location' ? {this.messages('zoomTo')} {activeComponent}}> @@ -173,7 +190,7 @@ export default class EntityDetailsHeader extends Component { @@ -186,11 +203,11 @@ export default class EntityDetailsHeader extends Component { ? : iconName @@ -257,7 +274,7 @@ export default class EntityDetailsHeader extends Component { onClick={this._showTripPatterns} > {this.messages('tripPatterns')} - {activeEntity.tripPatterns ? activeEntity.tripPatterns.length : 0} + {patterns.length} @@ -287,3 +304,10 @@ export default class EntityDetailsHeader extends Component { ) } } + +const mapStateToProps = (state: AppState, ownProps: Props) => ({ + locations: getLocations(state) +}) + +const ActiveEntityDetailsHeader = connect(mapStateToProps)(EntityDetailsHeader) +export default ActiveEntityDetailsHeader diff --git a/lib/editor/components/EntityList.js b/lib/editor/components/EntityList.js index ac0fe0364..efc52673c 100644 --- a/lib/editor/components/EntityList.js +++ b/lib/editor/components/EntityList.js @@ -199,16 +199,27 @@ export default class EntityList extends Component { > {this.messages('createStop')} - :
- -
+ : activeComponent === 'location' + ? ( +
+ Create a new location to begin flex area editing. +
+ ) : ( +
+ +
+ ) return (
diff --git a/lib/editor/components/EntityListSecondaryActions.js b/lib/editor/components/EntityListSecondaryActions.js index 02a676fc4..f8542278b 100644 --- a/lib/editor/components/EntityListSecondaryActions.js +++ b/lib/editor/components/EntityListSecondaryActions.js @@ -97,12 +97,31 @@ export default class EntityListSecondaryActions extends Component { } } + _onSelectLocation = () => { + if (this.props.activeComponent !== 'location') { + this.props.setActiveEntity(this.props.feedSource.id, 'location') + } + } + + _onSelectStop = () => { + if (this.props.activeComponent !== 'stop') { + this.props.setActiveEntity(this.props.feedSource.id, 'stop') + } + } + + _onTableSelect = (option: any) => { + if (this.props.activeComponent !== option.value) { + this.props.setActiveEntity(this.props.feedSource.id, option.value) + } + } + // eslint-disable-next-line complexity render () { const { activeComponent, activeEntity, - entities + entities, + feedSource } = this.props switch (activeComponent) { case 'calendar': @@ -126,16 +145,39 @@ export default class EntityListSecondaryActions extends Component { ) + case 'location': case 'route': case 'stop': return ( - + // FIXME[React 16.2+]: Extract into <>... markup. +
+ {((activeComponent === 'stop' || activeComponent === 'location') && feedSource && feedSource.flexUIFeaturesEnabled) && + + } + +
) default: return null diff --git a/lib/editor/components/ExceptionDate.js b/lib/editor/components/ExceptionDate.js index e40052ab6..571cca8ee 100644 --- a/lib/editor/components/ExceptionDate.js +++ b/lib/editor/components/ExceptionDate.js @@ -9,7 +9,7 @@ import {toast} from 'react-toastify' import {updateActiveGtfsEntity} from '../actions/active' import type {ScheduleException} from '../../types' -import type {EditorValidationIssue} from '../util/validation' +import type {EditorValidationIssue} from '../util/validation/common' import { getComponentMessages } from '../../common/util/config' import {sortDates} from './ExceptionDateRange' diff --git a/lib/editor/components/ExceptionDateRange.js b/lib/editor/components/ExceptionDateRange.js index 275873923..641a574e6 100644 --- a/lib/editor/components/ExceptionDateRange.js +++ b/lib/editor/components/ExceptionDateRange.js @@ -9,7 +9,7 @@ import {updateActiveGtfsEntity} from '../actions/active' import { getComponentMessages } from '../../common/util/config' import {modifyRangeOfDates, updateDates} from '../../common/util/exceptions' import type {ExceptionDate, ScheduleException, ScheduleExceptionDateRange} from '../../types' -import type {EditorValidationIssue} from '../util/validation' +import type {EditorValidationIssue} from '../util/validation/common' import {inputStyleProps} from './ExceptionDate' import ExceptionValidationErrorsList from './ExceptionValidationErrorsList' diff --git a/lib/editor/components/ExceptionValidationErrorsList.js b/lib/editor/components/ExceptionValidationErrorsList.js index 97173a122..d125b94bc 100644 --- a/lib/editor/components/ExceptionValidationErrorsList.js +++ b/lib/editor/components/ExceptionValidationErrorsList.js @@ -3,7 +3,7 @@ import React from 'react' import { getComponentMessages } from '../../common/util/config' -import type { EditorValidationIssue } from '../util/validation' +import type { EditorValidationIssue } from '../util/validation/common' const VALIDATION_ERROR_LIMIT = 3 diff --git a/lib/editor/components/FareRulesForm.js b/lib/editor/components/FareRulesForm.js index 1437bedc2..ae49c28cc 100644 --- a/lib/editor/components/FareRulesForm.js +++ b/lib/editor/components/FareRulesForm.js @@ -9,7 +9,7 @@ import {getFareRuleFieldName} from '../util' import {generateNullProps, getTableById} from '../util/gtfs' import type {FareRule, GtfsFare, ZoneOption, Zones} from '../../types' import type {EditorTables} from '../../types/reducers' -import type {EditorValidationIssue} from '../util/validation' +import type {EditorValidationIssue} from '../util/validation/common' import FareRuleSelections from './FareRuleSelections' diff --git a/lib/editor/components/FeedInfoPanel.js b/lib/editor/components/FeedInfoPanel.js index 3508fb4d8..e50f7ac93 100644 --- a/lib/editor/components/FeedInfoPanel.js +++ b/lib/editor/components/FeedInfoPanel.js @@ -163,7 +163,7 @@ export default class FeedInfoPanel extends Component { title={} id='add-entity-dropdown' onSelect={this._onAddSelect}> - {GTFS_ICONS.map(c => { + {GTFS_ICONS.filter(item => !item.flexOnly || (item.flexOnly && feedSource && feedSource.flexUIFeaturesEnabled)).map(c => { if (!c.addable) return null return ( , + activePatternLocations: Array, activePatternStops: Array, activeSubSubEntity: string, addStopAtIntersection: typeof stopStrategiesActions.addStopAtIntersection, @@ -116,6 +119,7 @@ type Props = ContainerProps & { updateMapSetting: typeof mapActions.updateMapSetting, updatePatternStops: typeof tripPatternActions.updatePatternStops, updateUserData: typeof userActions.updateUserData, + updatingLocationShape: typeof activeActions.updatingLocationShape, uploadBrandingAsset: typeof editorActions.uploadBrandingAsset, user: ManagerUserState, validationErrors: Array, @@ -277,8 +281,9 @@ export default class GtfsEditor extends Component { const { editingIsDisabled } = getEditorEnabledState(feedSource, user, feedIsLocked) const LIST_WIDTH = 220 + const locations: Array = getTableById(tableData, 'location') const stops: Array = getTableById(tableData, 'stop') - const DETAILS_WIDTH = 300 + const DETAILS_WIDTH = activeComponent === 'bookingrule' ? 400 : 300 // Booking rule fields require more space. const entityDetails = activeEntityId ? { }