diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 14ddaa388..000000000 --- a/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": ["es2015", "stage-0", "react"], - "env": { - "development": { - "plugins": ["transform-decorators-legacy", ["react-transform", { - "transforms": [{ - "transform": "react-transform-hmr", - // if you use React Native, pass "react-native" instead: - "imports": ["react"], - // this is important for Webpack HMR: - "locals": ["module"] - }] - // note: you can put more transforms into array - // this is just one of them! - }]] - } - } -} diff --git a/.gitignore b/.gitignore index e776b729a..b0a66b15a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,15 +3,11 @@ node_modules/ dist *.log coverage -.idea -server_config.js -src/main/client/config.js -src/main/resources/public -application.conf target/ -.java-version -datatools.iml src/main/client/config.js datatools-manager.iml config.yml config_server.yml +configurations/* +!configurations/default +tmp/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..84c6f8a50 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: node_js +notifications: + email: false +node_js: + - '6' +cache: + yarn: true +before_install: + - npm i -g codecov +script: + - yarn run lint + - yarn run cover-client + - codecov + - yarn run build + +# If sudo is disabled, CI runs on container based infrastructure (allows caching &c.) +sudo: false + +# Notify us of the build status on the Slack channel +notifications: + slack: conveyal:WQxmWiu8PdmujwLw4ziW72Gc + +# Push results to codecov.io +after_success: + - bash <(curl -s https://codecov.io/bash) + - yarn run semantic-release diff --git a/config.yml.template b/config.yml.template index b9cbc3920..71703a9f4 100644 --- a/config.yml.template +++ b/config.yml.template @@ -8,6 +8,7 @@ application: gtfs_s3_bucket: bucket-name port: 4000 title: Data Manager + assets_bucket: bucket-name logo: http://gtfs-assets-dev.conveyal.com/data_manager.png # defaults to src/main/client/assets/application_logo.png active_project: project-id notifications_enabled: false @@ -16,6 +17,7 @@ application: changelog_url: https://changelog.example.com support_email: support@example.com osm_vex: http://localhost:1000 + r5: http://localhost:8080 date_format: MMM Do YYYY auth0: diff --git a/config_server.yml.template b/config_server.yml.template deleted file mode 100644 index 630e7b494..000000000 --- a/config_server.yml.template +++ /dev/null @@ -1,8 +0,0 @@ -auth0: - client_secret: your-auth0-client-secret - api_token: your-auth0-api-token - -# For email notifications -sparkpost: - key: your-api-key - from_email: email@example.com diff --git a/configurations/default/settings.yml b/configurations/default/settings.yml new file mode 100644 index 000000000..c1f14225a --- /dev/null +++ b/configurations/default/settings.yml @@ -0,0 +1,66 @@ +entries: + - lib/main.js:dist/index.js + - lib/index.css:dist/index.css +application: + data: + mapdb: /path/to/mapdb + gtfs: /path/to/gtfs + editor_mapdb: /path/to/editor + regions: /path/to/regions/geojson + use_s3_storage: false + gtfs_s3_bucket: bucket-name + port: 4000 + title: Data Manager + assets_bucket: bucket-name + logo: http://gtfs-assets-dev.conveyal.com/data_manager.png # defaults to src/main/client/assets/application_logo.png + active_project: project-id + notifications_enabled: false + public_url: http://localhost:9000 + docs_url: http://docs.example.com + changelog_url: https://changelog.example.com + support_email: support@example.com + osm_vex: http://localhost:1000 + r5: http://localhost:8080 + date_format: MMM Do YYYY + +auth0: + domain: your-auth0-domain + client_id: your-auth0-client-id +MAPZEN_TURN_BY_TURN_KEY: API_KEY +mapbox: + access_token: MAPBOX_ACCESS_TOKEN + map_id: conveyal.ie3o67m0 +modules: + enterprise: + enabled: false + editor: + enabled: true + legacy_editor: + enabled: false + url: http://localhost:9001 + alerts: + enabled: false + use_extension: mtc + sign_config: + enabled: false + user_admin: + enabled: true + validator: + enabled: true + deployment: + enabled: false + gtfsapi: + enabled: false + load_on_fetch: false + load_on_startup: false + use_extension: mtc + update_frequency: 3600 # in seconds + +extensions: + transitland: + enabled: true + api: https://transit.land/api/v1/feeds + transitfeeds: + enabled: true + api: http://api.transitfeeds.com/v1/getFeeds + key: your-api-key diff --git a/docs/dev/deployment.md b/docs/dev/deployment.md index b72cd669c..37c6d0abf 100644 --- a/docs/dev/deployment.md +++ b/docs/dev/deployment.md @@ -2,7 +2,7 @@ ## Prerequisites -The application features a Spark-powered Java backend and a Javascript frontend written with React and Redux. To install and deploy the application, you will need Java 8, Maven, Node/npm, and Webpack. +The application features a Spark-powered Java backend and a Javascript frontend written with React and Redux. To install and deploy the application, you will need Java 8, Maven, Node/npm, yarn, and [mastarm](https://github.com/conveyal/mastarm). User athentication is done via [Auth0](http://auth0.com). You will need an Auth0 account and application to use the Data Manager. @@ -95,16 +95,17 @@ To allow for the creation, deletion and editing of users you must generate a tok ## Building and Running the Application -Install the Javascript dependencies using npm: +Install the Javascript dependencies using yarn: ```bash -$ npm install +$ yarn ``` -Build the frontend using webpack: +Build and deploy the frontend to s3 using npm script (which calls [mastarm](https://github.com/conveyal/mastarm)): ```bash -$ webpack +$ npm run deploy -- s3://$S3_BUCKET_NAME/dist +>>>>>>> Stashed changes ``` Package the application using Maven: @@ -119,7 +120,9 @@ Deploy the application with Java: $ java -jar target/datatools.jar ``` -The application should now be accessible at `http://localhost:9000` (or whatever port you specified in `config.yml`). + +The application back-end should now be running at `http://localhost:9000` (or whatever port you specified in `config.yml`). +The front-end assets are pointed to by the back end at whatever s3 bucket name is specified in `config.yml` at `application.assets_bucket`. ## Configuring Modules diff --git a/docs/dev/development.md b/docs/dev/development.md index 98c36686a..873204298 100644 --- a/docs/dev/development.md +++ b/docs/dev/development.md @@ -1,11 +1,13 @@ # Development -## Using `combine-serve` +## mastarm -Spark does not hot-reload static web files, i.e. the application frontend. To make life easier when doing frontend development, we recommend using [combine-serve](https://github.com/conveyal/combine-serve) to serve both the backend and frontend as a unified service. Used in conjunction with `webpack --watch`, this will eliminate the need to constantly rebuild/reload the frontend for testing. +We use Conveyal's front-end JS tool-belt [`mastarm`](https://github.com/conveyal/mastarm) to build, run, and lint while developing. -For example, if running the Java backend on port 9000 (typically via an IDE such as IntelliJ), and you want to serve the combined application on port 9001 for development purposes, use: +To kick off a development server at [http://localhost:9966](http://localhost:9966): ``` -combine-serve --serve / src/main/resources/public/ --proxy / http://localhost:9000 --port 9001 +npm start ``` + +This will use `mastarm` to run a browserify server at the above port along with a proxy for the back-end API, which is assumed to be running on http://localhost:4000. \ No newline at end of file diff --git a/gtfs.yml b/gtfs.yml index a8d82ccfe..5d86bca60 100644 --- a/gtfs.yml +++ b/gtfs.yml @@ -85,7 +85,7 @@ fields: - name: "agency_id" required: false - inputType: ID + 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" @@ -126,10 +126,10 @@ 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: agencyBrandingUrl + - name: agency_branding_url required: false displayName: Agency branding URL - datatools: true + datatools: false placeholder: https://agency.org/assets/agency/XYZ inputType: URL columnWidth: 12 @@ -141,12 +141,12 @@ fields: - name: "stop_id" required: true - inputType: ID + inputType: GTFS_ID columnWidth: 6 helpContent: "The stop_id field contains an ID that uniquely identifies a stop or station. Multiple routes may use the same stop. The stop_id is dataset unique." - name: "stop_code" required: false - inputType: ID + inputType: GTFS_ID columnWidth: 6 helpContent: "The stop_code field contains short text or a number that uniquely identifies the stop for passengers. Stop codes are often used in phone-based transit information systems or printed on stop signage to make it easier for riders to get a stop schedule or real-time arrival information for a particular stop." - name: "stop_name" @@ -233,16 +233,16 @@ required: true inputType: DROPDOWN options: - - value: 0 + - value: false text: 'No' - - value: 1 + - value: true text: 'Yes' columnWidth: 6 adminOnly: true # helpContent: The route_id field contains an ID that uniquely identifies a route. The route_id is dataset unique. - name: route_id required: true - inputType: ID + inputType: GTFS_ID columnWidth: 5 helpContent: The route_id field contains an ID that uniquely identifies a route. The route_id is dataset unique. - name: route_short_name @@ -309,10 +309,10 @@ text: Black columnWidth: 6 helpContent: The route_text_color field can be used to specify a legible color to use for text drawn against a background of route_color. The color must be provided as a six-character hexadecimal number, for example, FFD700. If no color is specified, the default text color is black (000000). - - name: routeBrandingUrl + - name: route_branding_url required: false displayName: Route branding URL - datatools: true + datatools: false placeholder: https://agency.org/assets/route/1234 inputType: URL columnWidth: 12 @@ -334,7 +334,7 @@ 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: ID + 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" @@ -460,7 +460,7 @@ fields: - name: "service_id" required: true - inputType: ID + 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" @@ -567,7 +567,7 @@ fields: - name: "fare_id" required: true - inputType: ID + 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" diff --git a/gtfsplus.yml b/gtfsplus.yml index 9f15f0264..6d961ff66 100644 --- a/gtfsplus.yml +++ b/gtfsplus.yml @@ -7,8 +7,7 @@ required: true inputType: GTFS_ROUTE columnWidth: 4 - helpContent: From GTFS routes.txt file. This is necessary to maintain relationship - between routes in this file and routes in the standard GTFS routes.txt file. + helpContent: From GTFS routes.txt file. This is necessary to maintain relationship between routes in this file and routes in the standard GTFS routes.txt file. - name: realtime_enabled required: true inputType: DROPDOWN @@ -18,29 +17,21 @@ - value: '1' text: Enabled columnWidth: 2 - helpContent: Standard GTFS does not include a field to indicate real-time status - of a route. This allows the flexibility for agencies to identify routes that - may not have real-time predictions. + helpContent: Standard GTFS does not include a field to indicate real-time status of a route. This allows the flexibility for agencies to identify routes that may not have real-time predictions. - name: realtime_routename required: false inputType: TEXT columnWidth: 3 - helpContent: This field may be empty if route_long_name in GTFS routes.txt file - is provided. Otherwise, it should contain the data defined for route_long_name - in GTFS routes.txt file. + helpContent: This field may be empty if route_long_name in GTFS routes.txt file is provided. Otherwise, it should contain the data defined for route_long_name in GTFS routes.txt file. - name: realtime_routecode required: false inputType: TEXT columnWidth: 3 - helpContent: This field can be empty if real-time route code in MTC’s real-time - feed is the same as route_short_name in GTFS routes.txt file + helpContent: This field can be empty if real-time route code in MTC’s real-time feed is the same as route_short_name in GTFS routes.txt file - id: realtime_stops name: realtime_stops.txt - helpContent: This file helps map regional real-time stop identifiers with the - stop_id in stops.txt file in GTFS. This file can be omitted if GTFS file - stops.txt contains the regional real-time stop id in the optional stop_code - field. + helpContent: This file helps map regional real-time stop identifiers with the stop_id in stops.txt file in GTFS. This file can be omitted if GTFS file stops.txt contains the regional real-time stop id in the optional stop_code field. fields: - name: trip_id required: true @@ -51,8 +42,7 @@ required: true inputType: GTFS_STOP columnWidth: 5 - helpContent: From GTFS stop_times.txt file when the stop_code in standard GTFS - is different from the stop_code used in real-time feed for MTC. + helpContent: From GTFS stop_times.txt file when the stop_code in standard GTFS is different from the stop_code used in real-time feed for MTC. - name: realtime_stop_id required: true inputType: TEXT @@ -61,21 +51,7 @@ - id: directions name: directions.txt - helpContent:

Users of version 1.7 or later must use this file and ignore - realtime_directions.txt file.

-

In the standard GTFS, trip direction can be specified as ‘0’ or ‘1’ only. It - does not allow for indication of specific directions such as North, South, etc. - This file captures the direction text for each of the directions in a route.

-

Route direction information can be provided through either this directions.txt - OR realtime_directions.txt file, but not both. Users of GTFS+ version 1.7 and - above are required to provide directions.txt file and ignore realtime_directions.txt - file. Users of version 1.6 or earlier may continue to use realtime_directions.txt, - unless they voluntarily choose to upgrade to directions.txt. Only one of - directions.txt or realtime_directions.txt should be included. Also, tripheadsign - field in GTFS trips.txt file should be populated in order to distinguish patterns - within a direction and to provide destination information to passengers.

-

When directions.txt file is used, optional direction_id field in GTFS trips.txt - must be filled in.

+ helpContent:

Users of version 1.7 or later must use this file and ignore realtime_directions.txt file.

In the standard GTFS, trip direction can be specified as ‘0’ or ‘1’ only. It does not allow for indication of specific directions such as North, South, etc. This file captures the direction text for each of the directions in a route.

Route direction information can be provided through either this directions.txt OR realtime_directions.txt file, but not both. Users of GTFS+ version 1.7 and above are required to provide directions.txt file and ignore realtime_directions.txt file. Users of version 1.6 or earlier may continue to use realtime_directions.txt, unless they voluntarily choose to upgrade to directions.txt. Only one of directions.txt or realtime_directions.txt should be included. Also, tripheadsign field in GTFS trips.txt file should be populated in order to distinguish patterns within a direction and to provide destination information to passengers.

When directions.txt file is used, optional direction_id field in GTFS trips.txt must be filled in.

fields: - name: route_id required: true @@ -89,8 +65,7 @@ - value: '0' - value: '1' columnWidth: 3 - helpContent: Binary direction_id from GTFS trips.txt file. Each (route_id, - direction_id) pair can only appear once in directions.txt. + helpContent: Binary direction_id from GTFS trips.txt file. Each (route_id, direction_id) pair can only appear once in directions.txt. - name: direction required: true inputType: DROPDOWN @@ -115,9 +90,7 @@ - id: realtime_trips name: realtime_trips.txt - helpContent: This file contains real-time identifiers for trips. This file can be - omitted if the GTFS trips.txt file contains the same Trip IDs included in - real-time feed for MTC within the trip_id field. + helpContent: This file contains real-time identifiers for trips. This file can be omitted if the GTFS trips.txt file contains the same Trip IDs included in real-time feed for MTC within the trip_id field. fields: - name: trip_id required: true @@ -133,8 +106,7 @@ - id: stop_attributes name: stop_attributes.txt - helpContent: This file contains additional attributes for a stop. This file is needed - because standard GTFS does not include these fields. + helpContent: This file contains additional attributes for a stop. This file is needed because standard GTFS does not include these fields. fields: - name: stop_id required: true @@ -214,9 +186,7 @@ - id: timepoints name: timepoints.txt - helpContent: This file can be omitted if the GTFS file stop_times.txt has blank - values for arrival_time and departure_time for all the stops that are NOT time - points. This file is needed because standard GTFS does not include these fields. + helpContent: This file can be omitted if the GTFS file stop_times.txt has blank values for arrival_time and departure_time for all the stops that are NOT time points. This file is needed because standard GTFS does not include these fields. fields: - name: trip_id required: true @@ -231,8 +201,7 @@ - id: rider_categories name: rider_categories.txt - helpContent: This file lists the rider categories for fares other than the Regular - category. This file is needed because standard GTFS does not include these fields. + helpContent: This file lists the rider categories for fares other than the Regular category. This file is needed because standard GTFS does not include these fields. fields: - name: rider_category_id required: true @@ -277,24 +246,17 @@ - value: '25' text: Custom (25) columnWidth: 6 - helpContent: Unique rider category ID (agency can assign categories that do not - fall under standard categories) + helpContent: Unique rider category ID (agency can assign categories that do not fall under standard categories) - name: rider_category_description required: true inputType: TEXT maxLength: 256 columnWidth: 6 - helpContent: Rider category as it should appear on 511.org, such as Child (ages 5- - 11), Seniors (Ages 62 & Up) + helpContent: Rider category as it should appear on 511.org, such as Child (ages 5-11), Seniors (Ages 62 & Up) - id: fare_rider_categories name: fare_rider_categories.txt - helpContent: This file specifies attributes for the fares for rider categories. GTFS - file fare_attributes.txt contains the fares for the Regular rider category. Fares - for other rider categories such as Child, Senior, etc will be provided in this plus - file fare_rider_categories.txt. The combination of fare_id and rider_category_id - should be unique in this file. This file is needed because standard GTFS does not - include these fields. + helpContent: This file specifies attributes for the fares for rider categories. GTFS file fare_attributes.txt contains the fares for the Regular rider category. Fares for other rider categories such as Child, Senior, etc will be provided in this plus file fare_rider_categories.txt. The combination of fare_id and rider_category_id should be unique in this file. This file is needed because standard GTFS does not include these fields. fields: - name: fare_id required: true @@ -351,8 +313,7 @@ - id: calendar_attributes name: calendar_attributes.txt - helpContent: This file contains calendar attributes. This file is needed because - standard GTFS does not include these fields. + helpContent: This file contains calendar attributes. This file is needed because standard GTFS does not include these fields. fields: - name: service_id required: true @@ -362,13 +323,11 @@ required: true inputType: TEXT maxLength: 30 - helpContent: Description of the service, as it should appear on 511.org such as - Weekdays, Sunday/Holiday + helpContent: Description of the service, as it should appear on 511.org such as Weekdays, Sunday/Holiday - id: farezone_attributes name: farezone_attributes.txt - helpContent: This file contains fare zone attributes. This file is needed because - standard GTFS does not include these fields. + helpContent: This file contains fare zone attributes. This file is needed because standard GTFS does not include these fields. fields: - name: zone_id required: true @@ -378,5 +337,4 @@ required: true inputType: TEXT maxLength: 35 - helpContent: Public name of the fare zone, as it should appear on 511.org such as - EastBay, WestBay, etc + helpContent: Public name of the fare zone, as it should appear on 511.org such as EastBay, WestBay, etc diff --git a/i18n/english.yml b/i18n/english.yml index 2096a9fdc..ad977d102 100644 --- a/i18n/english.yml +++ b/i18n/english.yml @@ -50,7 +50,9 @@ DeploymentsPanel: deployedTo: Deployed to ProjectViewer: mergeFeeds: Merge all + makePublic: Update public feeds settings: Settings + makePublic: Publish public feeds deployments: Deployments feeds: title: Feed Sources @@ -78,6 +80,7 @@ EditorFeedSourcePanel: date: Date name: Name download: Download + active: Active restore: Make active load: Load for Editing confirmLoad: This will override all active GTFS Editor data for this Feed Source with the data from this version. If there is unsaved work in the Editor you want to keep, you must snapshot the current Editor data first. Are you sure you want to continue? @@ -109,7 +112,6 @@ FeedSourceViewer: FeedVersionViewer: status: Status version: Version - validDates: Valid Dates noVersionsExist: No versions exist for this feed source. timestamp: File Timestamp download: Download @@ -118,11 +120,12 @@ FeedVersionViewer: feed: Feed delete: Delete confirmDelete: Are you sure you want to delete this version? This cannot be undone. - fileSize: File Size +FeedVersionReport: agencyCount: Agency Count routeCount: Route Count tripCount: Trip Count stopTimesCount: Stop time Count + validDates: Valid Dates GtfsValidationExplorer: title: Validation Explorer validationIssues: Validation Issues @@ -148,6 +151,7 @@ ResultTable: problemType: Problem Type priority: Priority affectedIds: Affected ID(s) + line: Line description: Description DeploymentViewer: versions: Feed Versions @@ -217,6 +221,7 @@ ProjectSettings: stairsReluctance: Stairs Reluctance carDropoffTime: Car Dropoff Time brandingUrlRoot: Branding URL Root + requestLogFile: Request log file servers: title: Servers new: Add server @@ -227,6 +232,7 @@ ProjectSettings: public: Public URL internal: Internal URLs admin: Admin access only? + s3Bucket: S3 bucket name osm: title: OSM Extract gtfs: Use GTFS-Derived Extract Bounds @@ -255,21 +261,51 @@ UserAdmin: title: Administration noAccess: You do not have sufficient user privileges to access this area. UserList: + filterByOrg: Filter by org. title: User Management showing: Showing Users of: of search: Search by username +UserRow: + appAdmin: App admin + orgAdmin: Org admin + cancel: Cancel + delete: Delete + deleteConfirm: Are you sure you want to permanently delete this user? + edit: Edit + save: Save UserHomePage: title: Projects noProjects: You currently do not have any projects. createFirst: Create my first project - search: Search projects table: name: Project Name new: New Project help: title: What's a project? content: A project is used to group GTFS feeds. For example, the feeds in a project may be in the same region or they may collectively define a planning scenario. +FeedSourcePanel: + search: Search feeds +OrganizationList: + search: Search orgs + new: Create org +OrganizationSettings: + orgDetails: Organization details + projects: Projects + extensions: Extensions + subDetails: Subscription details + name: + label: Name + placeholder: Big City Transit + logoUrl: + label: Logo URL + placeholder: http://example.com/logo_30x30.png + subscriptionBeginDate: Subscription begins + subscriptionEndDate: Subscription ends + usageTier: + low: Low + medium: Medium + high: High CreateUser: new: Create User @@ -279,6 +315,10 @@ UserSettings: cancel: Cancel save: Save application: Application Settings + org: + admin: Organization administrator + description: Organization administrators have full access to projects within the organization. + billing: Billing admin admin: title: Application Admininistrator description: Application administrators have full access to all projects. diff --git a/i18n/espanol.yml b/i18n/espanol.yml index fa9225ffd..e0b21ebc3 100644 --- a/i18n/espanol.yml +++ b/i18n/espanol.yml @@ -48,6 +48,8 @@ DeploymentsPanel: feedCount: Number of feeds ProjectViewer: mergeFeeds: Merge Feeds + settings: Settings + makePublic: Update public feeds feeds: title: Feed Sources search: Search by name @@ -84,13 +86,21 @@ FeedSourceViewer: update: Update FeedVersionViewer: status: Status - validDates: Valid Dates + version: Version + noVersionsExist: No versions exist for this feed source. timestamp: File Timestamp - size: File Size + download: Download + load: Load for Editing + confirmLoad: This will override all active GTFS Editor data for this Feed Source with the data from this version. If there is unsaved work in the Editor you want to keep, you must snapshot the current Editor data first. Are you sure you want to continue? + feed: Feed + delete: Delete + confirmDelete: Are you sure you want to delete this version? This cannot be undone. +FeedVersionReport: agencyCount: Agency Count routeCount: Route Count tripCount: Trip Count stopTimesCount: Stop time Count + validDates: Valid Dates GtfsValidationExplorer: title: Validation Explorer validationIssues: Validation Issues @@ -113,6 +123,7 @@ ResultTable: problemType: Problem Type priority: Priority affectedIds: Affected ID(s) + line: Line description: Description DeploymentViewer: versions: Feed Versions @@ -182,6 +193,7 @@ ProjectSettings: stairsReluctance: Stairs Reluctance carDropoffTime: Car Dropoff Time brandingUrlRoot: Branding URL Root + requestLogFile: Request log file servers: title: Servers new: Add server @@ -192,6 +204,7 @@ ProjectSettings: public: Public URL internal: Internal URLs admin: Admin access only? + s3Bucket: S3 bucket name osm: title: OSM Extract gtfs: Use GTFS-Derived Extract Bounds @@ -201,10 +214,53 @@ ProjectSettings: UserAdmin: title: User Management UserList: + filterByOrg: Filter by org. title: User Management showing: Showing Users of: of search: Search by username +UserRow: + appAdmin: App admin + orgAdmin: Org admin + cancel: Cancel + delete: Delete + deleteConfirm: Are you sure you want to permanently delete this user? + edit: Edit + save: Save +UserHomePage: + title: Projects + noProjects: You currently do not have any projects. + createFirst: Create my first project + search: Search feeds + table: + name: Project Name + new: New Project + help: + title: What's a project? + content: A project is used to group GTFS feeds. For example, the feeds in a project may be in the same region or they may collectively define a planning scenario. +FeedSourcePanel: + search: Search feeds +OrganizationList: + search: Search orgs + new: Create org +OrganizationSettings: + orgDetails: Organization details + projects: Projects + extensions: Extensions + subDetails: Subscription details + name: + label: Name + placeholder: Big City Transit + logoUrl: + label: Logo URL + placeholder: http://example.com/logo_30x30.png + subscriptionBeginDate: Subscription begins + subscriptionEndDate: Subscription ends + usageTier: + low: Low + medium: Medium + high: High + CreateUser: new: Create User UserSettings: @@ -213,6 +269,10 @@ UserSettings: cancel: Cancel save: Save application: Application Settings + org: + admin: Organization administrator + description: Organization administrators have full access to projects within the organization. + billing: Billing admin admin: title: Application Admininistrator description: Application administrators have full access to all projects. diff --git a/i18n/francais.yml b/i18n/francais.yml index 9dac381ae..3652de509 100644 --- a/i18n/francais.yml +++ b/i18n/francais.yml @@ -48,6 +48,8 @@ DeploymentsPanel: feedCount: Number of feeds ProjectViewer: mergeFeeds: Merge Feeds + settings: Settings + makePublic: Update public feeds feeds: title: Feed Sources search: Search by name @@ -84,13 +86,21 @@ FeedSourceViewer: update: Update FeedVersionViewer: status: Status - validDates: Valid Dates + version: Version + noVersionsExist: No versions exist for this feed source. timestamp: File Timestamp - size: File Size + download: Download + load: Load for Editing + confirmLoad: This will override all active GTFS Editor data for this Feed Source with the data from this version. If there is unsaved work in the Editor you want to keep, you must snapshot the current Editor data first. Are you sure you want to continue? + feed: Feed + delete: Delete + confirmDelete: Are you sure you want to delete this version? This cannot be undone. +FeedVersionReport: agencyCount: Agency Count routeCount: Route Count tripCount: Trip Count stopTimesCount: Stop time Count + validDates: Valid Dates GtfsValidationExplorer: title: Validation Explorer validationIssues: Validation Issues @@ -113,6 +123,7 @@ ResultTable: problemType: Problem Type priority: Priority affectedIds: Affected ID(s) + line: Line description: Description DeploymentViewer: versions: Feed Versions @@ -182,6 +193,7 @@ ProjectSettings: stairsReluctance: Stairs Reluctance carDropoffTime: Car Dropoff Time brandingUrlRoot: Branding URL Root + requestLogFile: Request log file servers: title: Servers new: Add server @@ -192,6 +204,7 @@ ProjectSettings: public: Public URL internal: Internal URLs admin: Admin access only? + s3Bucket: S3 bucket name osm: title: OSM Extract gtfs: Use GTFS-Derived Extract Bounds @@ -201,10 +214,53 @@ ProjectSettings: UserAdmin: title: User Management UserList: + filterByOrg: Filter by org. title: User Management showing: Showing Users of: of search: Search by username +UserRow: + appAdmin: App admin + orgAdmin: Org admin + cancel: Cancel + delete: Delete + deleteConfirm: Are you sure you want to permanently delete this user? + edit: Edit + save: Save +UserHomePage: + title: Projects + noProjects: You currently do not have any projects. + createFirst: Create my first project + search: Search feeds + table: + name: Project Name + new: New Project + help: + title: What's a project? + content: A project is used to group GTFS feeds. For example, the feeds in a project may be in the same region or they may collectively define a planning scenario. +FeedSourcePanel: + search: Search feeds +OrganizationList: + search: Search orgs + new: Create org +OrganizationSettings: + orgDetails: Organization details + projects: Projects + extensions: Extensions + subDetails: Subscription details + name: + label: Name + placeholder: Big City Transit + logoUrl: + label: Logo URL + placeholder: http://example.com/logo_30x30.png + subscriptionBeginDate: Subscription begins + subscriptionEndDate: Subscription ends + usageTier: + low: Low + medium: Medium + high: High + CreateUser: new: Create User UserSettings: @@ -213,6 +269,10 @@ UserSettings: cancel: Cancel save: Save application: Application Settings + org: + admin: Organization administrator + description: Organization administrators have full access to projects within the organization. + billing: Billing admin admin: title: Application Admininistrator description: Application administrators have full access to all projects. diff --git a/index.html b/index.html new file mode 100644 index 000000000..3acf1d168 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + Catalogue + + + + +
+ + + diff --git a/src/main/client/admin/actions/admin.js b/lib/admin/actions/admin.js similarity index 88% rename from src/main/client/admin/actions/admin.js rename to lib/admin/actions/admin.js index 3d90cbd0c..fc0c839d0 100644 --- a/src/main/client/admin/actions/admin.js +++ b/lib/admin/actions/admin.js @@ -17,15 +17,15 @@ export function receiveUsers (users, totalUserCount) { export function fetchUsers () { return function (dispatch, getState) { dispatch(requestingUsers()) - const queryString = getState().admin.userQueryString + const queryString = getState().admin.users.userQueryString let countUrl = '/api/manager/secure/usercount' - if(queryString) countUrl += `?queryString=${queryString}` + if (queryString) countUrl += `?queryString=${queryString}` const getCount = secureFetch(countUrl, getState()) .then(response => response.json()) - let usersUrl = `/api/manager/secure/user?page=${getState().admin.page}` - if(queryString) usersUrl += `&queryString=${queryString}` + let usersUrl = `/api/manager/secure/user?page=${getState().admin.users.page}` + if (queryString) usersUrl += `&queryString=${queryString}` const getUsers = secureFetch(usersUrl, getState()) .then(response => response.json()) @@ -42,9 +42,9 @@ export function fetchUserCount () { return secureFetch(url, getState()) .then(response => response.json()) .then(data => { - console.log(data); - //const users = JSON.parse(data) - //return dispatch(receiveUsers(users)) + console.log(data) + // const users = JSON.parse(data) + // return dispatch(receiveUsers(users)) }) } } diff --git a/lib/admin/actions/organizations.js b/lib/admin/actions/organizations.js new file mode 100644 index 000000000..02ebf2bac --- /dev/null +++ b/lib/admin/actions/organizations.js @@ -0,0 +1,138 @@ +import { secureFetch } from '../../common/util/util' +import { fetchProjects } from '../../manager/actions/projects' +function requestingOrganizations () { + return { + type: 'REQUESTING_ORGANIZATIONS' + } +} + +function receiveOrganizations (organizations) { + return { + type: 'RECEIVE_ORGANIZATIONS', + organizations + } +} + +export function fetchOrganizations () { + return function (dispatch, getState) { + dispatch(requestingOrganizations()) + return secureFetch('/api/manager/secure/organization', getState()) + .then(response => response.json()) + // .catch(err => console.log(err)) + .then(organizations => { + dispatch(receiveOrganizations(organizations)) + return organizations + }) + } +} + +// Single Organization Actions + +function requestingOrganization () { + return { + type: 'REQUESTING_ORGANIZATION' + } +} + +export function receiveOrganization (organization) { + return { + type: 'RECEIVE_ORGANIZATION', + organization + } +} + +export function fetchOrganization (organizationId, unsecure) { + return function (dispatch, getState) { + dispatch(requestingOrganization()) + const apiRoot = unsecure ? 'public' : 'secure' + const url = `/api/manager/${apiRoot}/organization/${organizationId}` + return secureFetch(url, getState()) + .then(response => response.json()) + // .catch(err => console.log(err)) + .then(organization => { + dispatch(receiveOrganization(organization)) + return organization + // if (!unsecure) + // return dispatch(fetchOrganizationsFeeds(organization.id)) + }) + } +} + +function deletingOrganization () { + return { + type: 'DELETING_ORGANIZATION' + } +} + +export function deletedOrganization (organization) { + return { + type: 'DELETED_ORGANIZATION', + organization + } +} + +export function deleteOrganization (organization) { + return function (dispatch, getState) { + dispatch(deletingOrganization()) + const url = `/api/manager/secure/organization/${organization.id}` + return secureFetch(url, getState(), 'delete') + .then(response => response.json()) + // .catch(err => console.log(err)) + .then(organization => { + dispatch(deletedOrganization(organization)) + return dispatch(fetchOrganizations()) + }) + } +} + +function savingOrganization (organization, changes = null) { + return { + type: 'SAVING_ORGANIZATION', + organization, + changes + } +} + +export function updateOrganization (organization, changes, fetchFeeds = false) { + return function (dispatch, getState) { + dispatch(savingOrganization(organization, changes)) + const url = `/api/manager/secure/organization/${organization.id}` + return secureFetch(url, getState(), 'put', changes) + .then((res) => { + // fetch projects because a project may have been (re)assigned to an org + dispatch(fetchProjects()) + return dispatch(fetchOrganizations()) + }) + } +} + +export function creatingOrganization (organization) { + return { + type: 'CREATING_ORGANIZATION', + organization + } +} + +export function createdOrganization (organization) { + return { + type: 'CREATED_ORGANIZATION', + organization + } +} + +// server call +export function createOrganization (organization) { + return function (dispatch, getState) { + dispatch(creatingOrganization(organization)) + console.log(organization) + const url = '/api/manager/secure/organization' + return secureFetch(url, getState(), 'post', organization) + .then(response => response.json()) + .then(org => { + dispatch(createdOrganization(org)) + // fetch projects because a project may have been (re)assigned to an org + dispatch(fetchProjects()) + return dispatch(fetchOrganizations()) + }) + } +} diff --git a/src/main/client/admin/components/CreateUser.js b/lib/admin/components/CreateUser.js similarity index 67% rename from src/main/client/admin/components/CreateUser.js rename to lib/admin/components/CreateUser.js index 63e1e4fad..ea4e19878 100644 --- a/src/main/client/admin/components/CreateUser.js +++ b/lib/admin/components/CreateUser.js @@ -1,13 +1,19 @@ -import React from 'react' -import { Button, Modal, FormControl, ControlLabel, FormGroup, Glyphicon } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import React, { Component, PropTypes } from 'react' +import { Button, Modal, FormControl, ControlLabel, FormGroup } from 'react-bootstrap' import ReactDOM from 'react-dom' -import UserSettings from './UserSettings' +import UserSettings from './UserSettings' import UserPermissions from '../../common/user/UserPermissions' import { getComponentMessages, getMessage } from '../../common/util/config' -export default class CreateUser extends React.Component { +export default class CreateUser extends Component { + static propTypes = { + projects: PropTypes.array, + createUser: PropTypes.func, + fetchProjectFeeds: PropTypes.func + } constructor (props) { super(props) this.state = { @@ -37,6 +43,12 @@ export default class CreateUser extends React.Component { render () { const messages = getComponentMessages('CreateUser') + const { + creatingUser, + organizations, + projects, + fetchProjectFeeds + } = this.props return (
@@ -53,18 +65,21 @@ export default class CreateUser extends React.Component { Create User - + Email Address - + Password diff --git a/lib/admin/components/OrganizationList.js b/lib/admin/components/OrganizationList.js new file mode 100644 index 000000000..5c5afb223 --- /dev/null +++ b/lib/admin/components/OrganizationList.js @@ -0,0 +1,232 @@ +import React, { PropTypes, Component} from 'react' +import ReactDOM from 'react-dom' +import { Panel, Modal, Row, Col, Button, FormControl, Label, ListGroup, ListGroupItem, Image } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import validator from 'validator' + +import UserPermissions from '../../common/user/UserPermissions' +import { getComponentMessages, getMessage } from '../../common/util/config' +import OrganizationSettings from './OrganizationSettings' + +export default class OrganizationList extends Component { + static propTypes = { + // userSearch: PropTypes.func, + // page: PropTypes.number, + // perPage: PropTypes.number, + // userCount: PropTypes.number, + // projects: PropTypes.array, + // fetchProjectFeeds: PropTypes.func, + // createUser: PropTypes.func, + // setPage: PropTypes.func, + // isFetching: PropTypes.bool, + organizations: PropTypes.object, + users: PropTypes.object + // setUserPermission: PropTypes.func, + // saveUser: PropTypes.func, + // deleteUser: PropTypes.func, + // token: PropTypes.string + } + constructor (props) { + super(props) + this.state = {} + } + componentWillMount () { + this.props.fetchOrganizations() + } + userSearch () { + this.props.userSearch(ReactDOM.findDOMNode(this.refs.searchInput).value) + } + showModal = () => { + this.setState({showModal: true}) + } + close = () => { + this.setState({showModal: false}) + } + save = () => { + const settings = this.refs.orgSettings.getSettings() + if (settings) { + this.props.createOrganization(settings) + .then(org => { + this.close() + }) + } else { + console.log('must provide org name') + // this.setState({errorMessage: true}) + } + } + render () { + const messages = getComponentMessages('OrganizationList') + const { isFetching, organizations, users } = this.props + return ( +
+ + + { + if (e.keyCode === 13) this.userSearch() + }} /> + + + + + + } + > + + {isFetching + ? + + + : organizations.data && organizations.data.map((organization, i) => { + const orgUsers = users.data ? users.data.filter(u => { + const permissions = new UserPermissions(u.app_metadata && u.app_metadata.datatools ? u.app_metadata.datatools : null) + return permissions.getOrganizationId() === organization.id + }) : [] + return + }) + } + + + + + Create Organization + + + + + + + + +
+ ) + } +} + +class OrganizationRow extends Component { + static propTypes = { + organization: PropTypes.object, + users: PropTypes.array + } + constructor (props) { + super(props) + this.state = { + isEditing: false + } + } + toggleExpansion () { + this.setState({ + isEditing: !this.state.isEditing + }) + } + + cancel () { + this.toggleExpansion() + } + + save () { + const settings = this.refs.orgSettings.getSettings() + this.props.updateOrganization(this.props.organization, settings) + this.toggleExpansion() + } + + delete () { + this.props.deleteOrganization(this.props.organization) + this.toggleExpansion() + } + render () { + const { + organization, + users + } = this.props + return ( + + + {organization.name} + + +
+ {organization.name}{' '} + {users.length ? : null} +
+ + + + + {this.state.isEditing + ? + : null + } + {this.state.isEditing + ? + : null + } + + + } + > + { this.state.isEditing + ? + : '' + } +
+ ) + } +} diff --git a/lib/admin/components/OrganizationSettings.js b/lib/admin/components/OrganizationSettings.js new file mode 100644 index 000000000..02f6cc071 --- /dev/null +++ b/lib/admin/components/OrganizationSettings.js @@ -0,0 +1,166 @@ +import React, { Component, PropTypes } from 'react' +import { ControlLabel, FormControl, FormGroup, Radio, Row, Col } from 'react-bootstrap' +import DateTimeField from 'react-bootstrap-datetimepicker' +import Select from 'react-select' +import moment from 'moment' +import {sentence as toSentenceCase} from 'change-case' + +import { getComponentMessages, getMessage } from '../../common/util/config' + +export default class OrganizationSettings extends Component { + static propTypes = { + organization: PropTypes.object + } + constructor (props) { + super(props) + this.state = this.props.organization || { + // default values for organization + usageTier: 'LOW', + subscriptionBeginDate: +moment().startOf('day'), + subscriptionEndDate: +moment().startOf('day').add(1, 'year').add(1, 'day') + } + } + getSettings () { + if (this.isValid()) { + return this.state + } else { + return null + } + } + isValid () { + if (this.state.name) { + return true + } else { + return false + } + } + save = () => { + if (!this.props.organization) { + this.props.createOrganization(this.state) + } else { + this.props.saveOrganization(this.props.organization) + } + } + handleChange = (evt) => { + const change = {} + console.log(evt.target.name) + change[evt.target.name] = evt.target.value + return this.setState(change) + } + handleDateChange (name, value) { + return this.setState({[name]: +value}) + } + render () { + console.log(this.state) + const { organization, projects } = this.props + const extensions = [ + 'GTFS_PLUS', + 'DEPLOYMENT', + 'VALIDATOR', + 'ALERTS', + 'SIGN_CONFIG' + ] + const messages = getComponentMessages('OrganizationSettings') + const orgFields = ['name', 'logoUrl'] + const projectToOption = project => ({label: project.name, value: project.id, project}) + const extensionToOption = e => ({value: e, label: toSentenceCase(e.replace('_', ' '))}) + const USAGE_TIERS = [ + { + label: getMessage(messages, 'usageTier.low'), + value: 'LOW' + }, + { + label: getMessage(messages, 'usageTier.medium'), + value: 'MEDIUM' + }, + { + label: getMessage(messages, 'usageTier.high'), + value: 'HIGH' + } + ] + return ( + + +

{getMessage(messages, 'orgDetails')}

+ {orgFields.map((f, index) => ( + + {getMessage(messages, `${f}.label`)} + + + ))} + + {getMessage(messages, 'projects')} + { + console.log(values) + this.setState({extensions: values && values.map(v => v.value) || []}) + }} + /> + + +
+ + + ) + } +} diff --git a/lib/admin/components/UserAdmin.js b/lib/admin/components/UserAdmin.js new file mode 100644 index 000000000..d7b2258e7 --- /dev/null +++ b/lib/admin/components/UserAdmin.js @@ -0,0 +1,144 @@ +import React, { Component, PropTypes } from 'react' +import { Grid, Row, Col, Panel, ListGroup, ListGroupItem, Button } from 'react-bootstrap' +import { LinkContainer } from 'react-router-bootstrap' +import Helmet from 'react-helmet' +import {Icon} from '@conveyal/woonerf' + +import ManagerPage from '../../common/components/ManagerPage' +import UserList from './UserList' +import OrganizationList from './OrganizationList' +import { getComponentMessages, getMessage, isModuleEnabled } from '../../common/util/config' + +export default class UserAdmin extends Component { + static propTypes = { + user: PropTypes.object, + users: PropTypes.object, + + onComponentMount: PropTypes.func, + setUserPermission: PropTypes.func, + createUser: PropTypes.func, + saveUser: PropTypes.func, + deleteUser: PropTypes.func, + setPage: PropTypes.func, + userSearch: PropTypes.func, + fetchProjectFeeds: PropTypes.func, + + admin: PropTypes.object, + activeComponent: PropTypes.string, + organizations: PropTypes.object, + projects: PropTypes.array + + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + render () { + const { + user, + users, + organizations, + projects, + activeComponent, + setUserPermission, + saveUser, + deleteUser, + fetchProjectFeeds, + createUser, + setPage, + userSearch + } = this.props + const messages = getComponentMessages('UserAdmin') + const isAdmin = user && user.permissions && (user.permissions.isApplicationAdmin() || user.permissions.canAdministerAnOrganization()) + const isApplicationAdmin = user.permissions.isApplicationAdmin() + return ( + + + + + +

+ + + + {getMessage(messages, 'title')} +

+ +
+ + {isAdmin + ?
+ + + + User management + {/* Do not show non-appAdmin users these application-level settings */} + {!isModuleEnabled('enterprise') && isApplicationAdmin && Organizations} + {isApplicationAdmin && Application logs} + {/* + Regions + */} + + + + + { + users.data && + projects && + organizations.data && + activeComponent === 'users' + ? + : activeComponent === 'logs' + ?

+ +

+ : activeComponent === 'organizations' && isApplicationAdmin && !isModuleEnabled('enterprise') + ? + : null + } + +
+ :
+ {user + ?

{getMessage(messages, 'noAccess')}

+ :

+ +

+ } +
+ } +
+
+
+ ) + } +} diff --git a/lib/admin/components/UserList.js b/lib/admin/components/UserList.js new file mode 100644 index 000000000..0ce33a03a --- /dev/null +++ b/lib/admin/components/UserList.js @@ -0,0 +1,306 @@ +import React, { PropTypes, Component} from 'react' +import ReactDOM from 'react-dom' +import { Panel, Row, Col, Button, Label, ButtonGroup, InputGroup, FormControl, Image, ListGroup, ListGroupItem } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +// import Select from 'react-select' + +import CreateUser from './CreateUser' +import ConfirmModal from '../../common/components/ConfirmModal' +import UserSettings from './UserSettings' +import UserPermissions from '../../common/user/UserPermissions' +import { getComponentMessages, getMessage } from '../../common/util/config' + +export default class UserList extends Component { + static propTypes = { + userSearch: PropTypes.func, + page: PropTypes.number, + perPage: PropTypes.number, + userCount: PropTypes.number, + projects: PropTypes.array, + fetchProjectFeeds: PropTypes.func, + createUser: PropTypes.func, + setPage: PropTypes.func, + isFetching: PropTypes.bool, + users: PropTypes.array, + setUserPermission: PropTypes.func, + saveUser: PropTypes.func, + deleteUser: PropTypes.func, + token: PropTypes.string + } + constructor (props) { + super(props) + this.state = {} + } + userSearch () { + this.props.userSearch(ReactDOM.findDOMNode(this.refs.searchInput).value) + } + + render () { + const { + creatingUser, + organizations, + page, + perPage, + userCount, + setPage, + userSearch, + projects, + fetchProjectFeeds, + createUser, + isFetching, + users, + saveUser, + deleteUser, + token + } = this.props + const headerStyle = { + fontSize: '18px', + marginLeft: '12px' + } + + const messages = getComponentMessages('UserList') + const minUserIndex = page * perPage + 1 + const maxUserIndex = Math.min((page + 1) * perPage, userCount) + const maxPage = Math.ceil(userCount / perPage) - 1 + // const isApplicationAdmin = creatingUser.permissions.isApplicationAdmin() + // const orgToOption = organization => ({organization, value: organization.id, label: organization.name}) + return ( +
+ + + + + + + {userCount > 0 + ? {getMessage(messages, 'showing')} {minUserIndex } - {maxUserIndex} {getMessage(messages, 'of')} {userCount} + : (No results to show) + } + + + + + + { + if (e.keyCode === 13) this.userSearch() + }} /> + + { + ReactDOM.findDOMNode(this.refs.searchInput).value = '' + userSearch('') + }} /> + + + + {/* TODO: add filter for organization */} + {/* isApplicationAdmin && + + + {this.state.organization && + this.orgAdminClicked(evt.target.checked, orgProjects, this.state.organization)} + ref='orgAdminCheckbox' + > + {getMessage(messages, 'org.admin')} + + } +
+ : null + } + + + + {this.state.appAdminChecked + ? {getMessage(messages, 'admin.description')} + : this.state.orgAdminChecked + ? {getMessage(messages, 'org.description')} + : projectPanel + } + + + ) + } +} + +class ProjectSettings extends Component { + constructor (props) { + super(props) + this.state = { + projectSettings: this.props.settings + } + } + static propTypes = { + project: PropTypes.object.isRequired, + settings: PropTypes.object.isRequired, + visible: PropTypes.bool, + + fetchProjectFeeds: PropTypes.func, + projectAccessUpdated: PropTypes.func, + projectFeedsUpdated: PropTypes.func, + projectPermissionsUpdated: PropTypes.func + } + componentWillMount () { + if (!this.props.project.feedSources) { + console.log('fetchingFeeds for ' + this.props.project.id) + this.props.fetchProjectFeeds(this.props.project.id) + } + } + setAccess (access) { + console.log(access) + this.props.projectAccessUpdated(this.props.project.id, access) + } + refCallback (ref) { + this.inputRef = ref + } + feedsUpdated () { + const selectedFeeds = [] + this.props.project.feedSources.forEach((feed) => { + var checkbox = this['feed-' + feed.id] + if (checkbox.checked) selectedFeeds.push(feed.id) + }) + this.props.projectFeedsUpdated(this.props.project.id, selectedFeeds) + } + permissionsUpdated () { + const selectedPermissions = [] + allPermissions.forEach((permission) => { + var checkbox = this['permission-' + permission.type] + if (checkbox.checked) selectedPermissions.push(permission.type) + }) + this.props.projectPermissionsUpdated(this.props.project.id, selectedPermissions) + } + render () { + const { + project, + visible, + settings + } = this.props + const messages = getComponentMessages('UserSettings') + + let feedSources = project.feedSources + if (feedSources) { + feedSources = feedSources.slice(0).sort((a, b) => { + if (a.name < b.name) return -1 + if (a.name > b.name) return 1 + return 0 + }) + } + return ( + + + + + + + + + + + + + + {settings.access === 'custom' ? ( + + +

{getMessage(messages, 'project.feeds')}

+ {feedSources + ? feedSources.map((feed, i) => { + const name = (feed.name === '') ? '(unnamed feed)' : feed.name + const refName = 'feed-' + feed.id + const checked = settings.defaultFeeds.indexOf(feed.id) !== -1 + return { this[refName] = ref }} + key={feed.id} + checked={checked} + onChange={() => this.feedsUpdated()} + > + {name} + + }) + : getMessage(messages, 'project.cannotFetchFeeds') + } + + +

{getMessage(messages, 'project.permissions')}

+ {allPermissions.map((permission, i) => { + const refName = 'permission-' + permission.type + const checked = settings.permissions.indexOf(permission.type) !== -1 + return { this[refName] = ref }} + key={permission.type} + checked={checked} + onChange={() => this.permissionsUpdated()} + > + {permission.name} + + })} + +
+ ) : ''} + +
+ ) + } +} diff --git a/src/main/client/admin/components/permissions.js b/lib/admin/components/permissions.js similarity index 98% rename from src/main/client/admin/components/permissions.js rename to lib/admin/components/permissions.js index 6066bcfb1..64ba472c0 100644 --- a/src/main/client/admin/components/permissions.js +++ b/lib/admin/components/permissions.js @@ -1,9 +1,9 @@ export default [ - /*{ + /* { type: 'administer-project', name: 'Administer Project', feedSpecific: false - },*/ + }, */ { type: 'manage-feed', name: 'Manage Feed Configuration', diff --git a/src/main/client/admin/containers/ActiveUserAdmin.js b/lib/admin/containers/ActiveUserAdmin.js similarity index 70% rename from src/main/client/admin/containers/ActiveUserAdmin.js rename to lib/admin/containers/ActiveUserAdmin.js index 49cbe029a..dd9f0c7de 100644 --- a/src/main/client/admin/containers/ActiveUserAdmin.js +++ b/lib/admin/containers/ActiveUserAdmin.js @@ -1,10 +1,7 @@ -import React from 'react' import { connect } from 'react-redux' import { browserHistory } from 'react-router' import UserAdmin from '../components/UserAdmin' -import { setVisibilitySearchText } from '../../manager/actions/visibilityFilter' - import { fetchUsers, createUser, @@ -12,7 +9,7 @@ import { setUserPage, setUserQueryString } from '../actions/admin' - +import { fetchOrganizations, createOrganization, updateOrganization, deleteOrganization } from '../actions/organizations' import { updateUserData } from '../../manager/actions/user' import { fetchProjects } from '../../manager/actions/projects' import { fetchProjectFeeds } from '../../manager/actions/feeds' @@ -21,7 +18,8 @@ const mapStateToProps = (state, ownProps) => { return { projects: state.projects.all, user: state.user, - admin: state.admin, + users: state.admin.users, + organizations: state.admin.organizations, activeComponent: ownProps.routeParams.subpage } } @@ -32,25 +30,28 @@ const mapDispatchToProps = (dispatch, ownProps) => { if (!initialProps.activeComponent) { browserHistory.push('/admin/users') } - if (!initialProps.users) + if (!initialProps.users.data) { dispatch(fetchUsers()) + } // always load projects to prevent interference with public feeds viewer loading of projects dispatch(fetchProjects()) + + // load orgs because they're needed both in org and user creation + dispatch(fetchOrganizations()) }, fetchProjectFeeds: (project) => { dispatch(fetchProjectFeeds(project)) }, + fetchOrganizations: () => { dispatch(fetchOrganizations()) }, + createOrganization: (org) => { return dispatch(createOrganization(org)) }, + deleteOrganization: (org) => { return dispatch(deleteOrganization(org)) }, + updateOrganization: (org, settings) => { return dispatch(updateOrganization(org, settings)) }, saveUser: (user, permissions) => { dispatch(updateUserData(user, permissions)) .then(() => { dispatch(fetchUsers()) }) }, - deleteUser: (user) => { - dispatch(deleteUser(user)) - .then(() => { - dispatch(fetchUsers()) - }) - }, + deleteUser: (user) => { dispatch(deleteUser(user)) }, // setUserPermission: (user, permissions) => { dispatch(setUserPermission(user, permissions)) }, createUser: (credentials) => { dispatch(createUser(credentials)) }, setPage: (page) => { diff --git a/lib/admin/reducers/index.js b/lib/admin/reducers/index.js new file mode 100644 index 000000000..cf5772222 --- /dev/null +++ b/lib/admin/reducers/index.js @@ -0,0 +1,9 @@ +import { combineReducers } from 'redux' + +import users from './users' +import organizations from './organizations' + +export default combineReducers({ + users, + organizations +}) diff --git a/lib/admin/reducers/organizations.js b/lib/admin/reducers/organizations.js new file mode 100644 index 000000000..6954864b8 --- /dev/null +++ b/lib/admin/reducers/organizations.js @@ -0,0 +1,29 @@ +import update from 'react-addons-update' + +const users = (state = { + isFetching: false, + data: null, + userQueryString: null +}, action) => { + switch (action.type) { + case 'REQUESTING_ORGANIZATIONS': + return update(state, {isFetching: { $set: true }}) + case 'RECEIVE_ORGANIZATIONS': + return update(state, { + isFetching: { $set: false }, + data: { $set: action.organizations }, + userCount: { $set: action.totalUserCount } + }) + case 'CREATED_ORGANIZATION': + if (state.data) { + return update(state, {data: { $push: [action.organization] }}) + } + break + case 'SET_ORGANIZATION_QUERY_STRING': + return update(state, {userQueryString: { $set: action.queryString }}) + default: + return state + } +} + +export default users diff --git a/src/main/client/admin/reducers/admin.js b/lib/admin/reducers/users.js similarity index 54% rename from src/main/client/admin/reducers/admin.js rename to lib/admin/reducers/users.js index f700bd298..5d5d4e8e7 100644 --- a/src/main/client/admin/reducers/admin.js +++ b/lib/admin/reducers/users.js @@ -1,8 +1,8 @@ import update from 'react-addons-update' -const admin = (state = { +const users = (state = { isFetching: false, - users: null, + data: null, userCount: 0, page: 0, perPage: 10, @@ -10,23 +10,25 @@ const admin = (state = { }, action) => { switch (action.type) { case 'REQUESTING_USERS': - return update(state, { isFetching: { $set: true }}) + return update(state, {isFetching: { $set: true }}) case 'RECEIVE_USERS': return update(state, { isFetching: { $set: false }, - users: { $set: action.users }, + data: { $set: action.users }, userCount: { $set: action.totalUserCount } }) case 'CREATED_USER': - if (state.users) - return update(state, { users: { $push: [action.profile] }}) + if (state.data) { + return update(state, {data: { $push: [action.profile] }}) + } + break case 'SET_USER_PAGE': - return update(state, { page: { $set: action.page }}) + return update(state, {page: { $set: action.page }}) case 'SET_USER_QUERY_STRING': - return update(state, { userQueryString: { $set: action.queryString }}) + return update(state, {userQueryString: { $set: action.queryString }}) default: return state } } -export default admin +export default users diff --git a/src/main/client/alerts/actions/activeAlert.js b/lib/alerts/actions/activeAlert.js similarity index 97% rename from src/main/client/alerts/actions/activeAlert.js rename to lib/alerts/actions/activeAlert.js index d43180207..d7bfd7707 100644 --- a/src/main/client/alerts/actions/activeAlert.js +++ b/lib/alerts/actions/activeAlert.js @@ -58,7 +58,7 @@ export const setActivePublished = (published) => { let nextEntityId = 0 export const addActiveEntity = (field = 'AGENCY', value = null, agency = null, newEntityId = 0) => { nextEntityId++ - let newEntity = { + const newEntity = { type: 'ADD_ACTIVE_ALERT_AFFECTED_ENTITY', entity: { id: newEntityId || nextEntityId, @@ -66,7 +66,7 @@ export const addActiveEntity = (field = 'AGENCY', value = null, agency = null, n } } // set agency of new entity - if (agency){ + if (agency) { newEntity.entity.agency = agency } newEntity.entity[field.toLowerCase()] = value diff --git a/src/main/client/alerts/actions/alerts.js b/lib/alerts/actions/alerts.js similarity index 53% rename from src/main/client/alerts/actions/alerts.js rename to lib/alerts/actions/alerts.js index 5d5bd6bd1..0016d1fcf 100644 --- a/src/main/client/alerts/actions/alerts.js +++ b/lib/alerts/actions/alerts.js @@ -1,10 +1,10 @@ -import { push } from 'react-router-redux' import { browserHistory } from 'react-router' +import fetch from 'isomorphic-fetch' +import { fetchStopsAndRoutes } from '../../gtfs/actions/general' import { secureFetch } from '../../common/util/util' import { getAlertsUrl, getFeedId } from '../../common/util/modules' import { setErrorMessage } from '../../manager/actions/status' -import moment from 'moment' // alerts management action @@ -14,18 +14,19 @@ let nextStopEntityId = 100 export function createAlert (entity, agency) { return function (dispatch, getState) { nextAlertId-- - let entities = [] + const entities = [] if (entity) { nextStopEntityId++ - let type = typeof entity.stop_id !== 'undefined' ? 'STOP' : 'ROUTE' - let newEntity = { + const type = typeof entity.stop_id !== 'undefined' ? 'STOP' : 'ROUTE' + const newEntity = { id: nextStopEntityId, type: type } - if (agency !== null) + if (agency !== null) { newEntity.agency = agency + } const typeKey = type.toLowerCase() newEntity[typeKey] = entity @@ -36,56 +37,15 @@ export function createAlert (entity, agency) { id: nextAlertId, title: '', // 'New Alert', affectedEntities: entities, - published: false, - // start: moment().unix()*1000, - // end: moment().add(30, 'day').unix()*1000 + published: false } - browserHistory.push('/alerts/new') dispatch(updateActiveAlert(alert)) + browserHistory.push('/alerts/new') } } -/*export const createAlert = (entity) => { - nextAlertId-- - let entities = [] - if (entity) { - nextStopEntityId++ - let type = typeof entity.stop_id !== 'undefined' ? 'STOP' : 'ROUTE' - let newEntity = { - id: nextStopEntityId, - type: type - } - const typeKey = type.toLowerCase() - newEntity[typeKey] = entity - entities.push(newEntity) - } - return { - type: 'CREATE_ALERT', - alert: { - id: nextAlertId, - title: 'New Alert', - affectedEntities: entities, - published: false - } - } -}*/ - -/*export const saveAlert = (alert) => { - return { - type: 'SAVE_ALERT', - alert - } -}*/ - -/*export const editAlert = (alert) => { - return { - type: 'EDIT_ALERT', - alert - } -}*/ - -export const deleteAlert = (alert) => { - return function (dispatch, getState){ +export function deleteAlert (alert) { + return function (dispatch, getState) { console.log('deleting', alert) const user = getState().user const url = getAlertsUrl() + '/' + alert.id @@ -99,28 +59,24 @@ export const deleteAlert = (alert) => { 'Authorization': 'Bearer ' + user.token } }).then((res) => { - console.log('status='+res.status) + if (res.status >= 300) { + dispatch(setErrorMessage('Failed to delete alert!')) + return + } browserHistory.push('/alerts') dispatch(fetchRtdAlerts()) + }).catch(e => { + console.log(e) }) } } export const requestRtdAlerts = () => { return { - type: 'REQUEST_RTD_ALERTS', + type: 'REQUEST_RTD_ALERTS' } } -export const receivedGtfsEntities = (gtfsObjects, gtfsAlerts) => { - return { - type: 'RECEIVED_ALERT_GTFS_ENTITIES', - gtfsObjects, - gtfsAlerts - } -} - - export const receivedRtdAlerts = (rtdAlerts, activeProject) => { return { type: 'RECEIVED_RTD_ALERTS', @@ -140,7 +96,7 @@ export function fetchRtdAlerts () { return function (dispatch, getState) { dispatch(requestRtdAlerts()) return secureFetch(getAlertsUrl(), getState()).then((res) => { - if (res.status >= 400) { + if (!res || res.status >= 400) { dispatch(setErrorMessage('Error fetching alerts!')) return [] } @@ -148,70 +104,10 @@ export function fetchRtdAlerts () { }).then((alerts) => { return dispatch(receivedRtdAlerts(alerts, getState().projects.active)) }).then(() => { - let feed = getState().projects.active - const fetchFunctions = getState().alerts.entities.map((entity) => { - return fetchEntity(entity, feed) - }) - return Promise.all(fetchFunctions) - .then((results) => { - let newEntities = getState().alerts.entities - for (var i = 0; i < newEntities.length; i++) { - newEntities[i].gtfs = results[i] - } - dispatch(receivedGtfsEntities(newEntities, getState().alerts.all)) - }).then((error) => { - console.log('error', error) - }) - + return dispatch(fetchStopsAndRoutes(getState().alerts.entities, 'ALERTS')) }) } } -// TODO: implement method for single alert fetch -// export const requestRtdAlert = () => { -// return { -// type: 'REQUEST_RTD_ALERT', -// } -// } -// -// export const receivedRtdAlert = (rtdAlerts, activeProject) => { -// return { -// type: 'RECEIVED_RTD_ALERT', -// rtdAlerts, -// activeProject -// } -// } -// -// export function fetchRtdAlert(alertId) { -// return function (dispatch, getState) { -// dispatch(requestRtdAlert()) -// return fetch(getAlertsUrl() + '/' + alertId).then((res) => { -// return res.json() -// }).then((alert) => { -// const project = getState().projects.active -// return dispatch(receivedRtdAlerts([alert], project)) -// }).then(() => { -// let feed = getState().projects.active -// const fetchFunctions = getState().alerts.entities.map((entity) => { -// return fetchEntity(entity, feed) -// }) -// return Promise.all(fetchFunctions) -// .then((results) => { -// let newEntities = getState().alerts.entities -// for (var i = 0; i < newEntities.length; i++) { -// newEntities[i].gtfs = results[i] -// } -// const alerts = getState().alerts.all -// const alert = alerts.find(a => a.id === +alertId) -// dispatch(receivedGtfsEntities(newEntities, alerts)) -// console.log('this alert', alert) -// dispatch(updateActiveAlert(alert)) -// }).then((error) => { -// console.log('error', error) -// }) -// -// }) -// } -// } export const updateActiveAlert = (alert) => { return { @@ -220,15 +116,14 @@ export const updateActiveAlert = (alert) => { } } -export function editAlert(alert) { +export function editAlert (alert) { return function (dispatch, getState) { dispatch(updateActiveAlert(alert)) - browserHistory.push('/alerts/alert/'+alert.id) + browserHistory.push(`/alerts/alert/${alert.id}`) } } -export function fetchEntity(entity, activeProject) { - console.log() +export function fetchEntity (entity, activeProject) { const feed = activeProject.feedSources.find(f => getFeedId(f) === entity.entity.AgencyId) const feedId = getFeedId(feed) const url = entity.type === 'stop' ? `/api/manager/stops/${entity.entity.StopId}?feed=${feedId}` : `/api/manager/routes/${entity.entity.RouteId}?feed=${feedId}` @@ -239,11 +134,11 @@ export function fetchEntity(entity, activeProject) { .then((object) => { return object }).catch((error) => { - // console.log('caught', error) + console.log('caught', error) }) } -export function saveAlert(alert) { +export function saveAlert (alert) { return function (dispatch, getState) { console.log('saving...') const user = getState().user @@ -255,10 +150,9 @@ export function saveAlert(alert) { Cause: alert.cause || 'UNKNOWN_CAUSE', Effect: alert.effect || 'UNKNOWN_EFFECT', Published: alert.published ? 'Yes' : 'No', - StartDateTime: alert.start/1000 || 0, - EndDateTime: alert.end/1000 || 0, + StartDateTime: alert.start / 1000 || 0, + EndDateTime: alert.end / 1000 || 0, ServiceAlertEntities: alert.affectedEntities.map((entity) => { - console.log('ent', entity) return { Id: entity.id, AlertId: alert.id, @@ -273,7 +167,6 @@ export function saveAlert(alert) { } }) } - console.log('saving', alert.id, json) const url = getAlertsUrl() + (alert.id < 0 ? '' : '/' + alert.id) const method = alert.id < 0 ? 'post' : 'put' @@ -287,9 +180,15 @@ export function saveAlert(alert) { }, body: JSON.stringify(json) }).then((res) => { - console.log('status='+res.status) + console.log('status=' + res.status) + if (res.status >= 300) { + dispatch(setErrorMessage('Failed to save alert!')) + return + } browserHistory.push('/alerts') dispatch(fetchRtdAlerts()) + }).catch(e => { + console.log(e) }) } } diff --git a/src/main/client/alerts/actions/projects.js b/lib/alerts/actions/projects.js similarity index 89% rename from src/main/client/alerts/actions/projects.js rename to lib/alerts/actions/projects.js index 5644352c5..65ddd6a46 100644 --- a/src/main/client/alerts/actions/projects.js +++ b/lib/alerts/actions/projects.js @@ -1,6 +1,6 @@ import { secureFetch } from '../../common/util/util' -import { updateGtfsFilter } from '../../gtfs/actions/gtfsFilter' +import { updateGtfsFilter } from '../../gtfs/actions/filter' import { fetchRtdAlerts } from './alerts' import { fetchProjectFeeds } from '../../manager/actions/feeds' import { setActiveProject } from '../../manager/actions/projects' @@ -26,7 +26,6 @@ export function fetchProjects () { return secureFetch('/api/manager/secure/project', getState()) .then(response => response.json()) .then((projects) => { - console.log('received projects', projects) dispatch(receiveProjects(projects)) if (getState().projects.active) { project = getState().projects.active @@ -38,7 +37,6 @@ export function fetchProjects () { return dispatch(fetchProjectFeeds(project.id)) }) .then(() => { - console.log('updating filter') dispatch(updateGtfsFilter(getState().projects.active, getState().user)) return dispatch(fetchRtdAlerts()) }) diff --git a/src/main/client/alerts/actions/visibilityFilter.js b/lib/alerts/actions/visibilityFilter.js similarity index 55% rename from src/main/client/alerts/actions/visibilityFilter.js rename to lib/alerts/actions/visibilityFilter.js index efd8abb29..97897c0ed 100644 --- a/src/main/client/alerts/actions/visibilityFilter.js +++ b/lib/alerts/actions/visibilityFilter.js @@ -13,3 +13,17 @@ export const setVisibilityFilter = (filter) => { filter } } + +export const setAlertAgencyFilter = (feedId) => { + return { + type: 'SET_ALERT_AGENCY_FILTER', + feedId + } +} + +export const setAlertSort = (sort) => { + return { + type: 'SET_ALERT_SORT', + sort + } +} diff --git a/lib/alerts/components/AffectedEntity.js b/lib/alerts/components/AffectedEntity.js new file mode 100644 index 000000000..af1288159 --- /dev/null +++ b/lib/alerts/components/AffectedEntity.js @@ -0,0 +1,199 @@ +import React, { Component } from 'react' +import {Icon} from '@conveyal/woonerf' + +import { ListGroupItem, Row, Col, Button, Collapse, Glyphicon, Label } from 'react-bootstrap' +import { getFeed } from '../../common/util/modules' +import { getRouteNameAlerts } from '../../editor/util/gtfs' +import AgencySelector from './AgencySelector' +import ModeSelector from './ModeSelector' +import StopSelector from './StopSelector' +import RouteSelector from './RouteSelector' + +export default class AffectedEntity extends Component { + constructor (props) { + super(props) + this.state = { + active: false + } + } + getEntitySummary (entity) { + const type = entity.type + const val = entity[type.toLowerCase()] + let agencyName = '' + if (typeof entity.agency !== 'undefined' && entity.agency !== null) { + agencyName = entity.agency.name + } else if (typeof entity.stop !== 'undefined' && entity.stop !== null) { + const feed = getFeed(this.props.feeds, entity.stop.feed_id) + agencyName = typeof feed !== 'undefined' ? feed.name : 'Unknown agency' + } + const routeName = entity.route ? getRouteNameAlerts(entity.route) : entity.route_id + const stopName = entity.stop + ? `${entity.stop.stop_name} (${entity.stop.stop_id}) ${agencyName}` + : entity.stop_id + let summary = '' + switch (type) { + case 'AGENCY' : + return ( + + {' '} + {agencyName}
+ Note: this selection will apply to all stops and routes for {agencyName}. +
+ ) + case 'STOP' : + summary = stopName + if (routeName) { + summary += ` for ${routeName}` + } + return {summary} + case 'ROUTE' : + summary = routeName + if (stopName) { + summary += ` at ${stopName}` + } + return {summary} + case 'MODE' : + summary = val.name + if (stopName) { + summary += ` at ${stopName}` + } + return ( + + {type}: {summary}
+ Note: this selection will apply to all {val.name.toLowerCase()} routes{stopName && ` stopping at ${stopName}`}. +
+ ) + } + } + renderHeader () { + return ( + this.setState({active: !this.state.active})} + style={{cursor: 'pointer'}} + > + +
+ + {this.getEntitySummary(this.props.entity)} +
+ + + + +
+ ) + } + renderEntity () { + var indent = { + paddingLeft: '30px' + } + const selectedFeeds = [this.props.entity.agency] || this.props.activeFeeds + const selectedRoute = this.props.entity.route + const selectedStop = this.props.entity.stop + switch (this.props.entity.type) { + case 'AGENCY': + return ( +
+ Agency: + +
+ ) + case 'MODE': + return ( +
+ Mode: + +
+ Refine by Agency: + + Refine by Stop: + +
+
+ ) + case 'STOP': + return ( +
+ Stop: + +
+ Refine by Route: + +
+
+ ) + case 'ROUTE': + return ( +
+ Route: + +
+ Refine by Stop: + +
+
+ ) + } + } + render () { + return ( + + + {this.renderEntity()} + + + ) + } +} diff --git a/lib/alerts/components/AffectedServices.js b/lib/alerts/components/AffectedServices.js new file mode 100644 index 000000000..9cb90d977 --- /dev/null +++ b/lib/alerts/components/AffectedServices.js @@ -0,0 +1,113 @@ +import React, {Component} from 'react' +import { Panel, Label, ListGroup, ListGroupItem, Row, Col, Button } from 'react-bootstrap' + +import AffectedEntity from './AffectedEntity' +import { isExtensionEnabled } from '../../common/util/config' +import GtfsSearch from '../../gtfs/components/gtfssearch' + +export default class AffectedServices extends Component { + render () { + const { sortedFeeds, onAddEntityClick, newEntityId, alert, activeFeeds, onDeleteEntityClick, entityUpdated } = this.props + return ( + } + > + + + + + + {!isExtensionEnabled('mtc') && + + } + + + { + console.log('we need to add this entity to the store', evt) + if (typeof evt !== 'undefined' && evt !== null) { + if (evt.stop) { + onAddEntityClick('STOP', evt.stop, evt.agency, newEntityId) + } else if (evt.route) { + onAddEntityClick('ROUTE', evt.route, evt.agency, newEntityId) + } + } + }} + /> + + + + {alert.affectedEntities + .sort((a, b) => b.id - a.id) // reverse sort by entity id + .map((entity) => { + return ( + + ) + })} + + + ) + } +} + +class ServicesHeader extends Component { + render () { + const { entities } = this.props + const counts = [ + { + singular: 'agency', + plural: 'agencies', + count: entities.filter(e => e.type === 'AGENCY').length + }, + { + singular: 'route', + plural: 'routes', + count: entities.filter(e => e.type === 'ROUTE').length + }, + { + singular: 'stop', + plural: 'stops', + count: entities.filter(e => e.type === 'STOP').length + }, + { + singular: 'mode', + plural: 'modes', + count: entities.filter(e => e.type === 'MODE').length + } + ] + const summary = counts.map(c => { + return c.count + ? + : null + }).filter(c => c !== null) + return ( + + Alert applies to:{' '} + {summary.length ? summary : [make selection below]} + + ) + } +} diff --git a/lib/alerts/components/AgencySelector.js b/lib/alerts/components/AgencySelector.js new file mode 100644 index 000000000..f3ffd42f9 --- /dev/null +++ b/lib/alerts/components/AgencySelector.js @@ -0,0 +1,30 @@ +import React, { Component, PropTypes } from 'react' +import { FormControl } from 'react-bootstrap' + +import { getFeed, getFeedId } from '../../common/util/modules' + +export default class AgencySelector extends Component { + static propTypes = { + feeds: PropTypes.array, + entity: PropTypes.object, + entityUpdated: PropTypes.func + } + render () { + const { feeds, entity, entityUpdated } = this.props + return ( +
+ { + entityUpdated(entity, 'AGENCY', getFeed(feeds, evt.target.value)) + }} + > + {feeds.map((feed) => { + return + })} + +
+ ) + } +} diff --git a/lib/alerts/components/AlertEditor.js b/lib/alerts/components/AlertEditor.js new file mode 100644 index 000000000..d4239a5be --- /dev/null +++ b/lib/alerts/components/AlertEditor.js @@ -0,0 +1,288 @@ +import {Icon} from '@conveyal/woonerf' +import React from 'react' +import Helmet from 'react-helmet' +import { sentence as toSentenceCase } from 'change-case' +import { Grid, Row, Col, ButtonToolbar, Button, FormControl, ControlLabel, FormGroup } from 'react-bootstrap' +import DateTimeField from 'react-bootstrap-datetimepicker' +import Toggle from 'react-toggle' +// import Switch from 'rc-switch' + +import ManagerPage from '../../common/components/ManagerPage' +import Loading from '../../common/components/Loading' +import AffectedServices from './AffectedServices' +import GtfsMapSearch from '../../gtfs/components/gtfsmapsearch' +import GlobalGtfsFilter from '../../gtfs/containers/GlobalGtfsFilter' + +import { checkEntitiesForFeeds } from '../../common/util/permissions' +import { CAUSES, EFFECTS } from '../util' +import { browserHistory } from 'react-router' + +import moment from 'moment' + +export default class AlertEditor extends React.Component { + componentWillMount () { + this.props.onComponentMount(this.props) + } + validateAndSave = () => { + console.log('times', this.props.alert.end, this.props.alert.start) + if (!this.props.alert.title) { + window.alert('You must specify an alert title') + return + } + if (!this.props.alert.end || !this.props.alert.start) { + window.alert('Alert must have a start and end date') + return + } + if (this.props.alert.end < this.props.alert.start) { + window.alert('Alert end date cannot be before start date') + return + } + if (moment(this.props.alert.end).isBefore(moment())) { + window.alert('Alert end date cannot be before the current date') + return + } + if (this.props.alert.affectedEntities.length === 0) { + window.alert('You must specify at least one affected entity') + return + } + this.props.onSaveClick(this.props.alert) + } + render () { + const { + alert, + publishableFeeds, + editableFeeds, + onPublishClick, + onDeleteClick, + titleChanged, + startChanged, + endChanged, + causeChanged, + effectChanged, + descriptionChanged, + urlChanged, + activeFeeds, + editorStopClick, + editorRouteClick + } = this.props + if (!alert) { + return ( + + + + ) + } + var compare = function (a, b) { + var aName = a.shortName || a.name + var bName = b.shortName || b.name + // return 511 Staff as first in list to avoid 511 Emergency being first in list + if (/511 Staff/.test(aName)) return -1 + if (/511 Staff/.test(bName)) return 1 + if (aName < bName) return -1 + if (aName > bName) return 1 + return 0 + } + const canPublish = checkEntitiesForFeeds(alert.affectedEntities, publishableFeeds) + const canEdit = checkEntitiesForFeeds(alert.affectedEntities, editableFeeds) + + const editingIsDisabled = alert.published && !canPublish ? true : !canEdit + const sortedFeeds = editableFeeds.sort(compare) + // if user has edit rights and alert is unpublished, user can delete alert, else check if they have publish rights + const deleteIsDisabled = !editingIsDisabled && !alert.published ? false : !canPublish + const deleteButtonMessage = alert.published && deleteIsDisabled ? 'Cannot delete because alert is published' + : !canEdit ? 'Cannot alter alerts for other agencies' : 'Delete alert' + + const editButtonMessage = alert.published && deleteIsDisabled ? 'Cannot edit because alert is published' + : !canEdit ? 'Cannot alter alerts for other agencies' : 'Edit alert' + + const newEntityId = alert.affectedEntities && alert.affectedEntities.length + ? 1 + alert.affectedEntities.map(e => e.id).reduce((initial, current) => initial > current ? initial : current) + : 1 + + return ( + + 0 ? `Alert ${alert.id}` : 'New Alert'} + /> + + + + + + + + + + + + } + // unCheckedChildren={} + checked={alert.published} + onChange={(evt) => onPublishClick(alert, !alert.published)} /> + + + + + + + + + + + + Alert Title +
Note: alert title serves as text for eTID alerts. Use descriptive language so it can serve as a standalone alert.
+
+ titleChanged(evt.target.value)} + /> +
+ + +
Start
+ {alert.start + ? startChanged(time)} + /> + : startChanged(time)} + /> + } + + +
End
+ {alert.end + ? endChanged(time)} + /> + : endChanged(time)} + /> + } + +
+ + + + Cause + causeChanged(evt.target.value)} + value={alert.cause} + > + {CAUSES.map((cause) => { + return + })} + + + + + + Effect + effectChanged(evt.target.value)} + value={alert.effect} + > + {EFFECTS.map((effect) => { + return + })} + + + + + + + + Description + descriptionChanged(evt.target.value)} + /> + + + + + URL + urlChanged(evt.target.value)} + /> + + + + + + + + + + + + + + + + + + +
+
+
+ ) + } +} diff --git a/lib/alerts/components/AlertPreview.js b/lib/alerts/components/AlertPreview.js new file mode 100644 index 000000000..95b53a40c --- /dev/null +++ b/lib/alerts/components/AlertPreview.js @@ -0,0 +1,78 @@ +import React, {Component} from 'react' +import {Icon} from '@conveyal/woonerf' +import moment from 'moment' + +import { Panel, Row, Col, ButtonGroup, Button, Label } from 'react-bootstrap' +import { checkEntitiesForFeeds } from '../../common/util/permissions' + +export default class AlertPreview extends Component { + render () { + const { + alert, + publishableFeeds, + editableFeeds, + onEditClick, + onDeleteClick + } = this.props + const canPublish = checkEntitiesForFeeds(alert.affectedEntities, publishableFeeds) + const canEdit = checkEntitiesForFeeds(alert.affectedEntities, editableFeeds) + + const editingIsDisabled = alert.published && !canPublish ? true : !canEdit + + // if user has edit rights and alert is unpublished, user can delete alert, else check if they have publish rights + const deleteIsDisabled = !editingIsDisabled && !alert.published ? false : !canPublish + const deleteButtonMessage = alert.published && deleteIsDisabled ? 'Cannot delete because alert is published' + : !canEdit ? 'Cannot alter alerts for other agencies' : 'Delete alert' + + const editButtonMessage = alert.published && deleteIsDisabled ? 'Cannot edit because alert is published' + : !canEdit ? 'Cannot alter alerts for other agencies' : 'Edit alert' + const publishedLabel = alert.published + ? + : + const entitiesLabel = alert.affectedEntities.length + ? + : + const header = ( + + +

{alert.title}

+

ID: #{alert.id} {publishedLabel} {entitiesLabel}

+ + + + + + + +
+ ) + return ( + +

+ {moment(alert.start).format('MMM Do YYYY, h:mm:ssa')} to {moment(alert.end).format('MMM Do YYYY, h:mm:ssa')} + {publishedLabel} {alert.published ? 'Published' : 'Draft'} +

+

{alert.description}

+

URL: {alert.url}

+

+ {entitiesLabel} affected service(s) +

+
+ ) + } +} diff --git a/lib/alerts/components/AlertsList.js b/lib/alerts/components/AlertsList.js new file mode 100644 index 000000000..8f04344a0 --- /dev/null +++ b/lib/alerts/components/AlertsList.js @@ -0,0 +1,130 @@ +import React, { PropTypes, Component } from 'react' +import { Row, Col, ButtonGroup, Button, FormControl, FormGroup, Badge, ControlLabel } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import { sentence as toSentenceCase } from 'change-case' + +import AlertPreview from './AlertPreview' +import { FILTERS } from '../util' +import { getFeedId } from '../../common/util/modules' + +export default class AlertsList extends Component { + static propTypes = { + alerts: PropTypes.array, + visibilityFilter: PropTypes.object, + isFetching: PropTypes.bool, + editableFeeds: PropTypes.array, + publishableFeeds: PropTypes.array, + filterCounts: PropTypes.object, + + onEditClick: PropTypes.func, + onZoomClick: PropTypes.func, + onDeleteClick: PropTypes.func, + + searchTextChanged: PropTypes.func, + visibilityFilterChanged: PropTypes.func + } + render () { + // console.log(this.props) + var compare = function (a, b) { + var aName = a.shortName || a.name + var bName = b.shortName || b.name + if (aName < bName) return -1 + if (aName > bName) return 1 + return 0 + } + const sortedFeeds = this.props.editableFeeds.sort(compare) + return ( +
+ + + + this.props.searchTextChanged(evt.target.value)} + defaultValue={this.props.visibilityFilter.searchText} + /> + + + + {/* Alert filters */} + + + + + {FILTERS.map(f => ( + + ))} + + + + Sort by + {' '} + { + const values = evt.target.value.split(':') + const sort = { + type: values[0], + direction: values[1] + } + this.props.sortChanged(sort) + }} + > + + + + + + + + + + + + Agency + {' '} + this.props.agencyFilterChanged(evt.target.value)} + > + + {sortedFeeds.map(fs => ( + + ))} + + + + + {/* List of alerts */} + + + {this.props.isFetching + ?

+ : this.props.alerts.length + ? this.props.alerts.map((alert) => { + return + }) + :

No alerts found.

+ } + +
+
+ ) + } +} diff --git a/src/main/client/alerts/components/AlertsViewer.js b/lib/alerts/components/AlertsViewer.js similarity index 75% rename from src/main/client/alerts/components/AlertsViewer.js rename to lib/alerts/components/AlertsViewer.js index 770d8aa48..04f15a26f 100644 --- a/src/main/client/alerts/components/AlertsViewer.js +++ b/lib/alerts/components/AlertsViewer.js @@ -1,7 +1,7 @@ import React from 'react' import Helmet from 'react-helmet' - -import { Grid, Row, Col, Button } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import { Grid, Row, Col } from 'react-bootstrap' import ManagerPage from '../../common/components/ManagerPage' import CreateAlert from '../components/CreateAlert' @@ -9,33 +9,28 @@ import VisibleAlertsList from '../containers/VisibleAlertsList' import GlobalGtfsFilter from '../../gtfs/containers/GlobalGtfsFilter' import GtfsMapSearch from '../../gtfs/components/gtfsmapsearch' -import { Link } from 'react-router' - export default class AlertsViewer extends React.Component { - - constructor (props) { - super(props) - //console.log("AV activeFeeds", this.props.activeFeeds); - } - componentWillMount () { this.props.onComponentMount(this.props) } - render () { - const createDisabled = this.props.project && this.props.user ? !this.props.user.permissions.hasProjectPermission(this.props.project.id, 'edit-alert') : true + const createDisabled = this.props.project && this.props.user ? !this.props.user.permissions.hasProjectPermission(this.props.project.organizationId, this.props.project.id, 'edit-alert') : true return ( - + - +

+ Service Alerts + +

diff --git a/src/main/client/alerts/components/CreateAlert.js b/lib/alerts/components/CreateAlert.js similarity index 82% rename from src/main/client/alerts/components/CreateAlert.js rename to lib/alerts/components/CreateAlert.js index e4b39d68e..8cf737f45 100644 --- a/src/main/client/alerts/components/CreateAlert.js +++ b/lib/alerts/components/CreateAlert.js @@ -1,16 +1,13 @@ import React from 'react' -import { connect } from 'react-redux' import { Button } from 'react-bootstrap' export default class CreateAlert extends React.Component { - constructor (props) { - super(props) - } render () { return ( + diff --git a/lib/common/components/DatatoolsNavbar.js b/lib/common/components/DatatoolsNavbar.js new file mode 100644 index 000000000..734b350b6 --- /dev/null +++ b/lib/common/components/DatatoolsNavbar.js @@ -0,0 +1,117 @@ +import React, {Component, PropTypes} from 'react' +// import {Icon} from '@conveyal/woonerf' +import { Navbar, Nav, NavItem } from 'react-bootstrap' +// import { browserHistory } from 'react-router' + +// import Breadcrumbs from './Breadcrumbs' +// import { getComponentMessages, getMessage } from '../util/config' +// import JobMonitor from './JobMonitor' + +export default class DatatoolsNavbar extends Component { + + static propTypes = { + title: PropTypes.string, + username: PropTypes.string, + managerUrl: PropTypes.string, + editorUrl: PropTypes.string, + userAdminUrl: PropTypes.string, + alertsUrl: PropTypes.string, + signConfigUrl: PropTypes.string, + loginHandler: PropTypes.func, + logoutHandler: PropTypes.func, + resetPassword: PropTypes.func, + jobMonitor: PropTypes.object, + setJobMonitorVisible: PropTypes.func + }; + + render () { + // var userControl + // const messages = getComponentMessages('DatatoolsNavbar') + // + // if (!this.props.username) { + // userControl = ({getMessage(messages, 'login')}) + // } else { + // userControl = ( + // {this.props.username} + // } id='basic-nav-dropdown'> + // browserHistory.push('/account')}>{getMessage(messages, 'account')} + // {getMessage(messages, 'resetPassword')} + // {getMessage(messages, 'logout')} + // + // ) + // } + // let projectControl + // if (!this.props.username) { + // projectControl = ('') + // } else if (!this.props.projects || !this.props.projects.active) { + // projectControl = ('') // (No project selected) + // } else { + // let activeProject = this.props.projects.active + // projectControl = ( + // {activeProject.name}} + // id='basic-nav-dropdown' + // > + // {this.props.projects.all.map(proj => { + // return ( + // { + // evt.preventDefault() + // this.props.setActiveProject(proj) + // }} + // >{proj.name} + // ) + // })} + // + // ) + // } + // + // let languageControl = ( + // } + // id='basic-nav-dropdown' + // > + // {this.props.languages.all ? this.props.languages.all.map(lang => { + // return ( + // { + // evt.preventDefault() + // this.props.setActiveLanguage(lang.id) + // }} + // >{lang.name} + // ) + // }) + // : null + // } + // + // ) + + const navBarStyle = { + left: this.props.sidebarExpanded ? 130 : 50, + height: '40px' + } + + return (
+ + + + + + + {this.props.breadcrumbs} + + +
) + } +} diff --git a/src/main/client/common/components/EditableTextField.js b/lib/common/components/EditableTextField.js similarity index 51% rename from src/main/client/common/components/EditableTextField.js rename to lib/common/components/EditableTextField.js index 289435d14..5836558c0 100644 --- a/src/main/client/common/components/EditableTextField.js +++ b/lib/common/components/EditableTextField.js @@ -1,6 +1,7 @@ +import {Icon} from '@conveyal/woonerf' import React, {Component, PropTypes} from 'react' import ReactDOM from 'react-dom' -import { Form, FormControl, InputGroup, FormGroup, Glyphicon, Button } from 'react-bootstrap' +import { Form, FormControl, InputGroup, FormGroup, Button } from 'react-bootstrap' import { Link } from 'react-router' export default class EditableTextField extends Component { @@ -82,65 +83,73 @@ export default class EditableTextField extends Component { onClick={(evt) => { evt.preventDefault() this.save() - }} - > - + }}> + //feed.name.length > 11 ? feed.name.substr(0, 11) + '...' : feed.name ) const displayValue = this.props.maxLength !== null && this.state.value && this.state.value.length > this.props.maxLength ? this.state.value.substr(0, this.props.maxLength) + '...' : this.state.value + const style = { + ...this.props.style + } + const spanStyle = {} + if (this.props.inline) { + style.display = 'inline-block' + } + if (this.props.maxWidth) { + spanStyle.maxWidth = this.props.maxWidth + spanStyle.textOverflow = 'ellipsis' + spanStyle.whiteSpace = 'nowrap' + spanStyle.overflow = 'hidden' + } return ( -
+
{this.state.isEditing ?
- - - this.handleKeyDown(e)} - onFocus={(e) => e.target.select()} - defaultValue={ this.state.value } - /> - {saveButton} - - -
- + + + this.handleKeyDown(e)} + onFocus={(e) => e.target.select()} + defaultValue={this.state.value} + /> + {saveButton} + + + : - {this.props.link - ? {displayValue} - : displayValue || '(none)' - } - {this.props.hideEditButton - ? null - : - {' '} - - - } - + title={this.state.value} + style={spanStyle}> + {this.props.link + ? {displayValue} + : displayValue || '(none)' + } + {this.props.hideEditButton + ? null + : + {' '} + + + } + }
) diff --git a/src/main/client/common/components/InfoModal.js b/lib/common/components/InfoModal.js similarity index 87% rename from src/main/client/common/components/InfoModal.js rename to lib/common/components/InfoModal.js index f7b49c3ce..0331fedb1 100644 --- a/src/main/client/common/components/InfoModal.js +++ b/lib/common/components/InfoModal.js @@ -1,5 +1,5 @@ -import React from 'react' -import { Modal, Button, Glyphicon } from 'react-bootstrap' +import React from 'react' +import { Modal, Button } from 'react-bootstrap' export default class InfoModal extends React.Component { @@ -20,7 +20,7 @@ export default class InfoModal extends React.Component { this.setState({ showModal: true, title: props.title, - body: props.body, + body: props.body }) } diff --git a/lib/common/components/JobMonitor.js b/lib/common/components/JobMonitor.js new file mode 100644 index 000000000..6c3ff2111 --- /dev/null +++ b/lib/common/components/JobMonitor.js @@ -0,0 +1,92 @@ +import React, {PropTypes} from 'react' +import {ProgressBar, Button} from 'react-bootstrap' +import {Icon, Pure} from '@conveyal/woonerf' +import truncate from 'truncate' + +import SidebarPopover from './SidebarPopover' + +export default class JobMonitor extends Pure { + static propTypes = { + expanded: PropTypes.bool, + jobMonitor: PropTypes.object, + target: PropTypes.object, + visible: PropTypes.bool.isRequired, + close: PropTypes.func, + removeRetiredJob: PropTypes.func + } + removeJob (job) { + this.props.removeRetiredJob(job) + } + componentWillReceiveProps (nextProps) { + // TODO: fix resizing when jobs are removed (height of popover appears to be incorrect) + // if (nextProps.jobMonitor.retired.length !== this.props.jobMonitor.retired.length) { + // this.popover._onResize() + // } else if (nextProps.jobMonitor.jobs.length !== this.props.jobMonitor.jobs.length) { + // this.popover._onResize() + // } + } + render () { + const jobContainerStyle = { + marginBottom: 20 + } + + const progressBarStyle = { + marginBottom: 2 + } + + const statusMessageStyle = { + fontSize: '12px', + color: 'darkGray' + } + + return ( + { this.popover = SidebarPopover }} + title='Server Jobs' + {...this.props}> + {this.props.jobMonitor.retired.map(job => { + return ( +
+
+ {job.status && job.status.error + ? + : + } +
+
+
+ + {truncate(job.name, 25)} +
+
{job.status && job.status.error ? 'Error!' : 'Completed!'}
+
+
+ ) + })} + {this.props.jobMonitor.jobs.length + ? this.props.jobMonitor.jobs.map(job => ( +
+
+ +
+
+
+ {job.name} +
+ +
{job.status ? job.status.message : 'waiting'}
+
+
+ )) + :

No active jobs.

+ } +
+ ) + } +} diff --git a/lib/common/components/LanguageSelect.js b/lib/common/components/LanguageSelect.js new file mode 100644 index 000000000..c4d2fe3cf --- /dev/null +++ b/lib/common/components/LanguageSelect.js @@ -0,0 +1,46 @@ +import React from 'react' +import { shallowEqual } from 'react-pure-render' +import Select from 'react-select' +import ISO6391 from 'iso-639-1' + +import { getComponentMessages, getMessage } from '../util/config' + +export default class LanguageSelect extends React.Component { + constructor (props) { + super(props) + this.state = { + value: this.props.value + } + } + componentWillReceiveProps (nextProps) { + if (!shallowEqual(nextProps.value, this.props.value)) { + this.setState({value: nextProps.value}) + console.log('props received', this.state.value) + } + } + _onChange = (value) => { + this.setState({value}) + this.props.onChange && this.props.onChange(value) + } + _getOptions = () => { + return ISO6391.getAllCodes().map(code => ({value: code, label: ISO6391.getName(code)})) + } + render () { + const messages = getComponentMessages('LanguageSelect') + const placeholder = getMessage(messages, 'placeholder') + return ( + + { + const props = {} + const val = input ? input.value : null + props[field.name] = val + updateActiveEntity(activeEntity, activeComponent, props) + }} + filterOptions={(options, filter, values) => { + // Filter already selected values + const valueKeys = values && values.map(i => i.value) + let filteredOptions = options.filter(option => { + return valueKeys ? valueKeys.indexOf(option.value) === -1 : [] + }) + + // Filter by label + if (filter !== undefined && filter != null && filter.length > 0) { + filteredOptions = filteredOptions.filter(option => { + return RegExp(filter, 'ig').test(option.label) + }) + } + + // Append Addition option + if (filteredOptions.length === 0) { + filteredOptions.push({ + label: Create new zone: {filter}, + value: filter, + create: true + }) + } + + return filteredOptions + }} + options={zoneOptions} /> + + ) + case 'TIMEZONE': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = option.value + this.setState({[editorField]: option.value}) + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'LANGUAGE': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = option.value + this.setState({[editorField]: option.value}) + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'TIME': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = evt.target.value + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'LATITUDE': + case 'LONGITUDE': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = evt.target.value + // this.setState({[editorField]: evt.target.value}) + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'NUMBER': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = evt.target.value + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'DATE': + const defaultValue = /end/.test(editorField) ? +moment().startOf('day').add(3, 'months') : +moment().startOf('day') + const dateTimeProps = { + mode: 'date', + dateTime: currentValue ? +moment(currentValue) : defaultValue, + onChange: (millis) => { + const props = {} + props[field.name] = +millis + updateActiveEntity(activeEntity, activeComponent, props) + } + } + if (!currentValue) { + dateTimeProps.defaultText = 'Please select a date' + } + return ( + + {basicLabel} + + + ) + case 'COLOR': + const hexColor = currentValue !== null ? `#${currentValue}` : '#000000' + const colorStyle = { + width: '36px', + height: '20px', + borderRadius: '2px', + background: hexColor + } + const wrapper = { + position: 'inherit', + zIndex: '100' + } + return ( + + {basicLabel} + { + + } + {this.state[editorField] + ?
+
{ + e.preventDefault() + // if [Esc] is pressed (for accessibility) + if (e.keyCode === 27) { + this.handleClose(editorField) + } + }} + onClick={() => this.handleClose(editorField)} /> +
+ { + const props = {} + props[field.name] = color.hex.split('#')[1] + updateActiveEntity(activeEntity, activeComponent, props) + }} /> +
+
+ : null + } + + ) + case 'POSITIVE_INT': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = evt.target.value + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'POSITIVE_NUM': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = evt.target.value + updateActiveEntity(activeEntity, activeComponent, props) + }} /> + + ) + case 'DAY_OF_WEEK_BOOLEAN': + return ( + + + {editorField === 'monday' + ?
Days of service
: null + } +
+ + { + const props = {} + props[field.name] = evt.target.checked ? 1 : 0 + updateActiveEntity(activeEntity, activeComponent, props) + }}> + {toSentenceCase(editorField.substr(0, 3))} + + {' '} + +
+ ) + case 'DROPDOWN': + return ( + + {basicLabel} + { + const props = {} + props[field.name] = evt.target.value + updateActiveEntity(activeEntity, activeComponent, props) + }}> + {field.options.map(option => { + return + })} + + + ) + case 'GTFS_ROUTE': + const routeEntity = getGtfsEntity('route', currentValue) + const routeValue = routeEntity + ? { + 'value': routeEntity.route_id, + 'label': routeEntity.route_short_name + ? `${routeEntity.route_short_name} - ${routeEntity.route_long_name}` + : routeEntity.route_long_name + } + : '' + return ( + { + fieldEdited(table.id, row, editorField, evt.route.route_id) + gtfsEntitySelected('route', evt.route) + }} + value={routeValue} /> + ) + case 'GTFS_AGENCY': + const agency = tableData.agency && tableData.agency.find(a => a.id === currentValue) + return ( + + {basicLabel} + { + console.log(input) + const rules = [...activeEntity.fareRules] + rules[index].contains_id = input ? input.value : null + updateActiveEntity(activeEntity, activeComponent, {fareRules: rules}) + }} + options={zoneOptions} + /> + : rule.origin_id || rule.destination_id + ? [ + { + console.log(input) + const rules = [...activeEntity.fareRules] + rules[index].destination_id = input ? input.value : null + updateActiveEntity(activeEntity, activeComponent, {fareRules: rules}) + }} + options={zoneOptions} + /> + ] + : null + } + + ) + })} +
+ ) + } +} diff --git a/src/main/client/editor/components/FeedInfoPanel.js b/lib/editor/components/FeedInfoPanel.js similarity index 63% rename from src/main/client/editor/components/FeedInfoPanel.js rename to lib/editor/components/FeedInfoPanel.js index 7e5212dc4..baa8262bb 100644 --- a/src/main/client/editor/components/FeedInfoPanel.js +++ b/lib/editor/components/FeedInfoPanel.js @@ -1,12 +1,11 @@ import React, {Component, PropTypes} from 'react' import { Button, ButtonGroup, DropdownButton, Dropdown, MenuItem, Tooltip, OverlayTrigger } from 'react-bootstrap' -import Icon from 'react-fa' +import {Icon} from '@conveyal/woonerf' import { browserHistory } from 'react-router' -import ReactCSSTransitionGroup from 'react-addons-css-transition-group' import CreateSnapshotModal from './CreateSnapshotModal' import SelectFileModal from '../../common/components/SelectFileModal.js' -import { gtfsIcons } from '../util/gtfs' +import { gtfsIcons } from '../util/ui' export default class FeedInfoPanel extends Component { @@ -29,11 +28,10 @@ export default class FeedInfoPanel extends Component { title: 'Upload route shapefile', body: 'Select a zipped shapefile to display on map:', onConfirm: (files) => { - let nameArray = files[0].name.split('.') + const nameArray = files[0].name.split('.') if (files[0].type !== 'application/zip' || nameArray[nameArray.length - 1] !== 'zip') { return false - } - else { + } else { this.props.displayRoutesShapefile(feedSource, files[0]) return true } @@ -42,11 +40,11 @@ export default class FeedInfoPanel extends Component { }) } render () { - let { feedSource, feedInfo } = this.props + const { feedSource, feedInfo } = this.props if (!feedInfo) return null - let panelWidth = 370 + const panelWidth = 400 // let panelHeight = '100px' - let panelStyle = { + const panelStyle = { // backgroundColor: 'white', position: 'absolute', right: this.state.right, @@ -66,14 +64,14 @@ export default class FeedInfoPanel extends Component { : 'Unnamed' return ( - 0 ? 'right' : 'left'}`} transitionEnterTimeout={500} transitionLeaveTimeout={300}> - -
- { - this.props.createSnapshot(feedSource, name, comment) - }} - /> +
+ +
+ { + this.props.createSnapshot(feedSource, name, comment) + }} + /> {/* Hide toolbar toggle */} {toolbarVisible ? 'Hide toolbar' : 'Show toolbar'}}> @@ -81,17 +79,19 @@ export default class FeedInfoPanel extends Component { onClick={() => { if (toolbarVisible) { this.setState({right: 30 - panelWidth}) - } - else { + } else { this.setState({right: 5}) } }} > - + {/* Navigation dropdown */} - Editing {feedName}} id='navigation-dropdown' + Editing {feedName}} + id='navigation-dropdown' onSelect={key => { switch (key) { case '1': @@ -101,16 +101,18 @@ export default class FeedInfoPanel extends Component { } }} > - Back to project - Back to feed source + Back to project + Back to feed source {/* Add entity dropdown */} - } + } id='add-entity-dropdown' onSelect={key => { console.log(key) @@ -119,9 +121,9 @@ export default class FeedInfoPanel extends Component { > {gtfsIcons.map(c => { if (!c.addable) return null - let name = c.id === 'scheduleexception' ? 'schedule exception' : c.id + const name = c.id === 'scheduleexception' ? 'schedule exception' : c.id return ( - Add {name} + Add {name} ) })} @@ -130,8 +132,9 @@ export default class FeedInfoPanel extends Component { { - let snapshot = this.props.feedSource.editorSnapshots.find(s => s.id === key) + const snapshot = this.props.feedSource.editorSnapshots.find(s => s.id === key) this.props.showConfirmModal({ title: `Restore ${key}?`, body: `Are you sure you want to restore this snapshot?`, @@ -148,30 +151,30 @@ export default class FeedInfoPanel extends Component { this.refs.snapshotModal.open() }} > - + - { - this.props.getSnapshots(feedSource) - }} - /> - - {this.props.feedSource && this.props.feedSource.editorSnapshots - ? this.props.feedSource.editorSnapshots.map(snapshot => { - return ( - Revert to {snapshot.name} - ) - }) - : No snapshots - } - + { + this.props.getSnapshots(feedSource) + }} + /> + + {this.props.feedSource && this.props.feedSource.editorSnapshots + ? this.props.feedSource.editorSnapshots.map(snapshot => { + return ( + Revert to {snapshot.name} + ) + }) + : No snapshots + } + } +
- ) } } diff --git a/lib/editor/components/GtfsEditor.js b/lib/editor/components/GtfsEditor.js new file mode 100644 index 000000000..674994b30 --- /dev/null +++ b/lib/editor/components/GtfsEditor.js @@ -0,0 +1,248 @@ +import React, {Component, PropTypes} from 'react' +import Helmet from 'react-helmet' +import { shallowEqual } from 'react-pure-render' + +import CurrentStatusMessage from '../../common/containers/CurrentStatusMessage' +import ConfirmModal from '../../common/components/ConfirmModal.js' +import CurrentStatusModal from '../../common/containers/CurrentStatusModal' +import EditorMap from './map/EditorMap' +import EditorHelpModal from './EditorHelpModal' +import EditorSidebar from './EditorSidebar' +import ActiveEntityList from '../containers/ActiveEntityList' +import EntityDetails from './EntityDetails' +import ActiveTimetableEditor from '../containers/ActiveTimetableEditor' +import ActiveFeedInfoPanel from '../containers/ActiveFeedInfoPanel' + +import { getConfigProperty } from '../../common/util/config' + +export default class GtfsEditor extends Component { + static propTypes = { + currentTable: PropTypes.string, + feedSourceId: PropTypes.string, + feedSource: PropTypes.object, + project: PropTypes.object, + user: PropTypes.object, + tableData: PropTypes.object, + feedInfo: PropTypes.object, + mapState: PropTypes.object, + + entities: PropTypes.array, + + onComponentMount: PropTypes.func, + onComponentUpdate: PropTypes.func, + clearGtfsContent: PropTypes.func, + getGtfsTable: PropTypes.func, + fetchTripPatternsForRoute: PropTypes.func, + fetchTripsForCalendar: PropTypes.func, + saveTripsForCalendar: PropTypes.func, + deleteTripsForCalendar: PropTypes.func, + setActiveEntity: PropTypes.func, + updateActiveEntity: PropTypes.func, + saveActiveEntity: PropTypes.func, + resetActiveEntity: PropTypes.func, + deleteEntity: PropTypes.func, + cloneEntity: PropTypes.func, + newGtfsEntity: PropTypes.func, + setTutorialHidden: PropTypes.func, + + sidebarExpanded: PropTypes.bool, + + activeEntity: PropTypes.object, + activeEntityId: PropTypes.string, + subEntityId: PropTypes.string, + activeSubSubEntity: PropTypes.string, + activeComponent: PropTypes.string, + subSubComponent: PropTypes.string + } + constructor (props) { + super(props) + + this.state = { + activeTableId: this.props.currentTable + } + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + componentDidUpdate (prevProps) { + // handles back button + this.props.onComponentUpdate(prevProps, this.props) + } + componentWillReceiveProps (nextProps) { + // clear GTFS content if feedSource changes (i.e., user switches feed sources) + if (nextProps.feedSourceId !== this.props.feedSourceId) { + this.props.clearGtfsContent() + this.props.onComponentMount(nextProps) + this.props.getGtfsTable('calendar', this.props.feedSourceId) + } + // fetch table if it doesn't exist already and user changes tabs + if (nextProps.activeComponent && nextProps.activeComponent !== this.props.activeComponent && !nextProps.tableData[nextProps.activeComponent]) { + this.props.getGtfsTable(nextProps.activeComponent, nextProps.feedSource.id) + } + // fetch sub components of active entity on active entity switch (e.g., fetch trip patterns when route changed) + if (nextProps.activeComponent === 'route' && nextProps.feedSource && nextProps.activeEntity && (!this.props.activeEntity || nextProps.activeEntity.id !== this.props.activeEntity.id)) { + this.props.fetchTripPatternsForRoute(nextProps.feedSource.id, nextProps.activeEntity.id) + } + // fetch required sub sub component entities if active sub entity changes + if (nextProps.subSubComponent && nextProps.activeSubSubEntity && !shallowEqual(nextProps.activeSubSubEntity, this.props.activeSubSubEntity)) { + switch (nextProps.subSubComponent) { + case 'timetable': + const pattern = nextProps.activeEntity.tripPatterns.find(p => p.id === nextProps.subEntityId) + // fetch trips if they haven't been fetched + if (!pattern[nextProps.activeSubSubEntity]) { + this.props.fetchTripsForCalendar(nextProps.feedSource.id, pattern, nextProps.activeSubSubEntity) + } + break + } + } + } + + showConfirmModal (props) { + this.refs.confirmModal.open(props) + } + getMapOffset (activeComponent, dWidth, activeEntityId, lWidth) { + return activeComponent === 'feedinfo' + ? dWidth + : activeEntityId + ? lWidth + dWidth + : activeComponent + ? lWidth + : 0 + } + render () { + const { + feedSource, + user, + activeEntityId, + tableData, + entities, + activeComponent, + sidebarExpanded, + feedInfo, + setActiveEntity, + subSubComponent, + activeEntity, + subEntityId, + activeSubSubEntity, + deleteEntity, + updateActiveEntity, + resetActiveEntity, + saveActiveEntity, + fetchTripsForCalendar, + updateCellValue, + cloneEntity, + newGtfsEntity, + mapState, + hideTutorial, + setTutorialHidden, + project + } = this.props + const editingIsDisabled = feedSource ? !user.permissions.hasFeedPermission(feedSource.projectId, feedSource.id, 'edit-gtfs') : true + if (feedSource && editingIsDisabled) { + console.log('editing disabled') + // browserHistory.push(`/feed/${feedSource.id}`) + } + const LIST_WIDTH = 220 + const DETAILS_WIDTH = 300 + const entityDetails = activeEntityId + ? this.showConfirmModal(props)} + {...this.props} + getGtfsEntity={(type, id) => { + return entities.find(ent => ent.id === id) + }} + getGtfsEntityIndex={(type, id) => { + return entities.findIndex(ent => ent.id === id) + }} /> + : null + const defaultTitle = `${getConfigProperty('application.title')}: GTFS Editor` + return ( +
+ + +
+ {subSubComponent === 'timetable' // && activeEntity + ? this.showConfirmModal(props)} + activePatternId={subEntityId} + activeScheduleId={activeSubSubEntity} + setActiveEntity={setActiveEntity} + tableData={tableData} + deleteEntity={deleteEntity} + updateActiveEntity={updateActiveEntity} + resetActiveEntity={resetActiveEntity} + saveActiveEntity={saveActiveEntity} + fetchTripsForCalendar={fetchTripsForCalendar} + sidebarExpanded={sidebarExpanded} + updateCellValue={updateCellValue} /> + : activeComponent === 'feedinfo' + ? + : activeComponent + ? [ + this.showConfirmModal(props)} + entities={entities} + activeEntityId={activeEntityId} + activeComponent={activeComponent} + feedSource={feedSource} + key='entity-list' />, + entityDetails + ] + : null + } +
+ + + +
+ ) + } +} diff --git a/src/main/client/editor/components/GtfsVersionSummary.js b/lib/editor/components/GtfsVersionSummary.js similarity index 56% rename from src/main/client/editor/components/GtfsVersionSummary.js rename to lib/editor/components/GtfsVersionSummary.js index fcfab2f5a..0a4649508 100644 --- a/src/main/client/editor/components/GtfsVersionSummary.js +++ b/lib/editor/components/GtfsVersionSummary.js @@ -1,37 +1,39 @@ import React, {Component, PropTypes} from 'react' -import { Panel, Row, Col, Table, Input, Button, Glyphicon, Well, Alert } from 'react-bootstrap' -import { Link, browserHistory } from 'react-router' +import { Panel, Row, Col, Table, Button, Glyphicon, Alert } from 'react-bootstrap' +import { browserHistory } from 'react-router' import moment from 'moment' import { getConfigProperty } from '../../common/util/config' export default class GtfsVersionSummary extends Component { - + static propTypes = { + editor: PropTypes.object + } constructor (props) { super(props) this.state = { expanded: false } } isTableIncluded (tableId) { - if(!this.props.editor.tableData) return null - return tableId in this.props.editor.tableData ? 'Yes' : 'No' + if (!this.props.editor.data.tables) return null + return tableId in this.props.editor.data.tables ? 'Yes' : 'No' } tableRecordCount (tableId) { - if(!this.props.editor.tableData) return null - if(!(tableId in this.props.editor.tableData)) return 'N/A' - return this.props.editor.tableData[tableId].length.toLocaleString() + if (!this.props.editor.data.tables) return null + if (!(tableId in this.props.editor.data.tables)) return 'N/A' + return this.props.editor.data.tables[tableId].length.toLocaleString() } validationIssueCount (tableId) { - if(!this.props.editor.validation) return null - if(!(tableId in this.props.editor.validation)) return 'None' + if (!this.props.editor.validation) return null + if (!(tableId in this.props.editor.validation)) return 'None' return this.props.editor.validation[tableId].length.toLocaleString() } feedStatus () { - if(!this.props.editor.timestamp) return null - if(!this.gtfsPlusEdited()) return GTFS+ data for this feed version has not been edited. + if (!this.props.editor.timestamp) return null + if (!this.gtfsPlusEdited()) return GTFS+ data for this feed version has not been edited. return GTFS+ Data updated {moment(this.props.editor.timestamp).format('MMM. DD, YYYY, h:MMa')} } @@ -40,11 +42,17 @@ export default class GtfsVersionSummary extends Component { } render () { - const editingIsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.version.feedSource.projectId, this.props.version.feedSource.id, 'edit-gtfs') - const publishingIsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.version.feedSource.projectId, this.props.version.feedSource.id, 'approve-gtfs') + const { + gtfsPlusDataRequested, + user, + version, + publishClicked + } = this.props + const editingIsDisabled = !user.permissions.hasFeedPermission(version.feedSource.organizationId, version.feedSource.projectId, version.feedSource.id, 'edit-gtfs') + const publishingIsDisabled = !user.permissions.hasFeedPermission(version.feedSource.organizationId, version.feedSource.projectId, version.feedSource.id, 'approve-gtfs') const header = (

{ - if(!this.state.expanded) this.props.gtfsPlusDataRequested(this.props.version) + if (!this.state.expanded) gtfsPlusDataRequested(version) this.setState({ expanded: !this.state.expanded }) }}> GTFS+ for this Version @@ -66,23 +74,23 @@ export default class GtfsVersionSummary extends Component { bsSize='large' disabled={editingIsDisabled} bsStyle='primary' - onClick={() => { browserHistory.push(`/editor/${this.props.version.feedSource.id}/${this.props.version.id}`) }} + onClick={() => { browserHistory.push(`/editor/${version.feedSource.id}/${version.id}`) }} > Edit GTFS+ {this.gtfsPlusEdited() ? + bsSize='large' + disabled={publishingIsDisabled} + bsStyle='primary' + style={{ marginLeft: '6px' }} + onClick={() => { + publishClicked(this.props.version) + this.setState({ expanded: false }) + }} + > + Publish as New Version + : null } @@ -96,7 +104,7 @@ export default class GtfsVersionSummary extends Component { Included? Records Validation Issues - + @@ -106,7 +114,7 @@ export default class GtfsVersionSummary extends Component { {this.isTableIncluded(table.id)} {this.tableRecordCount(table.id)} {this.validationIssueCount(table.id)} - + ) })} diff --git a/src/main/client/editor/components/HourMinuteInput.js b/lib/editor/components/HourMinuteInput.js similarity index 81% rename from src/main/client/editor/components/HourMinuteInput.js rename to lib/editor/components/HourMinuteInput.js index c7d066566..f0103eac7 100644 --- a/src/main/client/editor/components/HourMinuteInput.js +++ b/lib/editor/components/HourMinuteInput.js @@ -16,14 +16,14 @@ export default class HourMinuteInput extends Component { } } onChange (value) { - let seconds = this.convertStringToSeconds(value) + const seconds = this.convertStringToSeconds(value) if (seconds === this.state.seconds) { this.setState({string: value}) - } - else { + } else { this.setState({seconds, string: value}) - if (typeof this.props.onChange !== 'undefined') + if (typeof this.props.onChange !== 'undefined') { this.props.onChange(seconds) + } } } convertSecondsToString (seconds) { @@ -37,22 +37,19 @@ export default class HourMinuteInput extends Component { // if both hours and minutes are present if (!isNaN(hourMinute[0]) && !isNaN(hourMinute[1])) { return Math.abs(+hourMinute[0]) * 60 * 60 + Math.abs(+hourMinute[1]) * 60 - } - // if less than one hour - else if (isNaN(hourMinute[0])) { + } else if (isNaN(hourMinute[0])) { + // if less than one hour return Math.abs(+hourMinute[1]) * 60 - } - // if minutes are not present - else if (isNaN(hourMinute[1])) { + } else if (isNaN(hourMinute[1])) { + // if minutes are not present return Math.abs(+hourMinute[0]) * 60 * 60 - } - // if no input - else { + } else { + // if no input return 0 } } render () { - let seconds = this.state.seconds + const seconds = this.state.seconds return ( se.id === this.props.activeEntity.id) + if (exceptionIndex !== -1) { + allExceptions.splice(exceptionIndex, 1) + } + allExceptions.push(this.props.activeEntity) + } + for (let i = 0; i < allExceptions.length; i++) { + allExceptions[i].dates && allExceptions[i].dates.map(d => { + if (typeof dateMap[moment(d).format('YYYYMMDD')] === 'undefined') { + dateMap[moment(d).format('YYYYMMDD')] = [] + } + dateMap[moment(d).format('YYYYMMDD')].push(allExceptions[i].id) + }) + } + activeEntity && activeEntity.dates.length === 0 && validate({field: 'dates', invalid: true}) + return ( +
+
+ + Exception name + { + updateActiveEntity(activeEntity, activeComponent, {name: evt.target.value}) + }} /> + + + Run the following schedule: + { + // const props = {} + // props[field.name] = evt.target.value + updateActiveEntity(activeEntity, activeComponent, {exemplar: evt.target.value, customSchedule: null}) + }}> + {EXEMPLARS.map(exemplar => { + return ( + + ) + })} + + + {activeEntity && activeEntity.exemplar === 'CUSTOM' + ? + Select calendar to run: + { + const val = input ? input.map(i => i.value) : null + updateActiveEntity(activeEntity, activeComponent, {addedService: val}) + }} + options={tableData.calendar + ? tableData.calendar + .filter(cal => !activeEntity.removedService || activeEntity.removedService.indexOf(cal.id) === -1) + .map(calendar => { + return { + value: calendar.id, + label: calendar.description, + calendar + } + }) + : [] + } /> + Select calendars to remove: + Select calendar...} + valueRenderer={this._render} + optionRenderer={this._render} + disabled={!activePattern || activePattern.id === 'new'} + options={options} + onChange={(value) => { + const calendar = value && value.calendar + setActiveEntity(feedSource.id, 'route', route, 'trippattern', activePattern, 'timetable', calendar) + }} + filterOptions + /> + ) + } +} diff --git a/lib/editor/components/timetable/EditableCell.js b/lib/editor/components/timetable/EditableCell.js new file mode 100644 index 000000000..cb3e791bd --- /dev/null +++ b/lib/editor/components/timetable/EditableCell.js @@ -0,0 +1,274 @@ +import React, {Component, PropTypes} from 'react' +import ReactDOM from 'react-dom' +import moment from 'moment' + +import { TIMETABLE_FORMATS } from '../../util' + +export default class EditableCell extends Component { + static propTypes = { + isEditing: PropTypes.bool + } + constructor (props) { + super(props) + this.state = { + isEditing: this.props.isEditing, + edited: false, + data: this.props.data, + originalData: this.props.data + } + } + componentWillReceiveProps (nextProps) { + if (this.props.data !== nextProps.data) { + // console.log('setting data...', nextProps.data) + this.setState({ + data: nextProps.data + // edited: true + // originalData: nextProps.data + }) + } + if (this.state.isEditing !== nextProps.isEditing) this.setState({isEditing: nextProps.isEditing}) + } + cancel () { + this.setState({ + isEditing: false, + isFocused: false, + data: this.props.data + }) + this.props.onStopEditing() + } + beginEditing () { + this.setState({isEditing: true}) + } + handleClick (evt) { + if (this.props.isFocused) { + this.beginEditing() + } else { + this.props.onClick() + } + } + handleKeyDown (evt) { + const input = ReactDOM.findDOMNode(this.refs.cellInput) + switch (evt.keyCode) { + case 88: // x + if (this.props.renderTime) { + evt.preventDefault() + this.props.onRowSelect(evt) + break + } else { + return true + } + case 79: // o + if (this.props.renderTime) { + evt.preventDefault() + this.props.onRowSelect(evt) + break + } else { + return true + } + case 222: // single quote + if (evt.shiftKey) { + console.log('dupe left') + this.props.duplicateLeft(evt) + this.props.onRight(evt) + return false + } + break + case 13: // Enter + evt.preventDefault() + if (this.props.isFocused) { + this.beginEditing() + } + // handle shift + if (evt.shiftKey) { + this.save(evt) + this.props.onUp(evt) + } else { + this.save(evt) + } + break + case 9: // Tab + evt.preventDefault() + // handle shift + if (evt.shiftKey) { + this.save(evt) + } else { + this.save(evt) + } + break + case 27: // Esc + this.cancel() + break + case 39: // right + if (input.selectionStart === input.value.length) { + this.save(evt) + return false + } else { + return true + } + case 37: // left + if (input.selectionStart === 0 && input.selectionEnd === input.value.length) { + this.save(evt) + return false + } else { + return true + } + case 38: // up + this.save(evt) + break + case 40: // down + this.save(evt) + break + default: + return true + } + } + save () { + // for non-time rendering + if (!this.props.renderTime) { + if (this.state.data !== this.state.originalData) { + this.setState({isEditing: false}) + this.props.onChange(this.state.data) + this.props.onStopEditing() + } else { + this.cancel() + } + } else { + let data = this.state.data + const parts = this.state.data.split(':') + const hours = +parts[0] + let greaterThan24 = false + + // check for times greater than 24:00 + if (parts && parts[0] && hours >= 24 && hours < 34) { + parts[0] = `0${hours - 24}` + greaterThan24 = true + data = parts.join(':') + } + const date = moment().startOf('day').format('YYYY-MM-DD') + const momentTime = moment(date + 'T' + data, TIMETABLE_FORMATS, true) + let value = momentTime.isValid() ? momentTime.diff(date, 'seconds') : null + + if (greaterThan24 && momentTime.isValid()) { + value += 86400 + } + // check for valid time and new value + if ((data === '' || momentTime.isValid()) && value !== data) { + this.setState({data: value, isEditing: false}) + this.props.onChange(value) + this.props.onStopEditing() + } else { + this.cancel() + } + } + } + cellRenderer (value) { + if (this.props.cellRenderer) { + return this.props.cellRenderer(value) + } else { + return value + } + } + handleChange (evt) { + this.setState({data: evt.target.value}) + } + handlePaste (evt) { + const text = evt.clipboardData.getData('Text') + const rowDelimiter = text.indexOf('\n') > -1 // google sheets row delimiter + ? '\n' + : text.indexOf(String.fromCharCode(13)) > -1 // excel row delimiter + ? String.fromCharCode(13) + : null + const rows = text.split(rowDelimiter) + + for (var i = 0; i < rows.length; i++) { + rows[i] = rows[i].split(String.fromCharCode(9)) + } + + if (rows.length > 1 || rows[0].length > 1) { + this.cancel() + this.props.handlePastedRows(rows) + evt.preventDefault() + } + } + _onInputFocus (evt) { + evt.target.select() + } + render () { + const rowCheckedColor = '#F3FAF6' + const focusedNotEditing = this.props.isFocused && !this.state.isEditing + const edgeDiff = this.props.isFocused ? 0 : 0.5 + const divStyle = { + height: '100%', + display: 'inline-block', + paddingTop: `${3 + edgeDiff}px`, + paddingLeft: `${3 + edgeDiff}px`, + UserSelect: 'none', + userSelect: 'none', + fontWeight: this.state.edited ? 'bold' : 'normal' + } + const cellStyle = { + backgroundColor: this.props.invalidData && !this.state.isEditing + ? 'pink' + : focusedNotEditing + ? '#F4F4F4' + : this.state.isEditing + ? '#fff' + : this.props.isSelected + ? rowCheckedColor + : '#fff', + border: this.props.invalidData && focusedNotEditing + ? '2px solid red' + : this.props.isFocused + ? `2px solid #66AFA2` + : '1px solid #ddd', + margin: `${-0.5 + edgeDiff}px`, + padding: `${-edgeDiff}px`, + whiteSpace: 'nowrap', + cursor: 'default', + fontWeight: '400', + // fontFamily: '"Courier New", Courier, monospace', + color: this.props.lightText ? '#aaa' : '#000', + ...this.props.style + } + const cellHtml = this.state.isEditing + ? this.handlePaste(evt)} + style={{ + width: '100%', + height: '100%', + outline: 'none', + margin: '1px', + marginLeft: '2px', + padding: '1px', + border: 0, + backgroundColor: 'rgba(0,0,0,0)' + }} + autoFocus='true' + onKeyDown={(evt) => this.handleKeyDown(evt)} + onChange={(evt) => this.handleChange(evt)} + onFocus={(evt) => this._onInputFocus(evt)} + onBlur={(evt) => this.cancel(evt)} + /> + :
+ {this.cellRenderer(this.state.data)} +
+ return ( +
this.handleClick(evt)} + > + { + cellHtml + } +
+ ) + } +} diff --git a/lib/editor/components/timetable/HeaderCell.js b/lib/editor/components/timetable/HeaderCell.js new file mode 100644 index 000000000..f5d1c1a42 --- /dev/null +++ b/lib/editor/components/timetable/HeaderCell.js @@ -0,0 +1,46 @@ +import React, {Component} from 'react' + +export default class HeaderCell extends Component { + constructor (props) { + super(props) + this.state = { + active: this.props.active + } + } + componentWillReceiveProps (nextProps) { + const { active } = nextProps + if (this.props.active !== active) { + this.setState({active}) + } + } + _handleClick () { + if (this.props.selectable) { + this.setState({active: !this.state.active}) + this.props.onChange(!this.state.active) + } + } + render () { + const edgeDiff = 0.5 + const style = { + backgroundColor: this.state.active ? '#A8D4BB' : '#eee', + border: '1px solid #ddd', + margin: `${-0.5 + edgeDiff}px`, + padding: `${-edgeDiff}px`, + UserSelect: 'none', + userSelect: 'none', + paddingTop: '6px', + cursor: this.props.selectable ? 'pointer' : 'default', + ...this.props.style + } + return ( +
this._handleClick()} + > + {this.props.label} +
+ ) + } +} diff --git a/lib/editor/components/timetable/PatternSelect.js b/lib/editor/components/timetable/PatternSelect.js new file mode 100644 index 000000000..b9b55487d --- /dev/null +++ b/lib/editor/components/timetable/PatternSelect.js @@ -0,0 +1,43 @@ +import React, {Component} from 'react' +import {Icon} from '@conveyal/woonerf' +import { Label } from 'react-bootstrap' +import Select from 'react-select' + +import { getEntityName } from '../../util/gtfs' + +export default class PatternSelect extends Component { + _render = (option) => { + const calendarCount = Object.keys(option.pattern.tripCountByCalendar).length + return ( + + {option.label} + {' '} + + {' '} + + + ) + } + render () { + const { activePattern, route, feedSource, setActiveEntity } = this.props + const patterns = route && route.tripPatterns ? route.tripPatterns : [] + return ( + Select route...} + options={routes && routes.map(route => ({value: route.id, label: `${getEntityName('route', route)}` || '[Unnamed]', route, routeTrips: route.numberOfTrips}))} + clearable={false} + entities={routes} + onChange={(value) => { + const patt = {id: 'new'} + setActiveEntity(feedSource.id, 'route', value.route, 'trippattern', patt, 'timetable', null) + }} + /> + ) + } +} diff --git a/lib/editor/components/timetable/Timetable.js b/lib/editor/components/timetable/Timetable.js new file mode 100644 index 000000000..0ddb4b41b --- /dev/null +++ b/lib/editor/components/timetable/Timetable.js @@ -0,0 +1,424 @@ +import React, {Component, PropTypes} from 'react' +import { ArrowKeyStepper, Grid, AutoSizer, ScrollSync } from 'react-virtualized' +import ReactDOM from 'react-dom' +import moment from 'moment' +import update from 'react-addons-update' +import objectPath from 'object-path' +import scrollbarSize from 'dom-helpers/util/scrollbarSize' + +import HeaderCell from './HeaderCell' +import EditableCell from './EditableCell' +import Loading from '../../../common/components/Loading' +import { isTimeFormat, TIMETABLE_FORMATS } from '../../util' + +export default class Timetable extends Component { + static propTypes = { + columns: PropTypes.array, + data: PropTypes.array + } + constructor (props) { + super(props) + this.state = { + activeCell: null, // 'rowNum-colNum', e.g. 0-1 + edited: [], + offsetSeconds: 0, + + // scrollsync + columnWidth: 30, + overscanColumnCount: 10, + overscanRowCount: 5, + rowHeight: 25, + scrollToRow: this.props.scrollToRow, + scrollToColumn: this.props.scrollToColumn + } + this._getColumnWidth = this._getColumnWidth.bind(this) + this._getColumnHeaderWidth = this._getColumnHeaderWidth.bind(this) + this._renderHeaderCell = this._renderHeaderCell.bind(this) + this._renderLeftHeaderCell = this._renderLeftHeaderCell.bind(this) + this._renderLeftColumnCell = this._renderLeftColumnCell.bind(this) + } + componentWillReceiveProps (nextProps) { + // handle scrolling to new location based on props + const { scrollToRow, scrollToColumn } = nextProps + if (scrollToRow !== this.props.scrollToRow || scrollToColumn !== this.props.scrollToColumn) { + this.setState({scrollToRow, scrollToColumn}) + } + } + shouldComponentUpdate (nextProps) { + return true + } + parseTime (timeString) { + const date = moment().startOf('day').format('YYYY-MM-DD') + return moment(date + 'T' + timeString, TIMETABLE_FORMATS).diff(date, 'seconds') + } + handlePastedRows (pastedRows, rowIndex, colIndex) { + const newRows = [...this.props.data] + const editedRows = [] + let activeRow = rowIndex + let activeCol = colIndex + // iterate over rows in pasted selection + for (var i = 0; i < pastedRows.length; i++) { + activeRow = rowIndex + i + editedRows.push(i) + + // construct new row if it doesn't exist + if (typeof this.props.data[i + rowIndex] === 'undefined') { + this.props.addNewRow() + } + // iterate over number of this.props.columns in pasted selection + for (var j = 0; j < pastedRows[0].length; j++) { + activeCol = colIndex + j + const path = `${rowIndex + i}.${this.props.columns[colIndex + j].key}` + + // // construct new row if it doesn't exist + // if (typeof newRows[i + rowIndex] === 'undefined' || typeof objectPath.get(newRows, path) === 'undefined') { + // // newRows.push(this.props.constructNewRow()) + // // this.props.addNewRow() + // } + const value = this.parseTime(pastedRows[i][j]) + // objectPath.set(newRows, path, value) + this.props.updateCellValue(value, rowIndex + i, path) + // if departure times are hidden, paste into adjacent time column + const adjacentPath = `${rowIndex + i}.${this.props.columns[colIndex + j + 2].key}` + if (this.props.timetable.hideDepartureTimes && isTimeFormat(this.props.columns[colIndex + j].type) && typeof objectPath.get(newRows, adjacentPath) !== 'undefined') { + // objectPath.set(newRows, adjacentPath, value) + this.props.updateCellValue(value, rowIndex + i, adjacentPath) + } + } + } + const stateUpdate = { + activeCell: {$set: `${activeRow}-${activeCol}`}, + scrollToRow: {$set: activeRow}, + scrollToColumn: {$set: activeCol} + // data: {$set: newRows}, + // edited: { $push: editedRows } + } + this.setState(update(this.state, stateUpdate)) + } + _getColumnWidth ({ index }) { + index = this.props.timetable.hideDepartureTimes && index > 3 ? index * 2 - 3 : index + const col = this.props.columns[index] + const width = col.type === 'ARRIVAL_TIME' && this.props.timetable.hideDepartureTimes + ? col.width * 2 + : col + ? col.width + : 90 + return width + } + _getColumnHeaderWidth ({ index }) { + const col = this.getHeaderColumns()[index] + const width = col.type === 'ARRIVAL_TIME' && !this.props.timetable.hideDepartureTimes + ? col.width * 2 + : col + ? col.width + : 200 + return width + } + handleCellClick = (rowIndex, columnIndex) => { + this.setState({scrollToColumn: columnIndex, scrollToRow: rowIndex}) + } + cellValueInvalid (col, value, previousValue) { + // TRUE if value is invalid + return isTimeFormat(col.type) && value >= 0 && value < previousValue + } + _cellRenderer ({ columnIndex, key, rowIndex, scrollToColumn, scrollToRow, style }) { + // adjust columnIndex for hideDepartures + const colIndex = this.props.timetable.hideDepartureTimes && columnIndex > 3 ? columnIndex * 2 - 3 : columnIndex + const isFocused = colIndex === scrollToColumn && rowIndex === scrollToRow + const isEditing = this.state.activeCell === `${rowIndex}-${colIndex}` + const col = this.props.columns[colIndex] + const previousCol = this.props.columns[colIndex - 1] + const row = this.props.data[rowIndex] + + const rowIsChecked = this.props.selected[0] === '*' && + this.props.selected.indexOf(rowIndex) === -1 || this.props.selected[0] !== '*' && + this.props.selected.indexOf(rowIndex) !== -1 + + let val = objectPath.get(this.props.data[rowIndex], col.key) + if (col.key === 'gtfsTripId' && val === null) { + val = objectPath.get(this.props.data[rowIndex], 'id') !== 'new' ? objectPath.get(this.props.data[rowIndex], 'id') : null + } + const previousValue = previousCol && row && objectPath.get(row, previousCol.key) + const isInvalid = isTimeFormat(col.type) && val >= 0 && val < previousValue && val !== null + return ( + this.handleCellClick(rowIndex, colIndex)} + duplicateLeft={(evt) => this.props.updateCellValue(previousValue, rowIndex, `${rowIndex}.${col.key}`)} + handlePastedRows={(rows) => this.handlePastedRows(rows, rowIndex, colIndex, this.props.columns)} + invalidData={isInvalid} + isEditing={isEditing} + isSelected={rowIsChecked} + isFocused={isFocused} + lightText={col.type === 'DEPARTURE_TIME'} + placeholder={col.placeholder} + renderTime={isTimeFormat(col.type)} + cellRenderer={(value) => this.getCellRenderer(col, value)} + data={val} + style={style} + onStopEditing={() => this.handleEndEditing()} + onChange={(value) => { + this.props.updateCellValue(value, rowIndex, `${rowIndex}.${col.key}`) + // this.setState({activeCell: null}) + + // set departure time value equal to arrival time if departure times are hidden + const nextCol = this.props.timetable.columns[colIndex + 1] + if (this.props.timetable.hideDepartureTimes && nextCol && nextCol.type === 'DEPARTURE_TIME') { + this.props.updateCellValue(value, rowIndex, `${rowIndex}.${nextCol.key}`) + } + }} /> + ) + } + getHeaderColumns () { + return this.props.columns.filter(c => c.type !== 'DEPARTURE_TIME') + } + handleEndEditing () { + this.setState({activeCell: null}) + // refocus on grid after editing is done + ReactDOM.findDOMNode(this.grid).focus() + } + _renderHeaderCell ({ columnIndex, key, rowIndex, style }) { + const col = this.getHeaderColumns()[columnIndex] + return ( + 0 && this.props.selected.length === this.props.data.length} // render column headers as active if all rows selected + title={col.title ? col.title : col.name} + label={col.name} + style={{ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + ...style + }} + /> + ) + } + _renderLeftHeaderCell ({ columnIndex, key, rowIndex, style }) { + // Select all checkbox + return ( + this.props.toggleAllRows(value)} + style={style} + selectable + /> + ) + } + _renderLeftColumnCell ({ columnIndex, key, rowIndex, style }) { + const rowSelected = this.props.selected.indexOf(rowIndex) !== -1 + // Select row checkbox + return ( + this.props.toggleRowSelection(rowIndex)} + style={style} + selectable + /> + ) + } + handleKeyPress (evt, scrollToColumn, scrollToRow) { + switch (evt.keyCode) { + case 13: // Enter + if (!this.state.activeCell) { + this.setState({activeCell: `${scrollToRow}-${scrollToColumn}`}) + } else { + this.setState({activeCell: null}) + } + break + case 8: // DELETE + // TODO: add delete cell value + // this.props.updateCellValue('', rowIndex, `${scrollToRow}.${col.key}`) + break + case 67: + // handle copy + if (evt.ctrlKey) { + console.log('copy pasta') + } + return false + default: + break + } + // input was 0-9 + if (!this.state.activeCell && evt.keyCode >= 48 && evt.keyCode <= 57 && !evt.ctrlKey) { + this.setState({activeCell: `${scrollToRow}-${scrollToColumn}`}) + } + // input was a-z + if (!this.state.activeCell && evt.keyCode >= 65 && evt.keyCode <= 90 && !evt.ctrlKey) { + this.setState({activeCell: `${scrollToRow}-${scrollToColumn}`}) + } + } + getCellRenderer (col, value) { + if (!isTimeFormat(col.type)) { + return value + } else { + if (value === 0) { + return moment().startOf('day').seconds(value).format('HH:mm:ss') + } else if (value && value >= 86400) { + const text = moment().startOf('day').seconds(value).format('HH:mm:ss') + const parts = text.split(':') + parts[0] = +parts[0] + 24 + return parts.join(':') + } else if (value && value > 0) { + return moment().startOf('day').seconds(value).format('HH:mm:ss') + } else { + return '' + } + } + } + render () { + if (this.props.columns.length === 0 && this.props.data.length === 0) { + return ( +
+ +
+ ) + } else if (this.props.data.length === 0) { + return ( + + ) + } + const { columnWidth, overscanColumnCount, overscanRowCount, rowHeight } = this.state + const columnHeaderCount = this.getHeaderColumns().length + return ( + + {({ clientHeight, clientWidth, onScroll, scrollHeight, scrollLeft, scrollTop, scrollWidth }) => { + return ( +
+ + {({ onSectionRendered, scrollToColumn, scrollToRow }) => ( +
this.handleKeyPress(evt, scrollToColumn, scrollToRow)}> +
+ {/* Top Left Cell */} + +
+
+ + {({ width, height }) => { + return ( +
+
+ {/* Left Side Column */} + +
+
+ {/* Top Header Row */} + +
+
+ {/* Primary timetable grid */} + { this.grid = Grid }} + style={{outline: 'none'}} + columnWidth={this._getColumnWidth} + columnCount={this.props.timetable.hideDepartureTimes ? columnHeaderCount : this.props.columns.length} + height={height} + onScroll={onScroll} + overscanColumnCount={overscanColumnCount} + overscanRowCount={overscanRowCount} + onSectionRendered={onSectionRendered} + cellRenderer={({ columnIndex, key, rowIndex, style }) => this._cellRenderer({ columnIndex, key, rowIndex, scrollToColumn, scrollToRow, style })} + rowHeight={rowHeight} + rowCount={this.props.data.length} + scrollToColumn={scrollToColumn} + scrollToRow={scrollToRow} + width={width - scrollbarSize() - columnWidth} /> +
+
+ ) + }} +
+
+
+ )} +
+
+ ) + }} +
+ ) + } +} diff --git a/lib/editor/components/timetable/TimetableEditor.js b/lib/editor/components/timetable/TimetableEditor.js new file mode 100644 index 000000000..05f9c335e --- /dev/null +++ b/lib/editor/components/timetable/TimetableEditor.js @@ -0,0 +1,286 @@ +import React, {Component, PropTypes} from 'react' +import clone from 'clone' +import update from 'react-addons-update' +import objectPath from 'object-path' + +import { isTimeFormat } from '../../util' +import TimetableHeader from './TimetableHeader' +import Timetable from './Timetable' + +export default class TimetableEditor extends Component { + static propTypes = { + route: PropTypes.object, + activePatternId: PropTypes.string, + activeScheduleId: PropTypes.string, + feedSource: PropTypes.object, + saveTripsForCalendar: PropTypes.func, + tableData: PropTypes.object + } + constructor (props) { + super(props) + this.state = { + activeCell: null, // 'rowNum-colNum', e.g. 0-1 + // rows: [{block: 0, gtfsTripId: 'trp', tripHeadsign: 'trip'}], + edited: [], + selected: [], + offsetSeconds: 0 + } + } + _onResize = () => { + this.setState({width: window.innerWidth, height: window.innerHeight}) + } + componentWillMount () { + this._onResize() + } + componentDidMount () { + window.addEventListener('resize', this._onResize) + } + componentWillUnmount () { + window.removeEventListener('resize', this._onResize) + } + constructNewRow (toClone = null) { + const activePattern = this.props.route && this.props.route.tripPatterns ? this.props.route.tripPatterns.find(p => p.id === this.props.activePatternId) : null + const newRow = toClone ? clone(toClone) || {} : {} + if (toClone) { + objectPath.set(newRow, 'id', 'new') + objectPath.set(newRow, 'gtfsTripId', null) + return newRow + } + // set starting time for first arrival + let cumulativeTravelTime = !toClone ? 0 : objectPath.get(newRow, `stopTimes.0.arrivalTime`) + + // TODO: auto-add offset to any new trip? No, for now. Add toggle/checkbox that allows for this. + // cumulativeTravelTime += this.props.timetable.offset + + for (let i = 0; i < activePattern.patternStops.length; i++) { + const stop = activePattern.patternStops[i] + // if stopTime null/undefined, set as new object + if (!objectPath.get(newRow, `stopTimes.${i}`)) { + objectPath.set(newRow, `stopTimes.${i}`, {}) + } + objectPath.set(newRow, `stopTimes.${i}.stopId`, stop.stopId) + cumulativeTravelTime += +stop.defaultTravelTime + + // only set time if timepoint set to true or null + // if (stop.timepoint === null || stop.timepoint) { + objectPath.set(newRow, `stopTimes.${i}.arrivalTime`, cumulativeTravelTime) + // } + cumulativeTravelTime += +stop.defaultDwellTime + // if (stop.timepoint === null || stop.timepoint) { + objectPath.set(newRow, `stopTimes.${i}.departureTime`, cumulativeTravelTime) + // } + } + for (let i = 0; i < this.props.timetable.columns.length; i++) { + const col = this.props.timetable.columns[i] + if (isTimeFormat(col.type)) { + // TODO: add default travel/dwell times to new rows + // objectPath.ensureExists(newRow, col.key, 0) + } else { + objectPath.ensureExists(newRow, col.key, null) + } + } + // important: set id to "new" + objectPath.set(newRow, 'id', 'new') + objectPath.set(newRow, 'gtfsTripId', null) + objectPath.set(newRow, 'useFrequency', activePattern.useFrequency) + objectPath.set(newRow, 'feedId', this.props.feedSource.id) + objectPath.set(newRow, 'patternId', activePattern.id) + objectPath.set(newRow, 'routeId', activePattern.routeId) + objectPath.set(newRow, 'calendarId', this.props.activeScheduleId) + + return newRow + } + duplicateRows (indexArray) { + const arrayAscending = indexArray.sort((a, b) => { + return a - b + }) + const lastIndex = this.props.timetable.trips.length - 1 + for (var i = 0; i < arrayAscending.length; i++) { + const index = arrayAscending[i] + const toClone = this.props.timetable.trips[index] + const newRow = this.constructNewRow(toClone) + const stateUpdate = { + activeCell: {$set: null}, + scrollToRow: {$set: lastIndex + arrayAscending.length}, // increment selected row + scrollToColumn: {$set: 0} + } + this.props.addNewTrip(newRow) + this.setState(update(this.state, stateUpdate)) + } + } + addNewRow (blank = false, scroll = false) { + // set blank to true if there are no rows to clone + blank = blank || this.props.timetable.trips.length === 0 + const lastIndex = this.props.timetable.trips.length - 1 + const clone = blank ? null : this.props.timetable.trips[lastIndex] + const newRow = this.constructNewRow(clone) + + const stateUpdate = { + activeCell: {$set: null}, + scrollToRow: {$set: lastIndex + 1}, // increment selected row + scrollToColumn: {$set: 0} + } + this.props.addNewTrip(newRow) + if (scroll) { + this.setState(update(this.state, stateUpdate)) + } + } + removeSelectedRows () { + const indexes = [] + const tripsToDelete = [] + const newRows = [...this.props.timetable.trips] + const selectedDescending = this.props.timetable.selected.sort((a, b) => { + return b - a + }) + // loop over selected array in descending order to ensure that indexes operates on indexes in reverse + for (var i = 0; i < selectedDescending.length; i++) { + const rowIndex = selectedDescending[i] + + // removed.push([this.props.selected[i], 1]) + const row = newRows[rowIndex] + if (row.id === 'new') { + indexes.push([rowIndex, 1]) + } else { + tripsToDelete.push(row) + } + } + if (tripsToDelete.length > 0) { + this.props.deleteTripsForCalendar(this.props.feedSource.id, this.props.activePattern, this.props.activeScheduleId, tripsToDelete) + } + this.props.removeTrips(indexes) + this.props.toggleAllRows(false) + } + componentWillReceiveProps (nextProps) { + // console.log('receiving props') + // const activePattern = nextProps.route && nextProps.route.tripPatterns ? nextProps.route.tripPatterns.find(p => p.id === nextProps.activePatternId) : null + // const activeSchedule = nextProps.tableData.calendar ? nextProps.tableData.calendar.find(c => c.id === nextProps.activeScheduleId) : null + // const trips = activePattern && activeSchedule ? activePattern[nextProps.activeScheduleId] : [] + // // add unsaved trips to list of trips received + // if (nextProps.timetable.edited.length > 0) { + // console.log('changes found', nextProps.timetable.edited.length) + // for (var i = 0; i < nextProps.timetable.edited.length; i++) { + // let rowIndex = nextProps.timetable.edited[i] + // let trip = nextProps.timetable.trips[rowIndex] + // if (trip) { + // trips.push(trip) + // } + // } + // } + } + shouldComponentUpdate (nextProps) { + return true + } + offsetRows (rowIndexes, offsetAmount) { + const newRows = [...this.props.timetable.trips] + const editedRows = [] + console.log(`Offsetting ${rowIndexes.length} rows by ${offsetAmount} seconds`) + for (var i = 0; i < rowIndexes.length; i++) { + editedRows.push(rowIndexes[i]) + for (var j = 0; j < this.props.timetable.columns.length; j++) { + const col = this.props.timetable.columns[j] + const path = `${rowIndexes[i]}.${col.key}` + if (isTimeFormat(col.type)) { + const currentVal = objectPath.get(newRows, path) + const value = currentVal + offsetAmount % 86399 // ensure seconds does not exceed 24 hours + objectPath.set(newRows, path, value) + // this.props.updateCellValue(value, i, path) + } + } + } + const stateUpdate = { + data: {$set: newRows}, + edited: {$push: editedRows} + } + this.setState(update(this.state, stateUpdate)) + } + saveEditedTrips (pattern, activeScheduleId) { + const trips = [] + const tripIndexes = [] + for (var i = 0; i < this.props.timetable.edited.length; i++) { + const rowIndex = this.props.timetable.edited[i] + if (tripIndexes.indexOf(rowIndex) === -1) { + const trip = this.props.timetable.trips[rowIndex] + if (trip) { + trips.push(trip) + tripIndexes.push(rowIndex) + } + } + } + this.props.saveTripsForCalendar(this.props.feedSource.id, pattern, activeScheduleId, trips) + .then((errorIndexes) => { + console.log('errors for trips', errorIndexes) + const edited = [] + for (var i = 0; i < errorIndexes.length; i++) { + edited.push(this.props.timetable.edited[errorIndexes[i]]) + } + console.log(edited) + const stateUpdate = { + edited: {$set: edited} + } + this.setState(update(this.state, stateUpdate)) + }) + } + isDataValid (col, value, previousValue) { + if (isTimeFormat(col.type)) { + return value && value >= 0 && value < previousValue + } else { + return true + } + } + render () { + const { feedSource, activePattern, activeSchedule } = this.props + + const panelStyle = { + backgroundColor: 'white', + paddingRight: '5px', + paddingLeft: '5px' + } + const HEADER_HEIGHT = 118 + return ( +
+ this.removeSelectedRows()} + offsetRows={(rowIndexes, offsetAmount) => this.offsetRows(rowIndexes, offsetAmount)} + addNewRow={(blank, scroll) => this.addNewRow(blank, scroll)} + duplicateRows={(indexArray) => this.duplicateRows(indexArray)} + saveEditedTrips={(pattern, scheduleId) => this.saveEditedTrips(pattern, scheduleId)} + {...this.props} /> + {activeSchedule + ? this.constructNewRow(clone)} + addNewRow={(blank, scroll) => this.addNewRow(blank, scroll)} + toggleRowSelection={(rowIndex) => this.props.toggleRowSelection(rowIndex)} + toggleAllRows={(select) => this.props.toggleAllRows(select)} + selected={this.props.timetable.selected} + scrollToRow={this.state.scrollToRow} + scrollToColumn={this.state.scrollToColumn} + activePattern={activePattern} + data={this.props.timetable.trips} + columns={this.props.timetable.columns} + {...this.props} /> + :

+ {activePattern + ? + Choose a calendar to edit timetables or + {' '} + { + e.preventDefault() + this.props.setActiveEntity(feedSource.id, 'calendar', {id: 'new'}) + }} + >create a new one. + + : Choose a trip pattern. + } +

+ } +
+ ) + } +} diff --git a/lib/editor/components/timetable/TimetableHeader.js b/lib/editor/components/timetable/TimetableHeader.js new file mode 100644 index 000000000..f75f57874 --- /dev/null +++ b/lib/editor/components/timetable/TimetableHeader.js @@ -0,0 +1,196 @@ +import React, {Component, PropTypes} from 'react' +import { InputGroup, Col, Row, Checkbox, Button, Form, OverlayTrigger, Tooltip, ButtonGroup } from 'react-bootstrap' +import truncate from 'truncate' +import {Icon} from '@conveyal/woonerf' + +import HourMinuteInput from '../HourMinuteInput' +import CalendarSelect from './CalendarSelect' +import RouteSelect from './RouteSelect' +import PatternSelect from './PatternSelect' +import {getConfigProperty} from '../../../common/util/config' + +export default class TimetableHeader extends Component { + static propTypes = { + feedSource: PropTypes.object + } + render () { + const { feedSource, timetable, setOffset, offsetRows, toggleDepartureTimes, addNewRow, removeSelectedRows, saveEditedTrips, route, tableData, activeScheduleId, activePattern, setActiveEntity, fetchTripsForCalendar, duplicateRows } = this.props + const { selected, trips, hideDepartureTimes, edited, offset } = timetable + const calendars = tableData.calendar || [] + const activeCalendar = calendars.find(c => c.id === activeScheduleId) + const headerStyle = { + backgroundColor: 'white' + } + const tableType = activePattern && activePattern.useFrequency + ? 'Frequency editor' + : 'Timetable editor' + const patternName = activePattern && activePattern.name + const calendarName = activeCalendar && activeCalendar.service_id + const numberOfTrips = trips ? trips.length : 0 + return ( +
+ + +

+ Back to route}> + + + {tableType} +

+ + +

+ {numberOfTrips} trips for {truncate(patternName, 15)} on {truncate(calendarName, 13)} calendar +

+ + + + {activePattern && !activePattern.useFrequency && getConfigProperty('application.dev') + ? { + toggleDepartureTimes() + }} + > + Hiding departure times will keep arrival and departure times in sync. WARNING: do not use if arrivals and departures differ.}> + Hide departures + + + : null + } + {' '} + + { + setOffset(seconds) + }} + /> + + + + + + +
+ + + + + + + + + + + + + Add blank trip}> + + + Duplicate trips}> + + + Delete trips}> + + + Undo changes}> + + + Save changes}> + + + + + +
+ ) + } +} diff --git a/src/main/client/editor/containers/ActiveEditorFeedSourcePanel.js b/lib/editor/containers/ActiveEditorFeedSourcePanel.js similarity index 64% rename from src/main/client/editor/containers/ActiveEditorFeedSourcePanel.js rename to lib/editor/containers/ActiveEditorFeedSourcePanel.js index 1fa3c3e8a..b20bfdb82 100644 --- a/src/main/client/editor/containers/ActiveEditorFeedSourcePanel.js +++ b/lib/editor/containers/ActiveEditorFeedSourcePanel.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' -import { fetchSnapshots, restoreSnapshot, deleteSnapshot, loadFeedVersionForEditing } from '../actions/snapshots.js' -import { createFeedVersionFromSnapshot } from '../../manager/actions/feeds' +import { fetchSnapshots, restoreSnapshot, deleteSnapshot, loadFeedVersionForEditing, downloadSnapshotViaToken, createSnapshot } from '../actions/snapshots.js' +import { createFeedVersionFromSnapshot } from '../../manager/actions/versions' import EditorFeedSourcePanel from '../components/EditorFeedSourcePanel' @@ -12,12 +12,17 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch, ownProps) => { return { getSnapshots: (feedSource) => { dispatch(fetchSnapshots(feedSource)) }, + createSnapshot: (feedSource, name, comments) => { + dispatch(createSnapshot(feedSource, name, comments)) + .then(() => { + dispatch(fetchSnapshots(feedSource)) + }) + }, restoreSnapshot: (feedSource, snapshot) => { dispatch(restoreSnapshot(feedSource, snapshot)) }, deleteSnapshot: (feedSource, snapshot) => { dispatch(deleteSnapshot(feedSource, snapshot)) }, + downloadSnapshot: (feedSource, snapshot) => { dispatch(downloadSnapshotViaToken(feedSource, snapshot)) }, exportSnapshotAsVersion: (feedSource, snapshot) => { dispatch(createFeedVersionFromSnapshot(feedSource, snapshot.id)) }, - loadFeedVersionForEditing: (feedVersion) => { - dispatch(loadFeedVersionForEditing(feedVersion)) - } + loadFeedVersionForEditing: (feedVersion) => { dispatch(loadFeedVersionForEditing(feedVersion)) } } } diff --git a/lib/editor/containers/ActiveEntityList.js b/lib/editor/containers/ActiveEntityList.js new file mode 100644 index 000000000..862a4250f --- /dev/null +++ b/lib/editor/containers/ActiveEntityList.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux' + +import { getEntityName } from '../util/gtfs' +import EntityList from '../components/EntityList' + +const mapStateToProps = (state, ownProps) => { + const entity = + state.editor.data.active && state.editor.data.active.entity && state.editor.data.active.entity.id === ownProps.activeEntityId + ? state.editor.data.active.entity + : state.editor.data.active && state.editor.data.active.entity && ownProps.activeComponent === 'feedinfo' + ? state.editor.data.active.entity + : null + const activeEntity = entity + ? { + name: getEntityName(ownProps.activeComponent, entity), + id: entity.id + } + : null + return { + activeEntity + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { return { } } + +const ActiveEntityList = connect(mapStateToProps, mapDispatchToProps)(EntityList) + +export default ActiveEntityList diff --git a/src/main/client/editor/containers/ActiveFeedInfoPanel.js b/lib/editor/containers/ActiveFeedInfoPanel.js similarity index 93% rename from src/main/client/editor/containers/ActiveFeedInfoPanel.js rename to lib/editor/containers/ActiveFeedInfoPanel.js index f00457116..abbe88dfe 100644 --- a/src/main/client/editor/containers/ActiveFeedInfoPanel.js +++ b/lib/editor/containers/ActiveFeedInfoPanel.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { createSnapshot, fetchSnapshots, restoreSnapshot } from '../actions/snapshots' -import { displayRoutesShapefile } from '../actions/editor' +import { displayRoutesShapefile } from '../actions/map' import FeedInfoPanel from '../components/FeedInfoPanel' const mapStateToProps = (state, ownProps) => { return { } } diff --git a/lib/editor/containers/ActiveGtfsEditor.js b/lib/editor/containers/ActiveGtfsEditor.js new file mode 100644 index 000000000..f909717fd --- /dev/null +++ b/lib/editor/containers/ActiveGtfsEditor.js @@ -0,0 +1,253 @@ +import { connect } from 'react-redux' + +import GtfsEditor from '../components/GtfsEditor' +import { fetchFeedSourceAndProject } from '../../manager/actions/feeds' +import { fetchFeedInfo } from '../actions/feedInfo' +import { + fetchStops, + fetchStopsForTripPattern +} from '../actions/stop' +import { + fetchTripPatternsForRoute, + fetchTripPatterns, + undoActiveTripPatternEdits +} from '../actions/tripPattern' +import { + removeStopFromPattern, + addStopAtPoint, + addStopAtIntersection, + addStopAtInterval, + addStopToPattern } from '../actions/map/stopStrategies' +import { + updateControlPoint, + addControlPoint, + removeControlPoint, + updateMapSetting, + constructControlPoint, + updatePatternCoordinates +} from '../actions/map' +import { fetchTripsForCalendar } from '../actions/trip' +import { updateEditSetting, + setActiveGtfsEntity, + deleteGtfsEntity, + updateActiveGtfsEntity, + resetActiveGtfsEntity, + clearGtfsContent, + saveActiveGtfsEntity } from '../actions/active' +import { + fetchActiveTable, + newGtfsEntity, + newGtfsEntities, + cloneGtfsEntity, + getGtfsTable, + receiveGtfsEntities, + uploadBrandingAsset +} from '../actions/editor' +import { updateUserMetadata } from '../../manager/actions/user' +import { findProjectByFeedSource } from '../../manager/util' +import { setTutorialHidden } from '../../manager/actions/ui' + +const mapStateToProps = (state, ownProps) => { + const { + feedSourceId, + activeComponent, + subComponent, + subSubComponent, + activeEntityId, + subEntityId, + activeSubSubEntity + } = ownProps.routeParams + const activeEntity = + state.editor.data.active.entity && state.editor.data.active.entity.id === activeEntityId + ? state.editor.data.active.entity + : state.editor.data.active.entity && activeComponent === 'feedinfo' + ? state.editor.data.active.entity + : null + const activePattern = state.editor.data.active.subEntity + const entityEdited = subComponent === 'trippattern' + ? state.editor.data.active.patternEdited + : state.editor.data.active.edited + + const controlPoints = state.editor.editSettings.controlPoints && state.editor.editSettings.controlPoints.length + ? state.editor.editSettings.controlPoints[state.editor.editSettings.controlPoints.length - 1] + : [] + const editSettings = state.editor.editSettings + const mapState = state.editor.mapState + const stopTree = state.editor.mapState.stopTree + const tableView = ownProps.location.query && ownProps.location.query.table === 'true' + const entities = state.editor.data.tables[activeComponent] + const user = state.user + + // find the containing project + const project = findProjectByFeedSource(state, feedSourceId) + const feedSource = project && project.feedSources.find(fs => fs.id === feedSourceId) + + const feedInfo = state.editor.data.tables.feedinfo + + return { + tableData: state.editor.data.tables, + hideTutorial: state.ui.hideTutorial, + tripPatterns: state.editor.tripPatterns, + feedSource, + entities, + feedSourceId, + feedInfo, + entityEdited, + tableView, + project, + user, + activeComponent, + subSubComponent, + subComponent, + activeEntity, + activeEntityId, + subEntityId, + activePattern, + activeSubSubEntity, + editSettings, + mapState, + stopTree, + controlPoints, + sidebarExpanded: state.ui.sidebarExpanded + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + const { + feedSourceId, + activeComponent, + subComponent, + subSubComponent, + activeEntityId, + subEntityId, + activeSubSubEntity + } = ownProps.routeParams + return { + updateUserMetadata: (profile, props) => { + dispatch(updateUserMetadata(profile, props)) + }, + onComponentMount: (initialProps) => { + const tablesToFetch = ['calendar', 'agency', 'route', 'stop'] + + // Get all GTFS tables except for the active table (activeComponent) + if (!initialProps.feedSource || feedSourceId !== initialProps.feedSource.id) { + dispatch(fetchFeedSourceAndProject(feedSourceId)) + .then(() => { + dispatch(fetchFeedInfo(feedSourceId)) + for (var i = 0; i < tablesToFetch.length; i++) { + if (tablesToFetch[i] !== activeComponent) { + console.log('requesting ' + tablesToFetch[i]) + dispatch(getGtfsTable(tablesToFetch[i], feedSourceId)) + } + } + }) + .then(() => { + var newId = activeEntityId + dispatch(fetchActiveTable(activeComponent, newId, activeEntityId, feedSourceId, subComponent, subEntityId, subSubComponent, activeSubSubEntity)) + }) + } else { + dispatch(fetchFeedInfo(feedSourceId)) + for (var i = 0; i < tablesToFetch.length; i++) { + console.log(activeComponent, initialProps.tableData[activeComponent]) + if (tablesToFetch[i] !== activeComponent) { + dispatch(getGtfsTable(tablesToFetch[i], feedSourceId)) + // TODO: add setActive here (e.g., for redirections from validation summary) + } + } + var newId = activeEntityId + dispatch(fetchActiveTable(activeComponent, newId, activeEntityId, feedSourceId, subComponent, subEntityId, subSubComponent, activeSubSubEntity)) + } + + // TODO: replace fetch trip patterns with map layer + // dispatch(fetchTripPatterns(feedSourceId)) + }, + onComponentUpdate: (prevProps, newProps) => { + // handle back button presses by re-setting active gtfs entity + if (prevProps.activeEntityId !== 'new' && + (prevProps.activeComponent !== newProps.activeComponent || + prevProps.activeEntityId !== newProps.activeEntityId || + prevProps.subComponent !== newProps.subComponent || + prevProps.subEntityId !== newProps.subEntityId || + prevProps.subSubComponent !== newProps.subSubComponent || + prevProps.activeSubSubEntity !== newProps.activeSubSubEntity) + ) { + console.log('handling back button') + dispatch(setActiveGtfsEntity(feedSourceId, activeComponent, activeEntityId, subComponent, subEntityId, subSubComponent, activeSubSubEntity)) + } + }, + + // NEW GENERIC GTFS/EDITOR FUNCTIONS + getGtfsTable: (activeComponent, feedSourceId) => dispatch(getGtfsTable(activeComponent, feedSourceId)), + updateEditSetting: (setting, value) => { + dispatch(updateEditSetting(setting, value)) + }, + updateMapSetting: (props) => { + dispatch(updateMapSetting(props)) + }, + gtfsEntitySelected: (type, entity) => { + dispatch(receiveGtfsEntities([entity])) + }, + setActiveEntity: (feedSourceId, component, entity, subComponent, subEntity, subSubComponent, subSubEntity) => { + const entityId = entity && entity.id + const subEntityId = subEntity && subEntity.id + const subSubEntityId = subSubEntity && subSubEntity.id + dispatch(setActiveGtfsEntity(feedSourceId, component, entityId, subComponent, subEntityId, subSubComponent, subSubEntityId)) + }, + updateActiveEntity: (entity, component, props) => { + dispatch(updateActiveGtfsEntity(entity, component, props)) + }, + resetActiveEntity: (entity, component) => { + dispatch(resetActiveGtfsEntity(entity, component)) + }, + deleteEntity: (feedSourceId, component, entityId, routeId) => { + dispatch(deleteGtfsEntity(feedSourceId, component, entityId, routeId)) + }, + saveActiveEntity: (component) => { + return dispatch(saveActiveGtfsEntity(component)) + }, + cloneEntity: (feedSourceId, component, entityId, save) => { + dispatch(cloneGtfsEntity(feedSourceId, component, entityId, save)) + }, + newGtfsEntity: (feedSourceId, component, props, save) => { + return dispatch(newGtfsEntity(feedSourceId, component, props, save)) + }, + newGtfsEntities: (feedSourceId, component, propsArray, save) => { + return dispatch(newGtfsEntities(feedSourceId, component, propsArray, save)) + }, + + // ENTITY-SPECIFIC FUNCTIONS + uploadBrandingAsset: (feedSourceId, entityId, component, file) => { + dispatch(uploadBrandingAsset(feedSourceId, entityId, component, file)) + }, + + clearGtfsContent: () => { dispatch(clearGtfsContent()) }, + fetchTripPatternsForRoute: (feedSourceId, routeId) => { dispatch(fetchTripPatternsForRoute(feedSourceId, routeId)) }, + fetchTripPatterns: (feedSourceId) => { dispatch(fetchTripPatterns(feedSourceId)) }, + fetchStopsForTripPattern: (feedSourceId, tripPatternId) => { dispatch(fetchStopsForTripPattern(feedSourceId, tripPatternId)) }, + fetchStops: (feedSourceId) => { dispatch(fetchStops(feedSourceId)) }, + fetchTripsForCalendar: (feedSourceId, pattern, calendarId) => dispatch(fetchTripsForCalendar(feedSourceId, pattern, calendarId)), + + // TRIP PATTERN EDIT FUNCTIONS + undoActiveTripPatternEdits: () => { dispatch(undoActiveTripPatternEdits()) }, + updatePatternCoordinates: (coordinates) => { dispatch(updatePatternCoordinates(coordinates)) }, + removeStopFromPattern: (pattern, stop, index, controlPoints) => dispatch(removeStopFromPattern(pattern, stop, index, controlPoints)), + addStopToPattern: (pattern, stop, index) => dispatch(addStopToPattern(pattern, stop, index)), + addStopAtPoint: (latlng, addToPattern, index, activePattern) => dispatch(addStopAtPoint(latlng, addToPattern, index, activePattern)), + addStopAtInterval: (latlng, activePattern) => dispatch(addStopAtInterval(latlng, activePattern)), + addStopAtIntersection: (latlng, activePattern) => dispatch(addStopAtIntersection(latlng, activePattern)), + addControlPoint: (controlPoint, index) => { dispatch(addControlPoint(controlPoint, index)) }, + removeControlPoint: (index, begin, end, pattern, polyline) => { dispatch(removeControlPoint(index, begin, end, pattern, polyline)) }, + updateControlPoint: (index, point, distance) => { dispatch(updateControlPoint(index, point, distance)) }, + constructControlPoint: (pattern, latlng, controlPoints) => { dispatch(constructControlPoint(pattern, latlng, controlPoints)) }, + + // EDITOR UI + setTutorialHidden: (value) => { dispatch(setTutorialHidden(value)) } + } +} + +const ActiveGtfsEditor = connect( + mapStateToProps, + mapDispatchToProps +)(GtfsEditor) + +export default ActiveGtfsEditor diff --git a/src/main/client/editor/containers/ActiveGtfsVersionSummary.js b/lib/editor/containers/ActiveGtfsVersionSummary.js similarity index 90% rename from src/main/client/editor/containers/ActiveGtfsVersionSummary.js rename to lib/editor/containers/ActiveGtfsVersionSummary.js index 30af998c5..8bb2fdb24 100644 --- a/src/main/client/editor/containers/ActiveGtfsVersionSummary.js +++ b/lib/editor/containers/ActiveGtfsVersionSummary.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' -import GtfsVersionSummary from '../components/GtfsVersionSummary' +import GtfsVersionSummary from '../components/GtfsVersionSummary' import { downloadGtfsFeed, diff --git a/lib/editor/containers/ActiveTimetableEditor.js b/lib/editor/containers/ActiveTimetableEditor.js new file mode 100644 index 000000000..739f98e40 --- /dev/null +++ b/lib/editor/containers/ActiveTimetableEditor.js @@ -0,0 +1,45 @@ +import { connect } from 'react-redux' +import { + saveTripsForCalendar, + deleteTripsForCalendar, + updateCellValue, + toggleRowSelection, + toggleAllRows, + toggleDepartureTimes, + addNewTrip, + removeTrips, + setOffset +} from '../actions/trip' + +import TimetableEditor from '../components/timetable/TimetableEditor' + +const mapStateToProps = (state, ownProps) => { + const activePattern = state.editor.data.active.subEntity + const activeSchedule = state.editor.data.tables.calendar ? state.editor.data.tables.calendar.find(c => c.id === ownProps.activeScheduleId) : null + return { + activePattern, + activeSchedule, + timetable: state.editor.timetable + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + // NOTE: fetchTripsForCalendar is handled in ActiveGtfsEditor where it is used to fetch trips + saveTripsForCalendar: (feedSourceId, pattern, calendarId, trips) => dispatch(saveTripsForCalendar(feedSourceId, pattern, calendarId, trips)), + deleteTripsForCalendar: (feedSourceId, pattern, calendarId, trips) => dispatch(deleteTripsForCalendar(feedSourceId, pattern, calendarId, trips)), + + // TIMETABLE FUNCTIONS + updateCellValue: (value, rowIndex, key) => dispatch(updateCellValue(value, rowIndex, key)), + addNewTrip: (trip) => dispatch(addNewTrip(trip)), + removeTrips: (indexes) => dispatch(removeTrips(indexes)), + toggleAllRows: (select) => dispatch(toggleAllRows(select)), + toggleRowSelection: (rowIndex) => dispatch(toggleRowSelection(rowIndex)), + toggleDepartureTimes: () => dispatch(toggleDepartureTimes()), + setOffset: (seconds) => dispatch(setOffset(seconds)) + } +} + +const ActiveTimetableEditor = connect(mapStateToProps, mapDispatchToProps)(TimetableEditor) + +export default ActiveTimetableEditor diff --git a/lib/editor/containers/ActiveTripPatternList.js b/lib/editor/containers/ActiveTripPatternList.js new file mode 100644 index 000000000..2ab7d224a --- /dev/null +++ b/lib/editor/containers/ActiveTripPatternList.js @@ -0,0 +1,89 @@ +import { connect } from 'react-redux' +import { + updateEditSetting, + setActiveGtfsEntity, + deleteGtfsEntity, + saveActiveGtfsEntity, + updateActiveGtfsEntity, + resetActiveGtfsEntity } from '../actions/active' +import { + newGtfsEntity, + updateMapSetting, + cloneGtfsEntity } from '../actions/editor' +import { + // removeStopFromPattern, + // addStopAtPoint, + // addStopAtIntersection, + // addStopAtInterval, + addStopToPattern } from '../actions/map/stopStrategies' +import { setErrorMessage } from '../../manager/actions/status' +import { undoActiveTripPatternEdits } from '../actions/tripPattern' +import { findProjectByFeedSource } from '../../manager/util' + +import TripPatternList from '../components/pattern/TripPatternList' + +const mapStateToProps = (state, ownProps) => { + const mapState = state.editor.mapState + const editSettings = state.editor.editSettings + const stops = state.editor.data.tables.stop + const activePattern = state.editor.data.active.subEntity + const feedSourceId = state.editor.data.active.feedSourceId + // find the containing project + const project = findProjectByFeedSource(state, feedSourceId) + const feedSource = project && project.feedSources.find(fs => fs.id === feedSourceId) + + const activeEntity = state.editor.data.active.entity + // const subSubComponent = state.editor.data.active.subSubComponent + const activePatternId = state.editor.data.active.subEntityId + + return { + mapState, + editSettings, + stops, + activePattern, + feedSource, + activeEntity, + activePatternId + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + + // NEW GENERIC GTFS/EDITOR FUNCTIONS + updateEditSetting: (setting, value, activePattern) => dispatch(updateEditSetting(setting, value, activePattern)), + updateMapSetting: (props) => dispatch(updateMapSetting(props)), + setActiveEntity: (feedSourceId, component, entity, subComponent, subEntity, subSubComponent, subSubEntity) => { + const entityId = entity && entity.id + const subEntityId = subEntity && subEntity.id + const subSubEntityId = subSubEntity && subSubEntity.id + dispatch(setActiveGtfsEntity(feedSourceId, component, entityId, subComponent, subEntityId, subSubComponent, subSubEntityId)) + }, + updateActiveEntity: (entity, component, props) => { + dispatch(updateActiveGtfsEntity(entity, component, props)) + }, + resetActiveEntity: (entity, component) => { + dispatch(resetActiveGtfsEntity(entity, component)) + }, + deleteEntity: (feedSourceId, component, entityId, routeId) => { + dispatch(deleteGtfsEntity(feedSourceId, component, entityId, routeId)) + }, + saveActiveEntity: (component) => { + return dispatch(saveActiveGtfsEntity(component)) + }, + cloneEntity: (feedSourceId, component, entityId, save) => { + dispatch(cloneGtfsEntity(feedSourceId, component, entityId, save)) + }, + newGtfsEntity: (feedSourceId, component, props, save) => { + return dispatch(newGtfsEntity(feedSourceId, component, props, save)) + }, + + addStopToPattern: (pattern, stop, index) => dispatch(addStopToPattern(pattern, stop, index)), + undoActiveTripPatternEdits: () => { dispatch(undoActiveTripPatternEdits()) }, + setErrorMessage: (message) => { dispatch(setErrorMessage(message)) } + } +} + +const ActiveTripPatternList = connect(mapStateToProps, mapDispatchToProps)(TripPatternList) + +export default ActiveTripPatternList diff --git a/lib/editor/props/index.js b/lib/editor/props/index.js new file mode 100644 index 000000000..4a278f3d0 --- /dev/null +++ b/lib/editor/props/index.js @@ -0,0 +1,35 @@ +import { PropTypes } from 'react' + +export const EditorMapProps = { + subEntityId: PropTypes.string, + activeEntityId: PropTypes.string, + activeComponent: PropTypes.string, + subComponent: PropTypes.string, + currentPattern: PropTypes.object, + stops: PropTypes.array, + stopTree: PropTypes.object, + editSettings: PropTypes.object, + mapState: PropTypes.object, + feedSource: PropTypes.object, + feedInfo: PropTypes.object, + zoomToTarget: PropTypes.string, + entities: PropTypes.array, + activeEntity: PropTypes.object, + entityEdited: PropTypes.bool, + offset: PropTypes.number, + tripPatterns: PropTypes.array, + updateActiveEntity: PropTypes.func, + saveActiveEntity: PropTypes.func, + setActiveEntity: PropTypes.func, + updateControlPoint: PropTypes.func, + newGtfsEntity: PropTypes.func, + fetchTripPatterns: PropTypes.func, + updateMapSetting: PropTypes.func, + addControlPoint: PropTypes.func, + removeControlPoint: PropTypes.func, + updateUserMetadata: PropTypes.func, + user: PropTypes.object, + sidebarExpanded: PropTypes.bool, + hidden: PropTypes.bool, + drawStops: PropTypes.bool // whether to draw stops or not (based on zoom level) +} diff --git a/lib/editor/reducers/data.js b/lib/editor/reducers/data.js new file mode 100644 index 000000000..0fd85aba9 --- /dev/null +++ b/lib/editor/reducers/data.js @@ -0,0 +1,465 @@ +import update from 'react-addons-update' +import clone from 'clone' +import ll from 'lonlng' + +import { stopToGtfs, routeToGtfs, agencyToGtfs, calendarToGtfs, fareToGtfs, gtfsSort } from '../util/gtfs' +import { getStopsForPattern } from '../util' + +const defaultState = { + active: {}, + tables: {} +} +const data = (state = defaultState, action) => { + let stateUpdate, key, newTableData, activeEntity, activeSubEntity, newState, routeIndex, stopIndex, patternIndex, agencyIndex, fareIndex, calendarIndex, scheduleExceptionIndex, activePattern + const { type, component } = action + switch (type) { + case 'REQUESTING_FEED_INFO': + if (state.feedSourceId && action.feedId !== state.feedSourceId) { + return defaultState + } + return state + case 'CREATE_GTFS_ENTITY': + if (component === 'trippattern') { + activeEntity = { + isCreating: true, + name: '', + id: 'new', + feedId: action.feedSourceId, + ...action.props + } + routeIndex = state.tables.route.findIndex(r => r.id === action.props.routeId) + return update(newState || state, { + tables: {route: {[routeIndex]: {tripPatterns: {$unshift: [activeEntity]}}}}, + active: { + entity: {tripPatterns: {$unshift: [activeEntity]}} + // edited: {$set: typeof action.props !== 'undefined'} + } + }) + } else { + activeEntity = { + isCreating: true, + name: '', + id: 'new', + feedId: action.feedSourceId, + ...action.props + } + // if tables's component array is undefined, add it + if (!state.tables[component]) { + newState = update(state, { + tables: {[component]: {$set: []}} + }) + } + return update(newState || state, { + tables: {[component]: {$unshift: [activeEntity]}} + // active: { + // entity: {$set: activeEntity}, + // edited: {$set: typeof action.props !== 'undefined'} + // } + }) + } + case 'SETTING_ACTIVE_GTFS_ENTITY': + activeEntity = component === 'feedinfo' + ? clone(state.tables[component]) + : state.tables[component] && action.entityId + ? clone(state.tables[component].find(e => e.id === action.entityId)) + : null + switch (action.subComponent) { + case 'trippattern': + activeSubEntity = activeEntity && activeEntity.tripPatterns + ? clone(activeEntity.tripPatterns.find(p => p.id === action.subEntityId)) + : null + if (activeSubEntity) { + activeSubEntity.stops = clone(getStopsForPattern(activeSubEntity, state.tables.stop)) + } + break + } + const active = { + feedSourceId: action.feedSourceId, + entity: activeEntity, + entityId: action.entityId, + subEntity: activeSubEntity, + subEntityId: action.subEntityId, + subSubEntityId: action.subSubEntityId, + component: component, + subComponent: action.subComponent, + subSubComponent: action.subSubComponent, + edited: activeEntity && activeEntity.id === 'new' + } + return update(state, { + active: {$set: active} + }) + case 'RESET_ACTIVE_GTFS_ENTITY': + switch (component) { + case 'trippattern': + patternIndex = state.active.entity.tripPatterns.findIndex(p => p.id === action.entity.id) + activeEntity = Object.assign({}, state.active.entity.tripPatterns[patternIndex]) + return update(state, { + active: { + subEntity: {$set: activeEntity}, + patternEdited: {$set: false} + } + }) + case 'feedinfo': + activeEntity = Object.assign({}, state.tables[component]) + return update(state, { + active: { + entity: {$set: activeEntity}, + edited: {$set: false} + } + }) + default: + activeEntity = state.tables[component].find(e => e.id === action.entity.id) + return update(state, { + active: { + entity: {$set: activeEntity}, + edited: {$set: false} + } + }) + } + case 'SAVED_TRIP_PATTERN': + routeIndex = state.tables.route.findIndex(r => r.id === action.tripPattern.routeId) + patternIndex = state.active.entity.tripPatterns.findIndex(p => p.id === action.tripPattern.id) + stateUpdate = {active: {}} + + // if pattern is active + if (action.tripPattern.id === state.active.subEntityId) { + stateUpdate = { + active: { + subEntity: {$set: action.tripPattern}, + patternEdited: {$set: false} + } + } + } + // if not active, but present in active route + if (patternIndex !== -1) { + stateUpdate.tables = {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$set: action.tripPattern}}}}} + stateUpdate.active.entity = {tripPatterns: {[patternIndex]: {$set: action.tripPattern}}} + } else if (action.tripPattern.routeId === state.active.entity.id) { // if pattern is entirely new + const patterns = clone(state.active.entity.tripPatterns) + patterns.push(action.tripPattern) + return update(state, { + tables: {route: {[routeIndex]: {tripPatterns: {$push: [action.tripPattern]}}}}, + active: {entity: {tripPatterns: {$set: patterns}}} + }) + } + return update(state, stateUpdate) + case 'UPDATE_ACTIVE_GTFS_ENTITY': + switch (component) { + case 'trippattern': + patternIndex = state.active.entity.tripPatterns.findIndex(p => p.id === action.entity.id) + activeEntity = Object.assign({}, state.active.subEntity) + for (key in action.props) { + activeEntity[key] = action.props[key] + } + stateUpdate = { + active: { + subEntity: {$set: activeEntity}, + patternEdited: {$set: true} + } + } + return update(state, stateUpdate) + default: + activeEntity = Object.assign({}, state.active.entity) + for (key in action.props) { + activeEntity[key] = action.props[key] + } + return update(state, { + active: { + entity: {$set: activeEntity}, + edited: {$set: true} + } + }) + } + case 'RECEIVE_AGENCIES': + const agencies = action.agencies.map(agencyToGtfs) + agencies.sort(gtfsSort) + agencyIndex = state.active.entity && agencies.findIndex(a => a.id === state.active.entity.id) + if (agencyIndex !== -1) { + return update(state, { + tables: {agency: {$set: agencies}}, + active: { + entity: {$set: agencies[agencyIndex]}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {agency: {$set: agencies}} + }) + } + case 'RECEIVE_FARES': + const fares = action.fares.map(fareToGtfs) + fares.sort(gtfsSort) + fareIndex = state.active.entity && fares.findIndex(f => f.id === state.active.entity.id) + if (fareIndex !== -1) { + return update(state, { + tables: {fare: {$set: fares}}, + active: { + entity: {$set: fares[fareIndex]}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {fare: {$set: fares}} + }) + } + case 'RECEIVE_FEED_INFO': + const feedInfo = action.feedInfo + ? { + // datatools props + id: action.feedInfo.id, + color: action.feedInfo.color, + defaultLat: action.feedInfo.defaultLat, + defaultLon: action.feedInfo.defaultLon, + defaultRouteType: action.feedInfo.defaultRouteType, + + // gtfs spec props + feed_end_date: action.feedInfo.feedEndDate, + feed_start_date: action.feedInfo.feedStartDate, + feed_lang: action.feedInfo.feedLang, + feed_publisher_name: action.feedInfo.feedPublisherName, + feed_publisher_url: action.feedInfo.feedPublisherUrl, + feed_version: action.feedInfo.feedVersion + } + : { + // datatools props + id: null, + color: null, + defaultLat: null, + defaultLon: null, + defaultRouteType: null, + + // gtfs spec props + feed_end_date: null, + feed_start_date: null, + feed_lang: null, + feed_publisher_name: null, + feed_publisher_url: null, + feed_version: null + } + if (state.active.component === 'feedinfo') { + return update(state, { + tables: {feedinfo: {$set: feedInfo}}, + active: { + entity: {$set: feedInfo}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {feedinfo: {$set: feedInfo}} + }) + } + case 'RECEIVE_CALENDARS': + const calendars = action.calendars ? action.calendars.map(calendarToGtfs) : null + calendars.sort(gtfsSort) + calendarIndex = state.active.entity && calendars.findIndex(c => c.id === state.active.entity.id) + if (calendarIndex !== -1) { + return update(state, { + tables: {calendar: {$set: calendars}}, + active: { + entity: {$set: calendars[calendarIndex]}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {calendar: {$set: calendars}} + }) + } + case 'RECEIVE_SCHEDULE_EXCEPTIONS': + const scheduleExceptions = action.scheduleExceptions || [] // no mapping required + scheduleExceptionIndex = state.active.entity && action.scheduleExceptions.findIndex(se => se.id === state.active.entity.id) + if (scheduleExceptionIndex !== -1) { + return update(state, { + tables: {scheduleexception: {$set: scheduleExceptions}}, + active: { + entity: {$set: scheduleExceptions[scheduleExceptionIndex]}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {scheduleexception: {$set: scheduleExceptions}} + }) + } + case 'RECEIVE_ROUTES': + const routes = action.routes ? action.routes.map(routeToGtfs) : [] + routes.sort(gtfsSort) + routeIndex = state.active.entity && routes.findIndex(r => r.id === state.active.entity.id) + if (routeIndex !== -1) { + const activeRoute = routes[routeIndex] + if (state.active.entity && state.active.entity.tripPatterns) { + activeRoute.tripPatterns = clone(state.active.entity.tripPatterns) + } + return update(state, { + tables: {route: {$set: routes}}, + active: { + entity: {$set: activeRoute}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {route: {$set: routes}} + }) + } + case 'RECEIVE_TRIP_PATTERNS': + return update(state, { + tripPatterns: {$set: + Object.keys(action.tripPatterns).map(key => { + return { + id: key, + latLngs: action.tripPatterns[key].shape ? (action.tripPatterns[key].shape.coordinates.map(c => ll.fromCoordinates(c))) : null + } + }) + } + }) + case 'RECEIVE_TRIP_PATTERNS_FOR_ROUTE': + routeIndex = state.tables.route.findIndex(r => r.id === action.routeId) + activePattern = state.active.subEntityId && action.tripPatterns.find(p => p.id === state.active.subEntityId) + if (activePattern) { + activePattern.stops = getStopsForPattern(activePattern, state.tables.stop) + } + if (routeIndex === -1) { + return state + } + if (state.active.entity.id === action.routeId) { + return update(state, { + tables: {route: {[routeIndex]: {$merge: {tripPatterns: action.tripPatterns}}}}, + active: { + entity: {$merge: {tripPatterns: action.tripPatterns}}, + subEntity: {$set: Object.assign({}, activePattern)} + } + }) + } else { + return update(state, { + tables: {route: {[routeIndex]: {$merge: {tripPatterns: action.tripPatterns}}}} + }) + } + // case 'RECEIVE_TRIPS_FOR_CALENDAR': + // routeIndex = state.tables.route.findIndex(r => r.id === action.pattern.routeId) + // patternIndex = state.tables.route[routeIndex].tripPatterns.findIndex(p => p.id === action.pattern.id) + // // if (state.active.entity.id === action.pattern.routeId) { + // return update(state, { + // tables: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, + // active: {entity: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}} + // }) + // // } else { + // // return update(state, { + // // tables: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}} + // // }) + // // } + // case 'DELETED_TRIPS_FOR_CALENDAR': + // routeIndex = state.tables.route.findIndex(r => r.id === action.pattern.routeId) + // patternIndex = state.tables.route[routeIndex].tripPatterns.findIndex(p => p.id === action.pattern.id) + // let tripIndex = state.tables.route[routeIndex].tripPatterns[patternIndex][action.calendarId].findIndex(t => t.) + // if (state.active.entity.id === action.pattern.routeId) { + // return update(state, { + // tables: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, + // active: {entity: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}} + // }) + // } + // else { + // return update(state, { + // tables: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, + // }) + // } + case 'RECEIVE_STOPS': + const stops = action.stops ? action.stops.map(stopToGtfs) : [] + stops.sort(gtfsSort) + stopIndex = state.active.entity && stops.findIndex(s => s.id === state.active.entity.id) + if (stopIndex !== -1) { + return update(state, { + tables: {stop: {$set: stops}}, + active: { + entity: {$set: stops[stopIndex]}, + edited: {$set: false} + } + }) + } else { + return update(state, { + tables: {stop: {$set: stops}} + }) + } + case 'DELETING_STOP': + stopIndex = state.tables.stop.findIndex(s => s.id === action.stop.id) + return update(state, { + tables: {stop: {$splice: [[stopIndex, 1]]}} + }) + case 'RECEIVE_STOP': + const stop = stopToGtfs(action.stop) + if (!stop) { + return state + } + stopIndex = state.tables.stop.findIndex(s => s.id === stop.id) + + // TODO: handle adding to rbush tree + // TODO: updated sort with stops array + + // if stop is active, update active entity + if (stop.id === state.active.entityId && stopIndex !== -1) { + stateUpdate = { + tables: {stop: {[stopIndex]: {$set: stop}}}, + active: { + entity: {$set: stop}, + edited: {$set: false} + } + } + } else if (stopIndex === -1) { + stateUpdate = { + tables: {stop: {$push: [stop]}} + } + } else { + stateUpdate = { + tables: {stop: {[stopIndex]: {$set: stop}}} + } + } + return update(state, stateUpdate) + case 'CLEAR_GTFSEDITOR_CONTENT': + return defaultState + case 'RECEIVE_GTFSEDITOR_TABLE': + newTableData = {} + const getMappedEntities = (entities) => { + switch (action.tableId) { + case 'agency': + return action.entities.map(agencyToGtfs) + case 'route': + return action.entities.map(routeToGtfs) + case 'stop': + return action.entities.map(stopToGtfs) + case 'calendar': + return action.entities.map(calendarToGtfs) + case 'fare': + return action.entities.map(fareToGtfs) // no mapping exists for fares + default: + return action.entities + } + } + newTableData[action.tableId] = getMappedEntities(action.entities) + return update(state, { + tables: {$merge: newTableData} + }) + case 'RECEIVE_GTFS_ENTITIES': + const getType = function (entity) { + if (entity.hasOwnProperty('route_id')) return 'route' + if (entity.hasOwnProperty('stop_id')) return 'stop' + } + const newLookupEntries = {} + for (const entity of action.gtfsEntities) { + const type = getType(entity) + const key = type + '_' + entity[type + '_id'] + newLookupEntries[key] = entity + } + return update(state, + {gtfsEntityLookup: + {$merge: newLookupEntries} + } + ) + default: + return state + } +} + +export default data diff --git a/lib/editor/reducers/index.js b/lib/editor/reducers/index.js new file mode 100644 index 000000000..858a69237 --- /dev/null +++ b/lib/editor/reducers/index.js @@ -0,0 +1,13 @@ +import { combineReducers } from 'redux' + +import data from './data' +import editSettings from './settings' +import mapState from './mapState' +import timetable from './timetable' + +export default combineReducers({ + data, + editSettings, + mapState, + timetable +}) diff --git a/lib/editor/reducers/mapState.js b/lib/editor/reducers/mapState.js new file mode 100644 index 000000000..b2150cf56 --- /dev/null +++ b/lib/editor/reducers/mapState.js @@ -0,0 +1,49 @@ +import update from 'react-addons-update' +import { latLngBounds } from 'leaflet' +import rbush from 'rbush' + +import { getEntityBounds, stopToGtfs } from '../util/gtfs' + +const defaultState = { + zoom: null, + bounds: latLngBounds([[60, 60], [-60, -20]]), + target: null, + stopTree: null +} + +const mapState = (state = defaultState, action) => { + let updatedState + switch (action.type) { + case 'RECEIVE_FEED_INFO': + if (action.feedInfo && action.feedInfo.defaultLon && action.feedInfo.defaultLat) { + return update(state, { + bounds: {$set: getEntityBounds([action.feedInfo.defaultLon, action.feedInfo.defaultLat], 0.5)}, + target: {$set: action.feedInfo.id} + }) + } + return state + case 'RECEIVE_STOPS': + const tree = rbush(9, ['[0]', '[1]', '[0]', '[1]']) + tree.load(action.stops.map(stopToGtfs).map(s => ([s.stop_lon, s.stop_lat, s]))) + return update(state, { + stopTree: {$set: tree} + }) + case 'RECEIVED_ROUTES_SHAPEFILE': + return update(state, { + routesGeojson: {$set: action.geojson} + }) + case 'UPDATE_MAP_SETTING': + updatedState = {} + for (const key in action.props) { + updatedState[key] = {$set: action.props[key]} + } + if (!('target' in action.props)) { + updatedState.target = {$set: null} + } + return update(state, updatedState) + default: + return state + } +} + +export default mapState diff --git a/lib/editor/reducers/settings.js b/lib/editor/reducers/settings.js new file mode 100644 index 000000000..069976ae8 --- /dev/null +++ b/lib/editor/reducers/settings.js @@ -0,0 +1,160 @@ +import update from 'react-addons-update' + +import { getControlPoints } from '../util/gtfs' +import { CLICK_OPTIONS } from '../util' + +const defaultState = { + editGeometry: false, + followStreets: true, + onMapClick: CLICK_OPTIONS[0], + stopInterval: 400, + distanceFromIntersection: 5, + afterIntersection: true, + intersectionStep: 2, + snapToStops: true, + addStops: false, + hideStops: false, + controlPoints: [], + coordinatesHistory: [], + actions: [], + patternCoordinates: null +} + +const editSettings = (state = defaultState, action) => { + let stateUpdate, controlPoints, coordinates + switch (action.type) { + case 'SETTING_ACTIVE_GTFS_ENTITY': + switch (action.subComponent) { + case 'trippattern': + controlPoints = getControlPoints(action.activeSubEntity, state.snapToStops) + coordinates = action.activeSubEntity && action.activeSubEntity.shape && action.activeSubEntity.shape.coordinates + stateUpdate = { + controlPoints: {$set: [controlPoints]}, + patternCoordinates: {$set: coordinates} + } + if (coordinates) { + stateUpdate.coordinatesHistory = {$set: [coordinates]} + } + return update(state, stateUpdate) + default: + return state + } + case 'UPDATE_PATTERN_COORDINATES': + return update(state, { + patternCoordinates: {$set: action.coordinates} + }) + case 'UPDATE_ACTIVE_GTFS_ENTITY': + switch (action.component) { + case 'trippattern': + if (action.props && 'shape' in action.props) { + // add previous coordinates to history + coordinates = action.entity.shape && action.entity.shape.coordinates + const newCoordinates = action.props.shape && action.props.shape.coordinates + if (coordinates) { + return update(state, { + coordinatesHistory: {$push: [coordinates]}, + patternCoordinates: {$set: newCoordinates} + }) + } + } + break + default: + return state + } + return state + case 'RECEIVE_TRIP_PATTERNS_FOR_ROUTE': + // set controlPoints initially and then whenever isSnappingToStops changes + if (action.activePattern) { + coordinates = action.activePattern.shape && action.activePattern.shape.coordinates + controlPoints = getControlPoints(action.activePattern, state.snapToStops) + } else { + controlPoints = [] + } + return update(state, { + controlPoints: {$set: [controlPoints]}, + patternCoordinates: {$set: coordinates}, + coordinatesHistory: {$set: [coordinates]} + }) + case 'UPDATE_EDIT_SETTING': + if (action.setting === 'editGeometry' && !state.editGeometry) { + controlPoints = getControlPoints(action.activePattern, state.snapToStops) + return update(state, { + [action.setting]: {$set: action.value}, + controlPoints: {$set: [controlPoints]} + }) + } else { + return update(state, { + [action.setting]: {$set: action.value} + }) + } + case 'UNDO_TRIP_PATTERN_EDITS': + stateUpdate = { + actions: {$splice: [[action.lastActionIndex, 1]]} + } + switch (action.lastActionType) { + case 'ADD_CONTROL_POINT': + stateUpdate.controlPoints = {$splice: [[action.lastControlPointsIndex, 1]]} + break + case 'UPDATE_CONTROL_POINT': + stateUpdate.controlPoints = {$splice: [[action.lastControlPointsIndex, 1]]} + stateUpdate.coordinatesHistory = {$splice: [[action.lastCoordinatesIndex, 1]]} + stateUpdate.patternCoordinates = {$set: action.lastCoordinates} + break + case 'REMOVE_CONTROL_POINT': + stateUpdate.controlPoints = {$splice: [[action.lastControlPointsIndex, 1]]} + stateUpdate.coordinatesHistory = {$splice: [[action.lastCoordinatesIndex, 1]]} + stateUpdate.patternCoordinates = {$set: action.lastCoordinates} + break + } + return update(state, stateUpdate) + case 'ADD_CONTROL_POINT': + controlPoints = [...state.controlPoints[state.controlPoints.length - 1]] + controlPoints.splice(action.index, 0, action.controlPoint) + return update(state, { + controlPoints: {$push: [controlPoints]}, + actions: {$push: [action.type]} + }) + case 'RESET_ACTIVE_GTFS_ENTITY': + switch (action.component) { + case 'trippattern': + coordinates = state.coordinatesHistory[0] + return update(state, { + patternCoordinates: {$set: coordinates}, + coordinatesHistory: {$set: [coordinates]} + }) + default: + return state + } + case 'REMOVE_CONTROL_POINT': + controlPoints = [...state.controlPoints[state.controlPoints.length - 1]] + controlPoints.splice(action.index, 1) + return update(state, { + controlPoints: {$push: [controlPoints]}, + actions: {$push: [action.type]} + }) + case 'UPDATE_CONTROL_POINT': + const newControlPoints = [] + controlPoints = state.controlPoints[state.controlPoints.length - 1] + for (var i = 0; i < controlPoints.length; i++) { + newControlPoints.push(Object.assign({}, controlPoints[i])) + } + const newest = update(newControlPoints, {[action.index]: {point: {$set: action.point}, distance: {$set: action.distance}}}) + return update(state, { + controlPoints: {$push: [newest]}, + actions: {$push: [action.type]} + }) + case 'SAVED_TRIP_PATTERN': + // set controlPoints initially and then whenever isSnappingToStops changes + controlPoints = getControlPoints(action.tripPattern, state.snapToStops) + coordinates = action.tripPattern && action.tripPattern.shape && action.tripPattern.shape.coordinates + return update(state, { + controlPoints: {$set: [controlPoints]}, + patternCoordinates: {$set: coordinates}, + coordinatesHistory: {$set: [coordinates]} + }) + default: + return state + } +} + +export default editSettings diff --git a/lib/editor/reducers/timetable.js b/lib/editor/reducers/timetable.js new file mode 100644 index 000000000..43c9c0d59 --- /dev/null +++ b/lib/editor/reducers/timetable.js @@ -0,0 +1,92 @@ +import update from 'react-addons-update' +import objectPath from 'object-path' +import clone from 'clone' + +import { sortAndFilterTrips } from '../util' + +const defaultState = { + columns: [], + trips: [], + edited: [], + selected: [], + hideDepartureTimes: false, + offset: null +} + +const timetable = (state = defaultState, action) => { + let trips + switch (action.type) { + case 'RECEIVE_TRIP_PATTERNS_FOR_ROUTE': + return update(state, { + columns: {$set: action.activeColumns} + }) + case 'SETTING_ACTIVE_GTFS_ENTITY': + switch (action.subComponent) { + case 'trippattern': + if (action.subSubComponent === 'timetable') { + return update(state, { + columns: {$set: action.activeColumns}, + patternId: {$set: action.subEntityId}, + calendarId: {$set: action.subSubEntityId} + }) + } + break + } + return state + case 'RECEIVE_TRIPS_FOR_CALENDAR': + trips = clone(sortAndFilterTrips(action.trips, action.pattern.useFrequency)) + return update(state, { + trips: {$set: trips}, + edited: {$set: []} + }) + case 'SET_TIMETABLE_OFFSET': + return update(state, { + offset: {$set: action.seconds} + }) + case 'UPDATE_TIMETABLE_CELL_VALUE': + trips = clone(state.trips) + objectPath.set(trips, action.key, action.value) + return update(state, { + trips: {$set: trips}, + edited: {$push: [action.rowIndex]} + }) + case 'TOGGLE_ALL_TIMETABLE_ROW_SELECTION': + const selected = [] + if (action.select) { + for (let i = 0; i < state.trips.length; i++) { + selected.push(i) + } + } + return update(state, { + selected: {$set: selected} + }) + case 'TOGGLE_DEPARTURE_TIMES': + return update(state, { + hideDepartureTimes: {$set: !state.hideDepartureTimes} + }) + case 'ADD_NEW_TRIP': + return update(state, { + trips: {$push: [action.trip]}, + edited: {$push: [state.trips.length]} + }) + case 'REMOVE_TRIPS': + return update(state, { + trips: {$splice: action.indexes} + }) + case 'TOGGLE_SINGLE_TIMETABLE_ROW_SELECTION': + const selectIndex = state.selected.indexOf(action.rowIndex) + if (selectIndex === -1) { + return update(state, { + selected: {$push: [action.rowIndex]} + }) + } else { + return update(state, { + selected: {$splice: [[selectIndex, 1]]} + }) + } + default: + return state + } +} + +export default timetable diff --git a/lib/editor/util/gtfs.js b/lib/editor/util/gtfs.js new file mode 100644 index 000000000..4c497421b --- /dev/null +++ b/lib/editor/util/gtfs.js @@ -0,0 +1,321 @@ +import along from 'turf-along' +import lineDistance from 'turf-line-distance' +import { latLngBounds } from 'leaflet' + +export const componentList = ['route', 'stop', 'fare', 'feedinfo', 'calendar', 'scheduleexception', 'agency'] +export const subComponentList = ['trippattern'] +export const subSubComponentList = ['timetable'] + +export function isNew (entity) { + return entity.id === 'new' || typeof entity.id === 'undefined' +} + +export function getEntityBounds (entity, offset = 0.005) { + if (!entity) return null + + // [lng, lat] + if (entity.constructor === Array) { + return latLngBounds([[entity[1] + offset, entity[0] - offset], [entity[1] - offset, entity[0] + offset]]) + } else if (typeof entity.stop_lat !== 'undefined') { + // stop + return latLngBounds([[entity.stop_lat + offset, entity.stop_lon - offset], [entity.stop_lat - offset, entity.stop_lon + offset]]) + } else if (typeof entity.tripPatterns !== 'undefined') { + // route + let coordinates = [] + entity.tripPatterns.map(pattern => { + if (pattern.shape && pattern.shape.coordinates) { + coordinates = [ + ...coordinates, + ...pattern.shape.coordinates.map(c => ([c[1], c[0]])) + ] + } + }) + return latLngBounds(coordinates) + } else if (entity.shape) { + // trip pattern + return latLngBounds(entity.shape.coordinates.map(c => ([c[1], c[0]]))) + } else if (entity.patternStops) { + // TODO: add pattern stops bounds extraction + return null + } +} + +export function findEntityByGtfsId (component, gtfsId, entities) { + let entity + switch (component) { + case 'stop': + entity = entities.find(e => e.gtfsStopId === gtfsId) + break + case 'route': + entity = entities.find(e => e.gtfsRouteId === gtfsId) + break + case 'calendar': + entity = entities.find(e => e.gtfsServiceId === gtfsId) + break + default: + entity = null + break + } + return entity ? entity.id : -1 +} + +export function getEntityName (component, entity) { + if (!entity) { + return '[Unnamed]' + } + const nameKey = + 'route_id' in entity + ? 'route_short_name' + : 'patternStops' in entity + ? 'name' + : 'agency_name' in entity + ? 'agency_name' + : 'stop_id' in entity + ? 'stop_name' + : 'service_id' in entity + ? 'description' + : 'fare_id' in entity + ? 'fare_id' + : 'exemplar' in entity // schedule exception + ? 'name' + : null + // if (nameKey !== 'stop_name') console.log(nameKey) + switch (nameKey) { + case 'stop_name': + return entity.stop_name && entity.stop_code + ? `${entity.stop_name} (${entity.stop_code})` + : entity.stop_name && entity.stop_id + ? `${entity.stop_name} (${entity.stop_id})` + : entity.stop_name || '[no name]' + case 'route_short_name': + return entity.route_short_name && entity.route_long_name && entity.route_short_name !== '""' && entity.route_long_name !== '""' + ? `${entity.route_short_name} - ${entity.route_long_name}` + : entity.route_short_name && entity.route_short_name !== '""' + ? entity.route_short_name + : entity.route_long_name && entity.route_long_name !== '""' + ? entity.route_long_name + : entity.route_id || '[no name]' + case 'description': + return `${entity.service_id} (${entity.description})` + default: + return entity[nameKey] || '[no name]' + } +} + +export function getAbbreviatedStopName (stop, maxCharactersPerWord = 10) { + const stopName = getEntityName('stop', stop) + const stopNameParts = stopName ? stopName.split(/(\band\b|&|@|:|\+)+/i) : null + return stopNameParts && stopNameParts.length === 3 && stop.stop_name.length > maxCharactersPerWord * 2 + ? `${stopNameParts[0].substr(0, maxCharactersPerWord).trim()}... ${stopNameParts[2].substr(0, maxCharactersPerWord).trim()}` + : stop.stop_name +} + +export function getControlPoints (pattern, snapToStops) { + if (!pattern) { + return [] + } + const controlPoints = [] + pattern.shape && pattern.patternStops && pattern.patternStops.map((ps, index) => { + // set distance to average of patternStop and next patternStop, if last stop set to end of segment + const distance = pattern.patternStops[index + 1] + ? (pattern.patternStops[index + 1].shapeDistTraveled + ps.shapeDistTraveled) / 2 + : lineDistance(pattern.shape, 'meters') + const point = pattern.shape && along(pattern.shape, distance, 'meters') + const controlPoint = { + point, + distance: distance, + permanent: true + } + const stopPoint = pattern.shape && along(pattern.shape, ps.shapeDistTraveled, 'meters') + const stopControl = { + point: stopPoint, + permanent: true, + distance: ps.shapeDistTraveled, + ...ps + } + if (snapToStops) { + stopControl.hidden = true + } + controlPoints.push(stopControl) + controlPoints.push(controlPoint) + }) + return controlPoints +} + +export function getRouteNameAlerts (route) { + const routeName = route.route_short_name && route.route_long_name + ? `${route.route_short_name} - ${route.route_long_name}` + : route.route_long_name ? route.route_long_name + : route.route_short_name ? route.route_short_name + : null + return routeName +} + +export function getRouteName (route) { + let name = '' + if (route && route.route_short_name) { + name += route.route_short_name + if (route.route_long_name) { + name += ' - ' + } + } + + if (route && route.route_long_name) { + name += route.route_long_name + } + if (route && route.route_id && !route.route_long_name && !route.route_short_name) { + name += route.route_id + } + return name +} + +export function stopToGtfs (s) { + if (!s) { + return null + } + return { + // datatools props + id: s.id, + feedId: s.feedId, + bikeParking: s.bikeParking, + carParking: s.carParking, + pickupType: s.pickupType, + dropOffType: s.dropOffType, + + // gtfs spec props + stop_code: s.stopCode, + stop_name: s.stopName, + stop_desc: s.stopDesc, + stop_lat: s.lat, + stop_lon: s.lon, + zone_id: s.zoneId, + stop_url: s.stopUrl, + location_type: s.locationType, + parent_station: s.parentStation, + stop_timezone: s.stopTimezone, + wheelchair_boarding: s.wheelchairBoarding, + stop_id: s.gtfsStopId + } +} + +export function stopFromGtfs (stop) { + return { + gtfsStopId: stop.stop_id, + stopCode: stop.stop_code, + stopName: stop.stop_name, + stopDesc: stop.stop_desc, + lat: stop.stop_lat, + lon: stop.stop_lon, + zoneId: stop.zone_id, + stopUrl: stop.stop_url, + locationType: stop.location_type, + parentStation: stop.parent_station, + stopTimezone: stop.stop_timezone, + wheelchairBoarding: stop.wheelchair_boarding, + bikeParking: stop.bikeParking, + carParking: stop.carParking, + pickupType: stop.pickupType, + dropOffType: stop.dropOffType, + feedId: stop.feedId, + id: isNew(stop) ? null : stop.id + } +} + +export function routeToGtfs (route) { + return { + // datatools props + id: route.id, + feedId: route.feedId, + route_branding_url: route.routeBrandingUrl, + publiclyVisible: route.publiclyVisible, + status: route.status, + numberOfTrips: route.numberOfTrips, + + // gtfs spec props + agency_id: route.agencyId, + route_short_name: route.routeShortName, + route_long_name: route.routeLongName, + route_desc: route.routeDesc, + route_type: route.gtfsRouteType, + route_url: route.routeUrl, + route_color: route.routeColor, + route_text_color: route.routeTextColor, + route_id: route.gtfsRouteId + } +} + +export function agencyToGtfs (agency) { + return { + // datatools props + id: agency.id, + feedId: agency.feedId, + agency_branding_url: agency.agencyBrandingUrl, + + // gtfs spec props + agency_id: agency.agencyId, + agency_name: agency.name, + agency_url: agency.url, + agency_timezone: agency.timezone, + agency_lang: agency.lang, + agency_phone: agency.phone, + agency_fare_url: agency.agencyFareUrl, + agency_email: agency.email + } +} + +export function calendarToGtfs (cal) { + return { + // datatools props + id: cal.id, + feedId: cal.feedId, + description: cal.description, + routes: cal.routes, + numberOfTrips: cal.numberOfTrips, + + // gtfs spec props + service_id: cal.gtfsServiceId, + monday: cal.monday ? 1 : 0, + tuesday: cal.tuesday ? 1 : 0, + wednesday: cal.wednesday ? 1 : 0, + thursday: cal.thursday ? 1 : 0, + friday: cal.friday ? 1 : 0, + saturday: cal.saturday ? 1 : 0, + sunday: cal.sunday ? 1 : 0, + start_date: cal.startDate, + end_date: cal.endDate + } +} + +export function fareToGtfs (fare) { + return { + // datatools props + id: fare.id, + feedId: fare.feedId, + description: fare.description, + fareRules: fare.fareRules, + + // gtfs spec props + fare_id: fare.gtfsFareId, + price: fare.price, + currency_type: fare.currencyType, + payment_method: fare.paymentMethod, + transfers: fare.transfers, + transfer_duration: fare.transferDuration + } +} + +export function gtfsSort (a, b) { + const radix = 10 + var aName = getEntityName(null, a) + var bName = getEntityName(null, b) + if (a.isCreating && !b.isCreating) return -1 + if (!a.isCreating && b.isCreating) return 1 + if (!isNaN(parseInt(aName, radix)) && !isNaN(parseInt(bName, radix)) && !isNaN(+aName) && !isNaN(+bName)) { + if (parseInt(aName, radix) < parseInt(bName, radix)) return -1 + if (parseInt(aName, radix) > parseInt(bName, radix)) return 1 + return 0 + } + if (aName.toLowerCase() < bName.toLowerCase()) return -1 + if (aName.toLowerCase() > bName.toLowerCase()) return 1 + return 0 +} diff --git a/lib/editor/util/index.js b/lib/editor/util/index.js new file mode 100644 index 000000000..fed3f84f4 --- /dev/null +++ b/lib/editor/util/index.js @@ -0,0 +1,152 @@ +import { getAbbreviatedStopName } from './gtfs' +import { getConfigProperty } from '../../common/util/config' + +export const CLICK_OPTIONS = ['DRAG_HANDLES', 'ADD_STOP_AT_CLICK', 'ADD_STOPS_AT_INTERVAL', 'ADD_STOPS_AT_INTERSECTIONS'] +export const YEAR_FORMAT = 'YYYY-MM-DD' +export const TIMETABLE_FORMATS = ['HH:mm:ss', 'h:mm:ss a', 'h:mm:ssa', 'h:mm a', 'h:mma', 'h:mm', 'HHmm', 'hmm', 'HH:mm'].map(format => `YYYY-MM-DDT${format}`) +export const EXEMPLARS = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY', 'NO_SERVICE', 'CUSTOM', 'SWAP'] + +export function isTimeFormat (type) { + return /TIME/.test(type) +} + +export function getTimetableColumns (pattern, stops) { + const columns = [ + { + name: 'Block ID', + width: 60, + key: 'blockId', + type: 'TEXT', + placeholder: '300' + }, + { + name: 'Trip ID', + width: 200, + key: 'gtfsTripId', + type: 'TEXT', + placeholder: '12345678' + }, + { + name: 'Trip Headsign', + width: 200, + key: 'tripHeadsign', + type: 'TEXT', + placeholder: 'Destination via Transfer Center' + } + ] + if (pattern && pattern.patternStops) { + if (!pattern.useFrequency) { + pattern.patternStops.map((ps, index) => { + const stop = stops ? stops.find(st => st.id === ps.stopId) : null + const stopName = stop ? stop.stop_name : ps.stopId + const abbreviatedStopName = stop ? getAbbreviatedStopName(stop) : ps.stopId + const TIME_WIDTH = 80 + columns.push({ + name: abbreviatedStopName, + title: stopName, + width: TIME_WIDTH, + key: `stopTimes.${index}.arrivalTime`, + colSpan: '2', + // hidden: false, + type: 'ARRIVAL_TIME', + placeholder: 'HH:MM:SS' + }) + columns.push({ + key: `stopTimes.${index}.departureTime`, + width: TIME_WIDTH, + // hidden: hideDepartureTimes, + type: 'DEPARTURE_TIME', + placeholder: 'HH:MM:SS' + }) + }) + } else { + // columns added if using freqency schedule type + columns.push({ + name: 'Start time', + width: 100, + key: 'startTime', + type: 'TIME', + placeholder: 'HH:MM:SS' + }) + columns.push({ + name: 'End time', + width: 100, + key: 'endTime', + type: 'TIME', + placeholder: 'HH:MM:SS' + }) + columns.push({ + name: 'Headway', + width: 60, + key: 'headway', + type: 'MINUTES', + placeholder: '15 (min)' + }) + } + } + return columns +} + +export function getStopsForPattern (pattern, stops) { + return pattern && pattern.patternStops && stops + ? pattern.patternStops.map(ps => stops.find(s => s.id === ps.stopId)) + : [] +} + +export function sortAndFilterTrips (trips, useFrequency) { + return trips + ? trips.filter(t => t.useFrequency === useFrequency) // filter out based on useFrequency + .sort((a, b) => { + if (a.stopTimes[0].departureTime < b.stopTimes[0].departureTime) return -1 + if (a.stopTimes[0].departureTime > b.stopTimes[0].departureTime) return 1 + return 0 + }) + : [] +} + +export function getZones (stops, activeStop) { + const zones = {} + if (stops) { + for (var i = 0; i < stops.length; i++) { + const stop = stops[i] + if (stop.zone_id) { + let zone = zones[stop.zone_id] + if (!zone) { + zone = [] + } + zone.push(stop) + zones[stop.zone_id] = zone + } + } + // add any new zone + if (activeStop && activeStop.zone_id && !zones[activeStop.zone_id]) { + let zone = zones[activeStop.zone_id] + if (!zone) { + zone = [] + } + zone.push(activeStop) + zones[activeStop.zone_id] = zone + } + } + const zoneOptions = Object.keys(zones).map(key => { + return { + value: key, + label: `${key} zone (${zones[key] ? zones[key].length : 0} stops)` + } + }) + return {zones, zoneOptions} +} + +export function getEditorTable (component) { + return getConfigProperty('modules.editor.spec').find( + t => component === 'scheduleexception' + ? t.id === 'calendar_dates' + : component === 'fare' + ? t.id === 'fare_attributes' + : t.id === component + ) +} + +export function canApproveGtfs (project, feedSource, user) { + return project && feedSource && user && !user.permissions.hasFeedPermission(project.organizationId, project.id, feedSource.id, 'approve-gtfs') +} diff --git a/lib/editor/util/map.js b/lib/editor/util/map.js new file mode 100644 index 000000000..6e2cc9671 --- /dev/null +++ b/lib/editor/util/map.js @@ -0,0 +1,200 @@ +import lineString from 'turf-linestring' +import fetch from 'isomorphic-fetch' +import ll from 'lonlng' +import lineDistance from 'turf-line-distance' +import pointOnLine from 'turf-point-on-line' +import lineSlice from 'turf-line-slice' +import point from 'turf-point' + +import {getSegment} from '../../scenario-editor/utils/valhalla' +import { generateUID } from '../../common/util/util' +import { getConfigProperty } from '../../common/util/config' +import { reverseEsri as reverse } from '../../scenario-editor/utils/reverse' + +export const MAP_LAYERS = [ + { + name: 'Streets', + id: 'mapbox.streets' + }, + { + name: 'Light', + id: 'mapbox.light' + }, + { + name: 'Dark', + id: 'mapbox.dark' + }, + { + name: 'Satellite', + id: 'mapbox.streets-satellite' + } +] + +export function stopToStopTime (stop) { + return {stopId: stop.id, defaultDwellTime: 0, defaultTravelTime: 0} +} + +export function clickToLatLng (latlng) { + const precision = 100000000 // eight decimal places is accurate up to 1.1 meters + return {stop_lat: Math.round(latlng.lat * precision) / precision, stop_lon: Math.round(latlng.lng % 180 * precision) / precision} +} + +// TODO: not used currently, remove? +export function zoomToEntity (entity, map) { + if (entity && entity.id) { + map.leafletElement.panTo([entity.stop_lat, entity.stop_lon]) + } +} + +export async function constructStop (latlng, feedId) { + const stopLatLng = clickToLatLng(latlng) + const result = await reverse(latlng) + const stopId = generateUID() + let stopName = `New Stop (${stopId})` + if (result && result.address) { + stopName = result.address.Address + } + return { + stop_id: stopId, + stop_name: stopName, + feedId, + ...stopLatLng + } +} + +export function constructPoint (latlng) { + const coords = ll.toCoordinates(latlng) + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coords + } + } +} + +export async function route (from, to) { + const url = `${getConfigProperty('application.r5')}/plan?fromLat=${from.lat}&fromLon=${from.lng}&toLat=${to.lat}&toLon=${to.lng}&mode=CAR&full=false` + const response = await fetch(url) + return await response.json() +} + +export async function street (from, to) { + let json + try { + json = await route(from, to) + } catch (e) { + return null + } + if (json.errors) { + console.log('error getting r5 data', json) + return null + } else { + return json + } +} + +export function getPatternEndPoint (pattern) { + const coordinates = pattern.shape && pattern.shape.coordinates + const patternStops = [...pattern.patternStops] + let endPoint + if (coordinates) { + endPoint = ll.toLatlng(coordinates[coordinates.length - 1]) + } else { + endPoint = {lng: patternStops[0].stop_lon, lat: patternStops[0].stop_lat} + } + return endPoint +} + +export function getFeedBounds (feedSource, pad) { + return feedSource && feedSource.latestValidation && feedSource.latestValidation.bounds + ? [[feedSource.latestValidation.bounds.north + pad, feedSource.latestValidation.bounds.west - pad], [feedSource.latestValidation.bounds.south - pad, feedSource.latestValidation.bounds.east + pad]] + : [[60, 60], [-60, -20]] +} + +export async function handlePatternEdit (position, begin, end, pattern, followStreets, patternCoordinates) { + let originalLatLngs + let originalEndPoint + let from, to + + // set from, to for endPoint if we have position + if (begin && position) { + from = begin + to = [position.lng, position.lat] + } else if (begin) { // set just from (when controlPoint is removed) + from = begin + } else if (end) { // set from for beginPoint + from = [position.lng, position.lat] + } else { // otherwise use the original endpoint + originalLatLngs = pattern.shape.coordinates.map(c => ([c[1], c[0]])) + originalEndPoint = originalLatLngs[originalLatLngs.length - 1] + from = [originalEndPoint[1], originalEndPoint[0]] // [patternCoordinates[patternCoordinates.length - 1].lng, patternCoordinates[patternCoordinates.length - 1].lat] + to = [position.lng, position.lat] + } + + if (from) { + const points = [ + from.geometry ? from.geometry.coordinates : from + ] + if (to) { + points.push(to) + } + if (end) { + points.push(end.geometry.coordinates) + } + let newCoordinates + const newSegment = await getSegment(points, followStreets) + const originalSegment = lineString(patternCoordinates) + + // slice line if middle control point + if (end && begin) { + const beginPoint = point(patternCoordinates[0]) + const beginSlice = lineSlice(beginPoint, from, originalSegment) + const endPoint = point(patternCoordinates[patternCoordinates.length - 1]) + const endSlice = lineSlice(end, endPoint, originalSegment) + newCoordinates = [ + ...beginSlice.geometry.coordinates, + ...newSegment.coordinates, + ...endSlice.geometry.coordinates + ] + } else if (end) { // handle begin control point + const endPoint = point(patternCoordinates[patternCoordinates.length - 1]) + const endSlice = lineSlice(end, endPoint, originalSegment) + newCoordinates = [ + ...newSegment.coordinates, + ...endSlice.geometry.coordinates + ] + } else { // append latlngs if end control point + const beginPoint = point(patternCoordinates[0]) + const beginSlice = lineSlice(beginPoint, from, originalSegment) + newCoordinates = [ + ...beginSlice.geometry.coordinates, + ...newSegment.coordinates + ] + } + return newCoordinates + } +} + +function geojsonFromCoordinates (coordinates) { + return coordinates && { + type: 'Feature', + geometry: { + coordinates: coordinates, + type: 'LineString' + } + } +} + +export function handleControlPointDragEnd (e, coordinates) { + const geojson = geojsonFromCoordinates(coordinates) + + // snap control point to line + const controlPointLocation = e.target.toGeoJSON() + const snap = pointOnLine(geojson, controlPointLocation) + const lineSegment = lineSlice(point(geojson.geometry.coordinates[0]), snap, geojson) + + // measure line segment + const distTraveled = lineDistance(lineSegment, 'meters') + return { snap, distTraveled } +} diff --git a/lib/editor/util/ui.js b/lib/editor/util/ui.js new file mode 100644 index 000000000..5ddd0925c --- /dev/null +++ b/lib/editor/util/ui.js @@ -0,0 +1,51 @@ +export const gtfsIcons = [ + { + id: 'feedinfo', + icon: 'info', + addable: false, + title: 'Edit feed info', + label: 'Feed Info' + }, + { + id: 'agency', + icon: 'building', + addable: true, + title: 'Edit agencies', + label: 'Agencies' + }, + { + id: 'route', + icon: 'bus', + addable: true, + title: 'Edit routes', + label: 'Routes' + }, + { + id: 'stop', + icon: 'map-marker', + addable: true, + title: 'Edit stops', + label: 'Stops' + }, + { + id: 'calendar', + icon: 'calendar', + addable: true, + title: 'Edit calendars', + label: 'Calendars' + }, + { + id: 'scheduleexception', + icon: 'ban', + addable: true, + hideSidebar: true, + title: 'Edit schedule exceptions' + }, + { + id: 'fare', + icon: 'ticket', + addable: true, + title: 'Edit fares', + label: 'Fares' + } +] diff --git a/lib/editor/util/validation.js b/lib/editor/util/validation.js new file mode 100644 index 000000000..2e9e25a79 --- /dev/null +++ b/lib/editor/util/validation.js @@ -0,0 +1,99 @@ +import validator from 'validator' + +export function validate (type, required, name, value, entities, id) { + let isNotValid + switch (type) { + case 'GTFS_ID': + isNotValid = required && !value + const indices = [] + const idList = entities.map(e => e[name]) + let idx = idList.indexOf(value) + while (idx !== -1) { + indices.push(idx) + idx = idList.indexOf(value, idx + 1) + } + const isNotUnique = value && (indices.length > 1 || indices.length && entities[indices[0]].id !== id) + if (isNotValid || isNotUnique) { + return {field: name, invalid: isNotValid || isNotUnique} + } else { + return false + } + case 'TEXT': + case 'GTFS_TRIP': + case 'GTFS_SHAPE': + case 'GTFS_BLOCK': + case 'GTFS_FARE': + case 'GTFS_SERVICE': + isNotValid = required && !value + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'URL': + isNotValid = required && !value || value && !validator.isURL(value) + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'EMAIL': + isNotValid = required && !value || value && !validator.isEmail(value) + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'GTFS_ZONE': + isNotValid = required && (value === null || typeof value === 'undefined') + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'TIMEZONE': + isNotValid = required && (value === null || typeof value === 'undefined') + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'LANGUAGE': + isNotValid = required && (value === null || typeof value === 'undefined') + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'TIME': + case 'LATITUDE': + case 'LONGITUDE': + isNotValid = required && (value === null || typeof value === 'undefined') + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'NUMBER': + isNotValid = required && (value === null || typeof value === 'undefined') + if (isNotValid) { + return {field: name, invalid: isNotValid} + } else { + return false + } + case 'DATE': + case 'COLOR': + case 'POSITIVE_INT': + case 'POSITIVE_NUM': + case 'DAY_OF_WEEK_BOOLEAN': + case 'DROPDOWN': + // isNotValid = required && (value === null || typeof value === 'undefined') + // if (isNotValid) { + // return {field: name, invalid: isNotValid} + // } + break + case 'GTFS_ROUTE': + case 'GTFS_AGENCY': + case 'GTFS_STOP': + } +} diff --git a/lib/gtfs/actions/feed.js b/lib/gtfs/actions/feed.js new file mode 100644 index 000000000..5bd35efe2 --- /dev/null +++ b/lib/gtfs/actions/feed.js @@ -0,0 +1,95 @@ +import fetch from 'isomorphic-fetch' + +import { compose, feed } from '../../gtfs/util/graphql' +import { updateDateTimeFilter } from './filter' + +function fetchingFeed (feedId, date, from, to) { + return { + type: 'FETCH_GRAPHQL_FEED', + feedId, + date, + from, + to + } +} + +function errorFetchingFeed (feedId, date, from, to) { + return { + type: 'FETCH_GRAPHQL_FEED_REJECTED', + feedId, + date, + from, + to + } +} + +function receiveFeed (feedId, data) { + return { + type: 'FETCH_GRAPHQL_FEED_FULFILLED', + feedId, + data + } +} + +export function fetchFeed (feedId, date, from, to) { + return function (dispatch, getState) { + dispatch(fetchingFeed(feedId, date, from, to)) + return fetch(compose( + feed // (feedId, date, from, to) + , {feedId, date, from, to}) + ) + .then((response) => { + if (response.status >= 300) { + return dispatch(errorFetchingFeed(feedId, date, from, to)) + } + return response.json() + }) + .then(json => { + dispatch(receiveFeed(feedId, json)) + }) + } +} + +// export function updateStopRouteFilter (routeId) { +// return { +// type: 'STOP_ROUTE_FILTER_CHANGE', +// payload: routeId +// } +// } +// +// export function updateStopPatternFilter (patternId) { +// return { +// type: 'STOP_PATTERN_FILTER_CHANGE', +// payload: patternId +// } +// } + +export function feedDateTimeFilterChange (feedId, props) { + return function (dispatch, getState) { + dispatch(updateDateTimeFilter(props)) + dispatch(fetchFeed(feedId)) + } +} + +// export function feedPatternFilterChange (feedId, patternData) { +// return function (dispatch, getState) { +// const state = getState() +// const {date, from, to} = state.gtfs.filter.dateTimeFilter +// +// if (patternId) { +// dispatch(fetchFeed(feedId, null, patternId, date, from, to)) +// } else if (routeFilter) { // fetch stops for route if route filter set and pattern filter set to null +// dispatch(fetchFeed(feedId, routeFilter, null, date, from, to)) +// } +// } +// } + +// export function feedRouteFilterChange (feedId, routeData) { +// return function (dispatch, getState) { +// const routeId = (routeData && routeData.route_id) ? routeData.route_id : null +// dispatch(updateStopRouteFilter(routeId)) +// dispatch(updateStopPatternFilter(null)) +// dispatch(fetchPatterns(feedId, routeId, null)) +// dispatch(fetchFeedWithFilters(feedId)) +// } +// } diff --git a/src/main/client/gtfs/actions/gtfsFilter.js b/lib/gtfs/actions/filter.js similarity index 75% rename from src/main/client/gtfs/actions/gtfsFilter.js rename to lib/gtfs/actions/filter.js index c114caa0b..558b853bd 100644 --- a/src/main/client/gtfs/actions/gtfsFilter.js +++ b/lib/gtfs/actions/filter.js @@ -1,6 +1,5 @@ import { secureFetch } from '../../common/util/util' import { getFeed } from '../../common/util/modules' -import { fetchProjectFeeds } from '../../manager/actions/feeds' export const updatingGtfsFilter = (activeProject, user) => { return { type: 'UPDATE_GTFS_FILTER', @@ -16,13 +15,20 @@ export const updateLoadedFeeds = (loadedFeeds) => { } } +export function updateMapState (props) { + return { + type: 'UPDATE_GTFS_MAP_STATE', + props + } +} + export function updateGtfsFilter (activeProject, user) { return function (dispatch, getState) { dispatch(updatingGtfsFilter(activeProject, user)) return secureFetch('/api/manager/feeds', getState()) .then(response => response.json()) .then((feedIds) => { - let activeFeeds = getState().projects.active.feedSources + const activeFeeds = getState().projects.active.feedSources // if (!activeFeeds) { // return dispatch(fetchProjectFeeds(getState().projects.active.id)) // .then((projectFeeds) => { @@ -33,21 +39,27 @@ export function updateGtfsFilter (activeProject, user) { // }) // } // else { - let feeds = feedIds.map(id => getFeed(activeFeeds, id)).filter(n => n) - console.log(feeds) - dispatch(updateLoadedFeeds(feeds)) + const feeds = feedIds.map(id => getFeed(activeFeeds, id)).filter(n => n) + dispatch(updateLoadedFeeds(feeds)) // } }) } } -export const setPermissionFilter = (permission) => { +export const updatePermissionFilter = (permission) => { return { - type: 'SET_GTFS_PERMISSION_FILTER', + type: 'UPDATE_GTFS_PERMISSION_FILTER', permission } } +export const updateDateTimeFilter = (props) => { + return { + type: 'UPDATE_GTFS_DATETIME_FILTER', + props + } +} + export const addActiveFeed = (feed) => { return { type: 'ADD_ACTIVE_FEED', diff --git a/lib/gtfs/actions/general.js b/lib/gtfs/actions/general.js new file mode 100644 index 000000000..c071ad5d0 --- /dev/null +++ b/lib/gtfs/actions/general.js @@ -0,0 +1,101 @@ +import fetch from 'isomorphic-fetch' +import { clearStops } from './stops' +import { clearPatterns } from './patterns' +import { clearRoutes } from './routes' +import { stopsAndRoutes, compose, patternsAndStopsForBoundingBox } from '../util/graphql' +import { getFeedId } from '../../common/util/modules' + +export function clearGtfsElements () { + return function (dispatch, getState) { + dispatch(clearRoutes()) + dispatch(clearStops()) + dispatch(clearPatterns()) + } +} +function requestGtfsElements (feedIds, entities) { + return { + type: 'REQUESTING_GTFS_ELEMENTS', + feedIds, + entities + } +} +function receivedGtfsElements (feedIds, stops, patterns) { + return { + type: 'RECEIVED_GTFS_ELEMENTS', + feedIds, + stops, + patterns + } +} +export const requestStopsAndRoutes = (feedIds, routeids, stopIds, module) => { + return { + type: 'REQUEST_GTFS_STOPS_AND_ROUTES', + feedIds, + routeids, + stopIds, + module + } +} +export const receivedStopsAndRoutes = (results, module) => { + return { + type: 'RECEIVED_GTFS_STOPS_AND_ROUTES', + results, + module + } +} +export function fetchStopsAndRoutes (entities, module) { + return function (dispatch, getState) { + const activeProject = getState().projects.active + const feedId = [] + const routeId = [] + const stopId = [] + // map entities to entity IDs requested + entities.map(e => { + const feed = activeProject.feedSources.find(f => { + return getFeedId(f) === e.entity.AgencyId + }) + const id = getFeedId(feed) + if (feedId.indexOf(id) === -1) { + feedId.push(id) + } + if (routeId.indexOf(e.entity.RouteId) === -1) { + routeId.push(e.entity.RouteId) + } + if (stopId.indexOf(e.entity.StopId) === -1) { + stopId.push(e.entity.StopId) + } + }) + dispatch(requestStopsAndRoutes(feedId, routeId, stopId, module)) + return fetch(compose(stopsAndRoutes(feedId, routeId, stopId), {feedId, routeId, stopId})) + .then((response) => { + return response.json() + }) + .then(results => { + return dispatch(receivedStopsAndRoutes(results, module)) + }) + } +} +export function refreshGtfsElements (feedId, entities) { + return function (dispatch, getState) { + dispatch(requestGtfsElements(feedId, entities)) + const bounds = getState().gtfs.filter.map.bounds + const maxLat = bounds.getNorth() + const maxLon = bounds.getEast() + const minLat = bounds.getSouth() + const minLon = bounds.getWest() + const vars = { + feedId, + max_lat: maxLat, + max_lon: maxLon, + min_lat: minLat, + min_lon: minLon + } + return fetch(compose(patternsAndStopsForBoundingBox(feedId, entities, maxLat, maxLon, minLat, minLon), vars)) + .then((response) => { + return response.json() + }) + .then(results => { + return dispatch(receivedGtfsElements(feedId, results.stops, results.patterns)) + }) + } +} diff --git a/lib/gtfs/actions/patterns.js b/lib/gtfs/actions/patterns.js new file mode 100644 index 000000000..2af1281da --- /dev/null +++ b/lib/gtfs/actions/patterns.js @@ -0,0 +1,74 @@ +import fetch from 'isomorphic-fetch' + +import { compose, patterns } from '../../gtfs/util/graphql' + +export function fetchingPatterns (feedId, routeId) { + return { + type: 'FETCH_GRAPHQL_PATTERNS', + feedId, + routeId + } +} + +export function clearPatterns () { + return { + type: 'CLEAR_GRAPHQL_PATTERNS' + } +} + +export function errorFetchingPatterns (feedId, data) { + return { + type: 'FETCH_GRAPHQL_PATTERNS_REJECTED', + data + } +} + +export function receivePatterns (feedId, data) { + return { + type: 'FETCH_GRAPHQL_PATTERNS_FULFILLED', + data + } +} + +export function patternDateTimeFilterChange (feedId, props) { + return function (dispatch, getState) { + const routeId = getState().gtfs.patterns.routeFilter + const { date, from, to } = getState().gtfs.filter.dateTimeFilter + dispatch(fetchPatterns(feedId, routeId, date, from, to)) + } +} + +export function fetchPatterns (feedId, routeId, date, from, to) { + return function (dispatch, getState) { + dispatch(fetchingPatterns(feedId, routeId, date, from, to)) + if (!routeId) { + return dispatch(receivePatterns(feedId, {routes: []})) + } + return fetch(compose(patterns, {feedId, routeId, date, from, to})) + .then((response) => { + if (response.status >= 300) { + return dispatch(errorFetchingPatterns(feedId, routeId)) + } + return response.json() + }) + .then(json => { + dispatch(receivePatterns(feedId, json)) + }) + } +} + +export function updateRouteFilter (routeId) { + return { + type: 'PATTERN_ROUTE_FILTER_CHANGE', + payload: routeId + } +} + +export function patternRouteFilterChange (feedId, routeData) { + return function (dispatch, getState) { + const newRouteId = (routeData && routeData.route_id) ? routeData.route_id : null + const {date, from, to} = getState().gtfs.filter.dateTimeFilter + dispatch(updateRouteFilter(newRouteId)) + dispatch(fetchPatterns(feedId, newRouteId, date, from, to)) + } +} diff --git a/lib/gtfs/actions/routes.js b/lib/gtfs/actions/routes.js new file mode 100644 index 000000000..9f1d404c4 --- /dev/null +++ b/lib/gtfs/actions/routes.js @@ -0,0 +1,42 @@ +import fetch from 'isomorphic-fetch' +import { compose, routes } from '../../gtfs/util/graphql' + +export function fetchingRoutes (feedId) { + return { + type: 'FETCH_GRAPHQL_ROUTES', + feedId + } +} + +export function clearRoutes () { + return { + type: 'CLEAR_GRAPHQL_ROUTES' + } +} + +export function errorFetchingRoutes (feedId, data) { + return { + type: 'FETCH_GRAPHQL_ROUTES_REJECTED', + data + } +} + +export function receiveRoutes (feedId, data) { + return { + type: 'FETCH_GRAPHQL_ROUTES_FULFILLED', + data + } +} + +export function fetchRoutes (feedId) { + return function (dispatch, getState) { + dispatch(fetchingRoutes(feedId)) + return fetch(compose(routes, { feedId: feedId })) + .then((response) => { + return response.json() + }) + .then(json => { + dispatch(receiveRoutes(feedId, json)) + }) + } +} diff --git a/lib/gtfs/actions/stops.js b/lib/gtfs/actions/stops.js new file mode 100644 index 000000000..2c81c8af2 --- /dev/null +++ b/lib/gtfs/actions/stops.js @@ -0,0 +1,130 @@ +import fetch from 'isomorphic-fetch' + +import { compose, stopsFiltered } from '../../gtfs/util/graphql' +import { fetchPatterns } from './patterns' + +export function fetchingStops (feedId, routeId, patternId, date, from, to) { + return { + type: 'FETCH_GRAPHQL_STOPS', + feedId, + routeId, + patternId, + date, + from, + to + } +} + +export function clearStops () { + return { + type: 'CLEAR_GRAPHQL_STOPS' + } +} + +export function errorFetchingStops (feedId, routeId, patternId, date, from, to) { + return { + type: 'FETCH_GRAPHQL_STOPS_REJECTED', + feedId, + routeId, + patternId, + date, + from, + to + } +} + +export function receiveStops (feedId, routeId, patternId, data) { + return { + type: 'FETCH_GRAPHQL_STOPS_FULFILLED', + feedId, + routeId, + patternId, + data + } +} + +export function fetchStopsWithFilters (feedId) { + return function (dispatch, getState) { + const state = getState() + const {date, from, to} = state.gtfs.filter.dateTimeFilter + const routeId = state.gtfs.stops.routeFilter + const patternId = state.gtfs.stops.patternFilter + if (!routeId && !patternId) { + return dispatch(receiveStops(feedId, routeId, patternId, {stops: []})) + } + dispatch(fetchingStops(feedId, routeId, patternId, date, from, to)) + return fetch(compose(stopsFiltered(feedId, routeId, patternId, date, from, to), {feedId, routeId, patternId, date, from, to})) + .then((response) => { + if (response.status >= 300) { + return dispatch(errorFetchingStops(feedId, routeId, patternId, date, from, to)) + } + return response.json() + }) + .then(json => { + dispatch(receiveStops(feedId, routeId, patternId, json)) + }) + } +} + +export function fetchStops (feedId, routeId, patternId, date, from, to) { + return function (dispatch, getState) { + dispatch(fetchingStops(feedId, routeId, patternId, date, from, to)) + return fetch(compose(stopsFiltered(feedId, routeId, patternId, date, from, to), {feedId, routeId, patternId, date, from, to})) + .then((response) => { + return response.json() + }) + .then(json => { + dispatch(receiveStops(feedId, routeId, patternId, json)) + }) + } +} + +export function updateStopRouteFilter (routeId) { + return { + type: 'STOP_ROUTE_FILTER_CHANGE', + payload: routeId + } +} + +export function updateStopPatternFilter (patternId) { + return { + type: 'STOP_PATTERN_FILTER_CHANGE', + payload: patternId + } +} + +export function stopDateTimeFilterChange (feedId, props) { + return function (dispatch, getState) { + dispatch(fetchStopsWithFilters(feedId)) + } +} + +export function stopPatternFilterChange (feedId, patternData) { + return function (dispatch, getState) { + const state = getState() + const {date, from, to} = state.gtfs.filter.dateTimeFilter + const patternId = (patternData && patternData.pattern_id) ? patternData.pattern_id : null + const routeId = (patternData && patternData.route_id) ? patternData.route_id : null + dispatch(updateStopPatternFilter(patternId)) + + const routeFilter = state.gtfs.stops.routeFilter + if (!routeFilter && patternId) { + dispatch(updateStopRouteFilter(routeId)) + } + if (patternId) { + dispatch(fetchStops(feedId, null, patternId, date, from, to)) + } else if (routeFilter) { // fetch stops for route if route filter set and pattern filter set to null + dispatch(fetchStops(feedId, routeFilter, null, date, from, to)) + } + } +} + +export function stopRouteFilterChange (feedId, routeData) { + return function (dispatch, getState) { + const routeId = (routeData && routeData.route_id) ? routeData.route_id : null + dispatch(updateStopRouteFilter(routeId)) + dispatch(updateStopPatternFilter(null)) + dispatch(fetchPatterns(feedId, routeId, null)) + dispatch(fetchStopsWithFilters(feedId)) + } +} diff --git a/lib/gtfs/components/GtfsFilter.js b/lib/gtfs/components/GtfsFilter.js new file mode 100644 index 000000000..4b4e00262 --- /dev/null +++ b/lib/gtfs/components/GtfsFilter.js @@ -0,0 +1,84 @@ +import React from 'react' +import { Button, DropdownButton, MenuItem, Label, ButtonToolbar } from 'react-bootstrap' + +export default class GtfsFilter extends React.Component { + componentWillMount () { + this.props.onComponentMount(this.props) + } + render () { + const buttonMinimalStyle = { + marginTop: '10px', + marginBottom: '5px' + } + const compare = function (a, b) { + var aName = a.shortName || a.name + var bName = b.shortName || b.name + if (aName < bName) return -1 + if (aName > bName) return 1 + return 0 + } + + var activeFeeds = this.props.activeFeeds.sort(compare) + var activeAndLoadedFeeds = this.props.activeFeeds.filter(f => f && this.props.loadedFeeds.findIndex(feed => feed.id === f.id) !== -1) + // var nonActiveFeeds = this.props.allFeeds.filter((feed) => { + // return (activeFeeds.indexOf(feed) === -1) + // }).sort(compare) + + var feedLookup = {} + for (const f of this.props.allFeeds) feedLookup[f.id] = f + return ( + + feed.name.length > 11 ? feed.name.substr(0, 11) + '...' : feed.name).join(' and ')}` + : `Searching ${activeAndLoadedFeeds.length} feeds`} + alt={activeAndLoadedFeeds.join(', ')} + onSelect={eventKey => { + const feed = feedLookup[eventKey] + activeFeeds.indexOf(feed) === -1 ? this.props.onAddFeed(feed) : this.props.onRemoveFeed(feed) + }} + > + {this.props.allFeeds.map((feed) => { + const disabled = this.props.loadedFeeds.findIndex(f => f.id === feed.id) === -1 + return ( + + + + {feed.shortName || feed.name.length > 11 ? feed.name.substr(0, 11) + '...' : feed.name} + + + ) + })} + + + + ) + } +} diff --git a/lib/gtfs/components/GtfsMap.js b/lib/gtfs/components/GtfsMap.js new file mode 100644 index 000000000..d59fae437 --- /dev/null +++ b/lib/gtfs/components/GtfsMap.js @@ -0,0 +1,277 @@ +import React, { Component, PropTypes } from 'react' +import { shallowEqual } from 'react-pure-render' +import { Browser } from 'leaflet' +import { Map, Marker, TileLayer, GeoJson, FeatureGroup, Rectangle } from 'react-leaflet' + +import { getFeedId } from '../../common/util/modules' +import PatternGeoJson from './PatternGeoJson' +import StopMarker from './StopMarker' +import { getFeedsBounds } from '../../common/util/geo' +import { getConfigProperty } from '../../common/util/config' + +export default class GtfsMap extends Component { + static propTypes = { + searchFocus: PropTypes.string, + bounds: PropTypes.array, + feeds: PropTypes.array, + version: PropTypes.object, + onStopClick: PropTypes.func, + onRouteClick: PropTypes.func, + onZoomChange: PropTypes.func, + popupAction: PropTypes.string, + newEntityId: PropTypes.number, + entities: PropTypes.array, + position: PropTypes.array, + stops: PropTypes.array, + patterns: PropTypes.array, + routes: PropTypes.array, + width: PropTypes.string, // % or px + height: PropTypes.number // only px + } + constructor (props) { + super(props) + this.state = { + bounds: this.props.bounds || [[70, 130], [-70, -130]], + map: {} + } + } + componentDidMount () { + this.resetMap(true) + } + resetMap (resetBounds = false) { + setTimeout(() => { + this.refs.map.leafletElement.invalidateSize() + resetBounds && this.refs.map.leafletElement.fitBounds(this.getBounds()) + }, 500) + } + mapClicked (e) { + if (this.props.showIsochrones) { + this.fetchIsochrones(e.latlng) + } + } + mapMoved (e) { + const bounds = this.refs.map && this.refs.map.leafletElement.getBounds() + const zoom = this.refs.map && this.refs.map.leafletElement.getBoundsZoom(bounds) + this.props.updateMapState && this.props.updateMapState({bounds, zoom}) + this.refs.map && !this.props.disableRefresh && this.refreshGtfsElements() + } + componentWillReceiveProps (nextProps) { + if (!this.props.disableRefresh) { + // refresh elements if feeds change + if (nextProps.feeds.length !== this.props.feeds.length && this.refs.map) { + this.refreshGtfsElements(nextProps.feeds) + } + if (nextProps.entities !== this.props.entities && this.refs.map) { + this.refreshGtfsElements(nextProps.feeds, nextProps.entities) + } + } + if (nextProps.searchFocus && nextProps.searchFocus !== this.props.searchFocus) { + this.setState({searchFocus: nextProps.searchFocus}) + } + // handle stop: panning on stop select + // pattern panTo is handled with layerAddHandler and searchFocus + if (nextProps.stop && !shallowEqual(nextProps.stop, this.props.stop)) { + this.refs.map.leafletElement.panTo([nextProps.stop.stop_lat, nextProps.stop.stop_lon]) + } + // if height or width changes, reset map + if (nextProps.height !== this.props.height || nextProps.width !== this.props.width) { + this.resetMap() + } + // recalculate isochrone if date/time changes + if (!shallowEqual(nextProps.dateTime, this.props.dateTime)) { + this.state.lastClicked && this.props.showIsochrones && this.fetchIsochrones(this.state.lastClicked) + } + } + getBounds () { + let bounds + if (this.props.feeds) { + bounds = getFeedsBounds(this.props.feeds) + } else if (this.props.version) { + bounds = this.props.version.validationSummary.bounds + } + bounds = bounds && bounds.north ? [[bounds.north, bounds.east], [bounds.south, bounds.west]] : this.state.bounds + return bounds + } + fetchIsochrones (latlng) { + const center = this.refs.map.leafletElement.getCenter() + this.props.fetchIsochrones(this.props.version, latlng.lat, latlng.lng, center.lat, center.lng) + this.setState({ lastClicked: latlng }) + } + getIsochroneColor (time) { + return time ? 'blue' : 'red' + } + refreshGtfsElements (feeds, entities) { + const zoomLevel = this.refs.map.leafletElement.getZoom() + const feedIds = (feeds || this.props.feeds).map(getFeedId) + const ents = (entities || this.props.entities || ['routes', 'stops']) + if (feedIds.length === 0 || zoomLevel <= 13) { + // this.props.clearGtfsElements() + } else { + this.props.refreshGtfsElements(feedIds, ents) + } + } + renderIsochrones () { + let comps = [] + const bandTime = this.props.isochroneBand || 60 * 60 + if (this.props.version && this.props.version.isochrones && this.props.version.isochrones.features) { + comps = this.props.version.isochrones.features.map((iso, index) => { + if (iso.properties.time !== bandTime) return null + return ( + { + return { + color: this.getIsochroneColor(iso.properties.time) + } + }} + /> + ) + }) + } + if (this.state && this.state.lastClicked) { + comps.push( + { + this.fetchIsochrones(e.target.getLatLng()) + }} + /> + ) + } + return comps + } + layerAddHandler (e) { + // handle pattern panTo and popup open + const patternId = e.layer.feature && e.layer.feature.properties.patternId + if (patternId && patternId === this.props.searchFocus) { + this.refs.map && this.refs.map.leafletElement.fitBounds(e.layer.getBounds()) + e.layer.openPopup && e.layer.openPopup() + this.setState({searchFocus: null}) + } + // open popup for stop or pattern if searchFocus is set + if (this.props.stop && this.state.searchFocus === this.props.stop.stop_id) { + e.layer.openPopup && e.layer.openPopup() + this.setState({searchFocus: null}) + } + } + render () { + const { + width, + height, + disableScroll, + showBounds, + stops, + routes, + feeds, + renderTransferPerformance, + onStopClick, + newEntityId, + popupAction, + stop, + patterns, + onRouteClick, + pattern, + showIsochrones, + sidebarExpanded + } = this.props + let mapWidth = width + if (width.indexOf('px') !== -1) { + const diff = sidebarExpanded ? 30 : 0 + mapWidth = `${width.split('px')[0] - diff}px` + } + var mapStyle = { + width: mapWidth, // % or px + height: `${height}px` // only px + } + return ( +
+ this.mapClicked(e)} + onMoveEnd={(e) => this.mapMoved(e)} + onLayerAdd={(e) => this.layerAddHandler(e)} + className='Gtfs-Map' + > + + {/* feed bounds */} + {showBounds && + + } + + {/* Stops from map bounds search */} + {stops && stops.length + ? stops.map((s, index) => { + if (!s) return null + return ( + + ) + }) + : null + } + {/* Stop from GtfsSearch */} + {stop && } + + + {/* Patterns from map bounds search */} + {patterns + ? patterns.map((pattern, index) => ( + + )) + : null + } + {/* Pattern from GtfsSearch */} + {pattern && + + } + + + {/* Isochrones from map click */} + {showIsochrones && this.renderIsochrones()} + + +
+ ) + } +} diff --git a/lib/gtfs/components/PatternGeoJson.js b/lib/gtfs/components/PatternGeoJson.js new file mode 100644 index 000000000..428f31560 --- /dev/null +++ b/lib/gtfs/components/PatternGeoJson.js @@ -0,0 +1,70 @@ +import React, {PropTypes} from 'react' +import { GeoJson, Popup } from 'react-leaflet' +import {Icon} from '@conveyal/woonerf' +import { Button } from 'react-bootstrap' + +import { getRouteName } from '../../editor/util/gtfs' +import { getFeed } from '../../common/util/modules' + +const COLORS = ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a'] + +export default class PatternGeoJson extends GeoJson { + static propTypes = { + pattern: PropTypes.object, + index: PropTypes.number, + feeds: PropTypes.array, + onRouteClick: PropTypes.func, + popupAction: PropTypes.string, + newEntityId: PropTypes.number + } + render () { + const { pattern, index = 0, feeds, onRouteClick, newEntityId, popupAction } = this.props + if (!pattern) { + return null + } + const route = pattern.route + const feedId = route ? route.feed_id || route.feed.feed_id : null + const feed = getFeed(feeds, feedId) + const routeName = route ? getRouteName(route) : pattern.route_name + const routeId = route ? route.route_id : pattern.route_id + const popup = ( + +
+

{routeName}

+
    +
  • ID: {routeId}
  • +
  • Agency:{' '} + {// TODO: change this back to feedName + // route.feed_id + feed && feed.name + } +
  • +
+ {onRouteClick + ? + :

[Must add stops first]

+ } +
+
+ ) + return ( + { + layer.feature.properties.patternId = pattern.pattern_id + layer._leaflet_id = pattern.pattern_id + }} + > + {popup} + + ) + } +} diff --git a/lib/gtfs/components/StopMarker.js b/lib/gtfs/components/StopMarker.js new file mode 100644 index 000000000..4e15c6880 --- /dev/null +++ b/lib/gtfs/components/StopMarker.js @@ -0,0 +1,51 @@ +import React, { Component, PropTypes } from 'react' +import { Marker, Popup } from 'react-leaflet' +import { Button } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import { getFeed } from '../../common/util/modules' +import { divIcon } from 'leaflet' + +import TransferPerformance from './TransferPerformance' + +export default class StopMarker extends Component { + static propTypes = { + stop: PropTypes.object + } + render () { + const { stop, feeds, renderTransferPerformance, onStopClick, newEntityId, popupAction, routes } = this.props + if (!stop) { + return null + } + const feedId = stop.feed_id || stop.feed && stop.feed.feed_id + const feed = getFeed(feeds, feedId) + const busIcon = divIcon({ + html: ` + + + `, + className: '', + iconSize: [24, 24] + }) + return ( + + +
+

{stop.stop_name} ({stop.stop_id})

+ {renderTransferPerformance && } + {onStopClick && ( + + )} +
+
+
+ ) + } +} diff --git a/lib/gtfs/components/TransferPerformance.js b/lib/gtfs/components/TransferPerformance.js new file mode 100644 index 000000000..c1f2cbe54 --- /dev/null +++ b/lib/gtfs/components/TransferPerformance.js @@ -0,0 +1,52 @@ +import React, {Component} from 'react' +import { ControlLabel, FormControl } from 'react-bootstrap' +import moment from 'moment' + +export default class TransferPerformance extends Component { + constructor (props) { + super(props) + this.state = { + index: 0 + } + } + renderTransferPerformanceResult (transferPerformance) { + if (!transferPerformance) { + return

No transfers found

+ } + return ( +
    +
  • Typical case: {moment.duration(transferPerformance.typicalCase, 'seconds').humanize()}
  • +
  • Best case: {moment.duration(transferPerformance.bestCase, 'seconds').humanize()}
  • +
  • Worst case: {moment.duration(transferPerformance.worstCase, 'seconds').humanize()}
  • +
+ ) + } + render () { + const { stop, routes } = this.props + return stop.transferPerformance && stop.transferPerformance.length + ?
+ Transfer performance + { + const index = +evt.target.value + this.setState({index}) + }} + > + {stop.transferPerformance + // .sort((a, b) => { + // + // }) + .map((summary, index) => { + const fromRoute = routes.find(r => r.route_id === summary.fromRoute) + const toRoute = routes.find(r => r.route_id === summary.toRoute) + return + }) + } + + {this.renderTransferPerformanceResult(stop.transferPerformance[this.state.index])} +
+ :

No transfers found

+ } +} diff --git a/lib/gtfs/components/gtfsmapsearch.js b/lib/gtfs/components/gtfsmapsearch.js new file mode 100644 index 000000000..d15242aee --- /dev/null +++ b/lib/gtfs/components/gtfsmapsearch.js @@ -0,0 +1,105 @@ +import React, { Component, PropTypes } from 'react' +import fetch from 'isomorphic-fetch' +import { Button } from 'react-bootstrap' + +import ActiveGtfsMap from '../containers/ActiveGtfsMap' +import GtfsSearch from './gtfssearch' + +export default class GtfsMapSearch extends Component { + static propTypes = { + placeholder: PropTypes.string + } + constructor (props) { + super(props) + this.state = { + stop: null, + pattern: null, + message: '', + searching: ['stops', 'routes'], + map: {} + } + } + getPatterns (input) { + return fetch(`/api/manager/patterns?route=${input.route.route_id}&feed=${input.route.feed_id}`) + .then((response) => { + return response.json() + }) + .then((json) => { + const pattern = json[0] + // hack to associate route to pattern + pattern.route = input.route + return pattern + }) + } + handleSelection (input) { + if (!input) { + this.setState({stop: null, pattern: null, searchFocus: null}) + } else if (input && input.stop) { + const pattern = null + const stop = input.stop + this.setState({ stop, pattern, searchFocus: stop.stop_id }) + } else if (input && input.route) { + // TODO: replace with GraphQL + return Promise.all([this.getPatterns(input)]).then((results) => { + const pattern = results[0] + const stop = null + this.setState({ pattern, stop, searchFocus: pattern.pattern_id }) + }) + } + } + render () { + let zoomMessage = 'Zoom in to view ' + this.state.searching.join(' and ') + if (this.refs.map && this.refs.map.refs.map) { + const mapZoom = this.refs.map.refs.map.leafletElement.getZoom() + zoomMessage = mapZoom <= 13 ? zoomMessage : '' + } + const searchProps = { + stop: this.state.stop, + pattern: this.state.pattern, + searchFocus: this.state.searchFocus, + entities: this.state.searching + } + return ( +
+ this.handleSelection(input)} + entities={this.state.searching} + /> +
    +
  • + +
  • +
  • {zoomMessage}
  • +
+ +
+ ) + } +} diff --git a/src/main/client/gtfs/components/gtfssearch.js b/lib/gtfs/components/gtfssearch.js similarity index 57% rename from src/main/client/gtfs/components/gtfssearch.js rename to lib/gtfs/components/gtfssearch.js index 89d24de94..3dda68e25 100644 --- a/src/main/client/gtfs/components/gtfssearch.js +++ b/lib/gtfs/components/gtfssearch.js @@ -1,19 +1,19 @@ -import React, { PropTypes } from 'react' +import React, { Component, PropTypes } from 'react' import fetch from 'isomorphic-fetch' -import { Panel, Grid, Row, Col, Button, Glyphicon, Label } from 'react-bootstrap' -import { PureComponent, shallowEqual } from 'react-pure-render' -import { Map, Marker, Popup, TileLayer, Polyline, MapControl } from 'react-leaflet' +import { Glyphicon, Label } from 'react-bootstrap' import Select from 'react-select' import { getFeed, getFeedId } from '../../common/util/modules' -export default class GtfsSearch extends React.Component { - - constructor(props) { +export default class GtfsSearch extends Component { + static propTypes = { + value: PropTypes.string + } + constructor (props) { super(props) this.state = { value: this.props.value - }; + } } cacheOptions (options) { @@ -29,18 +29,28 @@ export default class GtfsSearch extends React.Component { } renderOption (option) { - return {option.stop ? : } {option.label} {option.link} + return ( + + {option.stop + ? + : + } {option.label} {option.link} + + ) } onChange (value) { - // if (typeof value !== 'undefined') - this.setState({value}) + this.props.onChange && this.props.onChange(value) + this.setState({value}) } - render() { - console.log('render search feeds', this.props.feeds) + render () { const getRouteName = (route) => { - let routeName = route.route_short_name && route.route_long_name ? `${route.route_short_name} - ${route.route_long_name}` : - route.route_long_name ? route.route_long_name : - route.route_short_name ? route.route_short_name : null + const routeName = route.route_short_name && route.route_long_name + ? `${route.route_short_name} - ${route.route_long_name}` + : route.route_long_name + ? route.route_long_name + : route.route_short_name + ? route.route_short_name + : null return routeName } const getStops = (input) => { @@ -57,7 +67,15 @@ export default class GtfsSearch extends React.Component { }) .then((stops) => { const stopOptions = stops !== null && stops.length > 0 - ? stops.map(stop => { + ? stops.sort((a, b) => { + const aStopName = a && a.stop_name && a.stop_name.toLowerCase() + const bStopName = b && b.stop_name && b.stop_name.toLowerCase() + if (aStopName.startsWith(input)) { + return bStopName.startsWith(input) ? aStopName.localeCompare(bStopName) : -1 + } else { + return bStopName.startsWith(input) ? 1 : aStopName.localeCompare(bStopName) + } + }).map(stop => { const agency = getFeed(this.props.feeds, stop.feed_id) return { stop, @@ -76,7 +94,6 @@ export default class GtfsSearch extends React.Component { } const getRoutes = (input) => { const feedIds = this.props.feeds.map(getFeedId) - console.log(feedIds) if (!feedIds.length) return [] @@ -90,7 +107,16 @@ export default class GtfsSearch extends React.Component { }) .then((routes) => { const routeOptions = routes !== null && routes.length > 0 - ? routes.map(route => ( + ? routes.sort((a, b) => { + const aRouteName = a && getRouteName(a).toLowerCase() + const bRouteName = b && getRouteName(b).toLowerCase() + if (aRouteName.startsWith(input)) { + return bRouteName.startsWith(input) ? aRouteName.localeCompare(bRouteName) : -1 + } else { + return bRouteName.startsWith(input) ? 1 : aRouteName.localeCompare(bRouteName) + } + // return 0 + }).map(route => ( { route, value: route.route_id, @@ -106,30 +132,22 @@ export default class GtfsSearch extends React.Component { }) } const getOptions = (input) => { - const entities = typeof this.props.entities !== 'undefined' ? this.props.entities : ['routes', 'stops'] - let entitySearches = [] - if (entities.indexOf('stops') > -1){ + const entitySearches = [] + if (entities.indexOf('stops') > -1) { entitySearches.push(getStops(input)) } - if (entities.indexOf('routes') > -1){ + if (entities.indexOf('routes') > -1) { entitySearches.push(getRoutes(input)) } return Promise.all(entitySearches).then((results) => { const stops = results[0] const routes = typeof results[1] !== 'undefined' ? results[1] : [] - const options = { options: [...stops,...routes] } + const options = { options: [...stops, ...routes] } // console.log('search options', options) return options }) } - const handleChange = (input) => { - console.log(input) - this.onChange(input) - this.props.onChange(input) - console.log(this.state) - } - const onFocus = (input) => { // clear options to onFocus to ensure only valid route/stop combinations are selected this.refs.gtfsSelect.loadOptions('') @@ -137,20 +155,21 @@ export default class GtfsSearch extends React.Component { const placeholder = 'Begin typing to search for ' + this.props.entities.join(' or ') + '...' return ( - + this.onChange(value)} + /> ) } } diff --git a/lib/gtfs/containers/ActiveGtfsMap.js b/lib/gtfs/containers/ActiveGtfsMap.js new file mode 100644 index 000000000..a7773197d --- /dev/null +++ b/lib/gtfs/containers/ActiveGtfsMap.js @@ -0,0 +1,60 @@ +import { connect } from 'react-redux' + +import GtfsMap from '../components/GtfsMap' +import { clearGtfsElements, refreshGtfsElements } from '../actions/general' +import { stopPatternFilterChange, stopRouteFilterChange, stopDateTimeFilterChange } from '../actions/stops' +import { updateMapState } from '../actions/filter' +import { fetchFeedVersionIsochrones } from '../../manager/actions/versions' + +const mapStateToProps = (state, ownProps) => { + return { + stops: state.gtfs.stops.data, + routes: state.gtfs.routes.data, + patterns: state.gtfs.patterns.data, + routing: state.routing.locationBeforeTransitions && state.routing.locationBeforeTransitions.pathname, + dateTime: state.gtfs.filter.dateTimeFilter, + sidebarExpanded: state.ui.sidebarExpanded + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + const feedId = ownProps.version && ownProps.version.id.replace('.zip', '') + return { + onComponentMount: (initialProps) => { + // if (!initialProps.routes.fetchStatus.fetched) { + // dispatch(fetchRoutes(feedId)) + // } + // if (!initialProps.patterns.fetchStatus.fetched) { + // dispatch(fetchPatterns(feedId, null)) + // } + }, + updateMapState: (props) => { + dispatch(updateMapState(props)) + }, + clearGtfsElements: () => { + dispatch(clearGtfsElements()) + }, + refreshGtfsElements: (feedIds, entities) => { + dispatch(refreshGtfsElements(feedIds, entities)) + }, + stopRouteFilterChange: (newValue) => { + dispatch(stopRouteFilterChange(feedId, newValue)) + }, + stopPatternFilterChange: (newValue) => { + dispatch(stopPatternFilterChange(feedId, newValue)) + }, + stopDateTimeFilterChange: (props) => { + dispatch(stopDateTimeFilterChange(feedId, props)) + }, + fetchIsochrones: (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) => { + dispatch(fetchFeedVersionIsochrones(feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime)) + } + } +} + +const ActiveGtfsMap = connect( + mapStateToProps, + mapDispatchToProps +)(GtfsMap) + +export default ActiveGtfsMap diff --git a/src/main/client/gtfs/containers/GlobalGtfsFilter.js b/lib/gtfs/containers/GlobalGtfsFilter.js similarity index 66% rename from src/main/client/gtfs/containers/GlobalGtfsFilter.js rename to lib/gtfs/containers/GlobalGtfsFilter.js index e47bd7355..25f5e7080 100644 --- a/src/main/client/gtfs/containers/GlobalGtfsFilter.js +++ b/lib/gtfs/containers/GlobalGtfsFilter.js @@ -1,16 +1,14 @@ -import React from 'react' import { connect } from 'react-redux' import GtfsFilter from '../components/GtfsFilter' - import { addActiveFeed, removeActiveFeed, addAllActiveFeeds, - removeAllActiveFeeds, setPermissionFilter, updateGtfsFilter } from '../actions/gtfsFilter' + removeAllActiveFeeds, updateGtfsFilter } from '../actions/filter' const mapStateToProps = (state, ownProps) => { return { - activeFeeds: state.gtfsFilter.activeFeeds, - allFeeds: state.gtfsFilter.allFeeds, - loadedFeeds: state.gtfsFilter.loadedFeeds, + activeFeeds: state.gtfs.filter.activeFeeds, + allFeeds: state.gtfs.filter.allFeeds, + loadedFeeds: state.gtfs.filter.loadedFeeds, user: state.user, project: state.projects.active } @@ -19,10 +17,11 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch, ownProps) => { return { onComponentMount: (initialProps) => { - let filter = initialProps.permissionFilter || 'view-feed' - dispatch(setPermissionFilter(filter)) - if (initialProps.project && initialProps.user) + // let filter = initialProps.permissionFilter || 'view-feed' + // dispatch(updatePermissionFilter(filter)) + if (initialProps.project && initialProps.user) { dispatch(updateGtfsFilter(initialProps.project, initialProps.user)) + } }, onAddFeed: (feed) => dispatch(addActiveFeed(feed)), onRemoveFeed: (feed) => dispatch(removeActiveFeed(feed)), diff --git a/lib/gtfs/reducers/feed.js b/lib/gtfs/reducers/feed.js new file mode 100644 index 000000000..86ac4b191 --- /dev/null +++ b/lib/gtfs/reducers/feed.js @@ -0,0 +1,67 @@ +import update from 'react-addons-update' + +const defaultState = { + fetchStatus: { + fetched: false, + fetching: false, + error: false + }, + data: [] +} + +const feedStatKeyDescription = { + feed_id: 'Feed Id', + feed_publisher_name: 'Publisher', + feed_publisher_url: 'Publisher URL', + feed_lang: 'Language Code', + feed_version: 'Feed Version', + route_count: 'Number of Routes in Feed', + stop_count: 'Number of Stops in Feed' +} + +export default function reducer (state = defaultState, action) { + switch (action.type) { + case 'SET_ACTIVE_FEEDVERSION': + return defaultState + case 'FETCH_GRAPHQL_FEED_PENDING': + return { + fetchStatus: { + fetched: false, + fetching: true, + error: false + }, + data: [] + } + case 'FETCH_GRAPHQL_FEED_REJECTED': + return update(state, { + fetchStatus: { + $set: { + fetched: false, + fetching: false, + error: true + } + }, + data: [] + }) + case 'FETCH_GRAPHQL_FEED_FULFILLED': + const feedData = action.data.feeds[0] + const feedStats = [] + const feedKeys = Object.keys(feedData) + for (let i = 0; i < feedKeys.length; i++) { + feedStats.push({ + statName: feedStatKeyDescription[feedKeys[i]], + statValue: feedData[feedKeys[i]] + }) + } + return { + fetchStatus: { + fetched: true, + fetching: false, + error: false + }, + data: feedStats + } + default: + return state + } +} diff --git a/src/main/client/gtfs/reducers/gtfsFilter.js b/lib/gtfs/reducers/filter.js similarity index 59% rename from src/main/client/gtfs/reducers/gtfsFilter.js rename to lib/gtfs/reducers/filter.js index f1c2adca6..e26e0372c 100644 --- a/src/main/client/gtfs/reducers/gtfsFilter.js +++ b/lib/gtfs/reducers/filter.js @@ -1,26 +1,50 @@ import update from 'react-addons-update' +import moment from 'moment' const gtfsFilter = (state = { allFeeds: [], activeFeeds: [], loadedFeeds: [], - permissionFilter: 'view-feed' + typeFilter: ['stops', 'routes'], + map: { + bounds: [], + zoom: null + }, + permissionFilter: 'view-feed', + version: null, + dateTimeFilter: { + date: moment().format('YYYY-MM-DD'), + from: 60 * 60 * 6, + to: 60 * 60 * 9 + } }, action) => { // console.log(action) - let activeFeeds + let activeFeeds, dateTimeFilter switch (action.type) { - case 'SET_GTFS_PERMISSION_FILTER': + case 'SET_ACTIVE_FEEDVERSION': + return update(state, {version: {$set: action.feedVersion ? action.feedVersion.id : null}}) + case 'UPDATE_GTFS_MAP_STATE': + return update(state, {map: {$set: action.props}}) + case 'UPDATE_GTFS_PERMISSION_FILTER': return update(state, {permissionFilter: {$set: action.permission}}) + case 'UPDATE_GTFS_DATETIME_FILTER': + dateTimeFilter = {...state.dateTimeFilter} + for (const key in action.props) { + dateTimeFilter[key] = action.props[key] + } + return update(state, { + dateTimeFilter: {$set: dateTimeFilter} + }) case 'UPDATE_GTFS_FILTER': let userFeeds = [] - if (action.user.permissions.isProjectAdmin(action.activeProject.id)) { + if (action.user.permissions.isProjectAdmin(action.activeProject.id, action.activeProject.organizationId)) { userFeeds = action.activeProject.feedSources || [] - } else if (action.user.permissions.hasProjectPermission(action.activeProject.id, state.permissionFilter)) { + } else if (action.user.permissions.hasProjectPermission(action.activeProject.organizationId, action.activeProject.id, state.permissionFilter)) { userFeeds = action.activeProject.feedSources ? action.activeProject.feedSources.filter((feed) => { - return action.user.permissions.hasFeedPermission(action.activeProject.id, feed.id, state.permissionFilter) !== null + return action.user.permissions.hasFeedPermission(action.activeProject.organizationId, action.activeProject.id, feed.id, state.permissionFilter) !== null }) : [] } - let validatedFeeds = userFeeds.filter((feed) => { + const validatedFeeds = userFeeds.filter((feed) => { return feed.latestVersionId !== undefined }) return update(state, { @@ -39,7 +63,7 @@ const gtfsFilter = (state = { return update(state, {activeFeeds: {$set: activeFeeds}}) case 'REMOVE_ACTIVE_FEED': - let foundIndex = state.activeFeeds.findIndex(f => f.id === action.feed.id) + const foundIndex = state.activeFeeds.findIndex(f => f.id === action.feed.id) if (foundIndex !== -1) { activeFeeds = [ ...state.activeFeeds.slice(0, foundIndex), diff --git a/lib/gtfs/reducers/index.js b/lib/gtfs/reducers/index.js new file mode 100644 index 000000000..808fd0276 --- /dev/null +++ b/lib/gtfs/reducers/index.js @@ -0,0 +1,15 @@ +import { combineReducers } from 'redux' + +import filter from './filter' +import feed from './feed' +import patterns from './patterns' +import routes from './routes' +import stops from './stops' + +export default combineReducers({ + filter, + feed, + patterns, + routes, + stops +}) diff --git a/lib/gtfs/reducers/patterns.js b/lib/gtfs/reducers/patterns.js new file mode 100644 index 000000000..90bf2527e --- /dev/null +++ b/lib/gtfs/reducers/patterns.js @@ -0,0 +1,97 @@ +import update from 'react-addons-update' + +import { getRouteName } from '../../editor/util/gtfs' + +const defaultState = { + routeFilter: null, + fetchStatus: { + fetched: false, + fetching: false, + error: false + }, + data: [] +} + +export default function reducer (state = defaultState, action) { + switch (action.type) { + case 'SET_ACTIVE_FEEDVERSION': + return defaultState + case 'FETCH_GRAPHQL_PATTERNS': + return update(state, + { + fetchStatus: { + $set: { + fetched: false, + fetching: true, + error: false + } + }, + data: {$set: []} + } + ) + // case 'CLEAR_GRAPHQL_PATTERNS': + // return update(state, { + // fetchStatus: { + // $set: { + // fetched: false, + // fetching: false, + // error: false + // }, + // data: {$set: []} + // } + // }) + case 'FETCH_GRAPHQL_PATTERNS_REJECTED': + return update(state, { + fetchStatus: { + $set: { + fetched: false, + fetching: false, + error: true + } + } + }) + case 'RECEIVED_GTFS_ELEMENTS': + return update(state, { + fetchStatus: { + $set: { + fetched: true, + fetching: false, + error: false + } + }, + data: {$set: action.patterns} + }) + case 'FETCH_GRAPHQL_PATTERNS_FULFILLED': + const allRoutes = action.data ? action.data.routes : [] + const allPatterns = [] + + for (let i = 0; i < allRoutes.length; i++) { + const curRouteId = allRoutes[i].route_id + const curRouteName = getRouteName(allRoutes[i]) + + for (let j = 0; j < allRoutes[i].patterns.length; j++) { + const curPattern = allRoutes[i].patterns[j] + curPattern.route_id = curRouteId + curPattern.route_name = curRouteName + allPatterns.push(curPattern) + } + } + + return update(state, + { + fetchStatus: { + $set: { + fetched: true, + fetching: false, + error: false + } + }, + data: {$set: allPatterns} + } + ) + case 'PATTERN_ROUTE_FILTER_CHANGE': + return update(state, {routeFilter: { $set: action.payload }}) + default: + return state + } +} diff --git a/lib/gtfs/reducers/routes.js b/lib/gtfs/reducers/routes.js new file mode 100644 index 000000000..3186a14eb --- /dev/null +++ b/lib/gtfs/reducers/routes.js @@ -0,0 +1,55 @@ +import update from 'react-addons-update' + +import { getRouteName } from '../../editor/util/gtfs' + +const defaultState = { + fetchStatus: { + fetched: false, + fetching: false, + error: false + }, + data: [] +} + +export default function reducer (state = defaultState, action) { + switch (action.type) { + case 'SET_ACTIVE_FEEDVERSION': + return defaultState + case 'FETCH_GRAPHQL_ROUTES': + return { + fetchStatus: { + fetched: false, + fetching: true, + error: false + }, + data: [] + } + case 'FETCH_GRAPHQL_ROUTES_REJECTED': + return update(state, { + fetchStatus: { + $set: { + fetched: false, + fetching: false, + error: true + } + } + }) + case 'FETCH_GRAPHQL_ROUTES_FULFILLED': + const newRoutes = [] + for (let i = 0; i < action.data.routes.length; i++) { + const curRoute = action.data.routes[i] + curRoute.route_name = getRouteName(curRoute) + newRoutes.push(curRoute) + } + return { + fetchStatus: { + fetched: true, + fetching: false, + error: false + }, + data: newRoutes + } + default: + return state + } +} diff --git a/lib/gtfs/reducers/stops.js b/lib/gtfs/reducers/stops.js new file mode 100644 index 000000000..3650c9612 --- /dev/null +++ b/lib/gtfs/reducers/stops.js @@ -0,0 +1,96 @@ +import update from 'react-addons-update' + +import { getRouteName } from '../../editor/util/gtfs' + +const defaultState = { + routeFilter: null, + patternFilter: null, + fetchStatus: { + fetched: false, + fetching: false, + error: false + }, + data: [] +} + +export default function reducer (state = defaultState, action) { + switch (action.type) { + case 'SET_ACTIVE_FEEDVERSION': + return defaultState + case 'FETCH_GRAPHQL_STOPS': + return update(state, + { + fetchStatus: { + $set: { + fetched: false, + fetching: true, + error: false + } + }, + data: {$set: []} + } + ) + case 'FETCH_GRAPHQL_STOPS_REJECTED': + return update(state, { + fetchStatus: { + $set: { + fetched: false, + fetching: false, + error: true + }, + data: {$set: []} + } + }) + case 'RECEIVED_GTFS_ELEMENTS': + return update(state, { + fetchStatus: { + $set: { + fetched: true, + fetching: false, + error: false + } + }, + data: {$set: action.stops} + }) + case 'FETCH_GRAPHQL_STOPS_FULFILLED': + const allRoutes = action.data.routes || [] + const allPatterns = [] + const allStops = action.data.stops || [] + + for (let i = 0; i < allRoutes.length; i++) { + const route = allRoutes[i] + const curRouteId = route.route_id + const curRouteName = getRouteName(route) + + for (let j = 0; j < route.patterns.length; j++) { + const pattern = allRoutes[i].patterns[j] + pattern.route_id = curRouteId + pattern.route_name = curRouteName + allPatterns.push(pattern) + for (let k = 0; k < pattern.stops.length; k++) { + const stop = pattern.stops[k] + allStops.push(stop) + } + } + } + + return update(state, + { + fetchStatus: { + $set: { + fetched: true, + fetching: false, + error: false + } + }, + data: {$set: allStops} + } + ) + case 'STOP_ROUTE_FILTER_CHANGE': + return update(state, {routeFilter: { $set: action.payload }}) + case 'STOP_PATTERN_FILTER_CHANGE': + return update(state, {patternFilter: { $set: action.payload }}) + default: + return state + } +} diff --git a/lib/gtfs/util/__tests__/stats.js b/lib/gtfs/util/__tests__/stats.js new file mode 100644 index 000000000..c0d4ad088 --- /dev/null +++ b/lib/gtfs/util/__tests__/stats.js @@ -0,0 +1,9 @@ +/* globals describe, expect, it */ + +import * as stats from '../stats' + +describe('gtfs > util > stats', () => { + it('formatSpeed should work', () => { + expect(stats.formatSpeed(123)).toEqual(275) + }) +}) diff --git a/lib/gtfs/util/graphql.js b/lib/gtfs/util/graphql.js new file mode 100644 index 000000000..eeaf733db --- /dev/null +++ b/lib/gtfs/util/graphql.js @@ -0,0 +1,200 @@ +// variable names/keys must match those specified in GraphQL schema +export function compose (query, variables) { + return `/api/manager/graphql?query=${encodeURIComponent(query)}&variables=${encodeURIComponent(JSON.stringify(variables))}` +} + +export const feed = ` +query feedQuery($feedId: [String]) { + feeds (feed_id: $feedId) { + feed_id, + # feed_publisher_name, + # feed_publisher_url, + # feed_lang, + # feed_version, + # route_count, + # stop_count, + mergedBuffer + } +} +` + +export const routes = ` +query routeQuery($feedId: [String]) { + routes(feed_id: $feedId) { + route_id + route_short_name + route_long_name, + route_desc, + route_url, + trip_count, + pattern_count + } +} +` + +export const patterns = ` +query patternsQuery($feedId: [String], $routeId: [String], $date: String, $from: Long, $to: Long) { + routes (feed_id: $feedId, route_id: $routeId) { + route_id, + route_short_name, + route_long_name, + patterns { + pattern_id, + name, + geometry, + stop_count, + trip_count, + stats(date: $date, from: $from, to: $to){ + headway, + avgSpeed + }, + } + } +} +` + +export const stops = () => { + return ` + query allStopsQuery($feedId: [String]) { + stops(feed_id: $feedId) { + stops { + stop_id, + stop_name, + stop_name, + stop_code, + stop_desc, + stop_lon, + stop_lat, + zone_id, + stop_url, + stop_timezone + } + } + } + ` +} + +export const allStops = ` +query allStopsQuery($feedId: [String]) { + stops(feed_id: $feedId) { + stops { + stop_id, + stop_name, + stop_name, + stop_code, + stop_desc, + stop_lon, + stop_lat, + zone_id, + stop_url, + stop_timezone + } + } +} +` + +export const patternsAndStopsForBoundingBox = (feedId, entities, maxLat, maxLon, minLat, minLon) => ` + query patternsAndStopsGeo($feedId: [String], $max_lat: Float, $max_lon: Float, $min_lat: Float, $min_lon: Float){ + ${entities.indexOf('routes') !== -1 + ? `patterns(feed_id: $feedId, max_lat: $max_lat, max_lon: $max_lon, min_lat: $min_lat, min_lon: $min_lon){ + pattern_id, + geometry, + name, + route{ + route_id, + route_short_name, + route_long_name, + route_color, + feed{ + feed_id + }, + } + },` + : '' + } + ${entities.indexOf('stops') !== -1 + ? `stops(feed_id: $feedId, max_lat: $max_lat, max_lon: $max_lon, min_lat: $min_lat, min_lon: $min_lon){ + stop_id, + stop_code, + stop_name, + stop_desc, + stop_lat, + stop_lon, + feed{ + feed_id + } + }` + : '' + } + } +` + +// for use in entity fetching for signs / alerts +export const stopsAndRoutes = (feedId, routeId, stopId) => ` + query routeStopQuery($feedId: [String], ${routeId ? '$routeId: [String],' : ''}, ${stopId ? '$stopId: [String],' : ''}){ + feeds(feed_id: $feedId){ + feed_id, + ${stopId + ? `stops (stop_id: $stopId){ + stop_id, + stop_name, + stop_code + },` + : '' + } + ${routeId + ? `routes(route_id: $routeId){ + route_id, + route_short_name, + route_long_name, + },` + : '' + } + } + } +` + +// TODO: add back in patternId filter +export const stopsFiltered = (feedId, routeId, patternId, date, from, to) => { + const hasFrom = typeof from !== 'undefined' && from !== null + const hasTo = typeof to !== 'undefined' && to !== null + const query = ` + query filteredStopsQuery( + ${feedId ? '$feedId: [String],' : ''} + ${routeId ? '$routeId: [String],' : ''} + ${patternId ? '$patternId: [String],' : ''} + ${date ? '$date: String,' : ''} + ${hasFrom ? '$from: Long,' : ''} + ${hasTo ? '$to: Long' : ''} + ) { + stops(${feedId ? 'feed_id: $feedId,' : ''} ${routeId ? 'route_id: $routeId,' : ''} ${patternId ? 'pattern_id: $patternId,' : ''}) { + stop_id, + stop_name, + stop_name, + stop_code, + stop_desc, + stop_lon, + stop_lat, + ${date && hasFrom && hasTo + ? ` + stats(date: $date, from: $from, to: $to){ + headway, + tripCount + }, + transferPerformance(date: $date, from: $from, to: $to){ + fromRoute, + toRoute, + bestCase, + worstCase, + typicalCase + }, + ` + : ''} + # zone_id, + # stop_url, + # stop_timezone + } + } + ` + return query +} diff --git a/lib/gtfs/util/stats.js b/lib/gtfs/util/stats.js new file mode 100644 index 000000000..2b453649f --- /dev/null +++ b/lib/gtfs/util/stats.js @@ -0,0 +1,13 @@ +export function formatHeadway (seconds) { + return seconds > 0 + ? Math.round(seconds / 60) + : seconds === 0 + ? 0 + : 'N/A' +} + +export function formatSpeed (metersPerSecond) { + return metersPerSecond >= 0 + ? Math.round(metersPerSecond * 2.236936) + : 'N/A' +} diff --git a/src/main/client/gtfsplus/actions/gtfsplus.js b/lib/gtfsplus/actions/gtfsplus.js similarity index 82% rename from src/main/client/gtfsplus/actions/gtfsplus.js rename to lib/gtfsplus/actions/gtfsplus.js index 72fc09165..a6c23c8d3 100644 --- a/src/main/client/gtfsplus/actions/gtfsplus.js +++ b/lib/gtfsplus/actions/gtfsplus.js @@ -1,4 +1,5 @@ import JSZip from 'jszip' +import fetch from 'isomorphic-fetch' import { secureFetch } from '../../common/util/util' import { getConfigProperty } from '../../common/util/config' @@ -10,8 +11,8 @@ import { getFeedId } from '../../common/util/modules' export function addGtfsPlusRow (tableId) { const table = getConfigProperty('modules.gtfsplus.spec').find(t => t.id === tableId) - let rowData = {} - for(const field of table.fields) { + const rowData = {} + for (const field of table.fields) { rowData[field.name] = null } @@ -40,18 +41,17 @@ export function deleteGtfsPlusRow (tableId, rowIndex) { } } - // DOWNLOAD/RECEIVE DATA ACTIONS export function requestingGtfsPlusContent () { return { - type: 'REQUESTING_GTFSPLUS_CONTENT', + type: 'REQUESTING_GTFSPLUS_CONTENT' } } export function clearGtfsPlusContent () { return { - type: 'CLEAR_GTFSPLUS_CONTENT', + type: 'CLEAR_GTFSPLUS_CONTENT' } } @@ -69,12 +69,12 @@ export function downloadGtfsPlusFeed (feedVersionId) { return function (dispatch, getState) { dispatch(requestingGtfsPlusContent()) - const fetchFeed = fetch('/api/manager/secure/gtfsplus/'+ feedVersionId, { + const fetchFeed = fetch('/api/manager/secure/gtfsplus/' + feedVersionId, { method: 'get', cache: 'default', headers: { 'Authorization': 'Bearer ' + getState().user.token } }).then((response) => { - if(response.status !== 200) { + if (response.status !== 200) { console.log('error downloading gtfs+ feed', response.statusCode) dispatch(clearGtfsPlusContent()) } @@ -86,9 +86,9 @@ export function downloadGtfsPlusFeed (feedVersionId) { Promise.all([fetchFeed, fetchTimestamp]).then(([feed, timestamp]) => { JSZip.loadAsync(feed).then((zip) => { - let filenames = [] - let filePromises = [] - zip.forEach((path,file) => { + const filenames = [] + const filePromises = [] + zip.forEach((path, file) => { filenames.push(path) filePromises.push(file.async('string')) }) @@ -105,7 +105,7 @@ export function downloadGtfsPlusFeed (feedVersionId) { export function validatingGtfsPlusFeed () { return { - type: 'VALIDATING_GTFSPLUS_FEED', + type: 'VALIDATING_GTFSPLUS_FEED' } } @@ -123,7 +123,6 @@ export function validateGtfsPlusFeed (feedVersionId) { return secureFetch(url, getState()) .then(res => res.json()) .then(validationIssues => { - //console.log('got GTFS+ val result', validationResult) dispatch(receiveGtfsPlusValidation(validationIssues)) }) } @@ -133,13 +132,13 @@ export function validateGtfsPlusFeed (feedVersionId) { export function uploadingGtfsPlusFeed () { return { - type: 'UPLOADING_GTFSPLUS_FEED', + type: 'UPLOADING_GTFSPLUS_FEED' } } export function uploadedGtfsPlusFeed () { return { - type: 'UPLOADED_GTFSPLUS_FEED', + type: 'UPLOADED_GTFSPLUS_FEED' } } @@ -147,7 +146,7 @@ export function uploadGtfsPlusFeed (feedVersionId, file) { return function (dispatch, getState) { dispatch(uploadingGtfsPlusFeed()) const url = `/api/manager/secure/gtfsplus/${feedVersionId}` - var data = new FormData() + var data = new window.FormData() data.append('file', file) return fetch(url, { @@ -170,17 +169,15 @@ export function receiveGtfsEntities (gtfsEntities) { } export function loadGtfsEntities (tableId, rows, feedSource) { - return function (dispatch, getState) { - // lookup table for mapping tableId:fieldName keys to inputType values const typeLookup = {} - const getDataType = function(tableId, fieldName) { + const getDataType = function (tableId, fieldName) { const lookupKey = tableId + ':' + fieldName - if(lookupKey in typeLookup) return typeLookup[lookupKey] + if (lookupKey in typeLookup) return typeLookup[lookupKey] const fieldInfo = getConfigProperty('modules.gtfsplus.spec') .find(t => t.id === tableId).fields.find(f => f.name === fieldName) - if(!fieldInfo) return null + if (!fieldInfo) return null typeLookup[lookupKey] = fieldInfo.inputType return fieldInfo.inputType } @@ -191,22 +188,22 @@ export function loadGtfsEntities (tableId, rows, feedSource) { const currentLookup = getState().gtfsplus.gtfsEntityLookup - for(const rowData of rows) { - for(const fieldName in rowData) { - switch(getDataType(tableId, fieldName)) { + for (const rowData of rows) { + for (const fieldName in rowData) { + switch (getDataType(tableId, fieldName)) { case 'GTFS_ROUTE': const routeId = rowData[fieldName] - if(routeId && !(`route_${routeId}` in currentLookup)) routesToLoad.push(routeId) - break; + if (routeId && !(`route_${routeId}` in currentLookup)) routesToLoad.push(routeId) + break case 'GTFS_STOP': const stopId = rowData[fieldName] - if(stopId && !(`stop_${stopId}` in currentLookup)) stopsToLoad.push(stopId) - break; + if (stopId && !(`stop_${stopId}` in currentLookup)) stopsToLoad.push(stopId) + break } } } - if(routesToLoad.length === 0 && stopsToLoad.length === 0) return + if (routesToLoad.length === 0 && stopsToLoad.length === 0) return const feedId = getFeedId(feedSource) var loadRoutes = Promise.all(routesToLoad.map(routeId => { const url = `/api/manager/routes/${routeId}?feed=${feedId}` @@ -236,7 +233,7 @@ export function loadGtfsEntities (tableId, rows, feedSource) { export function publishingGtfsPlusFeed () { return { - type: 'PUBLISHING_GTFSPLUS_FEED', + type: 'PUBLISHING_GTFSPLUS_FEED' } } @@ -246,7 +243,7 @@ export function publishGtfsPlusFeed (feedVersion) { const url = `/api/manager/secure/gtfsplus/${feedVersion.id}/publish` return secureFetch(url, getState(), 'post') .then((res) => { - console.log('published done'); + console.log('published done') return dispatch(fetchFeedVersions(feedVersion.feedSource)) }) } diff --git a/src/main/client/gtfsplus/components/GtfsPlusEditor.js b/lib/gtfsplus/components/GtfsPlusEditor.js similarity index 70% rename from src/main/client/gtfsplus/components/GtfsPlusEditor.js rename to lib/gtfsplus/components/GtfsPlusEditor.js index d7c903d93..76249b45f 100644 --- a/src/main/client/gtfsplus/components/GtfsPlusEditor.js +++ b/lib/gtfsplus/components/GtfsPlusEditor.js @@ -8,7 +8,9 @@ import GtfsPlusTable from './GtfsPlusTable' import { getConfigProperty } from '../../common/util/config' export default class GtfsPlusEditor extends Component { - + static propTypes = { + onComponentMount: PropTypes.function + } constructor (props) { super(props) @@ -27,8 +29,8 @@ export default class GtfsPlusEditor extends Component { save () { const zip = new JSZip() - for(const table of getConfigProperty('modules.gtfsplus.spec')) { - if(!(table.id in this.props.tableData) || this.props.tableData[table.id].length === 0) continue + for (const table of getConfigProperty('modules.gtfsplus.spec')) { + if (!(table.id in this.props.tableData) || this.props.tableData[table.id].length === 0) continue let fileContent = '' // white the header line @@ -36,7 +38,7 @@ export default class GtfsPlusEditor extends Component { fileContent += fieldNameArr.join(',') + '\n' // write the data rows - var dataRows = this.props.tableData[table.id].map(rowData => { + this.props.tableData[table.id].map(rowData => { const rowText = fieldNameArr.map(fieldName => { return rowData[fieldName] || '' }).join(',') @@ -47,14 +49,27 @@ export default class GtfsPlusEditor extends Component { zip.file(table.name, fileContent) } - zip.generateAsync({type:"blob"}).then((content) => { + zip.generateAsync({type: 'blob'}).then((content) => { this.props.feedSaved(content) }) } render () { - if(!this.props.feedSource) return null - const editingIsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.feedSource.projectId, this.props.feedSource.id, 'edit-gtfs') + const { + feedSource, + user, + project, + validation, + tableData, + newRowClicked, + deleteRowClicked, + fieldEdited, + gtfsEntitySelected, + gtfsEntityLookup, + newRowsDisplayed + } = this.props + if (!feedSource) return null + const editingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'edit-gtfs') const buttonStyle = { display: 'block', width: '100%' @@ -71,22 +86,22 @@ export default class GtfsPlusEditor extends Component {
  • Explore
  • Projects
  • -
  • {this.props.project.name}
  • -
  • {this.props.feedSource.name}
  • +
  • {project.name}
  • +
  • {feedSource.name}
  • Edit GTFS+
- Editing GTFS+ for {this.props.feedSource.name} + Editing GTFS+ for {feedSource.name} @@ -106,8 +121,8 @@ export default class GtfsPlusEditor extends Component { this.setState({ activeTableId: table.id }) }} > - {this.props.validation && (table.id in this.props.validation) - ? + {validation && (table.id in validation) + ? : null } {table.id} @@ -117,19 +132,19 @@ export default class GtfsPlusEditor extends Component { { - this.props.gtfsEntitySelected(type, entity) + gtfsEntitySelected(type, entity) }} getGtfsEntity={(type, id) => { - return this.props.gtfsEntityLookup[`${type}_${id}`] + return gtfsEntityLookup[`${type}_${id}`] }} showHelpClicked={(tableId, fieldName) => { const helpContent = fieldName @@ -144,7 +159,7 @@ export default class GtfsPlusEditor extends Component { }) }} newRowsDisplayed={(rows) => { - this.props.newRowsDisplayed(activeTable.id, rows, this.props.feedSource) + newRowsDisplayed(activeTable.id, rows, feedSource) }} /> diff --git a/src/main/client/gtfsplus/components/GtfsPlusTable.js b/lib/gtfsplus/components/GtfsPlusTable.js similarity index 72% rename from src/main/client/gtfsplus/components/GtfsPlusTable.js rename to lib/gtfsplus/components/GtfsPlusTable.js index 498429701..17f7dc00e 100644 --- a/src/main/client/gtfsplus/components/GtfsPlusTable.js +++ b/lib/gtfsplus/components/GtfsPlusTable.js @@ -8,7 +8,9 @@ import EditableTextField from '../../common/components/EditableTextField' const recordsPerPage = 25 export default class GtfsPlusTable extends Component { - + static propTypes = { + table: PropTypes.object + } constructor (props) { super(props) @@ -17,9 +19,8 @@ export default class GtfsPlusTable extends Component { visibility: 'all' } } - componentWillReceiveProps (nextProps) { - if(this.props.table !== nextProps.table) { + if (this.props.table !== nextProps.table) { this.setState({ currentPage: 1 }) } } @@ -33,9 +34,9 @@ export default class GtfsPlusTable extends Component { } getActiveRowData (currentPage) { - if(!this.props.tableData) return [] + if (!this.props.tableData) return [] const tableValidation = this.props.validation || [] - if(this.state.visibility === 'validation' && tableValidation.length < 5000) { + if (this.state.visibility === 'validation' && tableValidation.length < 5000) { return this.props.tableData .filter(record => (tableValidation.find(v => v.rowIndex === record.origRowIndex))) .slice((currentPage - 1) * recordsPerPage, @@ -50,7 +51,7 @@ export default class GtfsPlusTable extends Component { const rowData = this.getActiveRowData(this.state.currentPage) const getInput = (row, field, currentValue) => { - switch(field.inputType) { + switch (field.inputType) { case 'TEXT': case 'GTFS_TRIP': case 'GTFS_FARE': @@ -83,11 +84,12 @@ export default class GtfsPlusTable extends Component { const routeEntity = this.props.getGtfsEntity('route', currentValue) const routeValue = routeEntity - ? { 'value': routeEntity.route_id, - 'label': routeEntity.route_short_name - ? `${routeEntity.route_short_name} - ${routeEntity.route_long_name}` - : routeEntity.route_long_name - } + ? { + 'value': routeEntity.route_id, + 'label': routeEntity.route_short_name + ? `${routeEntity.route_short_name} - ${routeEntity.route_long_name}` + : routeEntity.route_long_name + } : '' return ( @@ -106,7 +108,9 @@ export default class GtfsPlusTable extends Component { ) case 'GTFS_STOP': const stopEntity = this.props.getGtfsEntity('stop', currentValue) - const stopValue = stopEntity ? {'value': stopEntity.stop_id, 'label': stopEntity.stop_name } : '' + const stopValue = stopEntity + ? {'value': stopEntity.stop_id, 'label': stopEntity.stop_name} + : '' return ( {(pageCount > 1) ? - - - - Page {this.state.currentPage} of {pageCount} - + - + + Page {this.state.currentPage} of {pageCount} + - - Go to { - if (e.keyCode == 13) { - const newPage = parseInt(e.target.value) - if(newPage > 0 && newPage <= pageCount) { - e.target.value = '' - this.setState({ currentPage: newPage }) - } + + + + Go to { + if (e.keyCode === 13) { + const newPage = parseInt(e.target.value) + if (newPage > 0 && newPage <= pageCount) { + e.target.value = '' + this.setState({ currentPage: newPage }) } - }} - onFocus={(e) => e.target.select()} - /> - + } + }} + onFocus={(e) => e.target.select()} + /> + : null } Show  - { - console.log('evt', evt.target.value); + console.log('evt', evt.target.value) this.setState({ visibility: evt.target.value, currentPage: 1 @@ -245,14 +249,15 @@ export default class GtfsPlusTable extends Component { /> ) })} - + {rowData && rowData.length > 0 ? rowData.map((data, rowIndex) => { - const tableRowIndex = (this.state.currentPage - 1) * recordsPerPage + rowIndex - return ( + const tableRowIndex = (this.state.currentPage - 1) * recordsPerPage + rowIndex + return ( + {table.fields.map(field => { const validationIssue = this.props.validation ? this.props.validation.find(v => @@ -263,22 +268,24 @@ export default class GtfsPlusTable extends Component { {validationIssue.description} ) : null - return ( - {validationIssue - ?
+ return ( + + {validationIssue + ?
- : null - } -
- {getInput(tableRowIndex, field, data[field.name])} -
- ) + : null + } +
+ {getInput(tableRowIndex, field, data[field.name])} +
+ + ) })} - - ) - }) + + ) + }) : null } @@ -296,8 +304,8 @@ export default class GtfsPlusTable extends Component { {!rowData || rowData.length === 0 ? - No entries exist for this table. - + No entries exist for this table. + : null } diff --git a/src/main/client/gtfsplus/components/GtfsPlusVersionSummary.js b/lib/gtfsplus/components/GtfsPlusVersionSummary.js similarity index 62% rename from src/main/client/gtfsplus/components/GtfsPlusVersionSummary.js rename to lib/gtfsplus/components/GtfsPlusVersionSummary.js index 76c51d626..b4b2c7538 100644 --- a/src/main/client/gtfsplus/components/GtfsPlusVersionSummary.js +++ b/lib/gtfsplus/components/GtfsPlusVersionSummary.js @@ -1,12 +1,14 @@ import React, {Component, PropTypes} from 'react' -import { Panel, Row, Col, Table, Input, Button, Glyphicon, Well, Alert } from 'react-bootstrap' -import { Link, browserHistory } from 'react-router' +import { Panel, Row, Col, Table, Button, Glyphicon, Alert } from 'react-bootstrap' +import { browserHistory } from 'react-router' import moment from 'moment' import { getConfigProperty } from '../../common/util/config' export default class GtfsPlusVersionSummary extends Component { - + static propTypes = { + gtfsplus: PropTypes.object + } constructor (props) { super(props) this.state = { expanded: false } @@ -15,25 +17,25 @@ export default class GtfsPlusVersionSummary extends Component { this.props.gtfsPlusDataRequested(this.props.version) } isTableIncluded (tableId) { - if(!this.props.gtfsplus.tableData) return null + if (!this.props.gtfsplus.tableData) return null return tableId in this.props.gtfsplus.tableData ? 'Yes' : 'No' } tableRecordCount (tableId) { - if(!this.props.gtfsplus.tableData) return null - if(!(tableId in this.props.gtfsplus.tableData)) return 'N/A' + if (!this.props.gtfsplus.tableData) return null + if (!(tableId in this.props.gtfsplus.tableData)) return 'N/A' return this.props.gtfsplus.tableData[tableId].length.toLocaleString() } validationIssueCount (tableId) { - if(!this.props.gtfsplus.validation) return null - if(!(tableId in this.props.gtfsplus.validation)) return 'None' + if (!this.props.gtfsplus.validation) return null + if (!(tableId in this.props.gtfsplus.validation)) return 'None' return this.props.gtfsplus.validation[tableId].length.toLocaleString() } feedStatus () { - if(!this.props.gtfsplus.timestamp) return null - if(!this.gtfsPlusEdited()) return GTFS+ data for this feed version has not been edited. + if (!this.props.gtfsplus.timestamp) return null + if (!this.gtfsPlusEdited()) return GTFS+ data for this feed version has not been edited. return GTFS+ Data updated {moment(this.props.gtfsplus.timestamp).format('MMM. DD, YYYY, h:MMa')} } @@ -42,11 +44,18 @@ export default class GtfsPlusVersionSummary extends Component { } render () { - const editingIsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.version.feedSource.projectId, this.props.version.feedSource.id, 'edit-gtfs') - const publishingIsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.version.feedSource.projectId, this.props.version.feedSource.id, 'approve-gtfs') + const { + user, + version, + gtfsPlusDataRequested, + publishClicked + } = this.props + const { feedSource } = version + const editingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'edit-gtfs') + const publishingIsDisabled = !user.permissions.hasFeedPermission(feedSource.organizationId, feedSource.projectId, feedSource.id, 'approve-gtfs') const header = (

{ - if(!this.state.expanded) this.props.gtfsPlusDataRequested(this.props.version) + if (!this.state.expanded) gtfsPlusDataRequested(version) this.setState({ expanded: !this.state.expanded }) }}> GTFS+ for this Version @@ -65,22 +74,22 @@ export default class GtfsPlusVersionSummary extends Component { {this.gtfsPlusEdited() ? + disabled={publishingIsDisabled} + bsStyle='primary' + style={{ marginLeft: '6px' }} + onClick={() => { + publishClicked(version) + this.setState({ expanded: false }) + }} + > + Publish as New Version + : null } @@ -95,7 +104,7 @@ export default class GtfsPlusVersionSummary extends Component { Included? Records Validation Issues - + @@ -105,7 +114,7 @@ export default class GtfsPlusVersionSummary extends Component { {this.isTableIncluded(table.id)} {this.tableRecordCount(table.id)} {this.validationIssueCount(table.id)} - + ) })} diff --git a/src/main/client/gtfsplus/containers/ActiveGtfsPlusEditor.js b/lib/gtfsplus/containers/ActiveGtfsPlusEditor.js similarity index 74% rename from src/main/client/gtfsplus/containers/ActiveGtfsPlusEditor.js rename to lib/gtfsplus/containers/ActiveGtfsPlusEditor.js index 27dcbf040..7321785cf 100644 --- a/src/main/client/gtfsplus/containers/ActiveGtfsPlusEditor.js +++ b/lib/gtfsplus/containers/ActiveGtfsPlusEditor.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' -import GtfsPlusEditor from '../components/GtfsPlusEditor' +import GtfsPlusEditor from '../components/GtfsPlusEditor' import { fetchFeedSourceAndProject } from '../../manager/actions/feeds' import { addGtfsPlusRow, @@ -8,21 +8,19 @@ import { deleteGtfsPlusRow, uploadGtfsPlusFeed, downloadGtfsPlusFeed, - importGtfsPlusFromGtfs, loadGtfsEntities, receiveGtfsEntities } from '../actions/gtfsplus' const mapStateToProps = (state, ownProps) => { - - let feedSourceId = ownProps.routeParams.feedSourceId - let user = state.user + const feedSourceId = ownProps.routeParams.feedSourceId + const user = state.user // find the containing project - let project = state.projects.all + const project = state.projects.all ? state.projects.all.find(p => { - if (!p.feedSources) return false - return (p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1) - }) + if (!p.feedSources) return false + return (p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1) + }) : null let feedSource @@ -46,8 +44,8 @@ const mapDispatchToProps = (dispatch, ownProps) => { return { onComponentMount: (initialProps) => { - if(!initialProps.feedSource) dispatch(fetchFeedSourceAndProject(feedSourceId)) - if(!initialProps.tableData) dispatch(downloadGtfsPlusFeed(feedVersionId)) + if (!initialProps.feedSource) dispatch(fetchFeedSourceAndProject(feedSourceId)) + if (!initialProps.tableData) dispatch(downloadGtfsPlusFeed(feedVersionId)) }, newRowClicked: (tableId) => { dispatch(addGtfsPlusRow(tableId)) @@ -61,12 +59,12 @@ const mapDispatchToProps = (dispatch, ownProps) => { feedSaved: (file) => { dispatch(uploadGtfsPlusFeed(feedVersionId, file)) .then(() => { - console.log('re-downloading'); + console.log('re-downloading') dispatch(downloadGtfsPlusFeed(feedVersionId)) }) }, newRowsDisplayed: (tableId, rows, feedSource) => { - if(feedSource) dispatch(loadGtfsEntities(tableId, rows, feedSource)) + if (feedSource) dispatch(loadGtfsEntities(tableId, rows, feedSource)) }, gtfsEntitySelected: (type, entity) => { dispatch(receiveGtfsEntities([entity])) diff --git a/src/main/client/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js b/lib/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js similarity index 89% rename from src/main/client/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js rename to lib/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js index 4e6e956dc..24a1eefbe 100644 --- a/src/main/client/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js +++ b/lib/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' -import GtfsPlusVersionSummary from '../components/GtfsPlusVersionSummary' +import GtfsPlusVersionSummary from '../components/GtfsPlusVersionSummary' import { downloadGtfsPlusFeed, diff --git a/src/main/client/gtfsplus/reducers/gtfsplus.js b/lib/gtfsplus/reducers/gtfsplus.js similarity index 63% rename from src/main/client/gtfsplus/reducers/gtfsplus.js rename to lib/gtfsplus/reducers/gtfsplus.js index 0311a0b26..9edaae717 100644 --- a/src/main/client/gtfsplus/reducers/gtfsplus.js +++ b/lib/gtfsplus/reducers/gtfsplus.js @@ -1,6 +1,6 @@ import update from 'react-addons-update' -/*const emptyTableData = { +/* const emptyTableData = { 'realtime_routes': [], 'realtime_stops': [], 'directions': [], @@ -11,9 +11,7 @@ import update from 'react-addons-update' 'fare_rider_categories': [], 'calendar_attributes': [], 'farezone_attributes': [] -}*/ - -const emptyTableData = { } +} */ const gtfsplus = (state = { feedVersionId: null, @@ -33,10 +31,10 @@ const gtfsplus = (state = { } case 'RECEIVE_GTFSPLUS_CONTENT': - let newTableData = {} - for(let i = 0; i < action.filenames.length; i++) { + const newTableData = {} + for (let i = 0; i < action.filenames.length; i++) { const lines = action.fileContent[i].split(/\r\n|\r|\n/g) - if(lines.length < 2) continue + if (lines.length < 2) continue console.log(lines[0]) const fields = lines[0].split(',') console.log(fields) @@ -46,9 +44,9 @@ const gtfsplus = (state = { .filter(line => line.split(',').length === fields.length) .map((line, rowIndex) => { const values = line.split(',') - let rowData = { origRowIndex: rowIndex } - for(let f = 0; f < fields.length; f++) { - rowData[fields[f]] = values[f] + const rowData = { origRowIndex: rowIndex } + for (let f = 0; f < fields.length; f++) { + rowData[fields[f]] = values[f] } return rowData }) @@ -58,84 +56,78 @@ const gtfsplus = (state = { timestamp: {$set: action.timestamp}, tableData: {$set: newTableData} }) - case 'ADD_GTFSPLUS_ROW': // create this table if it doesn't already exist - if(!(action.tableId in state.tableData)) { + if (!(action.tableId in state.tableData)) { return update(state, {tableData: - {$merge: {[action.tableId]: [action.rowData]} } + {$merge: {[action.tableId]: [action.rowData]}} } ) } // otherwise, add it to the exising table - return update(state, - {tableData: - {[action.tableId]: - {$push: [action.rowData]} + return update(state, { + tableData: { + [action.tableId]: { + $push: [action.rowData] } } - ) - + }) case 'UPDATE_GTFSPLUS_FIELD': - return update(state, - {tableData: - {[action.tableId]: - {[action.rowIndex]: - {[action.fieldName]: - {$set: action.newValue} + return update(state, { + tableData: { + [action.tableId]: { + [action.rowIndex]: { + [action.fieldName]: { + $set: action.newValue } } } } - ) - + }) case 'DELETE_GTFSPLUS_ROW': const table = state.tableData[action.tableId] const newTable = [ ...table.slice(0, action.rowIndex), ...table.slice(action.rowIndex + 1) ] - return update(state, - {tableData: - {[action.tableId]: - {$set: newTable} + return update(state, { + tableData: { + [action.tableId]: { + $set: newTable } } - ) - + }) case 'RECEIVE_GTFS_PLUS_ENTITIES': const getType = function (entity) { - if(entity.hasOwnProperty('route_id')) return 'route' - if(entity.hasOwnProperty('stop_id')) return 'stop' + if (entity.hasOwnProperty('route_id')) return 'route' + if (entity.hasOwnProperty('stop_id')) return 'stop' } - const newLookupEntries = {} - for(const entity of action.gtfsEntities) { + for (const entity of action.gtfsEntities) { const type = getType(entity) - const key = type + '_' + entity[type+'_id'] + const key = type + '_' + entity[type + '_id'] newLookupEntries[key] = entity } - return update(state, - {gtfsEntityLookup: - {$merge: newLookupEntries} + return update(state, { + gtfsEntityLookup: { + $merge: newLookupEntries } - ) - + }) case 'RECEIVE_GTFSPLUS_VALIDATION': const validationTable = {} - for(const issue of action.validationIssues) { - if(!(issue.tableId in validationTable)) { + for (const issue of action.validationIssues) { + if (!(issue.tableId in validationTable)) { validationTable[issue.tableId] = [] } validationTable[issue.tableId].push(issue) } - return update(state, - {validation: - {$set: validationTable} + return update(state, { + validation: { + $set: validationTable } - ) + }) default: return state diff --git a/lib/gtfsplus/reducers/index.js b/lib/gtfsplus/reducers/index.js new file mode 100644 index 000000000..f24d524e9 --- /dev/null +++ b/lib/gtfsplus/reducers/index.js @@ -0,0 +1,3 @@ +module.exports = { + gtfsplus: require('./gtfsplus') +} diff --git a/lib/index.css b/lib/index.css new file mode 100644 index 000000000..fb5fe182d --- /dev/null +++ b/lib/index.css @@ -0,0 +1,18 @@ + +@import url(node_modules/font-awesome/css/font-awesome.css); +@import url(node_modules/leaflet/dist/leaflet.css); +@import url(node_modules/leaflet-draw/dist/leaflet.draw.css); + +@import url(node_modules/bootstrap/dist/css/bootstrap.min.css); +@import url(node_modules/react-bootstrap-table/dist/react-bootstrap-table.min.css); +@import url(node_modules/react-bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css); + +@import url(node_modules/react-select/dist/react-select.css); + +@import url(node_modules/react-virtualized/styles.css); +@import url(node_modules/react-virtualized-select/styles.css); +@import url(node_modules/rc-slider/assets/index.css); +@import url(node_modules/react-toggle/style.css); + +@import url(lib/common/style.css); +@import url(lib/alerts/style.css); diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 000000000..db2e91dc3 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,70 @@ +import 'babel-polyfill' +import React from 'react' +import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' +import { createStore, applyMiddleware, combineReducers } from 'redux' +import thunkMiddleware from 'redux-thunk' +import { browserHistory } from 'react-router' +import { syncHistoryWithStore, routerReducer } from 'react-router-redux' +import createLogger from 'redux-logger' + +import App from './common/containers/App' + +const config = JSON.parse(process.env.SETTINGS) +if (config.modules.gtfsplus && config.modules.gtfsplus.enabled) { + config.modules.gtfsplus.spec = require('../gtfsplus.yml') +} +config.modules.editor.spec = require('../gtfs.yml') + +const lang = [ + require('../i18n/english.yml'), + require('../i18n/espanol.yml'), + require('../i18n/francais.yml') +] + +// is an array containing all the matching modules +config.messages = {} +config.messages.all = lang +const languageId = window.localStorage.getItem('lang') +? window.localStorage.getItem('lang') +: navigator.language || navigator.userLanguage + +config.messages.active = lang.find(l => l.id === languageId) || lang.find(l => l.id === 'en-US') + +// console.log('config', config) +window.DT_CONFIG = config + +import * as managerReducers from './manager/reducers' +import admin from './admin/reducers' +import alerts from './alerts/reducers' +import signs from './signs/reducers' + +import * as gtfsPlusReducers from './gtfsplus/reducers' +import editor from './editor/reducers' +import gtfs from './gtfs/reducers' + +const logger = createLogger({duration: true, collapsed: true}) +const store = createStore( +combineReducers({ + ...managerReducers, + admin, + alerts, + signs, + ...gtfsPlusReducers, + editor, + // ...reportReducers, + routing: routerReducer, + gtfs +}), +applyMiddleware(thunkMiddleware, logger) +) + +// console.log('initial store', store.getState()) + +const appHistory = syncHistoryWithStore(browserHistory, store) + +ReactDOM.render( + + + , +document.getElementById('main')) diff --git a/src/main/client/manager/actions/deployments.js b/lib/manager/actions/deployments.js similarity index 91% rename from src/main/client/manager/actions/deployments.js rename to lib/manager/actions/deployments.js index 3b6f3ce05..a77349496 100644 --- a/src/main/client/manager/actions/deployments.js +++ b/lib/manager/actions/deployments.js @@ -1,5 +1,7 @@ +import { browserHistory } from 'react-router' + import { secureFetch } from '../../common/util/util' -import { fetchProject, receiveProject } from './projects' +import { receiveProject } from './projects' import { startJobMonitor } from './status' // Deployment Actions @@ -50,12 +52,11 @@ export function deployToTarget (deployment, target) { return function (dispatch, getState) { dispatch(deployingToTarget(deployment, target)) const url = `/api/manager/secure/deployments/${deployment.id}/deploy/${target}` - console.log('*** deploying...'); return secureFetch(url, getState(), 'post') .then(response => { - console.log(response); + console.log(response) if (response.status >= 300) { - alert('Deployment error: ' + response.statusText) + window.alert('Deployment error: ' + response.statusText) } else { dispatch(deployedToTarget(deployment, target)) dispatch(startJobMonitor()) @@ -64,7 +65,6 @@ export function deployToTarget (deployment, target) { } } - export function requestingDeployment () { return { type: 'REQUESTING_DEPLOYMENT' @@ -151,22 +151,10 @@ export function fetchDeploymentAndProject (id) { } } -export function fetchDeploymentTargets () { - return function (dispatch, getState) { - dispatch(requestingDeploymentTargets()) - const url = '/api/manager/secure/deployments/targets' - return secureFetch(url, getState()) - .then(response => response.json()) - .then(targets => { - return dispatch(receiveDeploymentTargets(targets)) - }) - } -} - export function createDeployment (projectId) { return { type: 'CREATE_DEPLOYMENT', - projectId, + projectId } } @@ -224,6 +212,7 @@ export function createDeploymentFromFeedSource (feedSource) { .then(response => response.json()) .then(deployment => { console.log('created deployment', deployment) + browserHistory.push(`/project/${feedSource.projectId}/deployments/${deployment.id}`) return deployment }) } diff --git a/lib/manager/actions/feeds.js b/lib/manager/actions/feeds.js new file mode 100644 index 000000000..d5b34d561 --- /dev/null +++ b/lib/manager/actions/feeds.js @@ -0,0 +1,243 @@ +import { secureFetch } from '../../common/util/util' +import { fetchProject, fetchProjectWithFeeds } from './projects' +import { setErrorMessage, startJobMonitor } from './status' +import { fetchFeedVersions, feedNotModified } from './versions' +import { fetchSnapshots } from '../../editor/actions/snapshots' + +export function requestingFeedSources () { + return { + type: 'REQUESTING_FEEDSOURCES' + } +} + +export function receiveFeedSources (projectId, feedSources) { + return { + type: 'RECEIVE_FEEDSOURCES', + projectId, + feedSources + } +} + +export function fetchProjectFeeds (projectId) { + return function (dispatch, getState) { + dispatch(requestingFeedSources()) + const url = '/api/manager/secure/feedsource?projectId=' + projectId + return secureFetch(url, getState()) + .then(response => response.json()) + .then(feedSources => { + dispatch(receiveFeedSources(projectId, feedSources)) + return feedSources + }) + } +} + +export function fetchUserFeeds (userId) { + return function (dispatch, getState) { + dispatch(requestingFeedSources()) + const url = '/api/manager/secure/feedsource?userId=' + userId + return secureFetch(url, getState()) + .then(response => response.json()) + .then(feedSources => { + dispatch(receiveFeedSources(feedSources)) + }) + } +} + +function receivePublicFeeds (feeds) { + return { + type: 'RECEIVE_PUBLIC_FEEDS', + feeds + } +} + +export function createFeedSource (projectId) { + return { + type: 'CREATE_FEEDSOURCE', + projectId + } +} + +export function savingFeedSource () { + return { + type: 'SAVING_FEEDSOURCE' + } +} + +export function saveFeedSource (props) { + return function (dispatch, getState) { + dispatch(savingFeedSource()) + const url = '/api/manager/secure/feedsource' + return secureFetch(url, getState(), 'post', props) + .then((res) => { + return dispatch(fetchProjectWithFeeds(props.projectId)) + }) + } +} + +export function updateFeedSource (feedSource, changes) { + return function (dispatch, getState) { + dispatch(savingFeedSource()) + const url = '/api/manager/secure/feedsource/' + feedSource.id + return secureFetch(url, getState(), 'put', changes) + .then((res) => { + if (res.status >= 400) { + console.log(res.json()) + dispatch(setErrorMessage('Error updating feed source.')) + } + // return dispatch(fetchProjectFeeds(feedSource.projectId)) + return dispatch(fetchFeedSource(feedSource.id, true)) + }) + } +} + +export function updateExternalFeedResource (feedSource, resourceType, properties) { + return function (dispatch, getState) { + console.log('updateExternalFeedResource', feedSource, resourceType, properties) + dispatch(savingFeedSource()) + const url = `/api/manager/secure/feedsource/${feedSource.id}/updateExternal?resourceType=${resourceType}` + return secureFetch(url, getState(), 'put', properties) + .then((res) => { + return dispatch(fetchFeedSource(feedSource.id, true)) + }) + } +} + +export function deletingFeedSource (feedSource) { + return { + type: 'DELETING_FEEDSOURCE', + feedSource + } +} + +export function deleteFeedSource (feedSource, changes) { + return function (dispatch, getState) { + dispatch(deletingFeedSource(feedSource)) + const url = '/api/manager/secure/feedsource/' + feedSource.id + return secureFetch(url, getState(), 'delete') + .then((res) => { + // if (res.status >= 400) { + // return dispatch(setErrorMessage('Error deleting feed source')) + // } + return dispatch(fetchProjectFeeds(feedSource.projectId)) + }) + } +} + +export function requestingFeedSource () { + return { + type: 'REQUESTING_FEEDSOURCE' + } +} + +export function receiveFeedSource (feedSource) { + return { + type: 'RECEIVE_FEEDSOURCE', + feedSource + } +} + +export function fetchFeedSource (feedSourceId, withVersions = false, withSnapshots = false) { + return function (dispatch, getState) { + console.log('fetchFeedSource', feedSourceId) + dispatch(requestingFeedSource()) + const url = '/api/manager/secure/feedsource/' + feedSourceId + return secureFetch(url, getState()) + .then(res => { + if (res.status >= 400) { + // dispatch(setErrorMessage('Error getting feed source')) + console.log('error getting feed source') + return null + } + return res.json() + }) + .then(feedSource => { + if (!feedSource) { + dispatch(receiveFeedSource(feedSource)) + return feedSource + } + console.log('got feedSource', feedSource) + dispatch(receiveFeedSource(feedSource)) + if (withVersions) dispatch(fetchFeedVersions(feedSource)) + if (withSnapshots) dispatch(fetchSnapshots(feedSource)) + return feedSource + }) + } +} + +export function fetchFeedSourceAndProject (feedSourceId, unsecured) { + return function (dispatch, getState) { + dispatch(requestingFeedSource()) + const apiRoot = unsecured ? 'public' : 'secure' + const url = `/api/manager/${apiRoot}/feedsource/${feedSourceId}` + return secureFetch(url, getState()) + .then(res => { + if (res.status >= 400) { + // dispatch(setErrorMessage('Error getting feed source')) + console.log('error getting feed source') + return null + } + return res.json() + }) + .then(feedSource => { + if (!feedSource) { + dispatch(receiveFeedSource(feedSource)) + return feedSource + } + return dispatch(fetchProject(feedSource.projectId, unsecured)) + .then(proj => { + dispatch(receiveFeedSource(feedSource)) + return feedSource + }) + }) + } +} + +export function fetchPublicFeedSource (feedSourceId) { + return function (dispatch, getState) { + dispatch(requestingFeedSource()) + const url = '/api/manager/public/feedsource/' + feedSourceId + return secureFetch(url, getState()) + .then(response => response.json()) + .then(feedSource => { + dispatch(receivePublicFeeds()) + return feedSource + }) + } +} + +export function runningFetchFeed () { + return { + type: 'RUNNING_FETCH_FEED' + } +} + +export function receivedFetchFeed (feedSource) { + return { + type: 'RECEIVED_FETCH_FEED', + feedSource + } +} + +export function runFetchFeed (feedSource) { + return function (dispatch, getState) { + dispatch(runningFetchFeed()) + const url = `/api/manager/secure/feedsource/${feedSource.id}/fetch` + return secureFetch(url, getState(), 'post') + .then(res => { + if (res.status === 304) { + dispatch(feedNotModified(feedSource, 'Feed fetch cancelled because it matches latest feed version.')) + } else if (res.status >= 400) { + dispatch(setErrorMessage('Error fetching feed source')) + } else { + dispatch(receivedFetchFeed(feedSource)) + dispatch(startJobMonitor()) + return res.json() + } + }) + .then(result => { + console.log('fetchFeed result', result) + // fetch feed source with versions + return dispatch(fetchFeedSource(feedSource.id, true)) + }) + } +} diff --git a/src/main/client/manager/actions/languages.js b/lib/manager/actions/languages.js similarity index 100% rename from src/main/client/manager/actions/languages.js rename to lib/manager/actions/languages.js diff --git a/lib/manager/actions/notes.js b/lib/manager/actions/notes.js new file mode 100644 index 000000000..6252591a3 --- /dev/null +++ b/lib/manager/actions/notes.js @@ -0,0 +1,69 @@ +import { secureFetch } from '../../common/util/util' + +export function requestingNotes () { + return { + type: 'REQUESTING_NOTES' + } +} + +export function receiveNotesForFeedSource (feedSource, notes) { + return { + type: 'RECEIVE_NOTES_FOR_FEEDSOURCE', + feedSource, + notes + } +} + +export function fetchNotesForFeedSource (feedSource) { + return function (dispatch, getState) { + dispatch(requestingNotes()) + const url = `/api/manager/secure/note?type=FEED_SOURCE&objectId=${feedSource.id}` + secureFetch(url, getState()) + .then(response => response.json()) + .then(notes => { + dispatch(receiveNotesForFeedSource(feedSource, notes)) + }) + } +} + +export function postNoteForFeedSource (feedSource, note) { + return function (dispatch, getState) { + const url = `/api/manager/secure/note?type=FEED_SOURCE&objectId=${feedSource.id}` + secureFetch(url, getState(), 'post', note) + .then(response => response.json()) + .then(note => { + dispatch(fetchNotesForFeedSource(feedSource)) + }) + } +} + +export function receiveNotesForFeedVersion (feedVersion, notes) { + return { + type: 'RECEIVE_NOTES_FOR_FEEDVERSION', + feedVersion, + notes + } +} + +export function fetchNotesForFeedVersion (feedVersion) { + return function (dispatch, getState) { + dispatch(requestingNotes()) + const url = `/api/manager/secure/note?type=FEED_VERSION&objectId=${feedVersion.id}` + secureFetch(url, getState()) + .then(response => response.json()) + .then(notes => { + dispatch(receiveNotesForFeedVersion(feedVersion, notes)) + }) + } +} + +export function postNoteForFeedVersion (feedVersion, note) { + return function (dispatch, getState) { + const url = `/api/manager/secure/note?type=FEED_VERSION&objectId=${feedVersion.id}` + secureFetch(url, getState(), 'post', note) + .then(response => response.json()) + .then(note => { + dispatch(fetchNotesForFeedVersion(feedVersion)) + }) + } +} diff --git a/src/main/client/manager/actions/projects.js b/lib/manager/actions/projects.js similarity index 88% rename from src/main/client/manager/actions/projects.js rename to lib/manager/actions/projects.js index dd3511ac0..57dfafb5e 100644 --- a/src/main/client/manager/actions/projects.js +++ b/lib/manager/actions/projects.js @@ -1,12 +1,12 @@ import { secureFetch } from '../../common/util/util' -import { updateGtfsFilter } from '../../gtfs/actions/gtfsFilter' +import { updateGtfsFilter } from '../../gtfs/actions/filter' import { setErrorMessage, startJobMonitor } from './status' -import { fetchProjectFeeds, updateFeedSource } from './feeds' +import { fetchProjectFeeds } from './feeds' // Bulk Project Actions function requestingProjects () { return { - type: 'REQUESTING_PROJECTS', + type: 'REQUESTING_PROJECTS' } } @@ -63,7 +63,7 @@ export function fetchProjectsWithPublicFeeds () { function requestingProject () { return { - type: 'REQUESTING_PROJECT', + type: 'REQUESTING_PROJECT' } } @@ -101,15 +101,16 @@ export function fetchProjectWithFeeds (projectId, unsecure) { // .catch(err => console.log(err)) .then(project => { dispatch(receiveProject(project)) - if (!unsecure) + if (!unsecure) { return dispatch(fetchProjectFeeds(project.id)) + } }) } } function deletingProject () { return { - type: 'DELETING_PROJECT', + type: 'DELETING_PROJECT' } } @@ -149,21 +150,34 @@ export function updateProject (project, changes, fetchFeeds = false) { } } -export function createProject () { +export function deployPublic (project) { + return function (dispatch, getState) { + // dispatch(savingProject()) + const url = `/api/manager/secure/project/${project.id}/deployPublic` + return secureFetch(url, getState(), 'post') + .then((res) => res.json()) + .then(json => { + return json + }) + } +} + +export function createProject (project) { return { - type: 'CREATE_PROJECT' + type: 'CREATE_PROJECT', + project } } export function requestingSync () { return { - type: 'REQUESTING_SYNC', + type: 'REQUESTING_SYNC' } } export function receiveSync () { return { - type: 'RECEIVE_SYNC', + type: 'RECEIVE_SYNC' } } @@ -183,7 +197,7 @@ export function thirdPartySync (projectId, type) { export function runningFetchFeedsForProject () { return { - type: 'RUNNING_FETCH_FEED_FOR_PROJECT', + type: 'RUNNING_FETCH_FEED_FOR_PROJECT' } } @@ -196,7 +210,6 @@ export function receiveFetchFeedsForProject (project) { export function fetchFeedsForProject (project) { return function (dispatch, getState) { - dispatch(runningFetchFeedsForProject()) const url = `/api/manager/secure/project/${project.id}/fetch` return secureFetch(url, getState(), 'post') @@ -204,11 +217,9 @@ export function fetchFeedsForProject (project) { if (res.status === 304) { // dispatch(feedNotModified(feedSource, 'Feed fetch cancelled because it matches latest feed version.')) console.log('fetch cancelled because matches latest') - } - else if (res.status >= 400) { + } else if (res.status >= 400) { dispatch(setErrorMessage('Error fetching project feeds')) - } - else { + } else { dispatch(receiveFetchFeedsForProject(project)) dispatch(startJobMonitor()) return res.json() @@ -222,15 +233,16 @@ export function fetchFeedsForProject (project) { } } -function savingProject () { +function savingProject (props) { return { type: 'SAVING_PROJECT', + props } } export function saveProject (props) { return function (dispatch, getState) { - dispatch(savingProject()) + dispatch(savingProject(props)) const url = '/api/manager/secure/project' return secureFetch(url, getState(), 'post', props) .then((res) => { diff --git a/src/main/client/manager/actions/status.js b/lib/manager/actions/status.js similarity index 81% rename from src/main/client/manager/actions/status.js rename to lib/manager/actions/status.js index 90e22d5e4..1b86a11c9 100644 --- a/src/main/client/manager/actions/status.js +++ b/lib/manager/actions/status.js @@ -1,5 +1,5 @@ import { secureFetch } from '../../common/util/util' -import { fetchFeedVersion } from './feeds' +import { fetchFeedSource } from './feeds' import { fetchSnapshots } from '../../editor/actions/snapshots' export function setErrorMessage (message) { @@ -15,7 +15,7 @@ export function clearStatusModal () { } } -/*function watchingStatus (job) { +/* function watchingStatus (job) { return { type: 'WATCHING_STATUS', job @@ -43,7 +43,7 @@ export function watchStatus (job) { } }, 1000); } -}*/ +} */ export function removeRetiredJob (job) { return { @@ -92,7 +92,7 @@ function stopCurrentTimer (state) { if (timer) clearInterval(timer) } -export function startJobMonitor () { +export function startJobMonitor (showMonitor = true) { return function (dispatch, getState) { stopCurrentTimer(getState()) @@ -102,7 +102,12 @@ export function startJobMonitor () { timerFunction() // make an initial call right now const timer = setInterval(timerFunction, 2000) - + if (showMonitor) { + console.log('showing monitor') + dispatch(setJobMonitorVisible(true)) + } else { + console.log('not showing monitor') + } dispatch(setJobMonitorTimer(timer)) } } @@ -121,15 +126,26 @@ export function setJobMonitorVisible (visible) { } } +export function handlingFinishedJob (job) { + return { + type: 'HANDLING_FINISHED_JOB', + job + } +} + export function handleFinishedJob (job) { return function (dispatch, getState) { + dispatch(handlingFinishedJob(job)) switch (job.type) { case 'VALIDATE_FEED': - dispatch(fetchFeedVersion(job.feedVersionId)) + dispatch(fetchFeedSource(job.feedVersion.feedSource.id, true, true)) break case 'PROCESS_SNAPSHOT': dispatch(fetchSnapshots(job.feedVersion.feedSource)) break + case 'CREATE_FEEDVERSION_FROM_SNAPSHOT': + dispatch(fetchFeedSource(job.feedVersion.feedSource.id, true, true)) + break } } } diff --git a/src/main/client/manager/actions/ui.js b/lib/manager/actions/ui.js similarity index 53% rename from src/main/client/manager/actions/ui.js rename to lib/manager/actions/ui.js index 6820d716c..5af9ae315 100644 --- a/src/main/client/manager/actions/ui.js +++ b/lib/manager/actions/ui.js @@ -13,3 +13,17 @@ export function setSidebarExpanded (value) { dispatch(updateUserMetadata(getState().user.profile, {sidebarExpanded: value})) } } + +export function settingTutorialHidden (value) { + return { + type: 'SETTING_TUTORIAL_HIDDEN', + value + } +} + +export function setTutorialHidden (value) { + return function (dispatch, getState) { + dispatch(settingTutorialHidden(value)) + dispatch(updateUserMetadata(getState().user.profile, {hideTutorial: value})) + } +} diff --git a/src/main/client/manager/actions/user.js b/lib/manager/actions/user.js similarity index 77% rename from src/main/client/manager/actions/user.js rename to lib/manager/actions/user.js index a4d0eb906..2786e0993 100644 --- a/src/main/client/manager/actions/user.js +++ b/lib/manager/actions/user.js @@ -1,22 +1,21 @@ import { secureFetch } from '../../common/util/util' -import { fetchProjects } from './projects' -import update from 'react-addons-update' import { getConfigProperty } from '../../common/util/config' import objectPath from 'object-path' -export const checkingExistingLogin = () => { +export function checkingExistingLogin (loginProps) { return { - type: 'CHECKING_EXISTING_LOGIN' + type: 'CHECKING_EXISTING_LOGIN', + loginProps } } -export const noExistingLogin = () => { +export function noExistingLogin () { return { type: 'NO_EXISTING_LOGIN' } } -export const userLoggedIn = (token, profile) => { +export function userLoggedIn (token, profile) { return { type: 'USER_LOGGED_IN', token, @@ -24,21 +23,19 @@ export const userLoggedIn = (token, profile) => { } } -export function checkExistingLogin() { +export function checkExistingLogin (loginProps) { return function (dispatch, getState) { - dispatch(checkingExistingLogin()) - var login = getState().user.auth0.checkExistingLogin() - if(login) { + dispatch(checkingExistingLogin(loginProps)) + var login = getState().user.auth0.checkExistingLogin(loginProps) + if (login) { return login.then((userTokenAndProfile) => { if (userTokenAndProfile) { dispatch(userLoggedIn(userTokenAndProfile.token, userTokenAndProfile.profile)) - } - else { + } else { console.log('error checking token') } }) - } - else { + } else { console.log('no login found') dispatch(noExistingLogin()) // return empty promise @@ -60,24 +57,33 @@ export function fetchUser (user) { } } +function updatingUserMetadata (profile, props, payload) { + return { + type: 'UPDATING_USER_METADATA', + profile, + props, + payload + } +} + export function updateUserMetadata (profile, props) { return function (dispatch, getState) { const CLIENT_ID = getConfigProperty('auth0.client_id') - let userMetadata = profile.user_metadata || { - lang: 'en', - datatools: [ - { - client_id: CLIENT_ID - } - ] + const userMetadata = profile.user_metadata || { + lang: 'en', + datatools: [ + { + client_id: CLIENT_ID } - let clientIndex = userMetadata.datatools.findIndex(d => d.client_id === CLIENT_ID) + ] + } + const clientIndex = userMetadata.datatools.findIndex(d => d.client_id === CLIENT_ID) for (var key in props) { objectPath.set(userMetadata, `datatools.${clientIndex}.${key}`, props[key]) } const payload = {user_metadata: userMetadata} - console.log('updating user metadata props', props, payload) + dispatch(updatingUserMetadata(profile, props, payload)) const url = `https://${getConfigProperty('auth0.domain')}/api/v2/users/${profile.user_id}` return secureFetch(url, getState(), 'PATCH', payload) .then(response => response.json()) @@ -93,8 +99,8 @@ export function updateUserMetadata (profile, props) { export function removeUserSubscription (profile, subscriptionType) { return function (dispatch, getState) { - let subscriptions = profile.app_metadata.datatools.find(dt => dt.client_id === getConfigProperty('auth0.client_id')).subscriptions || [{type: subscriptionType, target: []}] - let index = subscriptions.findIndex(sub => sub.type === subscriptionType) + const subscriptions = profile.app_metadata.datatools.find(dt => dt.client_id === getConfigProperty('auth0.client_id')).subscriptions || [{type: subscriptionType, target: []}] + const index = subscriptions.findIndex(sub => sub.type === subscriptionType) if (index > -1) { subscriptions.splice(index, 1) } else { @@ -106,18 +112,17 @@ export function removeUserSubscription (profile, subscriptionType) { export function updateTargetForSubscription (profile, target, subscriptionType) { return function (dispatch, getState) { - let subscriptions = profile.app_metadata.datatools.find(dt => dt.client_id === getConfigProperty('auth0.client_id')).subscriptions || [{type: subscriptionType, target: []}] + const subscriptions = profile.app_metadata.datatools.find(dt => dt.client_id === getConfigProperty('auth0.client_id')).subscriptions || [{type: subscriptionType, target: []}] if (subscriptions.findIndex(sub => sub.type === subscriptionType) === -1) { subscriptions.push({type: subscriptionType, target: []}) } for (var i = 0; i < subscriptions.length; i++) { - let sub = subscriptions[i] + const sub = subscriptions[i] if (sub.type === subscriptionType) { - let index = sub.target.indexOf(target) + const index = sub.target.indexOf(target) if (index > -1) { sub.target.splice(index, 1) - } - else { + } else { sub.target.push(target) } } @@ -205,16 +210,19 @@ export function createPublicUser (credentials) { export function login (credentials, user, lockOptions) { return function (dispatch, getState) { - if (!credentials){ + if (!credentials) { return getState().user.auth0.loginViaLock(lockOptions) - .then((userInfo) => { - return dispatch(userLoggedIn(userInfo.token, userInfo.profile)) - }) - // .then(() => { - // dispatch(fetchProjects()) - // }) - } - else { + .then(userInfo => { + if (userInfo) { + dispatch(userLoggedIn(userInfo.token, userInfo.profile)) + return userInfo + } + }) + .catch(e => { + console.log(e) + throw e + }) + } else { credentials.client_id = getConfigProperty('auth0.client_id') credentials.connection = 'Username-Password-Authentication' credentials.username = credentials.email @@ -225,9 +233,9 @@ export function login (credentials, user, lockOptions) { .then(response => response.json()) .then(token => { // save token to localStorage - localStorage.setItem('userToken', token.id_token) + window.localStorage.setItem('userToken', token.id_token) - return getState().user.auth0.loginFromToken (token.id_token) + return getState().user.auth0.loginFromToken(token.id_token) }).then((userInfo) => { console.log('got user info', userInfo) return dispatch(userLoggedIn(userInfo.token, userInfo.profile)) @@ -249,7 +257,7 @@ export function logout () { } } -export function resetPassword() { +export function resetPassword () { return function (dispatch, getState) { getState().user.auth0.resetPassword() } @@ -260,12 +268,10 @@ export function resetPassword() { export function getRecentActivity (user) { return function (dispatch, getState) { const userId = user.profile.user_id - console.log('getRecentActivity for ' + userId) const url = `/api/manager/secure/user/${userId}/recentactivity` return secureFetch(url, getState()) .then(response => response.json()) .then(activity => { - console.log('got recent activity', activity) return dispatch(receiveRecentActivity(activity)) }) } diff --git a/lib/manager/actions/versions.js b/lib/manager/actions/versions.js new file mode 100644 index 000000000..cf6a3b73c --- /dev/null +++ b/lib/manager/actions/versions.js @@ -0,0 +1,306 @@ +import qs from 'qs' +import fetch from 'isomorphic-fetch' + +import { secureFetch } from '../../common/util/util' +import { setErrorMessage, startJobMonitor } from './status' +import { fetchFeedSource } from './feeds' + +export function requestingFeedVersions () { + return { + type: 'REQUESTING_FEEDVERSIONS' + } +} + +export function receiveFeedVersions (feedSource, feedVersions) { + return { + type: 'RECEIVE_FEEDVERSIONS', + feedSource, + feedVersions + } +} + +export function setActiveVersion (feedVersion) { + return { + type: 'SET_ACTIVE_FEEDVERSION', + feedVersion + } +} + +export function fetchFeedVersions (feedSource, unsecured) { + return function (dispatch, getState) { + dispatch(requestingFeedVersions()) + const apiRoot = unsecured ? 'public' : 'secure' + const url = `/api/manager/${apiRoot}/feedversion?feedSourceId=${feedSource.id}` + return secureFetch(url, getState()) + .then(response => response.json()) + .then(versions => { + dispatch(receiveFeedVersions(feedSource, versions)) + return versions + }) + } +} + +export function requestingFeedVersion () { + return { + type: 'REQUESTING_FEEDVERSION' + } +} + +export function receiveFeedVersion (feedVersion) { + return { + type: 'RECEIVE_FEEDVERSION', + feedVersion + } +} + +export function fetchFeedVersion (feedVersionId) { + return function (dispatch, getState) { + dispatch(requestingFeedVersion()) + const url = `/api/manager/secure/feedversion/${feedVersionId}` + return secureFetch(url, getState()) + .then(response => response.json()) + .then(version => { + return dispatch(receiveFeedVersion(version)) + }) + } +} + +export function publishingFeedVersion (feedVersion) { + return { + type: 'PUBLISHING_FEEDVERSION', + feedVersion + } +} + +export function publishedFeedVersion (feedVersion) { + return { + type: 'PUBLISHED_FEEDVERSION', + feedVersion + } +} + +export function publishFeedVersion (feedVersion) { + return function (dispatch, getState) { + dispatch(publishingFeedVersion(feedVersion)) + const url = `/api/manager/secure/feedversion/${feedVersion.id}/publish` + return secureFetch(url, getState(), 'post') + .then(response => response.json()) + .then(version => { + return dispatch(publishedFeedVersion(version)) + }) + } +} + +export function fetchPublicFeedVersions (feedSource) { + return function (dispatch, getState) { + dispatch(requestingFeedVersions()) + const url = `/api/manager/public/feedversion?feedSourceId=${feedSource.id}&public=true` + return secureFetch(url, getState()) + .then(response => response.json()) + .then(versions => { + dispatch(receiveFeedVersions(feedSource, versions)) + }) + } +} + +// Upload a GTFS File as a new FeedVersion + +export function uploadingFeed (feedSource, file) { + return { + type: 'UPLOADING_FEED', + feedSource, + file + } +} + +export function uploadedFeed (feedSource) { + return { + type: 'UPLOADED_FEED', + feedSource + } +} + +export function feedNotModified (feedSource, message) { + return { + type: 'FEED_NOT_MODIFIED', + feedSource, + message + } +} + +export function uploadFeed (feedSource, file) { + return function (dispatch, getState) { + dispatch(uploadingFeed(feedSource, file)) + const url = `/api/manager/secure/feedversion?feedSourceId=${feedSource.id}&lastModified=${file.lastModified}` + + var data = new window.FormData() + data.append('file', file) + + return fetch(url, { + method: 'post', + headers: { 'Authorization': 'Bearer ' + getState().user.token }, + body: data + }).then(res => { + if (res.status === 304) { + dispatch(feedNotModified(feedSource, 'Feed upload cancelled because it matches latest feed version.')) + } else if (res.status >= 400) { + dispatch(setErrorMessage('Error uploading feed source')) + } else { + dispatch(uploadedFeed(feedSource)) + dispatch(startJobMonitor()) + } + console.log('uploadFeed result', res) + + // fetch feed source with versions + return dispatch(fetchFeedSource(feedSource.id, true)) + }) + } +} + +export function deletingFeedVersion (feedVersion) { + return { + type: 'DELETING_FEEDVERSION', + feedVersion + } +} + +export function deleteFeedVersion (feedVersion, changes) { + return function (dispatch, getState) { + dispatch(deletingFeedVersion(feedVersion)) + const url = '/api/manager/secure/feedversion/' + feedVersion.id + return secureFetch(url, getState(), 'delete') + .then((res) => { + // fetch feed source with versions + snapshots + return dispatch(fetchFeedSource(feedVersion.feedSource.id, true, true)) + }) + } +} +export function requestingValidationResult (feedVersion) { + return { + type: 'REQUESTING_VALIDATION_RESULT', + feedVersion + } +} +export function receiveValidationResult (feedVersion, validationResult) { + return { + type: 'RECEIVE_VALIDATION_RESULT', + feedVersion, + validationResult + } +} +export function fetchValidationResult (feedVersion, isPublic) { + return function (dispatch, getState) { + dispatch(requestingValidationResult(feedVersion)) + const route = isPublic ? 'public' : 'secure' + const url = `/api/manager/${route}/feedversion/${feedVersion.id}/validation` + return secureFetch(url, getState()) + .then(response => response.json()) + .then(result => { + dispatch(receiveValidationResult(feedVersion, result)) + }) + } +} + +export function requestingFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) { + return { + type: 'REQUESTING_FEEDVERSION_ISOCHRONES', + feedVersion, + fromLat, + fromLon, + toLat, + toLon, + date, + fromTime, + toTime + } +} + +export function receiveFeedVersionIsochrones (feedSource, feedVersion, isochrones, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) { + return { + type: 'RECEIVE_FEEDVERSION_ISOCHRONES', + feedSource, + feedVersion, + isochrones, + fromLat, + fromLon, + toLat, + toLon, + date, + fromTime, + toTime + } +} + +export function fetchFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) { + return function (dispatch, getState) { + if (typeof date === 'undefined' || typeof fromTime === 'undefined' || typeof toTime === 'undefined') { + date = getState().gtfs.filter.dateTimeFilter.date + fromTime = getState().gtfs.filter.dateTimeFilter.from + toTime = getState().gtfs.filter.dateTimeFilter.to + } + dispatch(requestingFeedVersionIsochrones(feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime)) + const params = {fromLat, fromLon, toLat, toLon, date, fromTime, toTime} + const url = `/api/manager/secure/feedversion/${feedVersion.id}/isochrones?${qs.stringify(params)}` + return secureFetch(url, getState()) + .then(res => { + console.log(res.status) + if (res.status === 202) { + // dispatch(setStatus) + console.log('building network') + return [] + } + return res.json() + }) + .then(isochrones => { + console.log('received isochrones ', isochrones) + dispatch(receiveFeedVersionIsochrones(feedVersion.feedSource, feedVersion, isochrones, fromLat, fromLon, toLat, toLon, date, fromTime, toTime)) + return isochrones + }) + } +} + +// Download a GTFS file for a FeedVersion +export function downloadFeedViaToken (feedVersion, isPublic) { + return function (dispatch, getState) { + const route = isPublic ? 'public' : 'secure' + const url = `/api/manager/${route}/feedversion/${feedVersion.id}/downloadtoken` + secureFetch(url, getState()) + .then(response => response.json()) + .then(result => { + window.location.assign(`/api/manager/downloadfeed/${result.id}`) + }) + } +} + +export function creatingFeedVersionFromSnapshot () { + return { + type: 'CREATING_FEEDVERSION_FROM_SNAPSHOT' + } +} +export function createFeedVersionFromSnapshot (feedSource, snapshotId) { + return function (dispatch, getState) { + dispatch(creatingFeedVersionFromSnapshot()) + const url = `/api/manager/secure/feedversion/fromsnapshot?feedSourceId=${feedSource.id}&snapshotId=${snapshotId}` + return secureFetch(url, getState(), 'post') + .then((res) => { + dispatch(startJobMonitor()) + }) + } +} + +export function renamingFeedVersion () { + return { + type: 'RENAMING_FEEDVERSION' + } +} + +export function renameFeedVersion (feedVersion, name) { + return function (dispatch, getState) { + dispatch(renamingFeedVersion()) + const url = `/api/manager/secure/feedversion/${feedVersion.id}/rename?name=${name}` + return secureFetch(url, getState(), 'put') + .then((res) => { + dispatch(fetchFeedVersion(feedVersion.id)) + }) + } +} diff --git a/src/main/client/manager/actions/visibilityFilter.js b/lib/manager/actions/visibilityFilter.js similarity index 100% rename from src/main/client/manager/actions/visibilityFilter.js rename to lib/manager/actions/visibilityFilter.js diff --git a/lib/manager/components/DeploymentPreviewButton.js b/lib/manager/components/DeploymentPreviewButton.js new file mode 100644 index 000000000..a13c5eaaf --- /dev/null +++ b/lib/manager/components/DeploymentPreviewButton.js @@ -0,0 +1,63 @@ +import {Icon} from '@conveyal/woonerf' +import React, {PropTypes, Component} from 'react' +import { Button } from 'react-bootstrap' + +export default class DeploymentPreviewButton extends Component { + static propTypes = { + deployment: PropTypes.object + } + render () { + const { deployment } = this.props + const { projectBounds } = deployment + // TODO: add Try it button + const server = deployment.project.otpServers.find(server => server.name === deployment.deployedTo) + let href = server && server.publicUrl + if (!href || href.length === 0) { + return null + } + const lat = (projectBounds.north + projectBounds.south) / 2 + const lon = (projectBounds.east + projectBounds.west) / 2 + + // figure out the zoom. assume that otp.js will open in a window of the same size (e.g. a new tab) + const width = window.innerWidth + const height = window.innerHeight + + // what fraction of the world is this from north to south? + // note that we are storing the denominator only, to avoid roundoff errors + const boundsHeightMerc = 180 / (projectBounds.north - projectBounds.south) + + // longitude is generally more complicated, because the length depends on the latitude + // however, because we're using a Mercator projection, the map doesn't understand this either, + // and maps 360 degrees of longitude to an invariant width + // This is why Greenland appears larger than Africa, but it does make the math easy. + const boundsWidthMerc = 360 / (projectBounds.east - projectBounds.west) + + // figure out the zoom level + // level 0 is the entireer world in a single 256x256 tile, next level + // is entire world in 256 * 2^1, then 256 * 2^2, and so on + let z = 23 + + while (true) { + const worldSize = 256 * Math.pow(2, z) + // again, store the denominator/reciprocal + const windowWidthMerc = worldSize / width + const windowHeightMerc = worldSize / height + + // if it fits. We use < not > because we have stored the reciprocals. + if (windowWidthMerc < boundsWidthMerc && windowHeightMerc < boundsHeightMerc || z === 0) { + break + } + + z-- + } + href += `#start/${lat}/${lon}/${z}/${deployment.routerId || 'default'}` + return ( + + ) + } +} diff --git a/lib/manager/components/DeploymentSettings.js b/lib/manager/components/DeploymentSettings.js new file mode 100644 index 000000000..3706f6b53 --- /dev/null +++ b/lib/manager/components/DeploymentSettings.js @@ -0,0 +1,340 @@ +import React, {Component} from 'react' +import { Row, Col, Button, Panel, Glyphicon, Form, Radio, Checkbox, FormGroup, ControlLabel, FormControl } from 'react-bootstrap' +import update from 'react-addons-update' +import { shallowEqual } from 'react-pure-render' + +import { getMessage, getComponentMessages } from '../../common/util/config' + +export default class DeploymentSettings extends Component { + constructor (props) { + super(props) + this.state = { + deployment: { + buildConfig: {}, + routerConfig: {}, + otpServers: this.props.project && this.props.project.otpServers ? this.props.project.otpServers : [] + } + } + } + componentWillReceiveProps (nextProps) { + this.setState({ + deployment: { + buildConfig: {}, + routerConfig: {}, + otpServers: nextProps.project && nextProps.project.otpServers ? nextProps.project.otpServers : [] + } + }) + } + shouldComponentUpdate (nextProps, nextState) { + return !shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state) + } + render () { + console.log(this.state) + const messages = getComponentMessages('ProjectSettings') + const { project, editDisabled, updateProjectSettings } = this.props + const noEdits = Object.keys(this.state.deployment.buildConfig).length === 0 && + Object.keys(this.state.deployment.routerConfig).length === 0 && + shallowEqual(this.state.deployment.otpServers, project.otpServers) + return ( +
+ {getMessage(messages, 'deployment.buildConfig.title')}

}> + + + + {getMessage(messages, 'deployment.buildConfig.fetchElevationUS')} + { + const stateUpdate = { deployment: { buildConfig: { fetchElevationUS: { $set: (evt.target.value === 'true') } } } } + this.setState(update(this.state, stateUpdate)) + }}> + + + + + + + + {getMessage(messages, 'deployment.buildConfig.stationTransfers')} + { + const stateUpdate = { deployment: { buildConfig: { stationTransfers: { $set: (evt.target.value === 'true') } } } } + this.setState(update(this.state, stateUpdate)) + }}> + + + + + + + + {getMessage(messages, 'deployment.buildConfig.subwayAccessTime')} + { + const stateUpdate = { deployment: { buildConfig: { subwayAccessTime: { $set: +evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + {getMessage(messages, 'deployment.buildConfig.fares')} + { + const stateUpdate = { deployment: { buildConfig: { fares: { $set: evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + Router Config

}> + + + + {getMessage(messages, 'deployment.routerConfig.numItineraries')} + { + const stateUpdate = { deployment: { routerConfig: { numItineraries: { $set: +evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + + + {getMessage(messages, 'deployment.routerConfig.walkSpeed')} + { + const stateUpdate = { deployment: { routerConfig: { walkSpeed: { $set: +evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + + + + + {getMessage(messages, 'deployment.routerConfig.stairsReluctance')} + { + const stateUpdate = { deployment: { routerConfig: { stairsReluctance: { $set: +evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + + + {getMessage(messages, 'deployment.routerConfig.carDropoffTime')} + { + const stateUpdate = { deployment: { routerConfig: { carDropoffTime: { $set: +evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + + + {getMessage(messages, 'deployment.routerConfig.brandingUrlRoot')} + { + const stateUpdate = { deployment: { routerConfig: { brandingUrlRoot: { $set: evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + {getMessage(messages, 'deployment.routerConfig.requestLogFile')} + { + const stateUpdate = { deployment: { routerConfig: { requestLogFile: { $set: evt.target.value } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + + + {getMessage(messages, 'deployment.servers.title')} + + }> +
+ {this.state.deployment.otpServers && this.state.deployment.otpServers.map((server, i) => { + const title = ( +
+ {server.name}{' '} + {server.publicUrl} +
+ ) + return ( + +
+ + + {getMessage(messages, 'deployment.servers.name')} + { + const stateUpdate = { deployment: { otpServers: { [i]: { $merge: { name: evt.target.value } } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + {getMessage(messages, 'deployment.servers.public')} + { + const stateUpdate = { deployment: { otpServers: { [i]: { $merge: { publicUrl: evt.target.value } } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + {getMessage(messages, 'deployment.servers.internal')} + { + const stateUpdate = { deployment: { otpServers: { [i]: { $merge: { internalUrl: evt.target.value.split(',') } } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + + {getMessage(messages, 'deployment.servers.s3Bucket')} + { + const stateUpdate = { deployment: { otpServers: { [i]: { $merge: { s3Bucket: evt.target.value } } } } } + this.setState(update(this.state, stateUpdate)) + }} /> + + { + const stateUpdate = { deployment: { otpServers: { [i]: { $merge: { admin: evt.target.checked } } } } } + this.setState(update(this.state, stateUpdate)) + }}> + {getMessage(messages, 'deployment.servers.admin')} + +
+
+ ) + })} +
+
+ {getMessage(messages, 'deployment.osm.title')}}> + { + const stateUpdate = { deployment: { useCustomOsmBounds: { $set: (evt.target.value === 'true') } } } + this.setState(update(this.state, stateUpdate)) + }} + > + + {getMessage(messages, 'deployment.osm.gtfs')} + + + {getMessage(messages, 'deployment.osm.custom')} + + + {project.useCustomOsmBounds || this.state.deployment.useCustomOsmBounds + ? + {( {getMessage(messages, 'deployment.osm.bounds')})} + { + const bBox = evt.target.value.split(',') + if (bBox.length === 4) { + const stateUpdate = { deployment: { $merge: { osmWest: bBox[0], osmSouth: bBox[1], osmEast: bBox[2], osmNorth: bBox[3] } } } + this.setState(update(this.state, stateUpdate)) + } + }} /> + + : null + } + + + + {/* Save button */} + + + +
+ ) + } +} diff --git a/lib/manager/components/DeploymentViewer.js b/lib/manager/components/DeploymentViewer.js new file mode 100644 index 000000000..e6a44db68 --- /dev/null +++ b/lib/manager/components/DeploymentViewer.js @@ -0,0 +1,263 @@ +import React, {Component, PropTypes} from 'react' +import moment from 'moment' +import {Icon} from '@conveyal/woonerf' +import { LinkContainer } from 'react-router-bootstrap' +import { Row, Col, Button, Table, FormControl, Panel, Glyphicon, Badge, ButtonToolbar, DropdownButton, MenuItem, Label } from 'react-bootstrap' +import { Link } from 'react-router' + +import Loading from '../../common/components/Loading' +import EditableTextField from '../../common/components/EditableTextField' +import { versionsSorter } from '../../common/util/util' +import { getComponentMessages, getMessage } from '../../common/util/config' +import DeploymentPreviewButton from './DeploymentPreviewButton' + +export default class DeploymentViewer extends Component { + static propTypes = { + project: PropTypes.object + } + constructor (props) { + super(props) + this.state = {} + } + render () { + const { + project, + deployment, + feedSources, + downloadDeployment, + deployToTargetClicked, + deploymentPropertyChanged, + searchTextChanged, + addFeedVersion, + user, + updateVersionForFeedSource, + deleteFeedVersion + } = this.props + const { feedVersions } = deployment + if (!deployment || !project || !feedSources) { + return + } + const deployableFeeds = feedSources + ? feedSources.filter(fs => + feedVersions && feedVersions.findIndex(v => v.feedSource.id === fs.id) === -1 && + fs.deployable && + fs.latestValidation + ) + : [] + const messages = getComponentMessages('DeploymentViewer') + const versions = feedVersions && feedVersions.sort(versionsSorter) || [] + return ( +
+ + +
+ + + {getMessage(messages, 'deploy')} + : {getMessage(messages, 'noServers')} + } + onSelect={(evt) => { + console.log(evt) + deployToTargetClicked(deployment, evt) + // setTimeout(() => getDeploymentStatus(deployment, evt), 5000) + }} + > + {project.otpServers + ? project.otpServers.map((server, i) => ( + {server.name} + )) + : null + } + + +

+ + + + deploymentPropertyChanged(deployment, 'name', value)} + /> + {deployment.deployedTo + ? + + {' '} + + + : null + } +

+
+ +
+ {getMessage(messages, 'versions')})} + collapsible + defaultExpanded + > + + + searchTextChanged(evt.target.value)} + /> + + + {getMessage(messages, 'addFeedSource')} : {getMessage(messages, 'allFeedsAdded')}} + onSelect={(evt) => { + console.log(evt) + const feed = deployableFeeds.find(fs => fs.id === evt) + addFeedVersion(deployment, {id: feed.latestVersionId}) + }} + > + { + deployableFeeds.map((fs, i) => ( + {fs.name} + )) + } + + + + + + + + + + + + + + + + + + + + + + {versions.map((version) => { + return + })} + +
{getMessage(messages, 'table.name')}Version{getMessage(messages, 'table.dateRetrieved')}{getMessage(messages, 'table.loadStatus')}{getMessage(messages, 'table.errorCount')}{getMessage(messages, 'table.routeCount')}{getMessage(messages, 'table.tripCount')}{getMessage(messages, 'table.stopTimesCount')}{getMessage(messages, 'table.validFrom')}{getMessage(messages, 'table.expires')} +
+ +
+
+
+ ) + } +} + +class FeedVersionTableRow extends Component { + render () { + const { + feedSource, + version, + deployment, + project, + user, + updateVersionForFeedSource, + deleteFeedVersionClicked + } = this.props + const result = version.validationResult + const na = (N/A) + const hasVersionStyle = {cursor: 'pointer'} + const noVersionStyle = {color: 'lightGray'} + const disabled = !user.permissions.hasFeedPermission(project.organizationId, project.id, feedSource.id, 'manage-feed') + return ( + + + {feedSource.name} + + + Version {version.version} + + + + + + + {na} + + + {result.loadStatus} + + {result.errorCount} + {result.routeCount} + {result.tripCount} + {result.stopTimesCount} + {moment(result.startDate).format('MMM Do YYYY')} ({moment(result.startDate).fromNow()}) + {moment(result.endDate).format('MMM Do YYYY')} ({moment(result.endDate).fromNow()}) + + + + + ) + } + +} diff --git a/lib/manager/components/DeploymentsPanel.js b/lib/manager/components/DeploymentsPanel.js new file mode 100644 index 000000000..7a37b1025 --- /dev/null +++ b/lib/manager/components/DeploymentsPanel.js @@ -0,0 +1,164 @@ +import React, {Component, PropTypes} from 'react' +import moment from 'moment' +import { LinkContainer } from 'react-router-bootstrap' +import { Row, Label, Col, Button, Table, FormControl, Glyphicon, Panel } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' + +import ActiveDeploymentViewer from '../containers/ActiveDeploymentViewer' +import EditableTextField from '../../common/components/EditableTextField' +import { getComponentMessages, getMessage } from '../../common/util/config' + +export default class DeploymentsPanel extends Component { + static propTypes = { + deployments: PropTypes.array, + deleteDeploymentConfirmed: PropTypes.func, + deploymentsRequested: PropTypes.func, + onNewDeploymentClick: PropTypes.func, + newDeploymentNamed: PropTypes.func, + updateDeployment: PropTypes.func, + expanded: PropTypes.bool + } + componentWillMount () { + if (this.props.expanded) { + this.props.deploymentsRequested() + } + } + // deleteDeployment (deployment) { + // console.log(this.refs) + // this.refs['page'].showConfirmModal({ + // title: 'Delete Deployment?', + // body: `Are you sure you want to delete the deployment ${deployment.name}?`, + // onConfirm: () => { + // console.log('OK, deleting') + // this.props.deleteDeploymentConfirmed(deployment) + // } + // }) + // } + render () { + const deployment = this.props.deployments && this.props.deployments.find(d => d.id && d.id === this.props.activeSubComponent) + if (deployment) { + return ( + + ) + } + return ( + + + + + + Deploying feeds to OTP}> +

A collection of feeds can be deployed to OpenTripPlanner (OTP) instances that have been defined in the organization settings.

+ + + +
+ +
+ ) + } +} + +class DeploymentsList extends Component { + render () { + const messages = getComponentMessages('DeploymentsPanel') + const na = (N/A) + return ( + + + this.props.searchTextChanged(evt.target.value)} + /> + + + + + + } + > + + + + + + + + + + + {this.props.deployments + ? this.props.deployments.map(dep => { + return ( + + + + + + + + ) + }) + : null + } + +
{getMessage(messages, 'table.name')}{getMessage(messages, 'table.creationDate')}{getMessage(messages, 'table.deployedTo')}{getMessage(messages, 'table.feedCount')} +
+ { + if (dep.isCreating) this.props.newDeploymentNamed(value) + else this.props.updateDeployment(dep, {name: value}) + }} + link={`/project/${dep.project.id}/deployments/${dep.id}`} + /> + + {dep.dateCreated + ? ({moment(dep.dateCreated).format('MMM Do YYYY')} ({moment(dep.dateCreated).fromNow()})) + : na + } + + {dep.deployedTo + ? () + : na + } + + {dep.feedVersions + ? ({dep.feedVersions.length}) + : na + } + + +
+
+ ) + } +} diff --git a/src/main/client/manager/components/ExternalPropertiesTable.js b/lib/manager/components/ExternalPropertiesTable.js similarity index 89% rename from src/main/client/manager/components/ExternalPropertiesTable.js rename to lib/manager/components/ExternalPropertiesTable.js index f5fa270bc..6f0572b7c 100644 --- a/src/main/client/manager/components/ExternalPropertiesTable.js +++ b/lib/manager/components/ExternalPropertiesTable.js @@ -7,14 +7,6 @@ export default class ExternalPropertiesTable extends Component { static propTypes = { resourceType: PropTypes.string } - constructor (props) { - super(props) - console.log('>> ExternalPropertiesTable props', this.props); - } - - componentWillMount () { - } - render () { return ( + deleteFeedSource(feedSource)} + onClose={() => setHold(false)} + /> + + this.confirmUpload(files)} + onClose={() => setHold(false)} + errorMessage='Uploaded file must be a valid zip file (.zip).' + /> + + this._selectItem(key)} + id={`feed-source-action-button`} + pullRight + > + + + + Fetch + Upload + + {/* show divider only if deployments and notifications are enabled (otherwise, we don't need it) */} + {isModuleEnabled('deployment') || getConfigProperty('application.notifications_enabled') + ? + : null + } + {isModuleEnabled('deployment') + ? + Deploy + + : null + } + {getConfigProperty('application.notifications_enabled') + ? + : null + } + View public page + + Delete + + +
+ } +} diff --git a/lib/manager/components/FeedSourcePanel.js b/lib/manager/components/FeedSourcePanel.js new file mode 100644 index 000000000..df410ded5 --- /dev/null +++ b/lib/manager/components/FeedSourcePanel.js @@ -0,0 +1,108 @@ +import React, {Component, PropTypes} from 'react' +import {Icon} from '@conveyal/woonerf' +import { ListGroupItem, Panel, Badge, ListGroup, FormControl, Button, ButtonGroup } from 'react-bootstrap' +import { Link } from 'react-router' + +import { getComponentMessages, getMessage } from '../../common/util/config' + +export default class FeedSourcePanel extends Component { + static propTypes = { + activeProject: PropTypes.object, + visibilityFilter: PropTypes.object, + searchTextChanged: PropTypes.func, + user: PropTypes.object, + visibilityFilterChanged: PropTypes.func + } + render () { + const messages = getComponentMessages('FeedSourcePanel') + const { + activeProject, + visibilityFilter, + searchTextChanged, + user, + visibilityFilterChanged + } = this.props + const renderFeedItems = (p, fs) => { + const feedName = `${p.name} / ${fs.name}` + return ( + + + + + {feedName.length > 33 ? `${feedName.substr(0, 33)}...` : feedName} + + + + ) + } + const feedVisibilityFilter = (feed) => { + const name = feed.name || 'unnamed' + const visible = name.toLowerCase().indexOf((visibilityFilter.searchText || '').toLowerCase()) !== -1 + switch (visibilityFilter.filter) { + case 'ALL': + return visible + case 'STARRED': + return [].indexOf(feed.id) !== -1 // check userMetaData + case 'PUBLIC': + return feed.isPublic + case 'PRIVATE': + return !feed.isPublic + default: + return visible + } + // if (feed.isCreating) return true // feeds actively being created are always visible + } + return ( + Feeds for {activeProject ? activeProject.name : '[choose project]'} {activeProject && activeProject.feedSources && + {activeProject.feedSources.length} + } + }> + + + searchTextChanged(evt.target.value)} + /> + + + + + + + + {activeProject && activeProject.feedSources + ? activeProject.feedSources.filter(feedVisibilityFilter).map(fs => renderFeedItems(activeProject, fs)) + : activeProject + ? +

+ No feeds yet.{' '} + {user.permissions.isProjectAdmin(activeProject.id, activeProject.organizationId) && Create one.} +

+
+ :

Choose a project to view feeds

+ // projects && projects.map(p => { + // return p.feedSources && p.feedSources.filter(feedVisibilityFilter).map(fs => renderFeedItems(p, fs)) + // }) + } +
+
+ ) + } +} diff --git a/lib/manager/components/FeedSourceTable.js b/lib/manager/components/FeedSourceTable.js new file mode 100644 index 000000000..c0e76ab9d --- /dev/null +++ b/lib/manager/components/FeedSourceTable.js @@ -0,0 +1,261 @@ +import React, { Component, PropTypes } from 'react' +import moment from 'moment' + +import { Button, ListGroupItem, ListGroup, OverlayTrigger, Tooltip } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import { shallowEqual } from 'react-pure-render' + +import EditableTextField from '../../common/components/EditableTextField' +import FeedSourceDropdown from './FeedSourceDropdown' +import { isModuleEnabled, getComponentMessages, getMessage, getConfigProperty } from '../../common/util/config' + +export default class FeedSourceTable extends Component { + + static propTypes = { + feedSources: PropTypes.array, + project: PropTypes.object, + user: PropTypes.object, + + isFetching: PropTypes.bool, + + createDeploymentFromFeedSource: PropTypes.func, + deleteFeedSource: PropTypes.func, + updateFeedSourceProperty: PropTypes.func, + saveFeedSource: PropTypes.func, + fetchFeed: PropTypes.func, + uploadFeed: PropTypes.func, + onNewFeedSourceClick: PropTypes.func + } + + constructor (props) { + super(props) + + this.state = { + activeFeedSource: null + } + } + + render () { + const messages = getComponentMessages('ProjectViewer') + const { + project, + user, + createDeploymentFromFeedSource, + deleteFeedSource, + uploadFeed, + fetchFeed, + isFetching, + feedSources, + updateFeedSourceProperty, + saveFeedSource, + onNewFeedSourceClick + } = this.props + const hover = createDeploymentFromFeedSource(fs)} + deleteFeedSource={(fs) => deleteFeedSource(fs)} + uploadFeed={(fs, file) => uploadFeed(fs, file)} + fetchFeed={(fs) => fetchFeed(fs)} + setHold={(fs) => this.setState({holdFeedSource: fs})} + /> + + return ( + + {isFetching + ? + : feedSources.length + ? feedSources.map((feedSource) => { + return this.setState({activeFeedSource: fs})} + active={this.state.activeFeedSource && this.state.activeFeedSource.id === feedSource.id} + hold={this.state.holdFeedSource && this.state.holdFeedSource.id === feedSource.id} + /> + }) + : + + + } + + ) + } +} + +class FeedSourceTableRow extends Component { + + static propTypes = { + feedSource: PropTypes.object, + hoverComponent: PropTypes.node, + project: PropTypes.object, + user: PropTypes.object, + + updateFeedSourceProperty: PropTypes.func, + saveFeedSource: PropTypes.func, + onHover: PropTypes.func, + active: PropTypes.bool, + hold: PropTypes.bool + } + + constructor (props) { + super(props) + + this.state = { + hovered: false + } + } + shouldComponentUpdate (nextProps, nextState) { + return !shallowEqual(nextProps.feedSource, this.props.feedSource) || this.props.active !== nextProps.active + } + render () { + const { + feedSource, + user, + project, + onHover, + hold, + active, + hoverComponent, + saveFeedSource, + updateFeedSourceProperty + } = this.props + // const na = (N/A) + const disabled = !user.permissions.hasFeedPermission(project.organizationId, project.id, feedSource.id, 'manage-feed') + // const dateFormat = getConfigProperty('application.date_format') + const messages = getComponentMessages('ProjectViewer') + return ( + onHover(feedSource)} + onMouseLeave={() => { + if (!hold) { + onHover(null) + } + }} + > +
+ + {active + ? hoverComponent + : null + } + +

+ { + if (feedSource.isCreating) saveFeedSource(value) + else updateFeedSourceProperty(feedSource, 'name', value) + }} + link={`/feed/${feedSource.id}`} + /> + {' '} + {!feedSource.isPublic ? : null} + {' '} + {feedSource.editedSinceSnapshot + ? + : + } +

+ +
+
+ ) + } +} + +class FeedSourceAttributes extends Component { + render () { + const { feedSource, messages } = this.props + const dateFormat = getConfigProperty('application.date_format') + const hasErrors = feedSource.latestValidation && feedSource.latestValidation.errorCount > 0 + const hasVersion = feedSource.latestValidation + const isExpired = feedSource.latestValidation && feedSource.latestValidation.endDate < +moment() + const end = feedSource.latestValidation && moment(feedSource.latestValidation.endDate) + const endDate = end && end.format(dateFormat) + const timeTo = end && moment().to(end) + return ( +
    + + + + {isModuleEnabled('deployment') + ? + : null + } + {feedSource.url + ? + : null + } +
+ ) + } +} + +class Attribute extends Component { + render () { + const { className, icon, minWidth, text, title } = this.props + const li = ( +
  • + {icon && } + {text && ` ${text}`} +
  • + ) + return title + ? {title}}> + {li} + + : li + } +} diff --git a/lib/manager/components/FeedSourceViewer.js b/lib/manager/components/FeedSourceViewer.js new file mode 100644 index 000000000..954d591e6 --- /dev/null +++ b/lib/manager/components/FeedSourceViewer.js @@ -0,0 +1,316 @@ +import React, {Component, PropTypes} from 'react' +import {Icon} from '@conveyal/woonerf' +import Helmet from 'react-helmet' +import { sentence as toSentenceCase } from 'change-case' +import { LinkContainer } from 'react-router-bootstrap' +import { Grid, Row, Col, ListGroup, ListGroupItem, Button, Badge, Panel, Glyphicon, Tabs, Tab, FormControl, InputGroup, ControlLabel, FormGroup, Checkbox } from 'react-bootstrap' +import { Link, browserHistory } from 'react-router' + +import ManagerPage from '../../common/components/ManagerPage' +import Breadcrumbs from '../../common/components/Breadcrumbs' +import ExternalPropertiesTable from './ExternalPropertiesTable' +import ManagerHeader from './ManagerHeader' +import ActiveFeedVersionNavigator from '../containers/ActiveFeedVersionNavigator' +import NotesViewer from './NotesViewer' +import ActiveEditorFeedSourcePanel from '../../editor/containers/ActiveEditorFeedSourcePanel' +import { isModuleEnabled, getComponentMessages, getMessage } from '../../common/util/config' + +export default class FeedSourceViewer extends Component { + + static propTypes = { + feedSource: PropTypes.object, + feedSourceId: PropTypes.string, + feedVersionIndex: PropTypes.number, + isFetching: PropTypes.bool, + project: PropTypes.object, + routeParams: PropTypes.object, + user: PropTypes.object, + activeComponent: PropTypes.string, + activeSubComponent: PropTypes.string, + + componentDidUpdate: PropTypes.func, + createDeployment: PropTypes.func, + deleteFeedSource: PropTypes.func, + deleteFeedVersionConfirmed: PropTypes.func, + downloadFeedClicked: PropTypes.func, + externalPropertyChanged: PropTypes.func, + feedSourcePropertyChanged: PropTypes.func, + feedVersionRenamed: PropTypes.func, + gtfsPlusDataRequested: PropTypes.func, + loadFeedVersionForEditing: PropTypes.func, + newNotePostedForFeedSource: PropTypes.func, + newNotePostedForVersion: PropTypes.func, + notesRequestedForFeedSource: PropTypes.func, + notesRequestedForVersion: PropTypes.func, + onComponentMount: PropTypes.func, + fetchFeed: PropTypes.func, + updateUserSubscription: PropTypes.func, + uploadFeed: PropTypes.func, + fetchValidationResult: PropTypes.func + } + + constructor (props) { + super(props) + this.state = {} + } + componentWillMount () { + this.props.onComponentMount(this.props) + // this.setState({willMount: true}) + } + componentDidMount () { + this.setState({didMount: true}) + } + componentDidUpdate (prevProps) { + this.props.componentDidUpdate(prevProps, this.props) + if (this.props.feedSource && this.state.didMount) { + this.setState({didMount: false}) + } + } + confirmDeleteFeedSource (feedSource) { + this.refs['page'].showConfirmModal({ + title: 'Delete Feed Source?', + body: 'Are you sure you want to delete this feed source? This action cannot be undone and all feed versions will be deleted.', + onConfirm: () => { + this.props.deleteFeedSource(feedSource) + .then(() => browserHistory.push(`/project/${feedSource.projectId}`)) + } + }) + } + + render () { + const fs = this.props.feedSource + if (this.props.isFetching && this.state.didMount) { + return ( + + +

    + +

    +
    +
    + ) + } else if (!fs) { + return ( + + + + +

    No feed source found for {this.props.feedSourceId}

    +

    Return to list of projects

    + +
    +
    +
    + ) + } + const { + user, + project, + activeComponent, + activeSubComponent, + routeParams, + feedSourcePropertyChanged, + externalPropertyChanged, + feedSource, + feedVersionIndex, + notesRequestedForFeedSource, + updateUserSubscription, + newNotePostedForFeedSource + } = this.props + const messages = getComponentMessages('FeedSourceViewer') + const disabled = !user.permissions.hasFeedPermission(project.organizationId, project.id, fs.id, 'manage-feed') + // const editGtfsDisabled = !user.permissions.hasFeedPermission(project.organizationId, project.id, fs.id, 'edit-gtfs') + const autoFetchFeed = fs.retrievalMethod === 'FETCHED_AUTOMATICALLY' + const resourceType = activeComponent === 'settings' && activeSubComponent && activeSubComponent.toUpperCase() + const activeTab = ['settings', 'comments', 'snapshots'].indexOf(activeComponent) === -1 || typeof routeParams.feedVersionIndex !== 'undefined' + ? '' + : activeComponent + // console.log(activeComponent, routeParams.feedVersionIndex) + const activeSettings = !resourceType + ? + Settings}> + + + + Feed source name + + { + this.setState({name: evt.target.value}) + }} + /> + + + + + + + + + feedSourcePropertyChanged(fs, 'deployable', !fs.deployable)}>Make feed source deployable + Enable this feed source to be deployed to an OpenTripPlanner (OTP) instance (defined in organization settings) as part of a collection of feed sources or individually. + + + + + Automatic fetch}> + + + + Feed source fetch URL + + { + this.setState({url: evt.target.value}) + }} + /> + + + + + + + + + feedSourcePropertyChanged(fs, 'retrievalMethod', autoFetchFeed ? 'MANUALLY_UPLOADED' : 'FETCHED_AUTOMATICALLY')} bsStyle='danger'>Auto fetch feed source + Set this feed source to fetch automatically. (Feed source URL must be specified and project auto fetch must be enabled.) + + + + + Danger zone}> + + + +

    Make this feed source {fs.isPublic ? 'private' : 'public'}.

    +

    This feed source is currently {fs.isPublic ? 'public' : 'private'}.

    +
    + + +

    Delete this feed source.

    +

    Once you delete a feed source, it cannot be recovered.

    +
    +
    +
    + + : + { + externalPropertyChanged(fs, resourceType, name, value) + }} + /> + + return ( + + } + > + + + + + {/* Feed Versions tab */} + browserHistory.push(`/feed/${fs.id}/${eventKey}`))} + > + {getMessage(messages, 'gtfs')}}> + + + + + + + + {isModuleEnabled('editor') + ? {getComponentMessages('EditorFeedSourcePanel').title} {fs.editorSnapshots ? fs.editorSnapshots.length : 0}} + > + + + : null + } + {/* Comments for feed source */} + {getComponentMessages('NotesViewer').title} {fs.noteCount}} + onEnter={() => notesRequestedForFeedSource(fs)} + > + { notesRequestedForFeedSource(fs) }} + newNotePosted={(note) => { newNotePostedForFeedSource(fs, note) }} + /> + + {/* Settings */} + {getMessage(messages, 'properties.title')}}> + + + + + General + {Object.keys(fs.externalProperties || {}).map(resourceType => { + const resourceLowerCase = resourceType.toLowerCase() + return ( + {toSentenceCase(resourceType)} properties + ) + })} + + + + + {activeSettings} + + + + + + ) + } +} diff --git a/lib/manager/components/FeedVersionNavigator.js b/lib/manager/components/FeedVersionNavigator.js new file mode 100644 index 000000000..c6b043826 --- /dev/null +++ b/lib/manager/components/FeedVersionNavigator.js @@ -0,0 +1,211 @@ +import React, {Component, PropTypes} from 'react' +import { Row, Col, ButtonGroup, ButtonToolbar, DropdownButton, MenuItem, Button, Glyphicon } from 'react-bootstrap' +import { browserHistory } from 'react-router' +import { LinkContainer } from 'react-router-bootstrap' +import {Icon} from '@conveyal/woonerf' + +import { isModuleEnabled, getComponentMessages, getMessage } from '../../common/util/config' +import { isValidZipFile } from '../../common/util/util' +import FeedVersionViewer from './FeedVersionViewer' +import ConfirmModal from '../../common/components/ConfirmModal' +import SelectFileModal from '../../common/components/SelectFileModal' + +export default class FeedVersionNavigator extends Component { + + static propTypes = { + deleteDisabled: PropTypes.bool, + feedSource: PropTypes.object, + feedVersionIndex: PropTypes.number, + isPublic: PropTypes.bool, + + deleteFeedVersionConfirmed: PropTypes.func, + downloadFeedClicked: PropTypes.func, + feedVersionRenamed: PropTypes.func, + gtfsPlusDataRequested: PropTypes.func, + loadFeedVersionForEditing: PropTypes.func, + newNotePostedForVersion: PropTypes.func, + notesRequestedForVersion: PropTypes.func, + createDeploymentFromFeedSource: PropTypes.func, + fetchValidationResult: PropTypes.func, + setVersionIndex: PropTypes.func + } + constructor (props) { + super(props) + this.state = {} + } + componentWillReceiveProps (nextProps) { + if (nextProps.feedVersionIndex !== this.props.feedVersionIndex && nextProps.feedSource && nextProps.feedSource.feedVersions) { + this.props.setVersionIndex(nextProps.feedSource, nextProps.feedVersionIndex, false) + } + } + render () { + console.log(this.props) + const versionTitleStyle = { + fontSize: '24px', + fontWeight: 'bold' + } + const { disabled } = this.props + const fs = this.props.feedSource + const versions = this.props.feedSource.feedVersions + const messages = getComponentMessages('FeedVersionNavigator') + const hasVersions = versions && versions.length > 0 + + const sortedVersions = hasVersions && versions.sort((a, b) => { + if (a.updated < b.updated) return -1 + if (a.updated > b.updated) return 1 + return 0 + }) + + let version + + if (typeof this.props.feedVersionIndex === 'undefined') { + return null + } else if (hasVersions && versions.length >= this.props.feedVersionIndex) { + version = sortedVersions[this.props.feedVersionIndex - 1] + } else { + console.log(`Error version ${this.props.feedVersionIndex} does not exist`) + } + + return ( +
    + { + console.log('OK, deleting') + this.props.deleteFeedSource(fs) + }} + /> + { + console.log(files[0].type) + if (isValidZipFile(files[0])) { + this.props.uploadFeed(fs, files[0]) + return true + } else { + return false + } + }} + errorMessage='Uploaded file must be a valid zip file (.zip).' + /> + + {/* Version Navigation Widget and Name Editor */} + + + + + + + {this.state.listView + ? null + : {/* Version Navigation/Selection Widget */} + {/* Previous Version Button */} + + + {/* Version Selector Dropdown */} + { + if (key !== this.props.feedVersionIndex) { + this.props.setVersionIndex(fs, key) + } + }} + > + {versions.map((version, k) => { + k = k + 1 + return {k}. {version.name} + })} + + + {/* Next Version Button */} + + + } + + {isModuleEnabled('deployment') + ? + : null + } + {isModuleEnabled('editor') + ? + + + : null + } + Create new version} id='bg-nested-dropdown' + onSelect={key => { + console.log(key) + switch (key) { + case 'delete': + return this.refs['deleteModal'].open() + case 'fetch': + return this.props.fetchFeed(fs) + case 'upload': + return this.refs['uploadModal'].open() + case 'deploy': + return this.props.createDeploymentFromFeedSource(fs) + case 'public': + return browserHistory.push(`/public/feed/${fs.id}`) + } + }} + > + Fetch + Upload + + From snapshot + + + + + + + + { + this.props.fetchValidationResult(version, this.props.isPublic) + }} + gtfsPlusDataRequested={(version) => { + this.props.gtfsPlusDataRequested(version) + }} + notesRequested={() => { this.props.notesRequestedForVersion(version) }} + newNotePosted={(note) => { this.props.newNotePostedForVersion(version, note) }} + {...this.props} + /> + + +
    + ) + } +} diff --git a/lib/manager/components/FeedVersionReport.js b/lib/manager/components/FeedVersionReport.js new file mode 100644 index 000000000..816158bd8 --- /dev/null +++ b/lib/manager/components/FeedVersionReport.js @@ -0,0 +1,324 @@ +import React, {Component, PropTypes} from 'react' +import { Row, Col, Button, Panel, ControlLabel, Label, Tabs, Tab, ButtonGroup, ListGroup, ListGroupItem } from 'react-bootstrap' +import moment from 'moment' +import {Icon} from '@conveyal/woonerf' +import numeral from 'numeral' +import Rcslider from 'rc-slider' +import fileDownload from 'react-file-download' +import area from 'turf-area' +import bboxPoly from 'turf-bbox-polygon' + +import EditableTextField from '../../common/components/EditableTextField' +import ActiveGtfsMap from '../../gtfs/containers/ActiveGtfsMap' +import { VersionButtonToolbar } from './FeedVersionViewer' +import { getComponentMessages, getConfigProperty, getMessage, isModuleEnabled, isExtensionEnabled } from '../../common/util/config' +import { getProfileLink } from '../../common/util/util' +// import { downloadAsShapefile } from '../util' +import Patterns from './reporter/containers/Patterns' +import Routes from './reporter/containers/Routes' +import Stops from './reporter/containers/Stops' +import GtfsValidationSummary from './validation/GtfsValidationSummary' +import TripsChart from './validation/TripsChart' +import ActiveDateTimeFilter from './reporter/containers/ActiveDateTimeFilter' + +const dateFormat = 'MMM. DD, YYYY' +const timeFormat = 'h:MMa' +const MAP_HEIGHTS = [200, 400] +const ISO_BANDS = [] +for (var i = 0; i < 24; i++) { + ISO_BANDS.push(300 * (i + 1)) +} +export default class FeedVersionReport extends Component { + + static propTypes = { + version: PropTypes.object, + versions: PropTypes.array, + feedVersionIndex: PropTypes.number, + isPublic: PropTypes.bool, + hasVersions: PropTypes.bool, + feedVersionRenamed: PropTypes.func, + fetchValidationResult: PropTypes.func, + publishFeedVersion: PropTypes.func + } + constructor (props) { + super(props) + this.state = { + tab: 'feed', + mapHeight: MAP_HEIGHTS[0], + isochroneBand: 60 * 60 + } + } + getBoundsArea (bounds) { + const poly = bounds && bboxPoly([bounds.west, bounds.south, bounds.east, bounds.east]) + return poly && area(poly) + } + getVersionDateLabel (version) { + const now = +moment() + const future = version.validationSummary && version.validationSummary.startDate > now + const expired = version.validationSummary && version.validationSummary.endDate < now + return version.validationSummary + ? + : null + } + selectTab (tab) { + this.setState({tab}) + } + renderIsochroneMessage (version) { + if (version.isochrones && version.isochrones.features) { + return + Move marker or change date/time to recalculate travel shed.
    + +
    + } else if (version.isochrones) { + return 'Reading transport network, please try again later.' + } else { + return 'Click on map above to show travel shed for this feed.' + } + } + renderValidatorTabs (version) { + if (!isModuleEnabled('validator')) { + return null + } + const validatorTabs = [ + { + title: 'Validation', + key: 'validation', + component: { this.props.fetchValidationResult(version) }} + /> + }, + { + title: 'Accessibility', + key: 'accessibility', + component:
    + + + {/* isochrone message */} +

    + {this.renderIsochroneMessage(version)} +

    + +
    + + + Travel time + this.setState({isochroneBand: value * 60})} + step={5} + marks={{ + 15: '¼ hour', + 30: '½ hour', + 60: 1 hour, + 120: '2 hours' + }} + tipFormatter={(value) => { + return `${value} minutes` + }} + /> + + + +
    + }, + { + title: 'Timeline', + key: 'timeline', + component:
    + + +

    Number of trips per date of service.

    + +
    + { this.props.fetchValidationResult(version) }} + /> +
    + } + ] + return validatorTabs.map(t => ( + + {t.component} + + )) + } + render () { + const version = this.props.version + const messages = getComponentMessages('FeedVersionReport') + + if (!version) return

    {getMessage(messages, 'noVersionsExist')}

    + + const versionHeader = ( +
    +

    + {/* Name Display / Editor */} + {version.validationSummary.loadStatus === 'SUCCESS' && version.validationSummary.errorCount === 0 + ? + : version.validationSummary.errorCount > 0 + ? + : + } + {this.props.isPublic + ? {version.name} + : this.props.feedVersionRenamed(version, value)} /> + } + +

    + + Version published {moment(version.updated).fromNow()} by {version.user ? {version.user} : '[unknown]'} + +
    + ) + const tableOptions = { + striped: true, + search: true, + hover: true, + exportCSV: true, + // maxHeight: '500px', + pagination: true, + options: { + paginationShowsTotal: true, + sizePerPageList: [10, 20, 50, 100] + } + } + const countFields = ['agencyCount', 'routeCount', 'tripCount', 'stopTimesCount'] + return ( + {numeral(version.fileSize || 0).format('0 b')} zip file last modified at {version.fileTimestamp ? moment(version.fileTimestamp).format(timeFormat + ', ' + dateFormat) : 'N/A' }} + > + + + + + + + + + +

    + {isExtensionEnabled('mtc') + ? + : null + } + {`Valid from ${moment(version.validationSummary.startDate).format(dateFormat)} to ${moment(version.validationSummary.endDate).format(dateFormat)}`} + {' '} + {this.getVersionDateLabel(version)} +

    +

    + {version.validationSummary && version.validationSummary.avgDailyRevenueTime + ? {Math.floor(version.validationSummary.avgDailyRevenueTime / 60 / 60 * 100) / 100} hours daily service (Tuesday) + : null + } + {version.validationSummary && version.validationSummary.bounds && getConfigProperty('application.dev') + ? {this.getBoundsArea(version.validationSummary.bounds)} square meters + : null + } +

    +
    + + { + if (this.state.tab !== key) { + this.selectTab(key) + } + }} + id='uncontrolled-tab-example' + bsStyle='pills' + unmountOnExit + > + + + {countFields.map(c => ( + +

    {numeral(version.validationSummary[c]).format('0 a')}

    +

    {getMessage(messages, c)}

    + + ))} +
    +
    + + this.selectTab(key)} + tableOptions={tableOptions} + /> + + + this.selectTab(key)} + tableOptions={tableOptions} + /> + + + this.selectTab(key)} + tableOptions={tableOptions} + /> + + {this.renderValidatorTabs(version)} +
    +
    +
    +
    + ) + } +} diff --git a/lib/manager/components/FeedVersionViewer.js b/lib/manager/components/FeedVersionViewer.js new file mode 100644 index 000000000..90e67b659 --- /dev/null +++ b/lib/manager/components/FeedVersionViewer.js @@ -0,0 +1,267 @@ +import React, {Component, PropTypes} from 'react' +import { Row, Col, Button, Panel, Label, Glyphicon, ButtonToolbar, ListGroup, ListGroupItem } from 'react-bootstrap' +import moment from 'moment' +import { LinkContainer } from 'react-router-bootstrap' +import {Icon} from '@conveyal/woonerf' + +import GtfsValidationViewer from './validation/GtfsValidationViewer' +// import GtfsValidationExplorer from './validation/GtfsValidationExplorer' +import FeedVersionReport from './FeedVersionReport' +import NotesViewer from './NotesViewer' +import ConfirmModal from '../../common/components/ConfirmModal' +import ActiveGtfsPlusVersionSummary from '../../gtfsplus/containers/ActiveGtfsPlusVersionSummary' +import { isModuleEnabled, getComponentMessages, getMessage } from '../../common/util/config' + +export default class FeedVersionViewer extends Component { + + static propTypes = { + version: PropTypes.object, + feedSource: PropTypes.object, + versions: PropTypes.array, + feedVersionIndex: PropTypes.number, + versionSection: PropTypes.string, + + isPublic: PropTypes.bool, + hasVersions: PropTypes.bool, + listView: PropTypes.bool, + + newNotePosted: PropTypes.func, + notesRequested: PropTypes.func, + fetchValidationResult: PropTypes.func, + downloadFeedClicked: PropTypes.func, + loadFeedVersionForEditing: PropTypes.func + } + render () { + const version = this.props.version + const messages = getComponentMessages('FeedVersionViewer') + + if (!version) return

    {getMessage(messages, 'noVersionsExist')}

    + + if (this.props.listView) { + // List view of feed versions + return ( + + + + + + ) + } + + switch (this.props.versionSection) { + // case 'validation': + // return ( + // + // ) + default: + return ( + + + + + + {!this.props.versionSection + ? + : this.props.versionSection === 'issues' + ? { this.props.fetchValidationResult(version) }} + /> + : this.props.versionSection === 'gtfsplus' && isModuleEnabled('gtfsplus') + ? + : this.props.versionSection === 'comments' + ? { this.props.notesRequested() }} + newNotePosted={(note) => { this.props.newNotePosted(note) }} + /> + : null + } + + + ) + } + } +} + +export class VersionButtonToolbar extends Component { + static propTypes = { + version: PropTypes.object, + versions: PropTypes.array, + feedVersionIndex: PropTypes.number, + // versionSection: PropTypes.string, + + isPublic: PropTypes.bool, + hasVersions: PropTypes.bool, + + downloadFeedClicked: PropTypes.func, + deleteFeedVersionConfirmed: PropTypes.func, + loadFeedVersionForEditing: PropTypes.func + } + render () { + const version = this.props.version + const messages = getComponentMessages('FeedVersionViewer') + return ( +
    + + + + {/* "Download Feed" Button */} + + + {/* "Load for Editing" Button */} + {isModuleEnabled('editor') && !this.props.isPublic + ? + : null + } + + {/* "Delete Version" Button */} + {!this.props.isPublic + ? + : null + } + +
    + ) + } +} + +class VersionSectionSelector extends Component { + static propTypes = { + version: PropTypes.object, + feedVersionIndex: PropTypes.number, + versionSection: PropTypes.string + } + renderIssuesLabel (version) { + const color = version.validationSummary.loadStatus !== 'SUCCESS' + ? 'danger' + : version.validationSummary.errorCount + ? 'warning' + : 'success' + const text = version.validationSummary.loadStatus !== 'SUCCESS' + ? 'critical error' + : version.validationSummary.errorCount + return ( + + ) + } + render () { + const { version } = this.props + return ( + + + + Version summary + + + + Validation issues {this.renderIssuesLabel(version)} + + + {isModuleEnabled('gtfsplus') + ? + + GTFS+ for this version + + + : null + } + + Version comments + + + + ) + } +} + +class VersionList extends Component { + static propTypes = { + versions: PropTypes.array + } + getVersionDateLabel (version) { + const now = +moment() + const future = version.validationSummary && version.validationSummary.startDate > now + const expired = version.validationSummary && version.validationSummary.endDate < now + return version.validationSummary + ? + : null + } + render () { + return List of feed versions}> + + {this.props.versions + ? this.props.versions.map(v => { + return ( + + {v.name} + {' '} + + {this.getVersionDateLabel(v)} + + + + ) + }) + : + No versions + + } + + + } +} diff --git a/lib/manager/components/GeneralSettings.js b/lib/manager/components/GeneralSettings.js new file mode 100644 index 000000000..adc5b9938 --- /dev/null +++ b/lib/manager/components/GeneralSettings.js @@ -0,0 +1,257 @@ +import React, {Component} from 'react' +import {Icon} from '@conveyal/woonerf' +import ReactDOM from 'react-dom' +import DateTimeField from 'react-bootstrap-datetimepicker' +import update from 'react-addons-update' +import { shallowEqual } from 'react-pure-render' +import moment from 'moment' +import { Row, Col, Button, Panel, Glyphicon, Checkbox, FormGroup, InputGroup, ControlLabel, FormControl, ListGroup, ListGroupItem } from 'react-bootstrap' + +import { getMessage, getComponentMessages } from '../../common/util/config' +import TimezoneSelect from '../../common/components/TimezoneSelect' +import LanguageSelect from '../../common/components/LanguageSelect' + +export default class GeneralSettings extends Component { + constructor (props) { + super(props) + this.state = { + general: {} + } + } + componentWillReceiveProps (nextProps) { + this.setState({ + general: {} + }) + } + shouldComponentUpdate (nextProps, nextState) { + return !shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state) + } + render () { + const messages = getComponentMessages('ProjectSettings') + const { project, editDisabled, updateProjectSettings, deleteProject } = this.props + const noEdits = Object.keys(this.state.general).length === 0 && this.state.general.constructor === Object + const autoFetchChecked = typeof this.state.general.autoFetchFeeds !== 'undefined' ? this.state.general.autoFetchFeeds : project.autoFetchFeeds + const DEFAULT_FETCH_TIME = moment().startOf('day').add(2, 'hours') + return ( +
    + {getMessage(messages, 'title')}}> + + + + Organization name + + { + this.setState({name: evt.target.value}) + }} + /> + + + + + + + + + {getMessage(messages, 'general.updates.title')}}> + + + + { + const minutes = moment(DEFAULT_FETCH_TIME).minutes() + const hours = moment(DEFAULT_FETCH_TIME).hours() + const stateUpdate = { general: { $merge: { autoFetchFeeds: evt.target.checked, autoFetchMinute: minutes, autoFetchHour: hours } } } + this.setState(update(this.state, stateUpdate)) + }} + > + {getMessage(messages, 'general.updates.autoFetchFeeds')} + + {autoFetchChecked + ? { + const time = moment(+seconds) + const minutes = moment(time).minutes() + const hours = moment(time).hours() + const stateUpdate = { general: { $merge: { autoFetchMinute: minutes, autoFetchHour: hours } } } + this.setState(update(this.state, stateUpdate)) + }} + /> + : null + } + + + + + {getMessage(messages, 'general.location.title')}}> + + + + {getMessage(messages, 'general.location.defaultLocation')} + + { + const latLng = evt.target.value.split(',') + if (typeof latLng[0] !== 'undefined' && typeof latLng[1] !== 'undefined') { + const stateUpdate = { general: { $merge: {defaultLocationLat: latLng[0], defaultLocationLon: latLng[1]} } } + this.setState(update(this.state, stateUpdate)) + } else { + console.log('invalid value for latlng') + } + }} + /> + + + + + + + + + {getMessage(messages, 'general.location.boundingBox')} + + { + const bBox = evt.target.value.split(',') + if (bBox.length === 4) { + const stateUpdate = { general: { $merge: {west: bBox[0], south: bBox[1], east: bBox[2], north: bBox[3]} } } + this.setState(update(this.state, stateUpdate)) + } + }} + /> + { + + + + } + + + + + {getMessage(messages, 'general.location.defaultTimeZone')} + { + const stateUpdate = { general: { $merge: { defaultTimeZone: option.value } } } + this.setState(update(this.state, stateUpdate)) + }} + /> + + + {getMessage(messages, 'general.location.defaultLanguage')} + { + const stateUpdate = { general: { $merge: { defaultLanguage: option.value } } } + this.setState(update(this.state, stateUpdate)) + }} + /> + + + + Danger zone}> + + + +

    Delete this organization.

    +

    Once you delete an organization, the organization and all feed sources it contains cannot be recovered.

    +
    +
    +
    + + + {/* Save button */} + + + +
    + ) + } +} diff --git a/lib/manager/components/HomeProjectDropdown.js b/lib/manager/components/HomeProjectDropdown.js new file mode 100644 index 000000000..a21084cef --- /dev/null +++ b/lib/manager/components/HomeProjectDropdown.js @@ -0,0 +1,66 @@ +import React, {Component, PropTypes} from 'react' +import {Icon} from '@conveyal/woonerf' +import { Button, DropdownButton, MenuItem } from 'react-bootstrap' +import { LinkContainer } from 'react-router-bootstrap' + +export default class HomeProjectDropdown extends Component { + static propTypes = { + activeProject: PropTypes.object + } + render () { + const { + activeProject, + user, + visibleProjects + } = this.props + const isAdmin = user.permissions.isApplicationAdmin() + return ( +
    + {activeProject + ? + + + : null + } + {activeProject.name} + : {user.email} {user.profile.nickname} + } + // onSelect={(eventKey) => { + // setActiveProject(eventKey) + // }} + > + {activeProject && ( + + + {user.email} {user.profile.nickname} + + + )} + {activeProject && } + {visibleProjects.length > 0 + ? visibleProjects.map((project, index) => { + if (activeProject && project.id === activeProject.id) { + return null + } + return ( + + + {project.name} + + + ) + }) + : null + } + {activeProject && visibleProjects.length > 1 || !activeProject ? : null} + {isAdmin && Manage projects} + {isAdmin && } + {isAdmin && Create project} + +
    + ) + } +} diff --git a/lib/manager/components/ManagerHeader.js b/lib/manager/components/ManagerHeader.js new file mode 100644 index 000000000..f00e34f9e --- /dev/null +++ b/lib/manager/components/ManagerHeader.js @@ -0,0 +1,89 @@ +import React, {Component, PropTypes} from 'react' +import {Icon} from '@conveyal/woonerf' +import { Row, Col, Button, ButtonToolbar } from 'react-bootstrap' +import { Link } from 'react-router' +import moment from 'moment' +import numeral from 'numeral' + +import WatchButton from '../../common/containers/WatchButton' +import StarButton from '../../common/containers/StarButton' +import { getConfigProperty } from '../../common/util/config' + +export default class ManagerHeader extends Component { + static propTypes = { + feedSource: PropTypes.object, + user: PropTypes.object + } + getAverageFileSize (feedVersions) { + let sum = 0 + let avg + if (feedVersions) { + for (var i = 0; i < feedVersions.length; i++) { + sum += feedVersions[i].fileSize + } + avg = sum / feedVersions.length + } + return numeral(avg || 0).format('0 b') + } + render () { + const { feedSource, project, user } = this.props + const isWatchingFeed = user.subscriptions.hasFeedSubscription(project.id, feedSource.id, 'feed-updated') + const dateFormat = getConfigProperty('application.date_format') + return ( + {/* Title + Shortcut Buttons Row */} + +

    + + {this.props.project.name} + {' '}/{' '} + {feedSource.name}{' '} + {feedSource.isPublic ? null : } + {' '} + {feedSource.editedSinceSnapshot + ? + : + } + + {getConfigProperty('application.dev') && + + } + + {getConfigProperty('application.dev') && + + } + +

    +
      +
    • {feedSource.lastUpdated ? moment(feedSource.lastUpdated).format(dateFormat) : 'n/a'}
    • +
    • {feedSource.url ? feedSource.url : '(none)'} +
    • + {
    • {this.getAverageFileSize(feedSource.feedVersions)}
    • } +
    + {/*
  • {feedSource.feedVersionCount}
  • {feedSource.url} */} + +
    + ) + } +} diff --git a/src/main/client/manager/components/NotesViewer.js b/lib/manager/components/NotesViewer.js similarity index 56% rename from src/main/client/manager/components/NotesViewer.js rename to lib/manager/components/NotesViewer.js index 0121f48bd..15415a88e 100644 --- a/src/main/client/manager/components/NotesViewer.js +++ b/lib/manager/components/NotesViewer.js @@ -1,7 +1,6 @@ import React, {Component, PropTypes} from 'react' import moment from 'moment' import gravatar from 'gravatar' -import ReactDOM from 'react-dom' import { Panel, Row, Col, Glyphicon, FormControl, Button, ButtonToolbar, Media } from 'react-bootstrap' import WatchButton from '../../common/containers/WatchButton' @@ -31,65 +30,74 @@ export default class NotesViewer extends Component { this.props.notesRequested(this.props.feedSource) } render () { + const { + user, + feedSource, + stacked, + version, + notesRequested, + notes, + newNotePosted + } = this.props const messages = getComponentMessages('NotesViewer') - const userLink = this.props.user ? {this.props.user.profile.email} : 'Unknown user' - const isWatchingComments = this.props.feedSource - ? this.props.user.subscriptions.hasFeedSubscription(this.props.feedSource.projectId, this.props.feedSource.id, 'feed-commented-on') + const userLink = user ? {user.profile.email} : 'Unknown user' + const isWatchingComments = feedSource + ? user.subscriptions.hasFeedSubscription(feedSource.projectId, feedSource.id, 'feed-commented-on') : false return ( - +

    {getMessage(messages, 'all')}

    - {this.props.notes && this.props.notes.length > 0 - ? this.props.notes.map(note => { - return ( - - - {note.userEmail}/ - - - - {this.props.user.profile.email} commented {moment(note.date).fromNow()} - - } - > + {notes && notes.length > 0 + ? notes.map(note => { + return ( + + + {note.userEmail} + + + + {user.profile.email} commented {moment(note.date).fromNow()} + + } + >

    {note.body || '(no content)'}

    -
    -
    -
    - ) - }) +
    +
    +
    + ) + }) :

    {getMessage(messages, 'none')}

    } - +

    {getMessage(messages, 'postComment')}

    - - + + {user.email} {userLink}}> @@ -106,7 +114,7 @@ export default class NotesViewer extends Component { style={{marginTop: '10px'}} disabled={this.state.value === ''} onClick={() => { - this.props.newNotePosted({ + newNotePosted({ body: this.state.value }) this.setState({value: ''}) diff --git a/src/main/client/manager/components/NotesViewerPanel.js b/lib/manager/components/NotesViewerPanel.js similarity index 100% rename from src/main/client/manager/components/NotesViewerPanel.js rename to lib/manager/components/NotesViewerPanel.js diff --git a/lib/manager/components/ProjectSettings.js b/lib/manager/components/ProjectSettings.js new file mode 100644 index 000000000..2e901ddd1 --- /dev/null +++ b/lib/manager/components/ProjectSettings.js @@ -0,0 +1,56 @@ +import React, {Component, PropTypes} from 'react' +import { LinkContainer } from 'react-router-bootstrap' + +import { Row, Col, Panel, ListGroup, ListGroupItem } from 'react-bootstrap' +import GeneralSettings from './GeneralSettings' +import DeploymentSettings from './DeploymentSettings' +import { isModuleEnabled, getComponentMessages, getMessage } from '../../common/util/config' +import MapModal from '../../common/components/MapModal.js' + +export default class ProjectSettings extends Component { + static propTypes = { + project: PropTypes.object, + projectEditDisabled: PropTypes.bool, + updateProjectSettings: PropTypes.func, + deleteProject: PropTypes.func + } + render () { + const { + project, + projectEditDisabled, + activeSettingsPanel, + updateProjectSettings, + deleteProject + } = this.props + const messages = getComponentMessages('ProjectSettings') + const activePanel = !activeSettingsPanel + ? + : + return ( + + + + + {getMessage(messages, 'general.title')} + {isModuleEnabled('deployment') + ? {getMessage(messages, 'deployment.title')} + : null + } + + + + + {activePanel} + + + + ) + } +} diff --git a/lib/manager/components/ProjectViewer.js b/lib/manager/components/ProjectViewer.js new file mode 100644 index 000000000..7c6a09239 --- /dev/null +++ b/lib/manager/components/ProjectViewer.js @@ -0,0 +1,324 @@ +import React, {Component, PropTypes} from 'react' +import Helmet from 'react-helmet' +import { Tabs, Tab, Grid, Row, Col, Button, InputGroup, ListGroup, ListGroupItem, FormControl, Glyphicon, ButtonToolbar, Panel, DropdownButton, MenuItem } from 'react-bootstrap' +import { sentence as toSentenceCase } from 'change-case' +import {Icon} from '@conveyal/woonerf' +import { browserHistory, Link } from 'react-router' +import { shallowEqual } from 'react-pure-render' + +import ManagerPage from '../../common/components/ManagerPage' +import Breadcrumbs from '../../common/components/Breadcrumbs' +import WatchButton from '../../common/containers/WatchButton' +import ProjectSettings from './ProjectSettings' +import DeploymentsPanel from './DeploymentsPanel' +import FeedSourceTable from './FeedSourceTable' +import ThirdPartySyncButton from './ThirdPartySyncButton' +import { defaultSorter } from '../../common/util/util' +import { isModuleEnabled, isExtensionEnabled, getComponentMessages, getMessage, getConfigProperty } from '../../common/util/config' + +export default class ProjectViewer extends Component { + static propTypes = { + project: PropTypes.object, + onComponentMount: PropTypes.func, + + visibilityFilter: PropTypes.object, + visibilityFilterChanged: PropTypes.func, + searchTextChanged: PropTypes.func + } + constructor (props) { + super(props) + + this.state = {} + } + + deleteFeedSource (feedSource) { + this.refs['page'].showConfirmModal({ + title: 'Delete Feed Source?', + body: `Are you sure you want to delete the feed source ${feedSource.name}?`, + onConfirm: () => { + console.log('OK, deleting') + this.props.deleteFeedSourceConfirmed(feedSource) + } + }) + } + _selectTab (key) { + if (key === 'sources') { + browserHistory.push(`/project/${this.props.project.id}/`) + } else { + browserHistory.push(`/project/${this.props.project.id}/${key}`) + } + if (key === 'deployments' && !this.props.project.deployments) { + this.props.deploymentsRequested() + } + } + showUploadFeedModal (feedSource) { + this.refs.page.showSelectFileModal({ + title: 'Upload Feed', + body: 'Select a GTFS feed to upload:', + onConfirm: (files) => { + const nameArray = files[0].name.split('.') + if (files[0].type !== 'application/zip' || nameArray[nameArray.length - 1] !== 'zip') { + return false + } else { + this.props.uploadFeedClicked(feedSource, files[0]) + return true + } + }, + errorMessage: 'Uploaded file must be a valid zip file (.zip).' + }) + } + shouldComponentUpdate (newProps) { + if (!shallowEqual(newProps, this.props)) { + return true + } else { + return false + } + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + render () { + const { + project, + user, + visibilityFilter, + visibilityFilterChanged, + searchTextChanged, + onNewFeedSourceClick, + thirdPartySync, + updateAllFeeds, + downloadMergedFeed, + activeComponent, + deployPublic, + activeSubComponent + } = this.props + if (!project) { + return + } + const messages = getComponentMessages('ProjectViewer') + const publicFeedsLink = `https://s3.amazonaws.com/${getConfigProperty('application.data.gtfs_s3_bucket')}/public/index.html` + const isWatchingProject = user.subscriptions.hasProjectSubscription(project.id, 'project-updated') + const projectEditDisabled = !user.permissions.isProjectAdmin(project.id, project.organizationId) + const filteredFeedSources = project.feedSources + ? project.feedSources.filter(feedSource => { + if (feedSource.isCreating) return true // feeds actively being created are always visible + const visible = feedSource.name !== null ? feedSource.name.toLowerCase().indexOf((visibilityFilter.searchText || '').toLowerCase()) !== -1 : '[unnamed project]' + switch (visibilityFilter.filter) { + case 'ALL': + return visible + case 'STARRED': + return [].indexOf(feedSource.id) !== -1 // check userMetaData + case 'PUBLIC': + return feedSource.isPublic + case 'PRIVATE': + return !feedSource.isPublic + default: + return visible + } + }).sort(defaultSorter) + : [] + const projectsHeader = ( + + + + { + visibilityFilterChanged(key) + }} + > + All + Starred + Public + Private + + searchTextChanged(evt.target.value)} + /> + + + + {!projectEditDisabled && + + } + + {isExtensionEnabled('transitland') || isExtensionEnabled('transitfeeds') || isExtensionEnabled('mtc') + ? + : null + } + + + + + + ) + return ( + + } + > + + + + +

    + {project.name} + + {getConfigProperty('application.notifications_enabled') + ? + : null + } + +

    +
      +
    • {project.defaultLocationLon ? `${project.defaultLocationLat}, ${project.defaultLocationLon}` : 'n/a'}
    • +
    • {project.autoFetchFeeds ? `${project.autoFetchHour}:${project.autoFetchMinute < 10 ? '0' + project.autoFetchMinute : project.autoFetchMinute}` : 'Auto fetch disabled'}
    • + {/* +
    • {fs.feedVersions ? `${this.getAverageFileSize(fs.feedVersions)} MB` : 'n/a'}
    • + */} +
    + +
    + this._selectTab(key)} + > + + + {getMessage(messages, 'feeds.title')} + + } + > + + + + + + + + {isModuleEnabled('enterprise') && !projectEditDisabled && +
    + +

    + Note: Public feeds page can be viewed here. +

    +
    + } + + What is a feed source?}> + A feed source defines the location or upstream source of a GTFS feed. GTFS can be populated via automatic fetch, directly editing or uploading a zip file. + + +
    +
    + {isModuleEnabled('deployment') + ? {getMessage(messages, 'deployments')}} + > + + + : null + } + {getMessage(messages, 'settings')}} + > + + +
    +
    +
    + ) + } +} + +class ProjectSummaryPanel extends Component { + render () { + const { project, feedSources } = this.props + const errorCount = feedSources.map(fs => fs.latestValidation ? fs.latestValidation.errorCount : 0).reduce((a, b) => a + b, 0) + const serviceSeconds = feedSources.map(fs => fs.latestValidation ? fs.latestValidation.avgDailyRevenueTime : 0).reduce((a, b) => a + b, 0) + return ( + {project.name} summary}> + + Number of feeds: {feedSources.length} + Total errors: {errorCount} + Total service: {Math.floor(serviceSeconds / 60 / 60 * 100) / 100} hours per weekday + + + ) + } +} diff --git a/lib/manager/components/ProjectsList.js b/lib/manager/components/ProjectsList.js new file mode 100644 index 000000000..23cd3c02f --- /dev/null +++ b/lib/manager/components/ProjectsList.js @@ -0,0 +1,134 @@ +import React, {Component, PropTypes} from 'react' +import Helmet from 'react-helmet' +import { Grid, Row, Col, Button, Table, FormControl, Panel, OverlayTrigger, Popover, Glyphicon } from 'react-bootstrap' + +import ManagerPage from '../../common/components/ManagerPage' +import EditableTextField from '../../common/components/EditableTextField' +import defaultSorter from '../../common/util/util' +import { getComponentMessages, getMessage } from '../../common/util/config' + +export default class ProjectsList extends Component { + static propTypes = { + projects: PropTypes.array, + user: PropTypes.object, + visibilitySearchText: PropTypes.object, + onNewProjectClick: PropTypes.func, + searchTextChanged: PropTypes.func, + saveProject: PropTypes.func, + projectNameChanged: PropTypes.func + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + _newProjectClicked = () => { + const project = {} + if (this.props.user.permissions.getOrganizationId()) { + project.organizationId = this.props.user.permissions.getOrganizationId() + } + this.props.onNewProjectClick(project) + } + render () { + const { + projects, + user, + visibilitySearchText, + searchTextChanged, + saveProject, + projectNameChanged + } = this.props + if (!projects) { + return + } + const messages = getComponentMessages('ProjectsList') + const projectCreationDisabled = !user.permissions.isApplicationAdmin() && !user.permissions.canAdministerAnOrganization() + const visibleProjects = projects.filter((project) => { + if (project.isCreating) return true // projects actively being created are always visible + return project.name.toLowerCase().indexOf((visibilitySearchText || '').toLowerCase()) !== -1 + }).sort(defaultSorter) + + return ( + + + + Projects)}> + + + searchTextChanged(evt.target.value)} + /> + + + + {getMessage(messages, 'help.content')}}> + + + + + + + + + + + + + + {visibleProjects.length > 0 ? visibleProjects.map((project) => { + const disabled = !user.permissions.isProjectAdmin(project.id, project.organizationId) + return ( + + + + ) + }) + : + + + } + +
    {getMessage(messages, 'table.name')} +
    +
    + { + const proj = {name} + if (project.organizationId) proj.organizationId = project.organizationId + if (project.isCreating) saveProject(proj) + else projectNameChanged(project, name) + }} + link={`/project/${project.id}`} + /> +
    +
    +
    + {getMessage(messages, 'noProjects')} + {' '} + +
    + +
    +
    +
    +
    + ) + } +} diff --git a/lib/manager/components/RecentActivity.js b/lib/manager/components/RecentActivity.js new file mode 100644 index 000000000..4251cb471 --- /dev/null +++ b/lib/manager/components/RecentActivity.js @@ -0,0 +1,59 @@ +import React, {Component, PropTypes} from 'react' +import moment from 'moment' +import {Icon} from '@conveyal/woonerf' +import { Link } from 'react-router' + +import { getProfileLink } from '../../common/util/util' + +export default class RecentActivity extends Component { + static propTypes = { + item: PropTypes.object + } + render () { + const { item } = this.props + const containerStyle = { + marginTop: 10, + paddingBottom: 12, + borderBottom: '1px solid #ddd' + } + + const iconStyle = { + float: 'left', + fontSize: 20, + color: '#bbb' + } + + const dateStyle = { + color: '#999', + fontSize: 11, + marginBottom: 2 + } + + const innerContainerStyle = { + marginLeft: 36 + } + + const commentStyle = { + backgroundColor: '#f0f0f0', + marginTop: 8, + padding: 8, + fontSize: 12 + } + + switch (item.type) { + case 'feed-commented-on': + return ( +
    +
    + +
    +
    +
    {moment(item.date).fromNow()}
    +
    {item.userName} commented on feed {item.targetName}:
    +
    {item.body}
    +
    +
    + ) + } + } +} diff --git a/lib/manager/components/ThirdPartySyncButton.js b/lib/manager/components/ThirdPartySyncButton.js new file mode 100644 index 000000000..6bdbe0ebb --- /dev/null +++ b/lib/manager/components/ThirdPartySyncButton.js @@ -0,0 +1,58 @@ +import React, {Component} from 'react' +import { DropdownButton, MenuItem, Glyphicon } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' + +import { isExtensionEnabled } from '../../common/util/config' + +export default class ThirdPartySyncButton extends Component { + render () { + const { projectEditDisabled, thirdPartySync } = this.props + return ( + Sync}> + {isExtensionEnabled('transitland') + ? { + thirdPartySync('TRANSITLAND') + }} + > + transit.land + + : null + } + {isExtensionEnabled('transitfeeds') + ? { + thirdPartySync('TRANSITFEEDS') + }} + > + transitfeeds.com + + : null + } + {isExtensionEnabled('mtc') + ? { + thirdPartySync('MTC') + }} + > + MTC + + : null + } + + ) + } +} diff --git a/lib/manager/components/UserAccountInfoPanel.js b/lib/manager/components/UserAccountInfoPanel.js new file mode 100644 index 000000000..d7e1c72f8 --- /dev/null +++ b/lib/manager/components/UserAccountInfoPanel.js @@ -0,0 +1,72 @@ +import React, {Component, PropTypes} from 'react' +import {Icon} from '@conveyal/woonerf' +import { Panel, Badge, Button, Row, Col, ButtonToolbar } from 'react-bootstrap' +import { LinkContainer } from 'react-router-bootstrap' + +export default class UserAccountInfoPanel extends Component { + static propTypes = { + user: PropTypes.object, + logoutHandler: PropTypes.func + } + render () { + const { + user, + logoutHandler + } = this.props + // const userOrganization = user.permissions.getOrganizationId() + return ( + + + +

    + + Hello, {user.profile.nickname}. +

    + +
    + + + + + +
    {user.profile.email}
    +
    + + {user.permissions.isApplicationAdmin() + ? 'Application admin' + : user.permissions.canAdministerAnOrganization() + ? 'Organization admin' + : 'Standard user' + } + + {/* TODO: fetch organization for user and show badge here */} + {' '} + {/* userOrganization && + + user.permissions.getOrganizationId() + + */} +
    +
    + + + + + {user.permissions.isApplicationAdmin() || user.permissions.canAdministerAnOrganization() + ? + + + : null + } + +
    + +
    +
    + ) + } +} diff --git a/lib/manager/components/UserHomePage.js b/lib/manager/components/UserHomePage.js new file mode 100644 index 000000000..638e26da8 --- /dev/null +++ b/lib/manager/components/UserHomePage.js @@ -0,0 +1,119 @@ +import React, {Component, PropTypes} from 'react' +import { Grid, Row, Col, Button, ButtonToolbar, Jumbotron } from 'react-bootstrap' +import {Icon} from '@conveyal/woonerf' +import { LinkContainer } from 'react-router-bootstrap' +import objectPath from 'object-path' + +import ManagerPage from '../../common/components/ManagerPage' +import RecentActivity from './RecentActivity' +import UserAccountInfoPanel from './UserAccountInfoPanel' +import FeedSourcePanel from './FeedSourcePanel' +import HomeProjectDropdown from './HomeProjectDropdown' +import { getConfigProperty } from '../../common/util/config' +import { defaultSorter } from '../../common/util/util' + +export default class UserHomePage extends Component { + + static propTypes = { + user: PropTypes.object, + projects: PropTypes.array, + project: PropTypes.object, + + onComponentMount: PropTypes.func, + logoutHandler: PropTypes.func, + fetchProjectFeeds: PropTypes.func, + + visibilityFilter: PropTypes.object, + searchTextChanged: PropTypes.func, + visibilityFilterChanged: PropTypes.func + } + constructor (props) { + super(props) + this.state = {} + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + componentWillReceiveProps (nextProps) { + const nextId = objectPath.get(nextProps, 'project.id') + const id = objectPath.get(this.props, 'project.id') + if (nextId && nextId !== id && !nextProps.project.feedSources) { + this.props.fetchProjectFeeds(nextProps.project.id) + } + } + componentWillUnmount () { + this.setState({showLoading: true}) + } + render () { + const { + projects, + project, + user, + logoutHandler, + // setActiveProject, + visibilityFilter, + searchTextChanged, + visibilityFilterChanged + } = this.props + // const projectCreationDisabled = !this.props.user.permissions.isApplicationAdmin() + const visibleProjects = projects.sort(defaultSorter) + const activeProject = project + const sortByDate = (a, b) => { + if (a.date < b.date) return 1 + if (a.date > b.date) return -1 + return 0 + } + + return ( + + + {this.state.showLoading ? : null} + + + {/* Top Welcome Box */} + +

    Welcome to {getConfigProperty('application.title')}!

    +

    Manage, edit, validate and deploy your data in a streamlined workflow.

    +

    + + + + +

    +
    + {/* Recent Activity List */} +

    + Recent Activity +

    + {user.recentActivity && user.recentActivity.length + ? user.recentActivity.sort(sortByDate).map(item => { + return + }) + : No Recent Activity for your subscriptions. + } + + + + + + +
    +
    +
    + ) + } +} diff --git a/lib/manager/components/reporter/components/DateTimeFilter.js b/lib/manager/components/reporter/components/DateTimeFilter.js new file mode 100644 index 000000000..441e5542f --- /dev/null +++ b/lib/manager/components/reporter/components/DateTimeFilter.js @@ -0,0 +1,100 @@ +import React, { Component, PropTypes } from 'react' +import { Row, Col, FormControl, Alert } from 'react-bootstrap' +import moment from 'moment' +import DateTimeField from 'react-bootstrap-datetimepicker' + +const timeOptions = [ + { + label: 'Morning peak (6am - 9am)', + from: 60 * 60 * 6, + to: 60 * 60 * 9 + }, + { + label: 'Midday peak (11am - 2pm)', + from: 60 * 60 * 11, + to: 60 * 60 * 14 + }, + { + label: 'Afternoon peak (4pm - 7pm)', + from: 60 * 60 * 16, + to: 60 * 60 * 19 + }, + { + label: 'Evening service (7pm - 10pm)', + from: 60 * 60 * 19, + to: 60 * 60 * 22 + }, + { + label: '24 hours (12am - 11:59pm)', + from: 0, + to: 60 * 60 * 24 - 1 // 86399 + } +] + +export default class DateTimeFilter extends Component { + static propTypes = { + onChange: PropTypes.func, + dateTime: PropTypes.object, + version: PropTypes.object, + + updateDateTimeFilter: PropTypes.func + } + render () { + const dateTime = this.props.dateTime || {date: null, from: null, to: null} + const dateTimeProps = { + mode: 'date', + dateTime: dateTime.date ? +moment(dateTime.date, 'YYYY-MM-DD') : +moment(), + onChange: (millis) => { + console.log(+millis) + const date = moment(+millis).format('YYYY-MM-DD') + console.log(date) + this.props.updateDateTimeFilter({date}) + this.props.onChange && this.props.onChange({date}) + } + } + if (!dateTime.date) { + dateTimeProps.defaultText = 'Please select a date' + } + const validDate = this.props.version && moment(dateTime.date).isBetween(this.props.version.validationSummary.startDate, this.props.version.validationSummary.endDate) + console.log(validDate, this.props, moment(dateTime.date).isBetween(this.props.version.validationSummary.startDate, this.props.version.validationSummary.endDate)) + return ( +
    + + + + + + { + const fromTo = evt.target.value.split('-') + const from = +fromTo[0] + const to = +fromTo[1] + this.props.updateDateTimeFilter({from, to}) + this.props.onChange && this.props.onChange({from, to}) + }} + > + {timeOptions.map((t, i) => { + return + })} + + + + {!validDate + ? + + + Warning! Chosen date is outside of valid date range for feed version. + + + + : null + } +
    + ) + } +} diff --git a/lib/manager/components/reporter/components/FeedLayout.js b/lib/manager/components/reporter/components/FeedLayout.js new file mode 100644 index 000000000..d9ea1c180 --- /dev/null +++ b/lib/manager/components/reporter/components/FeedLayout.js @@ -0,0 +1,46 @@ +import React, { PropTypes, Component } from 'react' +import { Alert } from 'react-bootstrap' +import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table' + +import Loading from '../../../../common/components/Loading' + +export default class FeedLayout extends Component { + + static propTypes = { + feed: PropTypes.object + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + + render () { + return ( + +
    + + {this.props.feed.fetchStatus.fetching && + + } + + {this.props.feed.fetchStatus.error && + + An error occurred while trying to fetch the data + + } + + {this.props.feed.fetchStatus.fetched && + + + Statistic + Value + + } + +
    + ) + } +} diff --git a/lib/manager/components/reporter/components/PageLayout.js b/lib/manager/components/reporter/components/PageLayout.js new file mode 100644 index 000000000..1ecfcfc4e --- /dev/null +++ b/lib/manager/components/reporter/components/PageLayout.js @@ -0,0 +1,41 @@ +import React from 'react' +import { Grid, PageHeader, Nav, NavItem } from 'react-bootstrap' + +import 'react-bootstrap-table/dist/react-bootstrap-table.min.css' +import 'react-select/dist/react-select.css' + +import Feed from '../containers/Feed' +import Patterns from '../containers/Patterns' +import Routes from '../containers/Routes' +import Stops from '../containers/Stops' + +export default class PageLayout extends React.Component { + + render () { + console.log(this.props) + return ( + + Reports + + {this.props.activeTab === 'feed' && + + } + {this.props.activeTab === 'routes' && + + } + {this.props.activeTab === 'patterns' && + + } + {this.props.activeTab === 'stop' && + + } + + ) + } +} diff --git a/lib/manager/components/reporter/components/PatternLayout.js b/lib/manager/components/reporter/components/PatternLayout.js new file mode 100644 index 000000000..26726f2da --- /dev/null +++ b/lib/manager/components/reporter/components/PatternLayout.js @@ -0,0 +1,102 @@ +import React, { Component, PropTypes } from 'react' +import { Row, Col, Alert, Button } from 'react-bootstrap' +import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table' +import Select from 'react-select' +import clone from 'clone' + +import { formatHeadway, formatSpeed } from '../../../../gtfs/util/stats' +import ActiveDateTimeFilter from '../containers/ActiveDateTimeFilter' +import Loading from '../../../../common/components/Loading' + +export default class PatternLayout extends Component { + static propTypes = { + onComponentMount: PropTypes.func, + patternRouteFilterChange: PropTypes.func, + patternDateTimeFilterChange: PropTypes.func, + + version: PropTypes.object, + routes: PropTypes.object, + patterns: PropTypes.object, + tableOptions: PropTypes.object + } + componentWillMount () { + this.props.onComponentMount(this.props) + } + render () { + const self = this + const rows = this.props.patterns.data.map(p => { + const flat = clone(p) + if (p.stats) { + delete flat.stats + for (const key in p.stats) { + flat[key] = p.stats[key] + } + } + return flat + }) + return ( +
    + + {this.props.routes.fetchStatus.fetched && +
    + + + + + + {this.props.stops.routeFilter && + + + this.handlePaste(evt)} - style={{ - width: '100%', - height: '100%' - }} - autoFocus='true' - onKeyDown={(evt) => this.handleKeyDown(evt)} - onChange={(evt) => this.handleChange(evt)} - onBlur={(evt) => this.cancel(evt)} - /> - ) - } - else { - cellHtml = -
    - {this.cellRenderer(this.state.data)} -
    - } - return ( - { - this.handleClick(evt) - }} - > - {cellHtml} - - ) - } -} diff --git a/src/main/client/common/components/JobMonitor.js b/src/main/client/common/components/JobMonitor.js deleted file mode 100644 index e231390cd..000000000 --- a/src/main/client/common/components/JobMonitor.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, { PropTypes, Component } from 'react' -import { ProgressBar, Button } from 'react-bootstrap' -import { Icon } from 'react-fa' -import truncate from 'truncate' - -import SidebarPopover from './SidebarPopover' - -export default class JobMonitor extends Component { - - static propTypes = { - expanded: PropTypes.bool, - jobMonitor: PropTypes.object, - target: PropTypes.object, - visible: PropTypes.func, - close: PropTypes.func, - removeRetiredJob: PropTypes.func - } - - render () { - const jobContainerStyle = { - marginBottom: 20 - } - - const progressBarStyle = { - marginBottom: 2 - } - - const statusMessageStyle = { - fontSize: '12px', - color: 'darkGray' - } - - return ( - - {this.props.jobMonitor.retired.map(job => { - return ( -
    -
    - {job.status && job.status.error - ? - : - } -
    -
    -
    - - {truncate(job.name, 25)} -
    -
    {job.status && job.status.error ? 'Error!' : 'Completed!'}
    -
    -
    - ) - })} - {this.props.jobMonitor.jobs.length - ? this.props.jobMonitor.jobs.map(job => { - return ( -
    -
    - -
    -
    -
    - {job.name}{/* */} -
    - -
    {job.status ? job.status.message : 'waiting'}
    -
    -
    - ) - }) - :

    No active jobs.

    - } -
    - ) - } -} diff --git a/src/main/client/common/components/LanguageSelect.js b/src/main/client/common/components/LanguageSelect.js deleted file mode 100644 index 4bd9140ea..000000000 --- a/src/main/client/common/components/LanguageSelect.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { PropTypes } from 'react' -import fetch from 'isomorphic-fetch' -import { Glyphicon, Label, FormControl } from 'react-bootstrap' -import { PureComponent, shallowEqual } from 'react-pure-render' -import Select from 'react-select' - -import languages from '../util/languages' -import { getComponentMessages, getMessage } from '../util/config' - -export default class LanguageSelect extends React.Component { - constructor(props) { - super(props) - this.state = { - value: this.props.value - }; - } - - cacheOptions (options) { - options.forEach(o => { - this.options[o.value] = o.feature - }) - } - - componentWillReceiveProps (nextProps) { - if (!shallowEqual(nextProps.value, this.props.value)) { - this.setState({value: nextProps.value}) - console.log('props received', this.state.value) - } - } - renderOption (option) { - return {option.region ? : } {option.label} {option.link} - } - onChange (value) { - this.setState({value}) - } - render() { - // console.log('render search feeds', this.props.feeds) - const messages = getComponentMessages('LanguageSelect') - - const options = languages.map(lang => ({value: lang.code, label: lang.name})) - const handleChange = (input) => { - this.onChange(input) - this.props.onChange && this.props.onChange(input) - } - - const onFocus = (input) => { - // clear options to onFocus to ensure only valid route/stop combinations are selected - // this.refs.gtfsSelect.loadOptions('') - } - - const placeholder = getMessage(messages, 'placeholder') - return ( - { - let props = {} - let val = input ? input.value : null - props[field.name] = val - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - filterOptions={(options, filter, values) => { - // Filter already selected values - let valueKeys = values && values.map(i => i.value) - let filteredOptions = options.filter(option => { - return valueKeys ? valueKeys.indexOf(option.value) === -1 : [] - }) - - // Filter by label - if (filter !== undefined && filter != null && filter.length > 0) { - filteredOptions = filteredOptions.filter(option => { - return RegExp(filter, 'ig').test(option.label) - }) - } - - // Append Addition option - if (filteredOptions.length === 0) { - filteredOptions.push({ - label: Create new zone: {filter}, - value: filter, - create: true - }) - } - - return filteredOptions - }} - options={zoneOptions} - /> - - ) - case 'TIMEZONE': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[field.name] = option.value - this.setState({[editorField]: option.value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'LANGUAGE': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[field.name] = option.value - this.setState({[editorField]: option.value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'TIME': - return ( - - {basicLabel} - { - let props = {} - props[field.name] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'LATITUDE': - case 'LONGITUDE': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[field.name] = evt.target.value - // this.setState({[editorField]: evt.target.value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'NUMBER': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[field.name] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'DATE': - let defaultValue = /end/.test(editorField) ? +moment().startOf('day').add(3, 'months') : +moment().startOf('day') - let dateTimeProps = { - mode: 'date', - dateTime: currentValue ? +moment(currentValue) : defaultValue, - onChange: (millis) => { - let props = {} - props[field.name] = +millis - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - } - } - if (!currentValue) { - dateTimeProps.defaultText = 'Please select a date' - } - return ( - - {basicLabel} -
    - -
    -
    - ) - case 'COLOR': - const hexColor = currentValue !== null ? `#${currentValue}` : '#000000' - const colorStyle = { - width: '36px', - height: '20px', - borderRadius: '2px', - background: hexColor - } - const wrapper = { - position: 'inherit', - zIndex: '100' - } - return ( - - {basicLabel} - { -
    this.handleClick(editorField)}> -
    -
    - } - {this.state[editorField] - ?
    -
    this.handleClose(editorField) }/> -
    - { - let props = {} - props[field.name] = color.hex.split('#')[1] - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - // onChangeComplete={(value) => { - // let props = {} - // props[field.name] = evt.target.value - // this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - // }} - /> -
    -
    - : null - } - - ) - case 'POSITIVE_INT': - return ( - - {basicLabel} - { - let props = {} - props[field.name] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'POSITIVE_NUM': - return ( - - {basicLabel} - { - let props = {} - props[field.name] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'DAY_OF_WEEK_BOOLEAN': - return ( - [ - - {editorField === 'monday' - ?
    Days of service
    : null - } -
    , - - { - console.log(evt.target.checked) - let props = {} - currentValue = evt.target.checked ? 1 : 0 - props[field.name] = currentValue - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - > - {toSentenceCase(editorField.substr(0, 3))} - - {' '} - - ] - ) - case 'DROPDOWN': - // isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - // if (isNotValid) { - // validationErrors.push({field: field.name, invalid: isNotValid}) - // } - return ( - - {basicLabel} - { - let props = {} - props[field.name] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - > - {field.options.map(option => { - return - })} - - - ) - case 'GTFS_ROUTE': - const routeEntity = this.props.getGtfsEntity('route', currentValue) - - const routeValue = routeEntity - ? { 'value': routeEntity.route_id, - 'label': routeEntity.route_short_name - ? `${routeEntity.route_short_name} - ${routeEntity.route_long_name}` - : routeEntity.route_long_name - } - : '' - return ( - { - this.props.fieldEdited(table.id, row, editorField, evt.route.route_id) - this.props.gtfsEntitySelected('route', evt.route) - }} - value={routeValue} - /> - ) - case 'GTFS_AGENCY': - const agency = this.props.tableData.agency && this.props.tableData.agency.find(a => a.id === currentValue) - return ( - - {basicLabel} - { - console.log(input) - let rules = [...this.props.activeEntity.fareRules] - rules[index].zone_id = input ? input.value : null - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, {fareRules: rules}) - }} - options={zoneOptions} - /> - : rule.origin_id || rule.destination_id - ? [ - { - console.log(input) - let rules = [...this.props.activeEntity.fareRules] - rules[index].destination_id = input ? input.value : null - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, {fareRules: rules}) - }} - options={zoneOptions} - /> - ] - : null - } - - ) - })} -
    - : null - let dateMap = {} - let allExceptions = [] - if (this.props.tableData.scheduleexception) { - allExceptions = [...this.props.tableData.scheduleexception] - } - if (this.props.activeEntity) { - let exceptionIndex = allExceptions.findIndex(se => se.id === this.props.activeEntity.id) - if (exceptionIndex !== -1) { - allExceptions.splice(exceptionIndex, 1) - } - allExceptions.push(this.props.activeEntity) - } - for (let i = 0; i < allExceptions.length; i++) { - allExceptions[i].dates && allExceptions[i].dates.map(d => { - if (typeof dateMap[moment(d).format('YYYYMMDD')] === 'undefined') { - dateMap[moment(d).format('YYYYMMDD')] = [] - } - dateMap[moment(d).format('YYYYMMDD')].push(allExceptions[i].id) - }) - } - const scheduleExceptionForm = this.props.activeComponent === 'scheduleexception' - ?
    -
    - - Exception name - { - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, {name: evt.target.value}) - }} - /> - - - Run the following schedule: - { - // let props = {} - // props[field.name] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, {exemplar: evt.target.value, customSchedule: null}) - }} - > - { - exemplars.map(exemplar => { - return ( - - ) - }) - } - - - {this.props.activeEntity && this.props.activeEntity.exemplar === 'CUSTOM' - ? - Select calendar to run: - { - console.log(input) - let val = input ? input.map(i => i.value) : null - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, {addedService: val}) - }} - options={this.props.tableData.calendar - ? this.props.tableData.calendar - .filter(cal => !this.props.activeEntity.removedService || this.props.activeEntity.removedService.indexOf(cal.id) === -1) - .map(calendar => { - return { - value: calendar.id, - label: calendar.description, - calendar - } - }) - : [] - } - /> - Select calendars to remove: - { - let props = {} - let val = input ? input.value : null - props[editorField] = val - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - - }} - filterOptions={(options, filter, values) => { - // Filter already selected values - let valueKeys = values && values.map(i => i.value) - let filteredOptions = options.filter(option => { - return valueKeys ? valueKeys.indexOf(option.value) === -1 : [] - }) - - // Filter by label - if (filter !== undefined && filter != null && filter.length > 0) { - filteredOptions = filteredOptions.filter(option => { - return RegExp(filter, 'ig').test(option.label) - }) - } - - // Append Addition option - if (filteredOptions.length == 0) { - filteredOptions.push({ - label: Create new zone: {filter}, - value: filter, - create: true - }) - } - - return filteredOptions - }} - options={this.props.zoneOptions} - /> - - ) - case 'TIMEZONE': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[editorField] = option.value - this.setState({[editorField]: option.value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'LANGUAGE': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[editorField] = option.value - this.setState({[editorField]: option.value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'TIME': - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'LATITUDE': - case 'LONGITUDE': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - // this.setState({[editorField]: evt.target.value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'NUMBER': - isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - if (isNotValid) { - validationErrors.push({field: field.name, invalid: isNotValid}) - } - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'DATE': - let defaultValue = /end/.test(editorField) ? +moment().startOf('day').add(3, 'months') : +moment().startOf('day') - let dateTimeProps = { - mode: 'date', - dateTime: currentValue ? +moment(currentValue) : defaultValue, - onChange: (millis) => { - let date = moment(+millis) - let props = {} - props[editorField] = +millis - // this.setState({[editorField]: +millis}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - } - } - if (!currentValue) { - dateTimeProps.defaultText = 'Please select a date' - } - return ( - - {basicLabel} -
    - -
    -
    - ) - case 'COLOR': - const popover = { - position: 'absolute', - zIndex: '2', - } - const cover = { - position: 'fixed', - top: '0px', - right: '0px', - bottom: '0px', - left: '0px', - } - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - { - //
    - //
    - //
    - } - { - // this.state.displayColorPicker ?
    - //
    - // { - // let props = {} - // props[editorField] = evt.target.value - // // this.setState({[editorField]: evt.target.value}) - // this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - // }} - // /> - //
    : null - } - { - // - } - { - // this.state.displayColorPicker - // ?
    - //
    - // - //
    - // : null - } - - ) - case 'POSITIVE_INT': - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'POSITIVE_NUM': - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - /> - - ) - case 'DAY_OF_WEEK_BOOLEAN': - return ([ - - {editorField === 'monday' - ?
    Days of service
    : null - } -
    - , - - { - console.log(evt.target.checked) - let props = {} - currentValue = evt.target.checked ? 1 : 0 - props[editorField] = value - this.setState({[editorField]: value}) - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - > - {toSentenceCase(editorField.substr(0, 3))} - - {' '} - ] - ) - case 'DROPDOWN': - // isNotValid = field.required && (currentValue === null || typeof currentValue === 'undefined') - // if (isNotValid) { - // validationErrors.push({field: field.name, invalid: isNotValid}) - // } - return ( - - {basicLabel} - { - let props = {} - props[editorField] = evt.target.value - this.props.updateActiveEntity(this.props.activeEntity, this.props.activeComponent, props) - }} - > - {field.options.map(option => { - return - })} - - - ) - case 'GTFS_ROUTE': - const routeEntity = this.props.getGtfsEntity('route', currentValue) - - const routeValue = routeEntity - ? { 'value': routeEntity.route_id, - 'label': routeEntity.route_short_name - ? `${routeEntity.route_short_name} - ${routeEntity.route_long_name}` - : routeEntity.route_long_name - } - : '' - return ( - { - this.props.fieldEdited(table.id, row, editorField, evt.route.route_id) - this.props.gtfsEntitySelected('route', evt.route) - }} - value={routeValue} - /> - ) - case 'GTFS_AGENCY': - const agency = this.props.getGtfsEntity('agency', currentValue) - return ( - - {basicLabel} - { - let selected = [] - if (this.state.selected.length !== this.state.data.length) { - for (let i = 0; i < this.state.data.length; i++) { - selected.push(i) - } - } - this.setState({selected}) - }} - /> - - {columns.map(c => { - if (!c.name) return null - return ( - - {c.name} - - ) - })} - - - - {this.state.data - ? this.state.data.map((row, rowIndex) => { - let rowValues = [] - let rowCheckedColor = '#F3FAF6' - let rowIsChecked = this.state.selected[0] === '*' && this.state.selected.indexOf(rowIndex) === -1 || this.state.selected[0] !== '*' && this.state.selected.indexOf(rowIndex) !== -1 - return ( - - - { - this.toggleRowSelection(rowIndex) - }} - /> - - {columns.map((col, colIndex) => { - let val = objectPath.get(row, col.key) - if (col.key === 'gtfsTripId' && val === null) { - val = objectPath.get(row, 'id') !== 'new' ? objectPath.get(row, 'id') : null - } - rowValues.push(val) - let cellStyle = { - width: '60px', - color: col.type === 'DEPARTURE_TIME' ? '#aaa' : '#000' - } - if (rowIsChecked) { - cellStyle.backgroundColor = rowCheckedColor - } - let previousValue = rowValues[colIndex - 1] - - // if departure times are hidden do not display cell - if (col.hidden) return null - - return ( - { - this.setCellValue(value, rowIndex, `${rowIndex}.${col.key}`) - - // set departure time value if departure times are hidden - if (this.state.hideDepartureTimes && columns[colIndex + 1] && columns[colIndex + 1].type === 'DEPARTURE_TIME') { - this.setCellValue(value, rowIndex, `${rowIndex}.${columns[colIndex + 1].key}`) - } - }} - key={`cell-${rowIndex}-${colIndex}`} - onRowSelect={(evt) => this.toggleRowSelection(rowIndex)} - onLeft={(evt) => this._onLeft(evt, rowIndex, colIndex)} - onRight={(evt) => this._onRight(evt, rowIndex, colIndex, columns)} - onUp={(evt) => this._onUp(evt, rowIndex, colIndex)} - onDown={(evt) => this._onDown(evt, rowIndex, colIndex)} - duplicateLeft={(evt) => this.setCellValue(previousValue, rowIndex, `${rowIndex}.${col.key}`)} - handlePastedRows={(rows) => this.handlePastedRows(rows, rowIndex, colIndex, columns)} - invalidData={this.isTimeFormat(col.type) && val >= 0 && val < previousValue} - isEditing={this.state.activeCell === `${rowIndex}-${colIndex}` } - isFocused={false} - placeholder={col.placeholder} - renderTime={this.isTimeFormat(col.type)} - cellRenderer={(value) => this.getCellRenderer(col, value)} - data={val} - style={cellStyle} - /> - ) - })} - - ) - }) - : null - } - - - :

    - Choose a calendar to edit timetables or - {' '} - { - e.preventDefault() - this.props.setActiveEntity(feedSource.id, 'calendar', {id: 'new'}) - }} - >create a new one. -

    - } -
    {/* End timetable body */} -
    - ) - } -} diff --git a/src/main/client/editor/components/TripPatternList.js b/src/main/client/editor/components/TripPatternList.js deleted file mode 100644 index f34ee7722..000000000 --- a/src/main/client/editor/components/TripPatternList.js +++ /dev/null @@ -1,574 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import { Table, Button, ButtonGroup, Checkbox, DropdownButton, MenuItem, ButtonToolbar, Collapse, FormGroup, OverlayTrigger, Tooltip, InputGroup, Form, FormControl, ControlLabel } from 'react-bootstrap' -import {Icon} from 'react-fa' - -import EditableTextField from '../../common/components/EditableTextField' -import { getConfigProperty } from '../../common/util/config' -import PatternStopContainer from './PatternStopContainer' -import MinuteSecondInput from './MinuteSecondInput' - -import { getEntityName } from '../util/gtfs' -import VirtualizedEntitySelect from './VirtualizedEntitySelect' -import { polyline as getPolyline, getSegment } from '../../scenario-editor/utils/valhalla' -import ll from 'lonlng' - -const DEFAULT_SPEED = 20 // km/hr - -export default class TripPatternList extends Component { - static propTypes = { - stops: PropTypes.array, - updateActiveEntity: PropTypes.func.isRequired, - saveActiveEntity: PropTypes.func.isRequired, - toggleEditSetting: PropTypes.func.isRequired, - resetActiveEntity: PropTypes.func.isRequired, - editSettings: PropTypes.object, - entity: PropTypes.object, - activeEntity: PropTypes.object, - feedSource: PropTypes.object, - activeComponent: PropTypes.string.isRequired, - activeSubEntity: PropTypes.string, - currentPattern: PropTypes.object - } - constructor (props) { - super(props) - this.state = { - // avgSpeed: 20, // km/hr - // dwellTime: 0 // seconds - } - } - async extendPatternToStop (pattern, endPoint, stop) { - let newShape = await getPolyline([endPoint, stop]) - if (newShape) { - this.props.updateActiveEntity(pattern, 'trippattern', {shape: {type: 'LineString', coordinates: [...pattern.shape.coordinates, ...newShape]}}) - this.props.saveActiveEntity('trippattern') - return true - } else { - this.props.updateActiveEntity(pattern, 'trippattern', {shape: {type: 'LineString', coordinates: [...pattern.shape.coordinates, ll.toCoordinates(stop)]}}) - this.props.saveActiveEntity('trippattern') - return false - } - } - componentWillReceiveProps (nextProps) { - } - shouldComponentUpdate (nextProps) { - return true - } - async drawPatternFromStops (pattern, stops) { - let newShape = await getPolyline(stops) - console.log(newShape) - this.props.updateActiveEntity(pattern, 'trippattern', {shape: {type: 'LineString', coordinates: newShape}}) - this.props.saveActiveEntity('trippattern') - return true - } - calculateDefaultTimes (pattern, speed = DEFAULT_SPEED, dwellTime = 0) { - console.log(speed, dwellTime) - if (!speed) { - speed = DEFAULT_SPEED - } - let patternStops = [...pattern.patternStops] - let convertedSpeed = speed * 1000 / 60 / 60 // km/hr -> m/s - // let cumulativeTravelTime = 0 - for (var i = 0; i < patternStops.length; i++) { - patternStops[i].defaultDwellTime = dwellTime - patternStops[i].defaultTravelTime = patternStops[i].shapeDistTraveled / convertedSpeed - // cumulativeTravelTime += dwellTime + - } - this.props.updateActiveEntity(pattern, 'trippattern', {patternStops}) - this.props.saveActiveEntity('trippattern') - } - render () { - const { feedSource, activeEntity } = this.props - const activePatternId = this.props.activeSubEntity - if (!activeEntity.tripPatterns) { - return - } - const activePattern = this.props.currentPattern // activeEntity.tripPatterns.find(p => p.id === activePatternId) - const sidePadding = '5px' - let panelWidth = '300px' - let panelStyle = { - width: panelWidth, - height: '85%', - position: 'absolute', - left: '0px', - overflowY: 'scroll', - zIndex: 99, - // backgroundColor: 'white', - paddingRight: '0px', - paddingLeft: sidePadding - } - const activeColor = '#fff' - const patternList = ( -
    - - - - {activeEntity.tripPatterns ? activeEntity.tripPatterns.map(pattern => { - const rowStyle = { - paddingTop: 5, - paddingBottom: 5 - } - const activeRowStyle = { - backgroundColor: activeColor, - paddingTop: 5, - paddingBottom: 5 - } - const cardStyle = { - border: '1px dashed gray', - padding: '0.5rem 0.5rem', - marginBottom: '.5rem', - backgroundColor: '#f2f2f2', - cursor: 'pointer' - } - const isActive = activePatternId && pattern.id === activePatternId - const timetableOptions = [ - Use timetables, - Use frequencies - ] - const showTimetable = isActive && this.props.subSubComponent === 'timetable' - const patternName = `${`${pattern.name.length > 35 ? pattern.name.substr(0, 35) + '...' : pattern.name}`} ${pattern.patternStops ? `(${pattern.patternStops.length} stops)` : ''}` - return ( - - - - ) - }) - : - } - -
    -

    { - if (isActive) this.props.setActiveEntity(feedSource.id, 'route', activeEntity, 'trippattern') - else this.props.setActiveEntity(feedSource.id, 'route', activeEntity, 'trippattern', pattern) - }} - > - - {' '} - {pattern.name ? patternName : '[Unnamed]'} -

    - - {isActive - ?
    - { - let props = {} - props.name = value - this.props.updateActiveEntity(pattern, 'trippattern', props) - this.props.saveActiveEntity('trippattern') - }} - /> -
    -

    - Pattern Shape -

    - - {this.props.editSettings.editGeometry - ? [ - - , - - , - - ] - : [ - - , - - , - - ] - } - {this.props.editSettings.editGeometry - ? - this.props.toggleEditSetting('followStreets')}> - Snap to streets - - this.props.toggleEditSetting('snapToStops')}> - Snap to stops - - this.props.toggleEditSetting('hideStops')}> - Show stops - - - : null - } - -
    -

    Schedules

    - - { - let useFrequency = key !== 'timetables' - let other = key === 'timetables' ? 'frequencies' : 'timetables' - this.props.showConfirmModal({ - title: `Use ${key} for ${activePattern.name}?`, - body: `Are you sure you want to use ${key} for this trip pattern? Any trips created using ${other} will be lost.`, - onConfirm: () => { - console.log('use ' + key) - this.props.updateActiveEntity(activePattern, 'trippattern', {useFrequency}) - this.props.saveActiveEntity('trippattern') - } - }) - }} - title={activePattern.useFrequency ? timetableOptions[1] : timetableOptions[0]} id='frequency-dropdown'> - {activePattern.useFrequency ? timetableOptions[0] : timetableOptions[1]} - - - -
    -

    - - - - {this.props.editSettings.addStops && this.props.mapState.zoom <= 14 - ? Zoom to view stops - : null - } - Stops -

    - {/* List of pattern stops */} -
    -

    Stop sequence

    -
    -
    -

    Travel time

    -
    -
    - - {/* Add stop selector */} - {this.props.editSettings.addStops - ?
    - { - let patternStops = [...activePattern.patternStops] - let stop = input.entity - let coordinates = activePattern.shape && activePattern.shape.coordinates - let newStop = {stopId: stop.id, defaultDwellTime: 0, defaultTravelTime: 0} - // if adding stop to end - if (typeof index === 'undefined') { - // if shape coordinates already exist, just extend them - if (coordinates) { - let endPoint = ll.toLatlng(coordinates[coordinates.length - 1]) - this.extendPatternToStop(activePattern, endPoint, {lng: stop.stop_lon, lat: stop.stop_lat}) - .then(() => { - patternStops.push(newStop) - this.props.updateActiveEntity(activePattern, 'trippattern', {patternStops: patternStops}) - this.props.saveActiveEntity('trippattern') - }) - } - // if shape coordinates do not exist, add pattern stop and get shape between stops (if multiple stops exist) - else { - patternStops.push(newStop) - if (patternStops.length > 1) { - let previousStop = this.props.stops.find(s => s.id === patternStops[patternStops.length - 2].stopId) - getSegment([[previousStop.stop_lon, previousStop.stop_lat], [stop.stop_lon, stop.stop_lat]], this.props.editSettings.followStreets) - .then(geojson => { - this.props.updateActiveEntity(activePattern, 'trippattern', {patternStops: patternStops, shape: {type: 'LineString', coordinates: geojson.coordinates}}) - this.props.saveActiveEntity('trippattern') - }) - } - else { - this.props.updateActiveEntity(activePattern, 'trippattern', {patternStops: patternStops}) - this.props.saveActiveEntity('trippattern') - } - } - - // if not following roads - // updateActiveEntity(pattern, 'trippattern', {patternStops: patternStops, shape: {type: 'LineString', coordinates: coordinates}}) - } - // if adding stop in middle - else { - // patternStops.splice(index, 0, newStop) - // updateActiveEntity(activePattern, 'trippattern', {patternStops: patternStops}) - // saveActiveEntity('trippattern') - } - // TODO: add strategy for stop at beginning - }} - /> -
    - -
    -
    - :
    -

    { - this.props.toggleEditSetting('addStops') - }} - className='small' - > - Add stop -

    -
    - } - - - Dwell time - { - this.setState({dwellTime: value}) - }} - /> - - {' '} - - { - this.setState({speed: evt.target.value}) - }} - /> - - - - - -
    - :
    - } -
    -
    -
    - ) - - const activeTable = getConfigProperty('modules.editor.spec') - .find(t => t.id === 'route') - - return ( -
    -
    -
    -

    - - Reverse trip pattern}> - - - Duplicate trip pattern}> - - - Delete trip pattern}> - - - - -

    -
    - {patternList} -
    -
    - ) - } -} diff --git a/src/main/client/editor/containers/ActiveEntityList.js b/src/main/client/editor/containers/ActiveEntityList.js deleted file mode 100644 index 85b9d918b..000000000 --- a/src/main/client/editor/containers/ActiveEntityList.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux' -import { - setActiveGtfsEntity, - newGtfsEntity, - saveActiveGtfsEntity, - deleteGtfsEntity, - getGtfsTable, -} from '../actions/editor' -import { getEntityName } from '../util/gtfs' - -import EntityList from '../components/EntityList' - -const mapStateToProps = (state, ownProps) => { - const entity = - state.editor.active && state.editor.active.entity && state.editor.active.entity.id === ownProps.activeEntityId - ? state.editor.active.entity - : state.editor.active && state.editor.active.entity && ownProps.activeComponent === 'feedinfo' - ? state.editor.active.entity - : null - let activeEntity = entity - ? { - name: getEntityName(ownProps.activeComponent, entity), - id: entity.id - } - : null - return { - activeEntity - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { return { } } - -const ActiveEntityList = connect(mapStateToProps, mapDispatchToProps)(EntityList) - -export default ActiveEntityList diff --git a/src/main/client/editor/containers/ActiveGtfsEditor.js b/src/main/client/editor/containers/ActiveGtfsEditor.js deleted file mode 100644 index 71876d7f1..000000000 --- a/src/main/client/editor/containers/ActiveGtfsEditor.js +++ /dev/null @@ -1,315 +0,0 @@ -import { connect } from 'react-redux' - -import GtfsEditor from '../components/GtfsEditor' -import { componentList } from '../util/gtfs' -import { fetchFeedSourceAndProject, fetchFeedVersion } from '../../manager/actions/feeds' -import { - fetchFeedInfo -} from '../actions/feedInfo' -import { - fetchStops, - fetchStopsForTripPattern, -} from '../actions/stop' -import { - fetchRoutes -} from '../actions/route' -import { - fetchTripPatterns, - fetchTripPatternsForRoute, - undoActiveTripPatternEdits, - updateControlPoint, - addControlPoint, - removeControlPoint, -} from '../actions/tripPattern' -import { - fetchTripsForCalendar, - saveTripsForCalendar, - deleteTripsForCalendar, -} from '../actions/trip' -import { - setActiveGtfsEntity, - newGtfsEntity, - cloneGtfsEntity, - toggleEditSetting, - updateMapSetting, - saveActiveGtfsEntity, - resetActiveGtfsEntity, - deleteGtfsEntity, - settingActiveGtfsEntity, - updateActiveGtfsEntity, - clearGtfsContent, - addGtfsRow, - updateGtfsField, - deleteGtfsRow, - saveGtfsRow, - getGtfsTable, - uploadGtfsFeed, - downloadGtfsFeed, - importGtfsFromGtfs, - loadGtfsEntities, - receiveGtfsEntities, - uploadBrandingAsset -} from '../actions/editor' -import { updateUserMetadata } from '../../manager/actions/user' - -const mapStateToProps = (state, ownProps) => { - const feedSourceId = ownProps.routeParams.feedSourceId // location.pathname.split('/')[2] - const activeComponent = ownProps.routeParams.subpage // location.pathname.split('/')[4] - const subComponent = ownProps.routeParams.subsubpage // location.pathname.split('/')[5] - const subSubComponent = ownProps.routeParams.subsubcomponent // location.pathname.split('/')[6] - const activeEntityId = ownProps.routeParams.entity // location.pathname.split('/')[7] - const activeSubEntity = ownProps.routeParams.subentity // location.pathname.split('/')[8] - const activeSubSubEntity = ownProps.routeParams.subsubentity // location.pathname.split('/')[9] - const activeEntity = - state.editor.active && state.editor.active.entity && state.editor.active.entity.id === activeEntityId - ? state.editor.active.entity - : state.editor.active && state.editor.active.entity && activeComponent === 'feedinfo' - ? state.editor.active.entity - : null - const currentPattern = state.editor.active && state.editor.active.subEntity - // const activeSubEntity = state.editor.active && state.editor.active.subEntity && state.editor.active.subEntity.id === activeEntityId - // ? state.editor.active.subEntity - // : state.editor.active && state.editor.active.subEntity && activeComponent === 'feedinfo' - // ? state.editor.active.subEntity - // : null - // ownProps.routeParams.entity && state.editor.tableData[activeComponent] - // ? state.editor.tableData[activeComponent].find(e => ownProps.routeParams.entity === e.id) - // : null - const entityEdited = subComponent === 'trippattern' - ? state.editor.active.patternEdited - : state.editor.active && state.editor.active.edited - - const controlPoints = state.editor.editSettings.controlPoints && state.editor.editSettings.controlPoints.length - ? state.editor.editSettings.controlPoints[state.editor.editSettings.controlPoints.length - 1] - : [] - const editSettings = state.editor.editSettings - const mapState = state.editor.mapState - const stopTree = state.editor.stopTree - const tableView = ownProps.location.query && ownProps.location.query.table === 'true' - const entities = state.editor.tableData[activeComponent] - let user = state.user - - // find the containing project - let project = state.projects.all - ? state.projects.all.find(p => { - if (!p.feedSources) return false - return (p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1) - }) - : null - - let feedSource - if (project) { - feedSource = project.feedSources.find(fs => fs.id === feedSourceId) - } - - let feedInfo = state.editor.tableData.feedinfo - - return { - tableData: state.editor.tableData, - tripPatterns: state.editor.tripPatterns, - // gtfsEntityLookup: state.editor.gtfsEntityLookup, - // validation: state.editor.validation, - // currentTable: state.routing.locationBeforeTransitions.hash ? state.routing.locationBeforeTransitions.hash.split('#')[1] : 'agency', - feedSource, - entities, - feedSourceId, - feedInfo, - entityEdited, - tableView, - project, - user, - activeComponent, - subSubComponent, - subComponent, - activeEntity, - activeEntityId, - activeSubEntity, - currentPattern, - // activeSubEntityId, - activeSubSubEntity, - editSettings, - mapState, - stopTree, - controlPoints, - sidebarExpanded: state.ui.sidebarExpanded - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const feedSourceId = ownProps.routeParams.feedSourceId - const activeComponent = ownProps.routeParams.subpage - const subComponent = ownProps.routeParams.subsubpage - const subSubComponent = ownProps.routeParams.subsubcomponent - const activeEntityId = ownProps.routeParams.entity - const activeSubEntity = ownProps.routeParams.subentity - const activeSubSubEntity = ownProps.routeParams.subsubentity - - return { - updateUserMetadata: (profile, props) => { - dispatch(updateUserMetadata(profile, props)) - }, - onComponentMount: (initialProps) => { - const tablesToFetch = ['calendar', 'agency', 'route', 'stop'] - - // Get all GTFS tables except for the active table (activeComponent) - if (!initialProps.feedSource || feedSourceId !== initialProps.feedSource.id) { - dispatch(fetchFeedSourceAndProject(feedSourceId)) - .then(() => { - dispatch(fetchFeedInfo(feedSourceId)) - for (var i = 0; i < tablesToFetch.length; i++) { - if (tablesToFetch[i] !== activeComponent) { - dispatch(getGtfsTable(tablesToFetch[i], feedSourceId)) - } - } - }) - .then(() => { - if (componentList.indexOf(activeComponent) !== -1) { - dispatch(getGtfsTable(activeComponent, feedSourceId)) - //// FETCH trip patterns if route selected - .then((entities) => { - if (activeEntityId === 'new') { - dispatch(newGtfsEntity(feedSourceId, activeComponent)) - } - else if (activeEntityId && entities.findIndex(e => e.id === activeEntityId) === -1) { - console.log('bad entity id, going back to ' + activeComponent) - return dispatch(setActiveGtfsEntity(feedSourceId, activeComponent)) - } - dispatch(setActiveGtfsEntity(feedSourceId, activeComponent, activeEntityId, subComponent, activeSubEntity, subSubComponent, activeSubSubEntity)) - if (activeComponent === 'route' && activeEntityId) { - dispatch(fetchTripPatternsForRoute(feedSourceId, activeEntityId)) - .then((tripPatterns) => { - let pattern = tripPatterns && tripPatterns.find(p => p.id === activeSubEntity) - if (subSubComponent === 'timetable' && activeSubSubEntity) { - dispatch(fetchTripsForCalendar(feedSourceId, pattern, activeSubSubEntity)) - } - }) - } - }) - } - else { - dispatch(setActiveGtfsEntity(feedSourceId)) - } - }) - } else { - dispatch(fetchFeedInfo(feedSourceId)) - for (var i = 0; i < tablesToFetch.length; i++) { - if (tablesToFetch[i] !== activeComponent) { - dispatch(getGtfsTable(tablesToFetch[i], feedSourceId)) - } - } - } - - // Clear gtfs content if no active component - // if (!activeComponent) { - // dispatch(clearGtfsContent()) - // } - - // otherwise, get active table - // else { - // - // } - dispatch(fetchTripPatterns(feedSourceId)) - }, - onComponentUpdate: (prevProps, newProps) => { - // handle back button presses by re-setting active gtfs entity - if (prevProps.activeEntityId !== 'new' && - (prevProps.activeComponent !== newProps.activeComponent || - prevProps.activeEntityId !== newProps.activeEntityId || - prevProps.subComponent !== newProps.subComponent || - prevProps.activeSubEntity !== newProps.activeSubEntity || - prevProps.subSubComponent !== newProps.subSubComponent || - prevProps.activeSubSubEntity !== newProps.activeSubSubEntity) - ) { - console.log('handling back button') - dispatch(setActiveGtfsEntity(feedSourceId, activeComponent, activeEntityId, subComponent, activeSubEntity, subSubComponent, activeSubSubEntity)) - } - }, - newRowClicked: (tableId) => { - dispatch(addGtfsRow(tableId)) - }, - deleteRowClicked: (tableId, rowIndex) => { - dispatch(deleteGtfsRow(tableId, rowIndex)) - }, - getGtfsTable: (tableId, feedId) => { - dispatch(getGtfsTable(tableId, feedId)) - }, - saveRowClicked: (tableId, rowIndex, feedId) => { - dispatch(saveGtfsRow(tableId, rowIndex, feedId)) - }, - fieldEdited: (tableId, rowIndex, fieldName, newValue) => { - dispatch(updateGtfsField(tableId, rowIndex, fieldName, newValue)) - }, - newRowsDisplayed: (tableId, rows, feedSource) => { - if(feedSource) dispatch(loadGtfsEntities(tableId, rows, feedSource)) - }, - toggleEditSetting: (setting) => { - dispatch(toggleEditSetting(setting)) - }, - updateMapSetting: (props) => { - dispatch(updateMapSetting(props)) - }, - gtfsEntitySelected: (type, entity) => { - dispatch(receiveGtfsEntities([entity])) - }, - uploadBrandingAsset: (feedSourceId, entityId, component, file) => { - dispatch(uploadBrandingAsset(feedSourceId, entityId, component, file)) - }, - setActiveEntity: (feedSourceId, component, entity, subComponent, subEntity, subSubComponent, subSubEntity) => { - let entityId = entity && entity.id - let subEntityId = subEntity && subEntity.id - let subSubEntityId = subSubEntity && subSubEntity.id - dispatch(setActiveGtfsEntity(feedSourceId, component, entityId, subComponent, subEntityId, subSubComponent, subSubEntityId)) - }, - updateActiveEntity: (entity, component, props) => { - dispatch(updateActiveGtfsEntity(entity, component, props)) - }, - resetActiveEntity: (entity, component) => { - dispatch(resetActiveGtfsEntity(entity, component)) - }, - deleteEntity: (feedSourceId, component, entityId, routeId) => { - dispatch(deleteGtfsEntity(feedSourceId, component, entityId, routeId)) - }, - saveActiveEntity: (component) => { - return dispatch(saveActiveGtfsEntity(component)) - // .then(entity => { - // // dispatch(setActiveGtfsEntity(feedSourceId, component, entityId, subComponent, subEntityId, subSubComponent, subSubEntityId)) - // }) - }, - saveTripsForCalendar: (feedSourceId, pattern, calendarId, trips) => { - return dispatch(saveTripsForCalendar(feedSourceId, pattern, calendarId, trips)) - }, - deleteTripsForCalendar: (feedSourceId, pattern, calendarId, trips) => { - return dispatch(deleteTripsForCalendar(feedSourceId, pattern, calendarId, trips)) - }, - cloneEntity: (feedSourceId, component, entityId, save) => { - dispatch(cloneGtfsEntity(feedSourceId, component, entityId, save)) - }, - newEntityClicked: (feedSourceId, component, props, save) => { - dispatch(newGtfsEntity(feedSourceId, component, props, save)) - }, - clearGtfsContent: () => { dispatch(clearGtfsContent()) }, - undoActiveTripPatternEdits: () => { dispatch(undoActiveTripPatternEdits()) }, - addControlPoint: (controlPoint, index) => { dispatch(addControlPoint(controlPoint, index)) }, - removeControlPoint: (index) => { dispatch(removeControlPoint(index)) }, - updateControlPoint: (index, point, distance) => { dispatch(updateControlPoint(index, point, distance)) }, - fetchTripPatternsForRoute: (feedSourceId, routeId) => { - dispatch(fetchTripPatternsForRoute(feedSourceId, routeId)) - }, - fetchStopsForTripPattern: (feedSourceId, tripPatternId) => { - dispatch(fetchStopsForTripPattern(feedSourceId, tripPatternId)) - }, - fetchStops: (feedSourceId) => { - dispatch(fetchStops(feedSourceId)) - }, - fetchTripsForCalendar: (feedSourceId, pattern, calendarId) => { - dispatch(fetchTripsForCalendar(feedSourceId, pattern, calendarId)) - }, - } -} - -const ActiveGtfsEditor = connect( - mapStateToProps, - mapDispatchToProps -)(GtfsEditor) - -export default ActiveGtfsEditor diff --git a/src/main/client/editor/containers/ActiveGtfsTableEditor.js b/src/main/client/editor/containers/ActiveGtfsTableEditor.js deleted file mode 100644 index 188f75194..000000000 --- a/src/main/client/editor/containers/ActiveGtfsTableEditor.js +++ /dev/null @@ -1,92 +0,0 @@ -import { connect } from 'react-redux' - -import GtfsTableEditor from '../components/GtfsTableEditor' -import { fetchFeedSourceAndProject } from '../../manager/actions/feeds' -import { - addGtfsRow, - updateGtfsField, - deleteGtfsRow, - saveGtfsRow, - getGtfsTable, - uploadGtfsFeed, - downloadGtfsFeed, - importGtfsFromGtfs, - loadGtfsEntities, - receiveGtfsEntities -} from '../actions/editor' - -const mapStateToProps = (state, ownProps) => { - - let feedSourceId = ownProps.routeParams.feedSourceId - let user = state.user - // find the containing project - let project = state.projects.all - ? state.projects.all.find(p => { - if (!p.feedSources) return false - return (p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1) - }) - : null - - let feedSource - if (project) { - feedSource = project.feedSources.find(fs => fs.id === feedSourceId) - } - - return { - tableData: state.editor.tableData, - gtfsEntityLookup: state.editor.gtfsEntityLookup, - validation: state.editor.validation, - currentTable: state.routing.locationBeforeTransitions.hash ? state.routing.locationBeforeTransitions.hash.split('#')[1] : 'agency', - feedSource, - project, - user - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const feedSourceId = ownProps.routeParams.feedSourceId - const feedVersionId = ownProps.routeParams.feedVersionId - - return { - onComponentMount: (initialProps) => { - if(!initialProps.feedSource) dispatch(fetchFeedSourceAndProject(feedSourceId)) - if(!initialProps.tableData) dispatch(downloadGtfsFeed(feedVersionId)) - if (initialProps.currentTable) dispatch(getGtfsTable(initialProps.currentTable, feedSourceId)) - }, - newRowClicked: (tableId) => { - dispatch(addGtfsRow(tableId)) - }, - deleteRowClicked: (tableId, rowIndex) => { - dispatch(deleteGtfsRow(tableId, rowIndex)) - }, - getGtfsTable: (tableId, feedId) => { - dispatch(getGtfsTable(tableId, feedId)) - }, - saveRowClicked: (tableId, rowIndex, feedId) => { - dispatch(saveGtfsRow(tableId, rowIndex, feedId)) - }, - fieldEdited: (tableId, rowIndex, fieldName, newValue) => { - dispatch(updateGtfsField(tableId, rowIndex, fieldName, newValue)) - }, - feedSaved: (file) => { - dispatch(uploadGtfsFeed(feedVersionId, file)) - .then(() => { - console.log('re-downloading'); - dispatch(downloadGtfsFeed(feedVersionId)) - }) - }, - newRowsDisplayed: (tableId, rows, feedSource) => { - if(feedSource) dispatch(loadGtfsEntities(tableId, rows, feedSource)) - }, - gtfsEntitySelected: (type, entity) => { - dispatch(receiveGtfsEntities([entity])) - } - } -} - -const ActiveGtfsTableEditor = connect( - mapStateToProps, - mapDispatchToProps -)(GtfsTableEditor) - -export default ActiveGtfsTableEditor diff --git a/src/main/client/editor/reducers/editor.js b/src/main/client/editor/reducers/editor.js deleted file mode 100644 index cfbabb003..000000000 --- a/src/main/client/editor/reducers/editor.js +++ /dev/null @@ -1,851 +0,0 @@ -import update from 'react-addons-update' -import rbush from 'rbush' -import polyUtil from 'polyline-encoded' -import { getControlPoints, getEntityBounds } from '../util/gtfs' -import { latLngBounds } from 'leaflet' - -const mapStop = (s) => { - return { - // datatools props - id: s.id, - feedId: s.feedId, - bikeParking: s.bikeParking, - carParking: s.carParking, - pickupType: s.pickupType, - dropOffType: s.dropOffType, - - // gtfs spec props - stop_code: s.stopCode, - stop_name: s.stopName, - stop_desc: s.stopDesc, - stop_lat: s.lat, - stop_lon: s.lon, - zone_id: s.zoneId, - stop_url: s.stopUrl, - location_type: s.locationType, - parent_station: s.parentStation, - stop_timezone: s.stopTimezone, - wheelchair_boarding: s.wheelchairBoarding, - stop_id: s.gtfsStopId - } -} -const defaultState = { - feedSourceId: null, - active: {}, - editSettings: { - editGeometry: false, - followStreets: true, - snapToStops: true, - addStops: false, - hideStops: false, - controlPoints: [], - coordinatesHistory: [], - actions: [] - }, - mapState: { - zoom: null, - bounds: latLngBounds([[60, 60], [-60, -20]]), - target: null - }, - tableData: {}, - stopTree: null, - validation: null -} -const editor = (state = defaultState, action) => { - let stateUpdate, key, newTableData, fields, rowData, mappedEntities, activeEntity, activeSubEntity, newState, routeIndex, patternIndex, agencyIndex, fareIndex, calendarIndex, scheduleExceptionIndex, controlPoints, coordinates, mapState - switch (action.type) { - case 'REQUESTING_FEED_INFO': - if (state.feedSourceId && action.feedId !== state.feedSourceId) { - return defaultState - } - return state - case 'UPDATE_MAP_SETTING': - mapState = {...state.mapState} - for (key in action.props) { - mapState[key] = action.props[key] - } - if (!('target' in action.props)) { - mapState.target = null - } - return update(state, { - mapState: {$set: mapState} - }) - case 'TOGGLE_EDIT_SETTING': - if (action.setting === 'editGeometry' && !state.editSettings.editGeometry) { - controlPoints = getControlPoints(state.active.subEntity, state.editSettings.snapToStops) - return update(state, { - editSettings: { - [action.setting]: {$set: !state.editSettings[action.setting]}, - controlPoints: {$set: [controlPoints]} - } - }) - } else { - return update(state, { - editSettings: { - [action.setting]: {$set: !state.editSettings[action.setting]} - } - }) - } - case 'UNDO_TRIP_PATTERN_EDITS': - patternIndex = state.active.entity.tripPatterns.findIndex(p => p.id === state.active.subEntityId) - let lastActionIndex = state.editSettings.actions.length - 1 - let lastActionType = state.editSettings.actions[lastActionIndex] - let lastCoordinatesIndex = state.editSettings.coordinatesHistory.length - 1 - let lastControlPointsIndex = state.editSettings.controlPoints.length - 1 - stateUpdate = { - editSettings: { - // coordinatesHistory: {$splice: [[lastEditIndex, 1]]}, - // controlPoints: {$splice: [[lastEditIndex, 1]]}, - actions: {$splice: [[lastActionIndex, 1]]} - } - } - switch (lastActionType) { - case 'ADD_CONTROL_POINT': - stateUpdate.editSettings.controlPoints = {$splice: [[lastControlPointsIndex, 1]]} - break - case 'UPDATE_CONTROL_POINT': - stateUpdate.editSettings.controlPoints = {$splice: [[lastControlPointsIndex, 1]]} - stateUpdate.editSettings.coordinatesHistory = {$splice: [[lastCoordinatesIndex, 1]]} - coordinates = state.editSettings.coordinatesHistory[lastCoordinatesIndex] - if (coordinates) { - stateUpdate.active = { - subEntity: {shape: {coordinates: {$set: coordinates}}} - } - } - break - case 'REMOVE_CONTROL_POINT': - stateUpdate.editSettings.controlPoints = {$splice: [[lastControlPointsIndex, 1]]} - stateUpdate.editSettings.coordinatesHistory = {$splice: [[lastCoordinatesIndex, 1]]} - coordinates = state.editSettings.coordinatesHistory[lastCoordinatesIndex] - if (coordinates) { - stateUpdate.active = { - subEntity: {shape: {coordinates: {$set: coordinates}}} - } - } - break - } - return update(state, stateUpdate) - case 'ADD_CONTROL_POINT': - controlPoints = [...state.editSettings.controlPoints[state.editSettings.controlPoints.length - 1]] - controlPoints.splice(action.index, 0, action.controlPoint) - return update(state, { - editSettings: { - controlPoints: {$push: [controlPoints]}, - actions: {$push: [action.type]} - } - }) - case 'REMOVE_CONTROL_POINT': - controlPoints = [...state.editSettings.controlPoints[state.editSettings.controlPoints.length - 1]] - controlPoints.splice(action.index, 1) - return update(state, { - editSettings: { - controlPoints: {$push: [controlPoints]}, - actions: {$push: [action.type]} - } - }) - case 'UPDATE_CONTROL_POINT': - let newControlPoints = [] - controlPoints = state.editSettings.controlPoints[state.editSettings.controlPoints.length - 1] - for (var i = 0; i < controlPoints.length; i++) { - newControlPoints.push(Object.assign({}, controlPoints[i])) - } - let newest = update(newControlPoints, {[action.index]: {point: {$set: action.point}, distance: {$set: action.distance}}}) - return update(state, { - editSettings: { - controlPoints: {$push: [newest]}, - actions: {$push: [action.type]} - } - }) - case 'RECEIVED_ROUTES_SHAPEFILE': - return update(state, { - mapState: { - routesGeojson: {$set: action.geojson} - } - }) - case 'CREATE_GTFS_ENTITY': - if (action.component === 'trippattern') { - activeEntity = { - isCreating: true, - name: '', - id: 'new', - feedId: action.feedSourceId, - ...action.props - } - routeIndex = state.tableData.route.findIndex(r => r.id === action.props.routeId) - return update(newState || state, { - tableData: {route: {[routeIndex]: {tripPatterns: {$unshift: [activeEntity]}}}}, - active: { - entity: {tripPatterns: {$unshift: [activeEntity]}}, - // edited: {$set: typeof action.props !== 'undefined'} - } - }) - } - else { - activeEntity = { - isCreating: true, - name: '', - id: 'new', - feedId: action.feedSourceId, - ...action.props - } - // if tableData's component array is undefined, add it - if(!state.tableData[action.component]) { - newState = update(state, { - tableData: {[action.component]: {$set: []}} - }) - } - return update(newState || state, { - tableData: {[action.component]: {$unshift: [activeEntity]}}, - // active: { - // entity: {$set: activeEntity}, - // edited: {$set: typeof action.props !== 'undefined'} - // } - }) - } - case 'SETTING_ACTIVE_GTFS_ENTITY': - activeEntity = action.component === 'feedinfo' - ? Object.assign({}, state.tableData[action.component]) - : state.tableData[action.component] && action.entityId - ? Object.assign({}, state.tableData[action.component].find(e => e.id === action.entityId)) - : null - switch (action.subComponent) { - case 'trippattern': - activeSubEntity = activeEntity && activeEntity.tripPatterns ? Object.assign({}, activeEntity.tripPatterns.find(p => p.id === action.subEntityId)) : null - controlPoints = getControlPoints(activeSubEntity, state.editSettings.snapToStops) - coordinates = activeSubEntity && activeSubEntity.shape && activeSubEntity.shape.coordinates - break - } - let active = { - feedSourceId: action.feedSourceId, - entity: activeEntity, - entityId: action.entityId, - subEntity: activeSubEntity, - subEntityId: action.subEntityId, - component: action.component, - subComponent: action.subComponent, - subSubComponent: action.subSubComponent, - edited: activeEntity && activeEntity.id === 'new', - } - stateUpdate = { - editSettings: { - controlPoints: {$set: controlPoints}, - }, - active: {$set: active}, - } - if (coordinates) { - stateUpdate.coordinatesHistory = {$set: [coordinates]} - } - return update(state, stateUpdate) - case 'RESET_ACTIVE_GTFS_ENTITY': - switch (action.component) { - case 'trippattern': - routeIndex = state.tableData.route.findIndex(r => r.id === action.entity.routeId) - patternIndex = state.tableData.route[routeIndex].tripPatterns.findIndex(p => p.id === action.entity.id) - activeEntity = Object.assign({}, state.tableData.route[routeIndex].tripPatterns[patternIndex]) - return update(state, { - active: { - // entity: {tripPatterns: {[patternIndex]: {$set: activeEntity}}}, - subEntity: {$set: activeEntity}, - patternEdited: {$set: false} - } - }) - case 'feedinfo': - activeEntity = Object.assign({}, state.tableData[action.component]) - return update(state, { - active: { - entity: {$set: activeEntity}, - edited: {$set: false} - } - }) - default: - activeEntity = state.tableData[action.component].find(e => e.id === action.entity.id) - return update(state, { - active: { - entity: {$set: activeEntity}, - edited: {$set: false} - }, - }) - } - case 'SAVED_TRIP_PATTERN': - if (action.tripPattern.id == state.active.subEntityId) { - stateUpdate = { - active: { - patternEdited: {$set: false} - } - } - return update(state, stateUpdate) - } - case 'UPDATE_ACTIVE_GTFS_ENTITY': - switch (action.component) { - case 'trippattern': - patternIndex = state.active.entity.tripPatterns.findIndex(p => p.id === action.entity.id) - activeEntity = Object.assign({}, state.active.entity.tripPatterns[patternIndex]) - for (key in action.props) { - activeEntity[key] = action.props[key] - } - stateUpdate = { - active: { - // entity: {tripPatterns: {[patternIndex]: {$set: activeEntity}}}, - subEntity: {$set: activeEntity}, - patternEdited: {$set: true} - } - } - if (action.props && 'shape' in action.props) { - // add previous coordinates to history - // stateUpdate.editSettings = {coordinatesHistory: {$push: [action.props.shape.coordinates]}} - coordinates = state.active.entity.tripPatterns[patternIndex].shape && state.active.entity.tripPatterns[patternIndex].shape.coordinates - if (coordinates) - stateUpdate.editSettings = {coordinatesHistory: {$push: [state.active.entity.tripPatterns[patternIndex].shape.coordinates]}} - } - return update(state, stateUpdate) - // case 'feedinfo': - // activeEntity = Object.assign({}, state.active.entity) - // case 'timetable': - // activeEntity = Object.assign({}, state.active.entity) - // patternIndex = activeEntity.tripPatterns.findIndex(p => p.id === action.entity.id) - // for (key in action.props) { - // activeEntity.tripPatterns[patternIndex][key] = action.props[key] - // } - // return update(state, { - // active: {entity: {$set: activeEntity}}, - // edited: {$set: true} - // }) - default: - activeEntity = Object.assign({}, state.active.entity) - for (key in action.props) { - activeEntity[key] = action.props[key] - } - return update(state, { - active: { - entity: {$set: activeEntity}, - edited: {$set: true} - }, - }) - } - case 'RECEIVE_AGENCIES': - const agencies = action.agencies.map(ent => { - return { - // datatools props - id: ent.id, - feedId: ent.feedId, - agencyBrandingUrl: ent.agencyBrandingUrl, - - // gtfs spec props - agency_id: ent.agencyId, - agency_name: ent.name, - agency_url: ent.url, - agency_timezone: ent.timezone, - agency_lang: ent.lang, - agency_phone: ent.phone, - agency_fare_url: ent.agencyFareUrl, - agency_email: ent.email, - } - }) - agencyIndex = state.active.entity && action.agencies.findIndex(a => a.id === state.active.entity.id) - if (agencyIndex !== -1) { - return update(state, { - tableData: {agency: {$set: agencies}}, - active: { - entity: {$set: agencies[agencyIndex]}, - edited: {$set: false} - } - }) - } else { - return update(state, { - tableData: {agency: {$set: agencies}} - }) - } - case 'RECEIVE_FARES': - const fares = action.fares.map(fare => { - return { - // datatools props - id: fare.id, - feedId: fare.feedId, - description: fare.description, - fareRules: fare.fareRules, - - // gtfs spec props - fare_id: fare.gtfsFareId, - price: fare.price, - currency_type: fare.currencyType, - payment_method: fare.paymentMethod, - transfers: fare.transfers, - transfer_duration: fare.transferDuration, - } - }) - fareIndex = state.active.entity && action.fares.findIndex(f => f.id === state.active.entity.id) - if (fareIndex !== -1) { - return update(state, { - tableData: {fare: {$set: fares}}, - active: { - entity: {$set: fares[fareIndex]}, - edited: {$set: false} - } - }) - } else { - return update(state, { - tableData: {fare: {$set: fares}} - }) - } - case 'RECEIVE_FEED_INFO': - const feedInfo = action.feedInfo - ? { - // datatools props - id: action.feedInfo.id, - color: action.feedInfo.color, - defaultLat: action.feedInfo.defaultLat, - defaultLon: action.feedInfo.defaultLon, - defaultRouteType: action.feedInfo.defaultRouteType, - - // gtfs spec props - feed_end_date: action.feedInfo.feedEndDate, - feed_start_date: action.feedInfo.feedStartDate, - feed_lang: action.feedInfo.feedLang, - feed_publisher_name: action.feedInfo.feedPublisherName, - feed_publisher_url: action.feedInfo.feedPublisherUrl, - feed_version: action.feedInfo.feedVersion, - } - : null - let mapState = {...state.mapState} - if (feedInfo && feedInfo.defaultLon && feedInfo.defaultLat) { - mapState.bounds = getEntityBounds([feedInfo.defaultLon, feedInfo.defaultLat], 0.5) - mapState.target = feedInfo.id - } - - if (state.active.component === 'feedinfo') { - return update(state, { - tableData: {feedinfo: {$set: feedInfo}}, - active: { - entity: {$set: feedInfo}, - edited: {$set: false} - }, - mapState: {$set: mapState} - }) - } else { - return update(state, { - tableData: {feedinfo: {$set: feedInfo}}, - mapState: {$set: mapState} - }) - } - case 'RECEIVE_CALENDARS': - const calendars = action.calendars ? action.calendars.map(c => { - return { - // datatools props - id: c.id, - feedId: c.feedId, - description: c.description, - - // gtfs spec props - service_id: c.gtfsServiceId, - monday: c.monday ? 1 : 0, - tuesday: c.tuesday ? 1 : 0, - wednesday: c.wednesday ? 1 : 0, - thursday: c.thursday ? 1 : 0, - friday: c.friday ? 1 : 0, - saturday: c.saturday ? 1 : 0, - sunday: c.sunday ? 1 : 0, - start_date: c.startDate, - end_date: c.endDate, - } - }) : null - calendarIndex = state.active.entity && action.calendars.findIndex(c => c.id === state.active.entity.id) - if (calendarIndex !== -1) { - return update(state, { - tableData: {calendar: {$set: calendars}}, - active: { - entity: {$set: calendars[calendarIndex]}, - edited: {$set: false} - } - }) - } else { - return update(state, { - tableData: {calendar: {$set: calendars}} - }) - } - case 'RECEIVE_SCHEDULE_EXCEPTIONS': - const scheduleExceptions = action.scheduleExceptions ? action.scheduleExceptions.map(se => { - return { - // datatools props - id: se.id, - name: se.name, - feedId: se.feedId, - exemplar: se.exemplar, - dates: se.dates, - customSchedule: se.customSchedule, - addedService: se.addedService, - removedService: se.removedService - - // gtfs spec props - // gtfs_prop: se.gtfs_prop - } - }) : [] - scheduleExceptionIndex = state.active.entity && action.scheduleExceptions.findIndex(se => se.id === state.active.entity.id) - if (scheduleExceptionIndex !== -1) { - return update(state, { - tableData: {scheduleexception: {$set: scheduleExceptions}}, - active: { - entity: {$set: scheduleExceptions[scheduleExceptionIndex]}, - edited: {$set: false} - } - }) - } else { - return update(state, { - tableData: {scheduleexception: {$set: scheduleExceptions}} - }) - } - case 'RECEIVE_ROUTES': - // feedTableData = state.tableData[action.feedId] - // if (!feedTableData) - // feedTableData = {} - const routes = action.routes ? action.routes.map(r => { - return { - // datatools props - id: r.id, - feedId: r.feedId, - routeBrandingUrl: r.routeBrandingUrl, - publiclyVisible: r.publiclyVisible, - status: r.status, - - // gtfs spec props - agency_id: r.agencyId, - route_short_name: r.routeShortName, - route_long_name: r.routeLongName, - route_desc: r.routeDesc, - route_type: r.gtfsRouteType, - route_url: r.routeUrl, - route_color: r.routeColor, - route_text_color: r.routeTextColor, - route_id: r.gtfsRouteId - } - }) : [] - // feedTableData.route = routes - routeIndex = state.active.entity && action.routes.findIndex(r => r.id === state.active.entity.id) - if (routeIndex !== -1) { - let followStreets = routes[routeIndex] ? routes[routeIndex].route_type === 3 || routes[routeIndex].route_type === 0 : true - return update(state, { - tableData: {route: {$set: routes}}, - active: { - entity: {$set: routes[routeIndex]}, - edited: {$set: false} - }, - editSettings: { - followStreets: {$set: followStreets} - } - }) - } else { - return update(state, { - tableData: {route: {$set: routes}} - }) - } - case 'RECEIVE_TRIP_PATTERNS': - return update(state, { - tripPatterns: {$set: - Object.keys(action.tripPatterns).map(key => { - return { - id: key, - latLngs: action.tripPatterns[key].shape ? polyUtil.decode(action.tripPatterns[key].shape) : null - } - }) - } - }) - case 'RECEIVE_TRIP_PATTERNS_FOR_ROUTE': - routeIndex = state.tableData.route.findIndex(r => r.id === action.routeId) - let activePattern = state.active.subEntityId && action.tripPatterns.find(p => p.id === state.active.subEntityId) - if (routeIndex === -1) { - return state - } - // set controlPoints initially and then whenever isSnappingToStops changes - if (activePattern) { - controlPoints = getControlPoints(activePattern, state.editSettings.snapToStops) - } - else { - controlPoints = [] - } - if (state.active.entity.id === action.routeId) { - return update(state, { - tableData: {route: {[routeIndex]: {$merge: {tripPatterns: action.tripPatterns}}}}, - active: { - entity: {$merge: {tripPatterns: action.tripPatterns}}, - subEntity: {$set: Object.assign({}, activePattern)} - }, - editSettings: {controlPoints: {$set: [controlPoints]}} - }) - } else { - return update(state, { - tableData: {route: {[routeIndex]: {$merge: {tripPatterns: action.tripPatterns}}}} - }) - } - case 'RECEIVE_TRIPS_FOR_CALENDAR': - routeIndex = state.tableData.route.findIndex(r => r.id === action.pattern.routeId) - patternIndex = state.tableData.route[routeIndex].tripPatterns.findIndex(p => p.id === action.pattern.id) - if (state.active.entity.id === action.pattern.routeId) { - return update(state, { - tableData: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, - active: {entity: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}} - }) - } - else { - return update(state, { - tableData: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, - }) - } - // case 'DELETED_TRIPS_FOR_CALENDAR': - // routeIndex = state.tableData.route.findIndex(r => r.id === action.pattern.routeId) - // patternIndex = state.tableData.route[routeIndex].tripPatterns.findIndex(p => p.id === action.pattern.id) - // let tripIndex = state.tableData.route[routeIndex].tripPatterns[patternIndex][action.calendarId].findIndex(t => t.) - // if (state.active.entity.id === action.pattern.routeId) { - // return update(state, { - // tableData: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, - // active: {entity: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}} - // }) - // } - // else { - // return update(state, { - // tableData: {route: {[routeIndex]: {tripPatterns: {[patternIndex]: {$merge: {[action.calendarId]: action.trips}}}}}}, - // }) - // } - case 'RECEIVE_STOPS': - const stops = action.stops ? action.stops.map(mapStop) : [] - var tree = rbush(9, ['[0]', '[1]', '[0]', '[1]']) - tree.load(stops.map(s => ([s.stop_lon, s.stop_lat, s]))) - stopIndex = state.active.entity && action.stops.findIndex(s => s.id === state.active.entity.id) - if (stopIndex !== -1) { - return update(state, { - tableData: {stop: {$set: stops}}, - stopTree: {$set: tree}, - active: { - entity: {$set: stops[stopIndex]}, - edited: {$set: false} - } - }) - } else { - return update(state, { - tableData: {stop: {$set: stops}}, - stopTree: {$set: tree} - }) - } - case 'RECEIVE_STOP': - const stop = mapStop(action.stop) - console.log(stop) - let stopIndex = state.tableData.stop.findIndex(s => s.id === stop.id) - - // if stop is active, update active entity - if (stop.id === state.active.entityId && stopIndex !== -1) { - stateUpdate = { - tableData: {stop: {[stopIndex]: {$merge: stop}}}, - active: {entity: {$set: stop}}, - edited: {$set: false}, - } - } - else if (stopIndex === -1) { - stateUpdate = { - tableData: {stop: {$unshift: [stop]}}, - edited: {$set: false}, - } - } - else { - stateUpdate = { - tableData: {stop: {[stopIndex]: {$merge: stop}}}, - edited: {$set: false}, - } - } - return update(state, stateUpdate) - case 'CLEAR_GTFSEDITOR_CONTENT': - return defaultState - case 'RECEIVE_GTFSEDITOR_TABLE': - newTableData = {} - const getMappedEntities = (entities) => { - switch (action.tableId) { - case 'agency': - return action.entities.map(ent => { - return { - id: ent.id, - feedId: ent.feedId, - agency_id: ent.agencyId, - agency_name: ent.name, - agency_url: ent.url, - agency_timezone: ent.timezone, - agency_lang: ent.lang, - agency_phone: ent.phone, - agency_fare_url: ent.fare_url, - agency_email: ent.email, - } - }) - case 'route': - mappedEntities = action.entities.map(ent => { - return { - id: ent.id, - feedId: ent.feedId, - agency_id: ent.agencyId, - route_short_name: ent.routeShortName, - route_long_name: ent.routeLongName, - route_desc: ent.routeDesc, - route_type: ent.routeTypeId, - route_url: ent.routeUrl, - route_color: ent.routeColor, - route_text_color: ent.routeTextColor, - route_id: ent.gtfsRouteId - } - }) - return mappedEntities - case 'stop': - return action.entities.map(mapStop) - case 'calendar': - return action.entities.map(ent => { - return { - service_id: ent.gtfsServiceId, - monday: ent.monday, - tuesday: ent.tuesday, - wednesday: ent.wednesday, - thursday: ent.thursday, - friday: ent.friday, - saturday: ent.saturday, - sunday: ent.sunday, - start_date: ent.startDate, - end_date: ent.endDate, - description: ent.description, - routes: ent.routes, - id: ent.id, - feedId: ent.feedId, - numberOfTrips: ent.numberOfTrips - } - }) - case 'fare': - return action.entities.map(ent => { - return ent - // return { - // id: ent.id, - // stop_id: ent.gtfsStopId, - // stop_code: ent.stopCode, - // stop_name: ent.stopName, - // stop_desc: ent.stopDesc, - // stop_lat: ent.lat, - // stop_lon: ent.lon, - // zone_id: ent.zoneId, - // stop_url: ent.stopUrl, - // location_type: ent.locationType, - // parent_station: ent.parentStation, - // stop_timezone: ent.stopTimezone, - // wheelchair_boarding: ent.wheelchairBoarding, - // bikeParking: ent.bikeParking, - // carParking: ent.carParking, - // pickupType: ent.pickupType, - // dropOffType: ent.dropOffType, - // } - }) - default: - return action.entities - } - } - - newTableData[action.tableId] = getMappedEntities(action.entities) - - return update(state, { - tableData: {$merge: newTableData} - }) - case 'RECEIVE_GTFSEDITOR_CONTENT': - newTableData = {} - for(let i = 0; i < action.filenames.length; i++) { - const lines = action.fileContent[i].split('\n') - if(lines.length < 2) continue - fields = lines[0].split(',') - newTableData[action.filenames[i].split('.')[0]] = lines.slice(1) - .filter(line => line.split(',').length === fields.length) - .map((line, rowIndex) => { - const values = line.split(',') - rowData = { origRowIndex: rowIndex } - for(let f = 0; f < fields.length; f++) { - rowData[fields[f]] = values[f] - } - return rowData - }) - } - return update(state, { - feedSourceId: {$set: action.feedSourceId}, - tableData: {$set: newTableData} - }) - - case 'ADD_GTFSEDITOR_ROW': - // create this table if it doesn already exist - if(!(action.tableId in state.tableData)) { - return update(state, - {tableData: - {$merge: {[action.tableId]: [action.rowData]} } - } - ) - } - // otherwise, add it to the exising table - return update(state, - {tableData: - {[action.tableId]: - {$push: [action.rowData]} - } - } - ) - - case 'UPDATE_GTFSEDITOR_FIELD': - return update(state, - {tableData: - {[action.tableId]: - {[action.rowIndex]: - {[action.fieldName]: - {$set: action.newValue} - } - } - } - } - ) - - case 'DELETE_GTFSEDITOR_ROW': - const table = state.tableData[action.tableId] - const newTable = [ - ...table.slice(0, action.rowIndex), - ...table.slice(action.rowIndex + 1) - ] - return update(state, - {tableData: - {[action.tableId]: - {$set: newTable} - } - } - ) - - case 'RECEIVE_GTFS_ENTITIES': - const getType = function (entity) { - if (entity.hasOwnProperty('route_id')) return 'route' - if (entity.hasOwnProperty('stop_id')) return 'stop' - } - - const newLookupEntries = {} - for(const entity of action.gtfsEntities) { - const type = getType(entity) - const key = type + '_' + entity[type+'_id'] - newLookupEntries[key] = entity - } - - return update(state, - {gtfsEntityLookup: - {$merge: newLookupEntries} - } - ) - - case 'RECEIVE_GTFSEDITOR_VALIDATION': - const validationTable = {} - for(const issue of action.validationIssues) { - if(!(issue.tableId in validationTable)) { - validationTable[issue.tableId] = [] - } - validationTable[issue.tableId].push(issue) - } - return update(state, - {validation: - {$set: validationTable} - } - ) - - default: - return state - } -} - -export default editor diff --git a/src/main/client/editor/reducers/index.js b/src/main/client/editor/reducers/index.js deleted file mode 100644 index 7735e1f82..000000000 --- a/src/main/client/editor/reducers/index.js +++ /dev/null @@ -1 +0,0 @@ -export editor from './editor' diff --git a/src/main/client/editor/util/gtfs.js b/src/main/client/editor/util/gtfs.js deleted file mode 100644 index 5eca9e151..000000000 --- a/src/main/client/editor/util/gtfs.js +++ /dev/null @@ -1,159 +0,0 @@ -import along from 'turf-along' -import lineDistance from 'turf-line-distance' -import { latLngBounds } from 'leaflet' -import { extent } from 'turf-extent' - -export const componentList = ['route', 'stop', 'fare', 'feedinfo', 'calendar', 'scheduleexception', 'agency'] -export const subComponentList = ['trippattern'] -export const subSubComponentList = ['timetable'] - -export const getEntityBounds = (entity, offset = 0.005) => { - if (!entity) return null - - // [lng, lat] - if (entity.constructor === Array) { - return latLngBounds([[entity[1] + offset, entity[0] - offset], [entity[1] - offset, entity[0] + offset]]) - } - // stop - else if (typeof entity.stop_lat !== 'undefined') { - return latLngBounds([[entity.stop_lat + offset, entity.stop_lon - offset], [entity.stop_lat - offset, entity.stop_lon + offset]]) - } - // route - else if (typeof entity.tripPatterns !== 'undefined') { - let coordinates = [] - entity.tripPatterns.map(pattern => { - if (pattern.shape && pattern.shape.coordinates) { - coordinates = [ - ...coordinates, - ...pattern.shape.coordinates.map(c => ([c[1], c[0]])) - ] - } - }) - return latLngBounds(coordinates) - } - // trip pattern - else if (typeof entity.shape !== 'undefined') { - return latLngBounds(entity.shape.coordinates.map(c => ([c[1], c[0]]))) - } -} - -export const getEntityName = (component, entity) => { - if (!entity) { - return '[Unnamed]' - } - let entName = component === 'agency' - ? 'agency_name' - : component === 'route' - ? 'route_short_name' - : component === 'stop' - ? 'stop_name' - : component === 'calendar' - ? 'description' - : component === 'fare' - ? 'fare_id' - : component === 'scheduleexception' - ? 'name' - : null - switch (component) { - case 'stop': - return entity.stop_name && entity.stop_code - ? `${entity.stop_name} (${entity.stop_code})` - : entity.stop_name && entity.stop_id - ? `${entity.stop_name} (${entity.stop_id})` - : entity.stop_name - case 'route': - return entity.route_short_name && entity.route_long_name && entity.route_short_name !== '""' && entity.route_long_name !== '""' - ? `${entity.route_short_name} - ${entity.route_long_name}` - : entity.route_short_name && entity.route_short_name !== '""' - ? entity.route_short_name - : entity.route_long_name && entity.route_long_name !== '""' - ? entity.route_long_name - : entity.route_id - default: - return entity[entName] - } -} - -export const gtfsIcons = [ - { - id: 'feedinfo', - icon: 'info', - addable: false, - title: 'Edit feed info', - label: 'Feed Info' - }, - { - id: 'agency', - icon: 'building', - addable: true, - title: 'Edit agencies', - label: 'Agencies' - }, - { - id: 'route', - icon: 'bus', - addable: true, - title: 'Edit routes', - label: 'Routes' - }, - { - id: 'stop', - icon: 'map-marker', - addable: true, - title: 'Edit stops', - label: 'Stops' - }, - { - id: 'calendar', - icon: 'calendar', - addable: true, - title: 'Edit calendars', - label: 'Calendars' - }, - { - id: 'scheduleexception', - icon: 'ban', - addable: true, - hideSidebar: true, - title: 'Edit schedule exceptions' - }, - { - id: 'fare', - icon: 'ticket', - addable: true, - title: 'Edit fares', - label: 'Fares' - } -] - -export const getControlPoints = (pattern, snapToStops) => { - if (!pattern) { - return [] - } - let controlPoints = [] - pattern.shape && pattern.patternStops && pattern.patternStops.map((ps, index) => { - // set distance to average of patternStop and next patternStop, if last stop set to end of segment - let distance = pattern.patternStops[index + 1] - ? (pattern.patternStops[index + 1].shapeDistTraveled + ps.shapeDistTraveled) / 2 - : lineDistance(pattern.shape, 'meters') - let point = pattern.shape && along(pattern.shape, distance, 'meters') - let controlPoint = { - point, - distance: distance, - permanent: true - } - let stopPoint = pattern.shape && along(pattern.shape, ps.shapeDistTraveled, 'meters') - let stopControl = { - point: stopPoint, - permanent: true, - distance: ps.shapeDistTraveled, - ...ps - } - if (snapToStops) { - stopControl.hidden = true - } - controlPoints.push(stopControl) - controlPoints.push(controlPoint) - }) - return controlPoints -} diff --git a/src/main/client/gtfs/components/GtfsFilter.js b/src/main/client/gtfs/components/GtfsFilter.js deleted file mode 100644 index b3413cf3f..000000000 --- a/src/main/client/gtfs/components/GtfsFilter.js +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react' - -import { Panel, Button, DropdownButton, MenuItem, Badge, Glyphicon, Label, ButtonToolbar } from 'react-bootstrap' - -export default class GtfsFilter extends React.Component { - - constructor (props) { - super(props) - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - - render () { - var buttonMinimalStyle = { - marginTop: '10px', - marginBottom: '5px', - // textAlign: 'right' - } - - var compare = function (a, b) { - var aName = a.shortName || a.name - var bName = b.shortName || b.name - if(aName < bName) return -1 - if(aName > bName) return 1 - return 0 - } - - var activeFeeds = this.props.activeFeeds.sort(compare) - var activeAndLoadedFeeds = this.props.activeFeeds.filter(f => f && this.props.loadedFeeds.findIndex(feed => feed.id === f.id) !== -1) - var nonActiveFeeds = this.props.allFeeds.filter((feed) => { - return (activeFeeds.indexOf(feed) === -1) - }).sort(compare) - - var feedLookup = {} - for(var f of this.props.allFeeds) feedLookup[f.id] = f - return ( - - feed.name.length > 11 ? feed.name.substr(0, 11) + '...' : feed.name).join(' and ')}` - : `Searching ${activeAndLoadedFeeds.length} feeds`} - alt={activeAndLoadedFeeds.join(', ')} - onSelect={eventKey => { - let feed = feedLookup[eventKey] - activeFeeds.indexOf(feed) === -1 ? this.props.onAddFeed(feed) : this.props.onRemoveFeed(feed) - }} - > - {this.props.allFeeds.map((feed) => { - let disabled = this.props.loadedFeeds.findIndex(f => f.id === feed.id) === -1 - return ( - - - - {feed.shortName || feed.name.length > 11 ? feed.name.substr(0, 11) + '...' : feed.name} - - - ) - })} - - - - ) - } -} diff --git a/src/main/client/gtfs/components/gtfsmap.js b/src/main/client/gtfs/components/gtfsmap.js deleted file mode 100644 index 988b0693d..000000000 --- a/src/main/client/gtfs/components/gtfsmap.js +++ /dev/null @@ -1,284 +0,0 @@ -import React, { PropTypes } from 'react' - -import fetch from 'isomorphic-fetch' - -import { Button } from 'react-bootstrap' -import { Browser } from 'leaflet' - -import { Map, Marker, Popup, TileLayer, GeoJson } from 'react-leaflet' - -import { getFeed, getFeedId } from '../../common/util/modules' -import { getFeedsBounds } from '../../common/util/geo' -import { getConfigProperty } from '../../common/util/config' - -export default class GtfsMap extends React.Component { - static propTypes = { - searchFocus: PropTypes.bool, - bounds: PropTypes.array, - feeds: PropTypes.array, - entities: PropTypes.array, - position: PropTypes.array, - stops: PropTypes.array, - onStopClick: PropTypes.func, - onRouteClick: PropTypes.func, - onZoomChange: PropTypes.func, - popupAction: PropTypes.func, - newEntityId: PropTypes.string, - patterns: PropTypes.array - } - constructor (props) { - super(props) - - this.state = { - stops: null, - routes: null, - searchFocus: this.props.searchFocus, - patterns: null, - message: '', - bounds: this.props.bounds || [[70, 130], [-70, -130]], - map: {} - } - } - - componentDidMount () { - } - - componentWillReceiveProps (nextProps) { - if (nextProps.feeds.length !== this.props.feeds.length && this.refs.map) { - this.refreshGtfsElements(nextProps.feeds) - } - - if (nextProps.entities !== this.props.entities && this.refs.map) { - this.refreshGtfsElements(nextProps.feeds, nextProps.entities) - } - - // handle stop: panning to - if (nextProps && nextProps.position !== this.props.position) { - this.refs.map.leafletElement.panTo(nextProps.position) - } - } - - render () { - var mapStyle = { - height: '400px', - width: '555px' - } - let bounds = getFeedsBounds(this.props.feeds) - bounds = bounds && bounds.north ? [[bounds.north, bounds.east], [bounds.south, bounds.west]] : this.state.bounds - - const layerAddHandler = (e) => { - // handle pattern: opening popup and fitting bounds - if (e.layer.feature && e.layer.feature.properties.patternId) { - this.refs.map.leafletElement.fitBounds(e.layer.getBounds()) - e.layer.openPopup() - } - } - - return ( -
    - { - this.props.onZoomChange(e) - this.refs.map && this.refreshGtfsElements() - }} - onMoveEnd={() => this.refs.map && this.refreshGtfsElements()} - onLayerAdd={layerAddHandler} - className='Gtfs-Map' - > - - {this.props.stops ? this.props.stops.map((stop, index) => { - if (stop) { - return ( - { - e.target.openPopup() - }} - position={[stop.stop_lat, stop.stop_lon]} - key={`marker-${stop.stop_id}`} - > - -
    -

    {stop.stop_name}

    -
      -
    • ID: {stop.stop_id}
    • -
    • Agency:{' '} - {// TODO: change this back to feedName - stop.feed_id - // getFeed(this.props.feeds, stop.feed_id).name - } -
    • - {stop.stop_desc &&
    • Desc: {stop.stop_desc}
    • } -
    - -
    -
    -
    - ) - } - }) - : null - } - {this.state.stops ? this.state.stops.map((stop, index) => { - if (stop) { - return ( - - -
    -

    {stop.stop_name}

    -
      -
    • ID: {stop.stop_id}
    • -
    • Agency:{' '} - {// TODO: change this back to feedName - stop.feed_id - // getFeed(this.props.feeds, stop.feed_id).name - } -
    • - {stop.stop_desc &&
    • Desc: {stop.stop_desc}
    • } -
    - -
    -
    -
    - ) - } - }) - : null - } - {this.state.patterns ? this.state.patterns.map((pattern, index) => { - if (pattern) { - const route = pattern.associatedRoutes[0] - const routeName = route.route_short_name !== null ? route.route_short_name : route.route_long_name - return ( - - -
    -

    {routeName}

    -
      -
    • ID: {route.route_id}
    • -
    • Agency:{' '} - {// TODO: change this back to feedName - route.feed_id - // getFeed(this.props.feeds, route.feed_id).name - } -
    • -
    - {this.props.onRouteClick - ? - :

    [Must add stops first]

    - } - -
    -
    -
    - ) - } - }) - : null - } - {this.props.patterns ? this.props.patterns.map((pattern, index) => { - if (pattern) { - const route = pattern.associatedRoutes[0] - const routeName = route.route_short_name !== null ? route.route_short_name : route.route_long_name - const popup = ( - -
    -

    {routeName}

    -
      -
    • ID: {route.route_id}
    • -
    • Agency:{' '} - {// TODO: change this back to feedName - route.feed_id - // getFeed(this.props.feeds, route.feed_id).name - } -
    • -
    - {this.props.onRouteClick - ? - :

    [Must add stops first]

    - } -
    -
    - ) - return ( - { - layer.feature.properties.route = route - layer.feature.properties.patternId = pattern.pattern_id - layer._leaflet_id = pattern.pattern_id - // layer.feature.geometry.coordinates.push(pattern.geometry.coordinates) - }} - properties={route} - > - {popup} - - ) - } - }) - : null - } -
    -
    - ) - } - - refreshGtfsElements (feeds, entities) { - const feedIds = (feeds || this.props.feeds).map(getFeedId) - const ents = (entities || this.props.entities || ['routes', 'stops']) - const zoomLevel = this.refs.map.leafletElement.getZoom() - if (feedIds.length === 0 || zoomLevel <= 13) { - this.setState({ stops: [], patterns: [], routes: [] }) - return - } - console.log('refresh GTFS', feedIds) - const bounds = this.refs['map'].leafletElement.getBounds() - const maxLat = bounds.getNorth() - const maxLng = bounds.getEast() - const minLat = bounds.getSouth() - const minLng = bounds.getWest() - - const getStops = fetch(`/api/manager/stops?max_lat=${maxLat}&max_lon=${maxLng}&min_lat=${minLat}&min_lon=${minLng}&feed=${feedIds.toString()}`) - .then((response) => { - return response.json() - }) - - const getRoutes = fetch(`/api/manager/routes?max_lat=${maxLat}&max_lon=${maxLng}&min_lat=${minLat}&min_lon=${minLng}&feed=${feedIds.toString()}`) - .then((response) => { - return response.json() - }) - - let entitySearches = [] - if (ents.indexOf('stops') > -1) { - entitySearches.push(getStops) - } else { - entitySearches.push(null) - } - if (ents.indexOf('routes') > -1) { - entitySearches.push(getRoutes) - } else { - entitySearches.push(null) - } - Promise.all(entitySearches).then((results) => { - const stops = results[0] ? results[0] : [] - const patterns = results[1] ? results[1] : [] - const routes = patterns.map(p => p.associatedRoutes[0]) - this.setState({ stops, patterns, routes }) - }) - } -} diff --git a/src/main/client/gtfs/components/gtfsmapsearch.js b/src/main/client/gtfs/components/gtfsmapsearch.js deleted file mode 100644 index 8cca54d26..000000000 --- a/src/main/client/gtfs/components/gtfsmapsearch.js +++ /dev/null @@ -1,119 +0,0 @@ -import React, { PropTypes } from 'react' - -import fetch from 'isomorphic-fetch' - -import { Button } from 'react-bootstrap' - -import { PureComponent, shallowEqual } from 'react-pure-render' - -import GtfsMap from './gtfsmap' -import GtfsSearch from './gtfssearch' - -export default class GtfsMapSearch extends React.Component { - - constructor(props) { - super(props) - this.state = { - stops: [], - routes: [], - patterns: [], - position: null, - message: '', - searching: ['stops', 'routes'], - map: {} - } - } - - componentDidMount() { - // this.fetchUsers() - console.log(this.props) - } - - render() { - let zoomMessage = 'Zoom in to view ' + this.state.searching.join(' and ') - if (this.refs.map && this.refs.map.refs.map) { - let mapZoom = this.refs.map.refs.map.leafletElement.getZoom() - zoomMessage = mapZoom <= 13 ? zoomMessage : '' - } - console.log(zoomMessage) - const onZoomChange = (e) => { - let mapZoom = e.target._zoom - zoomMessage = mapZoom <= 13 ? zoomMessage : '' - } - const {attribution, centerCoordinates, geojson, markers, transitive, url, zoom} = this.props - const getPatterns = (input) => { - return fetch(`/api/manager/patterns?route=${input.route.route_id}&feed=${input.route.feed_id}`) - .then((response) => { - return response.json() - }) - .then((json) => { - - const pattern = json[0] - console.log(pattern) - // hack to associate route to pattern - pattern.associatedRoutes = [] - pattern.associatedRoutes.push(input.route) - return pattern - }) - } - const handleSelection = (input) => { - if (!input) { - this.setState({stops: null, routes: null, patterns: null, searchFocus: false}) - } - else if (input && input.stop) { - this.setState(Object.assign({}, this.state, { stops: [input.stop], position: [input.stop.stop_lat, input.stop.stop_lon], routes: null, patterns: null, searchFocus: true })) - } - else if (input && input.route) { - return Promise.all([getPatterns(input)]).then((results) => { - const patterns = results[0] - console.log('patterns for route ' + input.route.route_id, patterns) - this.setState(Object.assign({}, this.state, { routes: [input.route], patterns: [patterns], stops: null, searchFocus: true })) - }) - } - } - - return ( -
    - -
      -
    • - -
    • -
    • {zoomMessage}
    • -
    - -
    - ) - } -} diff --git a/src/main/client/gtfs/reducers/index.js b/src/main/client/gtfs/reducers/index.js deleted file mode 100644 index 268aeb483..000000000 --- a/src/main/client/gtfs/reducers/index.js +++ /dev/null @@ -1 +0,0 @@ -export gtfsFilter from './gtfsFilter' diff --git a/src/main/client/gtfsplus/reducers/index.js b/src/main/client/gtfsplus/reducers/index.js deleted file mode 100644 index 5bc3ba0d6..000000000 --- a/src/main/client/gtfsplus/reducers/index.js +++ /dev/null @@ -1 +0,0 @@ -export gtfsplus from './gtfsplus' diff --git a/src/main/client/index.tpl.html b/src/main/client/index.tpl.html deleted file mode 100644 index 35a394ad2..000000000 --- a/src/main/client/index.tpl.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -
    - - diff --git a/src/main/client/main.js b/src/main/client/main.js deleted file mode 100644 index b8442fda3..000000000 --- a/src/main/client/main.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import { Provider } from 'react-redux' -import { createStore, applyMiddleware, combineReducers } from 'redux' -import thunkMiddleware from 'redux-thunk' -import { browserHistory, hashHistory } from 'react-router' -import { syncHistoryWithStore, routerReducer } from 'react-router-redux' -// import promise from 'redux-promise' -import createLogger from 'redux-logger' - -import { checkExistingLogin } from './manager/actions/user' -import App from './common/containers/App' - - -import 'react-virtualized/styles.css' - -import config from 'json!yaml!../../../config.yml' - -if(config.modules.gtfsplus && config.modules.gtfsplus.enabled) { - config.modules.gtfsplus.spec = require('json!yaml!../../../gtfsplus.yml') -} -config.modules.editor.spec = require('json!yaml!../../../gtfs.yml') - -// function to require all lang files in i18n dir -function requireAll (requireContext) { - return requireContext.keys().map(requireContext) -} -// requires and returns all modules that match -var lang = requireAll(require.context('json!yaml!../../../i18n', true, /.yml/)) -// is an array containing all the matching modules -config.messages = {} -config.messages.all = lang -const languageId = localStorage.getItem('lang') ? localStorage.getItem('lang') : navigator.language || navigator.userLanguage -config.messages.active = lang.find(l => l.id === languageId) || lang.find(l => l.id === 'en-US') - -console.log('config', config) -window.DT_CONFIG = config - - - -import * as managerReducers from './manager/reducers' -import * as adminReducers from './admin/reducers' -import * as alertsReducers from './alerts/reducers' -import * as signsReducers from './signs/reducers' - -import * as gtfsPlusReducers from './gtfsplus/reducers' -import * as editorReducers from './editor/reducers' -import * as gtfsReducers from './gtfs/reducers' - -const logger = createLogger({duration: true, collapsed: true}) -const store = createStore( - combineReducers({ - ...managerReducers, - ...adminReducers, - ...alertsReducers, - ...signsReducers, - ...gtfsPlusReducers, - ...editorReducers, - ...gtfsReducers, - routing: routerReducer - }), - applyMiddleware(thunkMiddleware, logger) -) - -console.log('initial store', store.getState()) - -// Every time the state changes, log it -// Note that subscribe() returns a function for unregistering the listener -let unsubscribe = store.subscribe(() => { - // console.log('store updated', store.getState()) -}) - -const appHistory = syncHistoryWithStore(browserHistory, store) - -ReactDOM.render( - - - , - document.getElementById('main')) diff --git a/src/main/client/manager/actions/feeds.js b/src/main/client/manager/actions/feeds.js deleted file mode 100644 index cbb291df1..000000000 --- a/src/main/client/manager/actions/feeds.js +++ /dev/null @@ -1,577 +0,0 @@ -import { secureFetch } from '../../common/util/util' -import { fetchProject, fetchProjectWithFeeds } from './projects' -import { setErrorMessage, startJobMonitor } from './status' -import { fetchSnapshots } from '../../editor/actions/snapshots' -// Feed Source Actions - -export function requestingFeedSources () { - return { - type: 'REQUESTING_FEEDSOURCES' - } -} - -export function receiveFeedSources (projectId, feedSources) { - return { - type: 'RECEIVE_FEEDSOURCES', - projectId, - feedSources - } -} - -export function fetchProjectFeeds (projectId) { - return function (dispatch, getState) { - dispatch(requestingFeedSources()) - const url = '/api/manager/secure/feedsource?projectId=' + projectId - return secureFetch(url, getState()) - .then(response => response.json()) - .then(feedSources => { - dispatch(receiveFeedSources(projectId, feedSources)) - }) - } -} - -export function fetchUserFeeds (userId) { - return function (dispatch, getState) { - dispatch(requestingFeedSources()) - const url = '/api/manager/secure/feedsource?userId=' + userId - return secureFetch(url, getState()) - .then(response => response.json()) - .then(feedSources => { - dispatch(receiveFeedSources(feedSources)) - }) - } -} - -function requestingPublicFeeds () { - return { - type: 'REQUESTING_PUBLIC_FEEDS' - } -} - -function receivePublicFeeds (feeds) { - return { - type: 'RECEIVE_PUBLIC_FEEDS', - feeds - } -} - -export function createFeedSource (projectId) { - return { - type: 'CREATE_FEEDSOURCE', - projectId - } -} - -export function savingFeedSource () { - return { - type: 'SAVING_FEEDSOURCE' - } -} - -export function saveFeedSource (props) { - return function (dispatch, getState) { - dispatch(savingFeedSource()) - const url = '/api/manager/secure/feedsource' - return secureFetch(url, getState(), 'post', props) - .then((res) => { - return dispatch(fetchProjectWithFeeds(props.projectId)) - }) - } -} - -export function updateFeedSource (feedSource, changes) { - return function (dispatch, getState) { - dispatch(savingFeedSource()) - const url = '/api/manager/secure/feedsource/' + feedSource.id - return secureFetch(url, getState(), 'put', changes) - .then((res) => { - if (res.status >= 400) { - console.log(res.json()) - dispatch(setErrorMessage('Error updating feed source.')) - } - //return dispatch(fetchProjectFeeds(feedSource.projectId)) - return dispatch(fetchFeedSource(feedSource.id, true)) - }) - } -} - -export function updateExternalFeedResource (feedSource, resourceType, properties) { - return function (dispatch, getState) { - console.log('updateExternalFeedResource', feedSource, resourceType, properties); - dispatch(savingFeedSource()) - const url = `/api/manager/secure/feedsource/${feedSource.id}/updateExternal?resourceType=${resourceType}` - return secureFetch(url, getState(), 'put', properties) - .then((res) => { - return dispatch(fetchFeedSource(feedSource.id, true)) - }) - } -} - - -export function deletingFeedSource (feedSource) { - return { - type: 'DELETING_FEEDSOURCE', - feedSource - } -} - -export function deleteFeedSource (feedSource, changes) { - return function (dispatch, getState) { - dispatch(deletingFeedSource(feedSource)) - const url = '/api/manager/secure/feedsource/' + feedSource.id - return secureFetch(url, getState(), 'delete') - .then((res) => { - // if (res.status >= 400) { - // return dispatch(setErrorMessage('Error deleting feed source')) - // } - return dispatch(fetchProjectFeeds(feedSource.projectId)) - }) - } -} - -export function requestingFeedSource () { - return { - type: 'REQUESTING_FEEDSOURCE' - } -} - -export function receiveFeedSource (feedSource) { - return { - type: 'RECEIVE_FEEDSOURCE', - feedSource - } -} - -export function fetchFeedSource (feedSourceId, fetchVersions) { - return function (dispatch, getState) { - console.log('fetchFeedSource', feedSourceId) - dispatch(requestingFeedSource()) - const url = '/api/manager/secure/feedsource/' + feedSourceId - return secureFetch(url, getState()) - .then(res => { - if (res.status >= 400) { - // dispatch(setErrorMessage('Error getting feed source')) - console.log('error getting feed source') - return null - } - return res.json() - }) - .then(feedSource => { - if (!feedSource) { - dispatch(receiveFeedSource(feedSource)) - return feedSource - } - console.log('got feedSource', feedSource) - dispatch(receiveFeedSource(feedSource)) - if(fetchVersions) dispatch(fetchFeedVersions(feedSource)) - return feedSource - }) - } -} - -export function fetchFeedSourceAndProject (feedSourceId, unsecured) { - return function (dispatch, getState) { - dispatch(requestingFeedSource()) - const apiRoot = unsecured ? 'public' : 'secure' - const url = `/api/manager/${apiRoot}/feedsource/${feedSourceId}` - return secureFetch(url, getState()) - .then(res => { - if (res.status >= 400) { - // dispatch(setErrorMessage('Error getting feed source')) - console.log('error getting feed source') - return null - } - return res.json() - }) - .then(feedSource => { - if (!feedSource) { - dispatch(receiveFeedSource(feedSource)) - return feedSource - } - return dispatch(fetchProject(feedSource.projectId, unsecured)) - .then(proj => { - dispatch(receiveFeedSource(feedSource)) - return feedSource - }) - }) - } -} - -export function fetchPublicFeedSource (feedSourceId) { - return function (dispatch, getState) { - dispatch(requestingFeedSource()) - const url = '/api/manager/public/feedsource/' + feedSourceId - return secureFetch(url, getState()) - .then(response => response.json()) - .then(feedSource => { - dispatch(receivePublicFeeds()) - return feedSource - }) - } -} - -export function runningFetchFeed () { - return { - type: 'RUNNING_FETCH_FEED' - } -} - -export function receivedFetchFeed (feedSource) { - return { - type: 'RECEIVED_FETCH_FEED', - feedSource - } -} - -export function runFetchFeed (feedSource) { - return function (dispatch, getState) { - dispatch(runningFetchFeed()) - const url = `/api/manager/secure/feedsource/${feedSource.id}/fetch` - return secureFetch(url, getState(), 'post') - .then(res => { - if (res.status === 304) { - dispatch(feedNotModified(feedSource, 'Feed fetch cancelled because it matches latest feed version.')) - } - else if (res.status >= 400) { - dispatch(setErrorMessage('Error fetching feed source')) - } - else { - dispatch(receivedFetchFeed(feedSource)) - dispatch(startJobMonitor()) - return res.json() - } - }) - .then(result => { - console.log('fetchFeed result', result) - // fetch feed source with versions - return dispatch(fetchFeedSource(feedSource.id, true)) - }) - } -} - -//** FEED VERSION ACTIONS **// - -// Get all FeedVersions for FeedSource - -export function requestingFeedVersions () { - return { - type: 'REQUESTING_FEEDVERSIONS' - } -} - -export function receiveFeedVersions (feedSource, feedVersions) { - return { - type: 'RECEIVE_FEEDVERSIONS', - feedSource, - feedVersions - } -} - -export function fetchFeedVersions (feedSource, unsecured) { - return function (dispatch, getState) { - dispatch(requestingFeedVersions()) - const apiRoot = unsecured ? 'public' : 'secure' - const url = `/api/manager/${apiRoot}/feedversion?feedSourceId=${feedSource.id}` - return secureFetch(url, getState()) - .then(response => response.json()) - .then(versions => { - dispatch(receiveFeedVersions(feedSource, versions)) - return versions - }) - } -} - - -export function requestingFeedVersion () { - return { - type: 'REQUESTING_FEEDVERSION' - } -} - -export function receiveFeedVersion (feedVersion) { - return { - type: 'RECEIVE_FEEDVERSION', - feedVersion - } -} - -export function fetchFeedVersion (feedVersionId) { - return function (dispatch, getState) { - dispatch(requestingFeedVersion()) - const url = `/api/manager/secure/feedversion/${feedVersionId}` - return secureFetch(url, getState()) - .then(response => response.json()) - .then(version => { - return dispatch(receiveFeedVersion(version)) - }) - } -} - - -export function fetchPublicFeedVersions (feedSource) { - return function (dispatch, getState) { - dispatch(requestingFeedVersions()) - const url = `/api/manager/public/feedversion?feedSourceId=${feedSource.id}&public=true` - return secureFetch(url, getState()) - .then(response => response.json()) - .then(versions => { - dispatch(receiveFeedVersions(feedSource, versions)) - }) - } -} - -// Upload a GTFS File as a new FeedVersion - -export function uploadingFeed () { - return { - type: 'UPLOADING_FEED' - } -} - -export function uploadedFeed (feedSource) { - return { - type: 'UPLOADED_FEED', - feedSource - } -} - -export function feedNotModified (feedSource, message) { - return { - type: 'FEED_NOT_MODIFIED', - feedSource, - message - } -} - -export function uploadFeed (feedSource, file) { - return function (dispatch, getState) { - dispatch(uploadingFeed()) - const url = `/api/manager/secure/feedversion?feedSourceId=${feedSource.id}` - - var data = new FormData() - data.append('file', file) - - return fetch(url, { - method: 'post', - headers: { 'Authorization': 'Bearer ' + getState().user.token }, - body: data - }).then(res => { - if (res.status === 304) { - dispatch(feedNotModified(feedSource, 'Feed upload cancelled because it matches latest feed version.')) - } - else if (res.status >= 400) { - dispatch(setErrorMessage('Error uploading feed source')) - } - else { - dispatch(uploadedFeed(feedSource)) - dispatch(startJobMonitor()) - } - console.log('uploadFeed result', res) - - // fetch feed source with versions - return dispatch(fetchFeedSource(feedSource.id, true)) - }) - } -} - -// Delete an existing FeedVersion - -export function deletingFeedVersion () { - return { - type: 'DELETING_FEEDVERSION' - } -} - -export function deleteFeedVersion (feedVersion, changes) { - return function (dispatch, getState) { - dispatch(deletingFeedVersion()) - const url = '/api/manager/secure/feedversion/' + feedVersion.id - return secureFetch(url, getState(), 'delete') - .then((res) => { - // fetch feed source with versions - return dispatch(fetchFeedSource(feedVersion.feedSource.id, true)) - }) - } -} - -// Get GTFS validation results for a FeedVersion - -export function requestingValidationResult () { - return { - type: 'REQUESTING_VALIDATION_RESULT' - } -} - -export function receiveValidationResult (feedVersion, validationResult) { - return { - type: 'RECEIVE_VALIDATION_RESULT', - feedVersion, - validationResult - } -} - -export function fetchValidationResult (feedVersion, isPublic) { - return function (dispatch, getState) { - dispatch(requestingValidationResult()) - const route = isPublic ? 'public' : 'secure' - const url = `/api/manager/${route}/feedversion/${feedVersion.id}/validation` - return secureFetch(url, getState()) - .then(response => response.json()) - .then(result => { - dispatch(receiveValidationResult(feedVersion, result)) - }) - } -} - -// Request a FeedVersion isochrone - -export function requestingFeedVersionIsochrones () { - return { - type: 'REQUESTING_FEEDVERSION_ISOCHRONES' - } -} - -export function receiveFeedVersionIsochrones (feedSource, feedVersion, isochrones) { - return { - type: 'RECEIVE_FEEDVERSION_ISOCHRONES', - feedSource, - feedVersion, - isochrones - } -} - -export function fetchFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon) { - return function (dispatch, getState) { - dispatch(requestingFeedVersionIsochrones()) - const url = `/api/manager/secure/feedversion/${feedVersion.id}/isochrones?fromLat=${fromLat}&fromLon=${fromLon}&toLat=${toLat}&toLon=${toLon}` - return secureFetch(url, getState()) - .then(response => response.json()) - .then(isochrones => { - console.log('received isochrones ', isochrones) - dispatch(receiveFeedVersionIsochrones(feedVersion.feedSource, feedVersion, isochrones)) - return isochrones - }) - } -} - -// Download a GTFS file for a FeedVersion - -export function downloadFeedViaToken (feedVersion, isPublic) { - return function (dispatch, getState) { - const route = isPublic ? 'public' : 'secure' - const url = `/api/manager/${route}/feedversion/${feedVersion.id}/downloadtoken` - secureFetch(url, getState()) - .then(response => response.json()) - .then(result => { - window.location.assign(`/api/manager/downloadfeed/${result.id}`) - }) - } -} - -// Create a Feed Version from an editor snapshot - -export function creatingFeedVersionFromSnapshot () { - return { - type: 'CREATING_FEEDVERSION_FROM_SNAPSHOT' - } -} - -export function createFeedVersionFromSnapshot (feedSource, snapshotId) { - return function (dispatch, getState) { - dispatch(creatingFeedVersionFromSnapshot()) - const url = `/api/manager/secure/feedversion/fromsnapshot?feedSourceId=${feedSource.id}&snapshotId=${snapshotId}` - return secureFetch(url, getState(), 'post') - .then((res) => { - dispatch(startJobMonitor()) - }) - } -} - -// Create a Feed Version from an editor snapshot - -export function renamingFeedVersion () { - return { - type: 'RENAMING_FEEDVERSION' - } -} - -export function renameFeedVersion (feedSource, feedVersion, name) { - return function (dispatch, getState) { - dispatch(renamingFeedVersion()) - const url = `/api/manager/secure/feedversion/${feedVersion.id}/rename?name=${name}` - return secureFetch(url, getState(), 'put') - .then((res) => { - dispatch(fetchFeedVersions(feedSource)) - }) - } -} - -//** NOTES ACTIONS **// - -export function requestingNotes () { - return { - type: 'REQUESTING_NOTES' - } -} - -export function receiveNotesForFeedSource (feedSource, notes) { - return { - type: 'RECEIVE_NOTES_FOR_FEEDSOURCE', - feedSource, - notes - } -} - -export function fetchNotesForFeedSource (feedSource) { - return function (dispatch, getState) { - dispatch(requestingNotes()) - const url = `/api/manager/secure/note?type=FEED_SOURCE&objectId=${feedSource.id}` - secureFetch(url, getState()) - .then(response => response.json()) - .then(notes => { - dispatch(receiveNotesForFeedSource(feedSource, notes)) - }) - } -} - -export function postNoteForFeedSource (feedSource, note) { - return function (dispatch, getState) { - const url = `/api/manager/secure/note?type=FEED_SOURCE&objectId=${feedSource.id}` - secureFetch(url, getState(), 'post', note) - .then(response => response.json()) - .then(note => { - dispatch(fetchNotesForFeedSource(feedSource)) - }) - } -} - -export function receiveNotesForFeedVersion (feedVersion, notes) { - return { - type: 'RECEIVE_NOTES_FOR_FEEDVERSION', - feedVersion, - notes - } -} - -export function fetchNotesForFeedVersion (feedVersion) { - return function (dispatch, getState) { - dispatch(requestingNotes()) - const url = `/api/manager/secure/note?type=FEED_VERSION&objectId=${feedVersion.id}` - secureFetch(url, getState()) - .then(response => response.json()) - .then(notes => { - dispatch(receiveNotesForFeedVersion(feedVersion, notes)) - }) - } -} - -export function postNoteForFeedVersion (feedVersion, note) { - return function (dispatch, getState) { - const url = `/api/manager/secure/note?type=FEED_VERSION&objectId=${feedVersion.id}` - secureFetch(url, getState(), 'post', note) - .then(response => response.json()) - .then(note => { - dispatch(fetchNotesForFeedVersion(feedVersion)) - }) - } -} diff --git a/src/main/client/manager/components/DeploymentViewer.js b/src/main/client/manager/components/DeploymentViewer.js deleted file mode 100644 index f5d547dda..000000000 --- a/src/main/client/manager/components/DeploymentViewer.js +++ /dev/null @@ -1,243 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import Helmet from 'react-helmet' -import moment from 'moment' -import moment_tz from 'moment-timezone' -import { Grid, Row, Col, Button, Table, FormControl, Panel, Glyphicon, Badge, ButtonToolbar, DropdownButton, MenuItem, Label } from 'react-bootstrap' -import { Link } from 'react-router' - -import ManagerPage from '../../common/components/ManagerPage' -import Breadcrumbs from '../../common/components/Breadcrumbs' -import EditableTextField from '../../common/components/EditableTextField' -import { versionsSorter, retrievalMethodString } from '../../common/util/util' -import languages from '../../common/util/languages' -import { isModuleEnabled, isExtensionEnabled, getComponentMessages, getMessage } from '../../common/util/config' - -export default class DeploymentViewer extends Component { - - constructor (props) { - super(props) - - this.state = {} - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - - render () { - - if(!this.props.deployment) { - return - } - const deployableFeeds = this.props.project.feedSources ? this.props.project.feedSources.filter(fs => - this.props.deployment.feedVersions.findIndex(v => v.feedSource.id === fs.id) === -1 && - fs.deployable && - fs.latestValidation - ) : [] - const messages = getComponentMessages('DeploymentViewer') - const versions = this.props.deployment.feedVersions.sort(versionsSorter) - - console.log(this.props.deployment) - return ( -
    - - -
    - - - {getMessage(messages, 'deploy')} - : {getMessage(messages, 'noServers')} - } - onSelect={(evt) => { - console.log(evt) - this.props.deployToTargetClicked(this.props.deployment, evt) - //setTimeout(() => this.props.getDeploymentStatus(this.props.deployment, evt), 5000) - }} - > - {this.props.project.otpServers - ? this.props.project.otpServers.map(server => ( - {server.name} - )) - : null - } - - -

    - {/* - this.props.deploymentPropertyChanged(this.props.deployment, 'name', value)} - /> - */} - {this.props.deployment.name} - {this.props.deployment.deployedTo - ? - : null - } -

    -
    - -
    - {getMessage(messages, 'versions')})} - collapsible - defaultExpanded={true} - > - - - this.props.searchTextChanged(evt.target.value)} - /> - - - {getMessage(messages, 'addFeedSource')} : {getMessage(messages, 'allFeedsAdded')}} - onSelect={(evt) => { - console.log(evt) - let feed = deployableFeeds.find(fs => fs.id === evt) - this.props.addFeedVersion(this.props.deployment, {id: feed.latestVersionId}) - }} - > - { - deployableFeeds.map(fs => ( - {fs.name} - )) - } - - - - - - - - - - - - - - - - - - - - - - - {versions.map((version) => { - return - })} - -
    {getMessage(messages, 'table.name')}Version{getMessage(messages, 'table.dateRetrieved')}{getMessage(messages, 'table.loadStatus')}{getMessage(messages, 'table.errorCount')}{getMessage(messages, 'table.routeCount')}{getMessage(messages, 'table.tripCount')}{getMessage(messages, 'table.stopTimesCount')}{getMessage(messages, 'table.validFrom')}{getMessage(messages, 'table.expires')}
    - -
    -
    -
    - ) - } -} - -class FeedVersionTableRow extends Component { - - constructor (props) { - super(props) - } - - render () { - const fs = this.props.feedSource - const version = this.props.version - const result = this.props.version.validationResult - const na = (N/A) - const hasVersionStyle = {cursor: 'pointer'} - const noVersionStyle = {color: 'lightGray'} - const disabled = !this.props.user.permissions.hasFeedPermission(this.props.project.id, fs.id, 'manage-feed') - return ( - - - {fs.name} - - - Version {version.version} - - - - - - - {na} - - - {result.loadStatus} - - {result.errorCount} - {result.routeCount} - {result.tripCount} - {result.stopTimesCount} - {moment(result.startDate).format('MMM Do YYYY')} ({moment(result.startDate).fromNow()}) - {moment(result.endDate).format('MMM Do YYYY')} ({moment(result.endDate).fromNow()}) - - - - - ) - } - -} diff --git a/src/main/client/manager/components/FeedSourceTable.js b/src/main/client/manager/components/FeedSourceTable.js deleted file mode 100644 index 551cd37cc..000000000 --- a/src/main/client/manager/components/FeedSourceTable.js +++ /dev/null @@ -1,351 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import moment from 'moment' - -import { Button, Table, Checkbox, Glyphicon, Dropdown, MenuItem, Panel, ListGroupItem, ListGroup } from 'react-bootstrap' -import { browserHistory, Link } from 'react-router' -import { LinkContainer } from 'react-router-bootstrap' -import { Icon, IconStack } from 'react-fa' -import { shallowEqual } from 'react-pure-render' - -import EditableTextField from '../../common/components/EditableTextField' -import ConfirmModal from '../../common/components/ConfirmModal' -import SelectFileModal from '../../common/components/SelectFileModal' -import WatchButton from '../../common/containers/WatchButton' -import { isModuleEnabled, getComponentMessages, getMessage, getConfigProperty } from '../../common/util/config' -import { isValidZipFile } from '../../common/util/util' - -export default class FeedSourceTable extends Component { - - static propTypes = { - feedSources: PropTypes.array, - project: PropTypes.object, - user: PropTypes.object, - - isFetching: PropTypes.bool, - - createDeploymentFromFeedSource: PropTypes.func, - deleteFeedSource: PropTypes.func, - updateFeedSourceProperty: PropTypes.func, - saveFeedSource: PropTypes.func, - fetchFeed: PropTypes.func, - uploadFeed: PropTypes.func, - onNewFeedSourceClick: PropTypes.func - } - - constructor (props) { - super(props) - - this.state = { - activeFeedSource: null - } - } - - render () { - const messages = getComponentMessages('ProjectViewer') - - const hover = this.props.createDeploymentFromFeedSource(fs)} - deleteFeedSource={(fs) => this.props.deleteFeedSource(fs)} - uploadFeed={(fs, file) => this.props.uploadFeed(fs, file)} - fetchFeed={(fs) => this.props.fetchFeed(fs)} - /> - - return ( - - {this.props.isFetching - ? - : this.props.feedSources.length - ? this.props.feedSources.map((feedSource) => { - return this.setState({activeFeedSource: fs})} - /> - }) - : - - - } - - ) - } -} - -class FeedSourceTableRow extends Component { - - static propTypes = { - feedSource: PropTypes.object, - hoverComponent: PropTypes.node, - project: PropTypes.object, - user: PropTypes.object, - - updateFeedSourceProperty: PropTypes.func, - saveFeedSource: PropTypes.func, - onHover: PropTypes.func - } - - constructor (props) { - super(props) - - this.state = { - hovered: false - } - } - - shouldComponentUpdate (nextProps, nextState) { - return !shallowEqual(nextProps.feedSource, this.props.feedSource) || this.state.hovered !== nextState.hovered - } - - render () { - const fs = this.props.feedSource - const na = (N/A) - const disabled = !this.props.user.permissions.hasFeedPermission(this.props.project.id, fs.id, 'manage-feed') - const dateFormat = getConfigProperty('application.date_format') - const messages = getComponentMessages('ProjectViewer') - const feedItem = ( - - { - if (fs.isCreating) this.props.saveFeedSource(value) - else this.props.updateFeedSourceProperty(fs, 'name', value) - }} - link={`/feed/${fs.id}`} - /> - {' '} - {!fs.isPublic ? : null} - {' '} - {fs.editedSinceSnapshot - ? - : - } - - } - key={fs.id} - // bsStyle={fs.isPublic ? 'default' : 'warning'} - onMouseEnter={() => { - if (!this.state.hovered) { - this.setState({ hovered: true }) - this.props.onHover(fs) - } - }} - onMouseLeave={() => { - if (this.state.hovered) this.setState({ hovered: false }) - }} - > - {this.state.hovered - ? this.props.hoverComponent - : null - } - -
      - {fs.lastUpdated - ?
    • {getMessage(messages, 'feeds.table.lastUpdated')} {moment(fs.lastUpdated).format(dateFormat)}
    • - :
    • No versions exist yet.
    • - } - {fs.latestValidation && fs.latestValidation.errorCount > 0 - ?
    • {fs.latestValidation.errorCount}
    • - : fs.latestValidation - ?
    • - :
    • - } - {fs.latestValidation && fs.latestValidation.endDate < +moment() - ?
    • - : fs.latestValidation - ?
    • - :
    • - } - {isModuleEnabled('deployment') && fs.deployable - ?
    • - : isModuleEnabled('deployment') - ?
    • - : null - } - {fs.url - ?
    • - : null - } -
    -
    - ) - const feedRow = ( - { - if (!this.state.hovered) { - this.setState({ hovered: true }) - this.props.onHover(fs) - } - }} - onMouseLeave={() => { - if (this.state.hovered) this.setState({ hovered: false }) - }} - > - -
    - { - if (fs.isCreating) this.props.saveFeedSource(value) - else this.props.updateFeedSourceProperty(fs, 'name', value) - }} - link={`/feed/${fs.id}`} - /> -
    - - - { - this.props.updateFeedSourceProperty(fs, 'isPublic', e.target.checked) - }} - /> - - - { - this.props.updateFeedSourceProperty(fs, 'deployable', e.target.checked) - }} - /> - - {fs.lastUpdated ? moment(fs.lastUpdated).format(dateFormat) : na} - {fs.latestValidation ? fs.latestValidation.errorCount : na} - {fs.latestValidation - ? ({moment(fs.latestValidation.startDate).format(dateFormat)} to {moment(fs.latestValidation.endDate).format(dateFormat)}) - : na - } - - {this.state.hovered - ? this.props.hoverComponent - : null - } - - - ) - - return feedItem - } -} - -class FeedSourceDropdown extends Component { - - static propTypes = { - feedSource: PropTypes.object, - project: PropTypes.object, - user: PropTypes.object, - - createDeploymentFromFeedSource: PropTypes.func, - deleteFeedSource: PropTypes.func, - fetchFeed: PropTypes.func, - uploadFeed: PropTypes.func - } - - render () { - const fs = this.props.feedSource - const disabled = !this.props.user.permissions.hasFeedPermission(this.props.project.id, fs.id, 'manage-feed') - const isWatchingFeed = this.props.user.subscriptions.hasFeedSubscription(this.props.project.id, fs.id, 'feed-updated') - const editGtfsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.project.id, fs.id, 'edit-gtfs') - - return
    - { - console.log('OK, deleting') - this.props.deleteFeedSource(fs) - }} - /> - - { - if (isValidZipFile(files[0])) { - this.props.uploadFeed(fs, files[0]) - return true - } else { - return false - } - }} - errorMessage='Uploaded file must be a valid zip file (.zip).' - /> - - { - console.log(key) - switch (key) { - case 'delete': - return this.refs['deleteModal'].open() - case 'fetch': - return this.props.fetchFeed(fs) - case 'upload': - return this.refs['uploadModal'].open() - case 'deploy': - return this.props.createDeploymentFromFeedSource(fs) - case 'public': - return browserHistory.push(`/public/feed/${fs.id}`) - } - }} - id={`feed-source-action-button`} - pullRight - > - - - - Fetch - Upload - {isModuleEnabled('deployment') || getConfigProperty('application.notifications_enabled') - ? - : null - } - {isModuleEnabled('deployment') - ? Deploy - : null - } - {getConfigProperty('application.notifications_enabled') - ? - : null - } - View public page - - Delete - - -
    - } -} diff --git a/src/main/client/manager/components/FeedSourceViewer.js b/src/main/client/manager/components/FeedSourceViewer.js deleted file mode 100644 index 96275d755..000000000 --- a/src/main/client/manager/components/FeedSourceViewer.js +++ /dev/null @@ -1,637 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import fetch from 'isomorphic-fetch' -import Icon from 'react-fa' -import Helmet from 'react-helmet' -import { sentence as toSentenceCase } from 'change-case' -import { LinkContainer } from 'react-router-bootstrap' -import { Grid, Row, Col, ListGroup, ListGroupItem, Well, Button, Table, Image, Badge, Panel, Label, Glyphicon, ButtonToolbar, ButtonGroup, Tabs, Tab, FormControl, InputGroup, ControlLabel, FormGroup, Checkbox, DropdownButton, MenuItem } from 'react-bootstrap' -import { Link, browserHistory } from 'react-router' -import moment from 'moment' -import numeral from 'numeral' - -import ManagerPage from '../../common/components/ManagerPage' -import Breadcrumbs from '../../common/components/Breadcrumbs' -import EditableTextField from '../../common/components/EditableTextField' -import WatchButton from '../../common/containers/WatchButton' -import StarButton from '../../common/containers/StarButton' -import { isValidZipFile, retrievalMethodString } from '../../common/util/util' -import ExternalPropertiesTable from './ExternalPropertiesTable' -import ActiveFeedVersionNavigator from '../containers/ActiveFeedVersionNavigator' -import NotesViewer from './NotesViewer' -import ActiveEditorFeedSourcePanel from '../../editor/containers/ActiveEditorFeedSourcePanel' -import { isModuleEnabled, getComponentMessages, getMessage, getConfigProperty } from '../../common/util/config' - -const retrievalMethods = [ - 'FETCHED_AUTOMATICALLY', - 'MANUALLY_UPLOADED', - 'PRODUCED_IN_HOUSE' -] - -export default class FeedSourceViewer extends Component { - - static propTypes = { - feedSource: PropTypes.object, - feedSourceId: PropTypes.string, - feedVersionIndex: PropTypes.number, - isFetching: PropTypes.bool, - project: PropTypes.object, - routeParams: PropTypes.object, - user: PropTypes.object, - activeComponent: PropTypes.string, - activeSubComponent: PropTypes.string, - - createDeployment: PropTypes.func, - deleteFeedVersionConfirmed: PropTypes.func, - downloadFeedClicked: PropTypes.func, - externalPropertyChanged: PropTypes.func, - feedSourcePropertyChanged: PropTypes.func, - feedVersionRenamed: PropTypes.func, - gtfsPlusDataRequested: PropTypes.func, - loadFeedVersionForEditing: PropTypes.func, - newNotePostedForFeedSource: PropTypes.func, - newNotePostedForVersion: PropTypes.func, - notesRequestedForFeedSource: PropTypes.func, - notesRequestedForVersion: PropTypes.func, - onComponentMount: PropTypes.func, - fetchFeed: PropTypes.func, - updateUserSubscription: PropTypes.func, - uploadFeed: PropTypes.func, - fetchValidationResult: PropTypes.func - } - - constructor (props) { - super(props) - this.state = {} - } - - componentWillMount () { - this.props.onComponentMount(this.props) - // this.setState({willMount: true}) - } - componentDidMount () { - this.setState({didMount: true}) - - } - componentDidUpdate (prevProps) { - this.props.componentDidUpdate(prevProps, this.props) - if (this.props.feedSource && this.state.didMount) { - this.setState({didMount: false}) - } - } - - getAverageFileSize (feedVersions) { - let sum = 0 - let avg - if (feedVersions) { - for (var i = 0; i < feedVersions.length; i++) { - sum += feedVersions[i].fileSize - } - avg = sum / feedVersions.length - } - return numeral(avg || 0).format('0 b') - } - deleteFeedVersion (feedSource, feedVersion) { - this.refs['page'].showConfirmModal({ - title: 'Delete Feed Version?', - body: 'Are you sure you want to delete this version?', - onConfirm: () => { - this.props.deleteFeedVersionConfirmed(feedSource, feedVersion) - } - }) - } - - // showUploadFeedModal () { - // this.refs.page.showSelectFileModal({ - // title: 'Upload Feed', - // body: 'Select a GTFS feed (.zip) to upload:', - // onConfirm: (files) => { - // if (isValidZipFile(files[0])) { - // this.props.uploadFeed(this.props.feedSource, files[0]) - // return true - // } else { - // return false - // } - // }, - // errorMessage: 'Uploaded file must be a valid zip file (.zip).' - // }) - // } - - render () { - const fs = this.props.feedSource - if (this.props.isFetching && this.state.didMount) { - return ( - - -

    - -

    -
    -
    - ) - } else if (!fs) { - return ( - - - - -

    No feed source found for {this.props.feedSourceId}

    -

    Return to list of projects

    - -
    -
    -
    - ) - } - - const messages = getComponentMessages('FeedSourceViewer') - const disabled = !this.props.user.permissions.hasFeedPermission(this.props.project.id, fs.id, 'manage-feed') - const isWatchingFeed = this.props.user.subscriptions.hasFeedSubscription(this.props.project.id, fs.id, 'feed-updated') - const editGtfsDisabled = !this.props.user.permissions.hasFeedPermission(this.props.project.id, fs.id, 'edit-gtfs') - const dateFormat = getConfigProperty('application.date_format') - const autoFetchFeed = fs.retrievalMethod === 'FETCHED_AUTOMATICALLY' - const fsCenter = fs.latestValidation && fs.latestValidation.bounds - ? `${(fs.latestValidation.bounds.east + fs.latestValidation.bounds.west) / 2},${(fs.latestValidation.bounds.north + fs.latestValidation.bounds.south) / 2}` - : null - const fsOverlay = fsCenter - ? `pin-l-bus(${fsCenter})/` - : '' - const mapUrl = fsCenter - ? `https://api.mapbox.com/v4/mapbox.light/${fsOverlay}${fsCenter},6/1000x400@2x.png?access_token=${getConfigProperty('mapbox.access_token')}` - : '' - const resourceType = this.props.activeComponent === 'settings' && this.props.activeSubComponent && this.props.activeSubComponent.toUpperCase() - const activeTab = ['settings', 'comments', 'snapshots'].indexOf(this.props.activeComponent) === -1 || typeof this.props.routeParams.feedVersionIndex !== 'undefined' - ? '' - : this.props.activeComponent - console.log(this.props.activeComponent, this.props.routeParams.feedVersionIndex) - const activeSettings = !resourceType - ? - Settings}> - - - - Feed source name - - { - this.setState({name: evt.target.value}) - }} - /> - - - - - - - - - this.props.feedSourcePropertyChanged(fs, 'deployable', !fs.deployable)}>Make feed source deployable - Enable this feed source to be deployed to an OpenTripPlanner (OTP) instance (defined in organization settings) as part of a collection of feed sources or individually. - - - - - Automatic fetch}> - - - - Feed source fetch URL - - { - this.setState({url: evt.target.value}) - }} - /> - - - - - - - - - this.props.feedSourcePropertyChanged(fs, 'retrievalMethod', autoFetchFeed ? 'MANUALLY_UPLOADED' : 'FETCHED_AUTOMATICALLY')} bsStyle='danger'>Auto fetch feed source - Set this feed source to fetch automatically. (Feed source URL must be specified and project auto fetch must be enabled.) - - - - - Danger zone}> - - - -

    Make this feed source {fs.isPublic ? 'private' : 'public'}.

    -

    This feed source is currently {fs.isPublic ? 'public' : 'private'}.

    -
    - - -

    Delete this feed source.

    -

    Once you delete a feed source, it cannot be recovered.

    -
    -
    -
    - - : - { - this.props.externalPropertyChanged(fs, resourceType, name, value) - }} - /> - - return ( - - } - > - - - {/* Title + Shortcut Buttons Row */} - -

    - - {this.props.project.name} - {' '}/{' '} - {fs.name}{' '} - {fs.isPublic ? null : } - {' '} - {fs.editedSinceSnapshot - ? - : - } - - - - - - -

    -
      -
    • {fs.lastUpdated ? moment(fs.lastUpdated).format(dateFormat) : 'n/a'}
    • -
    • {fs.url ? fs.url : '(none)'} -
    • - {
    • {this.getAverageFileSize(fs.feedVersions)}
    • } -
    - {/*
  • {fs.feedVersionCount}
  • {fs.url}*/} - -
    - - {/* Feed Versions tab */} - browserHistory.push(`/feed/${fs.id}/${eventKey}`))} - > - {getMessage(messages, 'gtfs')}}> - - {/* - -

    Feed Summary

    -
      -
    • Last Updated: {fs.lastUpdated ? moment(fs.lastUpdated).format(dateFormat) : 'n/a'}
    • -
    • Number of versions: {fs.feedVersionCount}
    • -
      -
    • URL: this.props.feedSourcePropertyChanged(fs, 'url', value)} - /> -
    • -
    -
    - */} - - - {/**/} - {/* - - - {isModuleEnabled('editor') - ? - : null - } - - - - -
    - Fetch URL:  -
    -
    - this.props.feedSourcePropertyChanged(fs, 'url', value)} - /> -
    - -
    - */} - - - - {/* - - - - - - - - -
    Create new version
    -
      -
    • -
    • -
      -
    • -
    - -
    */} - {/* - New version} - > - Upload - Fetch - - From snapshot - - - */} - -
    - {/* - 12 Versions - */} - - - - - {/* - Snapshots}> - - - Snapshot 1 - - - - */} - -
    - - {isModuleEnabled('editor') - ? {getComponentMessages('EditorFeedSourcePanel').title} {fs.editorSnapshots ? fs.editorSnapshots.length : 0}} - > - - - : null - } - {/* Comments for feed source */} - {getComponentMessages('NotesViewer').title} {fs.noteCount}} - onEnter={() => this.props.notesRequestedForFeedSource(fs)} - > - { this.props.notesRequestedForFeedSource(fs) }} - newNotePosted={(note) => { this.props.newNotePostedForFeedSource(fs, note) }} - /> - - {/* Settings */} - {getMessage(messages, 'properties.title')}}> - - - - - General - {Object.keys(fs.externalProperties || {}).map(resourceType => { - const resourceLowerCase = resourceType.toLowerCase() - return ( - {toSentenceCase(resourceType)} properties - ) - })} - - - - - {/* - - - - - - - - - - - - - - - - - { - this.props.feedSourcePropertyChanged(fs, 'retrievalMethod', evt.target.value) - }} - > - {retrievalMethods.map(method => { - return - })} - - - - {this.props.feedSource.retrievalMethod === 'MANUALLY_UPLOADED' - ? - : - } - - - - - - {fs.retrievalMethod === 'FETCHED_AUTOMATICALLY' - ? - - - - : null - } - - - - - - - - - - -
    {getMessage(messages, 'properties.property')}{getMessage(messages, 'properties.value')}
    Name - this.props.feedSourcePropertyChanged(fs, 'name', value)} - /> -
    {getMessage(messages, 'properties.retrievalMethod.title')} - -
    Retrieval URL - this.props.feedSourcePropertyChanged(fs, 'url', value)} - /> -
    {getMessage(messages, 'properties.public')} - { - this.props.feedSourcePropertyChanged(fs, 'isPublic', e.target.checked) - }} - /> -
    {getMessage(messages, 'properties.deployable')} - { - this.props.feedSourcePropertyChanged(fs, 'deployable', e.target.checked) - }} - /> -
    - */} - - {activeSettings} - {/* - - {Object.keys(fs.externalProperties || {}).map(resourceType => { - return ( - { - this.props.externalPropertyChanged(fs, resourceType, name, value) - }} - /> - ) - })} - */} - -
    -
    -
    -
    -
    - ) - } -} diff --git a/src/main/client/manager/components/FeedVersionNavigator.js b/src/main/client/manager/components/FeedVersionNavigator.js deleted file mode 100644 index b035393e9..000000000 --- a/src/main/client/manager/components/FeedVersionNavigator.js +++ /dev/null @@ -1,199 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import { Row, Col, ButtonGroup, ButtonToolbar, DropdownButton, MenuItem, Button, Glyphicon } from 'react-bootstrap' -import { browserHistory } from 'react-router' -import Icon from 'react-fa' - -import { isModuleEnabled, getComponentMessages, getMessage } from '../../common/util/config' -import { isValidZipFile } from '../../common/util/util' -import FeedVersionViewer from './FeedVersionViewer' -import ConfirmModal from '../../common/components/ConfirmModal' -import SelectFileModal from '../../common/components/SelectFileModal' - -export default class FeedVersionNavigator extends Component { - - static propTypes = { - deleteDisabled: PropTypes.bool, - feedSource: PropTypes.object, - feedVersionIndex: PropTypes.number, - isPublic: PropTypes.bool, - - deleteFeedVersionConfirmed: PropTypes.func, - downloadFeedClicked: PropTypes.func, - feedVersionRenamed: PropTypes.func, - gtfsPlusDataRequested: PropTypes.func, - loadFeedVersionForEditing: PropTypes.func, - newNotePostedForVersion: PropTypes.func, - notesRequestedForVersion: PropTypes.func, - fetchValidationResult: PropTypes.func - } - - constructor (props) { - super(props) - this.state = {} - } - - componentWillReceiveProps (nextProps) { - } - - render () { - console.log(this.props) - const versionTitleStyle = { - fontSize: '24px', - fontWeight: 'bold' - } - const { disabled } = this.props - const fs = this.props.feedSource - const versions = this.props.feedSource.feedVersions - const messages = getComponentMessages('FeedVersionNavigator') - const hasVersions = versions && versions.length > 0 - - const sortedVersions = hasVersions && versions.sort((a, b) => { - if (a.updated < b.updated) return -1 - if (a.updated > b.updated) return 1 - return 0 - }) - - let version - - if (typeof this.props.feedVersionIndex === 'undefined') { - return null - } else if (hasVersions && versions.length >= this.props.feedVersionIndex) { - version = sortedVersions[this.props.feedVersionIndex - 1] - } else { - console.log(`Error version ${this.props.feedVersionIndex} does not exist`) - } - - const publicPrefix = this.props.isPublic ? '/public' : '' - - return ( -
    - { - console.log('OK, deleting') - this.props.deleteFeedSource(fs) - }} - /> - { - console.log(files[0].type) - if (isValidZipFile(files[0])) { - this.props.uploadFeed(fs, files[0]) - return true - } else { - return false - } - }} - errorMessage='Uploaded file must be a valid zip file (.zip).' - /> - - {/* Version Navigation Widget and Name Editor */} - - - - - - - {this.state.listView - ? null - : {/* Version Navigation/Selection Widget */} - {/* Previous Version Button */} - - - {/* Version Selector Dropdown */} - browserHistory.push(`${publicPrefix}/feed/${fs.id}/version/${key}`)} - > - {versions.map((version, k) => { - k = k + 1 - return {k}. {version.name} - })} - - - {/* Next Version Button */} - - - } - - - {isModuleEnabled('editor') - ? - : null - } - Create new version} id='bg-nested-dropdown' - onSelect={key => { - console.log(key) - switch (key) { - case 'delete': - return this.refs['deleteModal'].open() - case 'fetch': - return this.props.fetchFeed(fs) - case 'upload': - return this.refs['uploadModal'].open() - case 'deploy': - return this.props.createDeploymentFromFeedSource(fs) - case 'public': - return browserHistory.push(`/public/feed/${fs.id}`) - } - }} - > - Fetch - Upload - - From snapshot - - - - - - - - { - this.props.fetchValidationResult(version, this.props.isPublic) - }} - gtfsPlusDataRequested={(version) => { - this.props.gtfsPlusDataRequested(version) - }} - notesRequested={() => { this.props.notesRequestedForVersion(version) }} - newNotePosted={(note) => { this.props.newNotePostedForVersion(version, note) }} - {...this.props} - /> - - -
    - ) - } -} diff --git a/src/main/client/manager/components/FeedVersionViewer.js b/src/main/client/manager/components/FeedVersionViewer.js deleted file mode 100644 index 9b4992abd..000000000 --- a/src/main/client/manager/components/FeedVersionViewer.js +++ /dev/null @@ -1,287 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import { Row, Col, Table, Image, ButtonGroup, Button, Panel, Label, Glyphicon, ButtonToolbar, ListGroup, ListGroupItem } from 'react-bootstrap' -import moment from 'moment' -import { LinkContainer } from 'react-router-bootstrap' -import Icon from 'react-fa' -import numeral from 'numeral' - -import GtfsValidationViewer from './validation/GtfsValidationViewer' -import GtfsValidationExplorer from './validation/GtfsValidationExplorer' -import NotesViewer from './NotesViewer' -import ConfirmModal from '../../common/components/ConfirmModal' -import EditableTextField from '../../common/components/EditableTextField' -import ActiveGtfsPlusVersionSummary from '../../gtfsplus/containers/ActiveGtfsPlusVersionSummary' -import { isModuleEnabled, getComponentMessages, getMessage, getConfigProperty } from '../../common/util/config' - -const dateFormat = 'MMM. DD, YYYY' -const timeFormat = 'h:MMa' - -export default class FeedVersionViewer extends Component { - - static propTypes = { - version: PropTypes.object, - versions: PropTypes.array, - - isPublic: PropTypes.bool, - hasVersions: PropTypes.bool, - listView: PropTypes.bool, - - newNotePosted: PropTypes.func, - notesRequested: PropTypes.func, - fetchValidationResult: PropTypes.func, - feedVersionRenamed: PropTypes.func, - downloadFeedClicked: PropTypes.func, - loadFeedVersionForEditing: PropTypes.func - } - getVersionDateLabel (version) { - const now = +moment() - const future = version.validationSummary && version.validationSummary.startDate > now - const expired = version.validationSummary && version.validationSummary.endDate < now - return version.validationSummary - ? - : null - } - render () { - const version = this.props.version - const messages = getComponentMessages('FeedVersionViewer') - - if (!version) return

    {getMessage(messages, 'noVersionsExist')}

    - - const fsCenter = version.validationSummary && version.validationSummary.bounds - ? `${(version.validationSummary.bounds.east + version.validationSummary.bounds.west) / 2},${(version.validationSummary.bounds.north + version.validationSummary.bounds.south) / 2}` - : null - const fsOverlay = fsCenter - ? `pin-l-bus(${fsCenter})/` - : '' - const mapUrl = fsCenter - ? `https://api.mapbox.com/v4/mapbox.light/${fsOverlay}${fsCenter},6/1000x800@2x.png?access_token=${getConfigProperty('mapbox.access_token')}` - : '' - const versionHeader = ( -
    -

    - {/* Name Display / Editor */} - {version.validationSummary.loadStatus === 'SUCCESS' && version.validationSummary.errorCount === 0 - ? - : version.validationSummary.errorCount > 0 - ? - : - } - {this.props.isPublic - ? {version.name} - : this.props.feedVersionRenamed(version, value)} - /> - } - -

    - Version published {moment(version.updated).fromNow()} -
    - ) - - - if (this.props.listView) { - return ( - - - List of feed versions}> - - {this.props.versions - ? this.props.versions.map(v => { - return ( - - {v.name} - {' '} - - {this.getVersionDateLabel(v)} - - - - ) - }) - : - No versions - - } - - - - - ) - } - - switch (this.props.versionSection) { - case 'validation': - return - default: - return ( - - - - - Version summary - Validation issues - {isModuleEnabled('gtfsplus') - ? GTFS+ for this version - : null - } - Version comments - - - - - {!this.props.versionSection - ? {numeral(version.fileSize || 0).format('0 b')} zip file last modified at {version.fileTimestamp ? moment(version.fileTimestamp).format(timeFormat + ', ' + dateFormat) : 'N/A' }} - > - - - - - - {`${moment(version.validationSummary.startDate).format(dateFormat)} to ${moment(version.validationSummary.endDate).format(dateFormat)}`} - {' '} - - {this.getVersionDateLabel(version)} - - - } - > - {getMessage(messages, 'validDates')} - - - - -

    {numeral(version.validationSummary.agencyCount).format('0 a')}

    -

    {getMessage(messages, 'agencyCount')}

    - - -

    {numeral(version.validationSummary.routeCount).format('0 a')}

    -

    {getMessage(messages, 'routeCount')}

    - - -

    {numeral(version.validationSummary.tripCount).format('0 a')}

    -

    {getMessage(messages, 'tripCount')}

    - - -

    {numeral(version.validationSummary.stopTimesCount).format('0 a')}

    -

    {getMessage(messages, 'stopTimesCount')}

    - -
    -
    -
    -
    - : this.props.versionSection === 'issues' - ? { this.props.fetchValidationResult(version) }} - /> - : this.props.versionSection === 'gtfsplus' && isModuleEnabled('gtfsplus') - ? - : this.props.versionSection === 'comments' - ? { this.props.notesRequested() }} - newNotePosted={(note) => { this.props.newNotePosted(note) }} - /> - : null - } - {/**/} - -
    - ) - } - } -} - -class VersionButtonToolbar extends Component { - render () { - const version = this.props.version - const messages = getComponentMessages('FeedVersionViewer') - return ( -
    - - - - {/* "Download Feed" Button */} - - - {/* "Load for Editing" Button */} - {isModuleEnabled('editor') && !this.props.isPublic - ? - : null - } - - {/* "Delete Version" Button */} - {!this.props.isPublic - ? - : null - } - -
    - ) - } -} diff --git a/src/main/client/manager/components/ProjectSettings.js b/src/main/client/manager/components/ProjectSettings.js deleted file mode 100644 index 738f9a1cb..000000000 --- a/src/main/client/manager/components/ProjectSettings.js +++ /dev/null @@ -1,599 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import ReactDOM from 'react-dom' -import moment from 'moment' -// import moment_tz from 'moment-timezone' -import DateTimeField from 'react-bootstrap-datetimepicker' -import update from 'react-addons-update' -import { shallowEqual } from 'react-pure-render' -import { LinkContainer } from 'react-router-bootstrap' -import Icon from 'react-fa' - -import { Row, Col, Button, Panel, Glyphicon, Form, Tabs, Tab, Radio, Checkbox, FormGroup, InputGroup, ControlLabel, FormControl, ListGroup, ListGroupItem } from 'react-bootstrap' -import TimezoneSelect from '../../common/components/TimezoneSelect' -import LanguageSelect from '../../common/components/LanguageSelect' -// import languages from '../../common/util/languages' -import { isModuleEnabled, getComponentMessages, getMessage } from '../../common/util/config' -import MapModal from '../../common/components/MapModal.js' - -export default class ProjectSettings extends Component { - static propTypes = { - project: PropTypes.object, - projectEditDisabled: PropTypes.bool, - updateProjectSettings: PropTypes.func, - deleteProject: PropTypes.func - } - constructor (props) { - super(props) - this.state = { - general: {}, - deployment: { - buildConfig: {}, - routerConfig: {}, - otpServers: this.props.project && this.props.project.otpServers ? this.props.project.otpServers : [] - } - } - } - - componentWillReceiveProps (nextProps) { - this.setState({ - general: {}, - deployment: { - buildConfig: {}, - routerConfig: {}, - otpServers: nextProps.project && nextProps.project.otpServers ? nextProps.project.otpServers : [] - } - }) - } - shouldComponentUpdate (nextProps, nextState) { - return !shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state) - } - render () { - const messages = getComponentMessages('ProjectSettings') - const project = this.props.project - const generalSettingsUnedited = Object.keys(this.state.general).length === 0 && this.state.general.constructor === Object - const deploymentSettingsUnedited = - Object.keys(this.state.deployment.buildConfig).length === 0 && - Object.keys(this.state.deployment.buildConfig).length === 0 && - shallowEqual(this.state.deployment.otpServers, this.props.project.otpServers) - const autoFetchChecked = typeof this.state.general.autoFetchFeeds !== 'undefined' ? this.state.general.autoFetchFeeds : project.autoFetchFeeds - const projectEditDisabled = this.props.projectEditDisabled - const defaultFetchTime = moment().startOf('day').add(2, 'hours') - const activeSettingsPanel = !this.props.activeSettingsPanel - ?
    - {getMessage(messages, 'title')}}> - - - - Organization name - - { - this.setState({name: evt.target.value}) - }} - /> - - - - - - - - - {getMessage(messages, 'general.updates.title')}}> - - - - { - let minutes = moment(defaultFetchTime).minutes() - let hours = moment(defaultFetchTime).hours() - let stateUpdate = { general: { $merge: { autoFetchFeeds: evt.target.checked, autoFetchMinute: minutes, autoFetchHour: hours } } } - this.setState(update(this.state, stateUpdate)) - }} - > - {getMessage(messages, 'general.updates.autoFetchFeeds')} - - {autoFetchChecked - ? { - let time = moment(+seconds) - let minutes = moment(time).minutes() - let hours = moment(time).hours() - let stateUpdate = { general: { $merge: { autoFetchMinute: minutes, autoFetchHour: hours } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - : null - } - - - - - {getMessage(messages, 'general.location.title')}}> - - - - {getMessage(messages, 'general.location.defaultLocation')} - - { - const latLng = evt.target.value.split(',') - if (typeof latLng[0] !== 'undefined' && typeof latLng[1] !== 'undefined') { - let stateUpdate = { general: { $merge: {defaultLocationLat: latLng[0], defaultLocationLon: latLng[1]} } } - this.setState(update(this.state, stateUpdate)) - } else { - console.log('invalid value for latlng') - } - }} - /> - - - - - - - - - {getMessage(messages, 'general.location.boundingBox')} - - { - const bBox = evt.target.value.split(',') - if (bBox.length === 4) { - let stateUpdate = { general: { $merge: {west: bBox[0], south: bBox[1], east: bBox[2], north: bBox[3]} } } - this.setState(update(this.state, stateUpdate)) - } - }} - /> - { - - - - } - - - - - {getMessage(messages, 'general.location.defaultTimeZone')} - { - let stateUpdate = { general: { $merge: { defaultTimeZone: option.value } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - {getMessage(messages, 'general.location.defaultLanguage')} - { - let stateUpdate = { general: { $merge: { defaultLanguage: option.value } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - - Danger zone}> - - - -

    Delete this organization.

    -

    Once you delete an organization, the organization and all feed sources it contains cannot be recovered.

    -
    -
    -
    - - - {/* Save button */} - - - -
    - :
    - {getMessage(messages, 'deployment.buildConfig.title')}}> - - - - { - let stateUpdate = { deployment: { buildConfig: { fetchElevationUS: { $set: (evt.target.value === 'true') } } } } - this.setState(update(this.state, stateUpdate)) - }} - > - - - - - - - - { - let stateUpdate = { deployment: { buildConfig: { stationTransfers: { $set: (evt.target.value === 'true') } } } } - this.setState(update(this.state, stateUpdate)) - }} - > - - - - - - - - { - let stateUpdate = { deployment: { buildConfig: { subwayAccessTime: { $set: +evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - { - let stateUpdate = { deployment: { buildConfig: { fares: { $set: evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - Router Config}> - - - - { - let stateUpdate = { deployment: { routerConfig: { numItineraries: { $set: +evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - - - { - let stateUpdate = { deployment: { routerConfig: { walkSpeed: { $set: +evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - - - - - { - let stateUpdate = { deployment: { routerConfig: { stairsReluctance: { $set: +evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - - - { - let stateUpdate = { deployment: { routerConfig: { carDropoffTime: { $set: +evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - - - { - let stateUpdate = { deployment: { routerConfig: { brandingUrlRoot: { $set: evt.target.value } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - - - {getMessage(messages, 'deployment.servers.title')} - - }> -
    - {this.state.deployment.otpServers && this.state.deployment.otpServers.map((server, i) => { - let title = ( -
    - {server.name}{' '} - {server.publicUrl} -
    - ) - return ( - -
    - - - {getMessage(messages, 'deployment.servers.name')} - { - let stateUpdate = { deployment: { otpServers: { [i]: { $merge: { name: evt.target.value } } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - {getMessage(messages, 'deployment.servers.public')} - { - let stateUpdate = { deployment: { otpServers: { [i]: { $merge: { publicUrl: evt.target.value } } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - - {getMessage(messages, 'deployment.servers.internal')} - { - let stateUpdate = { deployment: { otpServers: { [i]: { $merge: { internalUrl: evt.target.value.split(',') } } } } } - this.setState(update(this.state, stateUpdate)) - }} - /> - - { - let stateUpdate = { deployment: { otpServers: { [i]: { $merge: { admin: evt.target.checked } } } } } - this.setState(update(this.state, stateUpdate)) - }} - > - {getMessage(messages, 'deployment.servers.admin')} - -
    -
    - ) - }) - - } -
    -
    - {getMessage(messages, 'deployment.osm.title')}}> - { - let stateUpdate = { deployment: { useCustomOsmBounds: { $set: (evt.target.value === 'true') } } } - this.setState(update(this.state, stateUpdate)) - }} - > - - {getMessage(messages, 'deployment.osm.gtfs')} - - - {getMessage(messages, 'deployment.osm.custom')} - - - {project.useCustomOsmBounds || this.state.deployment.useCustomOsmBounds - ? {getMessage(messages, 'deployment.osm.bounds')})} - ref='osmBounds' - onChange={(evt) => { - const bBox = evt.target.value.split(',') - if (bBox.length === 4) { - let stateUpdate = { deployment: { $merge: { osmWest: bBox[0], osmSouth: bBox[1], osmEast: bBox[2], osmNorth: bBox[3] } } } - this.setState(update(this.state, stateUpdate)) - } - }} - /> - : null - } - - - - {/* Save button */} - - - -
    - - return ( - - - - - {getMessage(messages, 'general.title')} - {isModuleEnabled('deployment') - ? {getMessage(messages, 'deployment.title')} - : null - } - - - - - {activeSettingsPanel} - - - - - ) - } -} diff --git a/src/main/client/manager/components/ProjectViewer.js b/src/main/client/manager/components/ProjectViewer.js deleted file mode 100644 index db2b81b6b..000000000 --- a/src/main/client/manager/components/ProjectViewer.js +++ /dev/null @@ -1,440 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import Helmet from 'react-helmet' -import moment from 'moment' -import { Tabs, Tab, Grid, Row, Label, Col, Button, InputGroup, Table, FormControl, Glyphicon, ButtonToolbar, Panel, DropdownButton, MenuItem } from 'react-bootstrap' -import { sentence as toSentenceCase } from 'change-case' -import Icon from 'react-fa' -import { browserHistory, Link } from 'react-router' -import { shallowEqual } from 'react-pure-render' - -import ManagerPage from '../../common/components/ManagerPage' -import Breadcrumbs from '../../common/components/Breadcrumbs' -import WatchButton from '../../common/containers/WatchButton' -import ProjectSettings from './ProjectSettings' -import FeedSourceTable from './FeedSourceTable' -import EditableTextField from '../../common/components/EditableTextField' -import { defaultSorter } from '../../common/util/util' -import { isModuleEnabled, isExtensionEnabled, getComponentMessages, getMessage, getConfigProperty } from '../../common/util/config' - -export default class ProjectViewer extends Component { - static propTypes = { - project: PropTypes.object, - onComponentMount: PropTypes.func, - - visibilityFilter: PropTypes.object, - visibilityFilterChanged: PropTypes.func, - searchTextChanged: PropTypes.func, - } - constructor (props) { - super(props) - - this.state = {} - } - - deleteFeedSource (feedSource) { - this.refs['page'].showConfirmModal({ - title: 'Delete Feed Source?', - body: `Are you sure you want to delete the feed source ${feedSource.name}?`, - onConfirm: () => { - console.log('OK, deleting') - this.props.deleteFeedSourceConfirmed(feedSource) - } - }) - } - - showUploadFeedModal (feedSource) { - this.refs.page.showSelectFileModal({ - title: 'Upload Feed', - body: 'Select a GTFS feed to upload:', - onConfirm: (files) => { - let nameArray = files[0].name.split('.') - if (files[0].type !== 'application/zip' || nameArray[nameArray.length - 1] !== 'zip') { - return false - } - else { - this.props.uploadFeedClicked(feedSource, files[0]) - return true - } - }, - errorMessage: 'Uploaded file must be a valid zip file (.zip).' - }) - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - render () { - if(!this.props.project) { - return - } - const messages = getComponentMessages('ProjectViewer') - const isWatchingProject = this.props.user.subscriptions.hasProjectSubscription(this.props.project.id, 'project-updated') - const projectEditDisabled = !this.props.user.permissions.isProjectAdmin(this.props.project.id) - const filteredFeedSources = this.props.project.feedSources - ? this.props.project.feedSources.filter(feedSource => { - if(feedSource.isCreating) return true // feeds actively being created are always visible - let visible = feedSource.name !== null ? feedSource.name.toLowerCase().indexOf((this.props.visibilityFilter.searchText || '').toLowerCase()) !== -1 : '[unnamed project]' - switch (this.props.visibilityFilter.filter) { - case 'ALL': - return visible - case 'STARRED': - return [].indexOf(feedSource.id) !== -1 // check userMetaData - case 'PUBLIC': - return feedSource.isPublic - case 'PRIVATE': - return !feedSource.isPublic - default: - return visible - } - }).sort(defaultSorter) - : [] - const projectsHeader = ( - - - - { - this.props.visibilityFilterChanged(key) - }} - > - All - Starred - Public - Private - - this.props.searchTextChanged(evt.target.value)} - /> - - - - - - {isExtensionEnabled('transitland') || isExtensionEnabled('transitfeeds') || isExtensionEnabled('mtc') - ? Sync}> - {isExtensionEnabled('transitland') - ? { - this.props.thirdPartySync('TRANSITLAND') - }} - > - transit.land - - : null - } - {isExtensionEnabled('transitfeeds') - ? { - this.props.thirdPartySync('TRANSITFEEDS') - }} - > - transitfeeds.com - - : null - } - {isExtensionEnabled('mtc') - ? { - this.props.thirdPartySync('MTC') - }} - > - MTC - - : null - } - - : null - } - - - - - - ) - return ( - - } - > - - - - -

    - {this.props.project.name} - - {getConfigProperty('application.notifications_enabled') - ? - : null - } - -

    -
      -
    • {this.props.project.defaultLocationLon ? `${this.props.project.defaultLocationLat}, ${this.props.project.defaultLocationLon}` : 'n/a'}
    • -
    • {this.props.project.autoFetchFeeds ? `${this.props.project.autoFetchHour}:${this.props.project.autoFetchMinute < 10 ? '0' + this.props.project.autoFetchMinute : this.props.project.autoFetchMinute}` : 'Auto fetch disabled'}
    • - {/* -
    • {fs.feedVersions ? `${this.getAverageFileSize(fs.feedVersions)} MB` : 'n/a'}
    • - */} -
    - -
    - { - if (key === 'sources') { - browserHistory.push(`/project/${this.props.project.id}/`) - } - else { - browserHistory.push(`/project/${this.props.project.id}/${key}`) - } - if (key === 'deployments' && !this.props.project.deployments) { - this.props.deploymentsRequested() - } - }} - > - {getMessage(messages, 'feeds.title')}} - > - - - - - - - - What is a feed source?}> - A feed source defines the location or upstream source of a GTFS feed. GTFS can be populated via automatic fetch, directly editing or uploading a zip file. - - - - - {isModuleEnabled('deployment') - ? {getMessage(messages, 'deployments')}} - > - - - : null - } - {getMessage(messages, 'settings')}} - > - - - -
    -
    - ) - } -} - -class DeploymentsPanel extends Component { - static propTypes = { - deployments: PropTypes.object, - deleteDeploymentConfirmed: PropTypes.func, - deploymentsRequested: PropTypes.func, - onNewDeploymentClick: PropTypes.func, - newDeploymentNamed: PropTypes.func, - updateDeployment: PropTypes.func, - expanded: PropTypes.bool - } - constructor (props) { - super(props) - } - componentWillMount () { - if (this.props.expanded) { - this.props.deploymentsRequested() - } - } - shouldComponentUpdate (nextProps, nextState) { - return !shallowEqual(nextProps.deployments, this.props.deployments) - } - // deleteDeployment (deployment) { - // console.log(this.refs) - // this.refs['page'].showConfirmModal({ - // title: 'Delete Deployment?', - // body: `Are you sure you want to delete the deployment ${deployment.name}?`, - // onConfirm: () => { - // console.log('OK, deleting') - // this.props.deleteDeploymentConfirmed(deployment) - // } - // }) - // } - render () { - const messages = getComponentMessages('DeploymentsPanel') - const na = (N/A) - return ( - - - - - this.props.searchTextChanged(evt.target.value)} - /> - - - - - - } - > - - - - - - - - - - - - {this.props.deployments - ? this.props.deployments.map(dep => { - return ( - - - - - - - - ) - }) - : null - } - -
    {getMessage(messages, 'table.name')}{getMessage(messages, 'table.creationDate')}{getMessage(messages, 'table.deployedTo')}{getMessage(messages, 'table.feedCount')}
    - { - if (dep.isCreating) this.props.newDeploymentNamed(value) - else this.props.updateDeployment(dep, {name: value}) - }} - link={`/deployment/${dep.id}`} - /> - - {dep.dateCreated - ? ({moment(dep.dateCreated).format('MMM Do YYYY')} ({moment(dep.dateCreated).fromNow()})) - : na - } - - {dep.deployedTo - ? () - : na - } - - {dep.feedVersions - ? ({dep.feedVersions.length}) - : na - } - - -
    - - - - Deploying feeds to OTP}> -

    A collection of feeds can be deployed to OpenTripPlanner (OTP) instances that have been defined in the organization settings.

    -
    - - - ) - } -} diff --git a/src/main/client/manager/components/ProjectsList.js b/src/main/client/manager/components/ProjectsList.js deleted file mode 100644 index fa0447e83..000000000 --- a/src/main/client/manager/components/ProjectsList.js +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react' -import Helmet from 'react-helmet' -import { Grid, Row, Col, Button, Table, FormControl, Panel, OverlayTrigger, Popover, Glyphicon } from 'react-bootstrap' -import { Link } from 'react-router' - -import ManagerPage from '../../common/components/ManagerPage' -import EditableTextField from '../../common/components/EditableTextField' -import defaultSorter from '../../common/util/util' -import { getComponentMessages, getMessage } from '../../common/util/config' - -export default class ProjectsList extends React.Component { - - constructor (props) { - super(props) - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - - render () { - - if (!this.props.projects) { - return - } - const messages = getComponentMessages('ProjectsList') - const projectCreationDisabled = !this.props.user.permissions.isApplicationAdmin() - const visibleProjects = this.props.projects.filter((project) => { - if(project.isCreating) return true // projects actively being created are always visible - return project.name.toLowerCase().indexOf((this.props.visibilitySearchText || '').toLowerCase()) !== -1 - }).sort(defaultSorter) - - return ( - - - - Projects)}> - - - this.props.searchTextChanged(evt.target.value)} - /> - - - - - {getMessage(messages, 'help.content')}}> - - - - - - - - - - - - - - - {visibleProjects.length > 0 ? visibleProjects.map((project) => { - let disabled = !this.props.user.permissions.isProjectAdmin(project.id) - return ( - - - - - ) - }) - : - - } - -
    {getMessage(messages, 'table.name')}
    -
    - { - if(project.isCreating) this.props.newProjectNamed(value) - else this.props.projectNameChanged(project, value) - }} - link={`/project/${project.id}`} - /> -
    -
    -
    - {getMessage(messages, 'noProjects')} - {' '} - -
    - -
    -
    -
    -
    - ) - } -} diff --git a/src/main/client/manager/components/UserHomePage.js b/src/main/client/manager/components/UserHomePage.js deleted file mode 100644 index bc1a46df2..000000000 --- a/src/main/client/manager/components/UserHomePage.js +++ /dev/null @@ -1,283 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import { Grid, Row, Col, Panel, Button, ButtonToolbar, ButtonGroup, Jumbotron, Badge, FormControl, ListGroup, ListGroupItem, DropdownButton, MenuItem } from 'react-bootstrap' -import Icon from 'react-fa' -import { Link } from 'react-router' -import { LinkContainer } from 'react-router-bootstrap' -import moment from 'moment' - -import ManagerPage from '../../common/components/ManagerPage' -import { getConfigProperty, getComponentMessages, getMessage } from '../../common/util/config' -import { defaultSorter, getProfileLink } from '../../common/util/util' - -export default class UserHomePage extends Component { - - static propTypes = { - user: PropTypes.object, - projects: PropTypes.array, - - onComponentMount: PropTypes.func, - logoutHandler: PropTypes.func, - - visibilityFilter: PropTypes.object, - searchTextChanged: PropTypes.func, - visibilityFilterChanged: PropTypes.func - } - constructor (props) { - super(props) - this.state = {} - } - componentWillMount () { - this.props.onComponentMount(this.props) - } - componentWillUnmount () { - this.setState({showLoading: true}) - } - render () { - const messages = getComponentMessages('UserHomePage') - const projectCreationDisabled = !this.props.user.permissions.isApplicationAdmin() - const feedVisibilityFilter = (feed) => { - let visible = feed.name.toLowerCase().indexOf((this.props.visibilityFilter.searchText || '').toLowerCase()) !== -1 - switch (this.props.visibilityFilter.filter) { - case 'ALL': - return visible - case 'STARRED': - return [].indexOf(feed.id) !== -1 // check userMetaData - case 'PUBLIC': - return feed.isPublic - case 'PRIVATE': - return !feed.isPublic - default: - return visible - } - // if (feed.isCreating) return true // feeds actively being created are always visible - } - const renderFeedItems = (p, fs) => { - const feedName = `${p.name} / ${fs.name}` - return ( - - - - - {feedName.length > 33 ? `${feedName.substr(0, 33)}...` : feedName} - - - - ) - } - const visibleProjects = this.props.projects.sort(defaultSorter) - const activeProject = this.props.project - const sortByDate = (a, b) => { - if (a.date < b.date) return 1 - if (a.date > b.date) return -1 - return 0 - } - - return ( - - - {this.state.showLoading ? : null} - - - {/* Top Welcome Box */} - -

    Welcome to {getConfigProperty('application.title')}!

    -

    Manage, edit, validate and deploy your data in a streamlined workflow.

    -

    - - - - -

    -
    - {/* Recent Activity List */} -

    - Recent Activity -

    - {this.props.user.recentActivity && this.props.user.recentActivity.length - ? this.props.user.recentActivity.sort(sortByDate).map(item => renderRecentActivity(item)) - : No Recent Activity for your subscriptions. - } - - - - {/* User Account Info Panel */} - - - -

    - - Hello, {this.props.user.profile.nickname}. -

    - -
    - - - - - -
    {this.props.user.profile.email}
    -
    - {this.props.user.permissions.isApplicationAdmin() - ? 'Application admin' - : 'Standard user' - } -
    -
    - - - - - {this.props.user.permissions.isApplicationAdmin() - ? - - - : null - } - -
    - -
    -
    -
    - {activeProject - ? - : null - } - {activeProject.name} - : {this.props.user.profile.nickname} - } - // onSelect={(eventKey) => { - // this.props.setActiveProject(eventKey) - // }} - > - {activeProject - ? [ - - - {this.props.user.profile.nickname} - - , - - ] - : null - } - {visibleProjects.length > 0 - ? visibleProjects.map((project, index) => { - if (activeProject && project.id === activeProject.id) { - return null - } - return ( - - - {project.name} - - - ) - }) - : null - } - {activeProject && visibleProjects.length > 1 || !activeProject ? : null} - Manage organizations - - Create organization - -
    - {/* Starred Feeds Panel */} - Your feeds)}> - - - this.props.searchTextChanged(evt.target.value)} - /> - - - - - - - - {activeProject && activeProject.feedSources - ? activeProject.feedSources.filter(feedVisibilityFilter).map(fs => renderFeedItems(activeProject, fs)) - : this.props.projects && this.props.projects.map(p => { - return p.feedSources && p.feedSources.filter(feedVisibilityFilter).map(fs => renderFeedItems(p, fs)) - }) - } - - - -
    -
    -
    - ) - } -} - -function renderRecentActivity (item) { - const containerStyle = { - marginTop: 10, - paddingBottom: 12, - borderBottom: '1px solid #ddd' - } - - const iconStyle = { - float: 'left', - fontSize: 20, - color: '#bbb' - } - - const dateStyle = { - color: '#999', - fontSize: 11, - marginBottom: 2 - } - - const innerContainerStyle = { - marginLeft: 36 - } - - const commentStyle = { - backgroundColor: '#f0f0f0', - marginTop: 8, - padding: 8, - fontSize: 12 - } - - switch (item.type) { - case 'feed-commented-on': - return ( -
    -
    - -
    -
    -
    {moment(item.date).fromNow()}
    -
    {item.userName} commented on feed {item.targetName}:
    -
    {item.body}
    -
    -
    - ) - } -} diff --git a/src/main/client/manager/components/validation/GtfsValidationExplorer.js b/src/main/client/manager/components/validation/GtfsValidationExplorer.js deleted file mode 100644 index b802e9b1b..000000000 --- a/src/main/client/manager/components/validation/GtfsValidationExplorer.js +++ /dev/null @@ -1,114 +0,0 @@ -import React, {Component, PropTypes} from 'react' -import { Grid, Row, Col, ButtonGroup, Button, Glyphicon, Tabs, Tab } from 'react-bootstrap' -import { Link, browserHistory } from 'react-router' - -import ManagerPage from '../../../common/components/ManagerPage' -import Breadcrumbs from '../../../common/components/Breadcrumbs' -import IssuesMap from './IssuesMap' -import IsochroneMap from './IsochroneMap' -import GtfsValidationSummary from './GtfsValidationSummary' -import TripsChart from './TripsChart' -import { getComponentMessages, getMessage } from '../../../common/util/config' - -export default class GtfsValidationExplorer extends Component { - - constructor (props) { - super(props) - this.state = { - activeTab: 'issues' - } - } - - componentWillMount () { - // this.props.onComponentMount(this.props) - if (this.props.version && !this.props.version.validationResult) { - this.props.fetchValidationResult(this.props.version) - } - } - - componentWillReceiveProps (nextProps) { - // - if(this.props.version && nextProps && nextProps.version && - this.props.version.id !== nextProps.version.id && - !nextProps.version.validationResult) { - this.props.fetchValidationResult(nextProps.version) - } - } - - render() { - const version = this.props.version - const messages = getComponentMessages('GtfsValidationExplorer') - - if (!version || !this.props.version.validationResult) { - return ( - - - - - - - - - ) - } - - const tabRowStyle = { marginTop: '20px' } - - return ( - - - { - this.setState({activeTab: key}) - setTimeout(() => { - const map = this.refs[key + '-map'] - if(map) map.initializeMap() - }, 100); // Adjust timeout to tab transition - }} - > - - - - - - - { this.props.fetchValidationResult(version) }} - /> - - - - - - - - - - - - - - - - - - - - - - - ) - } -} diff --git a/src/main/client/manager/components/validation/GtfsValidationMap.js b/src/main/client/manager/components/validation/GtfsValidationMap.js deleted file mode 100644 index e38cceb4e..000000000 --- a/src/main/client/manager/components/validation/GtfsValidationMap.js +++ /dev/null @@ -1,274 +0,0 @@ -import React from 'react' -import { Grid, Row, Col, Button, Table, Input, Panel, Glyphicon } from 'react-bootstrap' -import { Link, browserHistory } from 'react-router' -import { Map, Marker, Popup, TileLayer, Rectangle, GeoJson, FeatureGroup } from 'react-leaflet' - -import ManagerPage from '../../../common/components/ManagerPage' -import GtfsValidationSummary from './GtfsValidationSummary' - -export default class GtfsValidationMap extends React.Component { - - constructor (props) { - super(props) - this.state = { - mapHeight: '500px' - } - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - componentDidMount () { - window.addEventListener('resize', this.handleResize) - } - handleResize () { - console.log(window.innerHeight) - } - render () { - console.log(this.props.version) - - if (!this.props.version || !this.props.version.validationResult) { - return ( - - - - - - - - - ) - } - const bs = ( - - - - - - - - - - - - - ) - const nonbs = ( - - - - - - - - ) - return ( - - - - - - - - - - - ) - } -} - -class ValidationMap extends React.Component { - constructor (props) { - super(props) - } - render () { - const version = this.props.version - const validation = version.validationResult - let errors = {} - validation && validation.errors.map(error => { - if (!errors[error.file]) { - errors[error.file] = [] - } - errors[error.file].push(error) - }) - const summary = version.validationSummary - console.log(validation) - const bounds = [[summary.bounds.north, summary.bounds.east], [summary.bounds.south, summary.bounds.west]] - - const getIsochrones = (e) => { - console.log(e) - const center = this.refs.validationMap.leafletElement.getCenter() - console.log(center) - this.props.fetchIsochrones(this.props.version, e.latlng.lat, e.latlng.lng, center.lat, center.lng) - } - const getIsochroneColor = (time) => { - return time ? 'blue' : 'red' - } - // console.log(errors.stop) - console.log(errors.route) - const stopIssues = errors.stop ? errors.stop.map(stop => { - // if (!stop.problemData) return null - // - // let s1 = stop.problemData.stop1 ? stop.problemData.stop1 : null - let s2 = null // stop.problemData.stop2 ? stop.problemData.stop2 : null - let s1 = stop.stop - return ( - - {s1 ? - - -
    -

    {s1.stop_name}

    -

    {stop.errorType}

    -
    -
    -
    - : null} - {s2 ? - - -
    -

    {s2.stop_name}

    -

    {stop.errorType}

    -
    -
    -
    - : null} -
    - ) - }) : null - const mapStyle = { - // position: 'absolute', - // top: '50px', - // bottom: 0, - height: '620px', - // height: '88%', - } - return ( - console.log(e)} - scrollWheelZoom={true} - > - -
    - -
    - - {stopIssues} - { - version.isochrones ? version.isochrones.features.map(iso => { - if (iso.properties.time !== 60*60) return null - return ( - { - console.log(feature) - return { - color: getIsochroneColor(iso.properties.time), - } - }} - onEachFeature={(feature, layer) => { - // feature.properties.time = iso.properties.time - }} - > - - - ) - }) - : null - } -
    - ) - } -} - -class ValidationPanel extends React.Component { - constructor (props) { - super(props) - this.state = { - isVisible: true - } - } - render () { - const version = this.props.version - - const validation = this.props.version.validationResult - console.log(validation) - const dockStyle = { - marginLeft: '20px', - marginRight: '20px', - } - const panelStyle = { - position: 'absolute', - right: 0, - // height: '88%', - // width: '33%', - backgroundColor: 'white', - } - return ( -
    -

    {version.feedSource.name} Validation Results

    - - { this.props.fetchValidationResult(version) }} - /> -
    - ) - } -} diff --git a/src/main/client/manager/components/validation/GtfsValidationViewer.js b/src/main/client/manager/components/validation/GtfsValidationViewer.js deleted file mode 100644 index 2ba438a81..000000000 --- a/src/main/client/manager/components/validation/GtfsValidationViewer.js +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react' -import { Panel, Table, Glyphicon, Button, Badge } from 'react-bootstrap' -import { browserHistory } from 'react-router' -import Icon from 'react-fa' - -import { isModuleEnabled, isExtensionEnabled, getComponentMessages, getMessage } from '../../../common/util/config' - -export default class GtfsValidationViewer extends React.Component { - - constructor (props) { - super(props) - this.state = { expanded: false } - } - componentWillMount () { - this.props.fetchValidationResult() - } - componentWillReceiveProps (nextProps) { - if(!nextProps.validationResult) this.setState({ expanded: false }) - } - - render () { - - const result = this.props.validationResult - const messages = getComponentMessages('GtfsValidationViewer') - - const header = ( -

    { - if(!result) this.props.fetchValidationResult() - this.setState({ expanded: !this.state.expanded }) - }}> - {getMessage(messages, 'title')} -

    - ) - - let report = null - let files = ['routes', 'stops', 'trips', 'shapes', 'stop_times'] - let errors = {} - result && result.errors.map(error => { - let key = files.indexOf(error.file) !== -1 ? error.file : 'other' - if (!errors[error.file]) { - errors[key] = [] - } - errors[key].push(error) - }) - if (result && errors) { // && result.loadStatus === 'SUCCESS') { - report = ( -
    - {files.map(file => { - return ( - - ) - })} - -
    - ) - } else if (result) { - report = (
    {getMessage(messages, 'noResults')}
    ) - } - - return
    -

    - {isModuleEnabled('validator') - ? - : null - } -

    - {report} -
    - } -} - -class ResultTable extends React.Component { - - render () { - const tableStyle = { - tableLayout: 'fixed' - } - const messages = getComponentMessages('ResultTable') - - const breakWordStyle = { - wordWrap: 'break-word', - overflowWrap: 'break-word' - } - if (!this.props.invalidValues) { - return ( - {this.props.title} 0)} - > - No issues found. - - ) - } - return ( - {this.props.title} {this.props.invalidValues.length})} - > - - - - - - - - - - - {this.props.invalidValues.map((val, index) => { - return ( - - - - - - - ) - })} - -
    {getMessage(messages, 'problemType')}{getMessage(messages, 'priority')}{getMessage(messages, 'affectedIds')}{getMessage(messages, 'description')}
    {val.errorType}{val.priority}{val.affectedEntityId}{val.message}
    -
    - ) - } -} diff --git a/src/main/client/manager/components/validation/IsochroneMap.js b/src/main/client/manager/components/validation/IsochroneMap.js deleted file mode 100644 index 8ea173c85..000000000 --- a/src/main/client/manager/components/validation/IsochroneMap.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import { Map, Marker, Popup, TileLayer, Rectangle, GeoJson, FeatureGroup } from 'react-leaflet' - -import ValidationMap from './ValidationMap' - -export default class IsochroneMap extends ValidationMap { - - constructor (props) { - super(props) - } - - mapClicked (e) { - this.fetchIsochrones(e.latlng) - } - - fetchIsochrones (latlng) { - const center = super.getMap().leafletElement.getCenter() - this.props.fetchIsochrones(this.props.version, latlng.lat, latlng.lng, center.lat, center.lng) - this.setState({ lastClicked: latlng }) - } - - getMapComponents () { - let comps = [] - - if(this.props.version && this.props.version.isochrones) { - comps = this.props.version.isochrones.features.map((iso, index) => { - if (iso.properties.time !== 60*60) return null - return ( - { - return { - color: this.getIsochroneColor(iso.properties.time), - } - }} - /> - ) - }) - } - - if(this.state && this.state.lastClicked) { - comps.push( - { - this.fetchIsochrones(e.target.getLatLng()) - }} - /> - ) - } - - return comps - } - - getIsochroneColor (time) { - return time ? 'blue' : 'red' - } -} diff --git a/src/main/client/manager/components/validation/IssuesMap.js b/src/main/client/manager/components/validation/IssuesMap.js deleted file mode 100644 index 5375b9367..000000000 --- a/src/main/client/manager/components/validation/IssuesMap.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import { Map, Marker, Popup, TileLayer, Rectangle, GeoJson, FeatureGroup } from 'react-leaflet' - -import ValidationMap from './ValidationMap' - -export default class IssuesMap extends ValidationMap { - - constructor (props) { - super(props) - } - - getMapComponents () { - const validation = this.props.version.validationResult - - let errors = {} - - validation && validation.errors.map(error => { - if (!errors[error.file]) { - errors[error.file] = [] - } - errors[error.file].push(error) - }) - - return errors.stop ? errors.stop.map(stop => { - let s2 = null // stop.problemData.stop2 ? stop.problemData.stop2 : null - let s1 = stop.stop - return ( - - {s1 ? - - -
    -

    {s1.stop_name}

    -

    {stop.errorType}

    -
    -
    -
    - : null} - {s2 ? - - -
    -

    {s2.stop_name}

    -

    {stop.errorType}

    -
    -
    -
    - : null} -
    - ) - }) : null - } -} diff --git a/src/main/client/manager/components/validation/TripsChart.js b/src/main/client/manager/components/validation/TripsChart.js deleted file mode 100644 index 1b2c8b1bc..000000000 --- a/src/main/client/manager/components/validation/TripsChart.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react' -import moment from 'moment' -// import rd3 from 'rd3' - -export default class TripsChart extends React.Component { - - render () { - const data = Object.keys(this.props.data).map(key => [key, this.props.data[key]]) - const graphHeight = 400 - const spacing = 8 - const leftMargin = 50, bottomMargin = 50 - const svgWidth = leftMargin + data.length * spacing, svgHeight = graphHeight + bottomMargin - const maxTrips = Math.max.apply(Math, data.map(d => d[1])) - const yAxisMax = Math.ceil(maxTrips / 1000) * 1000; - - const yAxisPeriod = maxTrips > 1000 ? 1000 : 100 - const yAxisLabels = []; - for (var i = yAxisPeriod; i <= yAxisMax; i += yAxisPeriod) { - yAxisLabels.push(i); - } - return ( -
    - - {yAxisLabels.map((l, index) => { - const y = graphHeight - l / yAxisMax * graphHeight - return - - - {l} - - - })} - {data.map((d, index) => { - const dow = moment(d[0]).day() - const x = leftMargin + spacing/2 + index * spacing - - // generate the bar for this date - return - - {index % 14 === 0 /* label the date every 14 days */ - ? - - - {d[0]} - - - : null - } - - }) - } - - -
    - ) - } -} diff --git a/src/main/client/manager/components/validation/ValidationMap.js b/src/main/client/manager/components/validation/ValidationMap.js deleted file mode 100644 index e509ebc3c..000000000 --- a/src/main/client/manager/components/validation/ValidationMap.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import { Map, Marker, Popup, TileLayer, Rectangle, GeoJson, FeatureGroup } from 'react-leaflet' - -export default class ValidationMap extends React.Component { - - constructor (props) { - super(props) - } - - getMap() { - return this.refs.map - } - - initializeMap() { - if(this.mapInitialized || this.props.initialized) return - const leafletMap = this.getMap().leafletElement - leafletMap.invalidateSize() - const summary = this.props.version.validationSummary - const bounds = [[summary.bounds.north, summary.bounds.east], [summary.bounds.south, summary.bounds.west]] - leafletMap.fitBounds(bounds) - this.mapInitialized = true - } - - mapClicked (evt) { } - - getMapComponents () { - return null - } - - render () { - const version = this.props.version - const summary = version.validationSummary - const bounds = [[summary.bounds.north, summary.bounds.east], [summary.bounds.south, summary.bounds.west]] - - const mapStyle = { - height: '620px', - } - - return ( - this.mapClicked(e)} - scrollWheelZoom={true} - > - - - - - {this.getMapComponents()} - - - ) - } -} diff --git a/src/main/client/manager/containers/validation/ActiveGtfsValidationMap.js b/src/main/client/manager/containers/validation/ActiveGtfsValidationMap.js deleted file mode 100644 index ba8303da6..000000000 --- a/src/main/client/manager/containers/validation/ActiveGtfsValidationMap.js +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' - -import GtfsValidationMap from '../../components/validation/GtfsValidationMap' - -import { - fetchFeedSource, - fetchFeedVersionIsochrones, - fetchFeedSourceAndProject, - updateFeedSource, - runFetchFeed, - fetchFeedVersions, - uploadFeed, - fetchPublicFeedSource, - receiveFeedVersions, - fetchPublicFeedVersions, - updateExternalFeedResource, - deleteFeedVersion, - fetchValidationResult, - downloadFeedViaToken, - fetchNotesForFeedSource, - postNoteForFeedSource, - fetchNotesForFeedVersion, - postNoteForFeedVersion -} from '../../actions/feeds' - -import { updateTargetForSubscription } from '../../actions/user' - -import { downloadGtfsPlusFeed } from '../../../gtfsplus/actions/gtfsplus' - -const mapStateToProps = (state, ownProps) => { - let feedSourceId = ownProps.routeParams.feedSourceId - let feedVersionId = ownProps.routeParams.feedVersionId - let user = state.user - // find the containing project - let project = state.projects.all - ? state.projects.all.find(p => { - if (!p.feedSources) return false - return (p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1) - }) - : null - - let feedSource, version - if (project) { - feedSource = project.feedSources.find(fs => fs.id === feedSourceId) - } - if (feedSource && feedSource.feedVersions) { - version = feedSource.feedVersions.find(v => v.id === feedVersionId) - } - return { - version, - project, - user - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const feedSourceId = ownProps.routeParams.feedSourceId - const feedVersionId = ownProps.routeParams.feedVersionId - return { - onComponentMount: (initialProps) => { - let unsecured = true - if (initialProps.user.profile !== null) { - unsecured = false - } - if (!initialProps.project) { - dispatch(fetchFeedSourceAndProject(feedSourceId, unsecured)) - .then((feedSource) => { - console.log(feedSource) - return dispatch(fetchFeedVersions(feedSource, unsecured)) - }) - .then((feedVersions) => { - console.log(feedVersions) - let version = feedVersions.find(v => v.id === feedVersionId) - dispatch(fetchValidationResult(version)) - }) - } - else if (!initialProps.feedSource) { - dispatch(fetchFeedSource(feedSourceId)) - .then((feedSource) => { - console.log(feedSource) - return dispatch(fetchFeedVersions(feedSource, unsecured)) - }) - .then((feedVersions) => { - console.log(feedVersions) - let version = feedVersions.find(v => v.id === feedVersionId) - dispatch(fetchValidationResult(version)) - }) - } - else if (!initialProps.feedSource.versions) { - dispatch(fetchFeedVersions(initialProps.feedSource, unsecured)) - .then((feedVersions) => { - console.log(feedVersions) - let version = feedVersions.find(v => v.id === feedVersionId) - dispatch(fetchValidationResult(version)) - }) - } - else if (!initialProps.feedSource.versions.validationResult) { - // dispatch(fetchValidationResult(version)) - } - }, - fetchIsochrones: (feedVersion, fromLat, fromLon, toLat, toLon) => { - dispatch(fetchFeedVersionIsochrones(feedVersion, fromLat, fromLon, toLat, toLon)) - }, - } -} - -const ActiveGtfsValidationMap = connect( - mapStateToProps, - mapDispatchToProps -)(GtfsValidationMap) - -export default ActiveGtfsValidationMap diff --git a/src/main/client/manager/reducers/index.js b/src/main/client/manager/reducers/index.js deleted file mode 100644 index abd0c8a9b..000000000 --- a/src/main/client/manager/reducers/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export user from './user' -export projects from './projects' -export status from './status' -export languages from './languages' -export ui from './ui' diff --git a/src/main/client/public/components/MarkerCluster2.js b/src/main/client/public/components/MarkerCluster2.js deleted file mode 100644 index 58fa24113..000000000 --- a/src/main/client/public/components/MarkerCluster2.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import { PropTypes } from 'react' -import { BaseTileLayer, MapLayer, Marker, Path } from 'react-leaflet' - -import Leaflet from 'leaflet' -require('leaflet.markercluster') - -export class MarkerCluster extends Path { - componentWillMount () { - super.componentWillMount() - console.log('mounting') - const { markers, map, ...props } = this.props - this.leafletElement = Leaflet.markerClusterGroup() - const newMarkers = markers.map( m => { - return ( - - ) - }) - console.log(newMarkers) - this.leafletElement.addLayers(newMarkers) - console.log('added markers') - } - - // componentDidUpdate () { - // const { markers, map, ...props } = this.props - // map.removeLayer(this.leafletElement) - // this.leafletElement = Leaflet.markerClusterGroup() - // this.leafletElement.addLayers(markers.map( m => { - // return ( - // - // ) - // })) - // } - - render () { - return null - } -} - -MarkerCluster.propTypes = { - markers: PropTypes.array.isRequired -} diff --git a/src/main/client/public/components/PublicFeedsViewer.js b/src/main/client/public/components/PublicFeedsViewer.js deleted file mode 100644 index ef536625b..000000000 --- a/src/main/client/public/components/PublicFeedsViewer.js +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react' -import moment from 'moment' -import { Grid, Row, Col, Button, Input, Glyphicon, form } from 'react-bootstrap' -import { Link, browserHistory } from 'react-router' -import { LinkContainer } from 'react-router-bootstrap' -import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table' -import RegionSearch from './RegionSearch' - -import PublicPage from './PublicPage' -import FeedsMap from './FeedsMap' -import { isModuleEnabled, isExtensionEnabled, getComponentMessages, getMessage } from '../../common/util/config' - -export default class PublicFeedsViewer extends React.Component { - - constructor (props) { - super(props) - this.state = { - - } - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - - render () { - const messages = getComponentMessages('PublicFeedsViewer') - if (!this.props.projects) { - return - } - let position = this.state.position - // let explore = - // - // - // Explore Transit Data - // - // - // - // - // - // let exploreHeader = - // - // Explore Transit Data - // - // - let feeds = [] - const feedArray = this.props.projects.map(p => { - const regions = p.name.split(', ') - if (p.feedSources) { - return p.feedSources.map(f => { - feeds.push(f) - return f - }) - } - }) - return ( - - - - - { - console.log(evt) - if (evt && evt.region) { - this.setState({ - position: [evt.region.lat, evt.region.lon], - bounds: [[evt.region.north, evt.region.east], [evt.region.south, evt.region.west]] - }) - } - if (evt && evt.feed) { - browserHistory.push('/public/feed/' + evt.feed.id) - } - else if (evt == null) - this.setState({position: null, bounds: null}) - }} - /> - - - - browserHistory.push('/public/feed/' + feedId) } - bounds={this.state.bounds} - /> - - - - - - - - - {isExtensionEnabled('mtc') - ? null - : - } - - - - - - ) - } -} - -class PublicFeedsTable extends React.Component { - feedFormat (cell, row) { - return cell ? {cell} : '' - } - dateFormat (cell, row) { - return cell ? moment(cell).format('MMMM Do YYYY, h:mm a') : '' - } - - urlFormat (cell, row) { - return cell ? : '' - } - - dateSort (a, b, order) { - return b.lastUpdated - a.lastUpdated - } - - constructor (props) { - super(props) - } - - render () { - let feeds = [] - const messages = getComponentMessages('PublicFeedsTable') - const feedArray = this.props.projects.map(p => { - const regions = p.name.split(', ') - if (p.feedSources) { - return p.feedSources.map(f => { - const feed = { - name: f.name, - id: f.id, - lastUpdated: moment(f.lastUpdated).format('MMMM Do YYYY, h:mm a'), - region: regions[regions.length - 3], - state: regions[regions.length - 2], - country: regions[regions.length - 1], - url: f.url - } - feeds.push(feed) - return feed - }) - } - }) - return ( - - - {getMessage(messages, 'name')} - {getMessage(messages, 'region')} - {getMessage(messages, 'stateProvince')} - {getMessage(messages, 'country')} - {getMessage(messages, 'lastUpdated')} - - {getMessage(messages, 'link')} - - ) - } -} - -class FeedRow extends React.Component { - - constructor (props) { - super(props) - } - - render () { - var buttons; - if (this.props.feed.url){ - // buttons = - } - return ( - - {this.props.feed.name} - {moment(this.props.feed.lastUpdated).format('MMMM Do YYYY, h:mm:ss a')} - - - - - - ) - } -} diff --git a/src/main/client/public/components/SignupPage.js b/src/main/client/public/components/SignupPage.js deleted file mode 100644 index 104b71894..000000000 --- a/src/main/client/public/components/SignupPage.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react' -import moment from 'moment' -import { Grid, Row, Col, Button, Table, FormControl, FormGroup, ControlLabel, Panel, Glyphicon, Badge, ButtonInput, form } from 'react-bootstrap' -import { Link } from 'react-router' -import { LinkContainer } from 'react-router-bootstrap' -import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table' - -import EditableTextField from '../../common/components/EditableTextField' -import PublicPage from './PublicPage' - -export default class SignupPage extends React.Component { - - constructor (props) { - super(props) - } - - render () { - const email = - - Email - { - const email = evt.target.value - this.setState({email}) - }} - /> - - const username = - - Username - { - const name = evt.target.value - this.setState({name}) - }} - /> - - const password = - - Password - { - const password = evt.target.value - this.setState({password}) - }} - /> - - return ( - - - - -

    - Create an account -

    - -
    - - - -
    -

    Already have an account?

    - {/* username */} - {email} - {password} - -
    - -
    -
    -
    - ) - } -} diff --git a/src/main/client/public/components/UserAccount.js b/src/main/client/public/components/UserAccount.js deleted file mode 100644 index 9fbd9333e..000000000 --- a/src/main/client/public/components/UserAccount.js +++ /dev/null @@ -1,208 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import { Grid, Row, Col, Button, Panel, Checkbox, ListGroup, ListGroupItem, ControlLabel } from 'react-bootstrap' -import { Link } from 'react-router' -import Icon from 'react-fa' -import { LinkContainer } from 'react-router-bootstrap' - -import EditableTextField from '../../common/components/EditableTextField' -import ManagerPage from '../../common/components/ManagerPage' -import { getConfigProperty, getComponentMessages, getMessage } from '../../common/util/config' - -export default class UserAccount extends Component { - static propTypes = { - activeComponent: PropTypes.string, - - resetPassword: PropTypes.func - } - constructor (props) { - super(props) - } - - componentWillMount () { - this.props.onComponentMount(this.props) - } - - render () { - var removeIconStyle = { - cursor: 'pointer' - } - const messages = getComponentMessages('UserAccount') - - let subscriptions = this.props.user.profile.app_metadata.datatools.find(dt => dt.client_id === getConfigProperty('auth0.client_id')).subscriptions - const accountSections = [ - { - id: 'profile', - component:
    - Profile information}> - - - Email address - { - this.props.updateUserName(this.props.user, value) - }} - /> - {/* */} - - -

    Avatar

    - - - Change on gravatar.com - -
    - -

    Password

    - -
    -
    -
    -
    - }, - { - id: 'account', - hidden: getConfigProperty('modules.enterprise.enabled') - }, - { - id: 'organizations' - }, - { - id: 'notifications', - hidden: !getConfigProperty('application.notifications_enabled'), - component: -
    - Notification methods} - > - - -

    Watching

    -

    Receive updates to any feed sources or comments you are watching.

    - Email{' '}Web -
    -
    -
    - - - {getMessage(messages, 'notifications.subscriptions')} - - } - > -
      - {subscriptions.length ? subscriptions.map(sub => { - return ( -
    • - {sub.type.replace('-', ' ')}   - { this.props.removeUserSubscription(this.props.user.profile, sub.type) }} - /> -
        - {sub.target.length ? sub.target.map(target => { - let fs = null // this.props.projects ? this.props.projects.reduce(proj => proj.feedSources.filter(fs => fs.id === target)) : null - if (this.props.projects) { - for (var i = 0; i < this.props.projects.length; i++) { - let feed = this.props.projects[i].feedSources ? this.props.projects[i].feedSources.find(fs => fs.id === target) : null - fs = feed ? feed : fs - } - } - return ( -
      • - { - fs ? {fs.name} - : {target} - } {' '} - { this.props.updateUserSubscription(this.props.user.profile, target, sub.type) }} - /> -
      • - ) - }) :
      • No feeds subscribed to.
      • - } -
      -
    • - ) - }) - :
    • No subscriptions.
    • - } -
    -
    -
    - }, - { - id: 'billing', - hidden: getConfigProperty('modules.enterprise.enabled') - } - ] - const activeSection = accountSections.find(section => section.id === this.props.activeComponent) - const visibleComponent = activeSection ? activeSection.component : null - return ( - - - - -

    - - - - My settings -

    - -
    - - - {getMessage(messages, 'personalSettings')}}> - - {accountSections.map(section => { - if (section.hidden) return null - console.log(section.id, messages) - return ( - - - {getMessage(messages, `${section.id}.title`)} - - - ) - })} - - - {getMessage(messages, 'organizationSettings')}}> - - {this.props.projects && this.props.projects.map(project => { - if (project.hidden) return null - - return ( - - - {project.name} - - - ) - })} - - - - - - {visibleComponent} - - -
    -
    - ) - } -} diff --git a/src/main/client/signs/components/DisplaySelector.js b/src/main/client/signs/components/DisplaySelector.js deleted file mode 100644 index 6c30918b9..000000000 --- a/src/main/client/signs/components/DisplaySelector.js +++ /dev/null @@ -1,210 +0,0 @@ -import React, { PropTypes } from 'react' - -import fetch from 'isomorphic-fetch' - -import { Button, Glyphicon, Label } from 'react-bootstrap' - -import { shallowEqual } from 'react-pure-render' - -import Select from 'react-select' - -import { getFeed, getFeedId, getDisplaysUrl } from '../../common/util/modules' - -export default class DisplaySelector extends React.Component { - - constructor (props) { - super(props) - this.state = { - value: this.props.value - } - } - - cacheOptions (options) { - options.forEach(o => { - this.options[o.value] = o.feature - }) - } - - componentWillReceiveProps (nextProps) { - if (!shallowEqual(nextProps.value, this.props.value)) { - this.setState({value: nextProps.value}) - console.log('props received', this.state.value) - } - } - onChange (value) { - this.setState({value}) - } - render () { - if (!this.props.sign) { - return '' - } - var style = { - marginBottom: '15px' - } - const renderValue = (option) => { - let draftLabel = getDraftStatusLabel(option.display) - let publishedLabel = getPublishedStatusLabel(option.display) - let combinedLabel = getDisplayStatusLabel(option.display) - // return {option.label} {draftLabel} {publishedLabel} - return {option.label} {combinedLabel} - } - const renderOption = (option) => { - let draftLabel = getDraftStatusLabel(option.display) - let publishedLabel = getPublishedStatusLabel(option.display) - let combinedLabel = getDisplayStatusLabel(option.display) - return {option.label} {option.location} {combinedLabel} {option.link} - } - const getDisplayStatusLabel = (display) => { - if (!display) return '' - let displayDraftId = display.DraftDisplayConfigurationId - let displayPublishedId = display.PublishedDisplayConfigurationId - // let draftLabel = displayDraftId === null ? - // : displayDraftId !== this.props.sign.id && displayDraftId > 0 ? - // : - // let publishedLabel = displayPublishedId !== this.props.sign.id && displayPublishedId > 0 ? - // : displayPublishedId !== null ? - // : '' - - - let label = displayPublishedId !== this.props.sign.id && displayPublishedId > 0 && displayDraftId === this.props.sign.id ? - : displayPublishedId !== this.props.sign.id && displayPublishedId > 0 ? - : displayPublishedId !== null && displayDraftId !== null ? - : displayDraftId === null ? - : displayDraftId !== this.props.sign.id && displayDraftId > 0 ? - : - return label - } - const getPublishedStatusLabel = (display) => { - if (!display) return '' - let displayDraftId = display.DraftDisplayConfigurationId - let displayPublishedId = display.PublishedDisplayConfigurationId - let draftLabel = displayDraftId === null ? - : displayDraftId !== this.props.sign.id && displayDraftId > 0 ? - : - let publishedLabel = displayPublishedId !== this.props.sign.id && displayPublishedId > 0 ? - : displayPublishedId !== null ? - : '' - let label = displayPublishedId !== this.props.sign.id && displayPublishedId > 0 ? - : displayPublishedId !== null ? - : displayDraftId === null ? - : displayDraftId !== this.props.sign.id && displayDraftId > 0 ? - : - return publishedLabel - } - const getDraftStatusLabel = (display) => { - if (!display) return '' - let displayDraftId = display.DraftDisplayConfigurationId - let displayPublishedId = display.PublishedDisplayConfigurationId - let draftLabel = displayDraftId === null ? - : displayDraftId !== this.props.sign.id && displayDraftId > 0 ? - : - let publishedLabel = displayPublishedId !== this.props.sign.id && displayPublishedId > 0 ? - : displayPublishedId !== null ? - : '' - let label = displayPublishedId !== this.props.sign.id && displayPublishedId > 0 ? - : displayPublishedId !== null ? - : displayDraftId === null ? - : displayDraftId !== this.props.sign.id && displayDraftId > 0 ? - : - return draftLabel - } - const handleValueClick = (val) => { - // Toggle value of draft/published config ID - let configId = null - let pubId = val.display.PublishedDisplayConfigurationId - let draftId = val.display.DraftDisplayConfigurationId - - // allow publishing or unpublishing to display if sign config is published AND display isn't assigned elsewhere - if (this.props.sign.published && (pubId === null || pubId === this.props.sign.id)) { - let newPubId = pubId ? null - : this.props.sign.id - this.props.toggleConfigForDisplay(val.display, 'PUBLISHED', newPubId) - // if (newPub) - // this.props.toggleConfigForDisplay(val.display, 'DRAFT', null) - } else if (pubId === this.props.sign.id) { // else already published to this config (but config not published), you can set it to null - this.props.toggleConfigForDisplay(val.display, 'PUBLISHED', null) - } else { // if config is draft, you can toggle draftId - let newDraftId = draftId ? null - : this.props.sign.id - this.props.toggleConfigForDisplay(val.display, 'DRAFT', newDraftId) - } - } - const filterOptions = (options, filter, values) => { - // Filter already selected values - let valueKeys = values.map(i => i.value) - let filteredOptions = options.filter(option => { - return valueKeys.indexOf(option.value) === -1 - }) - - // Filter by label - if (filter !== undefined && filter != null && filter.length > 0) { - filteredOptions = filteredOptions.filter(option => { - return RegExp(filter, 'ig').test(option.label) - }) - } - - // Append Addition option - if (filteredOptions.length == 0) { - filteredOptions.push({ - label: Create display: {filter}, - value: filter, - create: true - }) - } - - return filteredOptions - }; - const getDisplays = (input) => { - const url = getDisplaysUrl() - return fetch(url) - .then((response) => { - return response.json() - }) - .then((displays) => { - console.log(displays) - const displayOptions = displays !== null && displays.length > 0 ? displays.map(display => ({display, value: display.Id, label: display.DisplayTitle, location: display.LocationDescription })) : [] - return { options: displayOptions } - }) - .catch((error) => { - console.log(error) - return [] - }) - } - const handleChange = (input) => { - console.log('new value', input) - if (input.length && input[input.length - 1] && input[input.length - 1].create === true) { - console.log('creating display!!!') - this.props.createDisplay(input[input.length - 1].value) - return - } - this.onChange(input) - this.props.onChange(input) - } - - const onFocus = (input) => { - // clear options to onFocus to ensure only valid route/stop combinations are selected - this.refs.displaySelect.loadOptions('') - } - - const placeholder = '' - return ( - - ) - } -} diff --git a/src/main/client/signs/components/NoAccessScreen.js b/src/main/client/signs/components/NoAccessScreen.js deleted file mode 100644 index d561ce70d..000000000 --- a/src/main/client/signs/components/NoAccessScreen.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' - -import { Grid, Row, Col } from 'react-bootstrap' - -import ManagerNavbar from '../../common/containers/ManagerNavbar' - -export default class NoAccessScreen extends React.Component { - - constructor (props) { - super(props) - } - - render () { - return ( -
    - - - - - {(() => { - switch (this.props.reason) { - case 'NOT_LOGGED_IN': return You must be logged in to access this area. - case 'INSUFFICIENT_PERMISSIONS': return This user does not have permission to access this area. - default: return Unable to Access Module - } - })()} - - - -
    - ) - } -} diff --git a/src/main/client/signs/components/SignsList.js b/src/main/client/signs/components/SignsList.js deleted file mode 100644 index dd0159a4d..000000000 --- a/src/main/client/signs/components/SignsList.js +++ /dev/null @@ -1,83 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import { Row, ButtonGroup, Button, FormControl, FormGroup } from 'react-bootstrap' -import Icon from 'react-fa' - -import SignPreview from './SignPreview' - -export default class SignsList extends Component { - static propTypes = { - signs: PropTypes.array, - visibilityFilter: PropTypes.object, - isFetching: PropTypes.bool, - editableFeeds: PropTypes.array, - publishableFeeds: PropTypes.array, - - onEditClick: PropTypes.func, - onZoomClick: PropTypes.func, - onDeleteClick: PropTypes.func, - - searchTextChanged: PropTypes.func, - visibilityFilterChanged: PropTypes.func - } - constructor (props) { - super(props) - } - render () { - let sortedSigns = this.props.signs.sort((a, b) => { - if (a.id < b.id) return -1 - if (a.id > b.id) return 1 - return 0 - }) - - return ( -
    - - - this.props.searchTextChanged(evt.target.value)} - defaultValue={this.props.visibilityFilter.searchText} - /> - - - - - - - - -
     
    -
    - - - {this.props.isFetching - ?

    - : sortedSigns.length - ? sortedSigns.map((sign) => { - return - }) - :

    No alerts found.

    - } -
    -
    - ) - } -} diff --git a/src/main/client/signs/reducers/index.js b/src/main/client/signs/reducers/index.js deleted file mode 100644 index 24270e75b..000000000 --- a/src/main/client/signs/reducers/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export activeSign from './activeSign' -export signs from './signs' diff --git a/src/main/client/signs/style.css b/src/main/client/signs/style.css deleted file mode 100644 index e2c76a1aa..000000000 --- a/src/main/client/signs/style.css +++ /dev/null @@ -1,14 +0,0 @@ -@import url('~react-select/dist/react-select.css'); -/*@import url('~bootstrap/dist/css/bootstrap.min.css');*/ -@import url('~react-bootstrap-table/dist/react-bootstrap-table.min.css'); -@import url('~leaflet-draw/dist/leaflet.draw.css'); - - -.leaflet-top, -.leaflet-bottom { - z-index: 0; -} - -.editable-cell:focus { - background-color: '#FFC0CB' -} diff --git a/src/main/java/com/conveyal/datatools/common/status/MonitorableJob.java b/src/main/java/com/conveyal/datatools/common/status/MonitorableJob.java deleted file mode 100644 index b6be36549..000000000 --- a/src/main/java/com/conveyal/datatools/common/status/MonitorableJob.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.conveyal.datatools.common.status; - -import com.conveyal.datatools.manager.DataManager; -import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; -import jdk.nashorn.internal.scripts.JO; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; - -/** - * Created by landon on 6/13/16. - */ -public abstract class MonitorableJob implements Runnable { - - protected String owner; - protected String name; - protected JobType type; - protected EventBus eventBus; - - public String jobId = UUID.randomUUID().toString(); - - protected List nextJobs = new ArrayList<>(); - - public enum JobType { - UNKNOWN_TYPE, - BUILD_TRANSPORT_NETWORK, - CREATE_FEEDVERSION_FROM_SNAPSHOT, - PROCESS_SNAPSHOT, - VALIDATE_FEED, - FETCH_PROJECT_FEEDS, - FETCH_SINGLE_FEED - } - - public MonitorableJob(String owner, String name, JobType type) { - // register job with eventBus - this.eventBus = new EventBus(); - eventBus.register(this); - - this.owner = owner; - this.name = name; - this.type = type; - storeJob(); - } - - public MonitorableJob(String owner) { - this(owner, "Unnamed Job", JobType.UNKNOWN_TYPE); - } - - public String getName() { - return name; - } - - public JobType getType() { - return type; - } - - public abstract Status getStatus(); - - protected void storeJob() { - Set userJobs = DataManager.userJobsMap.get(this.owner); - if (userJobs == null) { - userJobs = new HashSet<>(); - } - userJobs.add(this); - - DataManager.userJobsMap.put(this.owner, userJobs); - } - - protected void jobFinished() { - // kick off any next jobs - for(Runnable job : nextJobs) { - new Thread(job).start(); - } - - // remove this job from the user-job map - Set userJobs = DataManager.userJobsMap.get(this.owner); - if (userJobs != null) userJobs.remove(this); - } - - public void addNextJob(Runnable job) { - nextJobs.add(job); - } - - @Subscribe - public abstract void handleStatusEvent (Map statusMap); - - /** - * Represents the current status of this job. - */ - public static class Status implements Cloneable { - /** What message (defined in messages.) should be displayed to the user? */ - public String message; - - /** Is this deployment completed (successfully or unsuccessfully) */ - public boolean completed; - - /** What was the error (null if no error)? */ - public boolean error; - - /** Is the item currently being uploaded to the server? */ - public boolean uploading; - - // What is the job/task/file called - public String name; - - /** How much of task is complete? */ - public double percentComplete; - - // When was the job initialized? - public String initialized; - - // When was the job last modified? - public String modified; - - // Name of file/item once completed - public String completedName; - - public Status() { - this.error = false; - this.completed = false; - this.initialized = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME); - this.modified= LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME); - this.percentComplete = 0; - } - - public Status clone () { - Status ret = new Status(); - ret.message = message; - ret.completed = completed; - ret.error = error; - ret.uploading = uploading; - ret.name = name; - ret.percentComplete = percentComplete; - ret.initialized = initialized; - ret.modified = modified; - ret.completedName = completedName; - return ret; - } - } -} diff --git a/src/main/java/com/conveyal/datatools/common/status/StatusEvent.java b/src/main/java/com/conveyal/datatools/common/status/StatusEvent.java deleted file mode 100644 index cfdb82fc5..000000000 --- a/src/main/java/com/conveyal/datatools/common/status/StatusEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.conveyal.datatools.common.status; - -/** - * Created by landon on 8/5/16. - */ -public class StatusEvent { - -// MonitorableJob.Status status; - public String message; - public double percentComplete; - public Boolean error; - - public StatusEvent (String message, double percentComplete, Boolean error) { -// this.status = status; - this.message = message; - this.percentComplete = percentComplete; - this.error = error; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/Base.java b/src/main/java/com/conveyal/datatools/editor/controllers/Base.java deleted file mode 100755 index b2f7df0ad..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/Base.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.conveyal.datatools.editor.controllers; - -import com.conveyal.datatools.editor.models.transit.GtfsRouteType; -import com.conveyal.geojson.GeoJsonModule; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.vividsolutions.jts.geom.LineString; -import java.time.LocalDate; - -import com.conveyal.datatools.editor.utils.JacksonSerializers; - -import java.io.IOException; -import java.io.StringWriter; - -public class Base { - public static ObjectMapper mapper = new ObjectMapper(); - private static JsonFactory jf = new JsonFactory(); - - static { - SimpleModule mod = new SimpleModule(); - mod.addDeserializer(LocalDate.class, new JacksonSerializers.LocalDateDeserializer()); - mod.addSerializer(LocalDate.class, new JacksonSerializers.LocalDateSerializer()); - mod.addDeserializer(GtfsRouteType.class, new JacksonSerializers.GtfsRouteTypeDeserializer()); - mod.addSerializer(GtfsRouteType.class, new JacksonSerializers.GtfsRouteTypeSerializer()); - mapper.registerModule(mod); - mapper.registerModule(new GeoJsonModule()); - } - - public static String toJson(Object pojo, boolean prettyPrint) - throws JsonMappingException, JsonGenerationException, IOException { - StringWriter sw = new StringWriter(); - JsonGenerator jg = jf.createJsonGenerator(sw); - if (prettyPrint) { - jg.useDefaultPrettyPrinter(); - } - mapper.writeValue(jg, pojo); - return sw.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/AgencyController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/AgencyController.java deleted file mode 100644 index 10049a367..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/AgencyController.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.Agency; -import com.conveyal.datatools.editor.utils.S3Utils; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - -public class AgencyController { - public static JsonManager json = - new JsonManager<>(Agency.class, JsonViews.UserInterface.class); - public static Object getAgency(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - Object json = null; - try { - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - if(id != null) { - if (!tx.agencies.containsKey(id)) { - tx.rollback(); - halt(404); - } - - json = Base.toJson(tx.agencies.get(id), false); - } - else { - json = Base.toJson(tx.agencies.values(), false); - } - - tx.rollback(); - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return json; - } - - public static Object createAgency(Request req, Response res) { - Agency agency; - String feedId = req.queryParams("feedId"); - if (feedId == null) - halt(400, "You must provide a valid feedId"); - - try { - agency = Base.mapper.readValue(req.body(), Agency.class); - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - if (tx.agencies.containsKey(agency.id)) { - tx.rollback(); - halt(400, "Agency " + agency.id + " already exists"); - } - - tx.agencies.put(agency.id, agency); - tx.commit(); - - return agency; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - - public static Object updateAgency(Request req, Response res) { - Agency agency; - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - try { - agency = Base.mapper.readValue(req.body(), Agency.class); - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - if(!tx.agencies.containsKey(agency.id)) { - tx.rollback(); - halt(400); - } - - tx.agencies.put(agency.id, agency); - tx.commit(); - - return Base.toJson(agency, false); - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object uploadAgencyBranding(Request req, Response res) { - Agency agency; - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - try { - if (feedId == null) { - halt(400); - } - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - if (!tx.agencies.containsKey(id)) { - tx.rollback(); - halt(404); - } - - agency = tx.agencies.get(id); - - String url = S3Utils.uploadBranding(req, id); - System.out.println(url); - // set agencyBrandingUrl to s3 location - agency.agencyBrandingUrl = url; - - tx.agencies.put(id, agency); - tx.commit(); - - return agency; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - public static Object deleteAgency(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - if(id == null) { - halt(400); - } - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - if(!tx.agencies.containsKey(id)) { - tx.rollback(); - halt(400); - } - - tx.agencies.remove(id); - tx.commit(); - - return true; // ok(); - } - - /** duplicate an agency */ - public static Object duplicateAgency(Request req, Response res) { - - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - // make sure the agency exists -// GlobalTx gtx = VersionedDataStore.getGlobalTx(); - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - if (!tx.agencies.containsKey(id)) { - tx.rollback(); - halt(404); - } - tx.rollback(); - - FeedTx.duplicate(id); - return true; // ok(); - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/agency/:id", AgencyController::getAgency, json::write); - options(apiPrefix + "secure/agency", (q, s) -> ""); - get(apiPrefix + "secure/agency", AgencyController::getAgency, json::write); - post(apiPrefix + "secure/agency", AgencyController::createAgency, json::write); - put(apiPrefix + "secure/agency/:id", AgencyController::updateAgency, json::write); - post(apiPrefix + "secure/agency/:id/duplicate", AgencyController::duplicateAgency, json::write); - post(apiPrefix + "secure/agency/:id/uploadbranding", AgencyController::uploadAgencyBranding, json::write); - delete(apiPrefix + "secure/agency/:id", AgencyController::deleteAgency, json::write); - - // Public routes -// get(apiPrefix + "public/agency/:id", AgencyController::getFeedSource, json::write); -// get(apiPrefix + "public/agency", AgencyController::getAllFeedSources, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/CalendarController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/CalendarController.java deleted file mode 100644 index f2ef24c93..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/CalendarController.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Sets; -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.ScheduleException; -import com.conveyal.datatools.editor.models.transit.ServiceCalendar; -import com.conveyal.datatools.editor.models.transit.ServiceCalendar.ServiceCalendarForPattern; -import com.conveyal.datatools.editor.models.transit.Trip; -import org.mapdb.Fun; -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - -import java.util.Calendar; -import java.util.Collection; -import java.util.Set; - - -public class CalendarController { - public static JsonManager json = - new JsonManager<>(Calendar.class, JsonViews.UserInterface.class); - public static Object getCalendar(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - String patternId = req.queryParams("patternId"); - - if (feedId == null) { - feedId = req.session().attribute("feedId"); - } - - if (feedId == null) { - halt(400); - } - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - if (id != null) { - if (!tx.calendars.containsKey(id)) { - halt(404); - tx.rollback(); - } - - else { - ServiceCalendar c = tx.calendars.get(id); - c.addDerivedInfo(tx); - return c; - } - } - else if (patternId != null) { - if (!tx.tripPatterns.containsKey(patternId)) { - tx.rollback(); - halt(404); - } - - Set serviceCalendarIds = Sets.newHashSet(); - for (Trip trip : tx.getTripsByPattern(patternId)) { - serviceCalendarIds.add(trip.calendarId); - } - - Collection ret = - Collections2.transform(serviceCalendarIds, new Function() { - - @Override - public ServiceCalendarForPattern apply(String input) { - ServiceCalendar cal = tx.calendars.get(input); - - Long count = tx.tripCountByPatternAndCalendar.get(new Fun.Tuple2(patternId, cal.id)); - - if (count == null) count = 0L; - - return new ServiceCalendarForPattern(cal, tx.tripPatterns.get(patternId), count); - } - - }); - - return ret; - } - else { - Collection cals = tx.calendars.values(); - for (ServiceCalendar c : cals) { - c.addDerivedInfo(tx); - } - return cals; - } - - tx.rollback(); - } catch (Exception e) { - tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object createCalendar(Request req, Response res) { - ServiceCalendar cal; - FeedTx tx = null; - - try { - cal = Base.mapper.readValue(req.body(), ServiceCalendar.class); - - if (!VersionedDataStore.agencyExists(cal.feedId)) { - halt(400); - } - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(cal.feedId)) - halt(400); - - tx = VersionedDataStore.getFeedTx(cal.feedId); - - if (tx.calendars.containsKey(cal.id)) { - tx.rollback(); - halt(400); - } - - // check if gtfsServiceId is specified, if not create from DB id - if(cal.gtfsServiceId == null) { - cal.gtfsServiceId = "CAL_" + cal.id.toString(); - } - - cal.addDerivedInfo(tx); - - tx.calendars.put(cal.id, cal); - tx.commit(); - - return cal; - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object updateCalendar(Request req, Response res) { - ServiceCalendar cal; - FeedTx tx = null; - - try { - cal = Base.mapper.readValue(req.body(), ServiceCalendar.class); - - if (!VersionedDataStore.agencyExists(cal.feedId)) { - halt(400); - } - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(cal.feedId)) - halt(400); - - tx = VersionedDataStore.getFeedTx(cal.feedId); - - if (!tx.calendars.containsKey(cal.id)) { - tx.rollback(); - halt(400); - } - - // check if gtfsServiceId is specified, if not create from DB id - if(cal.gtfsServiceId == null) { - cal.gtfsServiceId = "CAL_" + cal.id.toString(); - } - - cal.addDerivedInfo(tx); - - tx.calendars.put(cal.id, cal); - - Object json = Base.toJson(cal, false); - - tx.commit(); - - return json; - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object deleteCalendar(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - if (id == null || !tx.calendars.containsKey(id)) { - tx.rollback(); - halt(404); - } - - // we just don't let you delete calendars unless there are no trips on them - Long count = tx.tripCountByCalendar.get(id); - if (count != null && count > 0) { - tx.rollback(); - halt(400, "Cannot delete calendar that is referenced by trips."); - } - - // drop this calendar from any schedule exceptions - for (ScheduleException ex : tx.getExceptionsByCalendar(id)) { - ex.customSchedule.remove(id); - tx.exceptions.put(ex.id, ex); - } - - tx.calendars.remove(id); - - tx.commit(); - - return true; // ok(); - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/calendar/:id", CalendarController::getCalendar, json::write); - options(apiPrefix + "secure/calendar", (q, s) -> ""); - get(apiPrefix + "secure/calendar", CalendarController::getCalendar, json::write); - post(apiPrefix + "secure/calendar", CalendarController::createCalendar, json::write); - put(apiPrefix + "secure/calendar/:id", CalendarController::updateCalendar, json::write); - delete(apiPrefix + "secure/calendar/:id", CalendarController::deleteCalendar, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/FareController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/FareController.java deleted file mode 100644 index 939d0762a..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/FareController.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.Fare; -import com.conveyal.datatools.editor.models.transit.ScheduleException; -import com.conveyal.datatools.editor.models.transit.Trip; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Sets; -import org.mapdb.Fun; -import spark.Request; -import spark.Response; - -import java.util.Calendar; -import java.util.Collection; -import java.util.Set; - -import static spark.Spark.*; -import static spark.Spark.delete; - -/** - * Created by landon on 6/22/16. - */ -public class FareController { - public static JsonManager json = - new JsonManager<>(Calendar.class, JsonViews.UserInterface.class); - public static Object getFare(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - if (feedId == null) { - feedId = req.session().attribute("feedId"); - } - - if (feedId == null) { - halt(400); - } - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - if (id != null) { - if (!tx.fares.containsKey(id)) { - halt(404); - tx.rollback(); - } - - else { - Fare fare = tx.fares.get(id); -// fare.addDerivedInfo(tx); - return fare; - } - } - else { - Collection fares = tx.fares.values(); - for (Fare fare : fares) { -// fare.addDerivedInfo(tx); - } - return fares; - } - - tx.rollback(); - } catch (Exception e) { - tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object createFare(Request req, Response res) { - Fare fare; - FeedTx tx = null; - - try { - fare = Base.mapper.readValue(req.body(), Fare.class); - - if (!VersionedDataStore.agencyExists(fare.feedId)) { - halt(400); - } - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(fare.feedId)) - halt(400); - - tx = VersionedDataStore.getFeedTx(fare.feedId); - - if (tx.fares.containsKey(fare.id)) { - tx.rollback(); - halt(400); - } - - // check if gtfsFareId is specified, if not create from DB id - if(fare.gtfsFareId == null) { - fare.gtfsFareId = "CAL_" + fare.id.toString(); - } - -// fare.addDerivedInfo(tx); - - tx.fares.put(fare.id, fare); - tx.commit(); - - return fare; - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object updateFare(Request req, Response res) { - Fare fare; - FeedTx tx = null; - - try { - fare = Base.mapper.readValue(req.body(), Fare.class); - - if (!VersionedDataStore.agencyExists(fare.feedId)) { - halt(400); - } - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(fare.feedId)) - halt(400); - - tx = VersionedDataStore.getFeedTx(fare.feedId); - - if (!tx.fares.containsKey(fare.id)) { - tx.rollback(); - halt(400); - } - - // check if gtfsFareId is specified, if not create from DB id - if(fare.gtfsFareId == null) { - fare.gtfsFareId = "CAL_" + fare.id.toString(); - } - -// fare.addDerivedInfo(tx); - - tx.fares.put(fare.id, fare); - - Object json = Base.toJson(fare, false); - - tx.commit(); - - return json; - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object deleteFare(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - if (id == null || !tx.fares.containsKey(id)) { - tx.rollback(); - halt(404); - } - -// // we just don't let you delete calendars unless there are no trips on them -// Long count = tx.tripCountByCalendar.get(id); -// if (count != null && count > 0) { -// tx.rollback(); -// halt(400); -// } - - - tx.fares.remove(id); - - tx.commit(); - - return true; // ok(); - } - public static void register (String apiPrefix) { - get(apiPrefix + "secure/fare/:id", FareController::getFare, json::write); - options(apiPrefix + "secure/fare", (q, s) -> ""); - get(apiPrefix + "secure/fare", FareController::getFare, json::write); - post(apiPrefix + "secure/fare", FareController::createFare, json::write); - put(apiPrefix + "secure/fare/:id", FareController::updateFare, json::write); - delete(apiPrefix + "secure/fare/:id", FareController::deleteFare, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/FeedInfoController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/FeedInfoController.java deleted file mode 100644 index 278535845..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/FeedInfoController.java +++ /dev/null @@ -1,226 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.EditorFeed; -import com.conveyal.datatools.editor.models.transit.GtfsRouteType; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.conveyal.gtfs.model.FeedInfo; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import spark.Request; -import spark.Response; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.LocalDate; -import java.util.Iterator; -import java.util.Map; - -import static spark.Spark.*; -import static spark.Spark.delete; -import static spark.Spark.put; - -/** - * Created by landon on 6/14/16. - */ -public class FeedInfoController { - public static JsonManager json = - new JsonManager<>(EditorFeed.class, JsonViews.UserInterface.class); - - public static Object getFeedInfo(Request req, Response res) { - String id = req.params("id"); - - if (id == null) { - return null; - // TODO: return all feedInfos for project? - } - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - - EditorFeed fs = gtx.feeds.get(id); - return fs; - } - - public static Object createFeedInfo(Request req, Response res) { - EditorFeed fs; - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - - try { - fs = Base.mapper.readValue(req.body(), EditorFeed.class); -// -// // check if gtfsAgencyId is specified, if not create from DB id -// if(fs.gtfsAgencyId == null) { -// fs.gtfsAgencyId = "AGENCY_" + fs.id; -// } - - if (gtx.feeds.containsKey(fs.id)) { - gtx.rollback(); - halt(404); - return null; - } - - gtx.feeds.put(fs.id, fs); - gtx.commit(); - - return Base.toJson(fs, false); - } catch (Exception e) { - gtx.rollbackIfOpen(); - e.printStackTrace(); - halt(404); - return null; - } - } - - - public static Object updateFeedInfo(Request req, Response res) throws IOException { - String id = req.params("id"); - - EditorFeed feed; - - try { - feed = Base.mapper.readValue(req.body(), EditorFeed.class); - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - - if(!gtx.feeds.containsKey(feed.id)) { - gtx.rollback(); - halt(400); - return null; - } - - gtx.feeds.put(feed.id, feed); - gtx.commit(); - - return Base.toJson(feed, false); - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - public static void applyJsonToFeedInfo(EditorFeed source, String json) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(json); - Iterator> fieldsIter = node.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - - if (entry.getValue().isNull()) { - continue; - } - - if(entry.getKey().equals("color")) { - source.color = entry.getValue().asText(); - } - - if(entry.getKey().equals("defaultLat")) { - source.defaultLat = entry.getValue().asDouble(); - } - - if(entry.getKey().equals("defaultLon")) { - source.defaultLon = entry.getValue().asDouble(); - } - - if(entry.getKey().equals("routeTypeId")) { - source.defaultRouteType = GtfsRouteType.fromGtfs(entry.getValue().asInt()); - } - - if(entry.getKey().equals("feedPublisherName")) { - source.feedPublisherName = entry.getValue().asText(); - } - - if(entry.getKey().equals("feedVersion")) { - source.feedVersion = entry.getValue().asText(); - } - - if(entry.getKey().equals("feedPublisherUrl")) { - String url = entry.getValue().asText(); - try { - source.feedPublisherUrl = new URL(url); - - } catch (MalformedURLException e) { - halt(400, "URL '" + url + "' not valid."); - } - source.feedPublisherUrl = new URL(entry.getValue().asText()); - } - - if(entry.getKey().equals("feedLang")) { - source.feedLang = entry.getValue().asText(); - } - - if(entry.getKey().equals("feedStartDate")) { - System.out.println(entry.getValue()); - Long seconds = entry.getValue().asLong(); - Long days = seconds / 60 / 60 / 24; -// System.out.println(days); - try { - LocalDate date = LocalDate.ofEpochDay(days); -// System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE)); - source.feedStartDate = date; - } catch (Exception e) { - e.printStackTrace(); - halt(400, seconds + " is not a valid date"); - } - } - - if(entry.getKey().equals("feedEndDate")) { - Long seconds = entry.getValue().asLong(); - Long days = seconds / 60 / 60 / 24; - try { - LocalDate date = LocalDate.ofEpochDay(days); - source.feedEndDate = date; - } catch (Exception e) { - e.printStackTrace(); - halt(400, seconds + " is not a valid date"); - } - } - } - } - // TODO: deleting editor feed is handled in delete feed source? -// public static Object deleteFeedInfo(Request req, Response res) { -// String id = req.params("id"); -// String feedId = req.queryParams("feedId"); -// Object json = null; -// -// if (feedId == null) -// feedId = req.session().attribute("feedId"); -// -// if (feedId == null) { -// halt(400); -// } -// -// FeedTx tx = VersionedDataStore.getFeedTx(feedId); -// try { -// if (!tx.stops.containsKey(id)) { -// halt(404); -// } -// -// if (!tx.getTripPatternsByStop(id).isEmpty()) { -// halt(400); -// } -// -// FeedInfo s = tx.stops.remove(id); -// tx.commit(); -// json = Base.toJson(s, false); -// } catch (Exception e) { -// halt(400); -// e.printStackTrace(); -// } finally { -// tx.rollbackIfOpen(); -// } -// return json; -// } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/feedinfo/:id", FeedInfoController::getFeedInfo, json::write); - options(apiPrefix + "secure/feedinfo", (q, s) -> ""); - get(apiPrefix + "secure/feedinfo", FeedInfoController::getFeedInfo, json::write); - post(apiPrefix + "secure/feedinfo/:id", FeedInfoController::createFeedInfo, json::write); - put(apiPrefix + "secure/feedinfo/:id", FeedInfoController::updateFeedInfo, json::write); -// delete(apiPrefix + "secure/feedinfo/:id", FeedInfoController::deleteFeedInfo, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/RouteController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/RouteController.java deleted file mode 100644 index eccac3398..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/RouteController.java +++ /dev/null @@ -1,323 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.utils.S3Utils; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.Route; -import com.conveyal.datatools.editor.models.transit.Trip; -import com.conveyal.datatools.editor.models.transit.TripPattern; -import org.mapdb.Fun; -import org.mapdb.Fun.Tuple2; - -import java.util.Collection; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class RouteController { - public static JsonManager json = - new JsonManager<>(Route.class, JsonViews.UserInterface.class); - private static Logger LOG = LoggerFactory.getLogger(Route.class); - public static Object getRoute(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - Object json = null; - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - if (id != null) { - if (!tx.routes.containsKey(id)) { - tx.rollback(); - halt(400); - } - - Route route = tx.routes.get(id); - route.addDerivedInfo(tx); - - json = Base.toJson(route, false); - -// return route; - } - else { - Route[] ret = tx.routes.values().toArray(new Route[tx.routes.size()]); - - for (Route r : ret) { - r.addDerivedInfo(tx); - } - - json = Base.toJson(ret, false); - tx.rollback(); -// return json; - } - } catch (Exception e) { - tx.rollbackIfOpen(); - e.printStackTrace(); - halt(400); - } finally { - tx.rollbackIfOpen(); - } - return json; - } - - public static Object createRoute(Request req, Response res) { - Route route; - - try { - route = Base.mapper.readValue(req.body(), Route.class); - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - if (!gtx.feeds.containsKey(route.feedId)) { - gtx.rollback(); - halt(400); - } - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(route.feedId)) - halt(400); - - gtx.rollback(); - - FeedTx tx = VersionedDataStore.getFeedTx(route.feedId); - - if (tx.routes.containsKey(route.id)) { - tx.rollback(); - halt(400); - } - - // check if gtfsRouteId is specified, if not create from DB id - if(route.gtfsRouteId == null) { - route.gtfsRouteId = "ROUTE_" + route.id; - } - - tx.routes.put(route.id, route); - tx.commit(); - - return route; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - - public static Object updateRoute(Request req, Response res) { - Route route; - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - try { - route = Base.mapper.readValue(req.body(), Route.class); - if (feedId == null) { - halt(400); - } - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - if (!tx.routes.containsKey(id)) { - tx.rollback(); - halt(404); - } - - - // check if gtfsRouteId is specified, if not create from DB id - if(route.gtfsRouteId == null) { - route.gtfsRouteId = "ROUTE_" + id; - } - - tx.routes.put(id, route); - tx.commit(); - - return route; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object uploadRouteBranding(Request req, Response res) { - Route route; - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - try { - if (feedId == null) { - halt(400); - } - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - if (!tx.routes.containsKey(id)) { - tx.rollback(); - halt(404); - } - - route = tx.routes.get(id); - - String url = S3Utils.uploadBranding(req, id); - - // set routeBrandingUrl to s3 location - route.routeBrandingUrl = url; - - tx.routes.put(id, route); - tx.commit(); - - return route; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object deleteRoute(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if(id == null || feedId == null) - halt(400); - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - - - try { - if (!tx.routes.containsKey(id)) { - tx.rollback(); - halt(404); - } - - Route r = tx.routes.get(id); - - // delete affected trips - Set> affectedTrips = tx.tripsByRoute.subSet(new Tuple2(r.id, null), new Tuple2(r.id, Fun.HI)); - for (Tuple2 trip : affectedTrips) { - tx.trips.remove(trip.b); - } - - // delete affected patterns - // note that all the trips on the patterns will have already been deleted above - Set> affectedPatts = tx.tripPatternsByRoute.subSet(new Tuple2(r.id, null), new Tuple2(r.id, Fun.HI)); - for (Tuple2 tp : affectedPatts) { - tx.tripPatterns.remove(tp.b); - } - - tx.routes.remove(id); - tx.commit(); - return true; // ok(); - } catch (Exception e) { - tx.rollback(); - e.printStackTrace(); - halt(404, e.getMessage()); - } - return null; - } - - /** merge route from into route into, for the given agency ID */ - public static Object mergeRoutes (Request req, Response res) { - String from = req.queryParams("from"); - String into = req.queryParams("into"); - - String feedId = req.queryParams("feedId"); - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null || from == null || into == null) - halt(400); - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - // ensure the routes exist - if (!tx.routes.containsKey(from) || !tx.routes.containsKey(into)) { - tx.rollback(); - halt(400); - } - - // get all the trip patterns for route from - // note that we clone them here so we can later modify them - Collection tps = Collections2.transform( - tx.tripPatternsByRoute.subSet(new Tuple2(from, null), new Tuple2(from, Fun.HI)), - new Function, TripPattern>() { - @Override - public TripPattern apply(Tuple2 input) { - try { - return tx.tripPatterns.get(input.b).clone(); - } catch (CloneNotSupportedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - throw new RuntimeException(e); - } - } - }); - - for (TripPattern tp : tps) { - tp.routeId = into; - tx.tripPatterns.put(tp.id, tp); - } - - // now move all the trips - Collection ts = Collections2.transform( - tx.tripsByRoute.subSet(new Tuple2(from, null), new Tuple2(from, Fun.HI)), - new Function, Trip>() { - @Override - public Trip apply(Tuple2 input) { - try { - return tx.trips.get(input.b).clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - }); - - for (Trip t : ts) { - t.routeId = into; - tx.trips.put(t.id, t); - } - - tx.routes.remove(from); - - tx.commit(); - return true; // ok(); - } - catch (Exception e) { - e.printStackTrace(); - tx.rollback(); - throw e; - } - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/route/:id", RouteController::getRoute, json::write); - options(apiPrefix + "secure/route", (q, s) -> ""); - get(apiPrefix + "secure/route", RouteController::getRoute, json::write); - post(apiPrefix + "secure/route/merge", RouteController::mergeRoutes, json::write); - post(apiPrefix + "secure/route", RouteController::createRoute, json::write); - put(apiPrefix + "secure/route/:id", RouteController::updateRoute, json::write); - post(apiPrefix + "secure/route/:id/uploadbranding", RouteController::uploadRouteBranding, json::write); - delete(apiPrefix + "secure/route/:id", RouteController::deleteRoute, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/RouteTypeController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/RouteTypeController.java deleted file mode 100644 index 2aaf551be..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/RouteTypeController.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.RouteType; - -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class RouteTypeController { - public static JsonManager json = - new JsonManager<>(RouteType.class, JsonViews.UserInterface.class); - public static Object getRouteType(Request req, Response res) { - String id = req.params("id"); - Object json = null; - try { - GlobalTx tx = VersionedDataStore.getGlobalTx(); - - if(id != null) { - if(tx.routeTypes.containsKey(id)) - json = Base.toJson(tx.routeTypes.get(id), false); - else - halt(404); - - tx.rollback(); - } - else { - json = Base.toJson(tx.routeTypes.values(), false); - tx.rollback(); - } - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return json; - } - - public static Object createRouteType(Request req, Response res) { - RouteType routeType; - - try { - routeType = Base.mapper.readValue(req.body(), RouteType.class); - - GlobalTx tx = VersionedDataStore.getGlobalTx(); - - if (tx.routeTypes.containsKey(routeType.id)) { - tx.rollback(); - halt(400); - } - - tx.routeTypes.put(routeType.id, routeType); - tx.commit(); - - return routeType; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - - public static Object updateRouteType(Request req, Response res) { - RouteType routeType; - - try { - routeType = Base.mapper.readValue(req.body(), RouteType.class); - - if(routeType.id == null) { - halt(400); - } - - GlobalTx tx = VersionedDataStore.getGlobalTx(); - if (!tx.routeTypes.containsKey(routeType.id)) { - tx.rollback(); - halt(404); - } - - tx.routeTypes.put(routeType.id, routeType); - tx.commit(); - - return routeType; - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - // TODO: cascaded delete, etc. - public static Object deleteRouteType(Request req, Response res) { - String id = req.params("id"); - if (id == null) - halt(400); - - GlobalTx tx = VersionedDataStore.getGlobalTx(); - - if (!tx.routeTypes.containsKey(id)) { - tx.rollback(); - halt(400); - } - - tx.routeTypes.remove(id); - tx.commit(); - - return true; // ok(); - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/routetype/:id", RouteTypeController::getRouteType, json::write); - options(apiPrefix + "secure/routetype", (q, s) -> ""); - get(apiPrefix + "secure/routetype", RouteTypeController::getRouteType, json::write); - post(apiPrefix + "secure/routetype", RouteTypeController::createRouteType, json::write); - put(apiPrefix + "secure/routetype/:id", RouteTypeController::updateRouteType, json::write); - delete(apiPrefix + "secure/routetype/:id", RouteTypeController::deleteRouteType, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/ScheduleExceptionController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/ScheduleExceptionController.java deleted file mode 100644 index 7f2f59706..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/ScheduleExceptionController.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.ScheduleException; -import java.time.LocalDate; - -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class ScheduleExceptionController { - public static JsonManager json = - new JsonManager<>(ScheduleException.class, JsonViews.UserInterface.class); - /** Get all of the schedule exceptions for an agency */ - public static Object getScheduleException (Request req, Response res) { - String exceptionId = req.params("exceptionId"); - String feedId = req.queryParams("feedId"); - Object json = null; - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - FeedTx tx = null; - - try { - tx = VersionedDataStore.getFeedTx(feedId); - - if (exceptionId != null) { - if (!tx.exceptions.containsKey(exceptionId)) - halt(400); - else - json = Base.toJson(tx.exceptions.get(exceptionId), false); - } - else { - json = Base.toJson(tx.exceptions.values(), false); - } - tx.rollback(); - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return json; - } - - public static Object createScheduleException (Request req, Response res) { - FeedTx tx = null; - try { - ScheduleException ex = Base.mapper.readValue(req.body(), ScheduleException.class); - - if (!VersionedDataStore.agencyExists(ex.feedId)) { - halt(400); - } - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(ex.feedId)) - halt(400); - - tx = VersionedDataStore.getFeedTx(ex.feedId); - - if (ex.customSchedule != null) { - for (String cal : ex.customSchedule) { - if (!tx.calendars.containsKey(cal)) { - tx.rollback(); - halt(400); - } - } - } - if (ex.addedService != null) { - for (String cal : ex.addedService) { - if (!tx.calendars.containsKey(cal)) { - tx.rollback(); - halt(400); - } - } - } - if (ex.removedService != null) { - for (String cal : ex.removedService) { - if (!tx.calendars.containsKey(cal)) { - tx.rollback(); - halt(400); - } - } - } - - if (tx.exceptions.containsKey(ex.id)) { - tx.rollback(); - halt(400); - } - if (ex.dates != null) { - for (LocalDate date : ex.dates) { - if (tx.scheduleExceptionCountByDate.containsKey(date) && tx.scheduleExceptionCountByDate.get(date) > 0) { - tx.rollback(); - halt(400); - } - } - } - - tx.exceptions.put(ex.id, ex); - - tx.commit(); - - return Base.toJson(ex, false); - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object updateScheduleException (Request req, Response res) { - FeedTx tx = null; - try { - ScheduleException ex = Base.mapper.readValue(req.body(), ScheduleException.class); - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(ex.feedId)) - halt(400); - - if (!VersionedDataStore.agencyExists(ex.feedId)) { - halt(400); - } - - tx = VersionedDataStore.getFeedTx(ex.feedId); - - if (ex.customSchedule != null) { - for (String cal : ex.customSchedule) { - if (!tx.calendars.containsKey(cal)) { - tx.rollback(); - halt(400); - } - } - } - if (ex.addedService != null) { - for (String cal : ex.addedService) { - if (!tx.calendars.containsKey(cal)) { - tx.rollback(); - halt(400); - } - } - } - if (ex.removedService != null) { - for (String cal : ex.removedService) { - if (!tx.calendars.containsKey(cal)) { - tx.rollback(); - halt(400); - } - } - } - - if (!tx.exceptions.containsKey(ex.id)) { - tx.rollback(); - halt(400); - } - - tx.exceptions.put(ex.id, ex); - - tx.commit(); - - return ex; - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object deleteScheduleException (Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - tx.exceptions.remove(id); - tx.commit(); - - return true; // ok(); - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/scheduleexception/:id", ScheduleExceptionController::getScheduleException, json::write); - options(apiPrefix + "secure/scheduleexception", (q, s) -> ""); - get(apiPrefix + "secure/scheduleexception", ScheduleExceptionController::getScheduleException, json::write); - post(apiPrefix + "secure/scheduleexception", ScheduleExceptionController::createScheduleException, json::write); - put(apiPrefix + "secure/scheduleexception/:id", ScheduleExceptionController::updateScheduleException, json::write); - delete(apiPrefix + "secure/scheduleexception/:id", ScheduleExceptionController::deleteScheduleException, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/SnapshotController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/SnapshotController.java deleted file mode 100644 index 4f59a13e2..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/SnapshotController.java +++ /dev/null @@ -1,300 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.jobs.ProcessGtfsSnapshotExport; -import com.conveyal.datatools.editor.jobs.ProcessGtfsSnapshotMerge; -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.editor.models.transit.Stop; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import org.mapdb.Fun; -import org.mapdb.Fun.Tuple2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.conveyal.datatools.editor.utils.JacksonSerializers; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.List; - -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class SnapshotController { - - public static final Logger LOG = LoggerFactory.getLogger(SnapshotController.class); - public static JsonManager json = - new JsonManager<>(Snapshot.class, JsonViews.UserInterface.class); - - - - public static Object getSnapshot(Request req, Response res) throws IOException { - String id = req.params("id"); - String feedId= req.queryParams("feedId"); - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - Object json = null; - try { - if (id != null) { - Tuple2 sid = JacksonSerializers.Tuple2IntDeserializer.deserialize(id); - if (gtx.snapshots.containsKey(sid)) - json = Base.toJson(gtx.snapshots.get(sid), false); - else - halt(404); - } - else { - if (feedId == null) - feedId = req.session().attribute("feedId"); - - Collection snapshots; - if (feedId == null) { - // if it's still null just give them everything - // this is used in GTFS Data Manager to get snapshots in bulk - // TODO this allows any authenticated user to fetch GTFS data for any agency - snapshots = gtx.snapshots.values(); - } - else { - snapshots = gtx.snapshots.subMap(new Tuple2(feedId, null), new Tuple2(feedId, Fun.HI)).values(); - } - - json = Base.toJson(snapshots, false); - } - } finally { - gtx.rollback(); - } - return json; - } - - public static Object createSnapshot (Request req, Response res) { - GlobalTx gtx = null; - try { - // create a dummy snapshot from which to get values - Snapshot original = Base.mapper.readValue(req.body(), Snapshot.class); - Snapshot s = VersionedDataStore.takeSnapshot(original.feedId, original.name, original.comment); - s.validFrom = original.validFrom; - s.validTo = original.validTo; - gtx = VersionedDataStore.getGlobalTx(); - - // the snapshot we have just taken is now current; make the others not current - Collection snapshots = gtx.snapshots.subMap(new Tuple2(s.feedId, null), new Tuple2(s.feedId, Fun.HI)).values(); - for (Snapshot o : snapshots) { - if (o.id.equals(s.id)) - continue; - - Snapshot cloned = o.clone(); - cloned.current = false; - gtx.snapshots.put(o.id, cloned); - } - - gtx.commit(); - - return Base.toJson(s, false); - } catch (IOException e) { - e.printStackTrace(); - halt(400); - if (gtx != null) gtx.rollbackIfOpen(); - } - return null; - } - - public static Boolean importSnapshot (Request req, Response res) { - - Auth0UserProfile userProfile = req.attribute("user"); - String feedVersionId = req.queryParams("feedVersionId"); - - if(feedVersionId == null) { - halt(400, "No FeedVersion ID specified"); - } - - FeedVersion feedVersion = FeedVersion.get(feedVersionId); - if(feedVersion == null) { - halt(404, "Could not find FeedVersion with ID " + feedVersionId); - } - - ProcessGtfsSnapshotMerge processGtfsSnapshotMergeJob = - new ProcessGtfsSnapshotMerge(feedVersion, userProfile.getUser_id()); - - new Thread(processGtfsSnapshotMergeJob).start(); - - halt(200, "{status: \"ok\"}"); - return null; - } - - public static Object updateSnapshot (Request req, Response res) { - String id = req.params("id"); - GlobalTx gtx = null; - try { - Snapshot s = Base.mapper.readValue(req.body(), Snapshot.class); - - Tuple2 sid = JacksonSerializers.Tuple2IntDeserializer.deserialize(id); - - if (s == null || s.id == null || !s.id.equals(sid)) { - LOG.warn("snapshot ID not matched, not updating: {}, {}", s.id, id); - halt(400); - } - - gtx = VersionedDataStore.getGlobalTx(); - - if (!gtx.snapshots.containsKey(s.id)) { - gtx.rollback(); - halt(404); - } - - gtx.snapshots.put(s.id, s); - - gtx.commit(); - - return Base.toJson(s, false); - } catch (IOException e) { - e.printStackTrace(); - if (gtx != null) gtx.rollbackIfOpen(); - halt(400); - } - return null; - } - - public static Object restoreSnapshot (Request req, Response res) { - String id = req.params("id"); - Object json = null; - Tuple2 decodedId = null; - try { - decodedId = JacksonSerializers.Tuple2IntDeserializer.deserialize(id); - } catch (IOException e1) { - halt(400); - } - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - Snapshot local; - try { - if (!gtx.snapshots.containsKey(decodedId)) { - halt(404); - } - - local = gtx.snapshots.get(decodedId); - - List stops = VersionedDataStore.restore(local); - - // the snapshot we have just restored is now current; make the others not current - // TODO: add this loop back in... taken out in order to compile - Collection snapshots = gtx.snapshots.subMap(new Tuple2(local.feedId, null), new Tuple2(local.feedId, Fun.HI)).values(); - for (Snapshot o : snapshots) { - if (o.id.equals(local.id)) - continue; - - Snapshot cloned = o.clone(); - cloned.current = false; - gtx.snapshots.put(o.id, cloned); - } - - Snapshot clone = local.clone(); - clone.current = true; - gtx.snapshots.put(local.id, clone); - gtx.commit(); - - json = Base.toJson(stops, false); - } catch (IOException e) { - e.printStackTrace(); - halt(400); - } finally { - gtx.rollbackIfOpen(); - } - return json; - } - - /** Export a snapshot as GTFS */ - public static Object exportSnapshot (Request req, Response res) { - String id = req.params("id"); - Tuple2 decodedId; - try { - decodedId = JacksonSerializers.Tuple2IntDeserializer.deserialize(id); - } catch (IOException e1) { - halt(400); - return null; - } - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - Snapshot local; - try { - if (!gtx.snapshots.containsKey(decodedId)) { - halt(404); - return null; - } - - local = gtx.snapshots.get(decodedId); - - File out = new File(DataManager.config.get("application.publicDataDirectory").asText(), "gtfs_" + ".zip"); - - new ProcessGtfsSnapshotExport(local, out).run(); - -// redirect(Play.configuration.getProperty("application.appBase") + "/public/data/" + out.getName()); - } finally { - gtx.rollbackIfOpen(); - } - return null; - } - - /** Write snapshot to disk as GTFS */ - public static boolean writeSnapshotAsGtfs (String id, File outFile) { - Tuple2 decodedId; - try { - decodedId = JacksonSerializers.Tuple2IntDeserializer.deserialize(id); - } catch (IOException e1) { - return false; - } - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - Snapshot local; - try { - if (!gtx.snapshots.containsKey(decodedId)) { - return false; - } - - local = gtx.snapshots.get(decodedId); - - new ProcessGtfsSnapshotExport(local, outFile).run(); - } finally { - gtx.rollbackIfOpen(); - } - - return true; - } - - public static Object deleteSnapshot(Request req, Response res) { - String id = req.params("id"); - Tuple2 decodedId; - try { - decodedId = JacksonSerializers.Tuple2IntDeserializer.deserialize(id); - } catch (IOException e1) { - halt(400); - return null; - } - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - - gtx.snapshots.remove(decodedId); - gtx.commit(); - - return true; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/snapshot/:id", SnapshotController::getSnapshot, json::write); - options(apiPrefix + "secure/snapshot", (q, s) -> ""); - get(apiPrefix + "secure/snapshot", SnapshotController::getSnapshot, json::write); - post(apiPrefix + "secure/snapshot", SnapshotController::createSnapshot, json::write); - post(apiPrefix + "secure/snapshot/import", SnapshotController::importSnapshot, json::write); - put(apiPrefix + "secure/snapshot/:id", SnapshotController::updateSnapshot, json::write); - post(apiPrefix + "secure/snapshot/:id/restore", SnapshotController::restoreSnapshot, json::write); - delete(apiPrefix + "secure/snapshot/:id", SnapshotController::deleteSnapshot, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/StopController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/StopController.java deleted file mode 100644 index a101f3091..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/StopController.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.*; -import org.geotools.referencing.GeodeticCalculator; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.stream.Collector; -import java.util.stream.Collectors; - -import org.mapdb.BTreeMap; -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class StopController { - - public static JsonManager json = - new JsonManager<>(Stop.class, JsonViews.UserInterface.class); - - public static Object getStop(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - String patternId = req.queryParams("patternId"); - Boolean majorStops = Boolean.valueOf(req.queryParams("majorStops")); - Double west = null; - if (req.queryParams("west") != null) - west = Double.valueOf(req.queryParams("west")); - Double east = null; - if (req.queryParams("east") != null) - east = Double.valueOf(req.queryParams("east")); - Double north = null; - if (req.queryParams("north") != null) - north = Double.valueOf(req.queryParams("north")); - Double south = null; - if (req.queryParams("south") != null) - south = Double.valueOf(req.queryParams("south")); - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - if (id != null) { - if (!tx.stops.containsKey(id)) { - tx.rollback(); - halt(404); - } - - Object json = Base.toJson(tx.stops.get(id), false); - tx.rollback(); - return json; - } - else if (Boolean.TRUE.equals(majorStops)) { - // get the major stops for the agency - Collection stops = Collections2.transform(tx.majorStops, new Function() { - @Override - public Stop apply(String input) { - // TODO Auto-generated method stub - return tx.stops.get(input); - } - }); - - Object stopsJson = Base.toJson(stops, false); - tx.rollback(); - return stopsJson; - } - else if (west != null && east != null && south != null && north != null) { - Collection matchedStops = tx.getStopsWithinBoundingBox(north, east, south, west); - Object json = Base.toJson(matchedStops, false); - tx.rollback(); - return json; - } - else if (patternId != null) { - if (!tx.tripPatterns.containsKey(patternId)) { - halt(404); - } - - TripPattern p = tx.tripPatterns.get(patternId); - - Collection ret = Collections2.transform(p.patternStops, new Function() { - @Override - public Stop apply(TripPatternStop input) { - return tx.stops.get(input.stopId); - } - }); - - Object json = Base.toJson(ret, false); - tx.rollback(); - return json; - } - // return all - else { - BTreeMap stops; - try { - stops = tx.stops; - Collection matchedStops = stops.values(); - Object json = Base.toJson(matchedStops, false); - tx.rollback(); - return json; - } catch (IllegalAccessError e) { - return new ArrayList<>(); - } - - } - - } catch (Exception e) { - e.printStackTrace(); - halt(400); - tx.rollback(); - } - return null; - } - - public static Object createStop(Request req, Response res) { - FeedTx tx = null; - Object json = null; - try { - Stop stop = Base.mapper.readValue(req.body(), Stop.class); - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(stop.feedId)) - halt(400); - - if (!VersionedDataStore.agencyExists(stop.feedId)) { - halt(400); - } - - tx = VersionedDataStore.getFeedTx(stop.feedId); - - if (tx.stops.containsKey(stop.id)) { - halt(400); - } - - tx.stops.put(stop.id, stop); - tx.commit(); - json = Base.toJson(stop, false); - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } finally { - if (tx != null) tx.rollbackIfOpen(); - } - return json; - } - - - public static Object updateStop(Request req, Response res) throws IOException { - FeedTx tx = null; - Object json = null; - Stop stop = Base.mapper.readValue(req.body(), Stop.class); - String feedId = req.queryParams("feedId"); - if (feedId == null) { - halt(400, "Must provide feed ID"); - } - - if (!VersionedDataStore.agencyExists(feedId)) { - halt(400, "Feed ID ("+feedId+") does not exist"); - } - - tx = VersionedDataStore.getFeedTx(feedId); - - if (!tx.stops.containsKey(stop.id)) { - halt(400); - tx.rollback(); - } - - tx.stops.put(stop.id, stop); - tx.commit(); - json = Base.toJson(stop, false); - tx.rollbackIfOpen(); - return json; - } - - public static Object deleteStop(Request req, Response res) throws IOException { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - Object json = null; - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - if (!tx.stops.containsKey(id)) { - halt(404); - tx.rollback(); - } - - if (!tx.getTripPatternsByStop(id).isEmpty()) { - Set patterns = tx.getTripPatternsByStop(id).stream().map(tripPattern -> tripPattern.name).collect(Collectors.toSet()); - Set routes = tx.getTripPatternsByStop(id).stream().map(tripPattern -> tripPattern.routeId).collect(Collectors.toSet()); - halt(400, "Trip patterns ("+patterns.toString()+") for routes "+routes.toString()+" reference stop ID" + id); - tx.rollback(); - } - - Stop s = tx.stops.remove(id); - tx.commit(); - json = Base.toJson(s, false); - - tx.rollbackIfOpen(); - - return json; - } - - - public static Object findDuplicateStops(Request req, Response res) { - String feedId = req.queryParams("feedId"); - Object json = null; - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - FeedTx atx = VersionedDataStore.getFeedTx(feedId); - - try { - List> ret = new ArrayList>(); - - for (Stop stop : atx.stops.values()) { - // find nearby stops, within 5m - // at the equator, 1 degree is 111 km - // everywhere else this will overestimate, which is why we have a distance check as well (below) - double thresholdDegrees = 5 / 111000d; - - Collection candidateStops = atx.getStopsWithinBoundingBox( - stop.getLat() + thresholdDegrees, - stop.getLon() + thresholdDegrees, - stop.getLat() - thresholdDegrees, - stop.getLon() - thresholdDegrees); - - // we will always find a single stop, this one. - if (candidateStops.size() <= 1) - continue; - - List duplicatesOfThis = new ArrayList(); - - // note: this stop will be added implicitly because it is distance zero from itself - GeodeticCalculator gc = new GeodeticCalculator(); - gc.setStartingGeographicPoint(stop.getLon(), stop.getLat()); - for (Stop other : candidateStops) { - gc.setDestinationGeographicPoint(other.getLon(), other.getLat()); - if (gc.getOrthodromicDistance() < 10) { - duplicatesOfThis.add(other); - } - } - - if (duplicatesOfThis.size() > 1) { - ret.add(duplicatesOfThis); - } - } - - json = Base.toJson(ret, false); - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - finally { - atx.rollback(); - } - return json; - } - - public static Object mergeStops(Request req, Response res) { - List mergedStopIds = Arrays.asList(req.queryParams("mergedStopIds").split(",")); - String feedId = req.queryParams("feedId"); - - if (mergedStopIds.size() <= 1) { - halt(400); - } - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - Stop.merge(mergedStopIds, tx); - tx.commit(); - } finally { - tx.rollbackIfOpen(); - } - return true; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/stop/:id", StopController::getStop, json::write); - options(apiPrefix + "secure/stop", (q, s) -> ""); - get(apiPrefix + "secure/stop", StopController::getStop, json::write); - get(apiPrefix + "secure/stop/mergeStops", StopController::mergeStops, json::write); - post(apiPrefix + "secure/stop", StopController::createStop, json::write); - put(apiPrefix + "secure/stop/:id", StopController::updateStop, json::write); - delete(apiPrefix + "secure/stop/:id", StopController::deleteStop, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/TripController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/TripController.java deleted file mode 100644 index 710191ae1..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/TripController.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.StopTime; -import com.conveyal.datatools.editor.models.transit.Trip; -import com.conveyal.datatools.editor.models.transit.TripPattern; -import com.conveyal.datatools.editor.models.transit.TripPatternStop; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class TripController { - public static final Logger LOG = LoggerFactory.getLogger(TripController.class); - - public static JsonManager json = - new JsonManager<>(Trip.class, JsonViews.UserInterface.class); - - public static Object getTrip(Request req, Response res -// String id, String patternId, String calendarId, String feedId - ) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - String patternId = req.queryParams("patternId"); - String calendarId = req.queryParams("calendarId"); - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - if (id != null) { - if (tx.trips.containsKey(id)) - return Base.toJson(tx.trips.get(id), false); - else - halt(404); - } - else if (patternId != null && calendarId != null) { - if (!tx.tripPatterns.containsKey(patternId) || !tx.calendars.containsKey(calendarId)) { - halt(404); - } - else { - return Base.toJson(tx.getTripsByPatternAndCalendar(patternId, calendarId), false); - } - } - - else if(patternId != null) { - return Base.toJson(tx.getTripsByPattern(patternId), false); - } - else { - return Base.toJson(tx.trips.values(), false); - } - - } catch (Exception e) { - tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object createTrip(Request req, Response res) { - FeedTx tx = null; - - try { - Trip trip = Base.mapper.readValue(req.body(), Trip.class); - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(trip.feedId)) - halt(400); - - if (!VersionedDataStore.agencyExists(trip.feedId)) { - halt(400); - } - - tx = VersionedDataStore.getFeedTx(trip.feedId); - - if (tx.trips.containsKey(trip.id)) { - tx.rollback(); - halt(400); - } - - if (!tx.tripPatterns.containsKey(trip.patternId) || trip.stopTimes.size() != tx.tripPatterns.get(trip.patternId).patternStops.size()) { - tx.rollback(); - halt(400); - } - - tx.trips.put(trip.id, trip); - tx.commit(); - - return Base.toJson(trip, false); - } catch (Exception e) { - e.printStackTrace(); - if (tx != null) tx.rollbackIfOpen(); - halt(400); - } - return null; - } - - public static Object updateTrip(Request req, Response res) { - FeedTx tx = null; - - try { - Trip trip = Base.mapper.readValue(req.body(), Trip.class); - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(trip.feedId)) - halt(400); - - if (!VersionedDataStore.agencyExists(trip.feedId)) { - halt(400); - } - - tx = VersionedDataStore.getFeedTx(trip.feedId); - - if (!tx.trips.containsKey(trip.id)) { - tx.rollback(); - halt(400); - } - - if (!tx.tripPatterns.containsKey(trip.patternId) || trip.stopTimes.size() != tx.tripPatterns.get(trip.patternId).patternStops.size()) { - tx.rollback(); - halt(400); - } - - TripPattern patt = tx.tripPatterns.get(trip.patternId); - - // confirm that each stop in the trip matches the stop in the pattern - - for (int i = 0; i < trip.stopTimes.size(); i++) { - TripPatternStop ps = patt.patternStops.get(i); - StopTime st = trip.stopTimes.get(i); - - if (st == null) - // skipped stop - continue; - - if (!st.stopId.equals(ps.stopId)) { - LOG.error("Mismatch between stop sequence in trip and pattern at position {}, pattern: {}, stop: {}", i, ps.stopId, st.stopId); - tx.rollback(); - halt(400); - } - } - - tx.trips.put(trip.id, trip); - tx.commit(); - - return Base.toJson(trip, false); - } catch (Exception e) { - if (tx != null) tx.rollbackIfOpen(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object deleteTrip(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - Object json = null; - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (id == null || feedId == null) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - Trip trip = tx.trips.remove(id); - try { - json = Base.toJson(trip, false); - } catch (IOException e) { - halt(400); - } - tx.commit(); - - return json; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/trip/:id", TripController::getTrip, json::write); - options(apiPrefix + "secure/trip", (q, s) -> ""); - get(apiPrefix + "secure/trip", TripController::getTrip, json::write); - post(apiPrefix + "secure/trip", TripController::createTrip, json::write); - put(apiPrefix + "secure/trip/:id", TripController::updateTrip, json::write); - delete(apiPrefix + "secure/trip/:id", TripController::deleteTrip, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/TripPatternController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/TripPatternController.java deleted file mode 100644 index 7f9da407a..000000000 --- a/src/main/java/com/conveyal/datatools/editor/controllers/api/TripPatternController.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.conveyal.datatools.editor.controllers.api; - -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.conveyal.datatools.editor.controllers.Base; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.Trip; -import com.conveyal.datatools.editor.models.transit.TripPattern; -import org.mapdb.Fun; -import org.mapdb.Fun.Tuple2; - -import java.util.Collection; -import java.util.Set; - -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - - -public class TripPatternController { - - public static JsonManager json = - new JsonManager<>(TripPattern.class, JsonViews.UserInterface.class); - - public static Object getTripPattern(Request req, Response res) { - String id = req.params("id"); - String routeId = req.queryParams("routeId"); - String feedId = req.queryParams("feedId"); - Object json = null; - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if (feedId == null) { - halt(400); - } - - final FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - - if(id != null) { - if (!tx.tripPatterns.containsKey(id)) - halt(404); - else - json = Base.toJson(tx.tripPatterns.get(id), false); - } - else if (routeId != null) { - - if (!tx.routes.containsKey(routeId)) - halt(404, "routeId '" + routeId + "' does not exist"); - else { - Set> tpKeys = tx.tripPatternsByRoute.subSet(new Tuple2(routeId, null), new Tuple2(routeId, Fun.HI)); - - Collection patts = Collections2.transform(tpKeys, new Function, TripPattern>() { - - @Override - public TripPattern apply(Tuple2 input) { - return tx.tripPatterns.get(input.b); - } - }); - - json = Base.toJson(patts, false); - } - } - else { // get all patterns - json = Base.toJson(tx.tripPatterns, false); - } - - tx.rollback(); - - } catch (Exception e) { - tx.rollback(); - e.printStackTrace(); - halt(400); - } - return json; - } - - public static Object createTripPattern(Request req, Response res) { - TripPattern tripPattern; - - try { - tripPattern = Base.mapper.readValue(req.body(), TripPattern.class); - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(tripPattern.feedId)) - halt(400); - - if (!VersionedDataStore.agencyExists(tripPattern.feedId)) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(tripPattern.feedId); - - if (tx.tripPatterns.containsKey(tripPattern.id)) { - tx.rollback(); - halt(400); - } - - tripPattern.calcShapeDistTraveled(); - - tx.tripPatterns.put(tripPattern.id, tripPattern); - tx.commit(); - - return Base.toJson(tripPattern, false); - } catch (Exception e) { - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object updateTripPattern(Request req, Response res) { - TripPattern tripPattern; - FeedTx tx = null; - try { - tripPattern = Base.mapper.readValue(req.body(), TripPattern.class); - - if (req.session().attribute("feedId") != null && !req.session().attribute("feedId").equals(tripPattern.feedId)) - halt(400); - - if (!VersionedDataStore.agencyExists(tripPattern.feedId)) { - halt(400); - } - - if (tripPattern.id == null) { - halt(400); - } - - tx = VersionedDataStore.getFeedTx(tripPattern.feedId); - - TripPattern originalTripPattern = tx.tripPatterns.get(tripPattern.id); - - if(originalTripPattern == null) { - tx.rollback(); - halt(400); - } - - // update stop times - try { - TripPattern.reconcilePatternStops(originalTripPattern, tripPattern, tx); - } catch (IllegalStateException e) { - tx.rollback(); - halt(400); - } - - tripPattern.calcShapeDistTraveled(); - - tx.tripPatterns.put(tripPattern.id, tripPattern); - tx.commit(); - - return Base.toJson(tripPattern, false); - } catch (Exception e) { - if (tx != null) tx.rollback(); - e.printStackTrace(); - halt(400); - } - return null; - } - - public static Object deleteTripPattern(Request req, Response res) { - String id = req.params("id"); - String feedId = req.queryParams("feedId"); - - if (feedId == null) - feedId = req.session().attribute("feedId"); - - if(id == null || feedId == null) { - halt(400); - } - - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - - try { - // first zap all trips on this trip pattern - for (Trip trip : tx.getTripsByPattern(id)) { - tx.trips.remove(trip.id); - } - - tx.tripPatterns.remove(id); - tx.commit(); - } finally { - tx.rollbackIfOpen(); - } - return null; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/trippattern/:id", TripPatternController::getTripPattern, json::write); - options(apiPrefix + "secure/trippattern", (q, s) -> ""); - get(apiPrefix + "secure/trippattern", TripPatternController::getTripPattern, json::write); - post(apiPrefix + "secure/trippattern", TripPatternController::createTripPattern, json::write); - put(apiPrefix + "secure/trippattern/:id", TripPatternController::updateTripPattern, json::write); - delete(apiPrefix + "secure/trippattern/:id", TripPatternController::deleteTripPattern, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/datastore/DatabaseTx.java b/src/main/java/com/conveyal/datatools/editor/datastore/DatabaseTx.java deleted file mode 100644 index e0e616fef..000000000 --- a/src/main/java/com/conveyal/datatools/editor/datastore/DatabaseTx.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.conveyal.datatools.editor.datastore; - -import com.google.common.base.Function; -import com.google.common.collect.Iterators; -import org.mapdb.BTreeMap; -import org.mapdb.DB; -import org.mapdb.DB.BTreeMapMaker; -import org.mapdb.Fun.Tuple2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.conveyal.datatools.editor.utils.ClassLoaderSerializer; - -import java.util.Iterator; -import java.util.Map.Entry; -import java.util.NavigableSet; - -/** A wrapped transaction, so the database just looks like a POJO */ -public class DatabaseTx { - public static final Logger LOG = LoggerFactory.getLogger(DatabaseTx.class); - - /** the database (transaction). subclasses must initialize. */ - protected final DB tx; - - /** has this transaction been closed? */ - boolean closed = false; - - /** is this transaction read-only? */ - protected boolean readOnly; - - /** Convenience function to get a map */ - protected final BTreeMap getMap (String name) { - try { - return getMapMaker(tx, name) - .makeOrGet(); - } catch (UnsupportedOperationException e) { - // read-only data store - return null; - } - } - - /** get a map maker, that can then be further modified */ - private static final BTreeMapMaker getMapMaker (DB tx, String name) { - return tx.createTreeMap(name) - // use java serialization to allow for schema upgrades - .valueSerializer(new ClassLoaderSerializer()); - } - - /** - * Convenience function to get a set. These are used as indices so they use the default serialization; - * if we make a schema change we drop and recreate them. - */ - protected final NavigableSet getSet (String name) { - try { - return tx.createTreeSet(name) - .makeOrGet(); - } catch (UnsupportedOperationException e) { - // read-only data store - return null; - } - } - - protected DatabaseTx (DB tx) { - this.tx = tx; - } - - public void commit() { - try { - tx.commit(); - } catch (UnsupportedOperationException e) { - // probably read only, but warn - LOG.warn("Rollback failed; if this is a read-only database this is not unexpected"); - } closed = true; - } - - public void rollback() { - try { - tx.rollback(); - } catch (UnsupportedOperationException e) { - // probably read only, but warn - LOG.warn("Rollback failed; if this is a read-only database this is not unexpected"); - } - closed = true; - } - - /** roll this transaction back if it has not been committed or rolled back already */ - public void rollbackIfOpen () { - if (!closed) rollback(); - } - - /** efficiently copy a btreemap into this database */ - protected int pump(String mapName, BTreeMap source) { - return pump(tx, mapName, source); - } - - /** from a descending order iterator fill a new map in the specified database */ - protected static int pump(DB tx, String mapName, Iterator> pumpSource) { - if (!pumpSource.hasNext()) - return 0; - - return getMapMaker(tx, mapName) - .pumpSource(pumpSource) - .make() - .size(); - } - - /** efficiently create a BTreeMap in the specified database from another BTreeMap */ - protected static int pump (DB tx, String mapName, BTreeMap source) { - if (source.size() == 0) - return 0; - - return pump(tx, mapName, pumpSourceForMap(source)); - } - - /** get a pump source from a map */ - protected static Iterator> pumpSourceForMap(BTreeMap source) { - Iterator> values = source.descendingMap().entrySet().iterator(); - Iterator> valueTuples = Iterators.transform(values, new Function, Tuple2>() { - @Override - public Tuple2 apply(Entry input) { - return new Tuple2(input.getKey(), input.getValue()); - } - }); - - return valueTuples; - } - - protected final void finalize () { - if (!closed) { - LOG.error("DB transaction left unclosed, this signifies a memory leak!"); - rollback(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/datastore/FeedTx.java b/src/main/java/com/conveyal/datatools/editor/datastore/FeedTx.java deleted file mode 100644 index efc260dfd..000000000 --- a/src/main/java/com/conveyal/datatools/editor/datastore/FeedTx.java +++ /dev/null @@ -1,543 +0,0 @@ -package com.conveyal.datatools.editor.datastore; - -import com.conveyal.datatools.editor.models.transit.*; -import com.conveyal.datatools.manager.models.FeedSource; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Iterators; -import java.time.LocalDate; -import org.mapdb.Atomic; -import org.mapdb.BTreeMap; -import org.mapdb.Bind; -import org.mapdb.DB; -import org.mapdb.Fun; -import org.mapdb.Fun.Function2; -import org.mapdb.Fun.Tuple2; -//import play.i18n.Messages; -import com.conveyal.datatools.editor.utils.BindUtils; - -import java.util.Collection; -import java.util.Iterator; -import java.util.NavigableSet; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; - -/** a transaction in an agency database */ -public class FeedTx extends DatabaseTx { - // primary com.conveyal.datatools.editor.datastores - // if you add another, you MUST update SnapshotTx.java - // if you don't, not only will your new data not be backed up, IT WILL BE THROWN AWAY WHEN YOU RESTORE! - // AND ALSO the duplicate() function below - public BTreeMap tripPatterns; - public BTreeMap routes; - public BTreeMap trips; - public BTreeMap calendars; - public BTreeMap exceptions; - public BTreeMap stops; - public BTreeMap agencies; - public BTreeMap fares; - // if you add anything here, see warning above! - - // secondary indices - - /** Set containing tuples */ - public NavigableSet> tripsByRoute; - - /** */ - public NavigableSet> tripPatternsByRoute; - - /** */ - public NavigableSet> tripsByTripPattern; - - /** */ - public NavigableSet> tripsByCalendar; - - /** */ - public NavigableSet> exceptionsByCalendar; - - /** <, trip id> */ - public NavigableSet, String>> tripsByPatternAndCalendar; - - /** major stops for this agency */ - public NavigableSet majorStops; - - /** trip patterns using each stop */ - public NavigableSet> tripPatternsByStop; - - /** number of schedule exceptions on each date - this will always be null, 0, or 1, as we prevent save of others */ - public ConcurrentMap scheduleExceptionCountByDate; - - /** number of trips on each tuple2 */ - public ConcurrentMap, Long> tripCountByPatternAndCalendar; - - /** number of trips on each calendar */ - public ConcurrentMap tripCountByCalendar; - - /** - * Spatial index of stops. Set, stop ID>> - * This is not a true spatial index, but should be sufficiently efficient for our purposes. - * Jan Kotek describes this approach here, albeit in Czech: https://groups.google.com/forum/#!msg/mapdb/ADgSgnXzkk8/Q8J9rWAWXyMJ - */ - public NavigableSet, String>> stopsGix; - - /** snapshot versions. we use an atomic value so that they are (roughly) sequential, instead of using unordered UUIDs */ - private Atomic.Integer snapshotVersion; - -// public Atomic.Boolean editedSinceSnapshot; - /** - * Create an agency tx. - */ - public FeedTx(DB tx) { - this(tx, true); - } - - /** Create an agency tx, optionally without secondary indices */ - public FeedTx(DB tx, boolean buildSecondaryIndices) { - super(tx); - - tripPatterns = getMap("tripPatterns"); - routes = getMap("routes"); - trips = getMap("trips"); - calendars = getMap("calendars"); - exceptions = getMap("exceptions"); - snapshotVersion = tx.getAtomicInteger("snapshotVersion"); - stops = getMap("stops"); - agencies = getMap("agencies"); - fares = getMap("fares"); - - if (buildSecondaryIndices) - buildSecondaryIndices(); - -// editedSinceSnapshot = tx.getAtomicBoolean("editedSinceSnapshot") == null ? tx.createAtomicBoolean("editedSinceSnapshot", false) : tx.; - } - public void commit () { - try { -// editedSinceSnapshot.set(true); - tx.commit(); - } catch (UnsupportedOperationException e) { - // probably read only, but warn - LOG.warn("Rollback failed; if this is a read-only database this is not unexpected"); - } closed = true; - } - public void buildSecondaryIndices () { - // build secondary indices - // we store indices in the mapdb not because we care about persistence, but because then they - // will be managed within the context of MapDB transactions - tripsByRoute = getSet("tripsByRoute"); - - // bind the trips to the routes - Bind.secondaryKeys(trips, tripsByRoute, new Fun.Function2() { - - @Override - public String[] run(String tripId, Trip trip) { - return new String[] { trip.routeId }; - } - }); - - tripPatternsByRoute = getSet("tripPatternsByRoute"); - Bind.secondaryKeys(tripPatterns, tripPatternsByRoute, new Fun.Function2() { - - @Override - public String[] run(String tripId, TripPattern trip) { - // TODO Auto-generated method stub - return new String[] { trip.routeId }; - } - }); - - tripsByTripPattern = getSet("tripsByTripPattern"); - Bind.secondaryKeys(trips, tripsByTripPattern, new Fun.Function2 () { - - @Override - public String[] run(String tripId, Trip trip) { - // TODO Auto-generated method stub - return new String[] { trip.patternId }; - } - }); - - tripsByCalendar = getSet("tripsByCalendar"); - Bind.secondaryKeys(trips, tripsByCalendar, new Fun.Function2 () { - - @Override - public String[] run(String tripId, Trip trip) { - // TODO Auto-generated method stub - return new String[] { trip.calendarId }; - } - }); - - exceptionsByCalendar = getSet("exceptionsByCalendar"); - Bind.secondaryKeys(exceptions, exceptionsByCalendar, new Fun.Function2 () { - - @Override - public String[] run(String key, ScheduleException ex) { - if (ex.customSchedule == null) return new String[0]; - - return ex.customSchedule.toArray(new String[ex.customSchedule.size()]); - } - - }); - - tripsByPatternAndCalendar = getSet("tripsByPatternAndCalendar"); - Bind.secondaryKeys(trips, tripsByPatternAndCalendar, new Fun.Function2[], String, Trip>() { - - @Override - public Tuple2[] run(String key, Trip trip) { - return new Tuple2[] { new Tuple2(trip.patternId, trip.calendarId) }; - } - }); - - majorStops = getSet("majorStops"); - BindUtils.subsetIndex(stops, majorStops, new Fun.Function2 (){ - @Override - public Boolean run(String key, Stop val) { - // TODO Auto-generated method stub - return val.majorStop != null && val.majorStop; - } - - }); - - tripPatternsByStop = getSet("tripPatternsByStop"); - Bind.secondaryKeys(tripPatterns, tripPatternsByStop, new Fun.Function2() { - @Override - public String[] run(String key, TripPattern tp) { - String[] stops = new String[tp.patternStops.size()]; - - for (int i = 0; i < stops.length; i++) { - stops[i] = tp.patternStops.get(i).stopId; - } - - return stops; - } - }); - - tripCountByPatternAndCalendar = getMap("tripCountByPatternAndCalendar"); - Bind.histogram(trips, tripCountByPatternAndCalendar, new Fun.Function2, String, Trip>() { - - @Override - public Tuple2 run(String tripId, Trip trip) { - return new Tuple2(trip.patternId, trip.calendarId); - } - }); - - scheduleExceptionCountByDate = getMap("scheduleExceptionCountByDate"); - BindUtils.multiHistogram(exceptions, scheduleExceptionCountByDate, new Fun.Function2 () { - - @Override - public LocalDate[] run(String id, ScheduleException ex) { - return ex.dates.toArray(new LocalDate[ex.dates.size()]); - } - - }); - - tripCountByCalendar = getMap("tripCountByCalendar"); - BindUtils.multiHistogram(trips, tripCountByCalendar, new Fun.Function2() { - - @Override - public String[] run(String key, Trip trip) { - if (trip.calendarId == null) - return new String[] {}; - else - return new String[] { trip.calendarId }; - } - }); - - // "spatial index" - stopsGix = getSet("stopsGix"); - Bind.secondaryKeys(stops, stopsGix, new Function2[], String, Stop>() { - - @Override - public Tuple2[] run( - String stopId, Stop stop) { - return new Tuple2[] { new Tuple2(stop.location.getX(), stop.location.getY()) }; - } - - }); - } - - public Collection getTripsByPattern(String patternId) { - Set> matchedKeys = tripsByTripPattern.subSet(new Tuple2(patternId, null), new Tuple2(patternId, Fun.HI)); - - return Collections2.transform(matchedKeys, new Function, Trip>() { - public Trip apply(Tuple2 input) { - return trips.get(input.b); - } - }); - } - - public Collection getTripsByRoute(String routeId) { - Set> matchedKeys = tripsByRoute.subSet(new Tuple2(routeId, null), new Tuple2(routeId, Fun.HI)); - - return Collections2.transform(matchedKeys, new Function, Trip>() { - public Trip apply(Tuple2 input) { - return trips.get(input.b); - } - }); - } - - public Collection getTripsByCalendar(String calendarId) { - Set> matchedKeys = tripsByCalendar.subSet(new Tuple2(calendarId, null), new Tuple2(calendarId, Fun.HI)); - - return Collections2.transform(matchedKeys, new Function, Trip>() { - public Trip apply(Tuple2 input) { - return trips.get(input.b); - } - }); - } - - public Collection getExceptionsByCalendar(String calendarId) { - Set> matchedKeys = exceptionsByCalendar.subSet(new Tuple2(calendarId, null), new Tuple2(calendarId, Fun.HI)); - - return Collections2.transform(matchedKeys, new Function, ScheduleException>() { - public ScheduleException apply(Tuple2 input) { - return exceptions.get(input.b); - } - }); - } - - public Collection getTripsByPatternAndCalendar(String patternId, String calendarId) { - Set, String>> matchedKeys = - tripsByPatternAndCalendar.subSet(new Tuple2(new Tuple2(patternId, calendarId), null), new Tuple2(new Tuple2(patternId, calendarId), Fun.HI)); - - return Collections2.transform(matchedKeys, new Function, String>, Trip>() { - public Trip apply(Tuple2, String> input) { - return trips.get(input.b); - } - }); - } - - public Collection getStopsWithinBoundingBox (double north, double east, double south, double west) { - // find all the stops in this bounding box - // avert your gaze please as I write these generic types - Tuple2 min = new Tuple2(west, south); - Tuple2 max = new Tuple2(east, north); - - Set, String>> matchedKeys = - stopsGix.subSet(new Tuple2(min, null), new Tuple2(max, Fun.HI)); - - Collection matchedStops = - Collections2.transform(matchedKeys, new Function, String>, Stop>() { - - @Override - public Stop apply( - Tuple2, String> input) { - return stops.get(input.b); - } - }); - - return matchedStops; - } - - public Collection getTripPatternsByStop (String id) { - Collection> matchedPatterns = tripPatternsByStop.subSet(new Tuple2(id, null), new Tuple2(id, Fun.HI)); - return Collections2.transform(matchedPatterns, new Function, TripPattern>() { - public TripPattern apply(Tuple2 input) { - return tripPatterns.get(input.b); - } - }); - } - - /** return the version number of the next snapshot */ - public int getNextSnapshotId () { - return snapshotVersion.incrementAndGet(); - } - - /** duplicate an EditorFeed in its entirety. Return the new feed ID */ - public static String duplicate (String feedId) { - final String newId = UUID.randomUUID().toString(); - - FeedTx feedTx = VersionedDataStore.getFeedTx(feedId); - - DB newDb = VersionedDataStore.getRawFeedTx(newId); - - copy(feedTx, newDb, newId); - - // rebuild indices - FeedTx newTx = new FeedTx(newDb); - newTx.commit(); - - feedTx.rollback(); - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - EditorFeed feedCopy; - - try { - feedCopy = gtx.feeds.get(feedId).clone(); - } catch (CloneNotSupportedException e) { - // not likely - e.printStackTrace(); - gtx.rollback(); - return null; - } - - feedCopy.id = newId; -// a2.name = Messages.get("agency.copy-of", a2.name); - - gtx.feeds.put(feedCopy.id, feedCopy); - - gtx.commit(); - - return newId; - } - - /** copy a feed database */ - static void copy (FeedTx feedTx, DB newDb, final String newFeedId) { - // copy everything - try { - Iterator> stopSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.stops), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - Stop st; - try { - st = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - st.feedId = newFeedId; - return new Tuple2(input.a, st); - } - }); - pump(newDb, "stops", stopSource); - - Iterator> tripSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.trips), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - Trip st; - try { - st = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - st.feedId = newFeedId; - return new Tuple2(input.a, st); - } - }); - pump(newDb, "trips", tripSource); - - Iterator> pattSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.tripPatterns), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - TripPattern st; - try { - st = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - st.feedId = newFeedId; - return new Tuple2(input.a, st); - } - }); - pump(newDb, "tripPatterns", pattSource); - - Iterator> routeSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.routes), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - Route st; - try { - st = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - st.feedId = newFeedId; - return new Tuple2(input.a, st); - } - }); - pump(newDb, "routes", routeSource); - - Iterator> calSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.calendars), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - ServiceCalendar st; - try { - st = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - st.feedId = newFeedId; - return new Tuple2(input.a, st); - } - }); - pump(newDb, "calendars", calSource); - - Iterator> exSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.exceptions), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - ScheduleException st; - try { - st = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - st.feedId = newFeedId; - return new Tuple2(input.a, st); - } - }); - pump(newDb, "exceptions", exSource); - - Iterator> agencySource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.agencies), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - Agency agency; - try { - agency = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - agency.feedId = newFeedId; - return new Tuple2(input.a, agency); - } - }); - pump(newDb, "agencies", agencySource); - - Iterator> fareSource = Iterators.transform( - FeedTx.pumpSourceForMap(feedTx.agencies), - new Function, Tuple2>() { - @Override - public Tuple2 apply(Tuple2 input) { - Fare fare; - try { - fare = input.b.clone(); - } catch (CloneNotSupportedException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - fare.feedId = newFeedId; - return new Tuple2(input.a, fare); - } - }); - pump(newDb, "fares", fareSource); - - // copy histograms - pump(newDb, "tripCountByCalendar", (BTreeMap) feedTx.tripCountByCalendar); - pump(newDb, "scheduleExceptionCountByDate", (BTreeMap) feedTx.scheduleExceptionCountByDate); - pump(newDb, "tripCountByPatternAndCalendar", (BTreeMap) feedTx.tripCountByPatternAndCalendar); - - } - catch (Exception e) { - newDb.rollback(); - feedTx.rollback(); - throw new RuntimeException(e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/datastore/GlobalTx.java b/src/main/java/com/conveyal/datatools/editor/datastore/GlobalTx.java deleted file mode 100644 index 448855e29..000000000 --- a/src/main/java/com/conveyal/datatools/editor/datastore/GlobalTx.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.conveyal.datatools.editor.datastore; - -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.editor.models.transit.EditorFeed; -import com.conveyal.datatools.editor.models.transit.RouteType; -import com.conveyal.datatools.manager.models.FeedSource; -import org.mapdb.BTreeMap; -import org.mapdb.DB; -import org.mapdb.Fun.Tuple2; - -/** a transaction in the global database */ -public class GlobalTx extends DatabaseTx { - public BTreeMap feeds; - - /** Accounts */ -// public BTreeMap accounts; - - /** OAuth tokens */ -// public BTreeMap tokens; - - /** Route types */ - public BTreeMap routeTypes; - - /** Snapshots of agency DBs, keyed by agency_id, version */ - public BTreeMap, Snapshot> snapshots; - - public GlobalTx (DB tx) { - super(tx); - - feeds = getMap("feeds"); -// accounts = getMap("accounts"); -// tokens = getMap("tokens"); - routeTypes = getMap("routeTypes"); - snapshots = getMap("snapshots"); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/datastore/MigrateToMapDB.java b/src/main/java/com/conveyal/datatools/editor/datastore/MigrateToMapDB.java deleted file mode 100644 index ab07aeb16..000000000 --- a/src/main/java/com/conveyal/datatools/editor/datastore/MigrateToMapDB.java +++ /dev/null @@ -1,644 +0,0 @@ -package com.conveyal.datatools.editor.datastore; - -import com.beust.jcommander.internal.Maps; -import com.csvreader.CsvReader; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.io.WKTReader; -import gnu.trove.map.TLongLongMap; -import gnu.trove.map.hash.TLongLongHashMap; -//import com.conveyal.datatools.editor.models.Account; -import com.conveyal.datatools.editor.models.transit.*; -import java.time.LocalDate; -import org.mapdb.DBMaker; -import org.mapdb.Fun; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Map; -import java.util.NavigableMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -//import static org.opentripplanner.common.LoggingUtil.human; - -/** - * Migrate a Postgres database dump to the MapDB format. - */ -public class MigrateToMapDB { -// private GlobalTx gtx; -// private File fromDirectory; -// -// private static GeometryFactory gf = new GeometryFactory(); -// -// /** keep track of transactions for all feeds */ -// private Map atxes = Maps.newHashMap(); -// -// /** cache shapes; use a mapdb so it's not huge */ -// private Map shapeCache = DBMaker.newTempHashMap(); -// -// /** cache stop times: Tuple2 -> StopTime */ -// private NavigableMap, StopTime> stopTimeCache = DBMaker.newTempTreeMap(); -// -// /** cache stop times: Tuple2 -> TripPatternStop */ -// private NavigableMap, TripPatternStop> patternStopCache = DBMaker.newTempTreeMap(); -// -// /** cache exception dates, Exception ID -> Date */ -// private Multimap exceptionDates = HashMultimap.create(); -// -// /** cache custom calendars, exception ID -> calendar ID*/ -// private Multimap exceptionCalendars = HashMultimap.create(); -// -// /** route ID -> agency ID, needed because we need the agency ID to get a reference to the route . . . */ -// TLongLongMap routeAgencyMap = new TLongLongHashMap(); -// -// /** pattern ID -> agency ID */ -// TLongLongMap patternAgencyMap = new TLongLongHashMap(); -// -// /** actually perform the migration */ -// public void migrate(File fromDirectory) throws Exception { -// // import global stuff first: easy-peasy lemon squeezee -// gtx = VersionedDataStore.getGlobalTx(); -// this.fromDirectory = fromDirectory; -// -// try { -// readAgencies(); -// readAccounts(); -// readRouteTypes(); -// -// readStops(); -// -// readRoutes(); -// -// readShapes(); -// readPatternStops(); -// readTripPatterns(); -// -// readStopTimes(); -// readTrips(); -// -// readCalendars(); -// -// readExceptionDates(); -// readExceptionCustomCalendars(); -// readExceptions(); -// -// gtx.commit(); -// -// for (FeedTx atx : atxes.values()) { -// atx.commit(); -// } -// } finally { -// gtx.rollbackIfOpen(); -// -// for (FeedTx atx : atxes.values()) { -// atx.rollbackIfOpen(); -// } -// } -// } -// -// private void readAgencies () throws Exception { -// System.out.println("Reading feeds"); -// -// DatabaseCsv reader = getCsvReader("agency.csv"); -// -// reader.readHeaders(); -// -// int count = 0; -// while (reader.readRecord()) { -// Agency a = new Agency(); -// a.id = reader.get("id"); -// a.color = reader.get("color"); -// a.defaultLon = reader.getDouble("defaultlon"); -// a.defaultLat = reader.getDouble("defaultlat"); -// a.gtfsAgencyId = reader.get("gtfsagencyid"); -// a.lang = reader.get("lang"); -// a.name = reader.get("name"); -// a.phone = reader.get("phone"); -// a.timezone = reader.get("timezone"); -// a.url = reader.get("url"); -// // easy to maintain referential integrity; we're retaining DB IDs. -// a.routeTypeId = reader.get("defaultroutetype_id"); -// -// gtx.feeds.put(a.id, a); -// count++; -// } -// -// System.out.println("imported " + count + " feeds"); -// } -// -// private void readAccounts () throws Exception { -// System.out.println("Reading accounts"); -// -// DatabaseCsv reader = getCsvReader("account.csv"); -// reader.readHeaders(); -// int count = 0; -// -// while (reader.readRecord()) { -// String username = reader.get("username"); -// Boolean admin = reader.getBoolean("admin"); -// String email = reader.get("email"); -// String agencyId = reader.get("agency_id"); -// Account a = new Account(username, "password", email, admin, agencyId); -// a.password = reader.get("password"); -// a.active = reader.getBoolean("active"); -// a.id = a.username; -// -// gtx.accounts.put(a.id, a); -// -// count++; -// } -// -// System.out.println("Imported " + count + " accounts"); -// } -// -// private void readStops () throws Exception { -// System.out.println("reading stops"); -// -// DatabaseCsv reader = getCsvReader("stop.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// Stop s = new Stop(); -// s.location = gf.createPoint(new Coordinate(reader.getDouble("lon"), reader.getDouble("lat"))); -// s.agencyId = reader.get("agency_id"); -// s.bikeParking = reader.getAvail("bikeparking"); -// s.carParking = reader.getAvail("carparking"); -// s.dropOffType = reader.getPdType("dropofftype"); -// s.pickupType = reader.getPdType("pickuptype"); -// s.gtfsStopId = reader.get("gtfsstopid"); -// s.locationType = reader.getLocationType("locationtype"); -// s.majorStop = reader.getBoolean("majorstop"); -// s.parentStation = reader.get("parentstation"); -// s.stopCode = reader.get("stopcode"); -// s.stopIconUrl = reader.get("stopiconurl"); -// s.stopDesc = reader.get("stopdesc"); -// s.stopName = reader.get("stopname"); -// s.stopUrl = reader.get("stopurl"); -// s.wheelchairBoarding = reader.getAvail("wheelchairboarding"); -// s.zoneId = reader.get("zoneid"); -// s.id = reader.get("id"); -// -// getFeedTx(s.agencyId).stops.put(s.id, s); -// count ++; -// } -// -// System.out.println("Read " + count + " stops"); -// -// } -// -// /** Read the routes */ -// private void readRoutes () throws Exception { -// System.out.println("Reading routes"); -// DatabaseCsv reader = getCsvReader("route.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// Route r = new Route(); -// r.id = reader.get("id"); -// r.comments = reader.get("comments"); -// r.gtfsRouteId = reader.get("gtfsrouteid"); -// r.routeColor = reader.get("routecolor"); -// r.routeDesc = reader.get("routedesc"); -// r.routeLongName = reader.get("routelongname"); -// r.routeShortName = reader.get("routeshortname"); -// r.routeTextColor = reader.get("routetextcolor"); -// r.routeUrl = reader.get("routeurl"); -// String status = reader.get("status"); -// r.status = status != null ? StatusType.valueOf(status) : null; -// r.wheelchairBoarding = reader.getAvail("wheelchairboarding"); -// r.agencyId = reader.get("agency_id"); -// r.routeTypeId = reader.get("routetype_id"); -// -// // cache the agency ID -// routeAgencyMap.put(Long.parseLong(r.id), Long.parseLong(r.agencyId)); -// -// getFeedTx(r.agencyId).routes.put(r.id, r); -// count++; -// } -// -// System.out.println("Read " + count + " routes"); -// } -// -// /** -// * Read in the trip shapes. We put them in a MapDB keyed by Shape ID, because we don't store them directly; -// * rather, we copy them into their respective trip patterns when we import the patterns. -// */ -// private void readShapes () throws Exception { -// System.out.println("Reading shapes"); -// DatabaseCsv reader = getCsvReader("shapes.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// shapeCache.put(reader.get("id"), reader.getLineString("shape")); -// count++; -// } -// -// System.out.println("Read " + count + " shapes"); -// } -// -// /** read and cache the trip pattern stops */ -// private void readPatternStops () throws Exception { -// System.out.println("Reading trip pattern stops"); -// DatabaseCsv reader = getCsvReader("patternstop.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// TripPatternStop tps = new TripPatternStop(); -// Integer dtt = reader.getInteger("defaulttraveltime"); -// tps.defaultTravelTime = dtt != null ? dtt : 0; -// Integer ddt = reader.getInteger("defaultdwelltime"); -// tps.defaultDwellTime = ddt != null ? ddt : 0; -// tps.timepoint = reader.getBoolean("timepoint"); -// tps.stopId = reader.get("stop_id"); -// // note: not reading shape_dist_traveled as it was incorrectly computed. We'll recompute at the end. -// -// Fun.Tuple2 key = new Fun.Tuple2(reader.get("pattern_id"), reader.getInteger("stopsequence")); -// -// // make sure that we don't have a mess on our hands due to data import issues far in the past. -// if (patternStopCache.containsKey(key)) { -// throw new IllegalStateException("Duplicate pattern stops!"); -// } -// -// patternStopCache.put(key, tps); -// count++; -// } -// -// System.out.println("Read " + count + " pattern stops"); -// } -// -// /** Read the trip patterns */ -// private void readTripPatterns () throws Exception { -// System.out.println("Reading trip patterns"); -// DatabaseCsv reader = getCsvReader("trippattern.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// TripPattern p = new TripPattern(); -// p.id = reader.get("id"); -// p.headsign = reader.get("headsign"); -// p.name = reader.get("name"); -// p.routeId = reader.get("route_id"); -// String shapeId = reader.get("shape_id"); -// p.shape = shapeId != null ? shapeCache.get(shapeId) : null; -// -// // get the pattern stops -// p.patternStops = new ArrayList(); -// p.patternStops.addAll(patternStopCache.subMap(new Fun.Tuple2(p.id, null), new Fun.Tuple2(p.id, Fun.HI)).values()); -// -// p.agencyId = routeAgencyMap.get(Long.parseLong(p.routeId)) + ""; -// patternAgencyMap.put(Long.parseLong(p.id), Long.parseLong(p.agencyId)); -// -// p.calcShapeDistTraveled(getFeedTx(p.agencyId)); -// -// getFeedTx(p.agencyId).tripPatterns.put(p.id, p); -// count++; -// } -// -// System.out.println("Read " + count + " trip patterns"); -// } -// -// /** Read the stop times and cache them */ -// private void readStopTimes () throws Exception { -// System.out.println("Reading stop times (this could take a while) . . ."); -// DatabaseCsv reader = getCsvReader("stoptime.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// if (++count % 100000 == 0) { -// System.out.println(count + " stop times read . . ."); -// } -// -// StopTime st = new StopTime(); -// st.arrivalTime = reader.getInteger("arrivaltime"); -// st.departureTime = reader.getInteger("departuretime"); -// // note: not reading shape_dist_traveled as it was incorrectly computed. We'll recompute at the end. -// -// st.stopHeadsign = reader.get("stopheadsign"); -// st.dropOffType = reader.getPdType("dropofftype"); -// st.pickupType = reader.getPdType("pickuptype"); -// st.stopId = reader.get("stop_id"); -// -// Fun.Tuple2 key = new Fun.Tuple2(reader.get("trip_id"), reader.getInteger("stopsequence")); -// -// if (stopTimeCache.containsKey(key)) { -// throw new IllegalStateException("Duplicate stop times!"); -// } -// -// stopTimeCache.put(key, st); -// } -// -// System.out.println("read " + count + " stop times"); -// } -// -// private void readTrips () throws Exception { -// DatabaseCsv reader = getCsvReader("trip.csv"); -// reader.readHeaders(); -// int count = 0; -// int stCount = 0; -// -// while (reader.readRecord()) { -// Trip t = new Trip(); -// t.id = reader.get("id"); -// t.blockId = reader.get("blockid"); -// t.endTime = reader.getInteger("endtime"); -// t.gtfsTripId = reader.get("gtfstripid"); -// t.headway = reader.getInteger("headway"); -// t.invalid = reader.getBoolean("invalid"); -// t.startTime = reader.getInteger("starttime"); -// t.tripDescription = reader.get("tripdescription"); -// String dir = reader.get("tripdirection"); -// t.tripDirection = dir != null ? TripDirection.valueOf(dir) : null; -// t.tripHeadsign = reader.get("tripheadsign"); -// t.tripShortName = reader.get("tripshortname"); -// t.useFrequency = reader.getBoolean("usefrequency"); -// t.wheelchairBoarding = reader.getAvail("wheelchairboarding"); -// t.patternId = reader.get("pattern_id"); -// t.routeId = reader.get("route_id"); -// t.calendarId = reader.get("servicecalendar_id"); -// t.agencyId = routeAgencyMap.get(Long.parseLong(t.routeId)) + ""; -// -// // get stop times -// // make sure we put nulls in as needed for skipped stops -// t.stopTimes = new ArrayList(); -// -// // loop over the pattern stops and find the stop times that match -// for (Map.Entry, TripPatternStop> entry : -// patternStopCache.subMap(new Fun.Tuple2(t.patternId, null), new Fun.Tuple2(t.patternId, Fun.HI)).entrySet()) { -// // get the appropriate stop time, or null if the stop is skipped -// StopTime st = stopTimeCache.get(new Fun.Tuple2(t.id, entry.getKey().b)); -// t.stopTimes.add(st); -// -// if (st != null) -// stCount++; -// } -// -// count++; -// -// getFeedTx(t.agencyId).trips.put(t.id, t); -// } -// -// System.out.println("Read " + count + " trips"); -// System.out.println("Associated " + stCount + " stop times with trips"); -// } -// -// private void readRouteTypes () throws Exception { -// System.out.println("Reading route types"); -// -// DatabaseCsv reader = getCsvReader("routetype.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// RouteType rt = new RouteType(); -// rt.id = reader.get("id"); -// rt.description = reader.get("description"); -// String grt = reader.get("gtfsroutetype"); -// rt.gtfsRouteType = grt != null ? GtfsRouteType.valueOf(grt) : null; -// String hvt = reader.get("hvtroutetype"); -// rt.hvtRouteType = hvt != null ? HvtRouteType.valueOf(hvt) : null; -// rt.localizedVehicleType = reader.get("localizedvehicletype"); -// gtx.routeTypes.put(rt.id, rt); -// count++; -// } -// -// System.out.println("Imported " + count + " route types"); -// } -// -// private void readCalendars () throws Exception { -// System.out.println("Reading calendars"); -// DatabaseCsv reader = getCsvReader("servicecalendar.csv"); -// reader.readHeaders(); -// int count = 0; -// -// while (reader.readRecord()) { -// ServiceCalendar c = new ServiceCalendar(); -// c.id = reader.get("id"); -// c.description = reader.get("description"); -// c.endDate = reader.getLocalDate("enddate"); -// c.startDate = reader.getLocalDate("startdate"); -// c.gtfsServiceId = reader.get("gtfsserviceid"); -// c.monday = reader.getBoolean("monday"); -// c.tuesday = reader.getBoolean("tuesday"); -// c.wednesday = reader.getBoolean("wednesday"); -// c.thursday = reader.getBoolean("thursday"); -// c.friday = reader.getBoolean("friday"); -// c.saturday = reader.getBoolean("saturday"); -// c.sunday = reader.getBoolean("sunday"); -// c.agencyId = reader.get("agency_id"); -// -// getFeedTx(c.agencyId).calendars.put(c.id, c); -// count++; -// } -// -// System.out.println("Imported " + count + " calendars"); -// } -// -// private void readExceptionDates () throws Exception { -// System.out.println("Reading exception dates"); -// DatabaseCsv reader = getCsvReader("exception_dates.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// exceptionDates.put(reader.get("scheduleexception_id"), reader.getLocalDate("dates")); -// count++; -// } -// -// System.out.println("Read " + count + " exception dates"); -// } -// -// private void readExceptionCustomCalendars () throws Exception { -// System.out.println("Reading exception calendars"); -// DatabaseCsv reader = getCsvReader("exception_calendars.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// exceptionCalendars.put(reader.get("scheduleexception_id"), reader.get("customschedule_id")); -// count++; -// } -// -// System.out.println("Read " + count + " exception calendars"); -// } -// -// private void readExceptions () throws Exception { -// System.out.println("Reading exceptions"); -// DatabaseCsv reader = getCsvReader("exception.csv"); -// reader.readHeaders(); -// -// int count = 0; -// -// while (reader.readRecord()) { -// ScheduleException e = new ScheduleException(); -// e.id = reader.get("id"); -// e.exemplar = ScheduleException.ExemplarServiceDescriptor.valueOf(reader.get("exemplar")); -// e.name = reader.get("name"); -// e.agencyId = reader.get("agency_id"); -// -// e.dates = new ArrayList(exceptionDates.get(e.id)); -// e.customSchedule = new ArrayList(exceptionCalendars.get(e.id)); -// -// getFeedTx(e.agencyId).exceptions.put(e.id, e); -// count++; -// } -// -// System.out.println("Read " + count + " exceptions"); -// } -// -// private DatabaseCsv getCsvReader(String file) { -// try { -// InputStream is = new FileInputStream(new File(fromDirectory, file)); -// return new DatabaseCsv(new CsvReader(is, ',', Charset.forName("UTF-8"))); -// } catch (Exception e) { -// e.printStackTrace(); -// throw new RuntimeException(e); -// } -// } -// -// private FeedTx getFeedTx (String agencyId) { -// if (!atxes.containsKey(agencyId)) -// atxes.put(agencyId, VersionedDataStore.getFeedTx(agencyId)); -// -// return atxes.get(agencyId); -// } -// -// private static class DatabaseCsv { -// private CsvReader reader; -// -// private static Pattern datePattern = Pattern.compile("^([1-9][0-9]{3})-([0-9]{2})-([0-9]{2})"); -// -// public DatabaseCsv(CsvReader reader) { -// this.reader = reader; -// } -// -// public boolean readHeaders() throws IOException { -// return reader.readHeaders(); -// } -// -// public boolean readRecord () throws IOException { -// return reader.readRecord(); -// } -// -// public String get (String column) throws IOException { -// String ret = reader.get(column); -// if (ret.isEmpty()) -// return null; -// -// return ret; -// } -// -// public Double getDouble(String column) { -// try { -// String dbl = reader.get(column); -// return Double.parseDouble(dbl); -// } catch (Exception e) { -// return null; -// } -// } -// -// public StopTimePickupDropOffType getPdType (String column) throws Exception { -// String val = reader.get(column); -// -// try { -// return StopTimePickupDropOffType.valueOf(val); -// } catch (Exception e) { -// return null; -// } -// } -// -// public Boolean getBoolean (String column) throws Exception { -// String val = get(column); -// -// if (val == null) -// return null; -// -// switch (val.charAt(0)) { -// case 't': -// return Boolean.TRUE; -// case 'f': -// return Boolean.FALSE; -// default: -// return null; -// } -// -// } -// -// public LineString getLineString (String column) throws Exception { -// String val = reader.get(column); -// -// try { -// return (LineString) new WKTReader().read(val); -// } catch (Exception e) { -// return null; -// } -// } -// -// public AttributeAvailabilityType getAvail (String column) throws Exception { -// String val = reader.get(column); -// -// try { -// return AttributeAvailabilityType.valueOf(val); -// } catch (Exception e) { -// return null; -// } -// } -// -// public Integer getInteger (String column) throws Exception { -// String val = reader.get(column); -// -// try { -// return Integer.parseInt(val); -// } catch (Exception e) { -// return null; -// } -// } -// -// public LocationType getLocationType (String column) throws Exception { -// String val = reader.get(column); -// -// try { -// return LocationType.valueOf(val); -// } catch (Exception e) { -// return null; -// } -// } -// -// public LocalDate getLocalDate (String column) throws Exception { -// String val = get(column); -// -// try { -// Matcher m = datePattern.matcher(val); -// -// if (!m.matches()) -// return null; -// -// return LocalDate.of(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3))); -// } catch (Exception e) { -// return null; -// } -// } -// } -} diff --git a/src/main/java/com/conveyal/datatools/editor/datastore/SnapshotTx.java b/src/main/java/com/conveyal/datatools/editor/datastore/SnapshotTx.java deleted file mode 100644 index 1ead88134..000000000 --- a/src/main/java/com/conveyal/datatools/editor/datastore/SnapshotTx.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.conveyal.datatools.editor.datastore; - -import com.conveyal.gtfs.model.Calendar; -import com.conveyal.datatools.editor.models.transit.Route; -import com.conveyal.datatools.editor.models.transit.ScheduleException; -import com.conveyal.datatools.editor.models.transit.Stop; -import com.conveyal.datatools.editor.models.transit.Trip; -import com.conveyal.datatools.editor.models.transit.TripPattern; -import com.conveyal.datatools.editor.models.transit.TripPatternStop; -import org.mapdb.BTreeMap; -import org.mapdb.DB; -import org.mapdb.Fun.Tuple2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; - -/** represents a snapshot database. It's generally not actually a transaction, but rather writing to a transactionless db, for speed */ -public class SnapshotTx extends DatabaseTx { - /** create a snapshot database */ - public static final Logger LOG = LoggerFactory.getLogger(SnapshotTx.class); - public SnapshotTx(DB tx) { - super(tx); - } - - /** make the snapshot */ - public void make (FeedTx master) { - // make sure it's empty - if (tx.getAll().size() != 0) - throw new IllegalStateException("Cannot snapshot into non-empty db"); - - int acount = pump("agencies", (BTreeMap) master.agencies); - LOG.info("Snapshotted {} agencies", acount); - int rcount = pump("routes", (BTreeMap) master.routes); - LOG.info("Snapshotted {} routes", rcount); - int ccount = pump("calendars", (BTreeMap) master.calendars); - LOG.info("Snapshotted {} calendars", ccount); - int ecount = pump("exceptions", (BTreeMap) master.exceptions); - LOG.info("Snapshotted {} schedule exceptions", ecount); - int tpcount = pump("tripPatterns", (BTreeMap) master.tripPatterns); - LOG.info("Snapshotted {} patterns", tpcount); - int tcount = pump("trips", (BTreeMap) master.trips); - LOG.info("Snapshotted {} trips", tcount); - int scount = pump("stops", (BTreeMap) master.stops); - LOG.info("Snapshotted {} stops", scount); - int fcount = pump("fares", (BTreeMap) master.fares); - LOG.info("Snapshotted {} fares", fcount); - - // while we don't snapshot indices, we do need to snapshot histograms as they aren't restored - // (mapdb ticket 453) - pump("tripCountByCalendar", (BTreeMap) master.tripCountByCalendar); - pump("scheduleExceptionCountByDate", (BTreeMap) master.scheduleExceptionCountByDate); - pump("tripCountByPatternAndCalendar", (BTreeMap) master.tripCountByPatternAndCalendar); - - this.commit(); - LOG.info("Snapshot finished"); - } - - /** - * restore into an agency. this will OVERWRITE ALL DATA IN THE AGENCY's MASTER BRANCH, with the exception of stops - * @return any stop IDs that had been deleted and were restored so that this snapshot would be valid. - */ - public List restore (String agencyId) { - DB targetTx = VersionedDataStore.getRawFeedTx(agencyId); - - for (String obj : targetTx.getAll().keySet()) { - if (obj.equals("snapshotVersion") || obj.equals("stops")) - // except don't overwrite the counter that keeps track of snapshot versions - // we also don't overwrite the stops completely, as we need to merge them - continue; - else - targetTx.delete(obj); - } - - int acount, rcount, ccount, ecount, pcount, tcount, fcount; - - if (tx.exists("agencies")) - acount = pump(targetTx, "agencies", (BTreeMap) this.getMap("agencies")); - else - acount = 0; - LOG.info("Restored {} agencies", acount); - - if (tx.exists("routes")) - rcount = pump(targetTx, "routes", (BTreeMap) this.getMap("routes")); - else - rcount = 0; - LOG.info("Restored {} routes", rcount); - - if (tx.exists("calendars")) - ccount = pump(targetTx, "calendars", (BTreeMap) this.getMap("calendars")); - else - ccount = 0; - LOG.info("Restored {} calendars", ccount); - - if (tx.exists("exceptions")) - ecount = pump(targetTx, "exceptions", (BTreeMap) this.getMap("exceptions")); - else - ecount = 0; - LOG.info("Restored {} schedule exceptions", ecount); - - if (tx.exists("tripPatterns")) - pcount = pump(targetTx, "tripPatterns", (BTreeMap) this.getMap("tripPatterns")); - else - pcount = 0; - LOG.info("Restored {} patterns", pcount); - - if (tx.exists("trips")) - tcount = pump(targetTx, "trips", (BTreeMap) this.getMap("trips")); - else - tcount = 0; - LOG.info("Restored {} trips", tcount); - - if (tx.exists("fares")) - fcount = pump(targetTx, "fares", (BTreeMap) this.getMap("fares")); - else - fcount = 0; - LOG.info("Restored {} fares", fcount); - - // restore histograms, see jankotek/mapdb#453 - if (tx.exists("tripCountByCalendar")) - pump(targetTx, "tripCountByCalendar", (BTreeMap) this.getMap("tripCountByCalendar")); - - if (tx.exists("tripCountByPatternAndCalendar")) - pump(targetTx, "tripCountByPatternAndCalendar", - (BTreeMap) this., Long>getMap("tripCountByPatternAndCalendar")); - - // make an FeedTx to build indices and restore stops - LOG.info("Rebuilding indices, this could take a little while . . . "); - FeedTx atx = new FeedTx(targetTx); - LOG.info("done."); - - LOG.info("Restoring deleted stops"); - - // restore any stops that have been deleted - List restoredStops = new ArrayList(); - if (tx.exists("stops")) { - BTreeMap oldStops = this.getMap("stops"); - - for (TripPattern tp : atx.tripPatterns.values()) { - for (TripPatternStop ps : tp.patternStops) { - if (!atx.stops.containsKey(ps.stopId)) { - Stop stop = oldStops.get(ps.stopId); - atx.stops.put(ps.stopId, stop); - restoredStops.add(stop); - } - } - } - } - LOG.info("Restored {} deleted stops", restoredStops.size()); - - atx.commit(); - - return restoredStops; - } - - /** close the underlying data store */ - public void close () { - tx.close(); - closed = true; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/datastore/VersionedDataStore.java b/src/main/java/com/conveyal/datatools/editor/datastore/VersionedDataStore.java deleted file mode 100644 index 818a9987d..000000000 --- a/src/main/java/com/conveyal/datatools/editor/datastore/VersionedDataStore.java +++ /dev/null @@ -1,251 +0,0 @@ -package com.conveyal.datatools.editor.datastore; - -import com.conveyal.datatools.manager.DataManager; -import com.google.common.collect.Maps; -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.editor.models.transit.Stop; -import org.mapdb.BTreeMap; -import org.mapdb.DB; -import org.mapdb.DBMaker; -import org.mapdb.TxMaker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.conveyal.datatools.editor.utils.ClassLoaderSerializer; - -import java.io.File; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; - -/** - * Create a new versioned com.conveyal.datatools.editor.datastore. A versioned data store handles multiple databases, - * the global DB and the agency-specific DBs. It handles creating transactions, and saving and restoring - * snapshots. - * @author mattwigway - * - */ -public class VersionedDataStore { - public static final Logger LOG = LoggerFactory.getLogger(VersionedDataStore.class); - private static File dataDirectory = new File((String) DataManager.config.get("application").get("data").get("editor_mapdb").asText()); - private static TxMaker globalTxMaker; - - private static Map feedTxMakers = Maps.newConcurrentMap(); - - static { - File globalDataDirectory = new File(dataDirectory, "global"); - globalDataDirectory.mkdirs(); - - // initialize the global database - globalTxMaker = DBMaker.newFileDB(new File(globalDataDirectory, "global.db")) - .mmapFileEnable() - .compressionEnable() - .makeTxMaker(); - } - - /** Start a transaction in the global database */ - public static GlobalTx getGlobalTx () { - return new GlobalTx(globalTxMaker.makeTx()); - } - - /** - * Start a transaction in an agency database. No checking is done to ensure the agency exists; - * if it does not you will get a (hopefully) empty DB, unless you've done the same thing previously. - */ - public static FeedTx getFeedTx(String feedId) { - return new FeedTx(getRawFeedTx(feedId)); - } - - /** - * Get a raw MapDB transaction for the given database. Use at your own risk - doesn't properly handle indexing, etc. - * Intended for use primarily with database restore - */ - static DB getRawFeedTx(String feedId) { - if (!feedTxMakers.containsKey(feedId)) { - synchronized (feedTxMakers) { - if (!feedTxMakers.containsKey(feedId)) { - File path = new File(dataDirectory, feedId); - path.mkdirs(); - - TxMaker agencyTxm = DBMaker.newFileDB(new File(path, "master.db")) - .mmapFileEnable() - .compressionEnable() - .makeTxMaker(); - - feedTxMakers.put(feedId, agencyTxm); - } - } - } - - return feedTxMakers.get(feedId).makeTx(); - } - - /** Take a snapshot of an agency database. The snapshot will be saved in the global database. */ - public static Snapshot takeSnapshot (String feedId, String name, String comment) { - FeedTx tx = getFeedTx(feedId); - GlobalTx gtx = getGlobalTx(); - int version = -1; - DB snapshot = null; - Snapshot ret = null; - try { - version = tx.getNextSnapshotId(); - - LOG.info("Creating snapshot {} for feed {}", feedId, version); - long startTime = System.currentTimeMillis(); - - ret = new Snapshot(feedId, version); - - if (gtx.snapshots.containsKey(ret.id)) - throw new IllegalStateException("Duplicate snapshot IDs"); - - ret.snapshotTime = System.currentTimeMillis(); - ret.name = name; - ret.comment = comment; - ret.current = true; - - snapshot = getSnapshotDb(feedId, version, false); - - new SnapshotTx(snapshot).make(tx); - // for good measure - snapshot.commit(); - snapshot.close(); - - gtx.snapshots.put(ret.id, ret); - gtx.commit(); - tx.commit(); - String snapshotMessage = String.format("Saving snapshot took %.2f seconds", (System.currentTimeMillis() - startTime) / 1000D); - LOG.info(snapshotMessage); - - return ret; - } catch (Exception e) { - // clean up - if (snapshot != null && !snapshot.isClosed()) - snapshot.close(); - - if (version >= 0) { - File snapshotDir = getSnapshotDir(feedId, version); - - if (snapshotDir.exists()) { - for (File file : snapshotDir.listFiles()) { - file.delete(); - } - } - } - - // re-throw - throw new RuntimeException(e); - } finally { - tx.rollbackIfOpen(); - gtx.rollbackIfOpen(); - } - } - - /** - * restore a snapshot. - * @return a list of stops that were restored from deletion to make this snapshot valid. - */ - public static List restore (Snapshot s) { - SnapshotTx tx = new SnapshotTx(getSnapshotDb(s.feedId, s.version, true)); - try { - LOG.info("Restoring snapshot {} of agency {}", s.version, s.feedId); - long startTime = System.currentTimeMillis(); - List ret = tx.restore(s.feedId); - LOG.info("Restored snapshot in %.2f seconds", (System.currentTimeMillis() - startTime) / 1000D); - return ret; - } finally { - tx.close(); - } - } - - /** get the directory in which to store a snapshot */ - public static DB getSnapshotDb (String feedId, int version, boolean readOnly) { - File thisSnapshotDir = getSnapshotDir(feedId, version); - thisSnapshotDir.mkdirs(); - File snapshotFile = new File(thisSnapshotDir, "snapshot_" + version + ".db"); - - // we don't use transactions for snapshots - makes them faster - // and smaller. - // at the end everything gets committed and flushed to disk, so this thread - // will not complete until everything is done. - // also, we compress the snapshot databases - DBMaker maker = DBMaker.newFileDB(snapshotFile) - .compressionEnable(); - - if (readOnly) - maker.readOnly(); - - return maker.make(); - } - - /** get the directory in which a snapshot is stored */ - public static File getSnapshotDir (String feedId, int version) { - File agencyDir = new File(dataDirectory, feedId); - File snapshotsDir = new File(agencyDir, "snapshots"); - return new File(snapshotsDir, "" + version); - } - - /** Convenience function to check if an agency exists */ - public static boolean agencyExists (String feedId) { - GlobalTx tx = getGlobalTx(); - boolean exists = tx.feeds.containsKey(feedId); - tx.rollback(); - return exists; - } - - /** Get a (read-only) agency TX into a particular snapshot version of an agency */ - public static FeedTx getFeedTx(String feedId, int version) { - DB db = getSnapshotDb(feedId, version, true); - return new FeedTx(db, false); - } - - /** A wrapped transaction, so the database just looks like a POJO */ - public static class DatabaseTx { - /** the database (transaction). subclasses must initialize. */ - protected final DB tx; - - /** has this transaction been closed? */ - boolean closed = false; - - /** Convenience function to get a map */ - protected final BTreeMap getMap (String name) { - return tx.createTreeMap(name) - // use java serialization to allow for schema upgrades - .valueSerializer(new ClassLoaderSerializer()) - .makeOrGet(); - } - - /** - * Convenience function to get a set. These are used as indices so they use the default serialization; - * if we make a schema change we drop and recreate them. - */ - protected final NavigableSet getSet (String name) { - return tx.createTreeSet(name) - .makeOrGet(); - } - - protected DatabaseTx (DB tx) { - this.tx = tx; - } - - public void commit() { - tx.commit(); - closed = true; - } - - public void rollback() { - tx.rollback(); - closed = true; - } - - /** roll this transaction back if it has not been committed or rolled back already */ - public void rollbackIfOpen () { - if (!closed) rollback(); - } - - protected final void finalize () { - if (!closed) { - LOG.error("DB transaction left unclosed, this signifies a memory leak!"); - rollback(); - } - } - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/jobs/GisExport.java b/src/main/java/com/conveyal/datatools/editor/jobs/GisExport.java deleted file mode 100644 index 664b31a70..000000000 --- a/src/main/java/com/conveyal/datatools/editor/jobs/GisExport.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.conveyal.datatools.editor.jobs; - -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.manager.models.FeedSource; -import com.google.common.io.Files; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.transit.*; -import org.geotools.data.DataUtilities; -import org.geotools.data.DefaultTransaction; -import org.geotools.data.Transaction; -import org.geotools.data.collection.ListFeatureCollection; -import org.geotools.data.shapefile.ShapefileDataStore; -import org.geotools.data.shapefile.ShapefileDataStoreFactory; -import org.geotools.data.simple.SimpleFeatureCollection; -import org.geotools.data.simple.SimpleFeatureSource; -import org.geotools.data.simple.SimpleFeatureStore; -import org.geotools.feature.simple.SimpleFeatureBuilder; -import org.geotools.referencing.crs.DefaultGeographicCRS; -import org.opengis.feature.simple.SimpleFeature; -import org.opengis.feature.simple.SimpleFeatureType; -import com.conveyal.datatools.editor.utils.DirectoryZip; - -import java.io.File; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Export routes or stops as a shapefile */ -public class GisExport implements Runnable { - File file; - Type type; - Collection agencyIds; - - public GisExport(Type type, File file, Collection agencyIds) { - this.type = type; - this.file = file; - this.agencyIds = agencyIds; - } - - @Override - public void run() { - File outDir = Files.createTempDir(); - File outShp = new File(outDir, file.getName().replaceAll("\\.zip", "") + ".shp"); - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - FeedTx atx = null; - try { - ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory(); - - Map params = new HashMap(); - params.put("url", outShp.toURI().toURL()); - params.put("create spatial index", Boolean.TRUE); - - ShapefileDataStore datastore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params); - datastore.forceSchemaCRS(DefaultGeographicCRS.WGS84); - - SimpleFeatureType STOP_TYPE = DataUtilities.createType( - "Stop", - "the_geom:Point:srid=4326," + - "name:String," + - "code:String," + - "desc:String," + - "id:String," + - "agency:String" - ); - - SimpleFeatureType ROUTE_TYPE = DataUtilities.createType( - "Route", // <- the name for our feature type - "the_geom:LineString:srid=4326," + - "patternName:String," + - "shortName:String," + - "longName:String," + - "desc:String," + - "type:String," + - "url:String," + - "routeColor:String," + - "routeTextColor:String," + - "agency:String" - ); - - SimpleFeatureCollection collection; - - SimpleFeatureType collectionType; - - SimpleFeatureBuilder featureBuilder = null; - - List features = new ArrayList(); - - if (type.equals(Type.STOPS)) { - collectionType = STOP_TYPE; - datastore.createSchema(STOP_TYPE); - featureBuilder = new SimpleFeatureBuilder(STOP_TYPE); - - for (String feedId : agencyIds) { - EditorFeed fs = gtx.feeds.get(feedId); - - atx = VersionedDataStore.getFeedTx(feedId); - for (Stop s : atx.stops.values()) { - featureBuilder.add(s.location); - featureBuilder.add(s.stopName); - featureBuilder.add(s.stopCode); - featureBuilder.add(s.stopDesc); - featureBuilder.add(s.getGtfsId()); - featureBuilder.add(fs.feedPublisherName); - SimpleFeature feature = featureBuilder.buildFeature(null); - features.add(feature); - } - - atx.rollback(); - } - } else if (type.equals(Type.ROUTES)) { - collectionType = ROUTE_TYPE; - datastore.createSchema(ROUTE_TYPE); - featureBuilder = new SimpleFeatureBuilder(ROUTE_TYPE); - - GeometryFactory gf = new GeometryFactory(); - - for (String feedId : agencyIds) { - EditorFeed fs = gtx.feeds.get(feedId); - - atx = VersionedDataStore.getFeedTx(feedId); - - // we loop over trip patterns. Note that this will yield several lines for routes that have - // multiple patterns. There's no real good way to reconcile the shapes of multiple patterns. - for (TripPattern tp : atx.tripPatterns.values()) { - LineString shape; - if (tp.shape != null) { - shape = tp.shape; - } else { - // build the shape from the stops - Coordinate[] coords = new Coordinate[tp.patternStops.size()]; - - for (int i = 0; i < coords.length; i++) { - coords[i] = atx.stops.get(tp.patternStops.get(i).stopId).location.getCoordinate(); - } - - shape = gf.createLineString(coords); - } - - Route r = atx.routes.get(tp.routeId); - - featureBuilder.add(shape); - featureBuilder.add(tp.name); - featureBuilder.add(r.routeShortName); - featureBuilder.add(r.routeLongName); - featureBuilder.add(r.routeDesc); - - if (r.routeTypeId != null) - featureBuilder.add(gtx.routeTypes.get(r.routeTypeId).toString()); - else - featureBuilder.add(""); - - featureBuilder.add(r.routeUrl); - featureBuilder.add(r.routeColor); - featureBuilder.add(r.routeTextColor); - featureBuilder.add(fs.feedPublisherName); - SimpleFeature feature = featureBuilder.buildFeature(null); - features.add(feature); - } - - atx.rollback(); - } - } - else - throw new IllegalStateException("Invalid type"); - - // save the file - collection = new ListFeatureCollection(collectionType, features); - - Transaction transaction = new DefaultTransaction("create"); - - String typeName = datastore.getTypeNames()[0]; - SimpleFeatureSource featureSource = datastore.getFeatureSource(typeName); - - if (featureSource instanceof SimpleFeatureStore) - { - SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; - - featureStore.setTransaction(transaction); - - featureStore.addFeatures(collection); - transaction.commit(); - - transaction.close(); - } - else - { - throw new Exception(typeName + " does not support read/write access"); - } - - // zip the file - DirectoryZip.zip(outDir, file); - - // clean up - for (File f : outDir.listFiles()) { - f.delete(); - } - outDir.delete(); - - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (gtx != null) gtx.rollback(); - if (atx != null) atx.rollbackIfOpen(); - } - } - - public static enum Type { ROUTES, STOPS }; -} diff --git a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGisExport.java b/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGisExport.java deleted file mode 100755 index 5467a7c28..000000000 --- a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGisExport.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.conveyal.datatools.editor.jobs; - - -public class ProcessGisExport implements Runnable { - @Override - public void run() { - - } -/* - private Long _gisExportId; - - - public ProcessGisExport(Long gisExportId) - { - this._gisExportId = gisExportId; - } - - public void doJob() { - - String exportName = "gis_" + this._gisExportId; - - File outputZipFile = new File(Play.configuration.getProperty("application.publicDataDirectory"), exportName + ".zip"); - - File outputDirectory = new File(Play.configuration.getProperty("application.publicDataDirectory"), exportName); - - LOG.info("outfile path:" + outputDirectory.getAbsolutePath()); - - File outputShapefile = new File(outputDirectory, exportName + ".shp"); - - try - { - GisExport gisExport = null; - - while(gisExport == null) - { - gisExport = GisExport.findById(this._gisExportId); - Thread.sleep(1000); - - LOG.info("Waiting for gisExport object..."); - } - - - if(!outputDirectory.exists()) - { - outputDirectory.mkdir(); - } - - ShapefileDataStoreFactory com.conveyal.datatools.editor.datastoreFactory = new ShapefileDataStoreFactory(); - - Map params = new HashMap(); - params.put("url", outputShapefile.toURI().toURL()); - params.put("create spatial index", Boolean.TRUE); - - ShapefileDataStore com.conveyal.datatools.editor.datastore = (ShapefileDataStore)dataStoreFactory.createNewDataStore(params); - com.conveyal.datatools.editor.datastore.forceSchemaCRS(DefaultGeographicCRS.WGS84); - - SimpleFeatureType STOP_TYPE = DataUtilities.createType( - "Stop", - "location:Point:srid=4326," + - "name:String," + - "code:String," + - "desc:String," + - "id:String," + - "agency:String" - ); - - SimpleFeatureType ROUTE_TYPE = DataUtilities.createType( - "Route", // <- the name for our feature type - "route:LineString:srid=4326," + - "patternName:String," + - "shortName:String," + - "longName:String," + - "desc:String," + - "type:String," + - "url:String," + - "routeColor:String," + - "routeTextColor:String," + - "agency:String" - ); - - SimpleFeatureCollection collection; - - SimpleFeatureType collectionType; - - SimpleFeatureBuilder featureBuilder = null; - - List features = new ArrayList(); - - if(gisExport.type.equals(GisUploadType.STOPS)) - { - collectionType = STOP_TYPE; - com.conveyal.datatools.editor.datastore.createSchema(STOP_TYPE); - featureBuilder = new SimpleFeatureBuilder(STOP_TYPE); - - List stops = Stop.find("agency in (:ids)").bind("ids", gisExport.feeds).fetch(); - - for(Stop s : stops) - { - featureBuilder.add(s.locationPoint()); - featureBuilder.add(s.stopName); - featureBuilder.add(s.stopCode); - featureBuilder.add(s.stopDesc); - featureBuilder.add(s.gtfsStopId); - featureBuilder.add(s.agency.name); - SimpleFeature feature = featureBuilder.buildFeature(null); - features.add(feature); - } - } - else if(gisExport.type.equals(GisUploadType.ROUTES)) - { - collectionType = ROUTE_TYPE; - com.conveyal.datatools.editor.datastore.createSchema(ROUTE_TYPE); - featureBuilder = new SimpleFeatureBuilder(ROUTE_TYPE); - - List routes = Route.find("agency in (:ids)").bind("ids", gisExport.feeds).fetch(); - - // check for duplicates - - // HashMap existingRoutes = new HashMap(); - - for(Route r : routes) - { -// String routeId = r.routeLongName + "_" + r.routeDesc + "_ " + r.phone.id; -// -// if(existingRoutes.containsKey(routeId)) -// continue; -// else -// existingRoutes.put(routeId, true); - - - List patterns = TripPattern.find("route = ?", r).fetch(); - for(TripPattern tp : patterns) - { - if(tp.shape == null) - continue; - - featureBuilder.add(tp.shape.shape); - featureBuilder.add(tp.name); - featureBuilder.add(r.routeShortName); - featureBuilder.add(r.routeLongName); - featureBuilder.add(r.routeDesc); - - if(r.routeType != null) - featureBuilder.add(r.routeType.toString()); - else - featureBuilder.add(""); - - featureBuilder.add(r.routeUrl); - featureBuilder.add(r.routeColor); - featureBuilder.add(r.routeTextColor); - featureBuilder.add(r.agency.name); - SimpleFeature feature = featureBuilder.buildFeature(null); - features.add(feature); - } - } - } - else - throw new Exception("Unknown export type."); - - collection = new ListFeatureCollection(collectionType, features); - - Transaction transaction = new DefaultTransaction("create"); - - String typeName = com.conveyal.datatools.editor.datastore.getTypeNames()[0]; - SimpleFeatureSource featureSource = com.conveyal.datatools.editor.datastore.getFeatureSource(typeName); - - if (featureSource instanceof SimpleFeatureStore) - { - SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; - - featureStore.setTransaction(transaction); - - featureStore.addFeatures(collection); - transaction.commit(); - - transaction.close(); - } - else - { - throw new Exception(typeName + " does not support read/write access"); - } - - DirectoryZip.zip(outputDirectory, outputZipFile); - FileUtils.deleteDirectory(outputDirectory); - - gisExport.status = GisExportStatus.PROCESSED; - - gisExport.save(); - - } - catch(Exception e) - { - LOG.error("Unable to process GIS export: ", e.toString()); - e.printStackTrace(); - } - }*/ -} - - diff --git a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGisUpload.java b/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGisUpload.java deleted file mode 100755 index 7d4623b29..000000000 --- a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGisUpload.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.conveyal.datatools.editor.jobs; - -public class ProcessGisUpload implements Runnable { - @Override - public void run() { - - } - /* - private Long _gisUploadId; - - public ProcessGisUpload(Long gisUploadId) - { - this._gisUploadId = gisUploadId; - } - - public void doJob() { - - String uploadName = "gis_" + this._gisUploadId; - - File uploadedFile = new File(Play.configuration.getProperty("application.publicGisDataDirectory"), uploadName + ".zip"); - - File outputPath = new File(Play.configuration.getProperty("application.publicGisDataDirectory"), uploadName); - - - try - { - GisUpload gisUpload = null; - - while(gisUpload == null) - { - gisUpload = GisUpload.findById(this._gisUploadId); - Thread.sleep(1000); - - LOG.info("Waiting for gisUpload object..."); - } - - File shapeFile = null; - - // unpack the zip if needed - if(!outputPath.exists()) - { - outputPath.mkdir(); - - FileInputStream fileInputStream = new FileInputStream(uploadedFile); - ZipInputStream zipInput = new ZipInputStream(fileInputStream); - - ZipEntry zipEntry = null; - - while ((zipEntry = zipInput.getNextEntry()) != null) - { - if(zipEntry.isDirectory()) - { - LOG.info("Unexpected directory: ", zipEntry.getName()); - } - else - { - LOG.info("Unzipping", zipEntry.getName()); - - File entryFile = new File(outputPath, zipEntry.getName()); - - FileOutputStream unzippedFileOut = new FileOutputStream(entryFile); - - int length; - byte[] buffer = new byte[1000]; - - while ((length = zipInput.read(buffer))>0) - { - unzippedFileOut.write(buffer, 0, length); - } - - zipInput.closeEntry(); - unzippedFileOut.close(); - } - } - - zipInput.close(); - } - - // find the shapefile - for(File dirFile : outputPath.listFiles()) - { - if(FilenameUtils.getExtension(dirFile.getName()).toLowerCase().equals("shp")) - { - if(shapeFile == null) - shapeFile = dirFile; - else - LOG.warn("Zip contains more than one shapefile--ignoring others."); - } - } - - // (re)load the shapefile data - if(shapeFile != null) - { - - // remove existing imports - if(gisUpload.type == GisUploadType.ROUTES) - { - List routes = GisRoute.find("gisUpload = ?", gisUpload).fetch(); - - for(GisRoute route : routes) - { - route.clear(); - route.delete(); - } - } - else if(gisUpload.type == GisUploadType.STOPS) - GisStop.delete("gisUpload = ?", gisUpload); - - // remove existing updload field mappings - GisUploadField.delete("gisUpload = ?", gisUpload); - - FileDataStore store = FileDataStoreFinder.getDataStore(shapeFile); - SimpleFeatureSource featureSource = store.getFeatureSource(); - - SimpleFeatureCollection featureCollection = featureSource.getFeatures(); - SimpleFeatureIterator featureIterator = featureCollection.features(); - - List attributeDescriptors = featureSource.getSchema().getAttributeDescriptors(); - - // update field listing - Long position = new Long(0); - - for(AttributeDescriptor attribute : attributeDescriptors) - { - GisUploadField field = new GisUploadField(); - field.fieldName = attribute.getName().toString(); - field.fieldType = attribute.getType().getName().getLocalPart(); - field.fieldPosition = position; - - field.gisUpload = gisUpload; - - field.save(); - - position++; - } - - - CoordinateReferenceSystem dataCRS = featureSource.getSchema().getCoordinateReferenceSystem(); - - String code = "EPSG:4326"; - CRSAuthorityFactory crsAuthorityFactory = CRS.getAuthorityFactory(true); - CoordinateReferenceSystem mapCRS = crsAuthorityFactory.createCoordinateReferenceSystem(code); - - - boolean lenient = true; // allow for some error due to different datums - MathTransform transform = CRS.findMathTransform(dataCRS, mapCRS, lenient); - - while (featureIterator.hasNext()) - { - SimpleFeature feature = featureIterator.next(); - - GeometryType geomType = feature.getFeatureType().getGeometryDescriptor().getType(); - - // handle appropriate shape/upload type - if(gisUpload.type == GisUploadType.ROUTES) - { - if(geomType.getBinding() != MultiLineString.class) - { - LOG.error("Unexpected geometry type: ", geomType); - continue; - } - - MultiLineString multiLineString = (MultiLineString)JTS.transform((Geometry)feature.getDefaultGeometry(), transform); - - - GisRoute route = new GisRoute(); - - route.gisUpload = gisUpload; - route.agency = gisUpload.agency; - route.oid = feature.getID(); - route.originalShape = multiLineString; - route.originalShape.setSRID(4326); - - if(gisUpload.fieldName != null) - { - FeatureAttributeFormatter attribFormatter = new FeatureAttributeFormatter(gisUpload.fieldName); - route.routeName = attribFormatter.format(feature); - - } - if(gisUpload.fieldId != null) - { - FeatureAttributeFormatter attribFormatter = new FeatureAttributeFormatter(gisUpload.fieldId); - route.routeId = attribFormatter.format(feature); - } - if(gisUpload.fieldDescription != null) - { - FeatureAttributeFormatter attribFormatter = new FeatureAttributeFormatter(gisUpload.fieldDescription); - route.description = attribFormatter.format(feature); - } - - route.save(); - - route.processSegments(); - - } - else if(gisUpload.type == GisUploadType.STOPS) - { - if(geomType.getBinding() != Point.class) - { - LOG.error("Unexpected geometry type: ", geomType); - continue; - } - - GisStop stop = new GisStop(); - - stop.gisUpload = gisUpload; - stop.agency = gisUpload.agency; - stop.oid = feature.getID(); - stop.shape = (Point)JTS.transform((Geometry)feature.getDefaultGeometry(), transform); - stop.shape.setSRID(4326); - - if(gisUpload.fieldName != null) - { - FeatureAttributeFormatter attribFormatter = new FeatureAttributeFormatter(gisUpload.fieldName); - stop.stopName = attribFormatter.format(feature); - } - if(gisUpload.fieldId != null) - { - FeatureAttributeFormatter attribFormatter = new FeatureAttributeFormatter(gisUpload.fieldId); - stop.stopId = attribFormatter.format(feature); - } - if(gisUpload.fieldDescription != null) - { - FeatureAttributeFormatter attribFormatter = new FeatureAttributeFormatter(gisUpload.fieldDescription); - stop.description = attribFormatter.format(feature); - } - - stop.save(); - } - } - } - else - { - LOG.error("Zip didn't contain a valid shapefile."); - } - } - catch(Exception e) - { - LOG.error("Unable to process GIS Upload: ", e.toString()); - e.printStackTrace(); - } - } - */ -} - diff --git a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotExport.java b/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotExport.java deleted file mode 100755 index 91c1b399e..000000000 --- a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotExport.java +++ /dev/null @@ -1,290 +0,0 @@ -package com.conveyal.datatools.editor.jobs; - -import com.beust.jcommander.internal.Lists; -import com.conveyal.gtfs.GTFSFeed; -import com.conveyal.gtfs.model.CalendarDate; -import com.conveyal.gtfs.model.Entity; -import com.conveyal.gtfs.model.Frequency; -import com.conveyal.gtfs.model.Shape; -import com.conveyal.gtfs.model.ShapePoint; -import com.google.common.collect.Maps; -import com.vividsolutions.jts.geom.Coordinate; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.editor.models.transit.*; -import java.time.LocalDate; - -import org.mapdb.Fun; -import org.mapdb.Fun.Tuple2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.conveyal.datatools.editor.utils.GeoUtils; - -import java.io.File; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; - -public class ProcessGtfsSnapshotExport implements Runnable { - public static final Logger LOG = LoggerFactory.getLogger(ProcessGtfsSnapshotExport.class); - private Collection> snapshots; - private File output; - private LocalDate startDate; - private LocalDate endDate; - - /** Export the named snapshots to GTFS */ - public ProcessGtfsSnapshotExport(Collection> snapshots, File output, LocalDate startDate, LocalDate endDate) { - this.snapshots = snapshots; - this.output = output; -// this.startDate = startDate; -// this.endDate = endDate; - } - - /** - * Export the master branch of the named feeds to GTFS. The boolean variable can be either true or false, it is only to make this - * method have a different erasure from the other - */ - public ProcessGtfsSnapshotExport(Collection agencies, File output, LocalDate startDate, LocalDate endDate, boolean isagency) { - this.snapshots = Lists.newArrayList(agencies.size()); - - for (String agency : agencies) { - // leaving version null will cause master to be used - this.snapshots.add(new Tuple2(agency, null)); - } - - this.output = output; -// this.startDate = startDate; -// this.endDate = endDate; - } - - /** - * Export this snapshot to GTFS, using the validity range in the snapshot. - */ - public ProcessGtfsSnapshotExport (Snapshot snapshot, File output) { - this(Arrays.asList(new Tuple2[] { snapshot.id }), output, snapshot.validFrom, snapshot.validTo); - } - - @Override - public void run() { - GTFSFeed feed = new GTFSFeed(); - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - FeedTx feedTx = null; - - try { - for (Tuple2 ssid : snapshots) { - String feedId = ssid.a; - - feedTx = VersionedDataStore.getFeedTx(feedId); - Collection agencies = feedTx.agencies.values(); - - for (Agency agency : agencies) { - com.conveyal.gtfs.model.Agency gtfsAgency = agency.toGtfs(); - LOG.info("Exporting agency {}", gtfsAgency); - - // write the feeds.txt entry - feed.agency.put(agency.agencyId, agency.toGtfs()); - } - - // write all of the calendars and calendar dates - if (feedTx.calendars != null) { - for (ServiceCalendar cal : feedTx.calendars.values()) { - - com.conveyal.gtfs.model.Service gtfsService = cal.toGtfs(toGtfsDate(cal.startDate), toGtfsDate(cal.endDate)); - // note: not using user-specified IDs - - // add calendar dates - if (feedTx.exceptions != null) { - for (ScheduleException ex : feedTx.exceptions.values()) { - if (ex.equals(ScheduleException.ExemplarServiceDescriptor.SWAP) && !ex.addedService.contains(cal.id) && !ex.removedService.contains(cal.id)) - // skip swap exception if cal is not referenced by added or removed service - // this is not technically necessary, but the output is cleaner/more intelligible - continue; - - for (LocalDate date : ex.dates) { - if (date.isBefore(cal.startDate) || date.isAfter(cal.endDate)) - // no need to write dates that do not apply - continue; - - CalendarDate cd = new CalendarDate(); - cd.date = date; - cd.service_id = gtfsService.service_id; - cd.exception_type = ex.serviceRunsOn(cal) ? 1 : 2; - - if (gtfsService.calendar_dates.containsKey(date)) - throw new IllegalArgumentException("Duplicate schedule exceptions on " + date.toString()); - - gtfsService.calendar_dates.put(date, cd); - } - } - } - - feed.services.put(gtfsService.service_id, gtfsService); - } - } - - Map gtfsRoutes = Maps.newHashMap(); - - // write the routes - if(feedTx.routes != null) { - for (Route route : feedTx.routes.values()) { - // only export approved routes - // TODO: restore route approval check? - //if (route.status == StatusType.APPROVED) { - - com.conveyal.gtfs.model.Route gtfsRoute = route.toGtfs(feedTx.agencies.get(route.agencyId).toGtfs(), gtx); - - feed.routes.put(route.getGtfsId(), gtfsRoute); - - gtfsRoutes.put(route.id, gtfsRoute); - //} - } - } - - // write the trips on those routes - // we can't use the trips-by-route index because we may be exporting a snapshot database without indices - if(feedTx.trips != null) { - for (Trip trip : feedTx.trips.values()) { - if (!gtfsRoutes.containsKey(trip.routeId)) { - LOG.warn("Trip {} has not matching route", trip); - continue; - } - - com.conveyal.gtfs.model.Route gtfsRoute = gtfsRoutes.get(trip.routeId); - Route route = feedTx.routes.get(trip.routeId); - - com.conveyal.gtfs.model.Trip gtfsTrip = new com.conveyal.gtfs.model.Trip(); - - gtfsTrip.block_id = trip.blockId; - gtfsTrip.route_id = gtfsRoute.route_id; - gtfsTrip.trip_id = trip.getGtfsId(); - // not using custom ids for calendars - gtfsTrip.service_id = feed.services.get(trip.calendarId).service_id; - gtfsTrip.trip_headsign = trip.tripHeadsign; - gtfsTrip.trip_short_name = trip.tripShortName; - gtfsTrip.direction_id = trip.tripDirection == TripDirection.A ? 0 : 1; - - TripPattern pattern = feedTx.tripPatterns.get(trip.patternId); - - Tuple2 nextKey = feed.shape_points.ceilingKey(new Tuple2(pattern.id, null)); - if ((nextKey == null || !pattern.id.equals(nextKey.a)) && pattern.shape != null && !pattern.useStraightLineDistances) { - // this shape has not yet been saved - double[] coordDistances = GeoUtils.getCoordDistances(pattern.shape); - - for (int i = 0; i < coordDistances.length; i++) { - Coordinate coord = pattern.shape.getCoordinateN(i); - ShapePoint shape = new ShapePoint(pattern.id, coord.y, coord.x, i + 1, coordDistances[i]); - feed.shape_points.put(new Tuple2(pattern.id, shape.shape_pt_sequence), shape); - } - } - - if (pattern.shape != null && !pattern.useStraightLineDistances) - gtfsTrip.shape_id = pattern.id; - - if (trip.wheelchairBoarding != null) { - if (trip.wheelchairBoarding.equals(AttributeAvailabilityType.AVAILABLE)) - gtfsTrip.wheelchair_accessible = 1; - - else if (trip.wheelchairBoarding.equals(AttributeAvailabilityType.UNAVAILABLE)) - gtfsTrip.wheelchair_accessible = 2; - - else - gtfsTrip.wheelchair_accessible = 0; - - } else if (route.wheelchairBoarding != null) { - if (route.wheelchairBoarding.equals(AttributeAvailabilityType.AVAILABLE)) - gtfsTrip.wheelchair_accessible = 1; - - else if (route.wheelchairBoarding.equals(AttributeAvailabilityType.UNAVAILABLE)) - gtfsTrip.wheelchair_accessible = 2; - - else - gtfsTrip.wheelchair_accessible = 0; - - } - - feed.trips.put(gtfsTrip.trip_id, gtfsTrip); - - TripPattern patt = feedTx.tripPatterns.get(trip.patternId); - - Iterator psi = patt.patternStops.iterator(); - - int stopSequence = 1; - - // write the stop times - for (StopTime st : trip.stopTimes) { - TripPatternStop ps = psi.next(); - if (st == null) - continue; - - Stop stop = feedTx.stops.get(st.stopId); - - if (!st.stopId.equals(ps.stopId)) { - throw new IllegalStateException("Trip " + trip.id + " does not match its pattern!"); - } - - com.conveyal.gtfs.model.StopTime gst = new com.conveyal.gtfs.model.StopTime(); - gst.arrival_time = st.arrivalTime != null ? st.arrivalTime : Entity.INT_MISSING; - gst.departure_time = st.departureTime != null ? st.departureTime : Entity.INT_MISSING; - - if (st.dropOffType != null) - gst.drop_off_type = st.dropOffType.toGtfsValue(); - else if (stop.dropOffType != null) - gst.drop_off_type = stop.dropOffType.toGtfsValue(); - - if (st.pickupType != null) - gst.pickup_type = st.pickupType.toGtfsValue(); - else if (stop.dropOffType != null) - gst.drop_off_type = stop.dropOffType.toGtfsValue(); - - gst.shape_dist_traveled = ps.shapeDistTraveled; - gst.stop_headsign = st.stopHeadsign; - gst.stop_id = stop.getGtfsId(); - - // write the stop as needed - if (!feed.stops.containsKey(gst.stop_id)) { - feed.stops.put(gst.stop_id, stop.toGtfs()); - } - - gst.stop_sequence = stopSequence++; - - if (ps.timepoint != null) - gst.timepoint = ps.timepoint ? 1 : 0; - else - gst.timepoint = Entity.INT_MISSING; - - gst.trip_id = gtfsTrip.trip_id; - - feed.stop_times.put(new Tuple2(gtfsTrip.trip_id, gst.stop_sequence), gst); - } - - // create frequencies as needed - if (trip.useFrequency != null && trip.useFrequency) { - Frequency f = new Frequency(); - f.trip_id = gtfsTrip.trip_id; - f.start_time = trip.startTime; - f.end_time = trip.endTime; - f.exact_times = 0; - f.headway_secs = trip.headway; - feed.frequencies.add(Fun.t2(gtfsTrip.trip_id, f)); - } - } - } - } - - - feed.toFile(output.getAbsolutePath()); - } finally { - gtx.rollbackIfOpen(); - if (feedTx != null) feedTx.rollbackIfOpen(); - } - } - - public static int toGtfsDate (LocalDate date) { - return date.getYear() * 10000 + date.getMonthValue() * 100 + date.getDayOfMonth(); - } -} - diff --git a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotMerge.java b/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotMerge.java deleted file mode 100755 index fb1bd991c..000000000 --- a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotMerge.java +++ /dev/null @@ -1,577 +0,0 @@ -package com.conveyal.datatools.editor.jobs; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.editor.models.transit.Agency; -import com.conveyal.datatools.editor.models.transit.Fare; -import com.conveyal.datatools.editor.models.transit.Route; -import com.conveyal.datatools.editor.models.transit.Stop; -import com.conveyal.datatools.editor.models.transit.StopTime; -import com.conveyal.datatools.editor.models.transit.Trip; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.gtfs.GTFSFeed; -import com.conveyal.gtfs.model.*; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.PrecisionModel; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import gnu.trove.map.TIntObjectMap; -import gnu.trove.map.hash.TIntObjectHashMap; -import com.conveyal.datatools.editor.models.transit.*; -import org.joda.time.DateTimeConstants; - -import java.awt.geom.Rectangle2D; -import java.time.LocalDate; -import org.mapdb.DBMaker; -import org.mapdb.Fun; -import org.mapdb.Fun.Tuple2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -//import play.i18n.Messages; - -import java.io.File; -import java.util.*; -import java.util.Map.Entry; - -import static spark.Spark.halt; - - -public class ProcessGtfsSnapshotMerge extends MonitorableJob { - public static final Logger LOG = LoggerFactory.getLogger(ProcessGtfsSnapshotMerge.class); - /** map from GTFS agency IDs to Agencies */ - private Map agencyIdMap = new HashMap(); - private Map routeIdMap = new HashMap(); - /** map from (gtfs stop ID, database agency ID) -> stop */ - private Map, Stop> stopIdMap = Maps.newHashMap(); - private TIntObjectMap routeTypeIdMap = new TIntObjectHashMap(); - private Map shapes = DBMaker.newTempHashMap(); - - private GTFSFeed input; - private Status status; - private EditorFeed feed; - - public FeedVersion feedVersion; - - /*public ProcessGtfsSnapshotMerge (File gtfsFile) { - this(gtfsFile, null); - }*/ - - public ProcessGtfsSnapshotMerge (FeedVersion feedVersion, String owner) { - super(owner, "Creating snapshot for " + feedVersion.getFeedSource().name, JobType.PROCESS_SNAPSHOT); - this.feedVersion = feedVersion; - status = new Status(); - status.message = "Waiting to begin job..."; - status.percentComplete = 0; - LOG.info("GTFS Snapshot Merge for feedVersion {}", feedVersion.id); - } - public void run () { - long agencyCount = 0; - long routeCount = 0; - long stopCount = 0; - long stopTimeCount = 0; - long tripCount = 0; - long shapePointCount = 0; - long serviceCalendarCount = 0; - long shapeCount = 0; - long fareCount = 0; - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - - // create a new feed based on this version - FeedTx feedTx = VersionedDataStore.getFeedTx(feedVersion.feedSourceId); - - feed = new EditorFeed(); - feed.setId(feedVersion.feedSourceId); - Rectangle2D bounds = feedVersion.getValidationSummary().bounds; - if (bounds != null) { - feed.defaultLat = bounds.getCenterY(); - feed.defaultLon = bounds.getCenterX(); - } - - - try { - synchronized (status) { - status.message = "Wiping old data..."; - status.percentComplete = 2; - } - // clear the existing data - for(String key : feedTx.agencies.keySet()) feedTx.agencies.remove(key); - for(String key : feedTx.routes.keySet()) feedTx.routes.remove(key); - for(String key : feedTx.stops.keySet()) feedTx.stops.remove(key); - for(String key : feedTx.calendars.keySet()) feedTx.calendars.remove(key); - for(String key : feedTx.exceptions.keySet()) feedTx.exceptions.remove(key); - for(String key : feedTx.fares.keySet()) feedTx.fares.remove(key); - for(String key : feedTx.tripPatterns.keySet()) feedTx.tripPatterns.remove(key); - for(String key : feedTx.trips.keySet()) feedTx.trips.remove(key); - LOG.info("Cleared old data"); - - // input = feedVersion.getGtfsFeed(); - // TODO: use GtfsCache? - synchronized (status) { - status.message = "Loading GTFS file..."; - status.percentComplete = 5; - } - input = feedVersion.getGtfsFeed(); - if(input == null) return; - - LOG.info("GtfsImporter: importing feed..."); - synchronized (status) { - status.message = "Beginning feed import..."; - status.percentComplete = 8; - } - // load feed_info.txt - if(input.feedInfo.size() > 0) { - FeedInfo feedInfo = input.feedInfo.values().iterator().next(); - feed.feedPublisherName = feedInfo.feed_publisher_name; - feed.feedPublisherUrl = feedInfo.feed_publisher_url; - feed.feedLang = feedInfo.feed_lang; - feed.feedEndDate = feedInfo.feed_end_date; - feed.feedStartDate = feedInfo.feed_start_date; - feed.feedVersion = feedInfo.feed_version; - } - gtx.feeds.put(feedVersion.feedSourceId, feed); - - // load the GTFS agencies - for (com.conveyal.gtfs.model.Agency gtfsAgency : input.agency.values()) { - Agency agency = new Agency(gtfsAgency, feed); - - // don't save the agency until we've come up with the stop centroid, below. - agencyCount++; - - // we do want to use the modified agency ID here, because everything that refers to it has a reference - // to the agency object we updated. - feedTx.agencies.put(agency.id, agency); - agencyIdMap.put(gtfsAgency.agency_id, agency); - } - synchronized (status) { - status.message = "Agencies loaded: " + agencyCount; - status.percentComplete = 10; - } - LOG.info("Agencies loaded: " + agencyCount); - - LOG.info("GtfsImporter: importing stops..."); - synchronized (status) { - status.message = "Importing stops..."; - status.percentComplete = 15; - } - // TODO: remove stop ownership inference entirely? - // infer agency ownership of stops, if there are multiple feeds - SortedSet> stopsByAgency = inferAgencyStopOwnership(); - - // build agency centroids as we go - // note that these are not actually centroids, but the center of the extent of the stops . . . - Map stopEnvelopes = Maps.newHashMap(); - - for (Agency agency : agencyIdMap.values()) { - stopEnvelopes.put(agency.id, new Envelope()); - } - - GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); - for (com.conveyal.gtfs.model.Stop gtfsStop : input.stops.values()) { - Stop stop = new Stop(gtfsStop, geometryFactory, feed); - feedTx.stops.put(stop.id, stop); - stopIdMap.put(new Tuple2(gtfsStop.stop_id, feed.id), stop); - stopCount++; - - /* - // duplicate the stop for all of the feeds by which it is used - Collection agencies = Collections2.transform( - stopsByAgency.subSet(new Tuple2(gtfsStop.stop_id, null), new Tuple2(gtfsStop.stop_id, Fun.HI)), - new Function, Agency>() { - - @Override - public Agency apply(Tuple2 input) { - // TODO Auto-generated method stub - return agencyIdMap.get(input.b); - } - }); - - // impossible to tell to whom unused stops belong, so give them to everyone - if (agencies.size() == 0) - agencies = agencyIdMap.values(); - - for (Agency agency : agencies) { - Stop stop = new Stop(gtfsStop, geometryFactory, agency); - agencyTxs.get(agency.id).stops.put(stop.id, stop); - stopIdMap.put(new Tuple2(gtfsStop.stop_id, agency.id), stop); - stopEnvelopes.get(agency.id).expandToInclude(gtfsStop.stop_lon, gtfsStop.stop_lat); - }*/ - } - - LOG.info("Stops loaded: " + stopCount); - synchronized (status) { - status.message = "Stops loaded: " + stopCount; - status.percentComplete = 25; - } - LOG.info("GtfsImporter: importing routes..."); - synchronized (status) { - status.message = "Importing routes..."; - status.percentComplete = 30; - } - // import routes - for (com.conveyal.gtfs.model.Route gtfsRoute : input.routes.values()) { - Agency agency = agencyIdMap.get(gtfsRoute.agency_id); - - if (!routeTypeIdMap.containsKey(gtfsRoute.route_type)) { - RouteType rt = new RouteType(); - rt.gtfsRouteType = GtfsRouteType.fromGtfs(gtfsRoute.route_type); -// rt.hvtRouteType = rt.gtfsRouteType.toHvt(); -// rt.description = agencyIdMap.values().iterator().next().name + " " + rt.gtfsRouteType.toString(); - gtx.routeTypes.put(rt.id, rt); - routeTypeIdMap.put(gtfsRoute.route_type, rt.id); - } - - Route route = new Route(gtfsRoute, feed, agency); - - feedTx.routes.put(route.id, route); - routeIdMap.put(gtfsRoute.route_id, route); - routeCount++; - } - - LOG.info("Routes loaded: " + routeCount); - synchronized (status) { - status.message = "Routes loaded: " + routeCount; - status.percentComplete = 35; - } - - LOG.info("GtfsImporter: importing Service Calendars..."); - synchronized (status) { - status.message = "Importing service calendars..."; - status.percentComplete = 38; - } - // we don't put service calendars in the database just yet, because we don't know what agency they're associated with - // we copy them into the agency database as needed - // GTFS service ID -> ServiceCalendar - Map calendars = Maps.newHashMap(); - - for (Service svc : input.services.values()) { - - ServiceCalendar cal; - - if (svc.calendar != null) { - // easy case: don't have to infer anything! - cal = new ServiceCalendar(svc.calendar, feed); - } else { - // infer a calendar - // number of mondays, etc. that this calendar is active - int monday, tuesday, wednesday, thursday, friday, saturday, sunday; - monday = tuesday = wednesday = thursday = friday = saturday = sunday = 0; - - LocalDate startDate = null; - LocalDate endDate = null; - - for (CalendarDate cd : svc.calendar_dates.values()) { - if (cd.exception_type == 2) - continue; - - if (startDate == null || cd.date.isBefore(startDate)) - startDate = cd.date; - - if (endDate == null || cd.date.isAfter(endDate)) - endDate = cd.date; - - int dayOfWeek = cd.date.getDayOfWeek().getValue(); - - switch (dayOfWeek) { - case DateTimeConstants.MONDAY: - monday++; - break; - case DateTimeConstants.TUESDAY: - tuesday++; - break; - case DateTimeConstants.WEDNESDAY: - wednesday++; - break; - case DateTimeConstants.THURSDAY: - thursday++; - break; - case DateTimeConstants.FRIDAY: - friday++; - break; - case DateTimeConstants.SATURDAY: - saturday++; - break; - case DateTimeConstants.SUNDAY: - sunday++; - break; - } - } - - // infer the calendar. if there is service on more than half as many as the maximum number of - // a particular day that has service, assume that day has service in general. - int maxService = Ints.max(monday, tuesday, wednesday, thursday, friday, saturday, sunday); - - cal = new ServiceCalendar(); - cal.feedId = feed.id; - - if (startDate == null) { - // no service whatsoever - LOG.warn("Service ID " + svc.service_id + " has no service whatsoever"); - startDate = LocalDate.now().minusMonths(1); - endDate = startDate.plusYears(1); - cal.monday = cal.tuesday = cal.wednesday = cal.thursday = cal.friday = cal.saturday = cal.sunday = false; - } - else { - // infer parameters - - int threshold = (int) Math.round(Math.ceil((double) maxService / 2)); - - cal.monday = monday >= threshold; - cal.tuesday = tuesday >= threshold; - cal.wednesday = wednesday >= threshold; - cal.thursday = thursday >= threshold; - cal.friday = friday >= threshold; - cal.saturday = saturday >= threshold; - cal.sunday = sunday >= threshold; - - cal.startDate = startDate; - cal.endDate = endDate; - } - - cal.inferName(); - cal.gtfsServiceId = svc.service_id; - } - -// feedTx.calendars.put(cal.gtfsServiceId, cal); - calendars.put(svc.service_id, cal); - - serviceCalendarCount++; - } - - LOG.info("Service calendars loaded: " + serviceCalendarCount); - synchronized (status) { - status.message = "Service calendars loaded: " + serviceCalendarCount; - status.percentComplete = 45; - } - LOG.info("GtfsImporter: importing trips..."); - synchronized (status) { - status.message = "Importing trips..."; - status.percentComplete = 50; - } - // import trips, stop times and patterns all at once - Map patterns = input.patterns; - - for (Entry pattern : patterns.entrySet()) { - // it is possible, though unlikely, for two routes to have the same stopping pattern - // we want to ensure they get different trip patterns - Map tripPatternsByRoute = Maps.newHashMap(); - for (String tripId : pattern.getValue().associatedTrips) { - synchronized (status) { - status.message = "Importing trips... (id: " + tripId + ") " + tripCount + "/" + input.trips.size(); - status.percentComplete = 50 + 45 * tripCount / input.trips.size(); - } - com.conveyal.gtfs.model.Trip gtfsTrip = input.trips.get(tripId); - - if (!tripPatternsByRoute.containsKey(gtfsTrip.route_id)) { - TripPattern pat = createTripPatternFromTrip(gtfsTrip, feedTx); - feedTx.tripPatterns.put(pat.id, pat); - tripPatternsByRoute.put(gtfsTrip.route_id, pat); - } - - // there is more than one pattern per route, but this map is specific to only this pattern - // generally it will contain exactly one entry, unless there are two routes with identical - // stopping patterns. - // (in DC, suppose there were trips on both the E2/weekday and E3/weekend from Friendship Heights - // that short-turned at Missouri and 3rd). - TripPattern pat = tripPatternsByRoute.get(gtfsTrip.route_id); - - ServiceCalendar cal = calendars.get(gtfsTrip.service_id); - - // if the service calendar has not yet been imported, import it - if (feedTx.calendars != null && !feedTx.calendars.containsKey(cal.id)) { - // no need to clone as they are going into completely separate mapdbs - feedTx.calendars.put(cal.id, cal); - } - - Trip trip = new Trip(gtfsTrip, routeIdMap.get(gtfsTrip.route_id), pat, cal); - - Collection stopTimes = - input.stop_times.subMap(new Tuple2(gtfsTrip.trip_id, null), new Tuple2(gtfsTrip.trip_id, Fun.HI)).values(); - - for (com.conveyal.gtfs.model.StopTime st : stopTimes) { - trip.stopTimes.add(new StopTime(st, stopIdMap.get(new Tuple2(st.stop_id, feed.id)).id)); - } - - feedTx.trips.put(trip.id, trip); - - tripCount++; - - if (tripCount % 1000 == 0) { - LOG.info("Loaded {} / {} trips", tripCount, input.trips.size()); - } - } - } - - LOG.info("Trips loaded: " + tripCount); - synchronized (status) { - status.message = "Trips loaded: " + tripCount; - status.percentComplete = 90; - } - - LOG.info("GtfsImporter: importing fares..."); - Map fares = input.fares; - for (com.conveyal.gtfs.model.Fare f : fares.values()) { - com.conveyal.datatools.editor.models.transit.Fare fare = new com.conveyal.datatools.editor.models.transit.Fare(f.fare_attribute, f.fare_rules, feed); - feedTx.fares.put(fare.id, fare); - fareCount++; - } - LOG.info("Fares loaded: " + fareCount); - synchronized (status) { - status.message = "Fares loaded: " + fareCount; - status.percentComplete = 95; - } - // commit the feed TXs first, so that we have orphaned data rather than inconsistent data on a commit failure - feedTx.commit(); - gtx.commit(); - - // create an initial snapshot for this FeedVersion - Snapshot snapshot = VersionedDataStore.takeSnapshot(feed.id, "Snapshot of " + feedVersion.getName(), "none"); - - - LOG.info("Imported GTFS file: " + agencyCount + " agencies; " + routeCount + " routes;" + stopCount + " stops; " + stopTimeCount + " stopTimes; " + tripCount + " trips;" + shapePointCount + " shapePoints"); - synchronized (status) { - status.message = "Import complete!"; - status.percentComplete = 100; - } - } - catch (Exception e) { - e.printStackTrace(); - synchronized (status) { - status.message = "Failed to process GTFS snapshot."; - status.error = true; - } - halt(404, "Failed to process GTFS snapshot."); - } - finally { - feedTx.rollbackIfOpen(); - gtx.rollbackIfOpen(); - - // set job as complete - jobFinished(); - } - } - - /** infer the ownership of stops based on what stops there - * Returns a set of tuples stop ID, agency ID with GTFS IDs */ - private SortedSet> inferAgencyStopOwnership() { - // agency - SortedSet> ret = Sets.newTreeSet(); - - for (com.conveyal.gtfs.model.StopTime st : input.stop_times.values()) { - String stopId = st.stop_id; - com.conveyal.gtfs.model.Trip trip = input.trips.get(st.trip_id); - if (trip != null) { - String routeId = trip.route_id; - String agencyId = input.routes.get(routeId).agency_id; - Tuple2 key = new Tuple2(stopId, agencyId); - ret.add(key); - } - } - - return ret; - } - - /** - * Create a trip pattern from the given trip. - * Neither the trippattern nor the trippatternstops are saved. - */ - public TripPattern createTripPatternFromTrip (com.conveyal.gtfs.model.Trip gtfsTrip, FeedTx tx) { - TripPattern patt = new TripPattern(); - com.conveyal.gtfs.model.Route gtfsRoute = input.routes.get(gtfsTrip.route_id); - patt.routeId = routeIdMap.get(gtfsTrip.route_id).id; - patt.feedId = feed.id; - - String patternId = input.tripPatternMap.get(gtfsTrip.trip_id); - Pattern gtfsPattern = input.patterns.get(patternId); - patt.shape = gtfsPattern.geometry; - patt.id = gtfsPattern.pattern_id; - - patt.patternStops = new ArrayList(); - - com.conveyal.gtfs.model.StopTime[] stopTimes = - input.stop_times.subMap(new Tuple2(gtfsTrip.trip_id, 0), new Tuple2(gtfsTrip.trip_id, Fun.HI)).values().toArray(new com.conveyal.gtfs.model.StopTime[0]); - - if (gtfsTrip.trip_headsign != null && !gtfsTrip.trip_headsign.isEmpty()) - patt.name = gtfsTrip.trip_headsign; - else - patt.name = gtfsPattern.name; -// else if (gtfsRoute.route_long_name != null) -// patt.name = String.format("{} to {} ({} stops)", gtfsRoute.route_long_name, input.stops.get(stopTimes[stopTimes.length - 1].stop_id).stop_name, stopTimes.length); // Messages.get("gtfs.named-route-pattern", gtfsTrip.route.route_long_name, input.stops.get(stopTimes[stopTimes.length - 1].stop_id).stop_name, stopTimes.length); -// else -// patt.name = String.format("to {} ({{} stops)", input.stops.get(stopTimes[stopTimes.length - 1].stop_id).stop_name, stopTimes.length); // Messages.get("gtfs.unnamed-route-pattern", input.stops.get(stopTimes[stopTimes.length - 1].stop_id).stop_name, stopTimes.length); - - for (com.conveyal.gtfs.model.StopTime st : stopTimes) { - TripPatternStop tps = new TripPatternStop(); - - Stop stop = stopIdMap.get(new Tuple2(st.stop_id, patt.feedId)); - tps.stopId = stop.id; - - if (st.timepoint != Entity.INT_MISSING) - tps.timepoint = st.timepoint == 1; - - if (st.departure_time != Entity.INT_MISSING && st.arrival_time != Entity.INT_MISSING) - tps.defaultDwellTime = st.departure_time - st.arrival_time; - else - tps.defaultDwellTime = 0; - - patt.patternStops.add(tps); - } - - patt.calcShapeDistTraveled(tx); - - // infer travel times - if (stopTimes.length >= 2) { - int startOfBlock = 0; - // start at one because the first stop has no travel time - // but don't put nulls in the data - patt.patternStops.get(0).defaultTravelTime = 0; - for (int i = 1; i < stopTimes.length; i++) { - com.conveyal.gtfs.model.StopTime current = stopTimes[i]; - - if (current.arrival_time != Entity.INT_MISSING) { - // interpolate times - - int timeSinceLastSpecifiedTime = current.arrival_time - stopTimes[startOfBlock].departure_time; - - double blockLength = patt.patternStops.get(i).shapeDistTraveled - patt.patternStops.get(startOfBlock).shapeDistTraveled; - - // go back over all of the interpolated stop times and interpolate them - for (int j = startOfBlock + 1; j <= i; j++) { - TripPatternStop tps = patt.patternStops.get(j); - double distFromLastStop = patt.patternStops.get(j).shapeDistTraveled - patt.patternStops.get(j - 1).shapeDistTraveled; - tps.defaultTravelTime = (int) Math.round(timeSinceLastSpecifiedTime * distFromLastStop / blockLength); - } - - startOfBlock = i; - } - } - } - - return patt; - } - - @Override - public Status getStatus() { - synchronized (status) { - return status.clone(); - } - } - - @Override - public void handleStatusEvent(Map statusMap) { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } -} - diff --git a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotUpload.java b/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotUpload.java deleted file mode 100755 index c30be3030..000000000 --- a/src/main/java/com/conveyal/datatools/editor/jobs/ProcessGtfsSnapshotUpload.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.conveyal.datatools.editor.jobs; - -//import play.jobs.Job; - -public class ProcessGtfsSnapshotUpload implements Runnable { - @Override - public void run() { - - } - /* - private Long _gtfsSnapshotMergeId; - - private Map agencyIdMap = new HashMap(); - - public ProcessGtfsSnapshotUpload(Long gtfsSnapshotMergeId) { - this._gtfsSnapshotMergeId = gtfsSnapshotMergeId; - } - - public void doJob() { - - GtfsSnapshotMerge snapshotMerge = null; - while(snapshotMerge == null) - { - snapshotMerge = GtfsSnapshotMerge.findById(this._gtfsSnapshotMergeId); - LOG.warn("Waiting for snapshotMerge to save..."); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - GtfsReader reader = new GtfsReader(); - GtfsDaoImpl store = new GtfsDaoImpl(); - - Long agencyCount = new Long(0); - - try { - - File gtfsFile = new File(Play.configuration.getProperty("application.publicGtfsDataDirectory"), snapshotMerge.snapshot.getFilename()); - - reader.setInputLocation(gtfsFile); - reader.setEntityStore(store); - reader.run(); - - LOG.info("GtfsImporter: listing feeds..."); - - for (org.onebusaway.gtfs.model.Agency gtfsAgency : reader.getAgencies()) { - - GtfsAgency agency = new GtfsAgency(gtfsAgency); - agency.snapshot = snapshotMerge.snapshot; - agency.save(); - - } - - snapshotMerge.snapshot.agencyCount = store.getAllAgencies().size(); - snapshotMerge.snapshot.routeCount = store.getAllRoutes().size(); - snapshotMerge.snapshot.stopCount = store.getAllStops().size(); - snapshotMerge.snapshot.tripCount = store.getAllTrips().size(); - - snapshotMerge.snapshot.save(); - - } - catch (Exception e) { - - LOG.error(e.toString()); - - snapshotMerge.failed(e.toString()); - } - }*/ -} - diff --git a/src/main/java/com/conveyal/datatools/editor/models/Model.java b/src/main/java/com/conveyal/datatools/editor/models/Model.java deleted file mode 100644 index dec9c0d1a..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/Model.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.conveyal.datatools.editor.models; - -import javax.persistence.MappedSuperclass; -import java.io.Serializable; -import java.util.UUID; - -@MappedSuperclass -public class Model implements Cloneable, Serializable { - private static final long serialVersionUID = -2490147875977011741L; - - /** not final for purposes of deserialization, but don't change it once data has been persisted */ - public String id; - - /** Set the ID, unless it is null, in which case keep the generated ID */ - public void setId (String id) { - if (id != null) - this.id = id; - } - - /** Create a new model with an autogenerated ID */ - public Model () { - generateId(); - } - - /** - * Create a new model with the specified ID. - */ - public Model (String id) { - this.id = id; - } - - public void generateId () { - id = UUID.randomUUID().toString(); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/Snapshot.java b/src/main/java/com/conveyal/datatools/editor/models/Snapshot.java deleted file mode 100644 index 28db2c4ef..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/Snapshot.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.conveyal.datatools.editor.models; - -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import java.io.IOException; -import java.time.LocalDate; - -import org.mapdb.Fun; -import org.mapdb.Fun.Tuple2; -import com.conveyal.datatools.editor.utils.JacksonSerializers; - -import java.io.Serializable; -import java.util.Collection; - -/** - * Represents a snapshot of an agency database. - * @author mattwigway - * - */ -public class Snapshot implements Cloneable, Serializable { - public static final long serialVersionUID = -2450165077572197392L; - - /** Is this snapshot the current snapshot - the most recently created or restored (i.e. the most current view of what's in master */ - public boolean current; - - /** The version of this snapshot */ - public int version; - - /** The name of this snapshot */ - public String name; - - /** The comment of this snapshot */ - public String comment; - - /** ID: agency ID, version */ - @JsonSerialize(using=JacksonSerializers.Tuple2IntSerializer.class) - @JsonDeserialize(using=JacksonSerializers.Tuple2IntDeserializer.class) - public Tuple2 id; - - /** The feed associated with this */ - public String feedId; - - /** the date/time this snapshot was taken (millis since epoch) */ - public long snapshotTime; - - // TODO: these should become java.time.LocalDate - /** When is the earliest date that schedule information contained in this snapshot is valid? */ - @JsonSerialize(using = JacksonSerializers.LocalDateIsoSerializer.class) - @JsonDeserialize(using = JacksonSerializers.LocalDateIsoDeserializer.class) - public LocalDate validFrom; - - /** When is the last date that schedule information contained in this snapshot is valid? */ - @JsonSerialize(using = JacksonSerializers.LocalDateIsoSerializer.class) - @JsonDeserialize(using = JacksonSerializers.LocalDateIsoDeserializer.class) - public LocalDate validTo; - - /** Used for Jackson deserialization */ - public Snapshot () {} - - public Snapshot (String feedId, int version) { - this.feedId = feedId; - this.version = version; - this.computeId(); - } - - /** create an ID for this snapshot based on agency ID and version */ - public void computeId () { - this.id = new Tuple2(feedId, version); - } - - public Snapshot clone () { - try { - return (Snapshot) super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException(e); - } - } - - @JsonIgnore - public static Collection getSnapshots(String feedId) { - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - return gtx.snapshots.subMap(new Tuple2(feedId, null), new Tuple2(feedId, Fun.HI)).values(); - } - - public static Snapshot get(String snapshotId) { - Tuple2 decodedId; - try { - decodedId = JacksonSerializers.Tuple2IntDeserializer.deserialize(snapshotId); - } catch (IOException e) { - return null; - } - - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - - if (!gtx.snapshots.containsKey(decodedId)) return null; - - return gtx.snapshots.get(decodedId); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/Agency.java b/src/main/java/com/conveyal/datatools/editor/models/transit/Agency.java deleted file mode 100755 index 0b94153cf..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/Agency.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.conveyal.datatools.editor.models.Model; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; -import java.net.MalformedURLException; -import java.net.URL; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Agency extends Model implements Cloneable, Serializable, Comparable { - public static final long serialVersionUID = 1; - public static final Logger LOG = LoggerFactory.getLogger(Agency.class); - - public String agencyId; - public String name; - public String url; - public String timezone; - public String lang; - public String phone; - public String email; - public String feedId; - public String agencyBrandingUrl; - public String agencyFareUrl; - - public Agency(com.conveyal.gtfs.model.Agency agency, EditorFeed feed) { - this.agencyId = agency.agency_id; - this.name = agency.agency_name; - this.url = agency.agency_url != null ? agency.agency_url.toString() : null; - this.timezone = agency.agency_timezone; - this.lang = agency.agency_lang; - this.phone = agency.agency_phone; - this.feedId = feed.id; - } - - public Agency () {} - - public com.conveyal.gtfs.model.Agency toGtfs() { - com.conveyal.gtfs.model.Agency ret = new com.conveyal.gtfs.model.Agency(); - - ret.agency_id = agencyId; - ret.agency_name = name; - try { - ret.agency_url = new URL(url); - } catch (MalformedURLException e) { - LOG.warn("Unable to coerce agency URL {} to URL", url); - ret.agency_url = null; - } - try { - ret.agency_branding_url = new URL(agencyBrandingUrl); - } catch (MalformedURLException e) { - LOG.warn("Unable to coerce agency branding URL {} to URL", agencyBrandingUrl); - ret.agency_branding_url = null; - } - try { - ret.agency_fare_url = new URL(agencyFareUrl); - } catch (MalformedURLException e) { - LOG.warn("Unable to coerce agency fare URL {} to URL", agencyFareUrl); - ret.agency_fare_url = null; - } - ret.agency_timezone = timezone; - ret.agency_lang = lang; - ret.agency_phone = phone; -// ret.agency_email = email; - - return ret; - } - - public int compareTo (Object other) { - if (!(other instanceof Agency)) - return -1; - - Agency o = (Agency) other; - - if (this.name == null) - return -1; - - if (o.name == null) - return 1; - - return this.name.compareTo(o.name); - } - - public Agency clone () throws CloneNotSupportedException { - return (Agency) super.clone(); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/AttributeAvailabilityType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/AttributeAvailabilityType.java deleted file mode 100755 index 792a6228d..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/AttributeAvailabilityType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -public enum AttributeAvailabilityType { - UNKNOWN, - AVAILABLE, - UNAVAILABLE -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/EditorFeed.java b/src/main/java/com/conveyal/datatools/editor/models/transit/EditorFeed.java deleted file mode 100644 index 58fa23ba5..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/EditorFeed.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.Model; -import com.conveyal.datatools.manager.models.JsonViews; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; - -import java.io.Serializable; -import java.net.URL; -import java.time.LocalDate; - -/** - * Created by demory on 6/8/16. - */ -public class EditorFeed extends Model implements Cloneable, Serializable { - - // GTFS Editor defaults - public String color; - public Double defaultLat; - public Double defaultLon; - public GtfsRouteType defaultRouteType; - - // feed-info.txt fields - public String feedPublisherName; - public URL feedPublisherUrl; - public String feedLang; - public String feedVersion; - public LocalDate feedStartDate; - public LocalDate feedEndDate; - -// @JsonProperty -// public Integer getRouteCount() { -// FeedTx tx = VersionedDataStore.getFeedTx(id); -// return tx.routes.size(); -// } -// -// @JsonProperty -// public Integer getStopCount() { -// FeedTx tx = VersionedDataStore.getFeedTx(id); -// return tx.stops.size(); -// } -// @JsonInclude(JsonInclude.Include.NON_NULL) -// @JsonView(JsonViews.UserInterface.class) -// public boolean getEditedSinceSnapshot() { -// FeedTx tx = VersionedDataStore.getFeedTx(id); -//// return tx.editedSinceSnapshot.get(); -// return false; -// } - // the associated FeedSource in the data manager DB - //public String feedSourceId; - - - public EditorFeed() {} - - public EditorFeed clone () throws CloneNotSupportedException { - return (EditorFeed) super.clone(); - } - -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/Fare.java b/src/main/java/com/conveyal/datatools/editor/models/transit/Fare.java deleted file mode 100644 index 6b768138e..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/Fare.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.conveyal.datatools.editor.models.Model; -import com.conveyal.gtfs.model.FareRule; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.common.collect.Lists; - -import java.io.Serializable; -import java.util.List; - -/** - * Created by landon on 6/22/16. - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Fare extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - - public String feedId; - public String gtfsFareId; - public String description; - public Double price; - public String currencyType; - public Integer paymentMethod; - public Integer transfers; - public Integer transferDuration; - public List fareRules = Lists.newArrayList(); - - public Fare() {}; - - public Fare(com.conveyal.gtfs.model.FareAttribute fare, List rules, EditorFeed feed) { - this.gtfsFareId = fare.fare_id; - this.price = fare.price; - this.currencyType = fare.currency_type; - this.paymentMethod = fare.payment_method; - this.transfers = fare.transfers; - this.transferDuration = fare.transfer_duration; - this.fareRules.addAll(rules); - this.feedId = feed.id; - inferName(); - } - - /** - * Infer the name of this calendar - */ - public void inferName () { - StringBuilder sb = new StringBuilder(14); - - if (price != null) - sb.append(price); - if (currencyType != null) - sb.append(currencyType); - - this.description = sb.toString(); - - if (this.description.equals("") && this.gtfsFareId != null) - this.description = gtfsFareId; - } - - public Fare clone () throws CloneNotSupportedException { - Fare f = (Fare) super.clone(); - f.fareRules.addAll(fareRules); - return f; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/GtfsRouteType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/GtfsRouteType.java deleted file mode 100755 index b64c8aaa6..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/GtfsRouteType.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.conveyal.gtfs.model.Entity; - -public enum GtfsRouteType { - TRAM, - SUBWAY, - RAIL, - BUS, - FERRY, - CABLECAR, - GONDOLA, - FUNICULAR; - - public int toGtfs() { - switch(this) - { - case TRAM: - return 0; - case SUBWAY: - return 1; - case RAIL: - return 2; - case BUS: - return 3; - case FERRY: - return 4; - case CABLECAR: - return 5; - case GONDOLA: - return 6; - case FUNICULAR: - return 7; - default: - // can't happen - return Entity.INT_MISSING; - - } - } - - public static GtfsRouteType fromGtfs (int gtfsType) { - switch (gtfsType) - { - case 0: - return TRAM; - case 1: - return SUBWAY; - case 2: - return RAIL; - case 3: - return BUS; - case 4: - return FERRY; - case 5: - return CABLECAR; - case 6: - return GONDOLA; - case 7: - return FUNICULAR; - default: - return null; - } - } - - public HvtRouteType toHvt () { - switch (this) { - case TRAM: - return HvtRouteType.TRAM; - case SUBWAY: - return HvtRouteType.URBANRAIL_METRO; - case RAIL: - return HvtRouteType.RAIL; - case BUS: - // TODO overly specific - return HvtRouteType.BUS_LOCAL; - case FERRY: - return HvtRouteType.WATER; - case CABLECAR: - return HvtRouteType.MISCELLANEOUS_CABLE_CAR; - case GONDOLA: - return HvtRouteType.MISCELLANEOUS; - case FUNICULAR: - return HvtRouteType.FUNICULAR; - default: - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/HvtRouteType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/HvtRouteType.java deleted file mode 100755 index 68a3527a7..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/HvtRouteType.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -public enum HvtRouteType { - - // using the TPEG/HVT "standard" as documented in the 3/20/08 Google Group message from Joe Hughes. Oddly, this seems to be the document of record for this change! - // https://groups.google.com/forum/?fromgroups=#!msg/gtfs-changes/keT5rTPS7Y0/71uMz2l6ke0J - - RAIL, // 100 Railway Service - RAIL_HS, // 101 High Speed Rail Service - RAIL_LD, // 102 Long Distance Trains - RAIL_SHUTTLE, // 108 Rail Shuttle (within complex) - RAIL_SUBURBAN, // 109 Suburban Railway - - COACH, // 200 Coach Service - COACH_INTERNATIONAL, // 201 International Coach Service - COACH_NATIONAL, // 202 National Coach Service - COACH_REGIONAL, // 204 Regional Coach Service - COACH_COMMUTER, // 208 Commuter Coach Service - - URBANRAIL, // 400 Urban Railway Service - URBANRAIL_METRO, // 401 Metro Service - URBANRAIL_UNDERGROUND, // 402 Underground Service - URBANRAIL_MONORAIL, // 405 Monorail - - BUS, // 700 Bus Service - BUS_REGIONAL, // 701 Regional Bus Service - BUS_EXPRESS, // 702 Express Bus Service - BUS_LOCAL, // 704 Local Bus Service - BUS_UNSCHEDULED, // 70X Unscheduled Bus Service (used for "informal" services like jeepneys, collectivos, etc.) - // need to formally assign HVT id to this type -- unclear how to do this given there's no registry. - - TROLLEYBUS, // 800 Trolleybus Service - - TRAM, // 900 Tram Service - - WATER, // 1000 Water Transport Service - - AIR, // 1100 Air Service - - TELECABIN, // 1300 Telecabin Service - FUNICULAR, // 1400 Funicular Service - - MISCELLANEOUS, // 1700 Miscellaneous Service - MISCELLANEOUS_CABLE_CAR, //1701 Cable Car - MISCELLANEOUS_HORSE_CARRIAGE, // 1702 Horse-Drawn Carriage -} - - - - - - - - - - - - - - - - - - - diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/LocationType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/LocationType.java deleted file mode 100755 index 89c915036..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/LocationType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -public enum LocationType { - STOP, - STATION -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/Route.java b/src/main/java/com/conveyal/datatools/editor/models/transit/Route.java deleted file mode 100755 index a26d55bd0..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/Route.java +++ /dev/null @@ -1,205 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.models.Model; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; -import java.net.MalformedURLException; -import java.net.URL; - -public class Route extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - public static final Logger LOG = LoggerFactory.getLogger(Route.class); - public String gtfsRouteId; - public String routeShortName; - public String routeLongName; - - public String routeDesc; - - public String routeTypeId; - public GtfsRouteType gtfsRouteType; - public String routeUrl; - public String routeColor; - public String routeTextColor; - public String routeBrandingUrl; - - // Custom Fields - public String comments; - - public StatusType status; - - public Boolean publiclyVisible; - - public String agencyId; - public String feedId; - - //public GisRoute gisRoute; - - //public GisUpload gisUpload; - - public AttributeAvailabilityType wheelchairBoarding; - - /** on which days does this route have service? Derived from calendars on render */ - public transient Boolean monday, tuesday, wednesday, thursday, friday, saturday, sunday; - - // add getters so Jackson will serialize - - @JsonProperty("monday") - public Boolean jsonGetMonday() { - return monday; - } - - @JsonProperty("tuesday") - public Boolean jsonGetTuesday() { - return tuesday; - } - - @JsonProperty("wednesday") - public Boolean jsonGetWednesday() { - return wednesday; - } - - @JsonProperty("thursday") - public Boolean jsonGetThursday() { - return thursday; - } - - @JsonProperty("friday") - public Boolean jsonGetFriday() { - return friday; - } - - @JsonProperty("saturday") - public Boolean jsonGetSaturday() { - return saturday; - } - - @JsonProperty("sunday") - public Boolean jsonGetSunday() { - return sunday; - } - - public Route () {} - - public Route(com.conveyal.gtfs.model.Route route, EditorFeed feed, Agency agency) { - this.gtfsRouteId = route.route_id; - this.routeShortName = route.route_short_name; - this.routeLongName = route.route_long_name; - this.routeDesc = route.route_desc; - - this.gtfsRouteType = GtfsRouteType.fromGtfs(route.route_type); - - this.routeUrl = route.route_url != null ? route.route_url.toString() : null; - this.routeColor = route.route_color; - this.routeTextColor = route.route_text_color; - - this.feedId = feed.id; - this.agencyId = agency.id; - } - - - public Route(String routeShortName, String routeLongName, int routeType, String routeDescription, EditorFeed feed, Agency agency) { - this.routeShortName = routeShortName; - this.routeLongName = routeLongName; - this.gtfsRouteType = GtfsRouteType.fromGtfs(routeType); - this.routeDesc = routeDescription; - - this.feedId = feed.id; - this.agencyId = agency.id; - } - - public com.conveyal.gtfs.model.Route toGtfs(com.conveyal.gtfs.model.Agency a, GlobalTx tx) { - com.conveyal.gtfs.model.Route ret = new com.conveyal.gtfs.model.Route(); - ret.agency_id = a.agency_id; - ret.route_color = routeColor; - ret.route_desc = routeDesc; - ret.route_id = getGtfsId(); - ret.route_long_name = routeLongName; - ret.route_short_name = routeShortName; - ret.route_text_color = routeTextColor; - // TODO also handle HVT types here - //ret.route_type = mapGtfsRouteType(routeTypeId); - try { - ret.route_url = new URL(routeUrl); - } catch (MalformedURLException e) { - LOG.warn("Cannot coerce route URL {} to URL", routeUrl); - ret.route_url = null; - } - try { - ret.route_branding_url = new URL(routeBrandingUrl); - } catch (MalformedURLException e) { - LOG.warn("Unable to coerce route branding URL {} to URL", routeBrandingUrl); - ret.route_branding_url = null; - } - return ret; - } - - @JsonIgnore - public String getGtfsId() { - if(gtfsRouteId != null && !gtfsRouteId.isEmpty()) - return gtfsRouteId; - else - return id; - } - - - /** - * Get a name for this combining the short name and long name as available. - * @return - */ - @JsonIgnore - public String getName() { - if (routeShortName == null && routeLongName == null) - return id; - else if (routeShortName == null) - return routeLongName; - else if (routeLongName == null) - return routeShortName; - else - return routeShortName + " " + routeLongName; - - } - - // Add information about the days of week this route is active - public void addDerivedInfo(FeedTx tx) { - monday = tuesday = wednesday = thursday = friday = saturday = sunday = false; - - for (Trip trip : tx.getTripsByRoute(this.id)) { - ServiceCalendar cal = tx.calendars.get(trip.calendarId); - - if (cal.monday) - monday = true; - - if (cal.tuesday) - tuesday = true; - - if (cal.wednesday) - wednesday = true; - - if (cal.thursday) - thursday = true; - - if (cal.friday) - friday = true; - - if (cal.saturday) - saturday = true; - - if (cal.sunday) - sunday = true; - - if (monday && tuesday && wednesday && thursday && friday && saturday && sunday) - // optimization: no point in continuing - break; - } - } - - public Route clone () throws CloneNotSupportedException { - return (Route) super.clone(); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/RouteType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/RouteType.java deleted file mode 100755 index 974755417..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/RouteType.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - - -import com.conveyal.datatools.editor.models.Model; - -import java.io.Serializable; - -// TODO: destroy route type and replace with ENUM -public class RouteType extends Model implements Serializable { - public static final long serialVersionUID = 1; - - public String localizedVehicleType; - public String description; - - public GtfsRouteType gtfsRouteType; - - public HvtRouteType hvtRouteType; - - /* - @JsonCreator - public static RouteType factory(long id) { - return RouteType.findById(id); - } - - @JsonCreator - public static RouteType factory(String id) { - return RouteType.findById(Long.parseLong(id)); - } - */ - - -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/ScheduleException.java b/src/main/java/com/conveyal/datatools/editor/models/transit/ScheduleException.java deleted file mode 100644 index 4101a78d1..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/ScheduleException.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.conveyal.datatools.editor.models.Model; -import java.time.LocalDate; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -/** - * Represents an exception to the schedule, which could be "On January 18th, run a Sunday schedule" - * (useful for holidays), or could be "on June 23rd, run the following services" (useful for things - * like early subway shutdowns, re-routes, etc.) - * - * Unlike the GTFS schedule exception model, we assume that these special calendars are all-or-nothing; - * everything that isn't explicitly running is not running. That is, creating special service means the - * user starts with a blank slate. - * - * @author mattwigway - */ - -public class ScheduleException extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - - /** The agency whose service this schedule exception describes */ - public String feedId; - - /** - * If non-null, run service that would ordinarily run on this day of the week. - * Takes precedence over any custom schedule. - */ - public ExemplarServiceDescriptor exemplar; - - /** The name of this exception, for instance "Presidents' Day" or "Early Subway Shutdowns" */ - public String name; - - /** The dates of this service exception */ - public List dates; - - /** A custom schedule. Only used if like == null */ - public List customSchedule; - - public List addedService; - - public List removedService; - - public boolean serviceRunsOn(ServiceCalendar service) { - switch (exemplar) { - case MONDAY: - return service.monday; - case TUESDAY: - return service.tuesday; - case WEDNESDAY: - return service.wednesday; - case THURSDAY: - return service.thursday; - case FRIDAY: - return service.friday; - case SATURDAY: - return service.saturday; - case SUNDAY: - return service.sunday; - case NO_SERVICE: - // special case for quickly turning off all service. - return false; - case CUSTOM: - return customSchedule.contains(service.id); - case SWAP: - // new case to either swap one service id for another or add/remove a specific service - if (addedService != null && addedService.contains(service.id)) { - return true; - } - else if (removedService != null && removedService.contains(service.id)) { - return false; - } - default: - // can't actually happen, but java requires a default with a return here - return false; - } - } - - /** - * Represents a desire about what service should be like on a particular day. - * For example, run Sunday service on Presidents' Day, or no service on New Year's Day. - */ - public static enum ExemplarServiceDescriptor { - MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY, NO_SERVICE, CUSTOM, SWAP; - } - - public ScheduleException clone () throws CloneNotSupportedException { - ScheduleException c = (ScheduleException) super.clone(); - c.dates = new ArrayList(this.dates); - c.customSchedule = new ArrayList(this.customSchedule); - return c; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/ServiceCalendar.java b/src/main/java/com/conveyal/datatools/editor/models/transit/ServiceCalendar.java deleted file mode 100755 index 1f5ea1283..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/ServiceCalendar.java +++ /dev/null @@ -1,226 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - - -import com.beust.jcommander.internal.Sets; -import com.conveyal.gtfs.model.Calendar; -import com.conveyal.gtfs.model.Service; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.models.Model; -import java.time.LocalDate; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Set; - -public class ServiceCalendar extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - - public String feedId; - public String gtfsServiceId; - public String description; - public Boolean monday; - public Boolean tuesday; - public Boolean wednesday; - public Boolean thursday; - public Boolean friday; - public Boolean saturday; - public Boolean sunday; - public LocalDate startDate; - public LocalDate endDate; - - public ServiceCalendar() {}; - - public ServiceCalendar(Calendar calendar, EditorFeed feed) { - this.gtfsServiceId = calendar.service_id; - this.monday = calendar.monday == 1; - this.tuesday = calendar.tuesday == 1; - this.wednesday = calendar.wednesday == 1; - this.thursday = calendar.thursday == 1; - this.friday = calendar.friday == 1; - this.saturday = calendar.saturday == 1; - this.sunday = calendar.sunday == 1; - this.startDate = fromGtfs(calendar.start_date); - this.endDate = fromGtfs(calendar.end_date); - inferName(); - this.feedId = feed.id; - } - - public ServiceCalendar clone () throws CloneNotSupportedException { - return (ServiceCalendar) super.clone(); - } - - // TODO: time zones - private static LocalDate fromGtfs(int date) { - int day = date % 100; - date -= day; - int month = (date % 10000) / 100; - date -= month * 100; - int year = date / 10000; - - return LocalDate.of(year, month, day); - } - - // give the UI a little information about the content of this calendar - public transient Long numberOfTrips; - - @JsonProperty("numberOfTrips") - public Long jsonGetNumberOfTrips () { - return numberOfTrips; - } - - public transient Collection routes; - - @JsonProperty("routes") - public Collection jsonGetRoutes () { - return routes; - } - - // do-nothing setters - @JsonProperty("numberOfTrips") - public void jsonSetNumberOfTrips(Long numberOfTrips) { } - - @JsonProperty("routes") - public void jsonSetRoutes(Collection routes) { } - - /** - * Infer the name of this calendar - */ - public void inferName () { - StringBuilder sb = new StringBuilder(14); - - if (monday) - sb.append("Mo"); - - if (tuesday) - sb.append("Tu"); - - if (wednesday) - sb.append("We"); - - if (thursday) - sb.append("Th"); - - if (friday) - sb.append("Fr"); - - if (saturday) - sb.append("Sa"); - - if (sunday) - sb.append("Su"); - - this.description = sb.toString(); - - if (this.description.equals("") && this.gtfsServiceId != null) - this.description = gtfsServiceId; - } - - public String toString() { - - String str = ""; - - if(this.monday) - str += "Mo"; - - if(this.tuesday) - str += "Tu"; - - if(this.wednesday) - str += "We"; - - if(this.thursday) - str += "Th"; - - if(this.friday) - str += "Fr"; - - if(this.saturday) - str += "Sa"; - - if(this.sunday) - str += "Su"; - - return str; - } - - /** - * Convert this service to a GTFS service calendar. - * @param startDate int, in GTFS format: YYYYMMDD - * @param endDate int, again in GTFS format - */ - public Service toGtfs(int startDate, int endDate) { - Service ret = new Service(id); - ret.calendar = new Calendar(); - ret.calendar.service_id = ret.service_id; - ret.calendar.start_date = startDate; - ret.calendar.end_date = endDate; - ret.calendar.sunday = sunday ? 1 : 0; - ret.calendar.monday = monday ? 1 : 0; - ret.calendar.tuesday = tuesday ? 1 : 0; - ret.calendar.wednesday = wednesday ? 1 : 0; - ret.calendar.thursday = thursday ? 1 : 0; - ret.calendar.friday = friday ? 1 : 0; - ret.calendar.saturday = saturday ? 1 : 0; - - // TODO: calendar dates - return ret; - } - - // equals and hashcode use DB ID; they are used to put service calendar dates into a HashMultimap in ProcessGtfsSnapshotExport - public int hashCode () { - return id.hashCode(); - } - - public boolean equals(Object o) { - if (o instanceof ServiceCalendar) { - ServiceCalendar c = (ServiceCalendar) o; - - return id.equals(c.id); - } - - return false; - } - - /** - * Used to represent a service calendar and its service on a particular route. - */ - public static class ServiceCalendarForPattern { - public String description; - public String id; - public long routeTrips; - - public ServiceCalendarForPattern(ServiceCalendar cal, TripPattern patt, long routeTrips ) { - this.description = cal.description; - this.id = cal.id; - this.routeTrips = routeTrips; - } - } - - /** add transient info for UI with number of routes, number of trips */ - public void addDerivedInfo(final FeedTx tx) { - this.numberOfTrips = tx.tripCountByCalendar.get(this.id); - - if (this.numberOfTrips == null) - this.numberOfTrips = 0L; - - // note that this is not ideal as we are fetching all of the trips. however, it's not really very possible - // with MapDB to have an index involving three tables. - Set routeIds = Sets.newHashSet(); - - for (Trip trip : tx.getTripsByCalendar(this.id)) { - routeIds.add(trip.routeId); - } - -// this.routes = Collections2.transform(routeIds, new Function() { -// -// @Override -// public String apply(String routeId) { -// return tx.routes.get(routeId).getName(); -// } -// -// }); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/StatusType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/StatusType.java deleted file mode 100755 index 013365910..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/StatusType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -public enum StatusType { - IN_PROGRESS, - PENDING_APPROVAL, - APPROVED, - DISABLED -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/Stop.java b/src/main/java/com/conveyal/datatools/editor/models/transit/Stop.java deleted file mode 100755 index 88a436bee..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/Stop.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.Point; -import com.vividsolutions.jts.geom.PrecisionModel; -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.models.Model; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class Stop extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - public static final Logger LOG = LoggerFactory.getLogger(Stop.class); - private static GeometryFactory geometryFactory = new GeometryFactory(); - - public String gtfsStopId; - public String stopCode; - public String stopName; - public String stopDesc; - public String zoneId; - public String stopUrl; - - public String stopIconUrl; - - //public String agencyId; - public String feedId; - - public LocationType locationType; - - public AttributeAvailabilityType bikeParking; - - public AttributeAvailabilityType carParking; - - public AttributeAvailabilityType wheelchairBoarding; - - public StopTimePickupDropOffType pickupType; - - public StopTimePickupDropOffType dropOffType; - - public String parentStation; - - public String stopTimezone; - - // Major stop is a custom field; it has no corrolary in the GTFS. - public Boolean majorStop; - - @JsonIgnore - public Point location; - - public Stop(com.conveyal.gtfs.model.Stop stop, GeometryFactory geometryFactory, EditorFeed feed) { - - this.gtfsStopId = stop.stop_id; - this.stopCode = stop.stop_code; - this.stopName = stop.stop_name; - this.stopDesc = stop.stop_desc; - this.zoneId = stop.zone_id; - this.stopUrl = stop.stop_url != null ? stop.stop_url.toString() : null; - this.locationType = stop.location_type == 1 ? LocationType.STATION : LocationType.STOP; - this.parentStation = stop.parent_station; - this.pickupType = StopTimePickupDropOffType.SCHEDULED; - this.dropOffType = StopTimePickupDropOffType.SCHEDULED; - - this.location = geometryFactory.createPoint(new Coordinate(stop.stop_lon,stop.stop_lat)); - - this.feedId = feed.id; - } - - public Stop(EditorFeed feed, String stopName, String stopCode, String stopUrl, String stopDesc, Double lat, Double lon) { - this.feedId = feed.id; - this.stopCode = stopCode; - this.stopName = stopName; - this.stopDesc = stopDesc; - this.stopUrl = stopUrl; - this.locationType = LocationType.STOP; - this.pickupType = StopTimePickupDropOffType.SCHEDULED; - this.dropOffType = StopTimePickupDropOffType.SCHEDULED; - - GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); - - this.location = geometryFactory.createPoint(new Coordinate(lon, lat)); - } - - /** Create a stop. Note that this does *not* generate an ID, as you have to set the agency first */ - public Stop () {} - - public double getLat () { - return location.getY(); - } - - public double getLon () { - return location.getX(); - } - - @JsonCreator - public static Stop fromJson(@JsonProperty("lat") double lat, @JsonProperty("lon") double lon) { - Stop ret = new Stop(); - ret.location = geometryFactory.createPoint(new Coordinate(lon, lat)); - return ret; - } - - public com.conveyal.gtfs.model.Stop toGtfs() { - com.conveyal.gtfs.model.Stop ret = new com.conveyal.gtfs.model.Stop(); - ret.stop_id = getGtfsId(); - ret.stop_code = stopCode; - ret.stop_desc = stopDesc; - ret.stop_lat = location.getY(); - ret.stop_lon = location.getX(); - - if (stopName != null && !stopName.isEmpty()) - ret.stop_name = stopName; - else - ret.stop_name = id.toString(); - - try { - ret.stop_url = new URL(stopUrl); - } catch (MalformedURLException e) { - LOG.warn("Unable to coerce stop URL {} to URL", stopUrl); - ret.stop_url = null; - } - - return ret; - } - - /** Merge the given stops IDs within the given FeedTx, deleting stops and updating trip patterns and trips */ - public static void merge (List stopIds, FeedTx tx) { - Stop target = tx.stops.get(stopIds.get(0)); - for (int i = 1; i < stopIds.size(); i++) { - Stop source = tx.stops.get(stopIds.get(i)); - - // find all the patterns that stop at this stop - Collection tps = tx.getTripPatternsByStop(source.id); - - List tpToSave = new ArrayList(); - - // update them - for (TripPattern tp : tps) { - try { - tp = tp.clone(); - } catch (CloneNotSupportedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - tx.rollback(); - throw new RuntimeException(e); - } - for (TripPatternStop ps : tp.patternStops) { - if (source.id.equals(ps.stopId)) { - ps.stopId = target.id; - } - } - - // batch them for save at the end, as all of the sets we are working with still refer to the db, - // so changing it midstream is a bad idea - tpToSave.add(tp); - - // update the trips - List tripsToSave = new ArrayList(); - for (Trip trip : tx.getTripsByPattern(tp.id)) { - try { - trip = trip.clone(); - } catch (CloneNotSupportedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - tx.rollback(); - throw new RuntimeException(e); - } - - for (StopTime st : trip.stopTimes) { - if (source.id.equals(st.stopId)) { - // stop times have been cloned, so this is safe - st.stopId = target.id; - } - } - - tripsToSave.add(trip); - } - - for (Trip trip : tripsToSave) { - tx.trips.put(trip.id, trip); - } - } - - for (TripPattern tp : tpToSave) { - tx.tripPatterns.put(tp.id, tp); - } - - if (!tx.getTripPatternsByStop(source.id).isEmpty()) { - throw new IllegalStateException("Tried to move all trip patterns when merging stops but was not successful"); - } - - tx.stops.remove(source.id); - } - } - - @JsonIgnore - public String getGtfsId() { - if(gtfsStopId != null && !gtfsStopId.isEmpty()) - return gtfsStopId; - else - return "STOP_" + id; - } - - public Stop clone () throws CloneNotSupportedException { - Stop s = (Stop) super.clone(); - s.location = (Point) location.clone(); - return s; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/StopTime.java b/src/main/java/com/conveyal/datatools/editor/models/transit/StopTime.java deleted file mode 100755 index bdbee3aaf..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/StopTime.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -import java.io.Serializable; - -/** - * Represents a stop time. This is not a model, as it is stored directly as a list in Trip. - * @author mattwigway - * - */ -public class StopTime implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - - public Integer arrivalTime; - public Integer departureTime; - - public String stopHeadsign; - - /* reference to trip pattern stop is implied based on position, no stop sequence needed */ - - public StopTimePickupDropOffType pickupType; - - public StopTimePickupDropOffType dropOffType; - - public String stopId; - - public StopTime() - { - - } - - public StopTime(com.conveyal.gtfs.model.StopTime stopTime, String stopId) { - - this.arrivalTime = stopTime.arrival_time; - this.departureTime = stopTime.departure_time; - this.stopHeadsign = stopTime.stop_headsign; - this.pickupType = mapGtfsPickupDropOffType(stopTime.pickup_type); - this.dropOffType = mapGtfsPickupDropOffType(stopTime.drop_off_type); - - this.stopId = stopId; - } - - public static StopTimePickupDropOffType mapGtfsPickupDropOffType(Integer pickupDropOffType) - { - switch(pickupDropOffType) - { - case 0: - return StopTimePickupDropOffType.SCHEDULED; - case 1: - return StopTimePickupDropOffType.NONE; - case 2: - return StopTimePickupDropOffType.AGENCY; - case 3: - return StopTimePickupDropOffType.DRIVER; - default: - return null; - } - } - - public StopTime clone () throws CloneNotSupportedException { - return (StopTime) super.clone(); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/StopTimePickupDropOffType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/StopTimePickupDropOffType.java deleted file mode 100755 index 44a4475d8..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/StopTimePickupDropOffType.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -public enum StopTimePickupDropOffType { - SCHEDULED, - NONE, - AGENCY, - DRIVER; - - public Integer toGtfsValue() { - switch (this) { - case SCHEDULED: - return 0; - case NONE: - return 1; - case AGENCY: - return 2; - case DRIVER: - return 3; - default: - // can't happen, but Java requires a default statement - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/StopType.java b/src/main/java/com/conveyal/datatools/editor/models/transit/StopType.java deleted file mode 100755 index eda17faea..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/StopType.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - - -import com.conveyal.datatools.editor.models.Model; - -public class StopType extends Model { - - public String stopType; - public String description; - - public Boolean interpolated; - public Boolean majorStop; -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/Trip.java b/src/main/java/com/conveyal/datatools/editor/models/transit/Trip.java deleted file mode 100755 index 23495f560..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/Trip.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - - -import com.conveyal.gtfs.model.Frequency; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.conveyal.datatools.editor.models.Model; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - - -public class Trip extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - - public String gtfsTripId; - public String tripHeadsign; - public String tripShortName; - - public String tripDescription; - - public TripDirection tripDirection; - - public String blockId; - - public String routeId; - - public String patternId; - - public String calendarId; - - public AttributeAvailabilityType wheelchairBoarding; - - public Boolean useFrequency; - - public Integer startTime; - public Integer endTime; - - public Integer headway; - public Boolean invalid; - - public List stopTimes; - - public String feedId; - - public Trip () {} - - /** Create a trips entry from a GTFS trip. Does not import stop times. */ - public Trip(com.conveyal.gtfs.model.Trip trip, Route route, TripPattern pattern, ServiceCalendar serviceCalendar) { - gtfsTripId = trip.trip_id; - tripHeadsign = trip.trip_headsign; - tripShortName = trip.trip_short_name; - tripDirection = trip.direction_id == 0 ? TripDirection.A : TripDirection.B; - blockId = trip.block_id; - this.routeId = route.id; - this.patternId = pattern.id; - this.calendarId = serviceCalendar.id; - this.feedId = route.feedId; - this.stopTimes = new ArrayList(); - - if (trip.wheelchair_accessible == 1) - this.wheelchairBoarding = AttributeAvailabilityType.AVAILABLE; - else if (trip.wheelchair_accessible == 2) - this.wheelchairBoarding = AttributeAvailabilityType.UNAVAILABLE; - else - this.wheelchairBoarding = AttributeAvailabilityType.UNKNOWN; - - useFrequency = false; - } - - @JsonIgnore - public String getGtfsId () { - if (gtfsTripId != null && !gtfsTripId.isEmpty()) - return gtfsTripId; - else - return id.toString(); - } - - /*public com.conveyal.gtfs.model.Trip toGtfs(com.conveyal.gtfs.model.Route route, Service service) { - com.conveyal.gtfs.model.Trip ret = new com.conveyal.gtfs.model.Trip(); - - ret.block_id = blockId; - ret.route = route; - ret.trip_id = getGtfsId(); - ret.service = service; - ret.trip_headsign = tripHeadsign; - ret.trip_short_name = tripShortName; - ret.direction_id = tripDirection == tripDirection.A ? 0 : 1; - ret.block_id = blockId; - - - if (wheelchairBoarding != null) { - if (wheelchairBoarding.equals(AttributeAvailabilityType.AVAILABLE)) - ret.wheelchair_accessible = 1; - - else if (wheelchairBoarding.equals(AttributeAvailabilityType.UNAVAILABLE)) - ret.wheelchair_accessible = 2; - - else - ret.wheelchair_accessible = 0; - - } - else if (pattern.route.wheelchairBoarding != null) { - if(pattern.route.wheelchairBoarding.equals(AttributeAvailabilityType.AVAILABLE)) - ret.wheelchair_accessible = 1; - - else if (pattern.route.wheelchairBoarding.equals(AttributeAvailabilityType.UNAVAILABLE)) - ret.wheelchair_accessible = 2; - - else - ret.wheelchair_accessible = 0; - - } - - return ret; - }*/ - - /** get the frequencies.txt entry for this trip, or null if this trip should not be in frequencies.txt */ - public Frequency getFrequency(com.conveyal.gtfs.model.Trip trip) { - if (useFrequency == null || !useFrequency || headway <= 0 || trip.trip_id != getGtfsId()) - return null; - - Frequency ret = new Frequency(); - ret.start_time = startTime; - ret.end_time = endTime; - ret.headway_secs = headway; - ret.trip_id = trip.trip_id; - - return ret; - } - - public Trip clone () throws CloneNotSupportedException { - Trip ret = (Trip) super.clone(); - - // duplicate the stop times - ret.stopTimes = new ArrayList(); - - for (StopTime st : stopTimes) { - ret.stopTimes.add(st == null ? null : st.clone()); - } - - return ret; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/TripDirection.java b/src/main/java/com/conveyal/datatools/editor/models/transit/TripDirection.java deleted file mode 100755 index fb7e474f7..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/TripDirection.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - -public enum TripDirection { - A, - B -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/TripPattern.java b/src/main/java/com/conveyal/datatools/editor/models/transit/TripPattern.java deleted file mode 100755 index 5a4726682..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/TripPattern.java +++ /dev/null @@ -1,497 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - - -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.Polygon; -import com.vividsolutions.jts.linearref.LinearLocation; -import com.vividsolutions.jts.linearref.LocationIndexedLine; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.editor.models.Model; -import org.geotools.referencing.GeodeticCalculator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.conveyal.datatools.editor.utils.GeoUtils; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -import static com.conveyal.datatools.editor.utils.GeoUtils.getCoordDistances; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class TripPattern extends Model implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - public static final Logger LOG = LoggerFactory.getLogger(TripPattern.class); - public String name; - public String headsign; - - public LineString shape; - - // if true, use straight-line rather than shape-based distances - public boolean useStraightLineDistances; - - public boolean useFrequency; - - public String routeId; - - public String feedId; - - public List patternStops = new ArrayList(); - - /** - * Lines showing how stops are being snapped to the shape. - * @return - */ - @JsonProperty("stopConnections") - public LineString[] jsonGetStopConnections () { - if (useStraightLineDistances || shape == null) - return null; - - FeedTx tx = VersionedDataStore.getFeedTx(this.feedId); - - try { - LineString[] ret = new LineString[patternStops.size()]; - - double[] coordDistances = getCoordDistances(shape); - LocationIndexedLine shapeIdx = new LocationIndexedLine(shape); - - for (int i = 0; i < ret.length; i++) { - TripPatternStop ps = patternStops.get(i); - - if (ps.shapeDistTraveled == null) { - return null; - } - - Coordinate snapped = shapeIdx.extractPoint(getLoc(coordDistances, ps.shapeDistTraveled)); - // offset it slightly so that line creation does not fail if the stop is coincident - snapped.x = snapped.x - 0.00000001; - Coordinate stop = tx.stops.get(patternStops.get(i).stopId).location.getCoordinate(); - ret[i] = GeoUtils.geometyFactory.createLineString(new Coordinate[] {stop, snapped}); - } - - return ret; - } finally { - tx.rollback(); - } - - } - - public TripPattern() - { - - } - - public TripPattern(String name, String headsign, LineString shape, Route route) - { - this.name = name; - this.headsign = headsign; - this.shape = shape; - this.routeId = route.id; - } - - public TripPattern clone() throws CloneNotSupportedException { - TripPattern ret = (TripPattern) super.clone(); - - if (this.shape != null) - ret.shape = (LineString) this.shape.clone(); - else - ret.shape = null; - - ret.patternStops = new ArrayList(); - - for (TripPatternStop ps : this.patternStops) { - ret.patternStops.add(ps.clone()); - } - - return ret; - } - - - - /** - * update the trip pattern stops and the associated stop times - * see extensive discussion in ticket #102 - * basically, we assume only one stop has changed---either it's been removed, added or moved - * this is consistent with the use of Backbone.save in the UI, and - * also with the principle of least magic possible - * of course, we check to ensure that that is the case and fail if it's not - * this lets us easily detect what has happened simply by length - */ - public static void reconcilePatternStops(TripPattern originalTripPattern, TripPattern newTripPattern, FeedTx tx) { - // convenience - List originalStops = originalTripPattern.patternStops; - List newStops = newTripPattern.patternStops; - - // no need to do anything - // see #174 - if (originalStops.size() == 0) - return; - - // ADDITIONS - if (originalStops.size() == newStops.size() - 1) { - // we have an addition; find it - - int differenceLocation = -1; - for (int i = 0; i < newStops.size(); i++) { - if (differenceLocation != -1) { - // we've already found the addition - if (i < originalStops.size() && !originalStops.get(i).stopId.equals(newStops.get(i + 1).stopId)) { - // there's another difference, which we weren't expecting - throw new IllegalStateException("Multiple differences found when trying to detect stop addition"); - } - } - - // if we've reached where one trip has an extra stop, or if the stops at this position differ - else if (i == newStops.size() - 1 || !originalStops.get(i).stopId.equals(newStops.get(i).stopId)) { - // we have found the difference - differenceLocation = i; - } - } - - // insert a skipped stop at the difference location - - for (Trip trip : tx.getTripsByPattern(originalTripPattern.id)) { - trip.stopTimes.add(differenceLocation, null); - // TODO: safe? - tx.trips.put(trip.id, trip); - } - } - - // DELETIONS - else if (originalStops.size() == newStops.size() + 1) { - // we have an deletion; find it - int differenceLocation = -1; - for (int i = 0; i < originalStops.size(); i++) { - if (differenceLocation != -1) { - if (!originalStops.get(i).stopId.equals(newStops.get(i - 1).stopId)) { - // there is another difference, which we were not expecting - throw new IllegalStateException("Multiple differences found when trying to detect stop removal"); - } - } - - // we've reached the end and the only difference is length (so the last stop is the different one) - // or we've found the difference - else if (i == originalStops.size() - 1 || !originalStops.get(i).stopId.equals(newStops.get(i).stopId)) { - differenceLocation = i; - } - } - - // remove stop times for removed pattern stop - String removedStopId = originalStops.get(differenceLocation).stopId; - - for (Trip trip : tx.getTripsByPattern(originalTripPattern.id)) { - StopTime removed = trip.stopTimes.remove(differenceLocation); - - // the removed stop can be null if it was skipped. trip.stopTimes.remove will throw an exception - // rather than returning null if we try to do a remove out of bounds. - if (removed != null && !removed.stopId.equals(removedStopId)) { - throw new IllegalStateException("Attempted to remove wrong stop!"); - } - - // TODO: safe? - tx.trips.put(trip.id, trip); - } - } - - // TRANSPOSITIONS - else if (originalStops.size() == newStops.size()) { - // Imagine the trip patterns pictured below (where . is a stop, and lines indicate the same stop) - // the original trip pattern is on top, the new below - // . . . . . . . . - // | | \ \ \ | | - // * * * * * * * * - // also imagine that the two that are unmarked are the same - // (the limitations of ascii art, this is prettier on my whiteboard) - // There are three regions: the beginning and end, where stopSequences are the same, and the middle, where they are not - // The same is true of trips where stops were moved backwards - - // find the left bound of the changed region - int firstDifferentIndex = 0; - while (originalStops.get(firstDifferentIndex).stopId.equals(newStops.get(firstDifferentIndex).stopId)) { - firstDifferentIndex++; - - if (firstDifferentIndex == originalStops.size()) - // trip patterns do not differ at all, nothing to do - return; - } - - // find the right bound of the changed region - int lastDifferentIndex = originalStops.size() - 1; - while (originalStops.get(lastDifferentIndex).stopId.equals(newStops.get(lastDifferentIndex).stopId)) { - lastDifferentIndex--; - } - - // TODO: write a unit test for this - if (firstDifferentIndex == lastDifferentIndex) { - throw new IllegalStateException("stop substitutions are not supported, region of difference must have length > 1"); - } - - // figure out whether a stop was moved left or right - // note that if the stop was only moved one position, it's impossible to tell, and also doesn't matter, - // because the requisite operations are equivalent - int from, to; - - // TODO: ensure that this is all that happened (i.e. verify stop ID map inside changed region) - if (originalStops.get(firstDifferentIndex).stopId.equals(newStops.get(lastDifferentIndex).stopId)) { - // stop was moved right - from = firstDifferentIndex; - to = lastDifferentIndex; - } - - else if (newStops.get(firstDifferentIndex).stopId.equals(originalStops.get(lastDifferentIndex).stopId)) { - // stop was moved left - from = lastDifferentIndex; - to = firstDifferentIndex; - } - - else { - throw new IllegalStateException("not a simple, single move!"); - } - - for (Trip trip : tx.getTripsByPattern(originalTripPattern.id)) { - StopTime moved = trip.stopTimes.remove(from); - trip.stopTimes.add(to, moved); - trip.invalid = true; - - // TODO: safe? - tx.trips.put(trip.id, trip); - } - } - - - // OTHER STUFF IS NOT SUPPORTED - else { - throw new IllegalStateException("Changes to trip pattern stops must be made one at a time"); - } - } - - // cast generic Geometry object to LineString because jackson2-geojson library only returns generic Geometry objects - public void setShape (Geometry g) { - this.shape = (LineString) g; - } - - public void calcShapeDistTraveled () { - FeedTx tx = VersionedDataStore.getFeedTx(feedId); - calcShapeDistTraveled(tx); - tx.rollback(); - } - - /** - * Calculate the shape dist traveled along the current shape. Do this by snapping points but constraining order. - * - * To make this a bit more formal, here is the algorithm: - * - * 1. We snap each stop to the nearest point on the shape, sliced by the shape_dist_traveled of the previous stop to ensure monotonicity. - * 2. then compute the distance from stop to snapped point - * 3. multiply by 2, create a buffer of that radius around the stop, and intersect with the shape. - * 4. if it intersects in 1 or 2 places, assume that you have found the correct location for that stop and - * "fix" it into that position. - * 5. otherwise, mark it to be returned to on the second pass - * 6. on the second pass, just snap to the closest point on the subsection of the shape defined by the previous and next stop positions. - */ - public void calcShapeDistTraveled(FeedTx tx) { - if (patternStops.size() == 0) - return; - - // we don't actually store shape_dist_traveled, but rather the distance from the previous point along the shape - // however, for the algorithm it's more useful to have the cumulative dist traveled - double[] shapeDistTraveled = new double[patternStops.size()]; - - useStraightLineDistances = false; - - if (shape == null) { - calcShapeDistTraveledStraightLine(tx); - return; - } - - // compute the shape dist traveled of each coordinate of the shape - double[] shapeDist = getCoordDistances(shape); - - double[] coordDist = shapeDist; - - for (int i = 0; i < shapeDistTraveled.length; i++) { - shapeDistTraveled[i] = -1; - } - - // location along the entire shape - LocationIndexedLine shapeIdx = new LocationIndexedLine(shape); - // location along the subline currently being considered - LocationIndexedLine subIdx = shapeIdx; - - LineString subShape = shape; - - double lastShapeDistTraveled = 0; - - int fixed = 0; - - GeodeticCalculator gc = new GeodeticCalculator(); - - // detect backwards shapes - int backwards = 0; - - double lastPos = -1; - for (TripPatternStop tps : patternStops) { - Stop stop = tx.stops.get(tps.stopId); - double pos = getDist(shapeDist, shapeIdx.project(stop.location.getCoordinate())); - - if (lastPos > 0) { - if (pos > lastPos) - backwards--; - else if (pos > lastPos) - backwards++; - } - - lastPos = pos; - } - - if (backwards > 0) { - LOG.warn("Detected likely backwards shape for trip pattern {} ({}) on route {}, reversing", id, name, routeId); - this.shape = (LineString) this.shape.reverse(); - calcShapeDistTraveled(tx); - return; - } - else if (backwards == 0) { - LOG.warn("Unable to tell if shape is backwards for trip pattern {} ({}) on route {}, assuming it is correct", id, name, routeId); - } - - // first pass: fix the obvious stops - for (int i = 0; i < shapeDistTraveled.length; i++) { - TripPatternStop tps = patternStops.get(i); - Stop stop = tx.stops.get(tps.stopId); - LinearLocation candidateLoc = subIdx.project(stop.location.getCoordinate()); - Coordinate candidatePt = subIdx.extractPoint(candidateLoc); - - // step 2: compute distance - gc.setStartingGeographicPoint(stop.location.getX(), stop.location.getY()); - gc.setDestinationGeographicPoint(candidatePt.x, candidatePt.y); - double dist = gc.getOrthodromicDistance(); - - // don't snap stops more than 1km - if (dist > 1000) { - LOG.warn("Stop is more than 1km from its shape, using straight-line distances"); - this.calcShapeDistTraveledStraightLine(tx); - return; - } - - // step 3: compute buffer - // add 5m to the buffer so that if the stop sits exactly atop two lines we don't just pick one - Polygon buffer = GeoUtils.bufferGeographicPoint(stop.location.getCoordinate(), dist * 2 + 5, 20); - - Geometry intersection = buffer.intersection(shape); - if (intersection.getNumGeometries() == 1) { - // good, only one intersection - shapeDistTraveled[i] = lastShapeDistTraveled + getDist(coordDist, candidateLoc); - lastShapeDistTraveled = shapeDistTraveled[i]; - - // recalculate shape dist traveled and idx - subShape = (LineString) subIdx.extractLine(candidateLoc, subIdx.getEndIndex()); - subIdx = new LocationIndexedLine(subShape); - - coordDist = getCoordDistances(subShape); - - fixed++; - } - } - - LOG.info("Fixed {} / {} stops after first round for trip pattern {} ({}) on route {}", fixed, shapeDistTraveled.length, id, name, routeId); - - // pass 2: fix the rest of the stops - lastShapeDistTraveled = 0; - for (int i = 0; i < shapeDistTraveled.length; i++) { - TripPatternStop tps = patternStops.get(i); - Stop stop = tx.stops.get(tps.stopId); - - if (shapeDistTraveled[i] >= 0) { - lastShapeDistTraveled = shapeDistTraveled[i]; - continue; - } - - // find the next shape dist traveled - double nextShapeDistTraveled = shapeDist[shapeDist.length - 1]; - for (int j = i; j < shapeDistTraveled.length; j++) { - if (shapeDistTraveled[j] >= 0) { - nextShapeDistTraveled = shapeDistTraveled[j]; - break; - } - } - - // create and index the subshape - // recalculate shape dist traveled and idx - subShape = (LineString) shapeIdx.extractLine(getLoc(shapeDist, lastShapeDistTraveled), getLoc(shapeDist, nextShapeDistTraveled)); - - if (subShape.getLength() < 0.00000001) { - LOG.warn("Two stops on trip pattern {} map to same point on shape", id); - shapeDistTraveled[i] = lastShapeDistTraveled; - continue; - } - - subIdx = new LocationIndexedLine(subShape); - - coordDist = getCoordDistances(subShape); - - LinearLocation loc = subIdx.project(stop.location.getCoordinate()); - shapeDistTraveled[i] = lastShapeDistTraveled + getDist(coordDist, loc); - lastShapeDistTraveled = shapeDistTraveled[i]; - } - - // assign default distances - for (int i = 0; i < shapeDistTraveled.length; i++) { - patternStops.get(i).shapeDistTraveled = shapeDistTraveled[i]; - } - } - - /** Calculate distances using straight line geometries */ - public void calcShapeDistTraveledStraightLine(FeedTx tx) { - useStraightLineDistances = true; - GeodeticCalculator gc = new GeodeticCalculator(); - Stop prev = tx.stops.get(patternStops.get(0).stopId); - patternStops.get(0).shapeDistTraveled = 0D; - double previousDistance = 0; - for (int i = 1; i < patternStops.size(); i++) { - TripPatternStop ps = patternStops.get(i); - Stop stop = tx.stops.get(ps.stopId); - gc.setStartingGeographicPoint(prev.location.getX(), prev.location.getY()); - gc.setDestinationGeographicPoint(stop.location.getX(), stop.location.getY()); - previousDistance = ps.shapeDistTraveled = previousDistance + gc.getOrthodromicDistance(); - } - } - - /** - * From an array of distances at coordinates and a distance, get a linear location for that distance. - */ - private static LinearLocation getLoc(double[] distances, double distTraveled) { - if (distTraveled < 0) - return null; - - // this can happen due to rounding errors - else if (distTraveled >= distances[distances.length - 1]) { - LOG.warn("Shape dist traveled past end of shape, was {}, expected max {}, clamping", distTraveled, distances[distances.length - 1]); - return new LinearLocation(distances.length - 1, 0); - } - - for (int i = 1; i < distances.length; i++) { - if (distTraveled <= distances[i]) { - // we have found the appropriate segment - double frac = (distTraveled - distances[i - 1]) / (distances[i] - distances[i - 1]); - return new LinearLocation(i - 1, frac); - } - } - - return null; - } - - /** - * From an array of distances at coordinates and linear locs, get a distance for that location. - */ - private static double getDist(double[] distances, LinearLocation loc) { - if (loc.getSegmentIndex() == distances.length - 1) - return distances[distances.length - 1]; - - return distances[loc.getSegmentIndex()] + (distances[loc.getSegmentIndex() + 1] - distances[loc.getSegmentIndex()]) * loc.getSegmentFraction(); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/models/transit/TripPatternStop.java b/src/main/java/com/conveyal/datatools/editor/models/transit/TripPatternStop.java deleted file mode 100755 index bc42a7413..000000000 --- a/src/main/java/com/conveyal/datatools/editor/models/transit/TripPatternStop.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.conveyal.datatools.editor.models.transit; - - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -/** A stop on a trip pattern. This is not a model, as it is stored in a list within trippattern */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class TripPatternStop implements Cloneable, Serializable { - public static final long serialVersionUID = 1; - - public String stopId; - - public int defaultTravelTime; - public int defaultDwellTime; - - /** - * Is this stop a timepoint? - * - * If null, no timepoint information will be exported for this stop. - */ - public Boolean timepoint; - - public Double shapeDistTraveled; - - public TripPatternStop() - { - - } - - public TripPatternStop(Stop stop, Integer defaultTravelTime) - { - this.stopId = stop.id; - this.defaultTravelTime = defaultTravelTime; - } - - public TripPatternStop clone () throws CloneNotSupportedException { - return (TripPatternStop) super.clone(); - } -} - - diff --git a/src/main/java/com/conveyal/datatools/editor/utils/BindUtils.java b/src/main/java/com/conveyal/datatools/editor/utils/BindUtils.java deleted file mode 100644 index 96a15a5ef..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/BindUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import org.mapdb.Bind.MapListener; -import org.mapdb.Bind.MapWithModificationListener; -import org.mapdb.Fun.Function2; - -import java.util.Map; -import java.util.Set; - -public class BindUtils { - /** - * Index the map keys of a subset of the given map. The set contains all of the keys for which filter returns true. - */ - public static void subsetIndex (MapWithModificationListener map, final Set subset, final Function2 filter) { - // fill if empty - if (subset.isEmpty()) { - for (Map.Entry e : map.entrySet()) { - if (filter.run(e.getKey(), e.getValue())) { - subset.add(e.getKey()); - } - } - } - - map.modificationListenerAdd(new MapListener() { - @Override - public void update(K key, V oldVal, V newVal) { - // addition - if (newVal != null && filter.run(key, newVal)) { - subset.add(key); - } - else { - // doesn't matter if it's present - subset.remove(key); - } - } - }); - } - - /** - * Make a histogram where each item can be a member of multiple categories. - */ - public static void multiHistogram (MapWithModificationListener map, - final Map histogram, final Function2 categories) { - if (histogram.isEmpty()) { - for (Map.Entry e : map.entrySet()) { - for (C cat : categories.run(e.getKey(), e.getValue())) { - if (!histogram.containsKey(cat)) - histogram.put(cat, 1L); - else - histogram.put(cat, histogram.get(cat) + 1); - } - } - } - - map.modificationListenerAdd(new MapListener() { - @Override - public void update(K key, V oldVal, V newVal) { - if (oldVal != null) { - for (C cat : categories.run(key, oldVal)) { - histogram.put(cat, histogram.get(cat) - 1); - } - } - - if (newVal != null) { - for (C cat : categories.run(key, newVal)) { - if (!histogram.containsKey(cat)) - histogram.put(cat, 1L); - else - histogram.put(cat, histogram.get(cat) + 1); - } - } - } - }); - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/ClassLoaderSerializer.java b/src/main/java/com/conveyal/datatools/editor/utils/ClassLoaderSerializer.java deleted file mode 100644 index c0a785c88..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/ClassLoaderSerializer.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import org.apache.commons.io.input.ClassLoaderObjectInputStream; -import org.mapdb.Serializer; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.OutputStream; -import java.io.Serializable; - -/** - * Deserialize using the thread's class loader, not the root class loader. - */ -public class ClassLoaderSerializer implements Serializer, Serializable { - - @Override - public void serialize(DataOutput out, Object value) throws IOException { - ObjectOutputStream out2 = new ObjectOutputStream((OutputStream) out); - out2.writeObject(value); - out2.flush(); - } - - @Override - public Object deserialize(DataInput in, int available) throws IOException { - try { - ObjectInputStream in2 = new ClassLoaderObjectInputStream(Thread.currentThread().getContextClassLoader(), (InputStream) in); - return in2.readObject(); - } catch (ClassNotFoundException e) { - throw new IOException(e); - } - } - - @Override - public int fixedSize() { - return -1; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/DirectoryZip.java b/src/main/java/com/conveyal/datatools/editor/utils/DirectoryZip.java deleted file mode 100755 index 3ffe5a298..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/DirectoryZip.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.Deque; -import java.util.LinkedList; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public class DirectoryZip { - - - public static void zip(File directory, File zipfile) throws IOException { - URI base = directory.toURI(); - Deque queue = new LinkedList(); - queue.push(directory); - OutputStream out = new FileOutputStream(zipfile); - Closeable res = out; - try { - ZipOutputStream zout = new ZipOutputStream(out); - res = zout; - while (!queue.isEmpty()) { - directory = queue.pop(); - for (File kid : directory.listFiles()) { - String name = base.relativize(kid.toURI()).getPath(); - if (kid.isDirectory()) { - queue.push(kid); - name = name.endsWith("/") ? name : name + "/"; - zout.putNextEntry(new ZipEntry(name)); - } else { - zout.putNextEntry(new ZipEntry(name)); - copy(kid, zout); - zout.closeEntry(); - } - } - } - } finally { - res.close(); - } - } - - - private static void copy(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - while (true) { - int readCount = in.read(buffer); - if (readCount < 0) { - break; - } - out.write(buffer, 0, readCount); - } - } - - private static void copy(File file, OutputStream out) throws IOException { - InputStream in = new FileInputStream(file); - try { - copy(in, out); - } finally { - in.close(); - } - } - - private static void copy(InputStream in, File file) throws IOException { - OutputStream out = new FileOutputStream(file); - try { - copy(in, out); - } finally { - out.close(); - } - } - -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/EncodedPolylineBean.java b/src/main/java/com/conveyal/datatools/editor/utils/EncodedPolylineBean.java deleted file mode 100755 index 9439d3b19..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/EncodedPolylineBean.java +++ /dev/null @@ -1,105 +0,0 @@ -/* This program is free software: you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public License - as published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . */ - -package com.conveyal.datatools.editor.utils; - -import java.io.Serializable; - -/** - * A list of coordinates encoded as a string. - * - * See Encoded - * polyline algorithm format - */ - -public class EncodedPolylineBean implements Serializable { - - private static final long serialVersionUID = 1L; - - private String points; - - private String levels; - - private int length; - - public EncodedPolylineBean() { - - } - - public EncodedPolylineBean(String points, String levels, int length) { - this.points = points; - this.levels = levels; - this.length = length; - } - - /** - * The encoded points of the polyline. - */ - public String getPoints() { - return points; - } - - public void setPoints(String points) { - this.points = points; - } - - /** - * Levels describes which points should be shown at various zoom levels. Presently, we show all - * points at all zoom levels. - */ - public String getLevels() { - return levels; - } - - public String getLevels(int defaultLevel) { - if (levels == null) { - StringBuilder b = new StringBuilder(); - String l = encodeNumber(defaultLevel); - for (int i = 0; i < length; i++) - b.append(l); - return b.toString(); - } - return levels; - } - - public void setLevels(String levels) { - this.levels = levels; - } - - /** - * The number of points in the string - */ - public int getLength() { - return length; - } - - public void setLength(int length) { - this.length = length; - } - - private static String encodeNumber(int num) { - - StringBuffer encodeString = new StringBuffer(); - - while (num >= 0x20) { - int nextValue = (0x20 | (num & 0x1f)) + 63; - encodeString.append((char) (nextValue)); - num >>= 5; - } - - num += 63; - encodeString.append((char) (num)); - - return encodeString.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/utils/FeatureAttributeFormatter.java b/src/main/java/com/conveyal/datatools/editor/utils/FeatureAttributeFormatter.java deleted file mode 100755 index d787869d0..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/FeatureAttributeFormatter.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import org.opengis.feature.simple.SimpleFeature; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class FeatureAttributeFormatter { - public static final Logger LOG = LoggerFactory.getLogger(FeatureAttributeFormatter.class); - String formatString; - Matcher matches; - - public FeatureAttributeFormatter(String format) - { - this.formatString = format; - - Pattern pattern = Pattern.compile("#([0-9]+)"); - - this.matches = pattern.matcher(format); - } - - public String format(SimpleFeature feature) - { - String output = new String(formatString); - - while(matches.find()) - { - String sub = matches.group(); - - Integer fieldPosition = Integer.parseInt(sub.replace("#", "")); - - try - { - String attributeString = feature.getAttribute(fieldPosition).toString(); - - output = output.replace(sub, attributeString); - } - catch(Exception e) { - LOG.warn("Index out of range."); - } - - } - - return output; - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/GeoUtils.java b/src/main/java/com/conveyal/datatools/editor/utils/GeoUtils.java deleted file mode 100644 index 90da61672..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/GeoUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.Polygon; -import org.geotools.referencing.GeodeticCalculator; - -import java.awt.geom.Point2D; - -public class GeoUtils { - public static GeometryFactory geometyFactory = new GeometryFactory(); - - /** - * Create a buffer around the given point of the given size in meters. Uses geography rather than cartesian distance. - * @param point the point to buffer, in WGS84 geographic coordinates - * @param dist the buffer size, in meters - * @param npoints the number of points in the buffer - */ - public static Polygon bufferGeographicPoint (Coordinate point, double dist, int npoints) { - GeodeticCalculator calc = new GeodeticCalculator(); - calc.setStartingGeographicPoint(point.x, point.y); - - Coordinate[] coords = new Coordinate[npoints + 1]; - - for (int i = 0; i < npoints; i++) { - double deg = -180 + i * 360 / npoints; - calc.setDirection(deg, dist); - Point2D dest = calc.getDestinationGeographicPoint(); - coords[i] = new Coordinate(dest.getX(), dest.getY()); - } - - coords[npoints] = (Coordinate) coords[0].clone(); - - LinearRing ring = geometyFactory.createLinearRing(coords); - LinearRing[] holes = new LinearRing[0]; - - return geometyFactory.createPolygon(ring, holes); - } - - /** get the distances from the start of the line string to every coordinate along the line string */ - public static double[] getCoordDistances(LineString line) { - double[] coordDist = new double[line.getNumPoints()]; - coordDist[0] = 0; - - Coordinate prev = line.getCoordinateN(0); - GeodeticCalculator gc = new GeodeticCalculator(); - for (int j = 1; j < coordDist.length; j++) { - Coordinate current = line.getCoordinateN(j); - if (Double.isNaN(prev.x)) { - coordDist[j] = 0; - continue; - } - gc.setStartingGeographicPoint(prev.x, prev.y); - gc.setDestinationGeographicPoint(current.x, current.y); - coordDist[j] = coordDist[j - 1] + gc.getOrthodromicDistance(); - prev = current; - } - - return coordDist; - } - -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/JacksonSerializers.java b/src/main/java/com/conveyal/datatools/editor/utils/JacksonSerializers.java deleted file mode 100644 index 4c6bf95fd..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/JacksonSerializers.java +++ /dev/null @@ -1,206 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import com.conveyal.datatools.editor.models.transit.GtfsRouteType; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; -import com.google.common.io.BaseEncoding; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.LocalDate; -import java.time.LocalTime; -//import java.time.format.D; -import java.time.format.DateTimeFormatter; -import org.mapdb.Fun.Tuple2; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.List; - -public class JacksonSerializers { - private static final BaseEncoding encoder = BaseEncoding.base64Url(); - public static class Tuple2Serializer extends StdScalarSerializer> { - public Tuple2Serializer () { - super(Tuple2.class, true); - } - - @Override - public void serialize(Tuple2 t2, JsonGenerator jgen, - SerializerProvider arg2) throws IOException, - JsonProcessingException { - jgen.writeString(serialize(t2)); - } - - public static String serialize (Tuple2 t2) { - try { - return encoder.encode(t2.a.getBytes("UTF-8")) + ":" + encoder.encode(t2.b.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new UnsupportedOperationException(e); - } - } - } - - public static class Tuple2Deserializer extends StdScalarDeserializer> { - public Tuple2Deserializer () { - super(Tuple2.class); - } - - @Override - public Tuple2 deserialize(JsonParser jp, - DeserializationContext arg1) throws IOException, - JsonProcessingException { - return deserialize(jp.readValueAs(String.class)); - } - - public static Tuple2 deserialize (String serialized) throws IOException { - String[] val = serialized.split(":"); - if (val.length != 2) { - throw new IOException("Unable to parse value"); - } - - return new Tuple2(new String(encoder.decode(val[0]), "UTF-8"), new String(encoder.decode(val[1]), "UTF-8")); - } - } - - public static class Tuple2IntSerializer extends StdScalarSerializer> { - public Tuple2IntSerializer () { - super(Tuple2.class, true); - } - - @Override - public void serialize(Tuple2 t2, JsonGenerator jgen, - SerializerProvider arg2) throws IOException, - JsonProcessingException { - jgen.writeString(serialize(t2)); - } - - public static String serialize (Tuple2 t2) { - try { - return encoder.encode(t2.a.getBytes("UTF-8")) + ":" + t2.b.toString(); - } catch (UnsupportedEncodingException e) { - throw new UnsupportedOperationException(e); - } - } - } - - public static class Tuple2IntDeserializer extends StdScalarDeserializer> { - public Tuple2IntDeserializer () { - super(Tuple2.class); - } - - @Override - public Tuple2 deserialize(JsonParser jp, - DeserializationContext arg1) throws IOException, - JsonProcessingException { - return deserialize(jp.readValueAs(String.class)); - } - - public static Tuple2 deserialize (String serialized) throws IOException { - String[] val = serialized.split(":"); - if (val.length != 2) { - throw new IOException("Unable to parse value"); - } - - return new Tuple2(new String(encoder.decode(val[0]), "UTF-8"), Integer.parseInt(val[1])); - } - } - - /** serialize local dates as noon GMT epoch times */ - public static class LocalDateSerializer extends StdScalarSerializer { - public LocalDateSerializer() { - super(LocalDate.class, false); - } - - @Override - public void serialize(LocalDate ld, JsonGenerator jgen, - SerializerProvider arg2) throws IOException, - JsonGenerationException { - long millis = ld.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); - jgen.writeNumber(millis); - } - } - - /** deserialize local dates from GMT epochs */ - public static class LocalDateDeserializer extends StdScalarDeserializer { - public LocalDateDeserializer () { - super(LocalDate.class); - } - - @Override - public LocalDate deserialize(JsonParser jp, - DeserializationContext arg1) throws IOException, - JsonProcessingException { - - LocalDate date = Instant.ofEpochMilli(jp.getValueAsLong()).atZone(ZoneOffset.UTC).toLocalDate(); - return date; - } - } - - /** serialize local dates as noon GMT epoch times */ - public static class GtfsRouteTypeSerializer extends StdScalarSerializer { - public GtfsRouteTypeSerializer() { - super(GtfsRouteType.class, false); - } - - @Override - public void serialize(GtfsRouteType gtfsRouteType, JsonGenerator jgen, - SerializerProvider arg2) throws IOException, - JsonGenerationException { - jgen.writeNumber(gtfsRouteType.toGtfs()); - } - } - - /** deserialize local dates from GMT epochs */ - public static class GtfsRouteTypeDeserializer extends StdScalarDeserializer { - public GtfsRouteTypeDeserializer () { - super(GtfsRouteType.class); - } - - @Override - public GtfsRouteType deserialize(JsonParser jp, - DeserializationContext arg1) throws IOException, - JsonProcessingException { - return GtfsRouteType.fromGtfs(jp.getValueAsInt()); - } - } - - public static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - /** Serialize a local date to an ISO date (year-month-day) */ - public static class LocalDateIsoSerializer extends StdScalarSerializer { - public LocalDateIsoSerializer () { - super(LocalDate.class, false); - } - - @Override - public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonGenerationException { - jsonGenerator.writeString(localDate.format(format)); - } - } - - /** Deserialize an ISO date (year-month-day) */ - public static class LocalDateIsoDeserializer extends StdScalarDeserializer { - public LocalDateIsoDeserializer () { - super(LocalDate.class); - } - - @Override - public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { - return LocalDate.parse(jsonParser.getValueAsString(), format); - } - - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/PolylineEncoder.java b/src/main/java/com/conveyal/datatools/editor/utils/PolylineEncoder.java deleted file mode 100755 index 9072fd382..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/PolylineEncoder.java +++ /dev/null @@ -1,239 +0,0 @@ -/* This program is free software: you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public License - as published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . */ - -package com.conveyal.datatools.editor.utils; - -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.LineString; -import com.vividsolutions.jts.geom.MultiLineString; - -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.List; - -public class PolylineEncoder { - - public static EncodedPolylineBean createEncodings(double[] lat, double[] lon) { - return createEncodings(new PointAdapterList(lat, lon)); - } - - public static EncodedPolylineBean createEncodings(double[] lat, double[] lon, int level) { - return createEncodings(new PointAdapterList(lat, lon), level); - } - - public static EncodedPolylineBean createEncodings(double[] lat, double[] lon, int offset, - int length, int level) { - return createEncodings(new PointAdapterList(lat, lon, offset, length), level); - } - - public static EncodedPolylineBean createEncodings(Iterable points) { - return createEncodings(points, -1); - } - - public static EncodedPolylineBean createEncodings(Geometry geometry) { - if (geometry instanceof LineString) { - - LineString string = (LineString) geometry; - Coordinate[] coordinates = string.getCoordinates(); - return createEncodings(new CoordinateList(coordinates)); - } else if (geometry instanceof MultiLineString) { - MultiLineString mls = (MultiLineString) geometry; - return createEncodings(new CoordinateList(mls.getCoordinates())); - } else { - throw new IllegalArgumentException(geometry.toString()); - } - } - - /** - * If level < 0, then {@link EncodedPolylineBean#getLevels()} will be null. - * - * @param points - * @param level - * @return - */ - public static EncodedPolylineBean createEncodings(Iterable points, int level) { - - StringBuilder encodedPoints = new StringBuilder(); - StringBuilder encodedLevels = new StringBuilder(); - - int plat = 0; - int plng = 0; - int count = 0; - - for (Coordinate point : points) { - - int late5 = floor1e5(point.y); - int lnge5 = floor1e5(point.x); - - int dlat = late5 - plat; - int dlng = lnge5 - plng; - - plat = late5; - plng = lnge5; - - encodedPoints.append(encodeSignedNumber(dlat)).append(encodeSignedNumber(dlng)); - if (level >= 0) - encodedLevels.append(encodeNumber(level)); - count++; - } - - String pointsString = encodedPoints.toString(); - String levelsString = level >= 0 ? encodedLevels.toString() : null; - return new EncodedPolylineBean(pointsString, levelsString, count); - } - - public static List decode(EncodedPolylineBean polyline) { - - String pointString = polyline.getPoints(); - - double lat = 0; - double lon = 0; - - int strIndex = 0; - List points = new ArrayList(); - - while (strIndex < pointString.length()) { - - int[] rLat = decodeSignedNumberWithIndex(pointString, strIndex); - lat = lat + rLat[0] * 1e-5; - strIndex = rLat[1]; - - int[] rLon = decodeSignedNumberWithIndex(pointString, strIndex); - lon = lon + rLon[0] * 1e-5; - strIndex = rLon[1]; - - points.add(new Coordinate(lon, lat)); - } - - return points; - } - - /***************************************************************************** - * Private Methods - ****************************************************************************/ - - private static final int floor1e5(double coordinate) { - return (int) Math.floor(coordinate * 1e5); - } - - public static String encodeSignedNumber(int num) { - int sgn_num = num << 1; - if (num < 0) { - sgn_num = ~(sgn_num); - } - return (encodeNumber(sgn_num)); - } - - public static int decodeSignedNumber(String value) { - int[] r = decodeSignedNumberWithIndex(value, 0); - return r[0]; - } - - public static int[] decodeSignedNumberWithIndex(String value, int index) { - int[] r = decodeNumberWithIndex(value, index); - int sgn_num = r[0]; - if ((sgn_num & 0x01) > 0) { - sgn_num = ~(sgn_num); - } - r[0] = sgn_num >> 1; - return r; - } - - public static String encodeNumber(int num) { - - StringBuffer encodeString = new StringBuffer(); - - while (num >= 0x20) { - int nextValue = (0x20 | (num & 0x1f)) + 63; - encodeString.append((char) (nextValue)); - num >>= 5; - } - - num += 63; - encodeString.append((char) (num)); - - return encodeString.toString(); - } - - public static int decodeNumber(String value) { - int[] r = decodeNumberWithIndex(value, 0); - return r[0]; - } - - public static int[] decodeNumberWithIndex(String value, int index) { - - if (value.length() == 0) - throw new IllegalArgumentException("string is empty"); - - int num = 0; - int v = 0; - int shift = 0; - - do { - v = value.charAt(index++) - 63; - num |= (v & 0x1f) << shift; - shift += 5; - } while (v >= 0x20); - - return new int[] { num, index }; - } - - private static class PointAdapterList extends AbstractList { - - private double[] _lat; - private double[] _lon; - private int _offset; - private int _length; - - public PointAdapterList(double[] lat, double[] lon) { - this(lat, lon, 0, lat.length); - } - - public PointAdapterList(double[] lat, double[] lon, int offset, int length) { - _lat = lat; - _lon = lon; - _offset = offset; - _length = length; - } - - @Override - public Coordinate get(int index) { - return new Coordinate(_lon[_offset + index], _lat[_offset + index]); - } - - @Override - public int size() { - return _length; - } - } - - private static class CoordinateList extends AbstractList { - - private Coordinate[] _coordinates; - - public CoordinateList(Coordinate[] coordinates) { - _coordinates = coordinates; - } - - @Override - public Coordinate get(int index) { - return _coordinates[index]; - } - - @Override - public int size() { - return _coordinates.length; - } - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/S3Utils.java b/src/main/java/com/conveyal/datatools/editor/utils/S3Utils.java deleted file mode 100644 index ab8d3dc65..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/S3Utils.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.CannedAccessControlList; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.conveyal.datatools.manager.DataManager; -import org.apache.commons.io.IOUtils; -import spark.Request; - -import javax.servlet.MultipartConfigElement; -import javax.servlet.ServletException; -import javax.servlet.http.Part; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import static spark.Spark.halt; - -/** - * Created by landon on 8/2/16. - */ -public class S3Utils { - - public static String uploadBranding(Request req, String id) throws IOException, ServletException { - String url; - - String s3Bucket = DataManager.getConfigPropertyAsText("application.data.gtfs_s3_bucket"); - if (s3Bucket == null) { - halt(400); - } - - // Get file from request - if (req.raw().getAttribute("org.eclipse.jetty.multipartConfig") == null) { - MultipartConfigElement multipartConfigElement = new MultipartConfigElement(System.getProperty("java.io.tmpdir")); - req.raw().setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement); - } - -// req.attribute("org.eclipse.jetty.multipartConfig", new MultipartConfigElement("/temp")); - - Part part = req.raw().getPart("file"); - String extension = "." + part.getContentType().split("/", 0)[1]; - File tempFile = File.createTempFile(id, extension); - tempFile.deleteOnExit(); - - InputStream inputStream; - try { - inputStream = part.getInputStream(); - FileOutputStream out = new FileOutputStream(tempFile); - IOUtils.copy(inputStream, out); - } catch (Exception e) { -// LOG.error("Unable to open input stream from upload"); - halt("Unable to read uploaded file"); - } - - try { -// LOG.info("Uploading route branding to S3"); - // Upload file to s3 - AWSCredentials creds; - - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - - String keyName = "branding/" + id + extension; - url = "https://s3.amazonaws.com/" + s3Bucket + "/" + keyName; - AmazonS3 s3client = new AmazonS3Client(creds); - s3client.putObject(new PutObjectRequest( - s3Bucket, keyName, tempFile) - // grant public read - .withCannedAcl(CannedAccessControlList.PublicRead)); - return url; - } - catch (AmazonServiceException ase) { - halt("Error uploading feed to S3"); - return null; - } - } -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/StatusMessage.java b/src/main/java/com/conveyal/datatools/editor/utils/StatusMessage.java deleted file mode 100755 index 1a76c87ff..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/StatusMessage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -public class StatusMessage { - public String status; - public String message; - public String detail; - - public StatusMessage(String status, String message, String detail) { - this.status = status; - this.message = message; - this.detail = detail; - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/editor/utils/TimePoint.java b/src/main/java/com/conveyal/datatools/editor/utils/TimePoint.java deleted file mode 100755 index 107830cb5..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/TimePoint.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.conveyal.datatools.editor.utils; - -public class TimePoint { - - public String stopName; - public String time; - -} diff --git a/src/main/java/com/conveyal/datatools/editor/utils/tags/TimeExtensions.java b/src/main/java/com/conveyal/datatools/editor/utils/tags/TimeExtensions.java deleted file mode 100644 index fd0bc9e64..000000000 --- a/src/main/java/com/conveyal/datatools/editor/utils/tags/TimeExtensions.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.conveyal.datatools.editor.utils.tags; - -//import play.templates.JavaExtensions; - -import java.text.DecimalFormat; - -public class TimeExtensions { - - public static String ccyAmount(Number number) { - Integer min = (int)Math.floor(number.doubleValue()/new Double(60)); - Integer sec = (int)number.intValue() % 60; - - DecimalFormat twoDigits = new DecimalFormat("00"); - - return twoDigits.format(min) + ":" + twoDigits.format(sec); - } - - public static String hmsFormat(Number number) { - if(number == null) - return "00:00:00"; - - Integer hour = (int)Math.floor(number.doubleValue()/new Double(60 * 60)); - Integer min = (int)Math.floor(number.doubleValue()/new Double(60)) % 60; - Integer sec = (int)number.intValue() % 60; - - DecimalFormat twoDigits = new DecimalFormat("00"); - - return twoDigits.format(hour) + ":" + twoDigits.format(min) + ":" + twoDigits.format(sec); - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/DataManager.java b/src/main/java/com/conveyal/datatools/manager/DataManager.java deleted file mode 100644 index 5a1630bb9..000000000 --- a/src/main/java/com/conveyal/datatools/manager/DataManager.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.conveyal.datatools.manager; - -import com.conveyal.datatools.manager.auth.Auth0Connection; - -import com.conveyal.datatools.manager.controllers.DumpController; -import com.conveyal.datatools.manager.controllers.api.*; -import com.conveyal.datatools.editor.controllers.api.*; - -import com.conveyal.datatools.manager.extensions.ExternalFeedResource; -import com.conveyal.datatools.manager.extensions.mtc.MtcFeedResource; -import com.conveyal.datatools.manager.extensions.transitfeeds.TransitFeedsFeedResource; -import com.conveyal.datatools.manager.extensions.transitland.TransitLandFeedResource; - -import com.conveyal.datatools.manager.jobs.FetchProjectFeedsJob; -import com.conveyal.datatools.manager.jobs.LoadGtfsApiFeedJob; -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.Project; -import com.conveyal.datatools.manager.utils.CorsFilter; -import com.conveyal.datatools.manager.utils.ResponseError; -import com.conveyal.gtfs.GTFSCache; -import com.conveyal.gtfs.api.ApiMain; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.gson.Gson; -import org.apache.http.concurrent.Cancellable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.utils.IOUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; - -import static java.util.concurrent.TimeUnit.*; - -import static spark.Spark.*; - -public class DataManager { - - public static final Logger LOG = LoggerFactory.getLogger(DataManager.class); - - public static JsonNode config; - public static JsonNode serverConfig; - - public static JsonNode gtfsPlusConfig; - public static JsonNode gtfsConfig; - - public static final Map feedResources = new HashMap<>(); - - public static Map> userJobsMap = new HashMap<>(); - - public static Map autoFetchMap = new HashMap<>(); - public final static ScheduledExecutorService scheduler = - Executors.newScheduledThreadPool(1); - - public static GTFSCache gtfsCache; - public static String cacheDirectory; - private static List apiFeedSources = new ArrayList<>(); - - public static void main(String[] args) throws IOException { - - // load config - FileInputStream in; - - if (args.length == 0) - in = new FileInputStream(new File("config.yml")); - else - in = new FileInputStream(new File(args[0])); - - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - config = mapper.readTree(in); - - ObjectMapper serverMapper = new ObjectMapper(new YAMLFactory()); - serverConfig = serverMapper.readTree(new File("config_server.yml")); - - // set port - if(config.get("application").has("port")) { - port(Integer.parseInt(config.get("application").get("port").asText())); - } - - // initialize map of auto fetched projects - for (Project p : Project.getAll()) { - if (p.autoFetchFeeds != null && autoFetchMap.get(p.id) == null){ - if (p.autoFetchFeeds) { - ScheduledFuture scheduledFuture = ProjectController.scheduleAutoFeedFetch(p.id, p.autoFetchHour, p.autoFetchMinute, 1, p.defaultTimeZone); - autoFetchMap.put(p.id, scheduledFuture); - } - } - } - cacheDirectory = getConfigPropertyAsText("application.data.gtfs") + "/cache"; - File cacheDir = new File(cacheDirectory); - if (!cacheDir.isDirectory()) { - cacheDir.mkdir(); - } - gtfsCache = new GTFSCache(getConfigPropertyAsText("application.data.gtfs_s3_bucket"), cacheDir); - CorsFilter.apply(); - - String apiPrefix = "/api/manager/"; - - // core controllers - ProjectController.register(apiPrefix); - FeedSourceController.register(apiPrefix); - FeedVersionController.register(apiPrefix); - RegionController.register(apiPrefix); - NoteController.register(apiPrefix); - StatusController.register(apiPrefix); - - // Editor routes - if ("true".equals(getConfigPropertyAsText("modules.editor.enabled"))) { - gtfsConfig = mapper.readTree(new File("gtfs.yml")); - AgencyController.register(apiPrefix); - CalendarController.register(apiPrefix); - RouteController.register(apiPrefix); - RouteTypeController.register(apiPrefix); - ScheduleExceptionController.register(apiPrefix); - StopController.register(apiPrefix); - TripController.register(apiPrefix); - TripPatternController.register(apiPrefix); - SnapshotController.register(apiPrefix); - FeedInfoController.register(apiPrefix); - FareController.register(apiPrefix); - } - - // module-specific controllers - if (isModuleEnabled("deployment")) { - DeploymentController.register(apiPrefix); - } - if (isModuleEnabled("gtfsapi")) { - GtfsApiController.register(apiPrefix); - } - if (isModuleEnabled("gtfsplus")) { - GtfsPlusController.register(apiPrefix); - gtfsPlusConfig = mapper.readTree(new File("gtfsplus.yml")); - } - if (isModuleEnabled("user_admin")) { - UserController.register(apiPrefix); - } - if (isModuleEnabled("dump")) { - DumpController.register("/"); - } - - before(apiPrefix + "secure/*", (request, response) -> { - if(request.requestMethod().equals("OPTIONS")) return; - Auth0Connection.checkUser(request); - }); - - // lazy load feeds if new one is requested - if ("true".equals(getConfigPropertyAsText("modules.gtfsapi.load_on_fetch"))) { - before(apiPrefix + "*", (request, response) -> { - String feeds = request.queryParams("feed"); - if (feeds != null) { - String[] feedIds = feeds.split(","); - for (String feedId : feedIds) { - FeedSource fs = FeedSource.get(feedId); - if (fs == null) { - continue; - } - else if (!GtfsApiController.gtfsApi.registeredFeedSources.contains(fs.id) && !apiFeedSources.contains(fs.id)) { - apiFeedSources.add(fs.id); - - LoadGtfsApiFeedJob loadJob = new LoadGtfsApiFeedJob(fs); - new Thread(loadJob).start(); - halt(202, "Initializing feed load..."); - } - else if (apiFeedSources.contains(fs.id) && !GtfsApiController.gtfsApi.registeredFeedSources.contains(fs.id)) { - halt(202, "Loading feed, please try again later"); - } - } - - } - }); - } - - after(apiPrefix + "*", (request, response) -> { - - // only set content type if successful response -// if (response.status() < 300) { - response.type("application/json"); -// } - response.header("Content-Encoding", "gzip"); - }); - - get("/main.js", (request, response) -> { - try (InputStream stream = DataManager.class.getResourceAsStream("/public/main.js")) { - return IOUtils.toString(stream); - } catch (IOException e) { - return null; - // if the resource doesn't exist we just carry on. - } - }); - - // return 404 for any api response that's not found - get(apiPrefix + "*", (request, response) -> { - halt(404); - return null; - }); - - // return assets as byte array - get("/assets/*", (request, response) -> { - try (InputStream stream = DataManager.class.getResourceAsStream("/public" + request.pathInfo())) { - return IOUtils.toByteArray(stream); - } catch (IOException e) { - return null; - } - }); - // return index.html for any sub-directory - get("/*", (request, response) -> { - response.type("text/html"); - try (InputStream stream = DataManager.class.getResourceAsStream("/public/index.html")) { - return IOUtils.toString(stream); - } catch (IOException e) { - return null; - // if the resource doesn't exist we just carry on. - } - }); -// exception(IllegalArgumentException.class,(e,req,res) -> { -// res.status(400); -// res.body(new Gson().toJson(new ResponseError(e))); -// }); - registerExternalResources(); - } - - - public static JsonNode getConfigProperty(String name) { - // try the server config first, then the main config - JsonNode fromServerConfig = getConfigProperty(serverConfig, name); - if(fromServerConfig != null) return fromServerConfig; - - return getConfigProperty(config, name); - } - - public static JsonNode getConfigProperty(JsonNode config, String name) { - String parts[] = name.split("\\."); - JsonNode node = config; - for(int i = 0; i < parts.length; i++) { - if(node == null) return null; - node = node.get(parts[i]); - } - return node; - } - - public static String getConfigPropertyAsText(String name) { - JsonNode node = getConfigProperty(name); - return (node != null) ? node.asText() : null; - } - - public static boolean isModuleEnabled(String moduleName) { - return "true".equals(getConfigPropertyAsText("modules." + moduleName + ".enabled")); - } - - private static void registerExternalResources() { - - if ("true".equals(getConfigPropertyAsText("extensions.mtc.enabled"))) { - LOG.info("Registering MTC Resource"); - registerExternalResource(new MtcFeedResource()); - } - - if ("true".equals(getConfigPropertyAsText("extensions.transitland.enabled"))) { - LOG.info("Registering TransitLand Resource"); - registerExternalResource(new TransitLandFeedResource()); - } - - if ("true".equals(getConfigPropertyAsText("extensions.transitfeeds.enabled"))) { - LOG.info("Registering TransitFeeds Resource"); - registerExternalResource(new TransitFeedsFeedResource()); - } - } - - private static void registerExternalResource(ExternalFeedResource resource) { - feedResources.put(resource.getResourceType(), resource); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/auth/Auth0Connection.java b/src/main/java/com/conveyal/datatools/manager/auth/Auth0Connection.java deleted file mode 100644 index c9181ac52..000000000 --- a/src/main/java/com/conveyal/datatools/manager/auth/Auth0Connection.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.conveyal.datatools.manager.auth; - -import com.conveyal.datatools.manager.DataManager; -import com.fasterxml.jackson.databind.ObjectMapper; -import spark.Request; - -import javax.net.ssl.HttpsURLConnection; -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.InputStreamReader; -import java.net.URL; - -import static spark.Spark.halt; - -/** - * Created by demory on 3/22/16. - */ - -public class Auth0Connection { - - public static void checkUser(Request req) { - String token = getToken(req); - - if(token == null) { - halt(401, "Could not find authorization token"); - } - - try { - Auth0UserProfile profile = getUserProfile(token); - req.attribute("user", profile); - } - catch(Exception e) { -// e.printStackTrace(); - halt(401, "Could not verify user"); - } - } - - public static String getToken(Request req) { - String token = null; - - final String authorizationHeader = req.headers("Authorization"); - if (authorizationHeader == null) return null; - - // check format (Authorization: Bearer [token]) - String[] parts = authorizationHeader.split(" "); - if (parts.length != 2) return null; - - String scheme = parts[0]; - String credentials = parts[1]; - - if (scheme.equals("Bearer")) token = credentials; - return token; - } - - public static Auth0UserProfile getUserProfile(String token) throws Exception { - - URL url = new URL("https://" + DataManager.config.get("auth0").get("domain").asText() + "/tokeninfo"); - HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); - - //add request header - con.setRequestMethod("POST"); - - String urlParameters = "id_token=" + token; - - // Send post request - con.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(con.getOutputStream()); - wr.writeBytes(urlParameters); - wr.flush(); - wr.close(); - - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - - ObjectMapper m = new ObjectMapper(); - return m.readValue(response.toString(), Auth0UserProfile.class); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/auth/Auth0UserProfile.java b/src/main/java/com/conveyal/datatools/manager/auth/Auth0UserProfile.java deleted file mode 100644 index bd8d2bf32..000000000 --- a/src/main/java/com/conveyal/datatools/manager/auth/Auth0UserProfile.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.conveyal.datatools.manager.auth; - -import com.conveyal.datatools.manager.DataManager; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -/** - * Created by demory on 1/18/16. - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Auth0UserProfile { - String email; - String user_id; - AppMetadata app_metadata; - - public Auth0UserProfile() { - } - - - public String getUser_id() { - return user_id; - } - - public void setUser_id(String user_id) { - this.user_id = user_id; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setApp_metadata(AppMetadata app_metadata) { - this.app_metadata = app_metadata; - } - - public AppMetadata getApp_metadata() { return app_metadata; } - - @JsonIgnore - public void setDatatoolsInfo(DatatoolsInfo datatoolsInfo) { - this.app_metadata.getDatatoolsInfo().setClientId(datatoolsInfo.clientId); - this.app_metadata.getDatatoolsInfo().setPermissions(datatoolsInfo.permissions); - this.app_metadata.getDatatoolsInfo().setProjects(datatoolsInfo.projects); - this.app_metadata.getDatatoolsInfo().setSubscriptions(datatoolsInfo.subscriptions); - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class AppMetadata { - ObjectMapper mapper = new ObjectMapper(); - @JsonProperty("datatools") - List datatools; - - public AppMetadata() { - } - - @JsonIgnore - public void setDatatoolsInfo(DatatoolsInfo datatools) { - for(int i = 0; i < this.datatools.size(); i++) { - if (this.datatools.get(i).clientId.equals(DataManager.config.get("auth0").get("clientId").asText())) { - this.datatools.set(i, datatools); - } - } - } - @JsonIgnore - public DatatoolsInfo getDatatoolsInfo() { - for(int i = 0; i < this.datatools.size(); i++) { - DatatoolsInfo dt = this.datatools.get(i); - if (dt.clientId.equals(DataManager.config.get("auth0").get("client_id").asText())) { - return dt; - } - } - return null; - } - } - public static class DatatoolsInfo { - @JsonProperty("client_id") - String clientId; - Project[] projects; - Permission[] permissions; - Subscription[] subscriptions; - - public DatatoolsInfo() { - } - - public DatatoolsInfo(String clientId, Project[] projects, Permission[] permissions, Subscription[] subscriptions) { - this.clientId = clientId; - this.projects = projects; - this.permissions = permissions; - this.subscriptions = subscriptions; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public void setProjects(Project[] projects) { - this.projects = projects; - } - - public void setPermissions(Permission[] permissions) { - this.permissions = permissions; - } - - public void setSubscriptions(Subscription[] subscriptions) { - this.subscriptions = subscriptions; - } - - public Subscription[] getSubscriptions() { return subscriptions; } - - } - - - public static class Project { - - String project_id; - Permission[] permissions; - String[] defaultFeeds; - - public Project() { - } - - public Project(String project_id, Permission[] permissions, String[] defaultFeeds) { - this.project_id = project_id; - this.permissions = permissions; - this.defaultFeeds = defaultFeeds; - } - - public void setProject_id(String project_id) { - this.project_id = project_id; - } - - public void setPermissions(Permission[] permissions) { this.permissions = permissions; } - - public void setDefaultFeeds(String[] defaultFeeds) { - this.defaultFeeds = defaultFeeds; - } - - } - - public static class Permission { - - String type; - String[] feeds; - - public Permission() { - } - - public Permission(String type, String[] feeds) { - this.type = type; - this.feeds = feeds; - } - - public void setType(String type) { - this.type = type; - } - - public void setFeeds(String[] feeds) { - this.feeds = feeds; - } - } - - public static class Subscription { - - String type; - String[] target; - - public Subscription() { - } - - public Subscription(String type, String[] target) { - this.type = type; - this.target = target; - } - - public void setType(String type) { - this.type = type; - } - - public String getType() { return type; } - - public void setTarget(String[] target) { - this.target = target; - } - - public String[] getTarget() { return target; } - } - - public int getProjectCount() { - return app_metadata.getDatatoolsInfo().projects.length; - } - - public boolean hasProject(String projectID) { - if(app_metadata.getDatatoolsInfo() == null || app_metadata.getDatatoolsInfo().projects == null) return false; - for(Project project : app_metadata.getDatatoolsInfo().projects) { - if (project.project_id.equals(projectID)) return true; - } - return false; - } - - public boolean canAdministerApplication() { - if(app_metadata.getDatatoolsInfo() != null && app_metadata.getDatatoolsInfo().permissions != null) { - for(Permission permission : app_metadata.getDatatoolsInfo().permissions) { - if(permission.type.equals("administer-application")) { - return true; - } - } - } - return false; - } - - public boolean canAdministerProject(String projectID) { - if(canAdministerApplication()) return true; - for(Project project : app_metadata.getDatatoolsInfo().projects) { - if (project.project_id.equals(projectID)) { - for(Permission permission : project.permissions) { - if(permission.type.equals("administer-project")) { - return true; - } - } - } - } - return false; - } - - public boolean canViewFeed(String projectID, String feedID) { - if (canAdministerApplication() || canAdministerProject(projectID)) { - return true; - } - for(Project project : app_metadata.getDatatoolsInfo().projects) { - if (project.project_id.equals(projectID)) { - return checkFeedPermission(project, feedID, "view-feed"); - } - } - return false; - } - - public boolean canManageFeed(String projectID, String feedID) { - if (canAdministerApplication() || canAdministerProject(projectID)) { - return true; - } - Project[] projectList = app_metadata.getDatatoolsInfo().projects; - for(Project project : projectList) { - System.out.println("project_id: " + project.project_id); - if (project.project_id.equals(projectID)) { - return checkFeedPermission(project, feedID, "manage-feed"); - } - } - return false; - } - - public boolean checkFeedPermission(Project project, String feedID, String permissionType) { - String feeds[] = project.defaultFeeds; - - // check for permission-specific feeds - for (Permission permission : project.permissions) { - if(permission.type.equals(permissionType)) { - if(permission.feeds != null) { - feeds = permission.feeds; - } - } - } - - for(String thisFeedID : feeds) { - if (thisFeedID.equals(feedID) || thisFeedID.equals("*")) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/auth/Auth0Users.java b/src/main/java/com/conveyal/datatools/manager/auth/Auth0Users.java deleted file mode 100644 index e39183970..000000000 --- a/src/main/java/com/conveyal/datatools/manager/auth/Auth0Users.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.conveyal.datatools.manager.auth; - -import com.conveyal.datatools.manager.DataManager; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; - -/** - * Created by landon on 4/26/16. - */ -public class Auth0Users { - private static String AUTH0_DOMAIN = DataManager.config.get("auth0").get("domain").asText(); - private static String AUTH0_API_TOKEN = DataManager.serverConfig.get("auth0").get("api_token").asText(); - private static ObjectMapper mapper = new ObjectMapper(); - - private static URI getUrl(String searchQuery, int page, int perPage, boolean includeTotals) { - String clientId = DataManager.config.get("auth0").get("client_id").asText(); - String defaultQuery = "app_metadata.datatools.client_id:" + clientId; - URIBuilder builder = new URIBuilder(); - builder.setScheme("https").setHost(AUTH0_DOMAIN).setPath("/api/v2/users"); - builder.setParameter("sort", "email:1"); - builder.setParameter("per_page", Integer.toString(perPage)); - builder.setParameter("page", Integer.toString(page)); - builder.setParameter("include_totals", Boolean.toString(includeTotals)); - if (searchQuery != null) { - builder.setParameter("search_engine", "v2"); - builder.setParameter("q", searchQuery + " AND " + defaultQuery); - } - else { - builder.setParameter("search_engine", "v2"); - builder.setParameter("q", defaultQuery); - } - - URI uri = null; - - try { - uri = builder.build(); - - } catch (URISyntaxException e) { - e.printStackTrace(); - return null; - } - - return uri; - } - - private static String doRequest(URI uri) { - System.out.println("Auth0 getUsers URL=" + uri); - String charset = "UTF-8"; - - HttpClient client = HttpClientBuilder.create().build(); - HttpGet request = new HttpGet(uri); - - request.addHeader("Authorization", "Bearer " + AUTH0_API_TOKEN); - request.setHeader("Accept-Charset", charset); - HttpResponse response = null; - - try { - response = client.execute(request); - } catch (IOException e) { - e.printStackTrace(); - } - - String result = null; - - try { - result = EntityUtils.toString(response.getEntity()); - } catch (IOException e) { - e.printStackTrace(); - } - - return result; - } - - public static String getAuth0Users(String searchQuery, int page) { - - URI uri = getUrl(searchQuery, page, 10, false); - return doRequest(uri); - } - - public static Auth0UserProfile getUserById(String id) { - - URIBuilder builder = new URIBuilder(); - builder.setScheme("https").setHost(AUTH0_DOMAIN).setPath("/api/v2/users/" + id); - URI uri = null; - try { - uri = builder.build(); - - } catch (URISyntaxException e) { - e.printStackTrace(); - return null; - } - String response = doRequest(uri); - Auth0UserProfile user = null; - try { - user = mapper.readValue(response, Auth0UserProfile.class); - } catch (IOException e) { - e.printStackTrace(); - } - return user; - } - - public static String getUsersBySubscription(String subscriptionType, String target) { - return getAuth0Users("app_metadata.datatools.subscriptions.type:" + subscriptionType + " AND app_metadata.datatools.subscriptions.target:" + target); - } - - public static String getAuth0Users(String queryString) { - return getAuth0Users(queryString, 0); - } - - public static JsonNode getAuth0UserCount(String searchQuery) throws IOException { - URI uri = getUrl(searchQuery, 0, 1, true); - String result = doRequest(uri); - JsonNode jsonNode = new ObjectMapper().readTree(result); - return jsonNode.get("total"); - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/DumpController.java b/src/main/java/com/conveyal/datatools/manager/controllers/DumpController.java deleted file mode 100644 index 929c4dc0f..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/DumpController.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.conveyal.datatools.manager.controllers; - -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.auth.Auth0Users; -import com.conveyal.datatools.manager.models.Deployment; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.models.Note; -import com.conveyal.datatools.manager.models.Project; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.conveyal.datatools.manager.utils.json.JsonUtil; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import java.io.IOException; -import java.net.URL; -import java.util.Collection; -import java.util.Date; -import java.util.Iterator; -import java.util.Map; - -import static spark.Spark.*; - -/** - * Created by demory on 5/25/16. - */ - -public class DumpController { - public static final Logger LOG = LoggerFactory.getLogger(DumpController.class); - /** - * Represents a snapshot of the database. This require loading the entire database into RAM. - * This shouldn't be an issue, though, as the feeds are stored separately. This is only metadata. - */ - public static class DatabaseState { - public Collection projects; - public Collection feedSources; - public Collection feedVersions; - public Collection notes; - // public Collection users; - public Collection deployments; - } - - private static JsonManager json = - new JsonManager(DatabaseState.class, JsonViews.DataDump.class); - - public static DatabaseState dump (Request req, Response res) throws JsonProcessingException { - DatabaseState db = new DatabaseState(); - db.projects = Project.getAll(); - db.feedSources = FeedSource.getAll(); - db.feedVersions = FeedVersion.getAll(); - db.notes = Note.getAll(); - db.deployments = Deployment.getAll(); - - return db; - } - - // this is not authenticated, because it has to happen with a bare database (i.e. no users) - // this method in particular is coded to allow up to 500MB of data to be posted -// @BodyParser.Of(value=BodyParser.Json.class, maxLength = 500 * 1024 * 1024) - public static boolean load (Request req, Response res) { - // TODO: really ought to check all tables - LOG.info("loading data..."); - DatabaseState db = null; - try { - db = json.read(req.body()); - LOG.info("data loaded successfully"); - } catch (IOException e) { - e.printStackTrace(); - LOG.error("data load error. check json validity."); - return false; - } - for (Project c : db.projects) { - LOG.info("loading project {}", c.id); - c.save(false); - } - Project.commit(); - - for (FeedSource s : db.feedSources) { - LOG.info("loading feed source {}", s.id); - s.save(false); - } - FeedSource.commit(); - - for (FeedVersion v : db.feedVersions) { - LOG.info("loading version {}", v.id); - v.save(false); - } - FeedVersion.commit(); - - for (Note n : db.notes) { - LOG.info("loading note {}", n.id); - n.save(false); - } - Note.commit(); - - for (Deployment d : db.deployments) { - LOG.info("loading deployment {}", d.id); - d.save(false); - } - Deployment.commit(); - LOG.info("load completed."); - return true; - } - - public static boolean loadLegacy (Request req, Response res) throws Exception { - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(req.body()); - - Iterator> fieldsIter = node.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - switch(entry.getKey()) { - case "feedCollections": - for(int i=0; i< entry.getValue().size(); i++) { - loadLegacyProject(entry.getValue().get(i)); - } - Project.commit(); - break; - case "feedSources": - for(int i=0; i< entry.getValue().size(); i++) { - loadLegacyFeedSource(entry.getValue().get(i)); - } - FeedSource.commit(); - break; - case "feedVersions": - for(int i=0; i< entry.getValue().size(); i++) { - loadLegacyFeedVersion(entry.getValue().get(i)); - } - FeedVersion.commit(); - break; - - } - } - return true; - } - - private static void loadLegacyProject (JsonNode node) { - System.out.println("load legacy project " + node.findValue("name")); - Project project = new Project(); - project.id = node.findValue("id").asText(); - project.name = node.findValue("name").asText(); - project.save(false); - } - - private static void loadLegacyFeedSource (JsonNode node) throws Exception { - System.out.println("load legacy FeedSource " + node.findValue("name")); - FeedSource fs = new FeedSource(); - fs.id = node.findValue("id").asText(); - fs.projectId = node.findValue("feedCollectionId").asText(); - fs.name = node.findValue("name").asText(); - switch(node.findValue("retrievalMethod").asText()) { - case "FETCHED_AUTOMATICALLY": - fs.retrievalMethod = FeedSource.FeedRetrievalMethod.FETCHED_AUTOMATICALLY; - break; - case "MANUALLY_UPLOADED": - fs.retrievalMethod = FeedSource.FeedRetrievalMethod.MANUALLY_UPLOADED; - break; - case "PRODUCED_IN_HOUSE": - fs.retrievalMethod = FeedSource.FeedRetrievalMethod.PRODUCED_IN_HOUSE; - break; - } - fs.snapshotVersion = node.findValue("snapshotVersion").asText(); - Object url = node.findValue("url").asText(); - fs.url = url != null && !url.equals("null") ? new URL(url.toString()) : null; - - //fs.lastFetched = new Date(node.findValue("lastFetched").asText()); - //System.out.println("wrote lastFetched"); - - fs.deployable = node.findValue("deployable").asBoolean(); - fs.isPublic = node.findValue("isPublic").asBoolean(); - fs.save(false); - } - - private static void loadLegacyFeedVersion (JsonNode node) throws Exception { - System.out.println("load legacy FeedVersion " + node.findValue("id")); - FeedVersion version = new FeedVersion(); - version.id = node.findValue("id").asText(); - version.version = node.findValue("version").asInt(); - version.feedSourceId = node.findValue("feedSourceId").asText(); - version.hash = node.findValue("hash").asText(); - version.updated = new Date(node.findValue("updated").asLong()); - System.out.println("updated= " + node.findValue("updated").asText()); - version.save(false); - } - - public static boolean validateAll (Request req, Response res) throws Exception { - System.out.println("validating all feeds..."); - for(FeedVersion version: FeedVersion.getAll()) { - if(!req.queryParams("force").equals("true") && version.validationResult != null) continue; - LOG.info("Validating " + version.id); - version.validate(); - version.save(); - } - return true; - } - - public static void register (String apiPrefix) { - post(apiPrefix + "loadLegacy", DumpController::loadLegacy, json::write); - post(apiPrefix + "load", DumpController::load, json::write); - post(apiPrefix + "validateAll", DumpController::validateAll, json::write); - get(apiPrefix + "dump", DumpController::dump, json::write); - System.out.println("registered dump w/ prefix " + apiPrefix); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/DeploymentController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/DeploymentController.java deleted file mode 100644 index f8608b1bf..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/DeploymentController.java +++ /dev/null @@ -1,323 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.jobs.DeployJob; -import com.conveyal.datatools.manager.models.Deployment; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.models.OtpServer; -import com.conveyal.datatools.manager.models.Project; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import static spark.Spark.*; -import static spark.Spark.get; - -/** - * Created by landon on 5/18/16. - */ -public class DeploymentController { - private static ObjectMapper mapper = new ObjectMapper(); - private static JsonManager json = - new JsonManager(Deployment.class, JsonViews.UserInterface.class); - - private static JsonManager statusJson = - new JsonManager(DeployJob.DeployStatus.class, JsonViews.UserInterface.class); - - public static final Logger LOG = LoggerFactory.getLogger(DeploymentController.class); - - private static HashMap deploymentJobsByServer = new HashMap(); - - public static Object getDeployment (Request req, Response res) { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - Deployment d = Deployment.get(id); - if (d == null) { - halt(400, "Deployment does not exist."); - return null; - } - if (!userProfile.canAdministerProject(d.projectId) && !userProfile.getUser_id().equals(d.getUser())) - halt(401); - else - return d; - - return null; - } - - public static Object deleteDeployment (Request req, Response res) { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - Deployment d = Deployment.get(id); - if (d == null) { - halt(400, "Deployment does not exist."); - return null; - } - if (!userProfile.canAdministerProject(d.projectId) && !userProfile.getUser_id().equals(d.getUser())) - halt(401); - else { - d.delete(); - return d; - } - - return null; - } - - /** Download all of the GTFS files in the feed */ - public static Object downloadDeployment (Request req, Response res) throws IOException { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - System.out.println(id); - Deployment d = Deployment.get(id); - - if (d == null) { - halt(400, "Deployment does not exist."); - return null; - } - - if (!userProfile.canAdministerProject(d.projectId) && !userProfile.getUser_id().equals(d.getUser())) - halt(401); - - File temp = File.createTempFile("deployment", ".zip"); - // just include GTFS, not any of the ancillary information - d.dump(temp, false, false, false); - - FileInputStream fis = new FileInputStream(temp); - - res.type("application/zip"); - res.header("Content-Disposition", "attachment;filename=" + d.name.replaceAll("[^a-zA-Z0-9]", "") + ".zip"); - - // will not actually be deleted until download has completed - // http://stackoverflow.com/questions/24372279 - temp.delete(); - - return fis; - } - - public static Object getAllDeployments (Request req, Response res) throws JsonProcessingException { - Auth0UserProfile userProfile = req.attribute("user"); - String projectId = req.queryParams("projectId"); - System.out.println("getting deployments..."); - if (!userProfile.canAdministerProject(projectId)) - halt(401); - - if (projectId != null) { - Project p = Project.get(projectId); - return p.getProjectDeployments(); - } - else { - return Deployment.getAll(); - } - } - - public static Object createDeployment (Request req, Response res) throws IOException { - Auth0UserProfile userProfile = req.attribute("user"); - JsonNode params = mapper.readTree(req.body()); - - // find the project - Project p = Project.get(params.get("projectId").asText()); - - if (!userProfile.canAdministerProject(p.id)) - halt(401); - - Deployment d = new Deployment(p); - d.setUser(userProfile); - - applyJsonToDeployment(d, params); - - d.save(); - - return d; - } - - /** - * Create a deployment for a particular feedsource - * @throws JsonProcessingException - */ - public static Object createDeploymentFromFeedSource (Request req, Response res) throws JsonProcessingException { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - FeedSource s = FeedSource.get(id); - - // three ways to have permission to do this: - // 1) be an admin - // 2) be the autogenerated user associated with this feed - // 3) have access to this feed through project permissions - // if all fail, the user cannot do this. - if ( - !userProfile.canAdministerProject(s.projectId) - && !userProfile.getUser_id().equals(s.getUser()) -// && !userProfile.hasWriteAccess(s.id) - ) - halt(401); - - // never loaded - if (s.getLatestVersionId() == null) - halt(400); - - Deployment d = new Deployment(s); - d.setUser(userProfile); - d.save(); - - return d; - } - -// @BodyParser.Of(value=BodyParser.Json.class, maxLength=1024*1024) - public static Object updateDeployment (Request req, Response res) throws IOException { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - Deployment d = Deployment.get(id); - - if (d == null) - halt(404); - - if (!userProfile.canAdministerProject(d.projectId) && !userProfile.getUser_id().equals(d.getUser())) - halt(401); - - JsonNode params = mapper.readTree(req.body()); - applyJsonToDeployment(d, params); - - d.save(); - - return d; - } - - /** - * Apply JSON params to a deployment. - * @param d - * @param params - */ - private static void applyJsonToDeployment(Deployment d, JsonNode params) { - Iterator> fieldsIter = params.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - if (entry.getKey() == "feedVersions") { - JsonNode versions = entry.getValue(); - ArrayList versionsToInsert = new ArrayList(versions.size()); - for (JsonNode version : versions) { - if (!version.has("id")) { - halt(400, "Version not supplied"); - } - FeedVersion v = FeedVersion.get(version.get("id").asText()); - if (v == null) { - halt(404, "Version not found"); - } - if (v.getFeedSource().projectId.equals(d.projectId)) { - versionsToInsert.add(v); - } - } - - d.setFeedVersions(versionsToInsert); - } - if (entry.getKey() == "name") { - d.name = entry.getValue().asText(); - } - } - } - - /** - * Create a deployment bundle, and push it to OTP - * @throws IOException - */ - public static Object deploy (Request req, Response res) throws IOException { - Auth0UserProfile userProfile = req.attribute("user"); - String target = req.params("target"); - String id = req.params("id"); - Deployment d = Deployment.get(id); - Project p = Project.get(d.projectId); - - if (!userProfile.canAdministerProject(d.projectId) && !userProfile.getUser_id().equals(d.getUser())) - halt(401); - - if (!userProfile.canAdministerProject(d.projectId) && p.getServer(target).admin) - halt(401); - - // check if we can deploy - if (deploymentJobsByServer.containsKey(target)) { - DeployJob currentJob = deploymentJobsByServer.get(target); - if (currentJob != null && !currentJob.getStatus().completed) { - // send a 503 service unavailable as it is not possible to deploy to this target right now; - // someone else is deploying - halt(503, "Deployment currently in progress for target: " + target); - LOG.warn("Deployment currently in progress for target: " + target); - } - } - OtpServer otpServer = p.getServer(target); - List targetUrls = otpServer.internalUrl; - - Deployment oldD = Deployment.getDeploymentForServerAndRouterId(target, d.routerId); - if (oldD != null) { - oldD.deployedTo = null; - oldD.save(); - } - - d.deployedTo = target; - d.save(); - - DeployJob job = new DeployJob(d, userProfile.getUser_id(), targetUrls, p.getServer(target).publicUrl, p.getServer(target).s3Bucket, p.getServer(target).s3Credentials); - deploymentJobsByServer.put(target, job); - - Thread tnThread = new Thread(job); - tnThread.start(); - - halt(200, "{status: \"ok\"}"); - return null; - } - - /** - * The current status of a deployment, polled to update the progress dialog. - * @throws JsonProcessingException - */ - public static Object deploymentStatus (Request req, Response res) throws JsonProcessingException { - // this is not access-controlled beyond requiring auth, which is fine - // there's no good way to know who should be able to see this. - String target = req.queryParams("target"); - - if (!deploymentJobsByServer.containsKey(target)) - halt(404, "Deployment target '"+target+"' not found"); - - DeployJob j = deploymentJobsByServer.get(target); - - if (j == null) - halt(404, "No active job for " + target + " found"); - - return j.getStatus(); - } - - /** - * The servers that it is possible to deploy to. - */ -// public static Object deploymentTargets (Request req, Response res) { -// Auth0UserProfile userProfile = req.attribute("user"); -// return DeploymentManager.getDeploymentNames(userProfile.canAdministerApplication()); -// } - - public static void register (String apiPrefix) { - post(apiPrefix + "secure/deployments/:id/deploy/:target", DeploymentController::deploy, json::write); - options(apiPrefix + "secure/deployments", (q, s) -> ""); - get(apiPrefix + "secure/deployments/status/:target", DeploymentController::deploymentStatus, json::write); -// get(apiPrefix + "secure/deployments/targets", DeploymentController::deploymentTargets, json::write); - get(apiPrefix + "secure/deployments/:id/download", DeploymentController::downloadDeployment); - get(apiPrefix + "secure/deployments/:id", DeploymentController::getDeployment, json::write); - delete(apiPrefix + "secure/deployments/:id", DeploymentController::deleteDeployment, json::write); - get(apiPrefix + "secure/deployments", DeploymentController::getAllDeployments, json::write); - post(apiPrefix + "secure/deployments", DeploymentController::createDeployment, json::write); - put(apiPrefix + "secure/deployments/:id", DeploymentController::updateDeployment, json::write); - post(apiPrefix + "secure/deployments/fromfeedsource/:id", DeploymentController::createDeploymentFromFeedSource, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/FeedSourceController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/FeedSourceController.java deleted file mode 100644 index d7ba6db0f..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/FeedSourceController.java +++ /dev/null @@ -1,303 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.jobs.FetchSingleFeedJob; -import com.conveyal.datatools.manager.jobs.NotifyUsersForSubscriptionJob; -import com.conveyal.datatools.manager.models.*; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.conveyal.datatools.manager.utils.json.JsonUtil; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import spark.Request; -import spark.Response; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; - -import static com.conveyal.datatools.manager.auth.Auth0Users.getUserById; -import static spark.Spark.*; - -/** - * Created by demory on 3/21/16. - */ - -public class FeedSourceController { - - public static JsonManager json = - new JsonManager<>(FeedSource.class, JsonViews.UserInterface.class); - private static ObjectMapper mapper = new ObjectMapper(); - public static FeedSource getFeedSource(Request req, Response res) { - String id = req.params("id"); - Boolean publicFilter = req.pathInfo().contains("public"); - FeedSource fs = FeedSource.get(id); - if (fs == null) { - halt(404, null); - } - if (publicFilter && !fs.isPublic) { - halt(503); - } - return fs; - } - - public static Collection getAllFeedSources(Request req, Response res) { - Collection sources = new ArrayList<>(); - Auth0UserProfile requestingUser = req.attribute("user"); - System.out.println(requestingUser.getEmail()); - String projectId = req.queryParams("projectId"); - Boolean publicFilter = req.pathInfo().contains("public"); - String userId = req.queryParams("userId"); - - if (projectId != null) { - for (FeedSource source: FeedSource.getAll()) { - if ( - source != null && source.projectId != null && source.projectId.equals(projectId) - && requestingUser != null && (requestingUser.canManageFeed(source.projectId, source.id) || requestingUser.canViewFeed(source.projectId, source.id)) - ) { - // if requesting public sources and source is not public; skip source - if (publicFilter && !source.isPublic) - continue; - sources.add(source); - } - } - } - // request feed sources a specified user has permissions for - else if (userId != null) { - Auth0UserProfile user = getUserById(userId); - if (user == null) return sources; - - for (FeedSource source: FeedSource.getAll()) { - if ( - source != null && source.projectId != null && - (user.canManageFeed(source.projectId, source.id) || user.canViewFeed(source.projectId, source.id)) - ) { - - sources.add(source); - } - } - } - // request feed sources that are public - else { - for (FeedSource source: FeedSource.getAll()) { - // if user is logged in and cannot view feed; skip source - if ((requestingUser != null && !requestingUser.canManageFeed(source.projectId, source.id) && !requestingUser.canViewFeed(source.projectId, source.id))) - continue; - - // if requesting public sources and source is not public; skip source - if (publicFilter && !source.isPublic) - continue; - sources.add(source); - } - } - - return sources; - } - - public static FeedSource createFeedSource(Request req, Response res) throws IOException { - FeedSource source; - /*if (req.queryParams("type") != null){ - //FeedSource.FeedSourceType type = FeedSource.FeedSourceType.TRANSITLAND; - source = new FeedSource("onestop-id"); - applyJsonToFeedSource(source, req.body()); - source.save(); - - return source; - } - else { - source = new FeedSource(); - - }*/ - - source = new FeedSource(); - - applyJsonToFeedSource(source, req.body()); - source.save(); - - for(String resourceType : DataManager.feedResources.keySet()) { - DataManager.feedResources.get(resourceType).feedSourceCreated(source, req.headers("Authorization")); - } - - return source; - } - - public static FeedSource updateFeedSource(Request req, Response res) throws IOException { - String id = req.params("id"); - FeedSource source = FeedSource.get(id); - if (source == null) { - halt(404); - } - - applyJsonToFeedSource(source, req.body()); - source.save(); - - // notify users after successful save - NotifyUsersForSubscriptionJob notifyFeedJob = new NotifyUsersForSubscriptionJob("feed-updated", id, "Feed property updated for " + source.name); - Thread notifyThread = new Thread(notifyFeedJob); - notifyThread.start(); - - NotifyUsersForSubscriptionJob notifyProjectJob = new NotifyUsersForSubscriptionJob("project-updated", source.projectId, "Project updated (feed source property for " + source.name + ")"); - Thread notifyProjectThread = new Thread(notifyProjectJob); - notifyProjectThread.start(); - - return source; - } - - public static void applyJsonToFeedSource(FeedSource source, String json) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(json); - Iterator> fieldsIter = node.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - - if(entry.getKey().equals("projectId")) { - System.out.println("setting fs project"); - source.setProject(Project.get(entry.getValue().asText())); - } - - if(entry.getKey().equals("name")) { - source.name = entry.getValue().asText(); - } - - if(entry.getKey().equals("url")) { - String url = entry.getValue().asText(); - try { - source.url = new URL(url); - - // reset the last fetched date so it can be fetched again - source.lastFetched = null; - - } catch (MalformedURLException e) { - halt(400, "URL '" + url + "' not valid."); - } - - } - - if(entry.getKey().equals("retrievalMethod")) { - source.retrievalMethod = FeedSource.FeedRetrievalMethod.FETCHED_AUTOMATICALLY.valueOf(entry.getValue().asText()); - } - - if(entry.getKey().equals("snapshotVersion")) { - source.snapshotVersion = entry.getValue().asText(); - } - - if(entry.getKey().equals("isPublic")) { - source.isPublic = entry.getValue().asBoolean(); - } - - if(entry.getKey().equals("deployable")) { - source.deployable = entry.getValue().asBoolean(); - } - - } - } - - public static FeedSource updateExternalFeedResource(Request req, Response res) throws IOException { - String id = req.params("id"); - FeedSource source = FeedSource.get(id); - String resourceType = req.queryParams("resourceType"); - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(req.body()); - Iterator> fieldsIter = node.fields(); - - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - ExternalFeedSourceProperty prop = - ExternalFeedSourceProperty.find(source, resourceType, entry.getKey()); - - if (prop != null) { - // update the property in our DB - String previousValue = prop.value; - prop.value = entry.getValue().asText(); - prop.save(); - - // trigger an event on the external resource - if(DataManager.feedResources.containsKey(resourceType)) { - DataManager.feedResources.get(resourceType).propertyUpdated(prop, previousValue, req.headers("Authorization")); - } - - } - - } - - return source; - } - - public static FeedSource deleteFeedSource(Request req, Response res) { - String id = req.params("id"); - FeedSource source = FeedSource.get(id); - - if (source != null) { - source.delete(); - return source; - } - else { - halt(404); - return null; - } - } - - /** - * Refetch this feed - * @throws JsonProcessingException - */ - public static FeedVersion fetch (Request req, Response res) throws JsonProcessingException { - Auth0UserProfile userProfile = req.attribute("user"); - FeedSource s = FeedSource.get(req.params("id")); - - System.out.println("fetching feed for source "+ s.name); - // ways to have permission to do this: - // 1) be an admin - // 2) have access to this feed through project permissions - // if all fail, the user cannot do this. - - if (!userProfile.canAdministerProject(s.projectId) && !userProfile.canManageFeed(s.projectId, s.id)) - halt(401); - - FetchSingleFeedJob job = new FetchSingleFeedJob(s, userProfile.getUser_id()); - job.run(); -// if (job.getStatus().error) { -// halt(304); -// } - return job.result; -// return true; - } - -// /** -// * The current status of a deployment, polled to update the progress dialog. -// * @throws JsonProcessingException -// */ -// public static Object fetchFeedStatus (Request req, Response res) throws JsonProcessingException { -// // this is not access-controlled beyond requiring auth, which is fine -// // there's no good way to know who should be able to see this. -// String id = req.params("id"); -// fetchJobsByFeed.forEach((s, deployJob) -> System.out.println(s)); -// if (!fetchJobsByFeed.containsKey(id)) -// halt(404, "Feed source id '"+id+"' not found"); -// -// FetchSingleFeedJob j = fetchJobsByFeed.get(id); -// -// if (j == null) -// halt(404, "No active job for " + id + " found"); -// -// return j.getStatus(); -// } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/feedsource/:id", FeedSourceController::getFeedSource, json::write); - options(apiPrefix + "secure/feedsource", (q, s) -> ""); -// get(apiPrefix + "secure/feedsource/:id/status", FeedSourceController::fetchFeedStatus, json::write); - get(apiPrefix + "secure/feedsource", FeedSourceController::getAllFeedSources, json::write); - post(apiPrefix + "secure/feedsource", FeedSourceController::createFeedSource, json::write); - put(apiPrefix + "secure/feedsource/:id", FeedSourceController::updateFeedSource, json::write); - put(apiPrefix + "secure/feedsource/:id/updateExternal", FeedSourceController::updateExternalFeedResource, json::write); - delete(apiPrefix + "secure/feedsource/:id", FeedSourceController::deleteFeedSource, json::write); - post(apiPrefix + "secure/feedsource/:id/fetch", FeedSourceController::fetch, JsonUtil.objectMapper::writeValueAsString); - - // Public routes - get(apiPrefix + "public/feedsource/:id", FeedSourceController::getFeedSource, json::write); - get(apiPrefix + "public/feedsource", FeedSourceController::getAllFeedSources, json::write); - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/FeedVersionController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/FeedVersionController.java deleted file mode 100644 index cff912389..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/FeedVersionController.java +++ /dev/null @@ -1,489 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.S3Object; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.jobs.BuildTransportNetworkJob; -import com.conveyal.datatools.manager.jobs.CreateFeedVersionFromSnapshotJob; -import com.conveyal.datatools.manager.jobs.ProcessSingleFeedJob; -import com.conveyal.datatools.manager.models.FeedDownloadToken; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.conveyal.r5.analyst.PointSet; -import com.conveyal.r5.analyst.cluster.AnalystClusterRequest; -import com.conveyal.r5.analyst.cluster.ResultEnvelope; -import com.conveyal.r5.analyst.cluster.TaskStatistics; -import com.conveyal.r5.api.util.LegMode; -import com.conveyal.r5.api.util.TransitModes; -import com.conveyal.r5.profile.ProfileRequest; -import com.conveyal.r5.profile.RepeatedRaptorProfileRouter; -import com.conveyal.r5.profile.StreetMode; -import com.conveyal.r5.streets.LinkedPointSet; -import com.conveyal.r5.transit.TransportNetwork; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.*; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import javax.servlet.MultipartConfigElement; -import javax.servlet.ServletException; -import javax.servlet.http.Part; - -import static spark.Spark.*; - -public class FeedVersionController { - - public static final Logger LOG = LoggerFactory.getLogger(FeedVersionController.class); - private static ObjectMapper mapper = new ObjectMapper(); - public static JsonManager json = - new JsonManager(FeedVersion.class, JsonViews.UserInterface.class); - - /** - * Grab this feed version. - * If you pass in ?summarized=true, don't include the full tree of validation results, only the counts. - */ - public static FeedVersion getFeedVersion (Request req, Response res) throws JsonProcessingException { - String id = req.params("id"); - if (id == null) { - halt(404, "Must specify feed version ID"); - } - FeedVersion v = FeedVersion.get(id); - - if (v == null) { - halt(404, "Version ID does not exist"); - } - FeedSource s = v.getFeedSource(); - - return v; - // ways to have permission to do this: - // 1) be an admin - // 2) have access to this feed through project permissions - /*if (userProfile.canAdministerProject(s.feedCollectionId) || userProfile.canViewFeed(s.feedCollectionId, s.id)) { - if ("true".equals(request().getQueryString("summarized"))) { - return ok(jsonSummarized.write(new SummarizedFeedVersion(v))).as("application/json"); - } - else { - return ok(json.write(v)).as("application/json"); - } - } - else { - return unauthorized(); - }*/ - } - - /** - * Grab this feed version's GTFS. - */ - /*public static Result getGtfs (String id) throws JsonProcessingException { - Auth0UserProfile userProfile = getSessionProfile(); - if(userProfile == null) return unauthorized(); - - FeedVersion v = FeedVersion.get(id); - FeedSource s = v.getFeedSource(); - - if (userProfile.canAdministerProject(s.feedCollectionId) || userProfile.canViewFeed(s.feedCollectionId, s.id)) { - return ok(v.getGtfsFile()); - } - else { - return unauthorized(); - } - }*/ - - - public static Collection getAllFeedVersions (Request req, Response res) throws JsonProcessingException { - - // parse the query parameters - String sourceId = req.queryParams("feedSourceId"); - if (sourceId == null) { - halt("Please specify a feedsource"); - } - Boolean publicFilter = Boolean.valueOf(req.queryParams("public")); - - FeedSource s = FeedSource.get(sourceId); - - Collection versions = new ArrayList<>(); - - for (FeedVersion v : s.getFeedVersions()){ - // if requesting public sources and source is not public; skip source - if (publicFilter && !s.isPublic) - continue; - versions.add(v); - } - - return versions; - } - - - /** - * Upload a feed version directly. This is done behind Backbone's back, and as such uses - * x-multipart-formdata rather than a json blob. This is done because uploading files in a JSON - * blob is not pretty, and we don't really need to get the Backbone object directly; page re-render isn't - * a problem. - * @return - * @throws JsonProcessingException - */ - public static Boolean createFeedVersion (Request req, Response res) throws IOException, ServletException { - - Auth0UserProfile userProfile = req.attribute("user"); - FeedSource s = FeedSource.get(req.queryParams("feedSourceId")); - - if (FeedSource.FeedRetrievalMethod.FETCHED_AUTOMATICALLY.equals(s.retrievalMethod)) - halt(400, "Feed is autofetched! Cannot upload."); - - FeedVersion v = new FeedVersion(s); - //v.setUser(req.attribute("auth0User")); - - if (req.raw().getAttribute("org.eclipse.jetty.multipartConfig") == null) { - MultipartConfigElement multipartConfigElement = new MultipartConfigElement(System.getProperty("java.io.tmpdir")); - req.raw().setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement); - } - - Part part = req.raw().getPart("file"); - - LOG.info("Saving feed {} from upload", s); - - - InputStream uploadStream; - try { - uploadStream = part.getInputStream(); - v.newGtfsFile(uploadStream); - } catch (Exception e) { - LOG.error("Unable to open input stream from upload"); - halt(400, "Unable to read uploaded feed"); - } - - v.hash(); - - FeedVersion latest = s.getLatest(); - if (latest != null && latest.hash.equals(v.hash)) { - v.getGtfsFile().delete(); - // Uploaded feed is same as latest version - halt(304); - } - - v.name = v.getFormattedTimestamp() + " Upload"; - - v.save(); - new ProcessSingleFeedJob(v, userProfile.getUser_id()).run(); - - /*if (DataManager.config.get("modules").get("validator").get("enabled").asBoolean()) { - BuildTransportNetworkJob btnj = new BuildTransportNetworkJob(v); - Thread tnThread = new Thread(btnj); - tnThread.start(); - }*/ - - return true; - } - - public static Boolean createFeedVersionFromSnapshot (Request req, Response res) throws IOException, ServletException { - - Auth0UserProfile userProfile = req.attribute("user"); - FeedSource s = FeedSource.get(req.queryParams("feedSourceId")); - - CreateFeedVersionFromSnapshotJob createFromSnapshotJob = - new CreateFeedVersionFromSnapshotJob(s, req.queryParams("snapshotId"), userProfile.getUser_id()); - - new Thread(createFromSnapshotJob).start(); - - return true; - } - - public static FeedVersion deleteFeedVersion(Request req, Response res) { - String id = req.params("id"); - FeedVersion version = FeedVersion.get(id); - version.delete(); - - // renumber the versions - Collection versions = version.getFeedSource().getFeedVersions(); - FeedVersion[] versionArray = versions.toArray(new FeedVersion[versions.size()]); - Arrays.sort(versionArray, (v1, v2) -> v1.updated.compareTo(v2.updated)); - for(int i = 0; i < versionArray.length; i++) { - FeedVersion v = versionArray[i]; - v.version = i + 1; - v.save(); - } - - return version; - } - - public static Object getValidationResult(Request req, Response res) { - return getValidationResult(req, res, false); - } - - public static Object getPublicValidationResult(Request req, Response res) { - return getValidationResult(req, res, true); - } - - public static Object getValidationResult(Request req, Response res, boolean checkPublic) { - String id = req.params("id"); - FeedVersion version = FeedVersion.get(id); - - if(checkPublic && !version.getFeedSource().isPublic) { - halt(401, "Not a public feed"); - return null; - } - - - // TODO: separate this out if non s3 bucket - String s3Bucket = DataManager.config.get("application").get("data").get("gtfs_s3_bucket").asText(); - String keyName = "validation/" + version.id + ".json"; - JsonNode n = null; - InputStream objectData = null; - if (s3Bucket != null && DataManager.getConfigProperty("application.data.use_s3_storage").asBoolean() == true) { - AWSCredentials creds; - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - try { - LOG.info("Getting validation results from s3"); - AmazonS3 s3Client = new AmazonS3Client(new ProfileCredentialsProvider()); - S3Object object = s3Client.getObject( - new GetObjectRequest(s3Bucket, keyName)); - objectData = object.getObjectContent(); - } catch (AmazonS3Exception e) { - // if json file does not exist, validate feed. - version.validate(); - version.save(); - halt(503, "Try again later. Validating feed"); - } catch (AmazonServiceException ase) { - LOG.error("Error downloading from s3"); - ase.printStackTrace(); - } - - } - // if s3 upload set to false - else { - File file = new File(DataManager.getConfigPropertyAsText("application.data.gtfs") + "/validation/" + version.id + ".json"); - try { - objectData = new FileInputStream(file); -// } catch (IOException e) { -// e.printStackTrace(); - } catch (Exception e) { - LOG.warn("Validation does not exist. Validating feed."); - version.validate(); - version.save(); - halt(503, "Try again later. Validating feed"); - } - } - - // Process the objectData stream. - try { - n = mapper.readTree(objectData); - if (!n.has("errors") || !n.has("tripsPerDate")) { - throw new Exception("Validation for feed version not up to date"); - } - - return n; - } catch (IOException e) { - // if json file does not exist, validate feed. - version.validate(); - version.save(); - halt(503, "Try again later. Validating feed"); - } catch (Exception e) { - e.printStackTrace(); - version.validate(); - version.save(); - halt(503, "Try again later. Validating feed"); - } - - return null; - } - - public static Object getIsochrones(Request req, Response res) { - LOG.info(req.uri()); - Auth0UserProfile userProfile = req.attribute("user"); - - String id = req.params("id"); - FeedVersion version = FeedVersion.get(id); - Double fromLat = Double.valueOf(req.queryParams("fromLat")); - Double fromLon = Double.valueOf(req.queryParams("fromLon")); - Double toLat = Double.valueOf(req.queryParams("toLat")); - Double toLon = Double.valueOf(req.queryParams("toLon")); - - if (version.transportNetwork == null) { - InputStream is = null; - try { - is = new FileInputStream(DataManager.config.get("application").get("data").get("gtfs").asText() + "/" + version.feedSourceId + "/" + version.id + "_network.dat"); - version.transportNetwork = TransportNetwork.read(is); - } - // Catch if transport network not built yet - catch (Exception e) { - e.printStackTrace(); - if (DataManager.config.get("modules").get("validator").get("enabled").asBoolean()) { -// new BuildTransportNetworkJob(version).run(); - BuildTransportNetworkJob btnj = new BuildTransportNetworkJob(version, userProfile.getUser_id()); - Thread tnThread = new Thread(btnj); - tnThread.start(); - } - halt(503, "Try again later. Building transport network"); - } - } - - if (version.transportNetwork != null) { - AnalystClusterRequest clusterRequest = new AnalystClusterRequest(); - clusterRequest.profileRequest = new ProfileRequest(); - clusterRequest.profileRequest.transitModes = EnumSet.of(TransitModes.TRANSIT); - clusterRequest.profileRequest.accessModes = EnumSet.of(LegMode.WALK); - clusterRequest.profileRequest.date = LocalDate.now(); - clusterRequest.profileRequest.fromLat = fromLat; - clusterRequest.profileRequest.fromLon = fromLon; - clusterRequest.profileRequest.toLat = toLat; - clusterRequest.profileRequest.toLon = toLon; - clusterRequest.profileRequest.fromTime = 9*3600; - clusterRequest.profileRequest.toTime = 10*3600; - clusterRequest.profileRequest.egressModes = EnumSet.of(LegMode.WALK); - clusterRequest.profileRequest.zoneId = ZoneId.of("America/New_York"); - PointSet targets = version.transportNetwork.getGridPointSet(); - StreetMode mode = StreetMode.WALK; - final LinkedPointSet linkedTargets = targets.link(version.transportNetwork.streetLayer, mode); - RepeatedRaptorProfileRouter router = new RepeatedRaptorProfileRouter(version.transportNetwork, clusterRequest, linkedTargets, new TaskStatistics()); - ResultEnvelope result = router.route(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - JsonGenerator jgen = new JsonFactory().createGenerator(out); - jgen.writeStartObject(); - result.avgCase.writeIsochrones(jgen); - jgen.writeEndObject(); - jgen.close(); - out.close(); - String outString = new String( out.toByteArray(), StandardCharsets.UTF_8 ); - //System.out.println(outString); - ObjectMapper mapper = new ObjectMapper(); - return mapper.readTree(outString); - } catch (IOException e) { - e.printStackTrace(); - } - - - } - return null; - } - - private static Object downloadFeedVersion(FeedVersion version, Response res) { - if(version == null) halt(500, "FeedVersion is null"); - - File file = version.getGtfsFile(); - - res.raw().setContentType("application/octet-stream"); - res.raw().setHeader("Content-Disposition", "attachment; filename=" + file.getName()); - - try { - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(res.raw().getOutputStream()); - BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(file)); - - byte[] buffer = new byte[1024]; - int len; - while ((len = bufferedInputStream.read(buffer)) > 0) { - bufferedOutputStream.write(buffer, 0, len); - } - - bufferedOutputStream.flush(); - bufferedOutputStream.close(); - } catch (Exception e) { - halt(500, "Error serving GTFS file"); - } - - return res.raw(); - } - - public static Boolean renameFeedVersion (Request req, Response res) throws JsonProcessingException { - String id = req.params("id"); - if (id == null) { - halt(404, "Must specify feed version ID"); - } - FeedVersion v = FeedVersion.get(id); - - if (v == null) { - halt(404, "Version ID does not exist"); - } - - String name = req.queryParams("name"); - if (name == null) { - halt(400, "Name parameter not specified"); - } - - v.name = name; - v.save(); - return true; - } - - private static Object downloadFeedVersionDirectly(Request req, Response res) { - FeedVersion version = FeedVersion.get(req.params("id")); - return downloadFeedVersion(version, res); - } - - private static FeedDownloadToken getDownloadToken (Request req, Response res) { - FeedVersion version = FeedVersion.get(req.params("id")); - FeedDownloadToken token = new FeedDownloadToken(version); - token.save(); - return token; - } - - private static FeedDownloadToken getPublicDownloadToken (Request req, Response res) { - FeedVersion version = FeedVersion.get(req.params("id")); - if(!version.getFeedSource().isPublic) { - halt(401, "Not a public feed"); - return null; - } - FeedDownloadToken token = new FeedDownloadToken(version); - token.save(); - return token; - } - - - private static Object downloadFeedVersionWithToken (Request req, Response res) { - FeedDownloadToken token = FeedDownloadToken.get(req.params("token")); - - if(token == null || !token.isValid()) { - halt(400, "Feed download token not valid"); - } - - FeedVersion version = token.getFeedVersion(); - - token.delete(); - - return downloadFeedVersion(version, res); - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/feedversion/:id", FeedVersionController::getFeedVersion, json::write); - get(apiPrefix + "secure/feedversion/:id/download", FeedVersionController::downloadFeedVersionDirectly); - get(apiPrefix + "secure/feedversion/:id/downloadtoken", FeedVersionController::getDownloadToken, json::write); - get(apiPrefix + "secure/feedversion/:id/validation", FeedVersionController::getValidationResult, json::write); - get(apiPrefix + "secure/feedversion/:id/isochrones", FeedVersionController::getIsochrones, json::write); - get(apiPrefix + "secure/feedversion", FeedVersionController::getAllFeedVersions, json::write); - post(apiPrefix + "secure/feedversion", FeedVersionController::createFeedVersion, json::write); - post(apiPrefix + "secure/feedversion/fromsnapshot", FeedVersionController::createFeedVersionFromSnapshot, json::write); - put(apiPrefix + "secure/feedversion/:id/rename", FeedVersionController::renameFeedVersion, json::write); - delete(apiPrefix + "secure/feedversion/:id", FeedVersionController::deleteFeedVersion, json::write); - - get(apiPrefix + "public/feedversion", FeedVersionController::getAllFeedVersions, json::write); - get(apiPrefix + "public/feedversion/:id/validation", FeedVersionController::getPublicValidationResult, json::write); - get(apiPrefix + "public/feedversion/:id/downloadtoken", FeedVersionController::getPublicDownloadToken, json::write); - - get(apiPrefix + "downloadfeed/:token", FeedVersionController::downloadFeedVersionWithToken); - - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/GtfsApiController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/GtfsApiController.java deleted file mode 100644 index 00c104a6a..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/GtfsApiController.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectListing; -import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.utils.FeedUpdater; -import com.conveyal.gtfs.api.ApiMain; -import com.conveyal.gtfs.api.Routes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Created by landon on 4/12/16. - */ -public class GtfsApiController { - public static final Logger LOG = LoggerFactory.getLogger(ProjectController.class); - public static String feedBucket; - public static FeedUpdater feedUpdater; - private static AmazonS3Client s3 = new AmazonS3Client(); - public static ApiMain gtfsApi; - public static String directory; - public static void register (String apiPrefix) throws IOException { - - // store list of GTFS feed eTags here - List eTagList = new ArrayList<>(); - - // check for use extension... - String extensionType = DataManager.getConfigPropertyAsText("modules.gtfsapi.use_extension"); - - if ("mtc".equals(extensionType)){ - LOG.info("Using extension " + extensionType + " for service alerts module"); - feedBucket = DataManager.getConfigPropertyAsText("extensions." + extensionType + ".s3_bucket"); - directory = DataManager.getConfigPropertyAsText("extensions." + extensionType + ".s3_download_prefix"); - - gtfsApi.initialize(feedBucket, directory); - - eTagList.addAll(registerS3Feeds(feedBucket, directory)); - - // set feedUpdater to poll for new feeds at specified frequency (in seconds) - feedUpdater = new FeedUpdater(eTagList, 0, DataManager.getConfigProperty("modules.gtfsapi.update_frequency").asInt()); - } - // if not using MTC extension - else if ("true".equals(DataManager.getConfigPropertyAsText("modules.gtfsapi.enabled"))) { - LOG.warn("No extension provided for GTFS API"); - if ("true".equals(DataManager.getConfigPropertyAsText("application.data.use_s3_storage"))) { - feedBucket = DataManager.getConfigPropertyAsText("application.data.gtfs_s3_bucket"); - directory = "gtfs/cache/"; - } - else { - feedBucket = null; - directory = DataManager.getConfigPropertyAsText("application.data.gtfs") + "/cache/api/"; - File dir = new File(directory); - if (!dir.isDirectory()) { - dir.mkdirs(); - } - } - gtfsApi.initialize(feedBucket, directory); - } - - // check for load on startup - if ("true".equals(DataManager.getConfigPropertyAsText("modules.gtfsapi.load_on_startup"))) { - LOG.warn("Loading all feeds into gtfs api (this may take a while)..."); - // use s3 - if ("true".equals(DataManager.getConfigPropertyAsText("application.data.use_s3_storage"))) { - eTagList.addAll(registerS3Feeds(feedBucket, directory)); - - // set feedUpdater to poll for new feeds at specified frequency (in seconds) - feedUpdater = new FeedUpdater(eTagList, 0, DataManager.config.get("modules").get("gtfsapi").get("update_frequency").asInt()); - } -// else, use local directory - else { - String dir = DataManager.config.get("application").get("data").get("gtfs").asText(); - gtfsApi.initialize(null, dir); - - // iterate over latest feed versions - for (FeedSource fs : FeedSource.getAll()) { - FeedVersion v = fs.getLatest(); - if (v != null) { - try { - gtfsApi.registerFeedSource(fs.id, v.getGtfsFile()); - eTagList.add(v.hash); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - } - - // set gtfs-api routes with apiPrefix - Routes.routes(apiPrefix); - } - -// public static void registerFeedSourceLatest(String id) { -// FeedSource fs = FeedSource.get(id); -// FeedVersion v = fs.getLatest(); -// if (v != null) { -// try { -// gtfsApi.registerFeedSource(id, v.getGtfsFile()); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } -// } - - public static List registerS3Feeds (String bucket, String dir) { - List eTags = new ArrayList<>(); - // iterate over feeds in download_prefix folder and register to gtfsApi (MTC project) - ObjectListing gtfsList = s3.listObjects(bucket, dir); - for (S3ObjectSummary objSummary : gtfsList.getObjectSummaries()) { - - String eTag = objSummary.getETag(); - if (!eTags.contains(eTag)) { - String keyName = objSummary.getKey(); - - // don't add object if it is a dir - if (keyName.equals(dir)){ - continue; - } - LOG.info("Adding feed " + keyName); - String feedId = keyName.split("/")[1]; - try { - gtfsApi.registerFeedSource(feedId, FeedVersion.get(feedId).getGtfsFile()); - } catch (Exception e) { - e.printStackTrace(); - } - eTags.add(eTag); - } - } - return eTags; - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/GtfsPlusController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/GtfsPlusController.java deleted file mode 100644 index e48729038..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/GtfsPlusController.java +++ /dev/null @@ -1,391 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.persistence.FeedStore; -import com.conveyal.datatools.manager.utils.json.JsonUtil; -import com.conveyal.gtfs.GTFSFeed; -import com.fasterxml.jackson.databind.JsonNode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import javax.servlet.MultipartConfigElement; -import javax.servlet.ServletException; -import javax.servlet.http.Part; -import java.io.*; -import java.util.*; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import static spark.Spark.get; -import static spark.Spark.halt; -import static spark.Spark.post; - -/** - * Created by demory on 4/13/16. - */ -public class GtfsPlusController { - - public static final Logger LOG = LoggerFactory.getLogger(GtfsPlusController.class); - - private static FeedStore gtfsPlusStore = new FeedStore("gtfsplus"); - - - public static Boolean uploadGtfsPlusFile (Request req, Response res) throws IOException, ServletException { - - //FeedSource s = FeedSource.get(req.queryParams("feedSourceId")); - String feedVersionId = req.params("versionid"); - - if (req.raw().getAttribute("org.eclipse.jetty.multipartConfig") == null) { - MultipartConfigElement multipartConfigElement = new MultipartConfigElement(System.getProperty("java.io.tmpdir")); - req.raw().setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement); - } - - Part part = req.raw().getPart("file"); - - LOG.info("Saving GTFS+ feed {} from upload for version " + feedVersionId); - - - InputStream uploadStream; - try { - uploadStream = part.getInputStream(); - //v.newGtfsFile(uploadStream); - gtfsPlusStore.newFeed(feedVersionId, uploadStream, null); - } catch (Exception e) { - LOG.error("Unable to open input stream from upload"); - halt("Unable to read uploaded feed"); - } - - return true; - } - - private static Object getGtfsPlusFile(Request req, Response res) { - String feedVersionId = req.params("versionid"); - LOG.info("Downloading GTFS+ file for FeedVersion " + feedVersionId); - - // check for saved - File file = gtfsPlusStore.getFeed(feedVersionId); - if(file == null) { - return getGtfsPlusFromGtfs(feedVersionId, res); - } - LOG.info("Returning updated GTFS+ data"); - return downloadGtfsPlusFile(file, res); - } - - private static Object getGtfsPlusFromGtfs(String feedVersionId, Response res) { - LOG.info("Extracting GTFS+ data from main GTFS feed"); - FeedVersion version = FeedVersion.get(feedVersionId); - - File gtfsPlusFile = null; - - // create a set of valid GTFS+ table names - Set gtfsPlusTables = new HashSet<>(); - for(int i = 0; i < DataManager.gtfsPlusConfig.size(); i++) { - JsonNode tableNode = DataManager.gtfsPlusConfig.get(i); - gtfsPlusTables.add(tableNode.get("name").asText()); - } - - try { - - // create a new zip file to only contain the GTFS+ tables - gtfsPlusFile = File.createTempFile(version.id + "_gtfsplus", ".zip"); - ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(gtfsPlusFile)); - - // iterate through the existing GTFS file, copying any GTFS+ tables - ZipFile gtfsFile = new ZipFile(version.getGtfsFile()); - final Enumeration entries = gtfsFile.entries(); - byte[] buffer = new byte[512]; - while (entries.hasMoreElements()) { - final ZipEntry entry = entries.nextElement(); - if(!gtfsPlusTables.contains(entry.getName())) continue; - - // create a new empty ZipEntry and copy the contents - ZipEntry newEntry = new ZipEntry(entry.getName()); - zos.putNextEntry(newEntry); - InputStream in = gtfsFile.getInputStream(entry); - while (0 < in.available()){ - int read = in.read(buffer); - zos.write(buffer,0,read); - } - in.close(); - zos.closeEntry(); - } - zos.close(); - - } catch (Exception e) { - halt(500, "Error getting GTFS+ file from GTFS"); - } - - return downloadGtfsPlusFile(gtfsPlusFile, res); - } - - private static Object downloadGtfsPlusFile(File file, Response res) { - res.raw().setContentType("application/octet-stream"); - res.raw().setHeader("Content-Disposition", "attachment; filename=" + file.getName() + ".zip"); - - try { - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(res.raw().getOutputStream()); - BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(file)); - - byte[] buffer = new byte[1024]; - int len; - while ((len = bufferedInputStream.read(buffer)) > 0) { - bufferedOutputStream.write(buffer, 0, len); - } - - bufferedOutputStream.flush(); - bufferedOutputStream.close(); - } catch (Exception e) { - halt(500, "Error serving GTFS+ file"); - } - - return res.raw(); - } - - private static Long getGtfsPlusFileTimestamp(Request req, Response res) { - String feedVersionId = req.params("versionid"); - - // check for saved GTFS+ data - File file = gtfsPlusStore.getFeed(feedVersionId); - if(file == null) { - FeedVersion feedVersion = FeedVersion.get(feedVersionId); - file = feedVersion.getGtfsFile(); - } - - return file.lastModified(); - } - - private static Boolean publishGtfsPlusFile(Request req, Response res) { - String feedVersionId = req.params("versionid"); - LOG.info("Publishing GTFS+ for " + feedVersionId); - File plusFile = gtfsPlusStore.getFeed(feedVersionId); - if(plusFile == null || !plusFile.exists()) { - halt(400, "No saved GTFS+ data for version"); - } - - FeedVersion feedVersion = FeedVersion.get(feedVersionId); - - // create a set of valid GTFS+ table names - Set gtfsPlusTables = new HashSet<>(); - for(int i = 0; i < DataManager.gtfsPlusConfig.size(); i++) { - JsonNode tableNode = DataManager.gtfsPlusConfig.get(i); - gtfsPlusTables.add(tableNode.get("name").asText()); - } - - File newFeed = null; - - try { - - // create a new zip file to only contain the GTFS+ tables - newFeed = File.createTempFile(feedVersionId + "_new", ".zip"); - ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newFeed)); - - // iterate through the existing GTFS file, copying all non-GTFS+ tables - ZipFile gtfsFile = new ZipFile(feedVersion.getGtfsFile()); - final Enumeration entries = gtfsFile.entries(); - byte[] buffer = new byte[512]; - while (entries.hasMoreElements()) { - final ZipEntry entry = entries.nextElement(); - if(gtfsPlusTables.contains(entry.getName()) || entry.getName().startsWith("_")) continue; // skip GTFS+ and non-standard tables - - // create a new empty ZipEntry and copy the contents - ZipEntry newEntry = new ZipEntry(entry.getName()); - zos.putNextEntry(newEntry); - InputStream in = gtfsFile.getInputStream(entry); - while (0 < in.available()){ - int read = in.read(buffer); - zos.write(buffer,0,read); - } - in.close(); - zos.closeEntry(); - } - - // iterate through the GTFS+ file, copying all entries - ZipFile plusZipFile = new ZipFile(plusFile); - final Enumeration plusEntries = plusZipFile.entries(); - while (plusEntries.hasMoreElements()) { - final ZipEntry entry = plusEntries.nextElement(); - - ZipEntry newEntry = new ZipEntry(entry.getName()); - zos.putNextEntry(newEntry); - InputStream in = plusZipFile.getInputStream(entry); - while (0 < in.available()){ - int read = in.read(buffer); - zos.write(buffer,0,read); - } - in.close(); - zos.closeEntry(); - } - - zos.close(); - - } catch (Exception e) { - e.printStackTrace(); - halt(500, "Error merging GTFS+ data with GTFS"); - } - - FeedVersion newFeedVersion = new FeedVersion(feedVersion.getFeedSource()); - - try { - newFeedVersion.newGtfsFile(new FileInputStream(newFeed)); - } catch (Exception e) { - e.printStackTrace(); - halt(500, "Error creating new FeedVersion from combined GTFS/GTFS+"); - } - - newFeedVersion.hash(); - - // validation for the main GTFS content hasn't changed - newFeedVersion.validationResult = feedVersion.validationResult; - - newFeedVersion.save(); - - for(String resourceType : DataManager.feedResources.keySet()) { - DataManager.feedResources.get(resourceType).feedVersionCreated(newFeedVersion, null); - } - - return true; - } - - private static Collection getGtfsPlusValidation(Request req, Response res) { - String feedVersionId = req.params("versionid"); - LOG.info("Validating GTFS+ for " + feedVersionId); - FeedVersion feedVersion = FeedVersion.get(feedVersionId); - - List issues = new LinkedList<>(); - - - // load the main GTFS -// GTFSFeed gtfsFeed = GTFSFeed.fromFile(feedVersion.getGtfsFile().getAbsolutePath()); - GTFSFeed gtfsFeed = DataManager.gtfsCache.get(feedVersion.id); - // check for saved GTFS+ data - File file = gtfsPlusStore.getFeed(feedVersionId); - if(file == null) { - file = feedVersion.getGtfsFile(); - } - - try { - ZipFile zipFile = new ZipFile(file); - final Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - final ZipEntry entry = entries.nextElement(); - for(int i = 0; i < DataManager.gtfsPlusConfig.size(); i++) { - JsonNode tableNode = DataManager.gtfsPlusConfig.get(i); - if(tableNode.get("name").asText().equals(entry.getName())) { - LOG.info("Validating GTFS+ table: " + entry.getName()); - validateTable(issues, tableNode, zipFile.getInputStream(entry), gtfsFeed); - } - } - } - - } catch(Exception e) { - e.printStackTrace(); - halt(500); - } - - return issues; - } - - private static void validateTable(Collection issues, JsonNode tableNode, InputStream inputStream, GTFSFeed gtfsFeed) throws IOException { - - String tableId = tableNode.get("id").asText(); - BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); - String line = in.readLine(); - String[] fields = line.split(","); - List fieldList = Arrays.asList(fields); - for (String field : fieldList) { - field = field.toLowerCase(); - } - - JsonNode[] fieldNodes = new JsonNode[fields.length]; - - JsonNode fieldsNode = tableNode.get("fields"); - for(int i = 0; i < fieldsNode.size(); i++) { - JsonNode fieldNode = fieldsNode.get(i); - int index = fieldList.indexOf(fieldNode.get("name").asText()); - if(index != -1) fieldNodes[index] = fieldNode; - } - - int rowIndex = 0; - while((line = in.readLine()) != null) { - String[] values = line.split(",", -1); - for(int v=0; v < values.length; v++) { - validateTableValue(issues, tableId, rowIndex, values[v], fieldNodes[v], gtfsFeed); - } - rowIndex++; - } - } - - private static void validateTableValue(Collection issues, String tableId, int rowIndex, String value, JsonNode fieldNode, GTFSFeed gtfsFeed) { - if(fieldNode == null) return; - String fieldName = fieldNode.get("name").asText(); - - if(fieldNode.get("required") != null && fieldNode.get("required").asBoolean()) { - if(value == null || value.length() == 0) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Required field missing value")); - } - } - - switch(fieldNode.get("inputType").asText()) { - case "TEXT": - if(fieldNode.get("maxLength") != null) { - int maxLength = fieldNode.get("maxLength").asInt(); - if(value.length() > maxLength) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Text value exceeds the max. length of "+maxLength)); - } - } - break; - case "GTFS_ROUTE": - if(!gtfsFeed.routes.containsKey(value)) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Route ID "+ value + " not found in GTFS")); - } - break; - case "GTFS_STOP": - if(!gtfsFeed.stops.containsKey(value)) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Stop ID "+ value + " not found in GTFS")); - } - break; - case "GTFS_TRIP": - if(!gtfsFeed.trips.containsKey(value)) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Trip ID "+ value + " not found in GTFS")); - } - break; - case "GTFS_FARE": - if(!gtfsFeed.fares.containsKey(value)) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Fare ID "+ value + " not found in GTFS")); - } - break; - case "GTFS_SERVICE": - if(!gtfsFeed.services.containsKey(value)) { - issues.add(new ValidationIssue(tableId, fieldName, rowIndex, "Service ID "+ value + " not found in GTFS")); - } - break; - } - - } - - public static class ValidationIssue implements Serializable { - public String tableId; - public String fieldName; - public int rowIndex; - public String description; - - public ValidationIssue(String tableId, String fieldName, int rowIndex, String description) { - this.tableId = tableId; - this.fieldName = fieldName; - this.rowIndex = rowIndex; - this.description = description; - } - } - - public static void register(String apiPrefix) { - post(apiPrefix + "secure/gtfsplus/:versionid", GtfsPlusController::uploadGtfsPlusFile, JsonUtil.objectMapper::writeValueAsString); - get(apiPrefix + "secure/gtfsplus/:versionid", GtfsPlusController::getGtfsPlusFile); - get(apiPrefix + "secure/gtfsplus/:versionid/timestamp", GtfsPlusController::getGtfsPlusFileTimestamp, JsonUtil.objectMapper::writeValueAsString); - get(apiPrefix + "secure/gtfsplus/:versionid/validation", GtfsPlusController::getGtfsPlusValidation, JsonUtil.objectMapper::writeValueAsString); - post(apiPrefix + "secure/gtfsplus/:versionid/publish", GtfsPlusController::publishGtfsPlusFile, JsonUtil.objectMapper::writeValueAsString); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/NoteController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/NoteController.java deleted file mode 100644 index bce03eec9..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/NoteController.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.jobs.NotifyUsersForSubscriptionJob; -import com.conveyal.datatools.manager.models.*; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import spark.Request; -import spark.Response; - -import java.io.IOException; -import java.util.Collection; -import java.util.Date; - -import static spark.Spark.*; -import static spark.Spark.get; -import static spark.Spark.post; - -/** - * Created by demory on 4/19/16. - */ -public class NoteController { - - private static JsonManager json = - new JsonManager(Note.class, JsonViews.UserInterface.class); - - public static Collection getAllNotes (Request req, Response res) throws JsonProcessingException { - Auth0UserProfile userProfile = req.attribute("user"); - if(userProfile == null) halt(401); - - String typeStr = req.queryParams("type"); - String objectId = req.queryParams("objectId"); - - if (typeStr == null || objectId == null) { - halt(400, "Please specify objectId and type"); - } - - Note.NoteType type = null; - try { - type = Note.NoteType.valueOf(typeStr); - } catch (IllegalArgumentException e) { - halt(400, "Please specify a valid type"); - } - - Model model = null; - - switch (type) { - case FEED_SOURCE: - model = FeedSource.get(objectId); - break; - case FEED_VERSION: - model = FeedVersion.get(objectId); - break; - default: - // this shouldn't ever happen, but Java requires that every case be covered somehow so model can't be used uninitialized - halt(400, "Unsupported type for notes"); - } - - FeedSource s; - - if (model instanceof FeedSource) { - s = (FeedSource) model; - } - else { - s = ((FeedVersion) model).getFeedSource(); - } - - // check if the user has permission - if (userProfile.canAdministerProject(s.projectId) || userProfile.canViewFeed(s.projectId, s.id)) { - return model.getNotes(); - } - else { - halt(401); - } - - return null; - } - - public static Note createNote (Request req, Response res) throws IOException { - Auth0UserProfile userProfile = req.attribute("user"); - if(userProfile == null) halt(401); - - String typeStr = req.queryParams("type"); - String objectId = req.queryParams("objectId"); - - Note.NoteType type = null; - try { - type = Note.NoteType.valueOf(typeStr); - } catch (IllegalArgumentException e) { - halt(400, "Please specify a valid type"); - } - - Model model = null; - - switch (type) { - case FEED_SOURCE: - model = FeedSource.get(objectId); - break; - case FEED_VERSION: - model = FeedVersion.get(objectId); - break; - default: - // this shouldn't ever happen, but Java requires that every case be covered somehow so model can't be used uninitialized - halt(400, "Unsupported type for notes"); - } - - FeedSource s; - - if (model instanceof FeedSource) { - s = (FeedSource) model; - } - else { - s = ((FeedVersion) model).getFeedSource(); - } - - // check if the user has permission - if (userProfile.canAdministerProject(s.projectId) || userProfile.canViewFeed(s.projectId, s.id)) { - Note n = new Note(); - n.setUser(userProfile); - - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(req.body()); - n.body = node.get("body").asText(); - - n.userEmail = userProfile.getEmail(); - n.date = new Date(); - n.type = type; - model.addNote(n); - n.save(); - model.save(); - - // send notifications - NotifyUsersForSubscriptionJob notifyFeedJob = new NotifyUsersForSubscriptionJob("feed-commented-on", s.id, n.userEmail + " commented on " + s.name + " at " + n.date.toString() + ":
    " + n.body + "
    "); - Thread notifyThread = new Thread(notifyFeedJob); - notifyThread.start(); - - return n; - } - else { - halt(401); - } - - return null; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/note", NoteController::getAllNotes, json::write); - post(apiPrefix + "secure/note", NoteController::createNote, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/ProjectController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/ProjectController.java deleted file mode 100644 index 4c67dd892..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/ProjectController.java +++ /dev/null @@ -1,667 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.jobs.FetchProjectFeedsJob; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.models.OtpBuildConfig; -import com.conveyal.datatools.manager.models.OtpRouterConfig; -import com.conveyal.datatools.manager.models.OtpServer; -import com.conveyal.datatools.manager.models.Project; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.concurrent.Cancellable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import spark.Request; -import spark.Response; - -import static spark.Spark.*; - -/** - * Created by demory on 3/14/16. - */ - -public class ProjectController { - - public static JsonManager json = - new JsonManager<>(Project.class, JsonViews.UserInterface.class); - - public static final Logger LOG = LoggerFactory.getLogger(ProjectController.class); - private static ObjectMapper mapper = new ObjectMapper(); - public static Collection getAllProjects(Request req, Response res) throws JsonProcessingException { - - Auth0UserProfile userProfile = req.attribute("user"); - - Collection filteredProjects = new ArrayList(); - - System.out.println("found projects: " + Project.getAll().size()); - for (Project proj : Project.getAll()) { - // Get feedSources if making a public call -// Supplier> supplier = () -> new LinkedList(); - if (req.pathInfo().contains("public")) { - proj.feedSources = proj.getProjectFeedSources().stream().filter(fs -> fs != null && fs.isPublic).collect(Collectors.toList()); - } - else { - proj.feedSources = null; - } - if (req.pathInfo().contains("public") || userProfile.canAdministerApplication() || userProfile.hasProject(proj.id)) { - filteredProjects.add(proj); - } - } - - return filteredProjects; - } - - public static Project getProject(Request req, Response res) { - String id = req.params("id"); - Project proj = Project.get(id); - if (proj == null) { -// return new MissingResourceException("No project found", Project.class.getSimpleName(), id); - halt(404, "No project with id: " + id); - } - // Get feedSources if making a public call - if (req.pathInfo().contains("public")) { - Collection feeds = proj.getProjectFeedSources().stream().filter(fs -> fs.isPublic).collect(Collectors.toList()); - proj.feedSources = feeds; - } - else { - proj.feedSources = null; - } - return proj; - } - - public static Project createProject(Request req, Response res) throws IOException { - Project proj = new Project(); - - applyJsonToProject(proj, req.body()); - proj.save(); - - return proj; - } - - public static Project updateProject(Request req, Response res) throws IOException { - String id = req.params("id"); - Project proj = Project.get(id); - - applyJsonToProject(proj, req.body()); - proj.save(); - - return proj; - } - - public static Project deleteProject(Request req, Response res) throws IOException { - String id = req.params("id"); - Project proj = Project.get(id); - - proj.delete(); - - return proj; - - } - - public static Boolean fetch(Request req, Response res) { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - System.out.println("project fetch for " + id); - Project proj = Project.get(id); - FetchProjectFeedsJob fetchProjectFeedsJob = new FetchProjectFeedsJob(proj, userProfile.getUser_id()); - new Thread(fetchProjectFeedsJob).start(); - return true; - } - - public static void applyJsonToProject(Project proj, String json) throws IOException { - JsonNode node = mapper.readTree(json); - Iterator> fieldsIter = node.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - if(entry.getKey().equals("name")) { - proj.name = entry.getValue().asText(); - } - else if(entry.getKey().equals("defaultLocationLat")) { - proj.defaultLocationLat = entry.getValue().asDouble(); - LOG.info("updating default lat"); - } - else if(entry.getKey().equals("defaultLocationLon")) { - proj.defaultLocationLon = entry.getValue().asDouble(); - LOG.info("updating default lon"); - } - else if(entry.getKey().equals("north")) { - proj.north = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("south")) { - proj.south = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("east")) { - proj.east = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("west")) { - proj.west = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("osmNorth")) { - proj.osmNorth = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("osmSouth")) { - proj.osmSouth = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("osmEast")) { - proj.osmEast = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("osmWest")) { - proj.osmWest = entry.getValue().asDouble(); - } - else if(entry.getKey().equals("useCustomOsmBounds")) { - proj.useCustomOsmBounds = entry.getValue().asBoolean(); - } - else if(entry.getKey().equals("defaultLanguage")) { - proj.defaultLanguage = entry.getValue().asText(); - } - else if(entry.getKey().equals("defaultTimeZone")) { - proj.defaultTimeZone = entry.getValue().asText(); - } - else if(entry.getKey().equals("autoFetchHour")) { - proj.autoFetchHour = entry.getValue().asInt(); - } - else if(entry.getKey().equals("autoFetchMinute")) { - proj.autoFetchMinute = entry.getValue().asInt(); - - // If auto fetch flag is turned on - if (proj.autoFetchFeeds){ - cancelAutoFetch(proj.id); - int interval = 1; // once per day interval - DataManager.autoFetchMap.put(proj.id, scheduleAutoFeedFetch(proj.id, proj.autoFetchHour, proj.autoFetchMinute, interval, proj.defaultTimeZone)); - } - - // otherwise, cancel any existing task for this id - else{ - cancelAutoFetch(proj.id); - } - } - else if(entry.getKey().equals("autoFetchMinute")) { - proj.autoFetchMinute = entry.getValue().asInt(); - - // If auto fetch flag is turned on - if (proj.autoFetchFeeds){ - cancelAutoFetch(proj.id); - int interval = 1; // once per day interval - DataManager.autoFetchMap.put(proj.id, scheduleAutoFeedFetch(proj.id, proj.autoFetchHour, proj.autoFetchMinute, interval, proj.defaultTimeZone)); - } - - // otherwise, cancel any existing task for this id - else{ - cancelAutoFetch(proj.id); - } - } - else if(entry.getKey().equals("autoFetchFeeds")) { - proj.autoFetchFeeds = entry.getValue().asBoolean(); - - // If auto fetch flag is turned on - if (proj.autoFetchFeeds){ - cancelAutoFetch(proj.id); - int interval = 1; // once per day interval - DataManager.autoFetchMap.put(proj.id, scheduleAutoFeedFetch(proj.id, proj.autoFetchHour, proj.autoFetchMinute, interval, proj.defaultTimeZone)); - } - - // otherwise, cancel any existing task for this id - else{ - cancelAutoFetch(proj.id); - } - } - else if(entry.getKey().equals("otpServers")) { - JsonNode otpServers = entry.getValue(); - if (otpServers.isArray()) { - proj.otpServers = new ArrayList<>(); - for (int i = 0; i < otpServers.size(); i++) { - JsonNode otpServer = otpServers.get(i); - - OtpServer otpServerObj = new OtpServer(); - if (otpServer.has("name")) { - JsonNode name = otpServer.get("name"); - otpServerObj.name = name.isNull() ? null : name.asText(); - } - - if (otpServer.has("admin")) { - JsonNode admin = otpServer.get("admin"); - otpServerObj.admin = admin.isNull() ? false : admin.asBoolean(); - } - - if (otpServer.has("publicUrl")) { - JsonNode publicUrl = otpServer.get("publicUrl"); - otpServerObj.publicUrl = publicUrl.isNull() ? null : publicUrl.asText(); - } - if (otpServer.has("internalUrl") && otpServer.get("internalUrl").isArray()) { - JsonNode internalUrl = otpServer.get("internalUrl"); - for (int j = 0; j < internalUrl.size(); j++) { - if (internalUrl.get(j).isNull()) { - continue; - } - String url = internalUrl.get(j).asText(); - if (otpServerObj.internalUrl == null) { - otpServerObj.internalUrl = new ArrayList<>(); - } - otpServerObj.internalUrl.add(url); - } - } - - proj.otpServers.add(otpServerObj); - } - } - } - else if (entry.getKey().equals("buildConfig")) { - JsonNode buildConfig = entry.getValue(); - if(proj.buildConfig == null) proj.buildConfig = new OtpBuildConfig(); - - if(buildConfig.has("subwayAccessTime")) { - JsonNode subwayAccessTime = buildConfig.get("subwayAccessTime"); - // allow client to un-set option via 'null' value - proj.buildConfig.subwayAccessTime = subwayAccessTime.isNull() ? null : subwayAccessTime.asDouble(); - } - - if(buildConfig.has("fetchElevationUS")) { - JsonNode fetchElevationUS = buildConfig.get("fetchElevationUS"); - proj.buildConfig.fetchElevationUS = fetchElevationUS.isNull() ? null : fetchElevationUS.asBoolean(); - } - - if(buildConfig.has("stationTransfers")) { - JsonNode stationTransfers = buildConfig.get("stationTransfers"); - proj.buildConfig.stationTransfers = stationTransfers.isNull() ? null : stationTransfers.asBoolean(); - } - - if (buildConfig.has("fares")) { - JsonNode fares = buildConfig.get("fares"); - proj.buildConfig.fares = fares.isNull() ? null : fares.asText(); - } - } - else if (entry.getKey().equals("routerConfig")) { - JsonNode routerConfig = entry.getValue(); - if (proj.routerConfig == null) proj.routerConfig = new OtpRouterConfig(); - - if (routerConfig.has("numItineraries")) { - JsonNode numItineraries = routerConfig.get("numItineraries"); - proj.routerConfig.numItineraries = numItineraries.isNull() ? null : numItineraries.asInt(); - } - - if (routerConfig.has("walkSpeed")) { - JsonNode walkSpeed = routerConfig.get("walkSpeed"); - proj.routerConfig.walkSpeed = walkSpeed.isNull() ? null : walkSpeed.asDouble(); - } - - if (routerConfig.has("carDropoffTime")) { - JsonNode carDropoffTime = routerConfig.get("carDropoffTime"); - proj.routerConfig.carDropoffTime = carDropoffTime.isNull() ? null : carDropoffTime.asDouble(); - } - - if (routerConfig.has("stairsReluctance")) { - JsonNode stairsReluctance = routerConfig.get("stairsReluctance"); - proj.routerConfig.stairsReluctance = stairsReluctance.isNull() ? null : stairsReluctance.asDouble(); - } - - if (routerConfig.has("updaters")) { - JsonNode updaters = routerConfig.get("updaters"); - if (updaters.isArray()) { - proj.routerConfig.updaters = new ArrayList<>(); - for (int i = 0; i < updaters.size(); i++) { - JsonNode updater = updaters.get(i); - - OtpRouterConfig.Updater updaterObj = new OtpRouterConfig.Updater(); - if(updater.has("type")) { - JsonNode type = updater.get("type"); - updaterObj.type = type.isNull() ? null : type.asText(); - } - - if(updater.has("sourceType")) { - JsonNode sourceType = updater.get("sourceType"); - updaterObj.sourceType = sourceType.isNull() ? null : sourceType.asText(); - } - - if(updater.has("defaultAgencyId")) { - JsonNode defaultAgencyId = updater.get("defaultAgencyId"); - updaterObj.defaultAgencyId = defaultAgencyId.isNull() ? null : defaultAgencyId.asText(); - } - - if(updater.has("url")) { - JsonNode url = updater.get("url"); - updaterObj.url = url.isNull() ? null : url.asText(); - } - - if(updater.has("frequencySec")) { - JsonNode frequencySec = updater.get("frequencySec"); - updaterObj.frequencySec = frequencySec.isNull() ? null : frequencySec.asInt(); - } - - proj.routerConfig.updaters.add(updaterObj); - } - } - } - } - } - } - -// private static Object downloadFeedVersionWithToken (Request req, Response res) { -// FeedDownloadToken token = FeedDownloadToken.get(req.params("token")); -// -// if(token == null || !token.isValid()) { -// halt(400, "Feed download token not valid"); -// } -// -// FeedVersion version = token.getFeedVersion(); -// -// token.delete(); -// -// return downloadMergedFeed(project, res); -// } - - private static Object downloadMergedFeed(Request req, Response res) throws IOException { - String id = req.params("id"); - Project p = Project.get(id); - - if(p == null) halt(500, "Project is null"); - - // get feed sources in project - Collection feeds = p.getProjectFeedSources(); - - // create temp merged zip file to add feed content to - File mergedFile; - try { - mergedFile = File.createTempFile(p.id + "-merged", ".zip"); -// mergedFile.deleteOnExit(); - - } catch (IOException e) { - LOG.error("Could not create temp file"); - e.printStackTrace(); - - // // TODO: 5/29/16 add status of download job, move downloadMergedFeed to job... -// synchronized (status) { -// status.error = true; -// status.completed = true; -// status.message = "app.deployment.error.dump"; -// } - - return null; - } - - // create the zipfile - ZipOutputStream out; - try { - out = new ZipOutputStream(new FileOutputStream(mergedFile)); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - - LOG.info("Created project merge file: " + mergedFile.getAbsolutePath()); - - // map of feed versions to table entries contained within version's GTFS - Map feedSourceMap = new HashMap<>(); - - for (FeedSource fs : feeds) { - - // check if feed source has version (use latest) - FeedVersion version = fs.getLatest(); - if (version == null) { - LOG.info("Skipping {} because it has no feed versions", fs.name); - continue; - } - // modify feed version to use prepended feed id - LOG.info("Adding {} feed to merged zip", fs.name); - try { - File file = version.getGtfsFile(); - ZipFile zipFile = new ZipFile(file); - feedSourceMap.put(fs, zipFile); - } catch(Exception e) { - e.printStackTrace(); - halt(500); - } - } - - // loop through GTFS tables - for(int i = 0; i < DataManager.gtfsConfig.size(); i++) { - JsonNode tableNode = DataManager.gtfsConfig.get(i); - byte[] tableOut = mergeTables(tableNode, feedSourceMap); - - // if at least one feed has the table, include it - if (tableOut != null) { - String tableName = tableNode.get("name").asText(); - - // create entry for zip file - ZipEntry tableEntry = new ZipEntry(tableName); - out.putNextEntry(tableEntry); - LOG.info("Writing {} to merged feed", tableEntry.getName()); - out.write(tableOut); - out.closeEntry(); - } - - } - out.close(); - - - -// FileInputStream fis = new FileInputStream(mergedFile); - -// res.type("application/zip"); -// res.header("Content-Disposition", "attachment;filename=" + p.name.replaceAll("[^a-zA-Z0-9]", "") + "-gtfs.zip"); - - // will not actually be deleted until download has completed - // http://stackoverflow.com/questions/24372279 -// mergedFile.delete(); - -// return fis; - -// // Deliver zipfile - res.raw().setContentType("application/octet-stream"); - res.raw().setHeader("Content-Disposition", "attachment; filename=" + mergedFile.getName()); - - - try { - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(res.raw().getOutputStream()); - BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(mergedFile)); - - byte[] buffer = new byte[1024]; - int len; - while ((len = bufferedInputStream.read(buffer)) > 0) { - bufferedOutputStream.write(buffer, 0, len); - } - - bufferedOutputStream.flush(); - bufferedOutputStream.close(); - } catch (Exception e) { - halt(500, "Error serving GTFS file"); - } - - return res.raw(); - } - - private static byte[] mergeTables(JsonNode tableNode, Map feedSourceMap) throws IOException { - - String tableName = tableNode.get("name").asText(); - ByteArrayOutputStream tableOut = new ByteArrayOutputStream(); - - int feedIndex = 0; - - JsonNode fieldsNode = tableNode.get("fields"); - String[] headers = new String[fieldsNode.size()]; - for (int i = 0; i < fieldsNode.size(); i++) { - JsonNode fieldNode = fieldsNode.get(i); - String fieldName = fieldNode.get("name").asText(); - - headers[i] = fieldName; - } - - // write headers to table - tableOut.write(String.join(",", headers).getBytes()); - tableOut.write("\n".getBytes()); - - for ( Map.Entry mapEntry : feedSourceMap.entrySet()) { - FeedSource fs = mapEntry.getKey(); - ZipFile zipFile = mapEntry.getValue(); - final Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - final ZipEntry entry = entries.nextElement(); - if(tableName.equals(entry.getName())) { - LOG.info("Adding {} table for {}", entry.getName(), fs.name); - - InputStream inputStream = zipFile.getInputStream(entry); - - BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); - String line = in.readLine(); - String[] fields = line.split(","); - - List fieldList = Arrays.asList(fields); - - int rowIndex = 0; - while((line = in.readLine()) != null) { - String[] newValues = new String[fieldsNode.size()]; - String[] values = line.split(",", -1); - for(int v=0; v < fieldsNode.size(); v++) { - JsonNode fieldNode = fieldsNode.get(v); - String fieldName = fieldNode.get("name").asText(); - - // get index of field from GTFS spec as it appears in feed - int index = fieldList.indexOf(fieldName); - String val = ""; - if(index != -1) { - val = values[index]; - } - - String fieldType = fieldNode.get("inputType").asText(); - - // if field is a gtfs identifier, prepend with feed id/name - if (fieldType.contains("GTFS") && !val.isEmpty()) { -// LOG.info("Adding feed id {} to entity {}: {}", fs.name, fieldName, val); - newValues[v] = fs.name + ":" + val; - } - else { - newValues[v] = val; - } - } - String newLine = String.join(",", newValues); - tableOut.write(newLine.getBytes()); - tableOut.write("\n".getBytes()); - rowIndex++; - } - } - } - feedIndex++; - } - return tableOut.toByteArray(); - } - - public static Project thirdPartySync(Request req, Response res) throws Exception { - Auth0UserProfile userProfile = req.attribute("user"); - String id = req.params("id"); - Project proj = Project.get(id); - - String syncType = req.params("type"); - - if (!userProfile.canAdministerProject(proj.id)) - halt(403); - - LOG.info("syncing with third party " + syncType); - - if(DataManager.feedResources.containsKey(syncType)) { - DataManager.feedResources.get(syncType).importFeedsForProject(proj, req.headers("Authorization")); - return proj; - } - - halt(404); - return null; - } - public static ScheduledFuture scheduleAutoFeedFetch (String id, int hour, int minute, int interval, String timezoneId){ - - // First cancel any already scheduled auto fetch task for this project id. - cancelAutoFetch(id); - Project p = Project.get(id); - if (p == null) - return null; - - Cancellable task = null; - - ZoneId timezone; - try { - timezone = ZoneId.of(timezoneId); - }catch(Exception e){ - timezone = ZoneId.of("America/New_York"); - } -// ZoneId.systemDefault(); - System.out.println("Using timezone: " + timezone.getId()); - - long initialDelay = 0; - - - // NOW in UTC - ZonedDateTime now = LocalDateTime.now().atZone(timezone); - System.out.println("Current time:" + now.toString()); - - // Format and parse datetime string - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); - String dtString = String.valueOf(now.getDayOfMonth()) + "/" + String.valueOf(now.getMonth()) + "/" + String.valueOf(now.getYear()) + " " + String.valueOf(hour) + ":" + String.valueOf(minute); - ZonedDateTime startTime = ZonedDateTime.parse(dtString, formatter); - System.out.println("Start time:" + startTime.toString()); - - // Get diff between start time and current time - long diffInMinutes = (startTime.toEpochSecond() - now.toEpochSecond()) / 60; - if ( diffInMinutes >= 0 ){ - initialDelay = diffInMinutes; // delay in minutes - } - else{ - initialDelay = 24 * 60 + diffInMinutes; // wait for one day plus difference (which is negative) - } - - System.out.println("Scheduling the feed auto fetch daemon to kick off in " + String.valueOf(initialDelay / 60) + " hours." ); - - FetchProjectFeedsJob fetchProjectFeedsJob = new FetchProjectFeedsJob(p, null); - - return DataManager.scheduler.scheduleAtFixedRate(fetchProjectFeedsJob, initialDelay, interval, TimeUnit.MINUTES); - } - public static void cancelAutoFetch(String id){ - Project p = Project.get(id); - if ( p != null && DataManager.autoFetchMap.get(p.id) != null) { - System.out.println("Cancelling the feed auto fetch daemon for projectID: " + p.id); - DataManager.autoFetchMap.get(p.id).cancel(true); - } - } - public static void register (String apiPrefix) { - options(apiPrefix + "secure/project", (q, s) -> ""); - options(apiPrefix + "secure/project/:id", (q, s) -> ""); - get(apiPrefix + "secure/project/:id", ProjectController::getProject, json::write); - get(apiPrefix + "secure/project", ProjectController::getAllProjects, json::write); - post(apiPrefix + "secure/project", ProjectController::createProject, json::write); - put(apiPrefix + "secure/project/:id", ProjectController::updateProject, json::write); - delete(apiPrefix + "secure/project/:id", ProjectController::deleteProject, json::write); - get(apiPrefix + "secure/project/:id/thirdPartySync/:type", ProjectController::thirdPartySync, json::write); - post(apiPrefix + "secure/project/:id/fetch", ProjectController::fetch, json::write); - - get(apiPrefix + "public/project/:id/download", ProjectController::downloadMergedFeed); - - get(apiPrefix + "public/project/:id", ProjectController::getProject, json::write); - get(apiPrefix + "public/project", ProjectController::getAllProjects, json::write); - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/RegionController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/RegionController.java deleted file mode 100644 index 30a6d55f2..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/RegionController.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.Region; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.vividsolutions.jts.geom.Envelope; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Point; -import org.apache.commons.io.FilenameUtils; -import org.geotools.geojson.geom.GeometryJSON; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; - -import static spark.Spark.*; -import static spark.Spark.get; - -/** - * Created by landon on 4/15/16. - */ -public class RegionController { - public static final Logger LOG = LoggerFactory.getLogger(ProjectController.class); - - public static JsonManager json = - new JsonManager<>(Region.class, JsonViews.UserInterface.class); - - public static Region getRegion(Request req, Response res) { - String id = req.params("id"); - return Region.get(id); - } - - public static Collection getAllRegions(Request req, Response res) throws JsonProcessingException { - Collection regions = new ArrayList<>(); - - String projectId = req.queryParams("projectId"); - System.out.println(req.pathInfo()); - regions = Region.getAll(); -// Boolean publicFilter = Boolean.valueOf(req.queryParams("public")); -// if(projectId != null) { -// for (Region region: Region.getAll()) { -// if(region.projectId.equals(projectId)) { -// // if requesting public regions and region is not public; skip region -// if (publicFilter && !region.isPublic) -// continue; -// regions.add(region); -// } -// } -// } -// else { -// for (Region region: Region.getAll()) { -// // if requesting public regions and region is not public; skip region -// if (publicFilter && !region.isPublic) -// continue; -// regions.add(region); -// } -// } - - return regions; - } - - public static Region createRegion(Request req, Response res) throws IOException { - Region region; - - region = new Region(); - - applyJsonToRegion(region, req.body()); - region.save(); - - return region; - } - - public static Region updateRegion(Request req, Response res) throws IOException { - String id = req.params("id"); - Region region = Region.get(id); - - applyJsonToRegion(region, req.body()); - region.save(); - - return region; - } - - public static void applyJsonToRegion(Region region, String json) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(json); - Iterator> fieldsIter = node.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - - if(entry.getKey().equals("name")) { - region.name = entry.getValue().asText(); - } - - if(entry.getKey().equals("order")) { - region.order = entry.getValue().asText(); - } - - if(entry.getKey().equals("geometry")) { - region.geometry = entry.getValue().asText(); - } - - if(entry.getKey().equals("isPublic")) { - region.isPublic = entry.getValue().asBoolean(); - } - - } - } - - public static Collection seedRegions(Request req, Response res) throws IOException { - Region.deleteAll(); - Collection regions = new ArrayList<>(); - String regionsDir = DataManager.config.get("application").get("data").get("regions").asText(); - LOG.info(regionsDir); - GeometryJSON gjson = new GeometryJSON(); - Files.walk(Paths.get(regionsDir)).forEach(filePath -> { - if (Files.isRegularFile(filePath) && FilenameUtils.getExtension(filePath.toString()).equalsIgnoreCase("geojson")) { - LOG.info(String.valueOf(filePath)); - ObjectMapper mapper = new ObjectMapper(); - JsonNode root; - try { - root = mapper.readTree(filePath.toFile()); - JsonNode features = root.get("features"); - for (JsonNode feature : features){ - Region region = new Region(); - String name; - if (feature.get("properties").has("NAME")) - name = feature.get("properties").get("NAME").asText(); - else if (feature.get("properties").has("name")) - name = feature.get("properties").get("name").asText(); - else - continue; - - region.name = name; -// LOG.info(region.name); - if (feature.get("properties").has("featurecla")) - region.order = feature.get("properties").get("featurecla").asText(); - -// LOG.info("getting geometry"); - if (feature.has("geometry")) { - region.geometry = feature.get("geometry").toString(); - Reader reader = new StringReader(feature.toString()); - MultiPolygon poly = gjson.readMultiPolygon(reader); - Point center = poly.getCentroid(); - region.lon = center.getX(); - region.lat = center.getY(); - Envelope envelope = poly.getEnvelopeInternal(); - region.east = envelope.getMaxX(); - region.west = envelope.getMinX(); - region.north = envelope.getMaxY(); - region.south = envelope.getMinY(); - } - else { - LOG.info("no geometry for " + region.name); - } - - region.isPublic = true; - region.save(); - regions.add(region); - } - } catch (IOException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - } - else { - LOG.warn(filePath.getFileName() + " is not geojson"); - } - }); - return regions; - } - public static Region deleteRegion(Request req, Response res) { - String id = req.params("id"); - Region region = Region.get(id); - region.delete(); - return region; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/region/:id", RegionController::getRegion, json::write); - options(apiPrefix + "secure/region", (q, s) -> ""); - get(apiPrefix + "secure/region", RegionController::getAllRegions, json::write); - post(apiPrefix + "secure/region", RegionController::createRegion, json::write); - put(apiPrefix + "secure/region/:id", RegionController::updateRegion, json::write); - delete(apiPrefix + "secure/region/:id", RegionController::deleteRegion, json::write); - - // Public routes - get(apiPrefix + "public/region/:id", RegionController::getRegion, json::write); - get(apiPrefix + "public/region", RegionController::getAllRegions, json::write); - - get(apiPrefix + "public/seedregions", RegionController::seedRegions, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/ServiceAlertsController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/ServiceAlertsController.java deleted file mode 100644 index 36c9e2a82..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/ServiceAlertsController.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.DataManager; - -/** - * Created by landon on 4/12/16. - */ -public class ServiceAlertsController { - - public static void register(String apiPrefix) { - String extensionType = DataManager.config.get("modules").get("alerts").get("use_extension").asText(); - - // set up as extension - if (extensionType != null) { - - } - // set up with service alerts controller routes - else { - - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/StatusController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/StatusController.java deleted file mode 100644 index 67608521d..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/StatusController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import java.util.HashSet; -import java.util.Set; - -import static spark.Spark.*; -import static spark.Spark.delete; -import static spark.Spark.get; - -/** - * Created by landon on 6/13/16. - */ -public class StatusController { - public static final Logger LOG = LoggerFactory.getLogger(ProjectController.class); - - public static JsonManager json = - new JsonManager<>(MonitorableJob.Status.class, JsonViews.UserInterface.class); - - /*public static Object getStatus(Request req, Response res) { -// Auth0UserProfile userProfile = req.attribute("user"); - String userId = req.params("id"); - System.out.println("getting status for: " + userId); - return DataManager.userJobsMap.get(userId); - }*/ - - public static Object getUserJobs(Request req, Response res) { - Auth0UserProfile userProfile = req.attribute("user"); - String userId = userProfile.getUser_id(); - return DataManager.userJobsMap.containsKey(userId) - ? DataManager.userJobsMap.get(userId).toArray() - : new HashSet<>(); - } - - public static void register (String apiPrefix) { - options(apiPrefix + "public/status", (q, s) -> ""); - get(apiPrefix + "secure/status/jobs", StatusController::getUserJobs, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/controllers/api/UserController.java b/src/main/java/com/conveyal/datatools/manager/controllers/api/UserController.java deleted file mode 100644 index 6c7a5bfcf..000000000 --- a/src/main/java/com/conveyal/datatools/manager/controllers/api/UserController.java +++ /dev/null @@ -1,239 +0,0 @@ -package com.conveyal.datatools.manager.controllers.api; - -import com.conveyal.datatools.manager.auth.Auth0UserProfile; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.JsonViews; -import com.conveyal.datatools.manager.models.Note; -import com.conveyal.datatools.manager.models.Project; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.conveyal.datatools.manager.DataManager; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.*; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import spark.Request; -import spark.Response; - -import java.io.*; -import java.net.URLEncoder; -import java.util.*; - -import com.conveyal.datatools.manager.auth.Auth0Users; - -import javax.persistence.Entity; - -import static com.conveyal.datatools.manager.auth.Auth0Users.getUserById; -import static spark.Spark.*; - -/** - * Created by landon on 3/29/16. - */ -public class UserController { - - private static String AUTH0_DOMAIN = DataManager.config.get("auth0").get("domain").asText(); - private static String AUTH0_CLIENT_ID = DataManager.config.get("auth0").get("client_id").asText(); - private static String AUTH0_API_TOKEN = DataManager.serverConfig.get("auth0").get("api_token").asText(); - private static Logger LOG = LoggerFactory.getLogger(UserController.class); - private static ObjectMapper mapper = new ObjectMapper(); - public static JsonManager json = - new JsonManager<>(Project.class, JsonViews.UserInterface.class); - - public static Object getUser(Request req, Response res) throws IOException { - String url = "https://" + AUTH0_DOMAIN + "/api/v2/users/" + URLEncoder.encode(req.params("id"), "UTF-8"); - String charset = "UTF-8"; - - HttpGet request = new HttpGet(url); - request.addHeader("Authorization", "Bearer " + AUTH0_API_TOKEN); - request.setHeader("Accept-Charset", charset); - - HttpClient client = HttpClientBuilder.create().build(); - HttpResponse response = client.execute(request); - String result = EntityUtils.toString(response.getEntity()); - - return result; - } - - public static Object getAllUsers(Request req, Response res) throws IOException { - res.type("application/json"); - int page = Integer.parseInt(req.queryParams("page")); - String queryString = req.queryParams("queryString"); - if(queryString != null) queryString = "email:" + queryString + "*"; - - return mapper.readTree(Auth0Users.getAuth0Users(queryString, page)); - } - - public static Object getUserCount(Request req, Response res) throws IOException { - res.type("application/json"); - String queryString = req.queryParams("queryString"); - if(queryString != null) queryString = "email:" + queryString + "*"; - return Auth0Users.getAuth0UserCount(queryString); - } - - public static Object createPublicUser(Request req, Response res) throws IOException { - String url = "https://" + AUTH0_DOMAIN + "/api/v2/users"; - String charset = "UTF-8"; - - HttpPost request = new HttpPost(url); - request.addHeader("Authorization", "Bearer " + AUTH0_API_TOKEN); - request.setHeader("Accept-Charset", charset); - request.setHeader("Content-Type", "application/json"); - JsonNode jsonNode = mapper.readTree(req.body()); - String json = String.format("{ \"connection\": \"Username-Password-Authentication\", \"email\": %s, \"password\": %s, \"app_metadata\": {\"datatools\": [{\"permissions\": [], \"projects\": [], \"subscriptions\": [], \"client_id\": \"%s\" }] } }", jsonNode.get("email"), jsonNode.get("password"), AUTH0_CLIENT_ID); - HttpEntity entity = new ByteArrayEntity(json.getBytes(charset)); - request.setEntity(entity); - - HttpClient client = HttpClientBuilder.create().build(); - HttpResponse response = client.execute(request); - String result = EntityUtils.toString(response.getEntity()); - int statusCode = response.getStatusLine().getStatusCode(); - if(statusCode >= 300) halt(statusCode, response.toString()); - - return result; - } - - public static Object createUser(Request req, Response res) throws IOException { - String url = "https://" + AUTH0_DOMAIN + "/api/v2/users"; - String charset = "UTF-8"; - - HttpPost request = new HttpPost(url); - request.addHeader("Authorization", "Bearer " + AUTH0_API_TOKEN); - request.setHeader("Accept-Charset", charset); - request.setHeader("Content-Type", "application/json"); - JsonNode jsonNode = mapper.readTree(req.body()); - String json = String.format("{ \"connection\": \"Username-Password-Authentication\", \"email\": %s, \"password\": %s, \"app_metadata\": {\"datatools\": [%s] } }", jsonNode.get("email"), jsonNode.get("password"), jsonNode.get("permissions")); - HttpEntity entity = new ByteArrayEntity(json.getBytes(charset)); - request.setEntity(entity); - - HttpClient client = HttpClientBuilder.create().build(); - HttpResponse response = client.execute(request); - String result = EntityUtils.toString(response.getEntity()); - - int statusCode = response.getStatusLine().getStatusCode(); - if(statusCode >= 300) halt(statusCode, response.toString()); - - System.out.println(result); - - return result; - } - - public static Object updateUser(Request req, Response res) throws IOException { - String userId = req.params("id"); - Auth0UserProfile user = getUserById(userId); - - LOG.info("Updating user {}", user.getEmail()); - - String url = "https://" + AUTH0_DOMAIN + "/api/v2/users/" + URLEncoder.encode(userId, "UTF-8"); - String charset = "UTF-8"; - - - HttpPatch request = new HttpPatch(url); - - request.addHeader("Authorization", "Bearer " + AUTH0_API_TOKEN); - request.setHeader("Accept-Charset", charset); - request.setHeader("Content-Type", "application/json"); - - JsonNode jsonNode = mapper.readTree(req.body()); -// JsonNode data = mapper.readValue(jsonNode.get("data"), Auth0UserProfile.DatatoolsInfo.class); //jsonNode.get("data"); - JsonNode data = jsonNode.get("data"); - System.out.println(data.asText()); - Iterator> fieldsIter = data.fields(); - while (fieldsIter.hasNext()) { - Map.Entry entry = fieldsIter.next(); - System.out.println(entry.getValue()); - } -// if (!data.has("client_id")) { -// ((ObjectNode)data).put("client_id", DataManager.config.get("auth0").get("client_id").asText()); -// } - String json = "{ \"app_metadata\": { \"datatools\" : " + data + " }}"; - System.out.println(json); - HttpEntity entity = new ByteArrayEntity(json.getBytes(charset)); - request.setEntity(entity); - - HttpClient client = HttpClientBuilder.create().build(); - HttpResponse response = client.execute(request); - String result = EntityUtils.toString(response.getEntity()); - - return mapper.readTree(result); - } - - public static Object deleteUser(Request req, Response res) throws IOException { - String url = "https://" + AUTH0_DOMAIN + "/api/v2/users/" + URLEncoder.encode(req.params("id"), "UTF-8"); - String charset = "UTF-8"; - - HttpDelete request = new HttpDelete(url); - request.addHeader("Authorization", "Bearer " + AUTH0_API_TOKEN); - request.setHeader("Accept-Charset", charset); - - HttpClient client = HttpClientBuilder.create().build(); - HttpResponse response = client.execute(request); - int statusCode = response.getStatusLine().getStatusCode(); - if(statusCode >= 300) halt(statusCode, response.getStatusLine().getReasonPhrase()); - - return true; - } - - public static Object getRecentActivity(Request req, Response res) { - Auth0UserProfile userProfile = req.attribute("user"); - - List activity = new ArrayList<>(); - - for (Auth0UserProfile.Subscription sub : userProfile.getApp_metadata().getDatatoolsInfo().getSubscriptions()) { - System.out.println("sub type = " + sub.getType()); - switch (sub.getType()) { - // TODO: add all activity types - case "feed-commented-on": - for (String targetId : sub.getTarget()) { - System.out.println(" target: " + targetId); - FeedSource fs = FeedSource.get(targetId); - if(fs == null) continue; - System.out.println(" obj=" + fs); - for (Note note : fs.getNotes()) { - // TODO: Check if actually recent - Activity act = new Activity(); - act.type = sub.getType(); - act.userId = note.userId; - act.userName = note.userEmail; - act.body = note.body; - act.date = note.date; - act.targetId = targetId; - act.targetName = fs.name; - activity.add(act); - } - } - break; - } - } - - return activity; - } - - static class Activity implements Serializable { - public String type; - public String userId; - public String userName; - public String body; - public String targetId; - public String targetName; - public Date date; - } - - public static void register (String apiPrefix) { - get(apiPrefix + "secure/user/:id", UserController::getUser, json::write); - get(apiPrefix + "secure/user/:id/recentactivity", UserController::getRecentActivity, json::write); - get(apiPrefix + "secure/user", UserController::getAllUsers, json::write); - get(apiPrefix + "secure/usercount", UserController::getUserCount, json::write); - post(apiPrefix + "secure/user", UserController::createUser, json::write); - put(apiPrefix + "secure/user/:id", UserController::updateUser, json::write); - delete(apiPrefix + "secure/user/:id", UserController::deleteUser, json::write); - - post(apiPrefix + "public/user", UserController::createPublicUser, json::write); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/ExternalFeedResource.java b/src/main/java/com/conveyal/datatools/manager/extensions/ExternalFeedResource.java deleted file mode 100644 index 98e9a788a..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/ExternalFeedResource.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.conveyal.datatools.manager.extensions; - -import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.Project; - -/** - * Created by demory on 3/30/16. - */ -public interface ExternalFeedResource { - - public String getResourceType(); - - public void importFeedsForProject(Project project, String authHeader); - - public void feedSourceCreated(FeedSource source, String authHeader); - - public void propertyUpdated(ExternalFeedSourceProperty property, String previousValue, String authHeader); - - public void feedVersionCreated(FeedVersion feedVersion, String authHeader); -} diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java b/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java deleted file mode 100644 index 36e1dca49..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java +++ /dev/null @@ -1,251 +0,0 @@ -package com.conveyal.datatools.manager.extensions.mtc; - -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.extensions.ExternalFeedResource; -import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.Project; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.lang.reflect.Field; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - -/** - * Created by demory on 3/30/16. - */ -public class MtcFeedResource implements ExternalFeedResource { - - public static final Logger LOG = LoggerFactory.getLogger(MtcFeedResource.class); - - private String rtdApi, s3Bucket, s3Prefix, s3CredentialsFilename; - - public MtcFeedResource() { - rtdApi = DataManager.config.get("extensions").get("mtc").get("rtd_api").asText(); - s3Bucket = DataManager.config.get("extensions").get("mtc").get("s3_bucket").asText(); - s3Prefix = DataManager.config.get("extensions").get("mtc").get("s3_prefix").asText(); - //s3CredentialsFilename = DataManager.config.get("extensions").get("mtc").get("s3_credentials_file").asText(); - } - - @Override - public String getResourceType() { - return "MTC"; - } - - @Override - public void importFeedsForProject(Project project, String authHeader) { - URL url; - ObjectMapper mapper = new ObjectMapper(); - // single list from MTC - try { - url = new URL(rtdApi + "/Carrier"); - } catch(MalformedURLException ex) { - LOG.error("Could not construct URL for RTD API: " + rtdApi); - return; - } - - try { - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - - // optional default is GET - con.setRequestMethod("GET"); - - //add request header - con.setRequestProperty("User-Agent", "User-Agent"); - - // add auth header - System.out.println("authHeader="+authHeader); - con.setRequestProperty("Authorization", authHeader); - - int responseCode = con.getResponseCode(); - System.out.println("\nSending 'GET' request to URL : " + url); - System.out.println("Response Code : " + responseCode); - - BufferedReader in = new BufferedReader( - new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - - String json = response.toString(); - RtdCarrier[] results = mapper.readValue(json, RtdCarrier[].class); - for (int i = 0; i < results.length; i++) { - // String className = "RtdCarrier"; - // Object car = Class.forName(className).newInstance(); - RtdCarrier car = results[i]; - //System.out.println("car id=" + car.AgencyId + " name=" + car.AgencyName); - - FeedSource source = null; - - // check if a FeedSource with this AgencyId already exists - for (FeedSource existingSource : project.getProjectFeedSources()) { - ExternalFeedSourceProperty agencyIdProp = - ExternalFeedSourceProperty.find(existingSource, this.getResourceType(), "AgencyId"); - if (agencyIdProp != null && agencyIdProp.value.equals(car.AgencyId)) { - //System.out.println("already exists: " + car.AgencyId); - source = existingSource; - } - } - - String feedName; - if (car.AgencyName != null) { - feedName = car.AgencyName; - } else if (car.AgencyShortName != null) { - feedName = car.AgencyShortName; - } else { - feedName = car.AgencyId; - } - - if (source == null) { - source = new FeedSource(feedName); - } - else source.name = feedName; - - source.setProject(project); - - source.save(); - - // create / update the properties - - for(Field carrierField : car.getClass().getDeclaredFields()) { - String fieldName = carrierField.getName(); - String fieldValue = carrierField.get(car) != null ? carrierField.get(car).toString() : null; - ExternalFeedSourceProperty.updateOrCreate(source, this.getResourceType(), fieldName, fieldValue); - } - } - } catch(Exception ex) { - LOG.error("Could not read feeds from MTC RTD API"); - ex.printStackTrace(); - } - } - - @Override - public void feedSourceCreated(FeedSource source, String authHeader) { - LOG.info("Processing new FeedSource " + source.name + " for RTD"); - - RtdCarrier carrier = new RtdCarrier(); - carrier.AgencyName = source.name; - - try { - for (Field carrierField : carrier.getClass().getDeclaredFields()) { - String fieldName = carrierField.getName(); - String fieldValue = carrierField.get(carrier) != null ? carrierField.get(carrier).toString() : null; - ExternalFeedSourceProperty.updateOrCreate(source, this.getResourceType(), fieldName, fieldValue); - } - } catch (Exception e) { - LOG.error("Error creating external properties for new FeedSource"); - } - } - - @Override - public void propertyUpdated(ExternalFeedSourceProperty property, String previousValue, String authHeader) { - LOG.info("Update property in MTC carrier table: " + property.name); - - // sync w/ RTD - RtdCarrier carrier = new RtdCarrier(); - String feedSourceId = property.getFeedSourceId(); - FeedSource source = FeedSource.get(feedSourceId); - - carrier.AgencyId = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyId").value; - carrier.AgencyPhone = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyPhone").value; - carrier.AgencyName = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyName").value; - carrier.RttAgencyName = ExternalFeedSourceProperty.find(source, this.getResourceType(), "RttAgencyName").value; - carrier.RttEnabled = ExternalFeedSourceProperty.find(source, this.getResourceType(), "RttEnabled").value; - carrier.AgencyShortName = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyShortName").value; - carrier.AgencyPublicId = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyPublicId").value; - carrier.AddressLat = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AddressLat").value; - carrier.AddressLon = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AddressLon").value; - carrier.DefaultRouteType = ExternalFeedSourceProperty.find(source, this.getResourceType(), "DefaultRouteType").value; - carrier.CarrierStatus = ExternalFeedSourceProperty.find(source, this.getResourceType(), "CarrierStatus").value; - carrier.AgencyAddress = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyAddress").value; - carrier.AgencyEmail = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyEmail").value; - carrier.AgencyUrl = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyUrl").value; - carrier.AgencyFareUrl = ExternalFeedSourceProperty.find(source, this.getResourceType(), "AgencyFareUrl").value; - - if(property.name.equals("AgencyId") && previousValue == null) { - writeCarrierToRtd(carrier, true, authHeader); - } - else { - writeCarrierToRtd(carrier, false, authHeader); - } - } - - @Override - public void feedVersionCreated(FeedVersion feedVersion, String authHeader) { - - LOG.info("Pushing to MTC S3 Bucket " + s3Bucket); - - if(s3Bucket == null) return; - - AWSCredentials creds; - if (this.s3CredentialsFilename != null) { - creds = new ProfileCredentialsProvider(this.s3CredentialsFilename, "default").getCredentials(); - LOG.info("Writing to S3 using supplied credentials file"); - } - else { - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - } - - ExternalFeedSourceProperty agencyIdProp = - ExternalFeedSourceProperty.find(feedVersion.getFeedSource(), this.getResourceType(), "AgencyId"); - - if(agencyIdProp == null) { - LOG.error("Could not read AgencyId for FeedSource " + feedVersion.feedSourceId); - return; - } - - String keyName = this.s3Prefix + agencyIdProp.value + ".zip"; - LOG.info("Pushing to MTC S3 Bucket: " + keyName); - - AmazonS3 s3client = new AmazonS3Client(creds); - s3client.putObject(new PutObjectRequest( - s3Bucket, keyName, feedVersion.getGtfsFile())); - - } - - private void writeCarrierToRtd(RtdCarrier carrier, boolean createNew, String authHeader) { - - try { - ObjectMapper mapper = new ObjectMapper(); - - String carrierJson = mapper.writeValueAsString(carrier); - - URL rtdUrl = new URL(rtdApi + "/Carrier/" + (createNew ? "" : carrier.AgencyId)); - LOG.info("Writing to RTD URL: " + rtdUrl); - HttpURLConnection connection = (HttpURLConnection) rtdUrl.openConnection(); - - connection.setRequestMethod(createNew ? "POST" : "PUT"); - connection.setDoOutput(true); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("Accept", "application/json"); - connection.setRequestProperty("Authorization", authHeader); - - OutputStreamWriter osw = new OutputStreamWriter(connection.getOutputStream()); - osw.write(carrierJson); - osw.flush(); - osw.close(); - LOG.info("RTD API response: " + connection.getResponseCode() + " / " + connection.getResponseMessage()); - } catch (Exception e) { - LOG.error("error writing to RTD"); - e.printStackTrace(); - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/mtc/RtdCarrier.java b/src/main/java/com/conveyal/datatools/manager/extensions/mtc/RtdCarrier.java deleted file mode 100644 index fe2d6c39f..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/mtc/RtdCarrier.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.conveyal.datatools.manager.extensions.mtc; - -import com.conveyal.datatools.manager.models.FeedSource; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Created by demory on 3/30/16. - */ - -public class RtdCarrier { - - @JsonProperty - String AgencyId; - - @JsonProperty - String AgencyName; - - @JsonProperty - String AgencyPhone; - - @JsonProperty - String RttAgencyName; - - @JsonProperty - String RttEnabled; - - @JsonProperty - String AgencyShortName; - - @JsonProperty - String AgencyPublicId; - - @JsonProperty - String AddressLat; - - @JsonProperty - String AddressLon; - - @JsonProperty - String DefaultRouteType; - - @JsonProperty - String CarrierStatus; - - @JsonProperty - String AgencyAddress; - - @JsonProperty - String AgencyEmail; - - @JsonProperty - String AgencyUrl; - - @JsonProperty - String AgencyFareUrl; - - @JsonProperty - String EditedBy; - - @JsonProperty - String EditedDate; - - public RtdCarrier() { - } - - /*public void mapFeedSource(FeedSource source){ - source.defaultGtfsId = this.AgencyId; - source.shortName = this.AgencyShortName; - source.AgencyPhone = this.AgencyPhone; - source.RttAgencyName = this.RttAgencyName; - source.RttEnabled = this.RttEnabled; - source.AgencyShortName = this.AgencyShortName; - source.AgencyPublicId = this.AgencyPublicId; - source.AddressLat = this.AddressLat; - source.AddressLon = this.AddressLon; - source.DefaultRouteType = this.DefaultRouteType; - source.CarrierStatus = this.CarrierStatus; - source.AgencyAddress = this.AgencyAddress; - source.AgencyEmail = this.AgencyEmail; - source.AgencyUrl = this.AgencyUrl; - source.AgencyFareUrl = this.AgencyFareUrl; - - source.save(); - }*/ -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/transitfeeds/TransitFeedsFeedResource.java b/src/main/java/com/conveyal/datatools/manager/extensions/transitfeeds/TransitFeedsFeedResource.java deleted file mode 100644 index 7c773e094..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/transitfeeds/TransitFeedsFeedResource.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.conveyal.datatools.manager.extensions.transitfeeds; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.extensions.ExternalFeedResource; -import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.Project; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - -/** - * Created by demory on 3/31/16. - */ -public class TransitFeedsFeedResource implements ExternalFeedResource { - - public static final Logger LOG = LoggerFactory.getLogger(TransitFeedsFeedResource.class); - - private String api, apiKey; - - public TransitFeedsFeedResource () { - api = DataManager.config.get("extensions").get("transitfeeds").get("api").asText(); - apiKey = DataManager.config.get("extensions").get("transitfeeds").get("key").asText(); - } - - @Override - public String getResourceType() { - return "TRANSITFEEDS"; - } - - @Override - public void importFeedsForProject(Project project, String authHeader) { - LOG.info("Importing feeds from TransitFeeds"); - - URL url; - ObjectMapper mapper = new ObjectMapper(); - // multiple pages for transitfeeds because of 100 feed limit per page - Boolean nextPage = true; - int count = 1; - - do { - try { - url = new URL(api + "?key=" + apiKey + "&limit=100" + "&page=" + String.valueOf(count)); - } catch (MalformedURLException ex) { - LOG.error("Could not construct URL for TransitFeeds API"); - return; - } - - - StringBuffer response = new StringBuffer(); - - try { - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - - // optional default is GET - con.setRequestMethod("GET"); - - //add request header - con.setRequestProperty("User-Agent", "User-Agent"); - - int responseCode = con.getResponseCode(); - System.out.println("\nSending 'GET' request to URL : " + url); - System.out.println("Response Code : " + responseCode); - - BufferedReader in = new BufferedReader( - new InputStreamReader(con.getInputStream())); - String inputLine; - - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - } catch (IOException ex) { - LOG.error("Could not read from Transit Feeds API"); - return; - } - - String json = response.toString(); - JsonNode transitFeedNode = null; - try { - transitFeedNode = mapper.readTree(json); - } catch (IOException ex) { - LOG.error("Error parsing TransitFeeds JSON response"); - return; - } - - for (JsonNode feed : transitFeedNode.get("results").get("feeds")) { - - // test that feed is in fact GTFS - if (!feed.get("t").asText().contains("GTFS")){ - continue; - } - - // test that feed falls in bounding box (if box exists) - if (project.north != null) { - Double lat = feed.get("l").get("lat").asDouble(); - Double lng = feed.get("l").get("lng").asDouble(); - if (lat < project.south || lat > project.north || lng < project.west || lng > project.east) { - continue; - } - } - - FeedSource source = null; - String tfId = feed.get("id").asText(); - - // check if a feed already exists with this id - for (FeedSource existingSource : project.getProjectFeedSources()) { - ExternalFeedSourceProperty idProp = - ExternalFeedSourceProperty.find(existingSource, this.getResourceType(), "id"); - if (idProp != null && idProp.value.equals(tfId)) { - source = existingSource; - } - } - - String feedName; - feedName = feed.get("t").asText(); - - if (source == null) source = new FeedSource(feedName); - else source.name = feedName; - - source.retrievalMethod = FeedSource.FeedRetrievalMethod.FETCHED_AUTOMATICALLY; - source.setName(feedName); - System.out.println(source.name); - - try { - if (feed.get("u") != null) { - if (feed.get("u").get("d") != null) { - source.url = new URL(feed.get("u").get("d").asText()); - } else if (feed.get("u").get("i") != null) { - source.url = new URL(feed.get("u").get("i").asText()); - } - } - } catch (MalformedURLException ex) { - LOG.error("Error constructing URLs from TransitFeeds API response"); - } - - source.setProject(project); - source.save(); - - // create/update the external props - ExternalFeedSourceProperty.updateOrCreate(source, this.getResourceType(), "id", tfId); - - } - if (transitFeedNode.get("results").get("page") == transitFeedNode.get("results").get("numPages")){ - LOG.info("finished last page of transitfeeds"); - nextPage = false; - } - count++; - } while(nextPage); - } - - @Override - public void feedSourceCreated(FeedSource source, String authHeader) { - - } - - @Override - public void propertyUpdated(ExternalFeedSourceProperty property, String previousValue, String authHeader) { - - } - - @Override - public void feedVersionCreated(FeedVersion feedVersion, String authHeader) { - - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeed.java b/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeed.java deleted file mode 100644 index e5cfcc40b..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeed.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.conveyal.datatools.manager.extensions.transitland; - -import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty; -import com.conveyal.datatools.manager.models.FeedSource; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; - -import java.lang.reflect.Field; -import java.net.MalformedURLException; -import java.net.URL; - -/** - * Created by demory on 3/31/16. - */ - -public class TransitLandFeed { - - @JsonProperty - String onestop_id; - - @JsonProperty - String url; - - @JsonProperty - String feed_format; - - @JsonProperty - String tags; - - @JsonIgnore - String geometry; - - @JsonIgnore - String type; - - @JsonIgnore - String coordinates; - - @JsonProperty - String license_name; - - @JsonProperty - String license_url; - - @JsonProperty - String license_use_without_attribution; - - @JsonProperty - String license_create_derived_product; - - @JsonProperty - String license_redistribute; - - @JsonProperty - String license_attribution_text; - - @JsonProperty - String last_fetched_at; - - @JsonProperty - String last_imported_at; - - @JsonProperty - String latest_fetch_exception_log; - - @JsonProperty - String import_status; - - @JsonProperty - String created_at; - - @JsonProperty - String updated_at; - - @JsonProperty - String feed_versions_count; - - @JsonProperty - String feed_versions_url; - - @JsonProperty - String[] feed_versions; - - @JsonProperty - String active_feed_version; - - @JsonProperty - String import_level_of_active_feed_version; - - @JsonProperty - String created_or_updated_in_changeset_id; - - @JsonIgnore - String changesets_imported_from_this_feed; - - @JsonIgnore - String operators_in_feed; - - @JsonIgnore - String gtfs_agency_id; - - @JsonIgnore - String operator_onestop_id; - - @JsonIgnore - String feed_onestop_id; - - @JsonIgnore - String operator_url; - - @JsonIgnore - String feed_url; - - public TransitLandFeed(JsonNode jsonMap){ - this.url = jsonMap.get("url").asText(); - this.onestop_id = jsonMap.get("onestop_id").asText(); - this.feed_format = jsonMap.get("feed_format").asText(); - this.tags = jsonMap.get("tags").asText(); - this.license_name = jsonMap.get("license_name").asText(); - this.license_url = jsonMap.get("license_url").asText(); - this.license_use_without_attribution = jsonMap.get("license_use_without_attribution").asText(); - this.license_create_derived_product = jsonMap.get("license_create_derived_product").asText(); - this.license_redistribute = jsonMap.get("license_redistribute").asText(); - this.license_attribution_text = jsonMap.get("license_attribution_text").asText(); - this.last_fetched_at = jsonMap.get("last_fetched_at").asText(); - this.last_imported_at = jsonMap.get("last_imported_at").asText(); - this.latest_fetch_exception_log = jsonMap.get("latest_fetch_exception_log").asText(); - this.import_status = jsonMap.get("import_status").asText(); - this.created_at = jsonMap.get("created_at").asText(); - this.updated_at = jsonMap.get("updated_at").asText(); - this.feed_versions_count = jsonMap.get("feed_versions_count").asText(); - this.feed_versions_url = jsonMap.get("feed_versions_url").asText(); -// this.feed_versions = jsonMap.get("feed_versions").asText(); - this.active_feed_version = jsonMap.get("active_feed_version").asText(); - this.import_level_of_active_feed_version = jsonMap.get("import_level_of_active_feed_version").asText(); - this.created_or_updated_in_changeset_id = jsonMap.get("created_or_updated_in_changeset_id").asText(); - this.changesets_imported_from_this_feed = jsonMap.get("changesets_imported_from_this_feed").asText(); - this.operators_in_feed = jsonMap.get("operators_in_feed").asText(); -// this.gtfs_agency_id = jsonMap.get("gtfs_agency_id").asText(); -// this.operator_onestop_id = jsonMap.get("operator_onestop_id").asText(); -// this.feed_onestop_id = jsonMap.get("feed_onestop_id").asText(); -// this.operator_url = jsonMap.get("operator_url").asText(); -// this.feed_url = jsonMap.get("feed_url").asText(); - } - - public void mapFeedSource(FeedSource source){ - - // set the - source.retrievalMethod = FeedSource.FeedRetrievalMethod.FETCHED_AUTOMATICALLY; - try { - source.url = new URL(this.url); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - - source.save(); - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeedResource.java b/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeedResource.java deleted file mode 100644 index 22b8146e8..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeedResource.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.conveyal.datatools.manager.extensions.transitland; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.extensions.ExternalFeedResource; -import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.Project; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.reflect.Field; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - -/** - * Created by demory on 3/31/16. - */ - -public class TransitLandFeedResource implements ExternalFeedResource { - - public static final Logger LOG = LoggerFactory.getLogger(TransitLandFeedResource.class); - - private String api; - - public TransitLandFeedResource() { - api = DataManager.config.get("extensions").get("transitland").get("api").asText(); - } - - @Override - public String getResourceType() { - return "TRANSITLAND"; - } - - @Override - public void importFeedsForProject(Project project, String authHeader) { - LOG.info("Importing TransitLand feeds"); - URL url = null; - ObjectMapper mapper = new ObjectMapper(); - String locationFilter = ""; - if (project.north != null && project.south != null && project.east != null && project.west != null) - locationFilter = "&bbox=" + project.west + "," + + project.south + "," + project.east + "," + project.north; - - try { - url = new URL(api + "?per_page=10000" + locationFilter); - } catch (MalformedURLException ex) { - LOG.error("Error constructing TransitLand API URL"); - } - - try { - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - - // optional default is GET - con.setRequestMethod("GET"); - - //add request header - con.setRequestProperty("User-Agent", "User-Agent"); - - int responseCode = con.getResponseCode(); - System.out.println("\nSending 'GET' request to URL : " + url); - System.out.println("Response Code : " + responseCode); - - BufferedReader in = new BufferedReader( - new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer response = new StringBuffer(); - - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - - String json = response.toString(); - JsonNode node = mapper.readTree(json); - - for (JsonNode feed : node.get("feeds")) { - TransitLandFeed tlFeed = new TransitLandFeed(feed); - - FeedSource source = null; - - // check if a feed already exists with this id - for (FeedSource existingSource : project.getProjectFeedSources()) { - ExternalFeedSourceProperty onestopIdProp = - ExternalFeedSourceProperty.find(existingSource, this.getResourceType(), "onestop_id"); - if (onestopIdProp != null && onestopIdProp.value.equals(tlFeed.onestop_id)) { - source = existingSource; - } - } - - String feedName; - feedName = tlFeed.onestop_id; - - if (source == null) source = new FeedSource(feedName); - else source.name = feedName; - tlFeed.mapFeedSource(source); - - source.setName(feedName); - System.out.println(source.name); - - source.setProject(project); - - source.save(); - - // create / update the properties - - for(Field tlField : tlFeed.getClass().getDeclaredFields()) { - String fieldName = tlField.getName(); - String fieldValue = tlField.get(tlFeed) != null ? tlField.get(tlFeed).toString() : null; - - ExternalFeedSourceProperty.updateOrCreate(source, this.getResourceType(), fieldName, fieldValue); - } - } - } catch (Exception ex) { - LOG.error("Error reading from TransitLand API"); - ex.printStackTrace(); - } - - } - - @Override - public void feedSourceCreated(FeedSource source, String authHeader) { - - } - - @Override - public void propertyUpdated(ExternalFeedSourceProperty property, String previousValue, String authHeader) { - - } - - @Override - public void feedVersionCreated(FeedVersion feedVersion, String authHeader) { - - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeeds.java b/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeeds.java deleted file mode 100644 index 1366ef1a7..000000000 --- a/src/main/java/com/conveyal/datatools/manager/extensions/transitland/TransitLandFeeds.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.conveyal.datatools.manager.extensions.transitland; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Created by demory on 3/31/16. - */ - -public class TransitLandFeeds { - @JsonProperty - List feeds = new ArrayList(); - -// @JsonProperty -// Map - -// @JsonProperty -// List feeds; - - @JsonProperty - Map meta; -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/BuildTransportNetworkJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/BuildTransportNetworkJob.java deleted file mode 100644 index 14988c633..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/BuildTransportNetworkJob.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.r5.point_to_point.builder.TNBuilderConfig; -import com.conveyal.r5.transit.TransportNetwork; -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Map; - -import static com.conveyal.datatools.manager.models.Deployment.getOsmExtract; - -/** - * Created by landon on 4/30/16. - */ -public class BuildTransportNetworkJob extends MonitorableJob { - - public FeedVersion feedVersion; - public TransportNetwork result; - public Status status; - - public BuildTransportNetworkJob (FeedVersion feedVersion, String owner) { - super(owner, "Building Transport Network for " + feedVersion.getFeedSource().name, JobType.BUILD_TRANSPORT_NETWORK); - this.feedVersion = feedVersion; - this.result = null; - this.status = new Status(); - status.message = "Waiting to begin job..."; - } - - @Override - public void run() { - System.out.println("Building network"); - feedVersion.buildTransportNetwork(eventBus); - synchronized (status) { - status.message = "Transport network built successfully!"; - status.percentComplete = 100; - status.completed = true; - } - jobFinished(); - } - - @Override - public Status getStatus() { - synchronized (status) { - return status.clone(); - } - } - - @Override - public void handleStatusEvent(Map statusMap) { - try { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - -// @Override -// public void handleStatusEvent(StatusEvent statusEvent) { -// synchronized (status) { -// status.message = statusEvent.message; -// status.percentComplete = statusEvent.percentComplete -// status.error = statusEvent.error; -// } -// } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/CreateFeedVersionFromSnapshotJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/CreateFeedVersionFromSnapshotJob.java deleted file mode 100644 index adb5adcc2..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/CreateFeedVersionFromSnapshotJob.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.editor.controllers.api.SnapshotController; -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.util.Map; - -import static spark.Spark.halt; - -/** - * Created by demory on 7/27/16. - */ -public class CreateFeedVersionFromSnapshotJob extends MonitorableJob { - public static final Logger LOG = LoggerFactory.getLogger(CreateFeedVersionFromSnapshotJob.class); - - private FeedSource feedSource; - private String snapshotId; - private Status status; - - public CreateFeedVersionFromSnapshotJob (FeedSource feedSource, String snapshotId, String owner) { - super(owner, "Creating Feed Version from Snapshot for " + feedSource.name, JobType.CREATE_FEEDVERSION_FROM_SNAPSHOT); - this.feedSource = feedSource; - this.snapshotId = snapshotId; - this.status = new Status(); - status.message = "Initializing..."; - } - - @Override - public void run() { - FeedVersion v = new FeedVersion(feedSource); - - File file = null; - - try { - file = File.createTempFile("snapshot", ".zip"); - SnapshotController.writeSnapshotAsGtfs(snapshotId, file); - } catch (Exception e) { - e.printStackTrace(); - LOG.error("Unable to create temp file for snapshot"); - halt(400); - } - - try { - v.newGtfsFile(new FileInputStream(file)); - } catch (Exception e) { - LOG.error("Unable to open input stream from upload"); - halt(400, "Unable to read uploaded feed"); - } - - v.name = Snapshot.get(snapshotId).name + " Snapshot Export"; - v.hash(); - v.save(); - - addNextJob(new ProcessSingleFeedJob(v, owner)); - - jobFinished(); - } - - @Override - public Status getStatus() { - synchronized (status) { - return status.clone(); - } - } - - @Override - public void handleStatusEvent(Map statusMap) { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/DeployJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/DeployJob.java deleted file mode 100644 index 9752c760a..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/DeployJob.java +++ /dev/null @@ -1,431 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.event.ProgressEvent; -import com.amazonaws.event.ProgressListener; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.CopyObjectRequest; -import com.amazonaws.services.s3.transfer.TransferManager; -import com.amazonaws.services.s3.transfer.Upload; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.WritableByteChannel; -import java.util.List; -import java.util.Map; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.manager.models.Deployment; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Deploy the given deployment to the OTP servers specified by targets. - * @author mattwigway - * - */ -public class DeployJob extends MonitorableJob { - - private static final Logger LOG = LoggerFactory.getLogger(DeployJob.class); - - /** The URLs to deploy to */ - private List targets; - - /** The base URL to otp.js on these targets */ - private String publicUrl; - - /** An optional AWS S3 bucket to copy the bundle to */ - private String s3Bucket; - - /** An AWS credentials file to use when uploading to S3 */ - private String s3CredentialsFilename; - - /** The number of servers that have successfully been deployed to */ - private DeployStatus status; - - /** The deployment to deploy */ - private Deployment deployment; - - public DeployJob(Deployment deployment, String owner, List targets, String publicUrl, String s3Bucket, String s3CredentialsFilename) { - super(owner); - this.deployment = deployment; - this.targets = targets; - this.publicUrl = publicUrl; - this.s3Bucket = s3Bucket; - this.s3CredentialsFilename = s3CredentialsFilename; - this.status = new DeployStatus(); - this.name = "Deploying " + deployment.name; - status.message = "Initializing..."; - status.built = false; - status.numServersCompleted = 0; - status.totalServers = targets == null ? 0 : targets.size(); - } - - public DeployStatus getStatus () { - synchronized (status) { - return status.clone(); - } - } - - @Override - public void handleStatusEvent(Map statusMap) { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } - - public void run() { - - int totalTasks = 1 + targets.size(); - int tasksCompleted = 0; - - // create a temporary file in which to save the deployment - File temp; - try { - temp = File.createTempFile("deployment", ".zip"); - } catch (IOException e) { - LOG.error("Could not create temp file"); - e.printStackTrace(); - - synchronized (status) { - status.error = true; - status.completed = true; - status.message = "app.deployment.error.dump"; - } - - return; - } - - LOG.info("Created deployment bundle file: " + temp.getAbsolutePath()); - - // dump the deployment bundle - try { - synchronized (status) { - status.message = "Creating OTP Bundle"; - } - this.deployment.dump(temp, true, true, true); - tasksCompleted++; - } catch (Exception e) { - LOG.error("Error dumping deployment"); - e.printStackTrace(); - - synchronized (status) { - status.error = true; - status.completed = true; - status.message = "app.deployment.error.dump"; - } - - return; - } - - synchronized (status) { - status.percentComplete = 100.0 * (double) tasksCompleted / totalTasks; - System.out.println("pctComplete = " + status.percentComplete); - status.built = true; - } - - // upload to S3, if applicable - if(this.s3Bucket != null) { - synchronized (status) { - status.message = "Uploading to S3"; - status.uploadingS3 = true; - } - - - try { - AWSCredentials creds; - if (this.s3CredentialsFilename != null) { - creds = new ProfileCredentialsProvider(this.s3CredentialsFilename, "default").getCredentials(); - } - else { - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - } - - TransferManager tx = new TransferManager(creds); - String key = deployment.name + ".zip"; - final Upload upload = tx.upload(this.s3Bucket, key, temp); - - upload.addProgressListener(new ProgressListener() { - public void progressChanged(ProgressEvent progressEvent) { - synchronized (status) { - status.percentUploaded = upload.getProgress().getPercentTransferred(); - } - } - }); - - upload.waitForCompletion(); - tx.shutdownNow(); - - // copy to [name]-latest.zip - String copyKey = deployment.getProject().name.toLowerCase() + "-latest.zip"; - AmazonS3 s3client = new AmazonS3Client(creds); - CopyObjectRequest copyObjRequest = new CopyObjectRequest( - this.s3Bucket, key, this.s3Bucket, copyKey); - s3client.copyObject(copyObjRequest); - - } catch (AmazonClientException|InterruptedException e) { - LOG.error("Error uploading deployment bundle to S3"); - e.printStackTrace(); - - synchronized (status) { - status.error = true; - status.completed = true; - status.message = "app.deployment.error.dump"; - } - - return; - } - - synchronized (status) { - status.uploadingS3 = false; - } - } - - // if no OTP targets (i.e. we're only deploying to S3), we're done - if(this.targets == null) { - synchronized (status) { - status.completed = true; - } - - return; - } - - // figure out what router we're using - String router = deployment.routerId != null ? deployment.routerId : "default"; - - // load it to OTP - for (String rawUrl : this.targets) { - synchronized (status) { - status.message = "Deploying to " + rawUrl; - status.uploading = true; - } - - URL url; - try { - url = new URL(rawUrl + "/routers/" + router); - } catch (MalformedURLException e) { - LOG.error("Malformed deployment URL {}", rawUrl); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.config"; - } - - continue; - } - - // grab them synchronously, so that we only take down one OTP server at a time - HttpURLConnection conn; - try { - conn = (HttpURLConnection) url.openConnection(); - } catch (IOException e) { - LOG.error("Unable to open URL of OTP server {}", url); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.net"; - } - - continue; - } - - conn.addRequestProperty("Content-Type", "application/zip"); - conn.setDoOutput(true); - // graph build can take a long time but not more than an hour, I should think - conn.setConnectTimeout(60 * 60 * 1000); - conn.setFixedLengthStreamingMode(temp.length()); - - // this makes it a post request so that we can upload our file - WritableByteChannel post; - try { - post = Channels.newChannel(conn.getOutputStream()); - } catch (IOException e) { - LOG.error("Could not open channel to OTP server {}", url); - e.printStackTrace(); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.net"; - status.completed = true; - } - - return; - } - - // get the input file - FileChannel input; - try { - input = new FileInputStream(temp).getChannel(); - } catch (FileNotFoundException e) { - LOG.error("Internal error: could not read dumped deployment!"); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.dump"; - status.completed = true; - } - - return; - } - - try { - conn.connect(); - } catch (IOException e) { - LOG.error("Unable to open connection to OTP server {}", url); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.net"; - status.completed = true; - } - - return; - } - - // copy - try { - input.transferTo(0, Long.MAX_VALUE, post); - } catch (IOException e) { - LOG.error("Unable to transfer deployment to server {}" , url); - e.printStackTrace(); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.net"; - status.completed = true; - } - - return; - } - - try { - post.close(); - } catch (IOException e) { - LOG.error("Error finishing connection to server {}", url); - e.printStackTrace(); - - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.net"; - status.completed = true; - } - - return; - } - - try { - input.close(); - } catch (IOException e) { - // do nothing - } - - synchronized (status) { - status.uploading = false; - } - - // wait for the server to build the graph - // TODO: timeouts? - try { - if (conn.getResponseCode() != HttpURLConnection.HTTP_CREATED) { - LOG.error("Got response code {} from server", conn.getResponseCode()); - synchronized (status) { - status.error = true; - status.message = "app.deployment.error.graph_build_failed"; - status.completed = true; - } - - // no reason to take out more servers, it's going to have the same result - return; - } - } catch (IOException e) { - LOG.error("Could not finish request to server {}", url); - - synchronized (status) { - status.completed = true; - status.error = true; - status.message = "app.deployment.error.net"; - } - } - - synchronized (status) { - status.numServersCompleted++; - tasksCompleted++; - status.percentComplete = 100.0 * (double) tasksCompleted / totalTasks; - } - } - - synchronized (status) { - status.completed = true; - status.baseUrl = this.publicUrl; - } - - jobFinished(); - - temp.deleteOnExit(); - } - - /** - * Represents the current status of this job. - */ - public static class DeployStatus extends Status { -// /** What error message (defined in messages.) should be displayed to the user? */ -// public String message; -// -// /** Is this deployment completed (successfully or unsuccessfully) */ -// public boolean completed; - -// /** Was there an error? */ -// public boolean error; - - /** Did the manager build the bundle successfully */ - public boolean built; - -// /** Is the bundle currently being uploaded to the server? */ -// public boolean uploading; - - /** Is the bundle currently being uploaded to an S3 bucket? */ - public boolean uploadingS3; - - /** How much of the bundle has been uploaded? */ - public double percentUploaded; - - /** To how many servers have we successfully deployed thus far? */ - public int numServersCompleted; - - /** How many servers are we attempting to deploy to? */ - public int totalServers; - - /** Where can the user see the result? */ - public String baseUrl; - - public DeployStatus clone () { - DeployStatus ret = new DeployStatus(); - ret.message = message; - ret.completed = completed; - ret.error = error; - ret.built = built; - ret.uploading = uploading; - ret.uploadingS3 = uploadingS3; - ret.percentComplete = percentComplete; - ret.percentUploaded = percentUploaded; - ret.numServersCompleted = numServersCompleted; - ret.totalServers = totalServers; - ret.baseUrl = baseUrl; - return ret; - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/FetchProjectFeedsJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/FetchProjectFeedsJob.java deleted file mode 100644 index a06bbd46b..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/FetchProjectFeedsJob.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.conveyal.datatools.manager.models.Project; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - -/** - * Created by landon on 3/25/16. - */ -public class FetchProjectFeedsJob extends MonitorableJob { - public static final Logger LOG = LoggerFactory.getLogger(FetchProjectFeedsJob.class); - private Project proj; - public Map result; - private Status status; - - public FetchProjectFeedsJob (Project proj, String owner) { - super(owner, "Fetching feeds for " + proj.name + " project.", JobType.FETCH_PROJECT_FEEDS); - this.proj = proj; - this.status = new Status(); - } - - @Override - public void run() { - LOG.info("fetch job running for proj: " + proj.name); - result = new HashMap<>(); - - for(FeedSource feedSource : proj.getProjectFeedSources()) { - - if (!FeedSource.FeedRetrievalMethod.FETCHED_AUTOMATICALLY.equals(feedSource.retrievalMethod)) - continue; - - FetchSingleFeedJob fetchSingleFeedJob = new FetchSingleFeedJob(feedSource, owner); - - new Thread(fetchSingleFeedJob).start(); - } - jobFinished(); - } - - @Override - public Status getStatus() { - synchronized (status) { - return status.clone(); - } - } - - @Override - public void handleStatusEvent(Map statusMap) { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/FetchSingleFeedJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/FetchSingleFeedJob.java deleted file mode 100644 index d0aab1da5..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/FetchSingleFeedJob.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.FeedVersion; - -import java.util.Map; - -public class FetchSingleFeedJob extends MonitorableJob { - - private FeedSource feedSource; - public FeedVersion result; - private Status status; - - public FetchSingleFeedJob (FeedSource feedSource, String owner) { - super(owner, "Fetching feed for " + feedSource.name, JobType.FETCH_SINGLE_FEED); - this.feedSource = feedSource; - this.result = null; - this.status = new Status(); - status.message = "Fetching..."; - status.percentComplete = 0.0; - status.uploading = true; - } - - @Override - public void run() { - // TODO: fetch automatically vs. manually vs. in-house - result = feedSource.fetch(eventBus); - jobFinished(); - if (result != null) { - new ProcessSingleFeedJob(result, this.owner).run(); - } - } - - @Override - public Status getStatus() { - synchronized (status) { - return status.clone(); - } - } - - @Override - public void handleStatusEvent(Map statusMap) { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/LoadGtfsApiFeedJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/LoadGtfsApiFeedJob.java deleted file mode 100644 index da5718e7c..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/LoadGtfsApiFeedJob.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.manager.controllers.api.GtfsApiController; -import com.conveyal.datatools.manager.models.FeedSource; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.logging.Logger; - -/** - * Created by landon on 4/30/16. - */ -public class LoadGtfsApiFeedJob implements Runnable { - public static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LoadGtfsApiFeedJob.class); - - public static FeedSource feedSource; - - public LoadGtfsApiFeedJob(FeedSource feedSource) { - this.feedSource = feedSource; - } - - @Override - public void run() { - File latest = feedSource.getLatest() != null ? feedSource.getLatest().getGtfsFile() : null; - if (latest != null) - try { - LOG.info("Loading feed into GTFS api: " + feedSource.id); - GtfsApiController.gtfsApi.registerFeedSource(feedSource.id, latest); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/NotifyUsersForSubscriptionJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/NotifyUsersForSubscriptionJob.java deleted file mode 100644 index 225b61606..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/NotifyUsersForSubscriptionJob.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.Project; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -import static com.conveyal.datatools.manager.auth.Auth0Users.getUsersBySubscription; -import static com.conveyal.datatools.manager.utils.NotificationsUtils.sendNotification; - -/** - * Created by landon on 6/6/16. - */ -public class NotifyUsersForSubscriptionJob implements Runnable { - private ObjectMapper mapper = new ObjectMapper(); - public static final Logger LOG = LoggerFactory.getLogger(NotifyUsersForSubscriptionJob.class); - private String subscriptionType; - private String target; - private String message; - - public NotifyUsersForSubscriptionJob(String subscriptionType, String target, String message) { - this.subscriptionType = subscriptionType; - this.target = target; - this.message = message; - } - - @Override - public void run() { - notifyUsersForSubscription(); - } - - // TODO: modify method so that it receives both a feed param and a updateFor param? - public void notifyUsersForSubscription() { - if (!DataManager.config.get("application").get("notifications_enabled").asBoolean()) { - return; - } - String userString = getUsersBySubscription(this.subscriptionType, this.target); - JsonNode subscribedUsers = null; - try { - subscribedUsers = this.mapper.readTree(userString); - } catch (IOException e) { - e.printStackTrace(); - } - for (JsonNode user : subscribedUsers) { - if (!user.has("email")) { - continue; - } - String email = user.get("email").asText(); - Boolean emailVerified = user.get("email_verified").asBoolean(); - LOG.info("sending notification to {}", email); - - // only send email if address has been verified - if (emailVerified) { - try { - String subject; - String url; - String bodyAction; - String[] subType = this.subscriptionType.split("-"); - switch (subType[0]) { - case "feed": - FeedSource fs = FeedSource.get(this.target); - subject = DataManager.getConfigPropertyAsText("application.title")+ " Notification: " + this.subscriptionType.replace("-", " ") + " (" + fs.name + ")"; - url = DataManager.getConfigPropertyAsText("application.public_url"); - bodyAction = "

    View this feed.

    "; - sendNotification(email, subject, "Body", "

    " + this.message + bodyAction); - break; - case "project": - Project p = Project.get(this.target); - subject = "Datatools Notification: " + this.subscriptionType.replace("-", " ") + " (" + p.name + ")"; - url = DataManager.getConfigPropertyAsText("application.public_url"); - bodyAction = "

    View this project.

    "; - sendNotification(email, subject, "Body", "

    " + this.message + bodyAction); - break; - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/ProcessSingleFeedJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/ProcessSingleFeedJob.java deleted file mode 100644 index 84f8c22a4..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/ProcessSingleFeedJob.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.editor.jobs.ProcessGtfsSnapshotMerge; -import com.conveyal.datatools.editor.models.Snapshot; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedVersion; - -import java.util.Collection; - - -/** - * Process/validate a single GTFS feed - * @author mattwigway - * - */ -public class ProcessSingleFeedJob implements Runnable { - FeedVersion feedVersion; - private String owner; - - /** - * Create a job for the given feed version. - * @param feedVersion - */ - public ProcessSingleFeedJob (FeedVersion feedVersion, String owner) { - this.feedVersion = feedVersion; - this.owner = owner; - } - - public void run() { - - // set up the validation job to run first - ValidateFeedJob validateJob = new ValidateFeedJob(feedVersion, owner); - - // use this FeedVersion to seed Editor DB provided no snapshots for feed already exist - if(DataManager.isModuleEnabled("editor")) { - // chain snapshot-creation job if no snapshots currently exist for feed - if (Snapshot.getSnapshots(feedVersion.feedSourceId).size() == 0) { - ProcessGtfsSnapshotMerge processGtfsSnapshotMergeJob = new ProcessGtfsSnapshotMerge(feedVersion, owner); - validateJob.addNextJob(processGtfsSnapshotMergeJob); - } - } - - // chain on a network builder job, if applicable - if(DataManager.isModuleEnabled("validator")) { - validateJob.addNextJob(new BuildTransportNetworkJob(feedVersion, owner)); - } - - new Thread(validateJob).start(); - - // notify any extensions of the change - for(String resourceType : DataManager.feedResources.keySet()) { - DataManager.feedResources.get(resourceType).feedVersionCreated(feedVersion, null); - } - - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/ValidateFeedJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/ValidateFeedJob.java deleted file mode 100644 index 7b283a28c..000000000 --- a/src/main/java/com/conveyal/datatools/manager/jobs/ValidateFeedJob.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.conveyal.datatools.manager.jobs; - -import com.conveyal.datatools.common.status.MonitorableJob; -import com.conveyal.datatools.common.status.StatusEvent; -import com.conveyal.datatools.manager.models.FeedVersion; -import com.google.common.eventbus.Subscribe; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; - -/** - * Created by demory on 6/16/16. - */ -public class ValidateFeedJob extends MonitorableJob { - public static final Logger LOG = LoggerFactory.getLogger(ValidateFeedJob.class); - - private FeedVersion feedVersion; - private Status status; - - public ValidateFeedJob(FeedVersion version, String owner) { - super(owner, "Validating Feed for " + version.getFeedSource().name, JobType.VALIDATE_FEED); - feedVersion = version; - status = new Status(); - status.message = "Waiting to begin validation..."; - status.percentComplete = 0; - } - - @Override - public Status getStatus() { - synchronized (status) { - return status.clone(); - } - } - - public String getFeedVersionId() { - return feedVersion.id; - } - - @Override - public void run() { - LOG.info("Running ValidateFeedJob for {}", feedVersion.id); - synchronized (status) { - status.message = "Running validation..."; - status.percentComplete = 30; - } - feedVersion.validate(eventBus); - synchronized (status) { - status.message = "Saving validation..."; - status.percentComplete = 90; - } - feedVersion.save(); - synchronized (status) { - status.message = "Validation complete!"; - status.percentComplete = 100; - } - jobFinished(); - } - - @Override - public void handleStatusEvent(Map statusMap) { - synchronized (status) { - status.message = (String) statusMap.get("message"); - status.percentComplete = (double) statusMap.get("percentComplete"); - status.error = (boolean) statusMap.get("error"); - } - } - -// public void handleGTFSValidationEvent(GTFSValidationEvent gtfsValidationEvent) { -// -// } -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/Deployment.java b/src/main/java/com/conveyal/datatools/manager/models/Deployment.java deleted file mode 100644 index ab648ac52..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/Deployment.java +++ /dev/null @@ -1,553 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.persistence.DataStore; -import com.conveyal.datatools.manager.utils.StringUtils; -import com.conveyal.datatools.manager.utils.json.JsonManager; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.io.ByteStreams; - -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A deployment of (a given version of) OTP on a given set of feeds. - * @author mattwigway - * - */ -@JsonInclude(Include.ALWAYS) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Deployment extends Model implements Serializable { - private static final long serialVersionUID = 1L; - private static final Logger LOG = LoggerFactory.getLogger(Deployment.class); - private static DataStore deploymentStore = new DataStore("deployments"); - - public String name; - - /** What server is this currently deployed to? */ - public String deployedTo; - - public Date dateCreated; - - @JsonView(JsonViews.DataDump.class) - public String projectId; - - @JsonView(JsonViews.UserInterface.class) - public Project getProject () { - return Project.get(projectId); - } - - public void setProject (Project project) { - this.projectId = project.id; - } - - @JsonView(JsonViews.DataDump.class) - public Collection feedVersionIds; - - /** All of the feed versions used in this deployment */ - @JsonIgnore - public List getFullFeedVersions () { - ArrayList ret = new ArrayList(feedVersionIds.size()); - - for (String id : feedVersionIds) { - FeedVersion v = FeedVersion.get(id); - if (v != null) - ret.add(v); - else - LOG.error("Reference integrity error for deployment {} ({}), feed version {} does not exist", this.name, this.id, id); - } - - return ret; - } - - /** All of the feed versions used in this deployment, summarized so that the Internet won't break */ - @JsonView(JsonViews.UserInterface.class) - public List getFeedVersions () { - ArrayList ret = new ArrayList(feedVersionIds.size()); - - for (String id : feedVersionIds) { - FeedVersion v = FeedVersion.get(id); - - // should never happen but can if someone monkeyed around with dump/restore - if (v != null) - ret.add(new SummarizedFeedVersion(FeedVersion.get(id))); - else - LOG.error("Reference integrity error for deployment {} ({}), feed version {} does not exist", this.name, this.id, id); - } - - return ret; - } - - public void setFeedVersions (Collection versions) { - feedVersionIds = new ArrayList(versions.size()); - - for (FeedVersion version : versions) { - feedVersionIds.add(version.id); - } - } - - // future use - public String osmFileId; - - /** The commit of OTP being used on this deployment */ - public String otpCommit; - - /** - * The routerId of this deployment - */ - public String routerId; - - /** - * If this deployment is for a single feed source, the feed source this deployment is for. - */ - public String feedSourceId; - - /** - * Feed sources that had no valid feed versions when this deployment was created, and ergo were not added. - */ - @JsonInclude(Include.ALWAYS) - @JsonView(JsonViews.DataDump.class) - public Collection invalidFeedSourceIds; - - /** - * Get all of the feed sources which could not be added to this deployment. - */ - @JsonView(JsonViews.UserInterface.class) - @JsonInclude(Include.ALWAYS) - public List getInvalidFeedSources () { - if (invalidFeedSourceIds == null) - return null; - - ArrayList ret = new ArrayList(invalidFeedSourceIds.size()); - - for (String id : invalidFeedSourceIds) { - ret.add(FeedSource.get(id)); - } - - return ret; - } - - /** Create a single-agency (testing) deployment for the given feed source */ - public Deployment(FeedSource feedSource) { - super(); - - this.feedSourceId = feedSource.id; - this.setProject(feedSource.getProject()); - this.dateCreated = new Date(); - this.feedVersionIds = new ArrayList(); - - DateFormat df = new SimpleDateFormat("yyyyMMdd"); - - this.name = StringUtils.getCleanName(feedSource.name) + "_" + df.format(dateCreated); - - // always use the latest, no matter how broken it is, so we can at least see how broken it is - this.feedVersionIds.add(feedSource.getLatestVersionId()); - - this.routerId = StringUtils.getCleanName(feedSource.name) + "_" + feedSourceId; - - this.deployedTo = null; - } - - /** Create a new deployment plan for the given feed collection */ - public Deployment(Project project) { - super(); - - this.feedSourceId = null; - - this.setProject(project); - - this.dateCreated = new Date(); - - this.feedVersionIds = new ArrayList(); - this.invalidFeedSourceIds = new ArrayList(); - - FEEDSOURCE: for (FeedSource s : project.getProjectFeedSources()) { - // only include deployable feeds - if (s.deployable) { - FeedVersion latest = s.getLatest(); - - // find the newest version that can be deployed - while (true) { - if (latest == null) { - invalidFeedSourceIds.add(s.id); - continue FEEDSOURCE; - } - - if (!latest.hasCriticalErrors()) { - break; - } - - latest = latest.getPreviousVersion(); - } - - // this version is the latest good version - this.feedVersionIds.add(latest.id); - } - } - - this.deployedTo = null; - } - - /** - * Create an empty deployment, for use with dump/restore. - */ - public Deployment() { - // do nothing. - } - - /** Get a deployment by ID */ - public static Deployment get (String id) { - return deploymentStore.getById(id); - } - - /** Save this deployment and commit it */ - public void save () { - this.save(true); - } - - /** Save this deployment */ - public void save (boolean commit) { - if (commit) - deploymentStore.save(id, this); - else - deploymentStore.saveWithoutCommit(id, this); - } - - /** - * Delete this deployment and everything that it contains. - */ - public void delete() { - deploymentStore.delete(this.id); - } - - /** Dump this deployment to the given file - * @param output the output file - * @param includeOsm should an osm.pbf file be included in the dump? - * @param includeOtpConfig should OTP build-config.json and router-config.json be included? - */ - public void dump (File output, boolean includeManifest, boolean includeOsm, boolean includeOtpConfig) throws IOException { - // create the zipfile - ZipOutputStream out; - try { - out = new ZipOutputStream(new FileOutputStream(output)); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - - if (includeManifest) { - // save the manifest at the beginning of the file, for read/seek efficiency - ZipEntry manifestEntry = new ZipEntry("manifest.json"); - out.putNextEntry(manifestEntry); - - // create the json manifest - JsonManager jsonManifest = new JsonManager(Deployment.class, - JsonViews.UserInterface.class); - // this mixin gives us full feed validation results, not summarized - jsonManifest.addMixin(Deployment.class, DeploymentFullFeedVersionMixin.class); - - byte[] manifest = jsonManifest.write(this).getBytes(); - - out.write(manifest); - - out.closeEntry(); - } - - // write each of the GTFS feeds - for (FeedVersion v : this.getFullFeedVersions()) { - File feed = v.getGtfsFile(); - - FileInputStream in; - - try { - in = new FileInputStream(feed); - } catch (FileNotFoundException e1) { - throw new RuntimeException(e1); - } - - ZipEntry e = new ZipEntry(feed.getName()); - out.putNextEntry(e); - - // copy the zipfile 100k at a time - int bufSize = 100 * 1024; - byte[] buff = new byte[bufSize]; - int readBytes; - - while (true) { - try { - readBytes = in.read(buff); - } catch (IOException e1) { - try { - in.close(); - } catch (IOException e2) { - throw new RuntimeException(e2); - } - throw new RuntimeException(e1); - } - - if (readBytes == -1) - // we've copied the whole file - break; - - out.write(buff, 0, readBytes); - } - - try { - in.close(); - } catch (IOException e1) { - throw new RuntimeException(e1); - } - - out.closeEntry(); - } - - if (includeOsm) { - // extract OSM and insert it into the deployment bundle - ZipEntry e = new ZipEntry("osm.pbf"); - out.putNextEntry(e); - InputStream is = getOsmExtract(getProjectBounds()); - ByteStreams.copy(is, out); - try { - is.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - - out.closeEntry(); - } - - if (includeOtpConfig) { - // write build-config.json and router-config.json - Project proj = this.getProject(); - - if (proj.buildConfig != null) { - ZipEntry buildConfigEntry = new ZipEntry("build-config.json"); - out.putNextEntry(buildConfigEntry); - - ObjectMapper mapper = new ObjectMapper(); - mapper.setSerializationInclusion(Include.NON_NULL); - byte[] buildConfig = mapper.writer().writeValueAsBytes(proj.buildConfig); - out.write(buildConfig); - - out.closeEntry(); - } - - String brandingUrlRoot = DataManager.config - .get("application").get("data").get("branding_public").asText(); - OtpRouterConfig routerConfig = proj.routerConfig; - if (routerConfig == null && brandingUrlRoot != null) { - routerConfig = new OtpRouterConfig(); - } - if (routerConfig != null) { - routerConfig.brandingUrlRoot = brandingUrlRoot; - ZipEntry routerConfigEntry = new ZipEntry("router-config.json"); - out.putNextEntry(routerConfigEntry); - - ObjectMapper mapper = new ObjectMapper(); - mapper.setSerializationInclusion(Include.NON_NULL); - out.write(mapper.writer().writeValueAsBytes(routerConfig)); - - out.closeEntry(); - } - } - - out.close(); - } - - // Get OSM extract - public static InputStream getOsmExtract(Rectangle2D bounds) { - // call vex server - URL vexUrl = null; - try { - vexUrl = new URL(String.format("%s/?n=%.6f&e=%.6f&s=%.6f&w=%.6f", - DataManager.getConfigPropertyAsText("application.osm_vex"), - bounds.getMaxY(), bounds.getMaxX(), bounds.getMinY(), bounds.getMinX())); - } catch (MalformedURLException e1) { - e1.printStackTrace(); - } - LOG.info("Getting OSM extract at " + vexUrl.toString()); - HttpURLConnection conn = null; - try { - conn = (HttpURLConnection) vexUrl.openConnection(); - } catch (IOException e1) { - e1.printStackTrace(); - } - try { - conn.connect(); - } catch (IOException e1) { - e1.printStackTrace(); - } - - InputStream is = null; - try { - is = conn.getInputStream(); - } catch (IOException e1) { - e1.printStackTrace(); - } - return is; - } - - public static Rectangle2D getFeedVersionBounds(FeedVersion version) { - return null; - } - - // Get the union of the bounds of all the feeds in this deployment - @JsonView(JsonViews.UserInterface.class) - public Rectangle2D getProjectBounds() { - - Project proj = this.getProject(); - if(proj.useCustomOsmBounds != null && proj.useCustomOsmBounds) { - Rectangle2D bounds = new Rectangle2D.Double(proj.osmWest, proj.osmSouth, - proj.osmEast - proj.osmWest, proj.osmNorth - proj.osmSouth); - return bounds; - } - - List versions = getFeedVersions(); - - if (versions.size() == 0) - return null; - - Rectangle2D bounds = new Rectangle2D.Double(); - boolean boundsSet = false; - - // i = 1 because we've already included bounds 0 - for (int i = 0; i < versions.size(); i++) { - SummarizedFeedVersion version = versions.get(i); -// return getFeedVersionBounds(version); - if (version.validationResult != null && version.validationResult.bounds != null) { - if (!boundsSet) { - // set the bounds, don't expand the null bounds - bounds.setRect(versions.get(0).validationResult.bounds); - boundsSet = true; - } else { - bounds.add(version.validationResult.bounds); - } - } - else - LOG.warn("Feed version %s has no bounds", version); - } - - // expand the bounds by (about) 10 km in every direction - double degreesPerKmLat = 360D / 40008; - double degreesPerKmLon = - // the circumference of the chord of the earth at this latitude - 360 / - (2 * Math.PI * 6371 * Math.cos(Math.toRadians(bounds.getCenterY()))); - - - double bufferKm = 10; - if(DataManager.config.get("modules").get("deployment").has("osm_buffer_km")) { - bufferKm = DataManager.config.get("modules").get("deployment").get("osm_buffer_km").asDouble(); - } - - // south-west - bounds.add(new Point2D.Double( - // lon - bounds.getMinX() - bufferKm * degreesPerKmLon, - bounds.getMinY() - bufferKm * degreesPerKmLat - )); - - // north-east - bounds.add(new Point2D.Double( - // lon - bounds.getMaxX() + bufferKm * degreesPerKmLon, - bounds.getMaxY() + bufferKm * degreesPerKmLat - )); - - return bounds; - } - - /** - * Commit changes to the datastore - */ - public static void commit () { - deploymentStore.commit(); - } - - /** - * Get all of the deployments. - */ - public static Collection getAll () { - return deploymentStore.getAll(); - } - - /** - * Get the deployment currently deployed to a particular server. - */ - public static Deployment getDeploymentForServerAndRouterId (String server, String routerId) { - for (Deployment d : getAll()) { - if (d.deployedTo != null && d.deployedTo.equals(server)) { - if ((routerId != null && routerId.equals(d.routerId)) || d.routerId == routerId) { - return d; - } - } - } - - return null; - } - - /** - * A summary of a FeedVersion, leaving out all of the individual validation errors. - */ - public static class SummarizedFeedVersion { - public FeedValidationResultSummary validationResult; - public FeedSource feedSource; - public String id; - public Date updated; - public String previousVersionId; - public String nextVersionId; - public int version; - - public SummarizedFeedVersion (FeedVersion version) { - this.validationResult = new FeedValidationResultSummary(version.validationResult); - this.feedSource = version.getFeedSource(); - this.updated = version.updated; - this.id = version.id; - this.nextVersionId = version.getNextVersionId(); - this.previousVersionId = version.getPreviousVersionId(); - this.version = version.version; - } - } - - /** - * A MixIn to be applied to this deployment, for generating manifests, so that full feed versions appear rather than - * summarized feed versions. - * - * Usually a mixin would be used on an external class, but since we are changing one thing about a single class, it seemed - * unnecessary to define a new view just for generating deployment manifests. - */ - public abstract static class DeploymentFullFeedVersionMixin { - @JsonIgnore - public abstract Collection getFeedVersions(); - - @JsonProperty("feedVersions") - @JsonIgnore(false) - public abstract Collection getFullFeedVersions (); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/ExternalFeedSourceProperty.java b/src/main/java/com/conveyal/datatools/manager/models/ExternalFeedSourceProperty.java deleted file mode 100644 index 364d65d5b..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/ExternalFeedSourceProperty.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.manager.persistence.DataStore; - -import java.util.Collection; - -/** - * Created by demory on 3/30/16. - */ -public class ExternalFeedSourceProperty extends Model { - private static final long serialVersionUID = 1L; - - private static DataStore propertyStore = new DataStore<>("externalFeedSourceProperties"); - - private FeedSource feedSource; - - public ExternalFeedSourceProperty(FeedSource feedSource, String resourceType, String name, String value) { - this.id = feedSource.id + "_" + resourceType + "_" + name; - this.feedSource = feedSource; - this.resourceType = resourceType; - this.name = name; - this.value = value; - } - - public String getFeedSourceId() { - return feedSource.id; - } - - public String resourceType; - - public String name; - - public String value; - - public void save () { - save(true); - } - - public void save (boolean commit) { - if (commit) - propertyStore.save(id, this); - else - propertyStore.saveWithoutCommit(id, this); - } - - public static ExternalFeedSourceProperty find(FeedSource source, String resourceType, String name) { - return propertyStore.getById(source.id + "_" +resourceType + "_" + name); - } - - public static ExternalFeedSourceProperty updateOrCreate(FeedSource source, String resourceType, String name, String value) { - ExternalFeedSourceProperty prop = - ExternalFeedSourceProperty.find(source, resourceType, name); - - if(prop == null) { - prop = new ExternalFeedSourceProperty(source, resourceType, name, value); - } - else prop.value = value; - - prop.save(); - - return prop; - } - - public static Collection getAll () { - return propertyStore.getAll(); - } - - public void delete() { - propertyStore.delete(this.id); - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedDownloadToken.java b/src/main/java/com/conveyal/datatools/manager/models/FeedDownloadToken.java deleted file mode 100644 index 1643d28ad..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/FeedDownloadToken.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.manager.persistence.DataStore; - -import java.util.Date; - -/** - * A one-time token used to download a feed. - * - * Created by demory on 4/14/16. - */ -public class FeedDownloadToken extends Model { - - private static DataStore tokenStore = new DataStore("feeddownloadtokens"); - - private String feedVersionId; - - private Date timestamp; - - public FeedDownloadToken (FeedVersion feedVersion) { - super(); - feedVersionId = feedVersion.id; - timestamp = new Date(); - } - - public FeedDownloadToken (Project project) { - super(); - feedVersionId = project.id; - timestamp = new Date(); - } - - public static FeedDownloadToken get (String id) { - return tokenStore.getById(id); - } - - public FeedVersion getFeedVersion () { - return FeedVersion.get(feedVersionId); - } - - public Project getProject () { - return Project.get(feedVersionId); - } - - public boolean isValid () { - return true; - } - - public void save () { - tokenStore.save(id, this); - } - - public void delete () { - tokenStore.delete(id); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java b/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java deleted file mode 100644 index f4b576a45..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java +++ /dev/null @@ -1,510 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.editor.datastore.FeedTx; -import com.conveyal.datatools.editor.datastore.GlobalTx; -import com.conveyal.datatools.editor.datastore.VersionedDataStore; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.jobs.NotifyUsersForSubscriptionJob; -import com.conveyal.datatools.manager.persistence.DataStore; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonView; -import com.google.common.eventbus.EventBus; -import org.mapdb.Fun; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; -import java.util.stream.Collectors; - -import static spark.Spark.halt; - -/** - * Created by demory on 3/22/16. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class FeedSource extends Model implements Cloneable { - private static final long serialVersionUID = 1L; - - public static final Logger LOG = LoggerFactory.getLogger(FeedSource.class); - - private static DataStore sourceStore = new DataStore("feedsources"); - - /** - * The collection of which this feed is a part - */ - //@JsonView(JsonViews.DataDump.class) - public String projectId; - - public String[] regions = {"1"}; - /** - * Get the Project of which this feed is a part - */ - @JsonIgnore - public Project getProject () { - return Project.get(projectId); - } - - @JsonIgnore - public List getRegionList () { - return Region.getAll().stream().filter(r -> Arrays.asList(regions).contains(r.id)).collect(Collectors.toList()); - } - - public void setProject(Project proj) { - this.projectId = proj.id; - this.save(); - proj.save(); - } - - /** The name of this feed source, e.g. MTA New York City Subway */ - public String name; - - /** Is this feed public, i.e. should it be listed on the - * public feeds page for download? - */ - public boolean isPublic; - - /** Is this feed deployable? */ - public boolean deployable; - - /** - * How do we receive this feed? - */ - public FeedRetrievalMethod retrievalMethod; - - /** - * When was this feed last fetched? - */ - public Date lastFetched; - - /** - * When was this feed last updated? - */ - public transient Date lastUpdated; - - /** - * From whence is this feed fetched? - */ - public URL url; - - /** - * What is the GTFS Editor snapshot for this feed? - * - * This is the String-formatted snapshot ID, which is the base64-encoded ID and the version number. - */ - public String snapshotVersion; - - /** - * Create a new feed. - */ - public FeedSource (String name) { - super(); - this.name = name; - this.retrievalMethod = FeedRetrievalMethod.MANUALLY_UPLOADED; - } - - /** - * No-arg constructor to yield an uninitialized feed source, for dump/restore. - * Should not be used in general code. - */ - public FeedSource () { - this(null); - } - - /** - * Fetch the latest version of the feed. - */ - public FeedVersion fetch (EventBus eventBus) { - Map statusMap = new HashMap<>(); - statusMap.put("message", "Downloading file"); - statusMap.put("percentComplete", 20.0); - statusMap.put("error", false); - eventBus.post(statusMap); - -// if (this.retrievalMethod.equals(FeedRetrievalMethod.MANUALLY_UPLOADED)) { -// String message = String.format("not fetching feed %s, not a fetchable feed", this.name); -// LOG.info(message); -// statusMap.put("message", message); -// statusMap.put("percentComplete", 0.0); -// statusMap.put("error", true); -// eventBus.post(statusMap); -// return null; -// } - - // fetchable feed, continue - FeedVersion latest = getLatest(); - - // We create a new FeedVersion now, so that the fetched date is (milliseconds) before - // fetch occurs. That way, in the highly unlikely event that a feed is updated while we're - // fetching it, we will not miss a new feed. - FeedVersion newFeed = new FeedVersion(this); - - // build the URL from which to fetch - URL url; -// if (this.retrievalMethod.equals(FeedRetrievalMethod.FETCHED_AUTOMATICALLY)) - url = this.url; -// else if (this.retrievalMethod.equals(FeedRetrievalMethod.PRODUCED_IN_HOUSE)) { -// if (this.snapshotVersion == null) { -// String message = String.format("Feed %s has no editor id; cannot fetch", this.name); -// LOG.error(message); -// statusMap.put("message", message); -// statusMap.put("percentComplete", 0.0); -// statusMap.put("error", true); -// eventBus.post(statusMap); -// return null; -// } -// -// String baseUrl = DataManager.getConfigPropertyAsText("modules.editor.url"); -// -// if (!baseUrl.endsWith("/")) -// baseUrl += "/"; -// -// // build the URL -// try { -// url = new URL(baseUrl + "api/mgrsnapshot/" + this.snapshotVersion + ".zip"); -// } catch (MalformedURLException e) { -// String message = "Invalid URL for editor, check your config."; -// LOG.error(message); -// statusMap.put("message", message); -// statusMap.put("percentComplete", 0.0); -// statusMap.put("error", true); -// eventBus.post(statusMap); -// return null; -// } -// } -// else { -// String message = "Unknown retrieval method: " + this.retrievalMethod; -// LOG.error(message); -// statusMap.put("message", message); -// statusMap.put("percentComplete", 0.0); -// statusMap.put("error", true); -// eventBus.post(statusMap); -// return null; -// } - - LOG.info(url.toString()); - - // make the request, using the proper HTTP caching headers to prevent refetch, if applicable - HttpURLConnection conn; - try { - conn = (HttpURLConnection) url.openConnection(); - } catch (IOException e) { - String message = String.format("Unable to open connection to %s; not fetching feed %s", url, this.name); - LOG.error(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 0.0); - statusMap.put("error", true); - eventBus.post(statusMap); - halt(400, message); - return null; - } catch (ClassCastException e) { - String message = String.format("Unable to open connection to %s; not fetching %s feed", url, this.name); - LOG.error(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 0.0); - statusMap.put("error", true); - eventBus.post(statusMap); - halt(400, message); - return null; - } catch (NullPointerException e) { - String message = String.format("Unable to open connection to %s; not fetching %s feed", url, this.name); - LOG.error(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 0.0); - statusMap.put("error", true); - eventBus.post(statusMap); - halt(400, message); - return null; - } - - conn.setDefaultUseCaches(true); - - /*if (oauthToken != null) - conn.addRequestProperty("Authorization", "Bearer " + oauthToken);*/ - - // lastFetched is set to null when the URL changes and when latest feed version is deleted - if (latest != null && this.lastFetched != null) - conn.setIfModifiedSince(Math.min(latest.updated.getTime(), this.lastFetched.getTime())); - - try { - conn.connect(); - - if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { - String message = String.format("Feed %s has not been modified", this.name); - LOG.info(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 100.0); - statusMap.put("error", true); - eventBus.post(statusMap); - halt(304, message); - return null; - } - - // TODO: redirects - else if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - String message = String.format("Saving %s feed", this.name); - LOG.info(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 100.0); - statusMap.put("error", false); - eventBus.post(statusMap); - File out = newFeed.newGtfsFile(conn.getInputStream()); - } - - else { - String message = String.format("HTTP status %s retrieving %s feed", conn.getResponseMessage(), this.name); - LOG.error(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 100.0); - statusMap.put("error", true); - eventBus.post(statusMap); - return null; - } - } catch (IOException e) { - String message = String.format("Unable to connect to %s; not fetching %s feed", url, this.name); - LOG.error(message); - statusMap.put("message", message); - statusMap.put("percentComplete", 100.0); - statusMap.put("error", true); - eventBus.post(statusMap); - return null; - } - - // validate the fetched file - // note that anything other than a new feed fetched successfully will have already returned from the function - newFeed.hash(); - - if (latest != null && newFeed.hash.equals(latest.hash)) { - String message = String.format("Feed %s was fetched but has not changed; server operators should add If-Modified-Since support to avoid wasting bandwidth", this.name); - LOG.warn(message); - newFeed.getGtfsFile().delete(); - statusMap.put("message", message); - statusMap.put("percentComplete", 100.0); - statusMap.put("error", false); - eventBus.post(statusMap); - return null; - } - else { - newFeed.userId = this.userId; - - this.lastFetched = newFeed.updated; - this.save(); - - NotifyUsersForSubscriptionJob notifyFeedJob = new NotifyUsersForSubscriptionJob("feed-updated", this.id, "New feed version created for " + this.name); - Thread notifyThread = new Thread(notifyFeedJob); - notifyThread.start(); - - return newFeed; - } - } - - public int compareTo(FeedSource o) { - return this.name.compareTo(o.name); - } - - public String toString () { - return ""; - } - - public void save () { - save(true); - } - public void setName(String name){ - this.name = name; - this.save(); - } - public void save (boolean commit) { - if (commit) - sourceStore.save(this.id, this); - else - sourceStore.saveWithoutCommit(this.id, this); - } - - /** - * Get the latest version of this feed - * @return the latest version of this feed - */ - @JsonIgnore - public FeedVersion getLatest () { -// DataStore vs = new FeedVersion(this); -// if (vs == null){ -// return null; -// } - FeedVersion v = FeedVersion.versionStore.findFloor("version", new Fun.Tuple2(this.id, Fun.HI)); - - // the ID doesn't necessarily match, because it will fall back to the previous source in the store if there are no versions for this source - if (v == null || !v.feedSourceId.equals(this.id)) - return null; - - return v; - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public String getLatestVersionId () { - FeedVersion latest = getLatest(); - return latest != null ? latest.id : null; - } - - /** - * We can't pass the entire latest feed version back, because it contains references back to this feedsource, - * so Jackson doesn't work. So instead we specifically expose the validation results and the latest update. - * @return - */ - // TODO: use summarized feed source here. requires serious refactoring on client side. - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public Date getLastUpdated() { - FeedVersion latest = getLatest(); - return latest != null ? latest.updated : null; - } - - - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public FeedValidationResultSummary getLatestValidation () { - FeedVersion latest = getLatest(); - return latest != null ? new FeedValidationResultSummary(latest.validationResult) : null; - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public boolean getEditedSinceSnapshot() { -// FeedTx tx; -// try { -// tx = VersionedDataStore.getFeedTx(id); -// } catch (Exception e) { -// -// } -// return tx.editedSinceSnapshot.get(); - return false; - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public Map> getExternalProperties() { - - Map> resourceTable = new HashMap<>(); - - for(String resourceType : DataManager.feedResources.keySet()) { - Map propTable = new HashMap<>(); - - for (ExternalFeedSourceProperty prop : ExternalFeedSourceProperty.getAll()) { - if (prop.getFeedSourceId().equals(this.id)) { - propTable.put(prop.name, prop.value); - } - } - - resourceTable.put(resourceType, propTable); - } - return resourceTable; - } - - public static FeedSource get(String id) { - return sourceStore.getById(id); - } - - public static Collection getAll() { - return sourceStore.getAll(); - } - - /** - * Get all of the feed versions for this source - * @return - */ - @JsonIgnore - public Collection getFeedVersions() { - // TODO Indices - ArrayList ret = new ArrayList(); - - for (FeedVersion v : FeedVersion.getAll()) { - if (this.id.equals(v.feedSourceId)) { - ret.add(v); - } - } - - return ret; - } - - @JsonView(JsonViews.UserInterface.class) - public int getFeedVersionCount() { - return getFeedVersions().size(); - } - - @JsonView(JsonViews.UserInterface.class) - public int getNoteCount() { - return this.noteIds != null ? this.noteIds.size() : 0; - } - - /** - * Represents ways feeds can be retrieved - */ - public static enum FeedRetrievalMethod { - FETCHED_AUTOMATICALLY, // automatically retrieved over HTTP on some regular basis - MANUALLY_UPLOADED, // manually uploaded by someone, perhaps the agency, or perhaps an internal user - PRODUCED_IN_HOUSE // produced in-house in a GTFS Editor instance - } - - public static void commit() { - sourceStore.commit(); - } - - /** - * Delete this feed source and everything that it contains. - */ - public void delete() { - for (FeedVersion v : getFeedVersions()) { - v.delete(); - } - - // Delete editor feed mapdb - // TODO: does the mapdb folder need to be deleted separately? - GlobalTx gtx = VersionedDataStore.getGlobalTx(); - if (!gtx.feeds.containsKey(id)) { - gtx.rollback(); - } - else { - gtx.feeds.remove(id); - gtx.commit(); - } - - for (ExternalFeedSourceProperty prop : ExternalFeedSourceProperty.getAll()) { - if(prop.getFeedSourceId().equals(this.id)) { - prop.delete(); - } - } - - // TODO: add delete for osm extract and r5 network (maybe that goes with version) - - sourceStore.delete(this.id); - } - - /*@JsonIgnore - public AgencyBranding getAgencyBranding(String agencyId) { - if(branding != null) { - for (AgencyBranding agencyBranding : branding) { - if (agencyBranding.agencyId.equals(agencyId)) return agencyBranding; - } - } - return null; - } - - @JsonIgnore - public void addAgencyBranding(AgencyBranding agencyBranding) { - if(branding == null) { - branding = new ArrayList<>(); - } - branding.add(agencyBranding); - }*/ - - public FeedSource clone () throws CloneNotSupportedException { - return (FeedSource) super.clone(); - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedValidationResult.java b/src/main/java/com/conveyal/datatools/manager/models/FeedValidationResult.java deleted file mode 100644 index 85e3b7bf7..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/FeedValidationResult.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.gtfs.model.ValidationResult; -import com.conveyal.gtfs.validator.json.LoadStatus; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.awt.geom.Rectangle2D; -import java.io.Serializable; -import java.time.LocalDate; -import java.util.Collection; - -/** - * Created by landon on 5/10/16. - */ -public class FeedValidationResult implements Serializable { - @JsonProperty - public LoadStatus loadStatus; - public String loadFailureReason; - public String feedFileName; - public Collection agencies; - public ValidationResult routes; - public ValidationResult stops; - public ValidationResult trips; - public ValidationResult shapes; - public int agencyCount; - public int routeCount; - public int tripCount; - public int stopTimesCount; - public int errorCount; - public LocalDate startDate; - public LocalDate endDate; - public Rectangle2D bounds; - - public FeedValidationResult() { - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedValidationResultSummary.java b/src/main/java/com/conveyal/datatools/manager/models/FeedValidationResultSummary.java deleted file mode 100644 index 975d4064d..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/FeedValidationResultSummary.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import java.awt.geom.Rectangle2D; -import java.io.Serializable; -import java.time.ZoneId; -import java.util.Collection; -import java.util.Date; - -import com.conveyal.gtfs.validator.json.LoadStatus; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; - -/** - * Represents a subset of a feed validation result, just enough for display, without overwhelming the browser - * or sending unnecessary amounts of data over the wire - */ -public class FeedValidationResultSummary implements Serializable { - private static final long serialVersionUID = 1L; - - public LoadStatus loadStatus; - - @JsonInclude(Include.ALWAYS) - public String loadFailureReason; - public Collection agencies; - - public int errorCount; - - // statistics - public int agencyCount; - public int routeCount; - public int tripCount; - public int stopTimesCount; - - /** The first date the feed has service, either in calendar.txt or calendar_dates.txt */ - @JsonInclude(Include.ALWAYS) - public Date startDate; - - /** The last date the feed has service, either in calendar.txt or calendar_dates.txt */ - @JsonInclude(Include.ALWAYS) - public Date endDate; - - @JsonInclude(Include.ALWAYS) - public Rectangle2D bounds; - - /** - * Construct a summarized version of the given FeedValidationResult. - * @param result - */ - public FeedValidationResultSummary (FeedValidationResult result) { - if (result != null) { - this.loadStatus = result.loadStatus; - this.loadFailureReason = result.loadFailureReason; - this.agencies = result.agencies; - - if (loadStatus == LoadStatus.SUCCESS) { -// if (result.routes != null) { -// this.errorCount = -// result.routes.invalidValues.size() + -// result.stops.invalidValues.size() + -// result.trips.invalidValues.size() + -// result.shapes.invalidValues.size(); -// } - this.errorCount = result.errorCount; - this.agencyCount = result.agencyCount; - this.routeCount = result.routeCount; - this.tripCount = result.tripCount; - this.stopTimesCount = result.stopTimesCount; - this.startDate = result.startDate != null ? Date.from(result.startDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) : null; - this.endDate = result.endDate != null ? Date.from(result.endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) : null; - this.bounds = result.bounds; - } - } - - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedVersion.java b/src/main/java/com/conveyal/datatools/manager/models/FeedVersion.java deleted file mode 100644 index d4b2b8449..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/FeedVersion.java +++ /dev/null @@ -1,573 +0,0 @@ -package com.conveyal.datatools.manager.models; - - -import java.awt.geom.Rectangle2D; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.conveyal.datatools.common.status.StatusEvent; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.controllers.api.GtfsApiController; -import com.conveyal.datatools.manager.persistence.DataStore; -import com.conveyal.datatools.manager.persistence.FeedStore; -import com.conveyal.datatools.manager.utils.HashUtils; -import com.conveyal.gtfs.GTFSFeed; -import com.conveyal.gtfs.validator.json.LoadStatus; -import com.conveyal.gtfs.stats.FeedStats; -import com.conveyal.r5.point_to_point.builder.TNBuilderConfig; -import com.conveyal.r5.transit.TransportNetwork; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.core.JsonGenerationException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.eventbus.EventBus; -import com.vividsolutions.jts.geom.Geometry; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.geotools.geojson.geom.GeometryJSON; -import org.mapdb.Fun.Function2; -import org.mapdb.Fun.Tuple2; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static com.conveyal.datatools.manager.models.Deployment.getOsmExtract; -import static com.conveyal.datatools.manager.utils.StringUtils.getCleanName; -import static spark.Spark.halt; - -/** - * Represents a version of a feed. - * @author mattwigway - * - */ -@JsonInclude(Include.ALWAYS) -@JsonIgnoreProperties(ignoreUnknown = true) -public class FeedVersion extends Model implements Serializable { - private static final long serialVersionUID = 1L; - private static ObjectMapper mapper = new ObjectMapper(); - public static final Logger LOG = LoggerFactory.getLogger(FeedVersion.class); - - static DataStore versionStore = new DataStore("feedversions"); - private static FeedStore feedStore = new FeedStore(); - - static { - // set up indexing on feed versions by feed source, indexed by - versionStore.secondaryKey("version", new Function2, String, FeedVersion> () { - @Override - public Tuple2 run(String key, FeedVersion fv) { - return new Tuple2(fv.feedSourceId, fv.version); - } - }); - } - - /** - * We generate IDs manually, but we need a bit of information to do so - */ - public FeedVersion (FeedSource source) { - this.updated = new Date(); - this.feedSourceId = source.id; - - // ISO time - DateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmssX"); - - // since we store directly on the file system, this lets users look at the DB directly - this.id = getCleanName(source.name) + "_" + df.format(this.updated) + "_" + source.id + ".zip"; - - // infer the version -// FeedVersion prev = source.getLatest(); -// if (prev != null) { -// this.version = prev.version + 1; -// } -// else { -// this.version = 1; -// } - int count = source.getFeedVersionCount(); - this.version = count + 1; - } - - /** - * Create an uninitialized feed version. This should only be used for dump/restore. - */ - public FeedVersion () { - // do nothing - } - - /** The feed source this is associated with */ - @JsonView(JsonViews.DataDump.class) - public String feedSourceId; - - @JsonIgnore - public transient TransportNetwork transportNetwork; - - @JsonView(JsonViews.UserInterface.class) - public FeedSource getFeedSource () { - return FeedSource.get(feedSourceId); - } - - @JsonIgnore - public FeedVersion getPreviousVersion () { - return versionStore.find("version", new Tuple2(this.feedSourceId, this.version - 1)); - } - - @JsonView(JsonViews.UserInterface.class) - public String getPreviousVersionId () { - FeedVersion p = getPreviousVersion(); - return p != null ? p.id : null; - } - - @JsonIgnore - public FeedVersion getNextVersion () { - return versionStore.find("version", new Tuple2(this.feedSourceId, this.version + 1)); - } - - @JsonView(JsonViews.UserInterface.class) - public String getNextVersionId () { - FeedVersion p = getNextVersion(); - return p != null ? p.id : null; - } - - /** The hash of the feed file, for quick checking if the file has been updated */ - @JsonView(JsonViews.DataDump.class) - public String hash; - - @JsonIgnore - public File getGtfsFile() { - return feedStore.getFeed(id); - } - - public File newGtfsFile(InputStream inputStream) { - return feedStore.newFeed(id, inputStream, getFeedSource()); - } - - @JsonIgnore - public GTFSFeed getGtfsFeed() { -// return GTFSFeed.fromFile(getGtfsFile().getAbsolutePath()); - // TODO: fix broken GTFS cache - try { - LOG.info("Checking for feed in cache.."); - if(!DataManager.gtfsCache.containsId(this.id)) { - File f = getGtfsFile(); - LOG.info("Did not find, putting file in cache: " + f); - DataManager.gtfsCache.put(id, f); - } - return DataManager.gtfsCache.get(id); - } catch (Exception e) { - System.out.println("Exception in getGtfsFeed: " + e.getMessage()); - e.printStackTrace(); - try { - FileUtils.cleanDirectory(new File(DataManager.cacheDirectory)); -// getGtfsFeed(); - } catch (IOException e1) { - e1.printStackTrace(); - } - } - return null; - } - - /** The results of validating this feed */ - @JsonView(JsonViews.DataDump.class) - public FeedValidationResult validationResult; - -// @JsonIgnore -// public List errors; - - @JsonView(JsonViews.UserInterface.class) - public FeedValidationResultSummary getValidationSummary() { - return new FeedValidationResultSummary(validationResult); - } - - - /** When this version was uploaded/fetched */ - public Date updated; - - /** The version of the feed, starting with 0 for the first and so on */ - public int version; - - /** A name for this version. Defaults to creation date if not specified by user */ - public String name; - - public Long fileSize; - - public Long fileTimestamp; - - public String getName() { - return name != null ? name : (getFormattedTimestamp() + " Version"); - } - - @JsonIgnore - public String getFormattedTimestamp() { - SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy H:mm"); - return format.format(this.updated); - } - - public static FeedVersion get(String id) { - // TODO Auto-generated method stub - return versionStore.getById(id); - } - - public static Collection getAll() { - return versionStore.getAll(); - } - - public void validate(EventBus eventBus) { - if (eventBus == null) { - eventBus = new EventBus(); - } - Map statusMap = new HashMap<>(); - File gtfsFile = null; - try { - eventBus.post(new StatusEvent("Loading feed...", 5, false)); - gtfsFile = getGtfsFile(); - } catch (Exception e) { - String errorString = String.format("No GTFS feed exists for version: %s", this.id); - LOG.warn(errorString); - statusMap.put("message", errorString); - statusMap.put("percentComplete", 0.0); - statusMap.put("error", true); - eventBus.post(statusMap); - return; - } - - GTFSFeed gtfsFeed = getGtfsFeed(); - if(gtfsFeed == null) { - String errorString = String.format("Could not get GTFSFeed object for FeedVersion {}", id); - LOG.warn(errorString); -// eventBus.post(new StatusEvent(errorString, 0, true)); - statusMap.put("message", errorString); - statusMap.put("percentComplete", 0.0); - statusMap.put("error", true); - eventBus.post(statusMap); - return; - } - - Map tripsPerDate; - // load feed into GTFS api - // TODO: pass GTFSFeed to GTFSApi? - if (DataManager.getConfigProperty("modules.gtfsapi.enabled").asBoolean() && DataManager.getConfigProperty("modules.gtfsapi.load_on_fetch").asBoolean()) { -// LOG.info("Loading feed into GTFS api"); -// String checksum = this.hash; -// try { -// GtfsApiController.gtfsApi.registerFeedSource(this.feedSourceId, gtfsFile); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// if (GtfsApiController.feedUpdater != null) { -// GtfsApiController.feedUpdater.addFeedETag(checksum); -// } - } - - try { -// eventBus.post(new StatusEvent("Validating feed...", 30, false)); - - statusMap.put("message", "Validating feed..."); - statusMap.put("percentComplete", 30.0); - statusMap.put("error", false); - eventBus.post(statusMap); - LOG.info("Beginning validation..."); - gtfsFeed.validate(); - LOG.info("Calculating stats..."); - FeedStats stats = gtfsFeed.calculateStats(); - validationResult = new FeedValidationResult(); - validationResult.agencies = stats.getAllAgencies().stream().map(agency -> agency.agency_id).collect(Collectors.toList()); - validationResult.agencyCount = stats.getAgencyCount(); - validationResult.routeCount = stats.getRouteCount(); - validationResult.bounds = stats.getBounds(); - LocalDate calDateStart = stats.getCalendarDateStart(); - LocalDate calSvcStart = stats.getCalendarServiceRangeStart(); - - LocalDate calDateEnd = stats.getCalendarDateEnd(); - LocalDate calSvcEnd = stats.getCalendarServiceRangeEnd(); - - if (calDateStart == null && calSvcStart == null) - // no service . . . this is bad - validationResult.startDate = null; - else if (calDateStart == null) - validationResult.startDate = calSvcStart; - else if (calSvcStart == null) - validationResult.startDate = calDateStart; - else - validationResult.startDate = calDateStart.isBefore(calSvcStart) ? calDateStart : calSvcStart; - - if (calDateEnd == null && calSvcEnd == null) - // no service . . . this is bad - validationResult.endDate = null; - else if (calDateEnd == null) - validationResult.endDate = calSvcEnd; - else if (calSvcEnd == null) - validationResult.endDate = calDateEnd; - else - validationResult.endDate = calDateEnd.isAfter(calSvcEnd) ? calDateEnd : calSvcEnd; - validationResult.loadStatus = LoadStatus.SUCCESS; - validationResult.tripCount = stats.getTripCount(); - validationResult.stopTimesCount = stats.getStopTimesCount(); - validationResult.errorCount = gtfsFeed.errors.size(); - tripsPerDate = stats.getTripCountPerDateOfService(); - } catch (Exception e) { - LOG.error("Unable to validate feed {}", this); -// eventBus.post(new StatusEvent("Unable to validate feed.", 0, true)); - statusMap.put("message", "Unable to validate feed."); - statusMap.put("percentComplete", 0.0); - statusMap.put("error", true); - eventBus.post(statusMap); - e.printStackTrace(); - this.validationResult = null; -// validationResult.loadStatus = LoadStatus.OTHER_FAILURE; -// halt(400, "Error validating feed..."); - return; - } - - String s3Bucket = DataManager.config.get("application").get("data").get("gtfs_s3_bucket").asText(); - File tempFile = null; - try { -// eventBus.post(new StatusEvent("Saving validation results...", 80, false)); - statusMap.put("message", "Saving validation results..."); - statusMap.put("percentComplete", 80.0); - statusMap.put("error", false); - eventBus.post(statusMap); - // Use tempfile - tempFile = File.createTempFile(this.id, ".json"); - tempFile.deleteOnExit(); - Map validation = new HashMap<>(); - validation.put("errors", gtfsFeed.errors); - validation.put("tripsPerDate", tripsPerDate -// .entrySet() -// .stream() -// .map(entry -> entry.getKey().format(DateTimeFormatter.BASIC_ISO_DATE)) -// .collect(Collectors.toList()) - ); - GeometryJSON g = new GeometryJSON(); - Geometry buffers = gtfsFeed.getMergedBuffers(); - validation.put("mergedBuffers", buffers != null ? g.toString(buffers) : null); - mapper.writeValue(tempFile, validation); - } catch (JsonGenerationException e) { - e.printStackTrace(); - } catch (JsonMappingException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - // upload to S3, if we have bucket name and use s3 storage - if(s3Bucket != null && DataManager.getConfigProperty("application.data.use_s3_storage").asBoolean() == true) { - AWSCredentials creds; - - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - - String keyName = "validation/" + this.id + ".json"; - try { - LOG.info("Uploading validation json to S3"); - AmazonS3 s3client = new AmazonS3Client(creds); - s3client.putObject(new PutObjectRequest( - s3Bucket, keyName, tempFile)); - } catch (AmazonServiceException ase) { - LOG.error("Error uploading validation json to S3"); - } - } - // save to validation directory in gtfs folder - else { - File validationDir = new File(DataManager.getConfigPropertyAsText("application.data.gtfs") + "/validation"); - validationDir.mkdir(); - try { - FileUtils.copyFile(tempFile, new File(validationDir.toPath() + "/" + this.id + ".json")); - } catch (IOException e) { - LOG.error("Error saving validation json to local disk"); - } - } - } - public void validate() { - validate(null); - } - - public void save () { - save(true); - } - - public void save(boolean commit) { - if (commit) - versionStore.save(this.id, this); - else - versionStore.saveWithoutCommit(this.id, this); - } - - public void hash () { - this.hash = HashUtils.hashFile(getGtfsFile()); - } - - public static void commit() { - versionStore.commit(); - } - public TransportNetwork buildTransportNetwork(EventBus eventBus) { - // return null if validation result is null (probably means something went wrong with validation, plus we won't have feed bounds). - if (this.validationResult == null) { - return null; - } - - if (eventBus == null) { - eventBus = new EventBus(); - } - - String gtfsDir = DataManager.config.get("application").get("data").get("gtfs").asText() + "/"; - String feedSourceDir = gtfsDir + feedSourceId + "/"; - File fsPath = new File(feedSourceDir); - if (!fsPath.isDirectory()) { - fsPath.mkdir(); - } - // Fetch OSM extract - Map statusMap = new HashMap<>(); - statusMap.put("message", "Fetching OSM extract..."); - statusMap.put("percentComplete", 10.0); - statusMap.put("error", false); - eventBus.post(statusMap); - - Rectangle2D bounds = this.validationResult.bounds; - String osmFileName = String.format("%s%.6f_%.6f_%.6f_%.6f.osm.pbf",feedSourceDir, bounds.getMaxX(), bounds.getMaxY(), bounds.getMinX(), bounds.getMinY()); - File osmExtract = new File(osmFileName); - if (!osmExtract.exists()) { - InputStream is = getOsmExtract(this.validationResult.bounds); - OutputStream out = null; - try { - out = new FileOutputStream(osmExtract); - IOUtils.copy(is, out); - is.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - // Create/save r5 network - statusMap.put("message", "Creating transport network..."); - statusMap.put("percentComplete", 50.0); - statusMap.put("error", false); - eventBus.post(statusMap); - - List feedList = new ArrayList<>(); - feedList.add( -// DataManager.gtfsCache.get(id) - getGtfsFeed() - ); - TransportNetwork tn = TransportNetwork.fromFeeds(osmExtract.getAbsolutePath(), feedList, TNBuilderConfig.defaultConfig()); - this.transportNetwork = tn; - File tnFile = new File(feedSourceDir + this.id + "_network.dat"); - OutputStream tnOut = null; - try { - tnOut = new FileOutputStream(tnFile); - tn.write(tnOut); - return transportNetwork; - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - public TransportNetwork buildTransportNetwork() { - return buildTransportNetwork(null); - } - - - /** - * Does this feed version have any critical errors that would prevent it being loaded to OTP? - * @return - */ - public boolean hasCriticalErrors() { - if (hasCriticalErrorsExceptingDate() || (LocalDate.now()).isAfter(validationResult.endDate)) - return true; - - else - return false; - } - - /** - * Does this feed have any critical errors other than possibly being expired? - */ - public boolean hasCriticalErrorsExceptingDate () { - if (validationResult == null) - return true; - - if (validationResult.loadStatus != LoadStatus.SUCCESS) - return true; - - if (validationResult.stopTimesCount == 0 || validationResult.tripCount == 0 || validationResult.agencyCount == 0) - return true; - - return false; - } - - @JsonView(JsonViews.UserInterface.class) - public int getNoteCount() { - return this.noteIds != null ? this.noteIds.size() : 0; - } - - @JsonInclude(Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public Long getFileTimestamp() { - if (fileTimestamp != null) { - return fileTimestamp; - } - - File file = getGtfsFile(); - if(file == null) return null; - this.fileTimestamp = file.lastModified(); - return file.lastModified(); - } - - @JsonInclude(Include.NON_NULL) - @JsonView(JsonViews.UserInterface.class) - public Long getFileSize() { - if (fileSize != null) { - return fileSize; - } - - File file = getGtfsFile(); - if(file == null) return null; - this.fileSize = file.length(); - return file.length(); - } - - /** - * Delete this feed version. - */ - public void delete() { - // reset lastModified if feed is latest version - FeedSource fs = getFeedSource(); - if (fs.getLatest().id == this.id) { - fs.lastFetched = null; - } - File feed = getGtfsFile(); - if (feed != null && feed.exists()) - feed.delete(); - - /*for (Deployment d : Deployment.getAll()) { - d.feedVersionIds.remove(this.id); - }*/ - - versionStore.delete(this.id); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/JsonViews.java b/src/main/java/com/conveyal/datatools/manager/models/JsonViews.java deleted file mode 100644 index ddc838647..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/JsonViews.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.conveyal.datatools.manager.models; - -/** - * Defines all of the JSON views. - * This is basically just a list. - * @author mattwigway - * - */ -public class JsonViews { - /** - * Data that is exposed to the UI via the API. - */ - public static class UserInterface {}; - - /** - * Data that should be included in a database dump. - */ - public static class DataDump {}; -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/Model.java b/src/main/java/com/conveyal/datatools/manager/models/Model.java deleted file mode 100644 index 5c8c63c8c..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/Model.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonView; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import com.conveyal.datatools.manager.auth.Auth0UserProfile; - -import javax.persistence.MappedSuperclass; - -/** - * The base class for all of the models used by GTFS Data Manager. - * @author mattwigway - */ - -@MappedSuperclass -public abstract class Model implements Serializable { - private static final long serialVersionUID = 1L; - - public Model () { - // This autogenerates an ID - // this is OK for dump/restore, because the ID will simply be overridden - this.id = UUID.randomUUID().toString(); - } - - public String id; - - /** - * The ID of the user who owns this object. - * For accountability, every object is owned by a user. - */ - @JsonView(JsonViews.DataDump.class) - public String userId; - - /** - * Notes on this object - */ - @JsonView(JsonViews.DataDump.class) - public List noteIds; - - /** - * Get the notes for this object - */ - // notes are handled through a separate controller and in a separate DB - @JsonIgnore - public List getNotes() { - ArrayList ret = new ArrayList(noteIds != null ? noteIds.size() : 0); - - if (noteIds != null) { - for (String id : noteIds) { - ret.add(Note.get(id)); - } - } - - // even if there were no notes, return an empty list - return ret; - } - /** - * Get the user who owns this object. - * @return the String user_id - */ - @JsonView(JsonViews.UserInterface.class) - public String getUser () { - return this.userId; - } - /** - * Set the owner of this object - */ - public void setUser (Auth0UserProfile profile) { - userId = profile.getUser_id(); - } - - public void addNote(Note n) { - if (noteIds == null) { - noteIds = new ArrayList(); - } - - noteIds.add(n.id); - n.objectId = this.id; - } - - public abstract void save(); -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/Note.java b/src/main/java/com/conveyal/datatools/manager/models/Note.java deleted file mode 100644 index 00a1c4f15..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/Note.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.manager.persistence.DataStore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Date; - -/** - * A note about a particular model. - * @author mattwigway - * - */ -@JsonInclude(Include.ALWAYS) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Note extends Model implements Serializable { - private static final long serialVersionUID = 1L; - - private static DataStore noteStore = new DataStore<>("notes"); - - /** The content of the note */ - public String body; - - /** What type of object it is recorded on */ - public NoteType type; - - /** What is the ID of the object it is recorded on */ - public String objectId; - - public String userEmail; - - /** When was this comment made? */ - public Date date; - - public void save () { - save(true); - } - - public void save (boolean commit) { - if (commit) - noteStore.save(id, this); - else - noteStore.saveWithoutCommit(id, this); - } - - public static Note get (String id) { - return noteStore.getById(id); - } - - /** - * The types of object that can have notes recorded on them. - */ - public static enum NoteType { - FEED_VERSION, FEED_SOURCE - } - - public static Collection getAll() { - return noteStore.getAll(); - } - - public static void commit() { - noteStore.commit(); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/OtpBuildConfig.java b/src/main/java/com/conveyal/datatools/manager/models/OtpBuildConfig.java deleted file mode 100644 index 3db73c992..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/OtpBuildConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import java.io.Serializable; - -/** - * Created by demory on 3/8/15. - */ - -public class OtpBuildConfig implements Serializable { - private static final long serialVersionUID = 1L; - - public Boolean fetchElevationUS; - - public Boolean stationTransfers; - - public Double subwayAccessTime; - - /** Currently only supports no-configuration fares, e.g. New York or San Francisco */ - public String fares; -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/OtpRouterConfig.java b/src/main/java/com/conveyal/datatools/manager/models/OtpRouterConfig.java deleted file mode 100644 index 77d9b4241..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/OtpRouterConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import java.io.Serializable; -import java.util.Collection; - -/** - * Created by demory on 3/8/15. - */ - -public class OtpRouterConfig implements Serializable { - - public Integer numItineraries; - - public Double walkSpeed; - - public Double stairsReluctance; - - public Double carDropoffTime; - - public Collection updaters; - - public static class Updater implements Serializable { - - public String type; - - public Integer frequencySec; - - public String sourceType; - - public String url; - - public String defaultAgencyId; - } - - public String brandingUrlRoot; -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/OtpServer.java b/src/main/java/com/conveyal/datatools/manager/models/OtpServer.java deleted file mode 100644 index 2f56c811d..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/OtpServer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import java.io.Serializable; -import java.util.List; - -/** - * Created by landon on 5/20/16. - */ -public class OtpServer implements Serializable { - public String name; - public List internalUrl; - public String publicUrl; - public Boolean admin; - public String s3Bucket; - public String s3Credentials; -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/Project.java b/src/main/java/com/conveyal/datatools/manager/models/Project.java deleted file mode 100644 index 8b6dbdd13..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/Project.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.manager.persistence.DataStore; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.sun.org.apache.bcel.internal.classfile.Unknown; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * Represents a collection of feed sources that can be made into a deployment. - * Generally, this would represent one agency that is managing the data. - * For now, there is one FeedCollection per instance of GTFS data manager, but - * we're trying to write the code in such a way that this is not necessary. - * - * @author mattwigway - * - */ -@JsonInclude(Include.ALWAYS) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Project extends Model { - private static final long serialVersionUID = 1L; - - private static DataStore projectStore = new DataStore<>("projects"); - - /** The name of this feed collection, e.g. NYSDOT. */ - public String name; - - public Boolean useCustomOsmBounds; - - public Double osmNorth, osmSouth, osmEast, osmWest; - - public OtpBuildConfig buildConfig; - - public OtpRouterConfig routerConfig; - - public Collection otpServers; - - @JsonIgnore - public OtpServer getServer (String name) { - for (OtpServer otpServer : otpServers) { - if (otpServer.name.equals(name)) { - return otpServer; - } - } - return null; - } - - public String defaultTimeZone; - - public String defaultLanguage; - - //@JsonView - public Collection feedSources; - - public Double defaultLocationLat, defaultLocationLon; - public Boolean autoFetchFeeds; - public int autoFetchHour, autoFetchMinute; - -// public Map boundingBox = new HashMap<>(); - - public Double north, south, east, west; - - public Project() { - this.buildConfig = new OtpBuildConfig(); - this.routerConfig = new OtpRouterConfig(); - this.useCustomOsmBounds = false; - } - - /** - * Get all of the FeedCollections that are defined - */ - public static Collection getAll () { - return projectStore.getAll(); - } - - public static Project get(String id) { - return projectStore.getById(id); - } - - public void save() { - save(true); - } - - public void save(boolean commit) { - if (commit) - projectStore.save(this.id, this); - else - projectStore.saveWithoutCommit(this.id, this); - } - - public void delete() { - for (FeedSource s : getProjectFeedSources()) { - s.delete(); - } - for (Deployment d : getProjectDeployments()) { - d.delete(); - } - - projectStore.delete(this.id); - } - - public static void commit () { - projectStore.commit(); - } - - /** - * Get all the feed sources for this feed collection - */ - @JsonIgnore - public Collection getProjectFeedSources() { -// ArrayList ret = new ArrayList<>(); - - // TODO: use index, but not important for now because we generally only have one FeedCollection - return FeedSource.getAll().stream().filter(fs -> this.id.equals(fs.projectId)).collect(Collectors.toList()); - - } - public int getNumberOfFeeds () { - return FeedSource.getAll().stream().filter(fs -> this.id.equals(fs.projectId)).collect(Collectors.toList()).size(); - } - /** - * Get all the deployments for this feed collection - */ - - @JsonIgnore - public Collection getProjectDeployments() { - ArrayList ret = new ArrayList(); - - for (Deployment d : Deployment.getAll()) { - if (this.id.equals(d.projectId)) { - ret.add(d); - } - } - - return ret; - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/models/Region.java b/src/main/java/com/conveyal/datatools/manager/models/Region.java deleted file mode 100644 index f0edd7991..000000000 --- a/src/main/java/com/conveyal/datatools/manager/models/Region.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.conveyal.datatools.manager.models; - -import com.conveyal.datatools.manager.persistence.DataStore; -import com.fasterxml.jackson.annotation.JsonIgnore; - -import java.util.Arrays; -import java.util.Collection; -import java.util.stream.Collectors; - -/** - * Created by landon on 4/15/16. - */ -public class Region extends Model { - private static final long serialVersionUID = 1L; - - private static DataStore regionStore = new DataStore<>("region"); - - /** The name of this region, e.g. Atlanta. */ - public String name; - - // Polygon geometry of region as GeoJSON string - @JsonIgnore - public String geometry; - public Double lat, lon; - // hierarchical order of region: country, 1st order admin, or region - public String order; - - public Boolean isPublic; - public String defaultLanguage; - public String defaultTimeZone; - - //@JsonView - public Collection feedSources; - - public Double north, south, east, west; - - public Region() { - - } - - /** - * Get all of the FeedCollections that are defined - */ - public static Collection getAll () { - return regionStore.getAll(); - } - - public static void deleteAll () { - Region.getAll().forEach(region -> region.delete()); - } - - public static Region get(String id) { - return regionStore.getById(id); - } - - public void save() { - save(true); - } - - public void save(boolean commit) { - if (commit) - regionStore.save(this.id, this); - else - regionStore.saveWithoutCommit(this.id, this); - } - - public void delete() { -// for (FeedSource fs : getRegionFeedSources()) { -// Arrays.asList(fs.regions).remove(this.id); -// fs.save(); -// } - - regionStore.delete(this.id); - } - - public static void commit () { - regionStore.commit(); - } - - /** - * Get all the feed sources for this feed collection - */ - - @JsonIgnore - public Collection getRegionFeedSources() { - - // TODO: use index, but not important for now because we generally only have one FeedCollection -// if (this.id != null && fs.regions != null) - return FeedSource.getAll().stream().filter(fs -> Arrays.asList(fs.regions).contains(this.id)).collect(Collectors.toList()); - - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/persistence/ClassLoaderSerializer.java b/src/main/java/com/conveyal/datatools/manager/persistence/ClassLoaderSerializer.java deleted file mode 100644 index b3d7f3b00..000000000 --- a/src/main/java/com/conveyal/datatools/manager/persistence/ClassLoaderSerializer.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.conveyal.datatools.manager.persistence; - -import org.apache.commons.io.input.ClassLoaderObjectInputStream; -import org.mapdb.Serializer; - -import java.io.*; - -/** - * Deserialize using the thread's class loader, not the root class loader. - */ -public class ClassLoaderSerializer implements Serializer, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public void serialize(DataOutput out, Object value) throws IOException { - ObjectOutputStream out2 = new ObjectOutputStream((OutputStream) out); - out2.writeObject(value); - out2.flush(); - } - - @Override - public Object deserialize(DataInput in, int available) throws IOException { - try { - ObjectInputStream in2 = new ClassLoaderObjectInputStream(Thread.currentThread().getContextClassLoader(), (InputStream) in); - return in2.readObject(); - } catch (ClassNotFoundException e) { - throw new IOException(e); - } - } - - @Override - public int fixedSize() { - return -1; - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/persistence/DataStore.java b/src/main/java/com/conveyal/datatools/manager/persistence/DataStore.java deleted file mode 100644 index 0087b42c5..000000000 --- a/src/main/java/com/conveyal/datatools/manager/persistence/DataStore.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.conveyal.datatools.manager.persistence; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.conveyal.datatools.manager.DataManager; -import org.mapdb.BTreeKeySerializer; -import org.mapdb.BTreeMap; -import org.mapdb.Bind; -import org.mapdb.DB; -import org.mapdb.DBMaker; -import org.mapdb.Fun; -import org.mapdb.Fun.Function2; -import org.mapdb.Pump; -import org.mapdb.Fun.Tuple2; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DataStore { - private static final Logger LOG = LoggerFactory.getLogger(DataStore.class); - - DB db; - BTreeMap map; - - public DataStore(String dataFile) { - this(new File(DataManager.config.get("application").get("data").get("mapdb").asText()), dataFile); - } - - public DataStore(File directory, String dataFile) { - - if(!directory.exists()) - directory.mkdirs(); - - try { - LOG.info(directory.getCanonicalPath()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - db = DBMaker.newFileDB(new File(directory, dataFile + ".db")) - .closeOnJvmShutdown() - .make(); - - DB.BTreeMapMaker maker = db.createTreeMap(dataFile); - maker.valueSerializer(new ClassLoaderSerializer()); - map = maker.makeOrGet(); - } - - public DataStore(File directory, String dataFile, List>inputData) { - - if(!directory.exists()) - directory.mkdirs(); - - try { - LOG.info(directory.getCanonicalPath()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - db = DBMaker.newFileDB(new File(directory, dataFile + ".db")) - .transactionDisable() - .closeOnJvmShutdown() - .make(); - - Comparator> comparator = new Comparator>(){ - - @Override - public int compare(Tuple2 o1, - Tuple2 o2) { - return o1.a.compareTo(o2.a); - } - }; - - // need to reverse sort list - Iterator> iter = Pump.sort(inputData.iterator(), - true, 100000, - Collections.reverseOrder(comparator), //reverse order comparator - db.getDefaultSerializer() - ); - - - BTreeKeySerializer keySerializer = BTreeKeySerializer.STRING; - - map = db.createTreeMap(dataFile) - .pumpSource(iter) - .pumpPresort(100000) - .keySerializer(keySerializer) - .make(); - - - - // close/flush db - db.close(); - - // re-connect with transactions enabled - db = DBMaker.newFileDB(new File(directory, dataFile + ".db")) - .closeOnJvmShutdown() - .make(); - - map = db.getTreeMap(dataFile); - } - - public void save(String id, T obj) { - map.put(id, obj); - db.commit(); - } - - public void saveWithoutCommit(String id, T obj) { - map.put(id, obj); - } - - public void commit() { - db.commit(); - } - - public void delete(String id) { - map.remove(id); - db.commit(); - } - - public T getById(String id) { - return map.get(id); - } - - /** - * Does an object with this ID exist in this data store? - * @param id - * @return boolean indicating result - */ - public boolean hasId(String id) { - return map.containsKey(id); - } - - public Collection getAll() { - return map.values(); - } - - public Integer size() { - return map.keySet().size(); - } - - /** Create a secondary (unique) key */ - public void secondaryKey (String name, Function2 fun) { - Map index = db.getTreeMap(name); - Bind.secondaryKey(map, index, fun); - } - - /** search using a secondary unique key */ - public T find(String name, K2 value) { - Map index = db.getTreeMap(name); - - String id = index.get(value); - - if (id == null) - return null; - - return map.get(id); - } - - /** find the value with largest key less than or equal to key */ - public T findFloor (String name, K2 floor) { - BTreeMap index = db.getTreeMap(name); - - Entry key = index.floorEntry(floor); - - if (key == null || key.getValue() == null) - return null; - - return map.get(key.getValue()); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/persistence/FeedStore.java b/src/main/java/com/conveyal/datatools/manager/persistence/FeedStore.java deleted file mode 100644 index dfa74f1c1..000000000 --- a/src/main/java/com/conveyal/datatools/manager/persistence/FeedStore.java +++ /dev/null @@ -1,243 +0,0 @@ -package com.conveyal.datatools.manager.persistence; - -import java.io.*; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.s3.model.*; -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedSource; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.IOUtils; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.event.ProgressEvent; -import com.amazonaws.event.ProgressListener; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.transfer.TransferManager; -import com.amazonaws.services.s3.transfer.Upload; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Store a feed on the file system - * @author mattwigway - * - */ -public class FeedStore { - - public static final Logger LOG = LoggerFactory.getLogger(FeedStore.class); - - - /** Local file storage path if working offline */ - private File path; - - /** An optional AWS S3 bucket to store the feeds */ - private String s3Bucket; - - private String s3Prefix = "gtfs/"; - - /** An AWS credentials file to use when uploading to S3 */ - private String s3CredentialsFilename; - - public FeedStore() { - this(null); - } - - public FeedStore(String subdir) { - // s3 storage - if (DataManager.getConfigPropertyAsText("application.data.use_s3_storage").equals("true")){ - this.s3Bucket = DataManager.getConfigPropertyAsText("application.data.gtfs_s3_bucket"); - } - // local storage - else { - String pathString = DataManager.getConfigPropertyAsText("application.data.gtfs"); - if(subdir != null) pathString += File.separator + subdir; - File path = new File(pathString); - if (!path.exists() || !path.isDirectory()) { - throw new IllegalArgumentException("Not a directory or not found: " + path.getAbsolutePath()); - } - this.path = path; - } - - } - - public List getAllFeeds () { - ArrayList ret = new ArrayList(); - - // local storage - if (path != null) { - for (File file : path.listFiles()) { - ret.add(file.getName()); - } - } - // s3 storage - else { - - } - return ret; - } - - /** - * Get the feed with the given ID. - */ - public File getFeed (String id) { - // local storage - if (path != null) { - System.out.println(path + "/" + id); - File feed = new File(path, id); - if (!feed.exists()) return null; - // don't let folks get feeds outside of the directory - if (!feed.getParentFile().equals(path)) return null; - else return feed; - } - // s3 storage - else { - - if (this.s3Bucket != null) { - String keyName = s3Prefix + id; - AWSCredentials creds; - if (this.s3CredentialsFilename != null) { - creds = new ProfileCredentialsProvider(this.s3CredentialsFilename, "default").getCredentials(); - } else { - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - } - try { - LOG.info("Downloading feed from s3"); - AmazonS3 s3Client = new AmazonS3Client(new ProfileCredentialsProvider()); - S3Object object = s3Client.getObject( - new GetObjectRequest(s3Bucket, keyName)); - InputStream objectData = object.getObjectContent(); - - // Process the objectData stream. - File tempFile = File.createTempFile("test", ".zip"); - try (FileOutputStream out = new FileOutputStream(tempFile)) { - IOUtils.copy(objectData, out); - out.close(); - objectData.close(); - } - tempFile.deleteOnExit(); - return tempFile; - } catch (IOException e) { - e.printStackTrace(); - } catch (AmazonServiceException ase) { - LOG.error("Error downloading from s3"); - ase.printStackTrace(); - } - - } - return null; - } - - } - - /** - * Create a new feed with the given ID. - */ - public File newFeed (String id, InputStream inputStream, FeedSource feedSource) { - // local storage - if (path != null) { - File out = new File(path, id); - FileOutputStream outStream; - - // store latest as feed-source-id.zip - if (feedSource != null) { - File copy = new File(path, feedSource.id + ".zip"); - FileOutputStream copyStream; - try { - copyStream = new FileOutputStream(copy); - - } catch (FileNotFoundException e) { - LOG.error("Unable to save latest at {}", copy); - return null; - } - } - - try { - outStream = new FileOutputStream(out); - - } catch (FileNotFoundException e) { - LOG.error("Unable to open {}", out); - return null; - } - - // copy the file - ReadableByteChannel rbc = Channels.newChannel(inputStream); - try { - outStream.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - outStream.close(); - return out; - } catch (IOException e) { - LOG.error("Unable to transfer from upload to saved file."); - return null; - } - } - // s3 storage - else { - // upload to S3, if applicable - if(this.s3Bucket != null) { - - String keyName = s3Prefix + id; - AWSCredentials creds; - - if (this.s3CredentialsFilename != null) { - creds = new ProfileCredentialsProvider(this.s3CredentialsFilename, "default").getCredentials(); - } - else { - // default credentials providers, e.g. IAM role - creds = new DefaultAWSCredentialsProviderChain().getCredentials(); - } - - try { - // Use tempfile - File tempFile = File.createTempFile("test", ".zip"); - tempFile.deleteOnExit(); - try (FileOutputStream out = new FileOutputStream(tempFile)) { - IOUtils.copy(inputStream, out); - out.close(); - inputStream.close(); - } - - LOG.info("Uploading feed to S3 from inputstream"); - AmazonS3 s3client = new AmazonS3Client(creds); - s3client.putObject(new PutObjectRequest( - s3Bucket, keyName, tempFile)); - - if (feedSource != null){ - LOG.info("Copying feed on s3 to latest version"); - // copy to [name]-latest.zip - String copyKey = s3Prefix + feedSource.id + ".zip"; - CopyObjectRequest copyObjRequest = new CopyObjectRequest( - this.s3Bucket, keyName, this.s3Bucket, copyKey); - s3client.copyObject(copyObjRequest); - } - - - return tempFile; - - - } - - catch (AmazonServiceException ase) { - LOG.error("Error uploading feed to S3"); - ase.printStackTrace(); - return null; - } catch (IOException e) { - e.printStackTrace(); - } - - } - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/utils/CorsFilter.java b/src/main/java/com/conveyal/datatools/manager/utils/CorsFilter.java deleted file mode 100644 index 5e72ce81f..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/CorsFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.conveyal.datatools.manager.utils; - -/** - * Created by landon on 4/21/16. - */ -import java.util.HashMap; -import spark.Filter; -import spark.Request; -import spark.Response; -import spark.Spark; - -/** - * Really simple helper for enabling CORS in a spark application; - */ -public final class CorsFilter { - - private static final HashMap corsHeaders = new HashMap(); - - static { - corsHeaders.put("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS"); - corsHeaders.put("Access-Control-Allow-Origin", "*"); - corsHeaders.put("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,"); - corsHeaders.put("Access-Control-Allow-Credentials", "true"); - } - - public final static void apply() { - Filter filter = new Filter() { - @Override - public void handle(Request request, Response response) throws Exception { - corsHeaders.forEach((key, value) -> { - response.header(key, value); - }); - } - }; - Spark.after(filter); - } -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/utils/FeedUpdater.java b/src/main/java/com/conveyal/datatools/manager/utils/FeedUpdater.java deleted file mode 100644 index 7458c5292..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/FeedUpdater.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.conveyal.datatools.manager.utils; - -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectListing; -import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.conveyal.datatools.manager.controllers.api.GtfsApiController; -import com.conveyal.datatools.manager.models.FeedVersion; - -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Created by landon on 3/24/16. - */ -public class FeedUpdater { - public List eTags; - private Timer timer; - private static AmazonS3Client s3; - - public static final Logger LOG = LoggerFactory.getLogger(FeedUpdater.class); - - public FeedUpdater(List eTagList, int delay, int seconds){ - this.eTags = eTagList; - this.timer = new Timer(); - - // TODO: check for credentials?? - this.s3 = new AmazonS3Client(); - - - this.timer.schedule(new UpdateFeedsTask(), delay*1000, seconds*1000); - - - } - - public void addFeedETag(String eTag){ - this.eTags.add(eTag); - } - - public void addFeedETags(List eTagList){ - this.eTags.addAll(eTagList); - } - - public void cancel(){ - this.timer.cancel(); //Terminate the timer thread - } - - - class UpdateFeedsTask extends TimerTask { - public void run() { - LOG.info("Fetching feeds..."); - LOG.info("Current eTag list " + eTags.toString()); - - ObjectListing gtfsList = s3.listObjects(GtfsApiController.feedBucket, GtfsApiController.directory); - Boolean feedsUpdated = false; - for (S3ObjectSummary objSummary : gtfsList.getObjectSummaries()) { - - String eTag = objSummary.getETag(); - if (!eTags.contains(eTag)) { - String keyName = objSummary.getKey(); - if (keyName.equals(GtfsApiController.directory)){ - continue; - } - LOG.info("Updating feed " + keyName); - String feedId = keyName.split("/")[1]; -// ApiMain.loadFeedFromBucket(GtfsApiController.feedBucket, feedId, GtfsApiController.directory); - try { - GtfsApiController.gtfsApi.registerFeedSource(feedId, FeedVersion.get(feedId).getGtfsFile()); - } catch (Exception e) { - e.printStackTrace(); - } - addFeedETag(eTag); - feedsUpdated = true; - } - } - if (!feedsUpdated) { - LOG.info("No feeds updated..."); - } - else { - LOG.info("New eTag list " + eTags); - } - // TODO: compare current list of eTags against list in completed folder - - // TODO: load feeds for any feeds with new eTags -// ApiMain.loadFeedFromBucket() - } - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/HashUtils.java b/src/main/java/com/conveyal/datatools/manager/utils/HashUtils.java deleted file mode 100644 index cda3ebb8e..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/HashUtils.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.conveyal.datatools.manager.utils; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.nio.ByteBuffer; -import java.security.DigestInputStream; -import java.security.MessageDigest; - - -public class HashUtils { - - public static String hashString(String input) { - - try { - - byte[] bytesOfMessage = input.getBytes("UTF-8"); - - return DigestUtils.md5Hex(bytesOfMessage); - - } - catch(Exception e) { - - return ""; - } - } - - - public static String hashFile(File file) { - - try { - - MessageDigest md = MessageDigest.getInstance("MD5"); - - FileInputStream fis = new FileInputStream(file); - - DigestInputStream dis = new DigestInputStream(fis, md); - - - // hash the size - dis.read(ByteBuffer.allocate(8).putLong(file.length()).array()); - - - // hash first 1000 bytes - int i = 0; - while (dis.read() != -1 && i < 1000) { - i++; - }; - - // hash 5000 bytes starting in the middle or the remainder of the file if under 10000 - if(file.length() > 10000) { - dis.skip(file.length() / 2); - - i = 0; - while (dis.read() != -1 && i < 5000) { - i++; - }; - } - else { - while (dis.read() != -1) { - }; - } - - dis.close(); - - return new String(Hex.encodeHex(md.digest())); - - - } - catch(Exception e) { - - return ""; - } - } - -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/NotificationsUtils.java b/src/main/java/com/conveyal/datatools/manager/utils/NotificationsUtils.java deleted file mode 100644 index f36f23351..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/NotificationsUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.conveyal.datatools.manager.utils; - -import com.conveyal.datatools.manager.DataManager; -import com.conveyal.datatools.manager.models.FeedSource; -import com.conveyal.datatools.manager.models.Project; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sparkpost.Client; -import com.sparkpost.exception.SparkPostException; -import com.sparkpost.model.responses.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.xml.crypto.Data; -import java.io.IOException; - -import static com.conveyal.datatools.manager.auth.Auth0Users.getUsersBySubscription; - -/** - * Created by landon on 4/26/16. - */ -public class NotificationsUtils { - - public static void sendNotification(String to_email, String subject, String text, String html) { - String API_KEY = DataManager.serverConfig.get("sparkpost").get("key").asText(); - Client client = new Client(API_KEY); - - try { - Response response = client.sendMessage( - DataManager.serverConfig.get("sparkpost").get("from_email").asText(), // from - to_email, // to - subject, - text, - html); - System.out.println(response.getResponseMessage()); - } catch (SparkPostException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/ResponseError.java b/src/main/java/com/conveyal/datatools/manager/utils/ResponseError.java deleted file mode 100644 index 6367e675c..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/ResponseError.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.conveyal.datatools.manager.utils; - -/** - * Created by landon on 5/24/16. - */ -public class ResponseError { - private String message; - - public ResponseError(String message, String... args) { - this.message = String.format(message, args); - } - - public ResponseError(Exception e) { - this.message = e.getMessage(); - } - - public String getMessage() { - return this.message; - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/StringUtils.java b/src/main/java/com/conveyal/datatools/manager/utils/StringUtils.java deleted file mode 100644 index d3808e31d..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/StringUtils.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.conveyal.datatools.manager.utils; - -public class StringUtils { - /** - * Clean a name to make it filesystem-friendly - * @param name a name with any letters - * @return a new name with weird letters removed/transliterated. - */ - public static String getCleanName (String name) { - return name.replace(' ', '_').replaceAll("[^A-Za-z0-9_-]", ""); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/json/InvalidValueMixIn.java b/src/main/java/com/conveyal/datatools/manager/utils/json/InvalidValueMixIn.java deleted file mode 100644 index 45e7e99ec..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/json/InvalidValueMixIn.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.conveyal.datatools.manager.utils.json; - -import com.conveyal.gtfs.model.Priority; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Specify the annotations needed to construct an InvalidValue. This is a Jackson mixin so we don't need to - * add a default constructor, etc. - */ -public abstract class InvalidValueMixIn { - InvalidValueMixIn ( - @JsonProperty("affectedEntity") String affectedEntity, - @JsonProperty("affectedField") String affectedField, - @JsonProperty("affectedEntityId") String affectedEntityId, - @JsonProperty("problemType") String problemType, - @JsonProperty("problemDescription") String problemDescription, - @JsonProperty("problemData") Object problemData, - @JsonProperty("priority") Priority priority - ) {}; -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/json/JsonManager.java b/src/main/java/com/conveyal/datatools/manager/utils/json/JsonManager.java deleted file mode 100644 index 3d09e7db7..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/json/JsonManager.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.conveyal.datatools.manager.utils.json; - -import java.awt.geom.Rectangle2D; -import java.io.IOException; -import java.time.LocalDate; -import java.util.Collection; -import java.util.Map; - -import com.conveyal.datatools.editor.models.transit.GtfsRouteType; -import com.conveyal.datatools.editor.utils.JacksonSerializers; -import com.conveyal.geojson.GeoJsonModule; -import com.conveyal.gtfs.model.InvalidValue; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; -import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -/** - * Helper methods for writing REST API routines - * @author mattwigway - * - */ -public class JsonManager { - private ObjectWriter ow; - private ObjectMapper om; - - /** - * Create a new JsonManager - * @param theClass The class to create a json manager for (yes, also in the diamonds). - * @param view The view to use - */ - public JsonManager (Class theClass, Class view) { - this.theClass = theClass; - this.om = new ObjectMapper(); - om.addMixInAnnotations(InvalidValue.class, InvalidValueMixIn.class); - om.addMixInAnnotations(Rectangle2D.class, Rectangle2DMixIn.class); - om.registerModule(new GeoJsonModule()); - SimpleModule deser = new SimpleModule(); - - deser.addDeserializer(LocalDate.class, new JacksonSerializers.LocalDateDeserializer()); - deser.addSerializer(LocalDate.class, new JacksonSerializers.LocalDateSerializer()); - - deser.addDeserializer(GtfsRouteType.class, new JacksonSerializers.GtfsRouteTypeDeserializer()); - deser.addSerializer(GtfsRouteType.class, new JacksonSerializers.GtfsRouteTypeSerializer()); - - deser.addDeserializer(Rectangle2D.class, new Rectangle2DDeserializer()); - om.registerModule(deser); -// om.registerModule(new JavaTimeModule()); - SimpleFilterProvider filters = new SimpleFilterProvider(); - filters.addFilter("bbox", SimpleBeanPropertyFilter.filterOutAllExcept("west", "east", "south", "north")); - this.ow = om.writer(filters).withView(view); - } - - private Class theClass; - - /** - * Add an additional mixin for serialization with this object mapper. - */ - public void addMixin(Class target, Class mixin) { - om.addMixInAnnotations(target, mixin); - } - - public String write(Object o) throws JsonProcessingException{ - if (o instanceof String) { - return (String) o; - } - return ow.writeValueAsString(o); - } - - /** - * Convert an object to its JSON representation - * @param o the object to convert - * @return the JSON string - * @throws JsonProcessingException - */ - /*public String write (T o) throws JsonProcessingException { - return ow.writeValueAsString(o); - }*/ - - /** - * Convert a collection of objects to their JSON representation. - * @param c the collection - * @return A JsonNode representing the collection - * @throws JsonProcessingException - */ - public String write (Collection c) throws JsonProcessingException { - return ow.writeValueAsString(c); - } - - public String write (Map map) throws JsonProcessingException { - return ow.writeValueAsString(map); - } - - public T read (String s) throws JsonParseException, JsonMappingException, IOException { - return om.readValue(s, theClass); - } - - public T read (JsonParser p) throws JsonParseException, JsonMappingException, IOException { - return om.readValue(p, theClass); - } - - public T read(JsonNode asJson) { - return om.convertValue(asJson, theClass); - } -} diff --git a/src/main/java/com/conveyal/datatools/manager/utils/json/JsonUtil.java b/src/main/java/com/conveyal/datatools/manager/utils/json/JsonUtil.java deleted file mode 100644 index e37574156..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/json/JsonUtil.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.conveyal.datatools.manager.utils.json; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Created by demory on 3/14/16. - */ - -public class JsonUtil { - public static ObjectMapper objectMapper = new ObjectMapper(); - - /*static { - objectMapper.registerModule(...); - }*/ -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/utils/json/Rectangle2DDeserializer.java b/src/main/java/com/conveyal/datatools/manager/utils/json/Rectangle2DDeserializer.java deleted file mode 100644 index 0a9b18868..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/json/Rectangle2DDeserializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.conveyal.datatools.manager.utils.json; - -import java.awt.geom.Rectangle2D; -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -public class Rectangle2DDeserializer extends JsonDeserializer { - - @Override - public Rectangle2D deserialize(JsonParser jp, DeserializationContext arg1) - throws IOException, JsonProcessingException { - - IntermediateBoundingBox bbox = jp.readValueAs(IntermediateBoundingBox.class); - - if (bbox.north == null || bbox.south == null || bbox.east == null || bbox.west == null) - throw new JsonParseException("Unable to deserialize bounding box; need north, south, east, and west.", jp.getCurrentLocation()); - - Rectangle2D.Double ret = new Rectangle2D.Double(bbox.west, bbox.north, 0, 0); - ret.add(bbox.east, bbox.south); - return ret; - } - - /** - * A place to hold information from the JSON stream temporarily. - */ - private static class IntermediateBoundingBox { - public Double north; - public Double south; - public Double east; - public Double west; - } - -} \ No newline at end of file diff --git a/src/main/java/com/conveyal/datatools/manager/utils/json/Rectangle2DMixIn.java b/src/main/java/com/conveyal/datatools/manager/utils/json/Rectangle2DMixIn.java deleted file mode 100644 index 91b598d99..000000000 --- a/src/main/java/com/conveyal/datatools/manager/utils/json/Rectangle2DMixIn.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.conveyal.datatools.manager.utils.json; - -import com.fasterxml.jackson.annotation.JsonFilter; -import com.fasterxml.jackson.annotation.JsonProperty; - -// ignore all by default -@JsonFilter("bbox") -public abstract class Rectangle2DMixIn { - // stored as lon, lat - @JsonProperty("west") public abstract double getMinX(); - @JsonProperty("east") public abstract double getMaxX(); - @JsonProperty("north") public abstract double getMaxY(); - @JsonProperty("south") public abstract double getMinY(); -} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index f83901398..000000000 --- a/webpack.config.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict' - -var path = require('path') -var webpack = require('webpack') -var HtmlWebpackPlugin = require('html-webpack-plugin') - -module.exports = { - devtool: 'eval-source-map', - entry: [ - 'babel-polyfill', - path.join(__dirname, 'src/main/client/main.js') - ], - output: { - path: path.join(__dirname, 'src/main/resources/public/'), - filename: '[name].js', - publicPath: '/' - }, - plugins: [ - new HtmlWebpackPlugin({ - template: 'src/main/client/index.tpl.html', - inject: 'body', - filename: 'index.html' - }), - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin(), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') - }) - ], - module: { - loaders: [ - { - test: /\.js?$/, - exclude: /node_modules/, - loader: 'babel' - }, { - test: /\.json?$/, - loader: 'json' - }, { - test: /\.yml$/, - loader: 'yaml' - // }, { - // test: /\.css$/, - // loader: 'style!css?modules&localIdentName=[name]---[local]---[hash:base64:5]' - }, - { - test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, - // test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, - // loader: 'url-loader?limit=10000&mimetype=application/font-woff&name=./assets/fonts/[hash].[ext]' - loader: 'url-loader?limit=10000&mimetype=application/font-woff&name=./assets/fonts/[hash].[ext]' - }, - { - test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'file-loader?name=./assets/img/[hash].[ext]' - }, - // css-loader - { test: /\.css$/, loader: 'style-loader!css-loader' }, - { test: /\.(png|jpg)$/, loader: 'url-loader?limit=10000&name=./assets/img/[hash].[ext]'}, - ] - } -} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..fb04e9ae1 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7243 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@allenfang/react-toastr@2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@allenfang/react-toastr/-/react-toastr-2.8.2.tgz#0bef6585189e0571dd6bdfc4ef98bc9f9c47da0c" + dependencies: + classnames "^2.2.5" + element-class "^0.2.2" + lodash "^4.16.1" + react "^0.14.0 || <15.4.0" + react-addons-update "^0.14.0 || <15.4.0" + react-dom "^0.14.0 || <15.4.0" + +"@conveyal/woonerf": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@conveyal/woonerf/-/woonerf-0.2.0.tgz#53d75d549152082b8ac7d1ab1e980b1e583c72a5" + dependencies: + auth0-js "^7.3.0" + auth0-lock "^10.5.1" + isomorphic-fetch "^2.2.1" + lodash.isequal "^4.4.0" + lodash.isobject "^3.0.2" + lodash.merge "^4.6.0" + react "^15.3.2" + react-addons-perf "^15.3.2" + react-dom "^15.3.2" + react-router "^3.0.0" + react-router-redux "^4.0.6" + redux "^3.6.0" + redux-actions "^0.13.0" + redux-logger "^2.7.4" + +Base64@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.1.4.tgz#e9f6c6bef567fd635ea4162ab14dd329e74aa6de" + +JSONStream@^1.0.3: + version "1.3.0" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.0.tgz#680ab9ac6572a8a1a207e0b38721db1c77b215e5" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abab@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" + +abbrev@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + +acorn-globals@^1.0.4: + version "1.0.9" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf" + dependencies: + acorn "^2.1.0" + +acorn-jsx@^3.0.0, acorn-jsx@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^1.0.3: + version "1.2.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" + +acorn@^2.1.0, acorn@^2.4.0, acorn@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" + +acorn@^3.0.4, acorn@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a" + +add-dom-event-listener@1.x: + version "1.0.1" + resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.0.1.tgz#70e74d0692b27108f9740554e0fa80a4683c2eb2" + dependencies: + object-assign "4.x" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv@^4.7.0: + version "4.11.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^1.1.0, ansi-escapes@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@^0.2.0, ansi-regex@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" + +ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + +ansi-styles@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.0.1.tgz#b033f57f93e2d28adeb8bc11138fa13da0fd20a3" + +ansi-styles@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansicolors@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" + +any-promise@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" + +anymatch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + dependencies: + arrify "^1.0.0" + micromatch "^2.1.5" + +append-transform@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" + dependencies: + default-require-extensions "^1.0.0" + +aproba@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0" + +are-we-there-yet@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.0 || ^1.1.13" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +argparse@~0.1.15: + version "0.1.16" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-0.1.16.tgz#cfd01e0fbba3d6caed049fbd758d40f65196f57c" + dependencies: + underscore "~1.7.0" + underscore.string "~2.4.0" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +array.filter@^0.1.0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/array.filter/-/array.filter-0.1.5.tgz#7ef07c37f472223f3e5c4b73be637face765bcd7" + dependencies: + curry2 "^1.0.1" + selectn "^1.0.5" + +arraymap@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/arraymap/-/arraymap-0.1.2.tgz#2bb1d4f5706d1ccf4fd83896225e92ef5fe41a51" + dependencies: + curry2 "^0.1.0" + selectn "^0.10.0" + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asap@^2.0.3, asap@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" + +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +ast-types@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.15.tgz#8eef0827f04dff0ec8857ba925abe3fea6194e52" + +astw@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astw/-/astw-2.0.0.tgz#08121ac8288d35611c0ceec663f6cd545604897d" + dependencies: + acorn "^1.0.3" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async@^1.4.0, async@^1.4.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4" + dependencies: + lodash "^4.14.0" + +async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +attr-accept@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.0.tgz#b5cd35227f163935a8f1de10ed3eba16941f6be6" + +auth0-js@6.8.4: + version "6.8.4" + resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-6.8.4.tgz#430dd4cacb64d8d15d69b1e621184f9a2a640a61" + dependencies: + Base64 "~0.1.3" + json-fallback "0.0.1" + jsonp "~0.0.4" + qs "git+https://github.com/jfromaniello/node-querystring.git#fix_ie7_bug_with_arrays" + reqwest "^1.1.4" + trim "~0.0.1" + winchan "^0.1.1" + xtend "~2.1.1" + +auth0-js@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-7.3.0.tgz#2a05a5a4ca21faa28aa927bbeef3194b2a95847d" + dependencies: + Base64 "~0.1.3" + json-fallback "0.0.1" + jsonp "~0.0.4" + qs "^6.2.1" + reqwest "2.0.5" + trim "~0.0.1" + winchan "0.1.4" + xtend "~2.1.1" + +auth0-js@^7.3.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-7.4.0.tgz#a2f1c472a4db261912afd20e1ed9c2773e251cdf" + dependencies: + Base64 "~0.1.3" + json-fallback "0.0.1" + jsonp "~0.0.4" + qs "^6.2.1" + reqwest "2.0.5" + trim "~0.0.1" + winchan "0.1.4" + xtend "~2.1.1" + +auth0-lock@9: + version "9.2.3" + resolved "https://registry.yarnpkg.com/auth0-lock/-/auth0-lock-9.2.3.tgz#c872a9211f5421e04e3c08bf0dd8d1e7fd0f23e8" + dependencies: + auth0-js "6.8.4" + bean "~1.0.4" + blueimp-md5 "^1.1.0" + bonzo "^1.3.6" + brfs "^1.4.0" + debug "^2.2.0" + domready "~0.2.13" + ejsify "0.1.0" + packageify "^0.2.0" + password-sheriff "^0.4.0" + sizzle "^2.0.0" + trim "0.0.1" + underscore "~1.5.2" + +auth0-lock@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/auth0-lock/-/auth0-lock-10.5.1.tgz#ae0a935ed05f6ff3c2abde29018decb302f9ec0b" + dependencies: + auth0-js "7.3.0" + blueimp-md5 "2.3.1" + fbjs "^0.3.1" + immutable "^3.7.3" + jsonp "^0.2.0" + password-sheriff "^1.0.0" + react "^15.0.0 || ^16.0.0" + react-addons-css-transition-group "^15.0.0 || ^16.0.0" + react-dom "^15.0.0 || ^16.0.0" + trim "0.0.1" + +autolinker@~0.15.0: + version "0.15.3" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.15.3.tgz#342417d8f2f3461b14cf09088d5edf8791dc9832" + +autoprefixer@^6.0.2: + version "6.7.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.1.tgz#d14d0842f6ef90741cfb2b1e8152a04e83b39ed2" + dependencies: + browserslist "^1.7.1" + caniuse-db "^1.0.30000617" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.11" + postcss-value-parser "^3.2.3" + +aws-sdk@^2.4.2: + version "2.9.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.9.0.tgz#f258dcc295b1e7eca49d3624abfbf5f7d644172c" + dependencies: + buffer "4.9.1" + crypto-browserify "1.0.9" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.1.5" + url "0.10.3" + uuid "3.0.0" + xml2js "0.4.15" + xmlbuilder "2.6.2" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" + +babel-code-frame@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^2.0.0" + +babel-code-frame@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +babel-core@^6.0.0, babel-core@^6.0.14, babel-core@^6.10.4, babel-core@^6.18.0: + version "6.18.2" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.18.2.tgz#d8bb14dd6986fa4f3566a26ceda3964fa0e04e5b" + dependencies: + babel-code-frame "^6.16.0" + babel-generator "^6.18.0" + babel-helpers "^6.16.0" + babel-messages "^6.8.0" + babel-register "^6.18.0" + babel-runtime "^6.9.1" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.11.0" + convert-source-map "^1.1.0" + debug "^2.1.1" + json5 "^0.5.0" + lodash "^4.2.0" + minimatch "^3.0.2" + path-is-absolute "^1.0.0" + private "^0.1.6" + slash "^1.0.0" + source-map "^0.5.0" + +babel-eslint@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.1.1.tgz#8a6a884f085aa7060af69cfc77341c2f99370fb2" + dependencies: + babel-code-frame "^6.16.0" + babel-traverse "^6.15.0" + babel-types "^6.15.0" + babylon "^6.13.0" + lodash.pickby "^4.6.0" + +babel-generator@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.18.0.tgz#e4f104cb3063996d9850556a45aae4a022060a07" + dependencies: + babel-messages "^6.8.0" + babel-runtime "^6.9.0" + babel-types "^6.18.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.2.0" + source-map "^0.5.0" + +babel-helper-bindify-decorators@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.22.0.tgz#d7f5bc261275941ac62acfc4e20dacfb8a3fe952" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helper-builder-binary-assignment-operator-visitor@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.22.0.tgz#29df56be144d81bdeac08262bfa41d2c5e91cdcd" + dependencies: + babel-helper-explode-assignable-expression "^6.22.0" + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-helper-builder-react-jsx@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.22.0.tgz#aafb31913e47761fd4d0b6987756a144a65fca0d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + esutils "^2.0.0" + lodash "^4.2.0" + +babel-helper-call-delegate@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.22.0.tgz#119921b56120f17e9dae3f74b4f5cc7bcc1b37ef" + dependencies: + babel-helper-hoist-variables "^6.22.0" + babel-runtime "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helper-define-map@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.22.0.tgz#9544e9502b2d6dfe7d00ff60e82bd5a7a89e95b7" + dependencies: + babel-helper-function-name "^6.22.0" + babel-runtime "^6.22.0" + babel-types "^6.22.0" + lodash "^4.2.0" + +babel-helper-explode-assignable-expression@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.22.0.tgz#c97bf76eed3e0bae4048121f2b9dae1a4e7d0478" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helper-explode-class@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.22.0.tgz#646304924aa6388a516843ba7f1855ef8dfeb69b" + dependencies: + babel-helper-bindify-decorators "^6.22.0" + babel-runtime "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helper-function-name@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.22.0.tgz#51f1bdc4bb89b15f57a9b249f33d742816dcbefc" + dependencies: + babel-helper-get-function-arity "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helper-get-function-arity@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.22.0.tgz#0beb464ad69dc7347410ac6ade9f03a50634f5ce" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-helper-hoist-variables@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.22.0.tgz#3eacbf731d80705845dd2e9718f600cfb9b4ba72" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-helper-optimise-call-expression@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.22.0.tgz#f8d5d4b40a6e2605a6a7f9d537b581bea3756d15" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-helper-regex@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.22.0.tgz#79f532be1647b1f0ee3474b5f5c3da58001d247d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + lodash "^4.2.0" + +babel-helper-remap-async-to-generator@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.22.0.tgz#2186ae73278ed03b8b15ced089609da981053383" + dependencies: + babel-helper-function-name "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helper-replace-supers@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.22.0.tgz#1fcee2270657548908c34db16bcc345f9850cf42" + dependencies: + babel-helper-optimise-call-expression "^6.22.0" + babel-messages "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-helpers@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.16.0.tgz#1095ec10d99279460553e67eb3eee9973d3867e3" + dependencies: + babel-runtime "^6.0.0" + babel-template "^6.16.0" + +babel-jest@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-16.0.0.tgz#348729aea6d624a4774b8a934d07a40dd2cfd640" + dependencies: + babel-core "^6.0.0" + babel-plugin-istanbul "^2.0.0" + babel-preset-jest "^16.0.0" + +babel-jest@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-17.0.2.tgz#8d51e0d03759713c331f108eb0b2eaa4c6efff74" + dependencies: + babel-core "^6.0.0" + babel-plugin-istanbul "^2.0.0" + babel-preset-jest "^17.0.2" + +babel-jest@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-18.0.0.tgz#17ebba8cb3285c906d859e8707e4e79795fb65e3" + dependencies: + babel-core "^6.0.0" + babel-plugin-istanbul "^3.0.0" + babel-preset-jest "^18.0.0" + +babel-messages@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.22.0.tgz#36066a214f1217e4ed4164867669ecb39e3ea575" + dependencies: + babel-runtime "^6.22.0" + +babel-messages@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.8.0.tgz#bf504736ca967e6d65ef0adb5a2a5f947c8e0eb9" + dependencies: + babel-runtime "^6.0.0" + +babel-plugin-add-module-exports@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz#9ae9a1f4a8dc67f0cdec4f4aeda1e43a5ff65e25" + +babel-plugin-check-es2015-constants@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-istanbul@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-2.0.3.tgz#266b304b9109607d60748474394676982f660df4" + dependencies: + find-up "^1.1.2" + istanbul-lib-instrument "^1.1.4" + object-assign "^4.1.0" + test-exclude "^2.1.1" + +babel-plugin-istanbul@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-3.1.2.tgz#11d5abde18425ec24b5d648c7e0b5d25cd354a22" + dependencies: + find-up "^1.1.2" + istanbul-lib-instrument "^1.4.2" + object-assign "^4.1.0" + test-exclude "^3.3.0" + +babel-plugin-jest-hoist@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-16.0.0.tgz#b58ca3f770982a7e7c25b5614b2e57e9dafc6e76" + +babel-plugin-jest-hoist@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-17.0.2.tgz#213488ce825990acd4c30f887dca09fffeb45235" + +babel-plugin-jest-hoist@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-18.0.0.tgz#4150e70ecab560e6e7344adc849498072d34e12a" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-async-generators@^6.5.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-decorators@^6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-flow@^6.18.0, babel-plugin-syntax-flow@^6.3.13: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + +babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.13.0, babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-generator-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.22.0.tgz#a720a98153a7596f204099cd5409f4b3c05bab46" + dependencies: + babel-helper-remap-async-to-generator "^6.22.0" + babel-plugin-syntax-async-generators "^6.5.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.22.0.tgz#194b6938ec195ad36efc4c33a971acf00d8cd35e" + dependencies: + babel-helper-remap-async-to-generator "^6.22.0" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-class-properties@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.22.0.tgz#aa78f8134495c7de06c097118ba061844e1dc1d8" + dependencies: + babel-helper-function-name "^6.22.0" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + +babel-plugin-transform-decorators@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.22.0.tgz#c03635b27a23b23b7224f49232c237a73988d27c" + dependencies: + babel-helper-explode-class "^6.22.0" + babel-plugin-syntax-decorators "^6.13.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.22.0.tgz#00d6e3a0bebdcfe7536b9d653b44a9141e63e47e" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + lodash "^4.2.0" + +babel-plugin-transform-es2015-classes@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.22.0.tgz#54d44998fd823d9dca15292324161c331c1b6f14" + dependencies: + babel-helper-define-map "^6.22.0" + babel-helper-function-name "^6.22.0" + babel-helper-optimise-call-expression "^6.22.0" + babel-helper-replace-supers "^6.22.0" + babel-messages "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-computed-properties@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.22.0.tgz#7c383e9629bba4820c11b0425bdd6290f7f057e7" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.22.0" + +babel-plugin-transform-es2015-destructuring@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.22.0.tgz#8e0af2f885a0b2cf999d47c4c1dd23ce88cfa4c6" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.22.0.tgz#672397031c21610d72dd2bbb0ba9fb6277e1c36b" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-for-of@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.22.0.tgz#180467ad63aeea592a1caeee4bf1c8b3e2616265" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.22.0.tgz#f5fcc8b09093f9a23c76ac3d9e392c3ec4b77104" + dependencies: + babel-helper-function-name "^6.22.0" + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-literals@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + +babel-plugin-transform-es2015-modules-commonjs@^6.22.0, babel-plugin-transform-es2015-modules-commonjs@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.22.0.tgz#6ca04e22b8e214fb50169730657e7a07dc941145" + dependencies: + babel-plugin-transform-strict-mode "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.12.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.22.0.tgz#810cd0cd025a08383b84236b92c6e31f88e644ad" + dependencies: + babel-helper-hoist-variables "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + +babel-plugin-transform-es2015-modules-umd@^6.12.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.22.0.tgz#60d0ba3bd23258719c64391d9bf492d648dc0fae" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + +babel-plugin-transform-es2015-object-super@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.22.0.tgz#daa60e114a042ea769dd53fe528fc82311eb98fc" + dependencies: + babel-helper-replace-supers "^6.22.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.22.0.tgz#57076069232019094f27da8c68bb7162fe208dbb" + dependencies: + babel-helper-call-delegate "^6.22.0" + babel-helper-get-function-arity "^6.22.0" + babel-runtime "^6.22.0" + babel-template "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-shorthand-properties@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.22.0.tgz#8ba776e0affaa60bff21e921403b8a652a2ff723" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-spread@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.22.0.tgz#ab316829e866ee3f4b9eb96939757d19a5bc4593" + dependencies: + babel-helper-regex "^6.22.0" + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-plugin-transform-es2015-template-literals@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.22.0.tgz#87faf2336d3b6a97f68c4d906b0cd0edeae676e1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.3.13: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.22.0.tgz#8d9cc27e7ee1decfe65454fb986452a04a613d20" + dependencies: + babel-helper-regex "^6.22.0" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.22.0.tgz#d57c8335281918e54ef053118ce6eb108468084d" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.22.0" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-flow-strip-types@^6.18.0, babel-plugin-transform-flow-strip-types@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.22.0.tgz#1d419b55e68d2e4f64a5ff3373bd67d73c8e83bc" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-display-name@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.22.0.tgz#077197520fa8562b8d3da4c3c4b0b1bdd7853f26" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-self@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-source@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.22.0.tgz#48556b7dd4c3fe97d1c943bcd54fc3f2561c1817" + dependencies: + babel-helper-builder-react-jsx "^6.22.0" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.6.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.22.0.tgz#65740593a319c44522157538d690b84094617ea6" + dependencies: + regenerator-transform "0.9.8" + +babel-plugin-transform-runtime@^6.9.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.22.0.tgz#10968d760bbf6517243081eec778e10fa828551c" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-strict-mode@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.22.0.tgz#e008df01340fdc87e959da65991b7e05970c8c7c" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.22.0" + +babel-polyfill@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.22.0.tgz#1ac99ebdcc6ba4db1e2618c387b2084a82154a3b" + dependencies: + babel-runtime "^6.22.0" + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-preset-env@^1.1.0: + version "1.1.8" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.1.8.tgz#c46734c6233c3f87d177513773db3cf3c1758aaa" + dependencies: + babel-plugin-check-es2015-constants "^6.3.13" + babel-plugin-syntax-trailing-function-commas "^6.13.0" + babel-plugin-transform-async-to-generator "^6.8.0" + babel-plugin-transform-es2015-arrow-functions "^6.3.13" + babel-plugin-transform-es2015-block-scoped-functions "^6.3.13" + babel-plugin-transform-es2015-block-scoping "^6.6.0" + babel-plugin-transform-es2015-classes "^6.6.0" + babel-plugin-transform-es2015-computed-properties "^6.3.13" + babel-plugin-transform-es2015-destructuring "^6.6.0" + babel-plugin-transform-es2015-duplicate-keys "^6.6.0" + babel-plugin-transform-es2015-for-of "^6.6.0" + babel-plugin-transform-es2015-function-name "^6.3.13" + babel-plugin-transform-es2015-literals "^6.3.13" + babel-plugin-transform-es2015-modules-amd "^6.8.0" + babel-plugin-transform-es2015-modules-commonjs "^6.6.0" + babel-plugin-transform-es2015-modules-systemjs "^6.12.0" + babel-plugin-transform-es2015-modules-umd "^6.12.0" + babel-plugin-transform-es2015-object-super "^6.3.13" + babel-plugin-transform-es2015-parameters "^6.6.0" + babel-plugin-transform-es2015-shorthand-properties "^6.3.13" + babel-plugin-transform-es2015-spread "^6.3.13" + babel-plugin-transform-es2015-sticky-regex "^6.3.13" + babel-plugin-transform-es2015-template-literals "^6.6.0" + babel-plugin-transform-es2015-typeof-symbol "^6.6.0" + babel-plugin-transform-es2015-unicode-regex "^6.3.13" + babel-plugin-transform-exponentiation-operator "^6.8.0" + babel-plugin-transform-regenerator "^6.6.0" + browserslist "^1.4.0" + +babel-preset-jest@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-16.0.0.tgz#417aabc2d7d93170f43c20ef1ea0145e8f7f2db5" + dependencies: + babel-plugin-jest-hoist "^16.0.0" + +babel-preset-jest@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-17.0.2.tgz#141e935debe164aaa0364c220d31ccb2176493b2" + dependencies: + babel-plugin-jest-hoist "^17.0.2" + +babel-preset-jest@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-18.0.0.tgz#84faf8ca3ec65aba7d5e3f59bbaed935ab24049e" + dependencies: + babel-plugin-jest-hoist "^18.0.0" + +babel-preset-react@^6.5.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.22.0.tgz#7bc97e2d73eec4b980fb6b4e4e0884e81ccdc165" + dependencies: + babel-plugin-syntax-flow "^6.3.13" + babel-plugin-syntax-jsx "^6.3.13" + babel-plugin-transform-flow-strip-types "^6.22.0" + babel-plugin-transform-react-display-name "^6.22.0" + babel-plugin-transform-react-jsx "^6.22.0" + babel-plugin-transform-react-jsx-self "^6.22.0" + babel-plugin-transform-react-jsx-source "^6.22.0" + +babel-preset-stage-2@^6.17.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07" + dependencies: + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.22.0" + babel-plugin-transform-decorators "^6.22.0" + babel-preset-stage-3 "^6.22.0" + +babel-preset-stage-3@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.22.0.tgz#a4e92bbace7456fafdf651d7a7657ee0bbca9c2e" + dependencies: + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-generator-functions "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-object-rest-spread "^6.22.0" + +babel-register@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.18.0.tgz#892e2e03865078dd90ad2c715111ec4449b32a68" + dependencies: + babel-core "^6.18.0" + babel-runtime "^6.11.6" + core-js "^2.4.0" + home-or-tmp "^2.0.0" + lodash "^4.2.0" + mkdirp "^0.5.1" + source-map-support "^0.4.2" + +babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.9.0, babel-runtime@^6.9.1: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-runtime@^5.6.18: + version "5.8.38" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19" + dependencies: + core-js "^1.0.0" + +babel-template@^6.16.0: + version "6.16.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.16.0.tgz#e149dd1a9f03a35f817ddbc4d0481988e7ebc8ca" + dependencies: + babel-runtime "^6.9.0" + babel-traverse "^6.16.0" + babel-types "^6.16.0" + babylon "^6.11.0" + lodash "^4.2.0" + +babel-template@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.22.0.tgz#403d110905a4626b317a2a1fcb8f3b73204b2edb" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.22.0" + babel-types "^6.22.0" + babylon "^6.11.0" + lodash "^4.2.0" + +babel-traverse@^6.15.0, babel-traverse@^6.16.0, babel-traverse@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.18.0.tgz#5aeaa980baed2a07c8c47329cd90c3b90c80f05e" + dependencies: + babel-code-frame "^6.16.0" + babel-messages "^6.8.0" + babel-runtime "^6.9.0" + babel-types "^6.18.0" + babylon "^6.11.0" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-traverse@^6.22.0: + version "6.22.1" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.22.1.tgz#3b95cd6b7427d6f1f757704908f2fc9748a5f59f" + dependencies: + babel-code-frame "^6.22.0" + babel-messages "^6.22.0" + babel-runtime "^6.22.0" + babel-types "^6.22.0" + babylon "^6.15.0" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-types@^6.15.0, babel-types@^6.16.0, babel-types@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.18.0.tgz#1f7d5a73474c59eb9151b2417bbff4e4fce7c3f8" + dependencies: + babel-runtime "^6.9.1" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + +babel-types@^6.19.0, babel-types@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.22.0.tgz#2a447e8d0ea25d2512409e4175479fd78cc8b1db" + dependencies: + babel-runtime "^6.22.0" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + +babelify@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/babelify/-/babelify-7.3.0.tgz#aa56aede7067fd7bd549666ee16dc285087e88e5" + dependencies: + babel-core "^6.0.14" + object-assign "^4.0.0" + +babylon@^6.11.0, babylon@^6.13.0: + version "6.13.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.13.1.tgz#adca350e088f0467647157652bafead6ddb8dfdb" + +babylon@^6.15.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" + +balanced-match@0.1.0, balanced-match@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + +balanced-match@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7" + +balanced-match@^0.4.1, balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +base62@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/base62/-/base62-1.1.1.tgz#974e82c11bd5e00816b508a7ed9c7b9086c9db6b" + +base64-js@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" + dependencies: + tweetnacl "^0.14.3" + +bean@~1.0.4: + version "1.0.15" + resolved "https://registry.yarnpkg.com/bean/-/bean-1.0.15.tgz#b4a9fff82618b071471676c8b190868048c34775" + +binary-extensions@^1.0.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +blueimp-md5@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.3.1.tgz#992a6737733b9da1edd641550dc3acab2e9cfc5a" + +blueimp-md5@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-1.1.1.tgz#cf84ba18285f5c8835dae8ddae5af6468ceace17" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.6" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" + +body-parser@^1.14.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.2.tgz#d7578cf4f1d11d5f6ea804cef35dc7a7ff6dae67" + dependencies: + bytes "2.4.0" + content-type "~1.0.2" + debug "~2.2.0" + depd "~1.1.0" + http-errors "~1.5.0" + iconv-lite "0.4.13" + on-finished "~2.3.0" + qs "6.2.0" + raw-body "~2.1.7" + type-is "~1.6.13" + +body-parser@~1.14.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.14.2.tgz#1015cb1fe2c443858259581db53332f8d0cf50f9" + dependencies: + bytes "2.2.0" + content-type "~1.0.1" + debug "~2.2.0" + depd "~1.1.0" + http-errors "~1.3.1" + iconv-lite "0.4.13" + on-finished "~2.3.0" + qs "5.2.0" + raw-body "~2.1.5" + type-is "~1.6.10" + +bole@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bole/-/bole-2.0.0.tgz#d8aa1c690467bfb4fe11b874acb2e8387e382615" + dependencies: + core-util-is ">=1.0.1 <1.1.0-0" + individual ">=3.0.0 <3.1.0-0" + json-stringify-safe ">=5.0.0 <5.1.0-0" + +bonzo@^1.3.6: + version "1.4.0" + resolved "https://registry.yarnpkg.com/bonzo/-/bonzo-1.4.0.tgz#d7d2e06f6b6f67eb3b8fc18774f79058f4ab34df" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +bootstrap: + version "3.3.7" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +brackets2dots@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brackets2dots/-/brackets2dots-1.1.0.tgz#3f3d40375fc660ce0fd004fa27d67b34f9469ac3" + +brfs@^1.4.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.4.3.tgz#db675d6f5e923e6df087fca5859c9090aaed3216" + dependencies: + quote-stream "^1.0.1" + resolve "^1.1.5" + static-module "^1.1.0" + through2 "^2.0.0" + +brorand@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5" + +browser-pack@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.2.tgz#f86cd6cef4f5300c8e63e07a4d512f65fbff4531" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + defined "^1.0.0" + through2 "^2.0.0" + umd "^3.0.0" + +browser-resolve@^1.11.0, browser-resolve@^1.11.2, browser-resolve@^1.7.0: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" + dependencies: + buffer-xor "^1.0.2" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + inherits "^2.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-markdown@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-markdown/-/browserify-markdown-1.0.0.tgz#56321cdbcf92478c0467ef56b321a0766db78243" + dependencies: + highlight.js "^8.6.0" + remarkable "^1.6.0" + string-to-js "0.0.1" + through "^2.3.7" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.0.tgz#10773910c3c206d5420a46aad8694f820b85968f" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserify@^13.0.1: + version "13.3.0" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-13.3.0.tgz#b5a9c9020243f0c70e4675bec8223bc627e415ce" + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^1.11.0" + browserify-zlib "~0.1.2" + buffer "^4.1.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.1" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "~1.1.0" + duplexer2 "~0.1.2" + events "~1.1.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "~0.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + module-deps "^4.0.8" + os-browserify "~0.1.1" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^2.0.0" + string_decoder "~0.10.0" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "~0.0.0" + url "~0.11.0" + util "~0.10.1" + vm-browserify "~0.0.1" + xtend "^4.0.0" + +browserify@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-14.0.0.tgz#67e6cfe7acb2fb1a1908e8a763452306de0bcf38" + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^1.11.0" + browserify-zlib "~0.1.2" + buffer "^5.0.2" + cached-path-relative "^1.0.0" + concat-stream "~1.5.1" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "~1.1.0" + duplexer2 "~0.1.2" + events "~1.1.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "~0.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + module-deps "^4.0.8" + os-browserify "~0.1.1" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^2.0.0" + string_decoder "~0.10.0" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "~0.0.0" + url "~0.11.0" + util "~0.10.1" + vm-browserify "~0.0.1" + xtend "^4.0.0" + +browserslist@^1.0.0, browserslist@^1.0.1, browserslist@^1.4.0, browserslist@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.1.tgz#cc9bd193979a2a4b09fdb3df6003fefe48ccefe1" + dependencies: + caniuse-db "^1.0.30000617" + electron-to-chromium "^1.2.1" + +bser@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169" + dependencies: + node-int64 "^0.4.0" + +budo@^9.0.0: + version "9.4.7" + resolved "https://registry.yarnpkg.com/budo/-/budo-9.4.7.tgz#a6cdcf2572c22ed1331ae91f34a07f265b3dd20b" + dependencies: + bole "^2.0.0" + browserify "^13.0.1" + chokidar "^1.0.1" + connect-pushstate "^1.0.0" + escape-html "^1.0.3" + events "^1.0.2" + garnish "^5.0.0" + get-ports "^1.0.2" + http-proxy "^1.14.0" + inject-lr-script "^2.0.0" + internal-ip "^1.0.1" + micromatch "^2.2.0" + minimist "^1.1.0" + on-finished "^2.3.0" + on-headers "^1.0.1" + once "^1.3.2" + opn "^3.0.2" + pem "^1.8.3" + resolve "^1.1.6" + resp-modifier "^6.0.0" + serve-static "^1.10.0" + simple-html-index "^1.4.0" + stacked "^1.1.1" + stdout-stream "^1.4.0" + strip-ansi "^3.0.0" + term-color "^1.0.1" + tiny-lr "^0.2.0" + url-trim "^1.0.0" + watchify-middleware "^1.6.0" + xtend "^4.0.0" + +buffer-equal@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" + +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + +buffer-xor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@4.9.1, buffer@^4.1.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +buffer@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.2.tgz#41d0407ff76782e9ec19f52f88e237ce6bb0de6d" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +cached-path-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.0.tgz#d1094c577fbd9a8b8bd43c96af6188aa205d05f4" + +cachedir@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-1.1.1.tgz#e1363075ea206a12767d92bb711c8a2f76a10f62" + dependencies: + os-homedir "^1.0.1" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + +camel-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +caniuse-api@^1.3.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.5.2.tgz#8f393c682f661c0a997b77bba6e826483fb3600e" + dependencies: + browserslist "^1.0.1" + caniuse-db "^1.0.30000346" + lodash.memoize "^4.1.0" + lodash.uniq "^4.3.0" + shelljs "^0.7.0" + +caniuse-db@^1.0.30000346, caniuse-db@^1.0.30000617: + version "1.0.30000617" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000617.tgz#9b7fd81f58a35526315c83e60cb5f076f0beb392" + +cardinal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" + dependencies: + ansicolors "~0.2.1" + redeyed "~1.0.0" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" + dependencies: + ansi-styles "^1.1.0" + escape-string-regexp "^1.0.0" + has-ansi "^0.1.0" + strip-ansi "^0.3.0" + supports-color "^0.2.0" + +change-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.0.tgz#6c9c8e35f8790870a82b6b0745be8c3cbef9b081" + dependencies: + camel-case "^3.0.0" + constant-case "^2.0.0" + dot-case "^2.1.0" + header-case "^1.0.0" + is-lower-case "^1.1.0" + is-upper-case "^1.1.0" + lower-case "^1.1.1" + lower-case-first "^1.0.0" + no-case "^2.2.0" + param-case "^2.1.0" + pascal-case "^2.0.0" + path-case "^2.1.0" + sentence-case "^2.1.0" + snake-case "^2.1.0" + swap-case "^1.1.0" + title-case "^2.1.0" + upper-case "^1.1.1" + upper-case-first "^1.1.0" + +chokidar@^1.0.0, chokidar@^1.0.1, chokidar@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +ci-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" + +cipher-base@^1.0.0, cipher-base@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" + dependencies: + inherits "^2.0.1" + +circular-json@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + +classnames@2.x, classnames@^2.1.2, classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-table@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" + dependencies: + colors "1.0.3" + +cli-usage@^0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.4.tgz#7c01e0dc706c234b39c933838c8e20b2175776e2" + dependencies: + marked "^0.3.6" + marked-terminal "^1.6.2" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + +color-convert@^1.3.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/color/-/color-0.10.1.tgz#c04188df82a209ddebccecdacd3ec320f193739f" + dependencies: + color-convert "^0.5.3" + color-string "^0.3.0" + +color@^0.11.0, color@^0.11.3, color@^0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +combine-source-map@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.5.0, commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commitizen@^2.8.2: + version "2.9.5" + resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-2.9.5.tgz#f9605c8c1170eef86331676b5b5f12ab595bf498" + dependencies: + cachedir "^1.1.0" + chalk "1.1.3" + cz-conventional-changelog "1.2.0" + dedent "0.6.0" + detect-indent "4.0.0" + find-node-modules "1.0.4" + find-root "1.0.0" + fs-extra "^1.0.0" + glob "7.1.1" + inquirer "1.2.3" + lodash "4.17.2" + minimist "1.2.0" + path-exists "2.1.0" + shelljs "0.7.5" + strip-json-comments "2.0.1" + +commoner@^0.10.1: + version "0.10.4" + resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.4.tgz#98f3333dd3ad399596bb2d384a783bb7213d68f8" + dependencies: + commander "^2.5.0" + detective "^4.3.1" + glob "^5.0.15" + graceful-fs "^4.1.2" + iconv-lite "^0.4.5" + mkdirp "^0.5.0" + private "^0.1.6" + q "^1.1.2" + recast "^0.10.0" + +component-classes@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691" + dependencies: + component-indexof "0.0.3" + +component-indexof@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.6, concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@~1.4.5: + version "1.4.10" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.4.10.tgz#acc3bbf5602cb8cc980c6ac840fa7d8603e3ef36" + dependencies: + inherits "~2.0.1" + readable-stream "~1.1.9" + typedarray "~0.0.5" + +concat-stream@~1.5.0, concat-stream@~1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +connect-pushstate@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/connect-pushstate/-/connect-pushstate-1.1.0.tgz#bcab224271c439604a0fb0f614c0a5f563e88e24" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constant-case@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" + dependencies: + snake-case "^2.1.0" + upper-case "^1.1.1" + +constants-browserify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-type-parser@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" + +content-type@~1.0.1, content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +conventional-commit-types@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/conventional-commit-types/-/conventional-commit-types-2.1.0.tgz#45d860386c9a2e6537ee91d8a1b61bd0411b3d04" + +convert-source-map@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" + +convert-source-map@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +core-js@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + +core-js@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" + +"core-util-is@>=1.0.1 <1.1.0-0", core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cors@^2.3.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.1.tgz#6181aa56abb45a2825be3304703747ae4e9d2383" + dependencies: + vary "^1" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.2.tgz#51210062d7bb7479f6c65bb41a92208b1d61abad" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^1.0.0" + sha.js "^2.3.6" + +create-hmac@^1.1.0, create-hmac@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.4.tgz#d3fb4ba253eb8b3f56e39ea2fbcb8af747bd3170" + dependencies: + create-hash "^1.1.0" + inherits "^2.0.1" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-browserify@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-1.0.9.tgz#cc5449685dfb85eb11c9828acc7cb87ab5bbfcc0" + +crypto-browserify@^3.0.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + +css-animation@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.3.0.tgz#315d9742bbe282c23b9951ceac2aa53cee05b708" + dependencies: + component-classes "^1.2.5" + +css-color-function@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" + dependencies: + balanced-match "0.1.0" + color "^0.11.0" + debug "~0.7.4" + rgb "~0.1.0" + +cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" + +"cssstyle@>= 0.2.36 < 0.3.0": + version "0.2.37" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" + dependencies: + cssom "0.3.x" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +curry2@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/curry2/-/curry2-0.1.0.tgz#cc9837ec9148a6eaea47f6cedf254d103031a233" + +curry2@^1.0.0, curry2@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/curry2/-/curry2-1.0.3.tgz#38191d55f1060bfea47ca08009385bb878f6612f" + dependencies: + fast-bind "^1.0.0" + +cz-conventional-changelog@1.2.0, cz-conventional-changelog@^1.1.6: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.2.0.tgz#2bca04964c8919b23f3fd6a89ef5e6008b31b3f8" + dependencies: + conventional-commit-types "^2.0.0" + lodash.map "^4.5.1" + longest "^1.0.1" + pad-right "^0.2.2" + right-pad "^1.0.1" + word-wrap "^1.0.3" + +d@^0.1.1, d@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" + dependencies: + es5-ext "~0.10.2" + +dashdash@^1.12.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.0.tgz#29e486c5418bf0f356034a993d51686a33e84141" + dependencies: + assert-plus "^1.0.0" + +date-now@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-1.0.1.tgz#bb7d086438debe4182a485fb3df3fbfb99d6153c" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +dbf@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/dbf/-/dbf-0.1.4.tgz#162f0d8fd1db37c976458ac6fe6f55b48c72f466" + dependencies: + jdataview "~2.5.0" + +debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.0.0.tgz#0948af513d2e4ce407916f8506a423d3f9cf72d8" + dependencies: + date-now "1.0.1" + +debug-log@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" + +debug@*, debug@^2.1.1, debug@^2.2.0, debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +debug@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.1.3.tgz#ce8ab1b5ee8fbee2bfa3b633cab93d366b63418e" + dependencies: + ms "0.7.0" + +debug@~0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +dedent@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.6.0.tgz#0e6da8f0ce52838ef5cec5c8f9396b0c1b64a3cb" + +deep-diff@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.4.tgz#aac5c39952236abe5f037a2349060ba01b00ae48" + +deep-equal@1.0.1, deep-equal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +default-require-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" + dependencies: + strip-bom "^2.0.0" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +deglob@^2.0.0, deglob@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/deglob/-/deglob-2.1.0.tgz#4d44abe16ef32c779b4972bd141a80325029a14a" + dependencies: + find-root "^1.0.0" + glob "^7.0.5" + ignore "^3.0.9" + pkg-config "^1.1.0" + run-parallel "^1.1.2" + uniq "^1.0.1" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +deps-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" + dependencies: + JSONStream "^1.0.3" + shasum "^1.0.0" + subarg "^1.0.0" + through2 "^2.0.0" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-file@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" + dependencies: + fs-exists-sync "^0.1.0" + +detect-indent@4.0.0, detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detective@^4.0.0, detective@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.3.2.tgz#77697e2e7947ac3fe7c8e26a6d6f115235afa91c" + dependencies: + acorn "^3.1.0" + defined "^1.0.0" + +diff@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +disposables@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.1.tgz#064727a25b54f502bd82b89aa2dfb8df9f1b39e3" + +dnd-core@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.0.2.tgz#4528da4fbeb1abb6c308414b2d2e8389f3514646" + dependencies: + asap "^2.0.3" + invariant "^2.0.0" + lodash "^4.2.0" + redux "^3.2.0" + +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +dom-align@1.x: + version "1.5.2" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.5.2.tgz#bceb6c109a3442ebc001c04c48cb37a9d5e96df2" + +dom-helpers@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-2.4.0.tgz#9bb4b245f637367b1fa670274272aa28fe06c367" + +domain-browser@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +domready@~0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/domready/-/domready-0.2.13.tgz#5ec8340b5ee5f76ad72fd32007cfb5d38a2cd361" + +dot-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.0.tgz#4b43dd0d7403c34cb645424add397e80bfe85ca6" + dependencies: + no-case "^2.2.0" + +dotsplit.js@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dotsplit.js/-/dotsplit.js-1.0.3.tgz#b71938c07364668d16ef4309273ca210ba00691d" + dependencies: + array.filter "^0.1.0" + arraymap "^0.1.2" + +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +duplexer2@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" + dependencies: + readable-stream "~1.1.9" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +ejs@~0.8.3: + version "0.8.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598" + +ejsify@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ejsify/-/ejsify-0.1.0.tgz#bf910f01a5eee71b7016ef614726003a6c1fae77" + dependencies: + ejs "~0.8.3" + through "~2.3.4" + +electron-to-chromium@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.2.1.tgz#63ac7579a1c5bedb296c8607621f2efc9a54b968" + +element-class@^0.2.0, element-class@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e" + +elliptic@^6.0.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.2.tgz#e4c81e0829cf0a65ab70e998b8232723b5c1bc48" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + inherits "^2.0.1" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +envify@^3.0.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8" + dependencies: + jstransform "^11.0.3" + through "~2.3.4" + +envify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/envify/-/envify-4.0.0.tgz#f791343e3d11cc29cce41150300a8af61c66cab0" + dependencies: + esprima "~3.1.0" + through "~2.3.4" + +"errno@>=0.1.1 <0.2.0-0": + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + +error-ex@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9" + dependencies: + is-arrayish "^0.2.1" + +errorify@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/errorify/-/errorify-0.3.1.tgz#53e0aaeeb18adc3e55f9f1eb4e2d95929f41b79b" + +es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: + version "0.10.12" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" + dependencies: + d "^0.1.1" + es5-ext "^0.10.7" + es6-symbol "3" + +es6-map@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-set "~0.1.3" + es6-symbol "~3.1.0" + event-emitter "~0.3.4" + +es6-promise@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" + +es6-set@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + es6-iterator "2" + es6-symbol "3" + event-emitter "~0.3.4" + +es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" + dependencies: + d "~0.1.1" + es5-ext "~0.10.11" + +es6-weak-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" + dependencies: + d "^0.1.1" + es5-ext "^0.10.8" + es6-iterator "2" + es6-symbol "3" + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@^1.6.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +escodegen@~0.0.24: + version "0.0.28" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-0.0.28.tgz#0e4ff1715f328775d6cab51ac44a406cd7abffd3" + dependencies: + esprima "~1.0.2" + estraverse "~1.3.0" + optionalDependencies: + source-map ">= 0.1.2" + +escodegen@~1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.3.3.tgz#f024016f5a88e046fd12005055e939802e6c5f23" + dependencies: + esprima "~1.1.1" + estraverse "~1.5.0" + esutils "~1.0.0" + optionalDependencies: + source-map "~0.1.33" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-config-standard-jsx@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-3.2.0.tgz#c240e26ed919a11a42aa4de8059472b38268d620" + +eslint-config-standard@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-6.2.1.tgz#d3a68aafc7191639e7ee441e7348739026354292" + +eslint-plugin-promise@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.4.0.tgz#6ba9048c2df57be77d036e0c68918bc9b4fc4195" + +eslint-plugin-react@~6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.7.1.tgz#1af96aea545856825157d97c1b50d5a8fb64a5a7" + dependencies: + doctrine "^1.2.2" + jsx-ast-utils "^1.3.3" + +eslint-plugin-standard@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-2.0.1.tgz#3589699ff9c917f2c25f76a916687f641c369ff3" + +eslint@~3.10.2: + version "3.10.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.10.2.tgz#c9a10e8bf6e9d65651204778c503341f1eac3ce7" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + escope "^3.6.0" + espree "^3.3.1" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.2.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~1.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.3.2.tgz#dbf3fadeb4ecb4d4778303e50103b3d36c88b89c" + dependencies: + acorn "^4.0.1" + acorn-jsx "^3.0.0" + +esprima-fb@^15001.1.0-dev-harmony-fb: + version "15001.1.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" + +esprima-fb@~15001.1001.0-dev-harmony-fb: + version "15001.1001.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" + +esprima@^2.6.0, esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@~1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad" + +esprima@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.1.1.tgz#5b6f1547f4d102e670e140c509be6771d6aeb549" + +esprima@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" + +esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + +estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.3.2.tgz#37c2b893ef13d723f276d878d60d8535152a6c42" + +estraverse@~1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.5.1.tgz#867a3e8e58a9f84618afb6c2ddbcd916b7cbaf71" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + +esutils@^2.0.0, esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +esutils@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.0.0.tgz#8151d358e20c8acc7fb745e7472c0025fe496570" + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + +event-emitter@~0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" + dependencies: + d "~0.1.1" + es5-ext "~0.10.7" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events@^1.0.2, events@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53" + dependencies: + create-hash "^1.1.1" + +exec-sh@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" + dependencies: + merge "^1.1.3" + +exenv@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +exorcist@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/exorcist/-/exorcist-0.4.0.tgz#1230ffdedd9248f42fbccf8b4a44d4cab29e3c64" + dependencies: + minimist "0.0.5" + mold-source-map "~0.4.0" + nave "~0.5.1" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + dependencies: + os-homedir "^1.0.1" + +extend@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-1.3.0.tgz#d1516fb0ff5624d2ebf9123ea1dac5a1994004f8" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +external-editor@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b" + dependencies: + extend "^3.0.0" + spawn-sync "^1.0.15" + tmp "^0.0.29" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +falafel@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/falafel/-/falafel-1.2.0.tgz#c18d24ef5091174a497f318cd24b026a25cddab4" + dependencies: + acorn "^1.0.3" + foreach "^2.0.5" + isarray "0.0.1" + object-keys "^1.0.6" + +fast-bind@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-bind/-/fast-bind-1.0.0.tgz#7fa9652cb3325f5cd1e252d6cb4f160de1a76e75" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +faye-websocket@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^1.8.0, fb-watchman@^1.9.0: + version "1.9.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-1.9.2.tgz#a24cf47827f82d38fb59a69ad70b76e3b6ae7383" + dependencies: + bser "1.0.2" + +fbjs@0.1.0-alpha.10: + version "0.1.0-alpha.10" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.1.0-alpha.10.tgz#46e457c09cbefb51fc752a3e030e7b67fcc384c8" + dependencies: + core-js "^1.0.0" + promise "^7.0.3" + whatwg-fetch "^0.9.0" + +fbjs@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.3.2.tgz#033a540595084b5de3509a405d06f1a2a8e5b9fb" + dependencies: + core-js "^1.0.0" + loose-envify "^1.0.0" + promise "^7.0.3" + ua-parser-js "^0.7.9" + whatwg-fetch "^0.9.0" + +fbjs@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7" + dependencies: + core-js "^1.0.0" + loose-envify "^1.0.0" + promise "^7.0.3" + ua-parser-js "^0.7.9" + whatwg-fetch "^0.9.0" + +fbjs@^0.8.1, fbjs@^0.8.4: + version "0.8.5" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.5.tgz#f69ba8a876096cb1b9bffe4d7c1e71c19d39d008" + dependencies: + core-js "^1.0.0" + immutable "^3.7.6" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + ua-parser-js "^0.7.9" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +filename-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" + +fileset@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" + dependencies: + glob "^7.0.3" + minimatch "^3.0.3" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-node-modules@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-1.0.4.tgz#b6deb3cccb699c87037677bcede2c5f5862b2550" + dependencies: + findup-sync "0.4.2" + merge "^1.2.0" + +find-root@1.0.0, find-root@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.0.0.tgz#962ff211aab25c6520feeeb8d6287f8f6e95807a" + +find-up@^1.0.0, find-up@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +findup-sync@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.2.tgz#a8117d0f73124f5a4546839579fe52d7129fb5e5" + dependencies: + detect-file "^0.1.0" + is-glob "^2.0.1" + micromatch "^2.3.7" + resolve-dir "^0.1.0" + +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +font-awesome: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + +for-in@^0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" + +for-own@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072" + dependencies: + for-in "^0.1.5" + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.1.tgz#4adf0342e1a79afa1e84c8c320a9ffc82392a1f3" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + +from2-string@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/from2-string/-/from2-string-1.1.0.tgz#18282b27d08a267cb3030cd2b8b4b0f212af752a" + dependencies: + from2 "^2.0.3" + +from2@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "http://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.0.17" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.17.tgz#8537f3f12272678765b4fd6528c0f1f66f8f4558" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.29" + +fstream-ignore@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" + +garnish@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/garnish/-/garnish-5.2.0.tgz#bed43659382e4b198e33c793897be7c701e65577" + dependencies: + chalk "^0.5.1" + minimist "^1.1.0" + pad-left "^2.0.0" + pad-right "^0.2.2" + prettier-bytes "^1.0.3" + pretty-ms "^2.1.0" + right-now "^1.0.0" + split2 "^0.2.1" + stdout-stream "^1.4.0" + url-trim "^1.0.0" + +gauge@~2.7.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.2.tgz#15cecc31b02d05345a5d6b0e171cdb3ad2307774" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + supports-color "^0.2.0" + wide-align "^1.1.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +geojson-area@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/geojson-area/-/geojson-area-0.2.1.tgz#2537b0982db86309f21d2c428a4257c7a6282cc6" + dependencies: + wgs84 "0.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-ports@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-ports/-/get-ports-1.0.3.tgz#f40bd580aca7ec0efb7b96cbfcbeb03ef894b5e8" + dependencies: + map-limit "0.0.1" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stdin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + +globals@^9.0.0, globals@^9.2.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.12.0.tgz#992ce90828c3a55fa8f16fada177adb64664cf9d" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.9.tgz#baacba37d19d11f9d146d3578bc99958c3787e29" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +gravatar@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/gravatar/-/gravatar-1.5.2.tgz#25b4c96790b82d1b3c714e3963999253bd2ba1cd" + +growly@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + +handlebars@^4.0.3: + version "4.0.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" + dependencies: + ansi-regex "^0.2.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash.js@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573" + dependencies: + inherits "^2.0.1" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +header-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-1.0.0.tgz#d9e335909505d56051ec16a0106821889e910781" + dependencies: + no-case "^2.2.0" + upper-case "^1.1.3" + +highlight.js@^8.6.0: + version "8.9.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-8.9.1.tgz#b8a9c5493212a9392f0222b649c9611497ebfb88" + +history@^2.0.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/history/-/history-2.1.2.tgz#4aa2de897a0e4867e4539843be6ecdb2986bfdec" + dependencies: + deep-equal "^1.0.0" + invariant "^2.0.0" + query-string "^3.0.0" + warning "^2.0.0" + +history@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/history/-/history-3.2.1.tgz#71c7497f4e6090363d19a6713bb52a1bfcdd99aa" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + query-string "^4.2.2" + warning "^3.0.0" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +homedir-polyfill@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + +hosted-git-info@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" + +html-encoding-sniffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" + dependencies: + whatwg-encoding "^1.0.1" + +htmlescape@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" + +http-errors@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" + dependencies: + inherits "~2.0.1" + statuses "1" + +http-errors@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.0.tgz#b1cb3d8260fd8e2386cad3189045943372d48211" + dependencies: + inherits "2.0.1" + setprototypeof "1.0.1" + statuses ">= 1.3.0 < 2" + +http-errors@~1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" + dependencies: + inherits "2.0.3" + setprototypeof "1.0.2" + statuses ">= 1.3.1 < 2" + +http-proxy@^1.14.0: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +iconv-lite@0.4.13, iconv-lite@^0.4.13, iconv-lite@^0.4.5, iconv-lite@~0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +ignore@^3.0.9, ignore@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410" + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + +immutable@^3.7.3, immutable@^3.7.6: + version "3.8.1" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +"individual@>=3.0.0 <3.1.0-0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/individual/-/individual-3.0.0.tgz#e7ca4f85f8957b018734f285750dc22ec2f9862d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1, inherits@^2.0.1, inherits@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +inject-lr-script@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.1.0.tgz#e61b5e84c118733906cbea01ec3d746698a39f65" + dependencies: + resp-modifier "^6.0.0" + +inline-source-map@~0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" + dependencies: + source-map "~0.5.3" + +inquirer@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + external-editor "^1.1.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +insert-module-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + concat-stream "~1.5.1" + is-buffer "^1.1.0" + lexical-scope "^1.2.0" + process "~0.11.0" + through2 "^2.0.0" + xtend "^4.0.0" + +internal-ip@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + +interpret@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" + +invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.1.tgz#b097010547668c7e337028ebe816ebe36c8a8d54" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2, is-buffer@^1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-ci@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + +is-dotfile@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0, is-finite@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-lower-case@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" + dependencies: + lower-case "^1.1.0" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-number@^2.0.2, is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-upper-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f" + dependencies: + upper-case "^1.1.0" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + +isarray@0.0.1, isarray@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" + +isnumeric@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" + +iso-639-1@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-1.2.4.tgz#b0cef68adf904839c7d3e90bea249e79cc9498ee" + dependencies: + babel-runtime "^6.18.0" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istanbul-api@^1.1.0-alpha.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.1.tgz#d36e2f1560d1a43ce304c4ff7338182de61c8f73" + dependencies: + async "^2.1.4" + fileset "^2.0.2" + istanbul-lib-coverage "^1.0.0" + istanbul-lib-hook "^1.0.0" + istanbul-lib-instrument "^1.3.0" + istanbul-lib-report "^1.0.0-alpha.3" + istanbul-lib-source-maps "^1.1.0" + istanbul-reports "^1.0.0" + js-yaml "^3.7.0" + mkdirp "^0.5.1" + once "^1.4.0" + +istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.0.tgz#c3f9b6d226da12424064cce87fce0fb57fdfa7a2" + +istanbul-lib-hook@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.0.tgz#fc5367ee27f59268e8f060b0c7aaf051d9c425c5" + dependencies: + append-transform "^0.4.0" + +istanbul-lib-instrument@^1.1.1, istanbul-lib-instrument@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.2.0.tgz#73d5d108ab7568c373fdcb7d01c1d42d565bc8c4" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.13.0" + istanbul-lib-coverage "^1.0.0" + semver "^5.3.0" + +istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.13.0" + istanbul-lib-coverage "^1.0.0" + semver "^5.3.0" + +istanbul-lib-report@^1.0.0-alpha.3: + version "1.0.0-alpha.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.0.0-alpha.3.tgz#32d5f6ec7f33ca3a602209e278b2e6ff143498af" + dependencies: + async "^1.4.2" + istanbul-lib-coverage "^1.0.0-alpha" + mkdirp "^0.5.1" + path-parse "^1.0.5" + rimraf "^2.4.3" + supports-color "^3.1.2" + +istanbul-lib-source-maps@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.1.0.tgz#9d429218f35b823560ea300a96ff0c3bbdab785f" + dependencies: + istanbul-lib-coverage "^1.0.0-alpha.0" + mkdirp "^0.5.1" + rimraf "^2.4.4" + source-map "^0.5.3" + +istanbul-reports@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.0.1.tgz#9a17176bc4a6cbebdae52b2f15961d52fa623fbc" + dependencies: + handlebars "^4.0.3" + +jdataview@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/jdataview/-/jdataview-2.5.0.tgz#3081b3fea651f9317ec6bd4feb2ddc98aa41d595" + +jest-changed-files@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-17.0.2.tgz#f5657758736996f590a51b87e5c9369d904ba7b7" + +jest-cli@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-18.1.0.tgz#5ead36ecad420817c2c9baa2aa7574f63257b3d6" + dependencies: + ansi-escapes "^1.4.0" + callsites "^2.0.0" + chalk "^1.1.1" + graceful-fs "^4.1.6" + is-ci "^1.0.9" + istanbul-api "^1.1.0-alpha.1" + istanbul-lib-coverage "^1.0.0" + istanbul-lib-instrument "^1.1.1" + jest-changed-files "^17.0.2" + jest-config "^18.1.0" + jest-environment-jsdom "^18.1.0" + jest-file-exists "^17.0.0" + jest-haste-map "^18.1.0" + jest-jasmine2 "^18.1.0" + jest-mock "^18.0.0" + jest-resolve "^18.1.0" + jest-resolve-dependencies "^18.1.0" + jest-runtime "^18.1.0" + jest-snapshot "^18.1.0" + jest-util "^18.1.0" + json-stable-stringify "^1.0.0" + node-notifier "^4.6.1" + sane "~1.4.1" + strip-ansi "^3.0.1" + throat "^3.0.0" + which "^1.1.1" + worker-farm "^1.3.1" + yargs "^6.3.0" + +jest-config@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-18.1.0.tgz#6111740a6d48aab86ff5a9e6ab0b98bd993b6ff4" + dependencies: + chalk "^1.1.1" + jest-environment-jsdom "^18.1.0" + jest-environment-node "^18.1.0" + jest-jasmine2 "^18.1.0" + jest-mock "^18.0.0" + jest-resolve "^18.1.0" + jest-util "^18.1.0" + json-stable-stringify "^1.0.0" + +jest-diff@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-18.1.0.tgz#4ff79e74dd988c139195b365dc65d87f606f4803" + dependencies: + chalk "^1.1.3" + diff "^3.0.0" + jest-matcher-utils "^18.1.0" + pretty-format "^18.1.0" + +jest-environment-jsdom@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-18.1.0.tgz#18b42f0c4ea2bae9f36cab3639b1e8f8c384e24e" + dependencies: + jest-mock "^18.0.0" + jest-util "^18.1.0" + jsdom "^9.9.1" + +jest-environment-node@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-18.1.0.tgz#4d6797572c8dda99acf5fae696eb62945547c779" + dependencies: + jest-mock "^18.0.0" + jest-util "^18.1.0" + +jest-file-exists@^17.0.0: + version "17.0.0" + resolved "https://registry.yarnpkg.com/jest-file-exists/-/jest-file-exists-17.0.0.tgz#7f63eb73a1c43a13f461be261768b45af2cdd169" + +jest-haste-map@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-18.1.0.tgz#06839c74b770a40c1a106968851df8d281c08375" + dependencies: + fb-watchman "^1.9.0" + graceful-fs "^4.1.6" + micromatch "^2.3.11" + sane "~1.4.1" + worker-farm "^1.3.1" + +jest-jasmine2@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-18.1.0.tgz#094e104c2c189708766c77263bb2aecb5860a80b" + dependencies: + graceful-fs "^4.1.6" + jest-matcher-utils "^18.1.0" + jest-matchers "^18.1.0" + jest-snapshot "^18.1.0" + jest-util "^18.1.0" + +jest-matcher-utils@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-18.1.0.tgz#1ac4651955ee2a60cef1e7fcc98cdfd773c0f932" + dependencies: + chalk "^1.1.3" + pretty-format "^18.1.0" + +jest-matchers@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-18.1.0.tgz#0341484bf87a1fd0bac0a4d2c899e2b77a3f1ead" + dependencies: + jest-diff "^18.1.0" + jest-matcher-utils "^18.1.0" + jest-util "^18.1.0" + pretty-format "^18.1.0" + +jest-mock@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-18.0.0.tgz#5c248846ea33fa558b526f5312ab4a6765e489b3" + +jest-resolve-dependencies@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-18.1.0.tgz#8134fb5caf59c9ed842fe0152ab01c52711f1bbb" + dependencies: + jest-file-exists "^17.0.0" + jest-resolve "^18.1.0" + +jest-resolve@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-18.1.0.tgz#6800accb536658c906cd5e29de412b1ab9ac249b" + dependencies: + browser-resolve "^1.11.2" + jest-file-exists "^17.0.0" + jest-haste-map "^18.1.0" + resolve "^1.2.0" + +jest-runtime@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-18.1.0.tgz#3abfd687175b21fc3b85a2b8064399e997859922" + dependencies: + babel-core "^6.0.0" + babel-jest "^18.0.0" + babel-plugin-istanbul "^3.0.0" + chalk "^1.1.3" + graceful-fs "^4.1.6" + jest-config "^18.1.0" + jest-file-exists "^17.0.0" + jest-haste-map "^18.1.0" + jest-mock "^18.0.0" + jest-resolve "^18.1.0" + jest-snapshot "^18.1.0" + jest-util "^18.1.0" + json-stable-stringify "^1.0.0" + micromatch "^2.3.11" + yargs "^6.3.0" + +jest-snapshot@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-18.1.0.tgz#55b96d2ee639c9bce76f87f2a3fd40b71c7a5916" + dependencies: + jest-diff "^18.1.0" + jest-file-exists "^17.0.0" + jest-matcher-utils "^18.1.0" + jest-util "^18.1.0" + natural-compare "^1.4.0" + pretty-format "^18.1.0" + +jest-util@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-18.1.0.tgz#3a99c32114ab17f84be094382527006e6d4bfc6a" + dependencies: + chalk "^1.1.1" + diff "^3.0.0" + graceful-fs "^4.1.6" + jest-file-exists "^17.0.0" + jest-mock "^18.0.0" + mkdirp "^0.5.1" + +jest@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-18.1.0.tgz#bcebf1e203dee5c2ad2091c805300a343d9e6c7d" + dependencies: + jest-cli "^18.1.0" + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +js-base64@^2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + +js-tokens@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" + +js-yaml@^3.5.1, js-yaml@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" + +jsdom@^9.9.1: + version "9.9.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.9.1.tgz#84f3972ad394ab963233af8725211bce4d01bfd5" + dependencies: + abab "^1.0.0" + acorn "^2.4.0" + acorn-globals "^1.0.4" + array-equal "^1.0.0" + content-type-parser "^1.0.1" + cssom ">= 0.3.0 < 0.4.0" + cssstyle ">= 0.2.36 < 0.3.0" + escodegen "^1.6.1" + html-encoding-sniffer "^1.0.1" + iconv-lite "^0.4.13" + nwmatcher ">= 1.3.9 < 2.0.0" + parse5 "^1.5.1" + request "^2.55.0" + sax "^1.1.4" + symbol-tree ">= 3.1.0 < 4.0.0" + tough-cookie "^2.3.1" + webidl-conversions "^3.0.1" + whatwg-encoding "^1.0.1" + whatwg-url "^4.1.0" + xml-name-validator ">= 2.0.1 < 3.0.0" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-fallback@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-fallback/-/json-fallback-0.0.1.tgz#e8e3083c3fddad0f9b5f09d3312074442580d781" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stable-stringify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" + dependencies: + jsonify "~0.0.0" + +"json-stringify-safe@>=5.0.0 <5.1.0-0", json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.0.tgz#9b20715b026cbe3778fd769edccd822d8332a5b2" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonp@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/jsonp/-/jsonp-0.2.0.tgz#95fcd751cf0a67adb9a5a9c4628ffeb97bec63e8" + dependencies: + debug "2.1.3" + +jsonp@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/jsonp/-/jsonp-0.0.4.tgz#94665a4b771aabecb8aac84135b99594756918bd" + dependencies: + debug "*" + +jsonparse@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.0.tgz#85fc245b1d9259acc6941960b905adf64e7de0e8" + +jsonpointer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +jstransform@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" + dependencies: + base62 "^1.1.0" + commoner "^0.10.1" + esprima-fb "^15001.1.0-dev-harmony-fb" + object-assign "^2.0.0" + source-map "^0.4.2" + +jsx-ast-utils@^1.3.3: + version "1.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.3.5.tgz#9ba6297198d9f754594d62e59496ffb923778dd4" + dependencies: + acorn-jsx "^3.0.1" + object-assign "^4.1.0" + +jszip@2.5.0, jszip@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-2.5.0.tgz#7444fd8551ddf3e5da7198fea0c91bc8308cc274" + dependencies: + pako "~0.2.5" + +jszip@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.3.tgz#8a920403b2b1651c0fc126be90192d9080957c37" + dependencies: + core-js "~2.3.0" + es6-promise "~3.0.2" + lie "~3.1.0" + pako "~1.0.2" + readable-stream "~2.0.6" + +jwt-decode@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.1.0.tgz#d3079cef1689d82d56bbb7aedcfea28b12f0e36a" + +keycode@^2.1.2: + version "2.1.7" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.7.tgz#7b9255919f6cff562b09a064d222dca70b020f5c" + +kind-of@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.0.4.tgz#7b8ecf18a4e17f8269d73b501c9f232c96887a74" + dependencies: + is-buffer "^1.0.2" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +labeled-stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59" + dependencies: + inherits "^2.0.1" + isarray "~0.0.1" + stream-splicer "^2.0.0" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +leaflet-draw@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/leaflet-draw/-/leaflet-draw-0.4.1.tgz#e46d21e42c82364ae871707373abad9723983ff6" + +leaflet-editable@^1.0.0-rc.1: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/leaflet-editable/-/leaflet-editable-1.0.0-rc.2.tgz#27d7e99acc9df4df8887e520c7d87079fae69176" + +leaflet@^0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-0.7.7.tgz#1e352ba54e63d076451fa363c900890cb2cf75ee" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lexical-scope@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4" + dependencies: + astw "^2.0.0" + +lie@^3.0.1, lie@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.0.tgz#65e0139eaef9ae791a1f5c8c53692c8d3b4718f4" + dependencies: + immediate "~3.0.5" + +livereload-js@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +lodash-es@^4.2.1: + version "4.16.6" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.16.6.tgz#c8552faedaa4d1d591de8da9b3980ef1c52efa08" + +lodash._arraycopy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" + +lodash._arrayeach@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._baseassign "^3.0.0" + lodash._basefor "^3.0.0" + lodash.isarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basefor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.clonedeep@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" + dependencies: + lodash._baseclone "^3.0.0" + lodash._bindcallback "^3.0.0" + +lodash.indexof@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isequal@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.4.0.tgz#6295768e98e14dc15ce8d362ef6340db82852031" + +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + +lodash.keys@^3.0.0, lodash.keys@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.map@^4.5.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + +lodash.memoize@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.memoize@~3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" + +lodash.merge@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" + +lodash.pickby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" + +lodash.template@^4.2.4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.uniq@^4.3.0, lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@4.17.2, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.3.0: + version "4.17.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" + +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.16.1, lodash@^4.2.0, lodash@^4.2.1: + version "4.16.6" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" + +lodash@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.5.0.tgz#19bb3f4d51278f0b8c818ed145c74ecf9fe40e6d" + +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + dependencies: + chalk "^1.0.0" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +lonlng@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/lonlng/-/lonlng-0.0.2.tgz#46d85e634a9e0c8d8e468eb61b375fd479b7973a" + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.0.tgz#6b26248c42f6d4fa4b0d8542f78edfcde35642a8" + dependencies: + js-tokens "^2.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lower-case-first@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" + dependencies: + lower-case "^1.1.2" + +lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.3.tgz#c92393d976793eee5ba4edb583cf8eae35bd9bfb" + +lru-cache@^2.7.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + +map-limit@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/map-limit/-/map-limit-0.0.1.tgz#eb7961031c0f0e8d001bf2d56fab685d58822f38" + dependencies: + once "~1.3.0" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +marked-terminal@^1.6.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904" + dependencies: + cardinal "^1.0.0" + chalk "^1.1.3" + cli-table "^0.3.1" + lodash.assign "^4.2.0" + node-emoji "^1.4.1" + +marked@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" + +mastarm@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/mastarm/-/mastarm-3.3.0.tgz#51918979fe1f3434fc6acf673628c81bc869925a" + dependencies: + aws-sdk "^2.4.2" + babel-core "^6.10.4" + babel-eslint "^7.0.0" + babel-jest "^17.0.2" + babel-plugin-add-module-exports "^0.2.1" + babel-plugin-transform-flow-strip-types "^6.18.0" + babel-plugin-transform-runtime "^6.9.0" + babel-preset-env "^1.1.0" + babel-preset-react "^6.5.0" + babel-preset-stage-2 "^6.17.0" + babelify "^7.3.0" + browserify "^13.0.1" + browserify-markdown "1.0.0" + budo "^9.0.0" + chokidar "^1.6.0" + commander "^2.9.0" + commitizen "^2.8.2" + concat-stream "^1.5.1" + cz-conventional-changelog "^1.1.6" + envify "^4.0.0" + errorify "^0.3.1" + exorcist "^0.4.0" + http-proxy "^1.14.0" + isomorphic-fetch "^2.2.1" + jest "^18.1.0" + lodash.uniq "^4.5.0" + mime "^1.3.4" + mkdirp "^0.5.1" + postcss "^5.0.21" + postcss-cssnext "^2.6.0" + postcss-import "^9.0.0" + postcss-reporter "^3.0.0" + postcss-safe-parser "^2.0.0" + rimraf "^2.5.4" + standard "^8.3.0" + standard-engine "^5.0.0" + through2 "^2.0.1" + uglifyify "^3.0.2" + uuid "^3.0.0" + watchify "^3.7.0" + yamljs "^0.2.8" + +material-colors@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.0.tgz#956f0c87660882333cff7b7557efb7b03592820e" + +math-expression-evaluator@^1.2.14: + version "1.2.15" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.15.tgz#38dc5f0194c5bf5ff1c690ad4c4b64df71ac0187" + dependencies: + lodash.indexof "^4.0.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge@^1.1.3, merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +mgrs@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mgrs/-/mgrs-0.0.3.tgz#3058d38ae92e1abfbf74b32a8f6cb5225a6eaa05" + +micromatch@^2.1.5, micromatch@^2.2.0, micromatch@^2.3.11, micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@~1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.24.0.tgz#e2d13f939f0016c6e4e9ad25a8652f126c467f0c" + +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.7: + version "2.1.12" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.12.tgz#152ba256777020dd4663f54c2e7bc26381e71729" + dependencies: + mime-db "~1.24.0" + +mime@1.3.4, mime@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +module-deps@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.0.8.tgz#55fd70623399706c3288bef7a609ff1e8c0ed2bb" + dependencies: + JSONStream "^1.0.3" + browser-resolve "^1.7.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.0" + defined "^1.0.0" + detective "^4.0.0" + duplexer2 "^0.1.2" + inherits "^2.0.1" + parents "^1.0.0" + readable-stream "^2.0.2" + resolve "^1.1.3" + stream-combiner2 "^1.1.1" + subarg "^1.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + +mold-source-map@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/mold-source-map/-/mold-source-map-0.4.0.tgz#cf67e0b31c47ab9badb5c9c25651862127bb8317" + dependencies: + convert-source-map "^1.1.0" + through "~2.2.7" + +moment-timezone@^0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.7.tgz#1305bcada16f046dbbc7ac89abf66effff886cb5" + dependencies: + moment ">= 2.6.0" + +"moment@>= 2.6.0", moment@^2.11.2, moment@^2.8.2: + version "2.15.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.15.2.tgz#1bfdedf6a6e345f322fe956d5df5bd08a8ce84dc" + +ms@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.0.tgz#865be94c2e7397ad8a57da6a633a6e2f30798b83" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +mute-stream@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + +nan@^2.3.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +nave@~0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/nave/-/nave-0.5.3.tgz#5acec72375856e5c76c83bd21a68d713eb5f1ba4" + +no-case@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.0.tgz#ca2825ccb76b18e6f79d573dcfbf1eace33dd164" + dependencies: + lower-case "^1.1.1" + +node-emoji@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.1.tgz#fd918e412769bf8c448051238233840b2aff16a1" + dependencies: + string.prototype.codepointat "^0.2.0" + +node-fetch@^1.0.1: + version "1.6.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-notifier@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" + dependencies: + cli-usage "^0.1.1" + growly "^1.2.0" + lodash.clonedeep "^3.0.0" + minimist "^1.1.1" + semver "^5.1.0" + shellwords "^0.1.0" + which "^1.0.5" + +node-pre-gyp@^0.6.29: + version "0.6.32" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz#fc452b376e7319b3d255f5f34853ef6fd8fe1fd5" + dependencies: + mkdirp "~0.5.1" + nopt "~3.0.6" + npmlog "^4.0.1" + rc "~1.1.6" + request "^2.79.0" + rimraf "~2.5.4" + semver "~5.3.0" + tar "~2.2.1" + tar-pack "~3.3.0" + +node-uuid@~1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" + +nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.3.5" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +npmlog@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.1" + set-blocking "~2.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +numeral@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/numeral/-/numeral-1.5.3.tgz#a4c3eba68239580509f818267c77243bce43ff62" + +"nwmatcher@>= 1.3.9 < 2.0.0": + version "1.3.9" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.3.9.tgz#8bab486ff7fa3dfd086656bbe8b17116d3692d2a" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@4.x, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +object-assign@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" + +object-inspect@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-0.4.0.tgz#f5157c116c1455b243b06ee97703392c5ad89fec" + +object-keys@^1.0.6: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + +object-path@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.2.tgz#74bf3b3c5a7f2024d75e333f12021353fa9d485e" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +on-finished@^2.3.0, on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0, once@^1.3.2, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +once@~1.3.0, once@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + +onecolor@~2.4.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-2.4.2.tgz#a53ec3ff171c3446016dd5210d1a1b544bf7d874" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +opn@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a" + dependencies: + object-assign "^4.0.1" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-browserify@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.1.2.tgz#49ca0293e0b19590a5f5de10c7f265a617d8fe54" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + +os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +outpipe@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2" + dependencies: + shell-quote "^1.4.2" + +packageify@^0.2.0: + version "0.2.3" + resolved "https://registry.yarnpkg.com/packageify/-/packageify-0.2.3.tgz#73f8f970dc7970cb6c00fb65fdd48b17bf6510f3" + dependencies: + through "^2.3.4" + +pad-left@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994" + dependencies: + repeat-string "^1.5.4" + +pad-right@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" + dependencies: + repeat-string "^1.5.2" + +pako@~0.2.0, pako@~0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +pako@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.3.tgz#5f515b0c6722e1982920ae8005eacb0b7ca73ccf" + +param-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.0.tgz#2619f90fd6c829ed0b958f1c84ed03a745a6d70a" + dependencies: + no-case "^2.2.0" + +parents@^1.0.0, parents@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" + dependencies: + path-platform "~0.11.15" + +parse-asn1@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.0.0.tgz#35060f6d5015d37628c770f4e091a0b5a278bc23" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse-ms@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-1.0.1.tgz#56346d4749d78f23430ca0c713850aef91aa361d" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + +parse5@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" + +parsedbf@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/parsedbf/-/parsedbf-0.1.2.tgz#7f9aaeff89961df9bea0d60957856dbc92f8a8ce" + +parseurl@~1.3.0, parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +pascal-case@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.0.tgz#39c248bde5a8dc02d5160696bdb01e044d016ee1" + dependencies: + camel-case "^3.0.0" + upper-case-first "^1.1.0" + +password-sheriff@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/password-sheriff/-/password-sheriff-0.4.0.tgz#0d64abcb6c9c74ac5ec9b5c83657c9ae7e0e50e4" + dependencies: + underscore "^1.6.0" + +password-sheriff@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/password-sheriff/-/password-sheriff-1.0.1.tgz#fd63fbb44714258a26419f4800c3121e0dcb40b2" + dependencies: + underscore "^1.6.0" + +path-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.0.tgz#5ac491de642936e5dfe0e18d16c461b8be8cf073" + dependencies: + no-case "^2.2.0" + +path-exists@2.1.0, path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-platform@~0.11.15: + version "0.11.15" + resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.9" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693" + dependencies: + create-hmac "^1.1.2" + +pem@^1.8.3: + version "1.9.4" + resolved "https://registry.yarnpkg.com/pem/-/pem-1.9.4.tgz#63e89c49c17629610e978e87514e5cdbf498374f" + dependencies: + os-tmpdir "^1.0.1" + which "^1.2.4" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pixrem@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pixrem/-/pixrem-3.0.2.tgz#30d1bafb4c3bdce8e9bb4bd56a13985619320c34" + dependencies: + browserslist "^1.0.0" + postcss "^5.0.0" + reduce-css-calc "^1.2.7" + +pkg-config@^1.0.1, pkg-config@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pkg-config/-/pkg-config-1.1.1.tgz#557ef22d73da3c8837107766c52eadabde298fe4" + dependencies: + debug-log "^1.0.0" + find-root "^1.0.0" + xtend "^4.0.1" + +pleeease-filters@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pleeease-filters/-/pleeease-filters-3.0.0.tgz#35a4d4c2086413eabc2ce17aaa2ec29054e3075c" + dependencies: + onecolor "~2.4.0" + postcss "^5.0.4" + +plur@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/plur/-/plur-1.0.0.tgz#db85c6814f5e5e5a3b49efc28d604fec62975156" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +polyline@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/polyline/-/polyline-0.2.0.tgz#4f2b716ca81134a6cbaa488975d236ecb1cc2840" + +postcss-apply@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/postcss-apply/-/postcss-apply-0.3.0.tgz#a2f37c5bdfa881e4c15f4f245ec0cd96dd2e70d5" + dependencies: + balanced-match "^0.4.1" + postcss "^5.0.21" + +postcss-attribute-case-insensitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-1.0.1.tgz#ceb73777e106167eb233f1938c9bd9f2e697308d" + dependencies: + postcss "^5.1.1" + postcss-selector-parser "^2.2.0" + +postcss-calc@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-color-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-2.0.1.tgz#9ad226f550e8a7c7f8b8a77860545b6dd7f55241" + dependencies: + css-color-function "^1.2.0" + postcss "^5.0.4" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^3.3.0" + +postcss-color-gray@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-3.0.1.tgz#74432ede66dd83b1d1363565c68b376e18ff6770" + dependencies: + color "^0.11.3" + postcss "^5.0.4" + postcss-message-helpers "^2.0.0" + reduce-function-call "^1.0.1" + +postcss-color-hex-alpha@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-2.0.0.tgz#44fd6ecade66028648c881cb6504cdcbfdc6cd09" + dependencies: + color "^0.10.1" + postcss "^5.0.4" + postcss-message-helpers "^2.0.0" + +postcss-color-hsl@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-color-hsl/-/postcss-color-hsl-1.0.5.tgz#f53bb1c348310ce307ad89e3181a864738b5e687" + dependencies: + postcss "^5.2.0" + postcss-value-parser "^3.3.0" + units-css "^0.4.0" + +postcss-color-hwb@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-hwb/-/postcss-color-hwb-2.0.1.tgz#d63afaf9b70cb595f900a29c9fe57bf2a32fabec" + dependencies: + color "^0.11.4" + postcss "^5.0.4" + postcss-message-helpers "^2.0.0" + reduce-function-call "^1.0.1" + +postcss-color-rebeccapurple@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-2.0.1.tgz#74c6444e7cbb7d85613b5f7286df7a491608451c" + dependencies: + color "^0.11.4" + postcss "^5.0.4" + +postcss-color-rgb@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postcss-color-rgb/-/postcss-color-rgb-1.1.4.tgz#f29243e22e8e8c13434474092372d4ce605be8bc" + dependencies: + postcss "^5.2.0" + postcss-value-parser "^3.3.0" + +postcss-color-rgba-fallback@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-2.2.0.tgz#6d29491be5990a93173d47e7c76f5810b09402ba" + dependencies: + postcss "^5.0.0" + postcss-value-parser "^3.0.2" + rgb-hex "^1.0.0" + +postcss-cssnext@^2.6.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-2.9.0.tgz#064df2a8c21fd2ebb88825df372cf20fca882868" + dependencies: + autoprefixer "^6.0.2" + caniuse-api "^1.3.2" + chalk "^1.1.1" + pixrem "^3.0.0" + pleeease-filters "^3.0.0" + postcss "^5.0.4" + postcss-apply "^0.3.0" + postcss-attribute-case-insensitive "^1.0.1" + postcss-calc "^5.0.0" + postcss-color-function "^2.0.0" + postcss-color-gray "^3.0.0" + postcss-color-hex-alpha "^2.0.0" + postcss-color-hsl "^1.0.5" + postcss-color-hwb "^2.0.0" + postcss-color-rebeccapurple "^2.0.0" + postcss-color-rgb "^1.1.4" + postcss-color-rgba-fallback "^2.0.0" + postcss-custom-media "^5.0.0" + postcss-custom-properties "^5.0.0" + postcss-custom-selectors "^3.0.0" + postcss-font-variant "^2.0.0" + postcss-initial "^1.3.1" + postcss-media-minmax "^2.1.0" + postcss-nesting "^2.0.5" + postcss-pseudo-class-any-link "^1.0.0" + postcss-pseudoelements "^3.0.0" + postcss-replace-overflow-wrap "^1.0.0" + postcss-selector-matches "^2.0.0" + postcss-selector-not "^2.0.0" + +postcss-custom-media@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-5.0.1.tgz#138d25a184bf2eb54de12d55a6c01c30a9d8bd81" + dependencies: + postcss "^5.0.0" + +postcss-custom-properties@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-5.0.1.tgz#e07d4f6c78e547cf04274f120f490d236e33ea19" + dependencies: + balanced-match "~0.1.0" + postcss "^5.0.0" + +postcss-custom-selectors@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-3.0.0.tgz#8f81249f5ed07a8d0917cf6a39fe5b056b7f96ac" + dependencies: + balanced-match "^0.2.0" + postcss "^5.0.0" + postcss-selector-matches "^2.0.0" + +postcss-font-variant@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-2.0.1.tgz#7ca29103f59fa02ca3ace2ca22b2f756853d4ef8" + dependencies: + postcss "^5.0.4" + +postcss-import@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-9.1.0.tgz#95fe9876a1e79af49fbdc3589f01fe5aa7cc1e80" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + promise-each "^2.2.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^1.3.1: + version "1.5.3" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-1.5.3.tgz#20c3e91c96822ddb1bed49508db96d56bac377d0" + dependencies: + lodash.template "^4.2.4" + postcss "^5.0.19" + +postcss-media-minmax@^2.1.0: + version "2.1.2" + resolved "http://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-2.1.2.tgz#444c5cf8926ab5e4fd8a2509e9297e751649cdf8" + dependencies: + postcss "^5.0.4" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-nesting@^2.0.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-2.3.1.tgz#94a6b6a4ef707fbec20a87fee5c957759b4e01cf" + dependencies: + postcss "^5.0.19" + +postcss-pseudo-class-any-link@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-1.0.0.tgz#903239196401d335fe73ac756186fa62e693af26" + dependencies: + postcss "^5.0.3" + postcss-selector-parser "^1.1.4" + +postcss-pseudoelements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudoelements/-/postcss-pseudoelements-3.0.0.tgz#6c682177c7900ba053b6df17f8c590284c7b8bbc" + dependencies: + postcss "^5.0.4" + +postcss-replace-overflow-wrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-1.0.0.tgz#f0a03b31eab9636a6936bfd210e2aef1b434a643" + dependencies: + postcss "^5.0.16" + +postcss-reporter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-3.0.0.tgz#09ea0f37a444c5693878606e09b018ebeff7cf8f" + dependencies: + chalk "^1.0.0" + lodash "^4.1.0" + log-symbols "^1.0.2" + postcss "^5.0.0" + +postcss-safe-parser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-2.0.0.tgz#5a629fe1363225a3a2b4b1f657b59d3462455c6b" + dependencies: + postcss "^5.2.0" + +postcss-selector-matches@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-2.0.5.tgz#fa0f43be57b68e77aa4cd11807023492a131027f" + dependencies: + balanced-match "^0.4.2" + postcss "^5.0.0" + +postcss-selector-not@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-2.0.0.tgz#c73ad21a3f75234bee7fee269e154fd6a869798d" + dependencies: + balanced-match "^0.2.0" + postcss "^5.0.0" + +postcss-selector-parser@^1.1.4: + version "1.3.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-1.3.3.tgz#d2ee19df7a64f8ef21c1a71c86f7d4835c88c281" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.2.tgz#3d70f5adda130da51c7c0c2fc023f56b1374fe08" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-value-parser@^3.0.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss@^5.0.0, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.21, postcss@^5.0.3, postcss@^5.0.4, postcss@^5.1.1, postcss@^5.2.0, postcss@^5.2.11: + version "5.2.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.11.tgz#ff29bcd6d2efb98bfe08a022055ec599bbe7b761" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +prettier-bytes@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.3.tgz#932b31c23efddb36fc66a82dcef362af3122982f" + +pretty-format@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-18.1.0.tgz#fb65a86f7a7f9194963eee91865c1bcf1039e284" + dependencies: + ansi-styles "^2.2.1" + +pretty-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-2.1.0.tgz#4257c256df3fb0b451d6affaab021884126981dc" + dependencies: + is-finite "^1.0.1" + parse-ms "^1.0.0" + plur "^1.0.0" + +private@^0.1.6, private@~0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@~0.11.0: + version "0.11.9" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +proj4@^2.1.4: + version "2.3.15" + resolved "https://registry.yarnpkg.com/proj4/-/proj4-2.3.15.tgz#5ad06e8bca30be0ffa389a49e4565f51f06d089e" + dependencies: + mgrs "~0.0.2" + +promise-each@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/promise-each/-/promise-each-2.2.0.tgz#3353174eff2694481037e04e01f77aa0fb6d1b60" + dependencies: + any-promise "^0.1.0" + +promise@^7.0.3, promise@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" + dependencies: + asap "~2.0.3" + +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.3.2, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + +qs@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.0.tgz#a9f31142af468cb72b25b30136ba2456834916be" + +qs@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" + +qs@^6.2.1, qs@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + +"qs@git+https://github.com/jfromaniello/node-querystring.git#fix_ie7_bug_with_arrays": + version "0.6.6" + resolved "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8" + +qs@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.1.0.tgz#4d932e5c7ea411cca76a312d39a606200fd50cd9" + +query-string@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-3.0.3.tgz#ae2e14b4d05071d4e9b9eb4873c35b0dcd42e638" + dependencies: + strict-uri-encode "^1.0.0" + +query-string@^4.2.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.2.3.tgz#9f27273d207a25a8ee4c7b8c74dcd45d556db822" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +quickselect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.0.0.tgz#02630818f9aae4ecab26f0103f98d061c17c58f3" + +quote-stream@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2" + dependencies: + buffer-equal "0.0.1" + minimist "^1.1.3" + through2 "^2.0.0" + +quote-stream@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-0.0.0.tgz#cde29e94c409b16e19dc7098b89b6658f9721d3b" + dependencies: + minimist "0.0.8" + through2 "~0.4.1" + +randomatic@^1.1.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.5.tgz#5e9ef5f2d573c67bd2b8124ae90b5156e457840b" + dependencies: + is-number "^2.0.2" + kind-of "^3.0.2" + +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@~2.1.5, raw-body@~2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.13" + unpipe "1.0.0" + +rbush@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-2.0.1.tgz#4cfaca28c3064bc0ee75431a1b79990e875eefa9" + dependencies: + quickselect "^1.0.0" + +rc-align@2.x: + version "2.3.2" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.3.2.tgz#23bfc2a403fd21c567ef4dae3410ad6502e3fba0" + dependencies: + dom-align "1.x" + rc-util "3.x" + +rc-animate@2.x: + version "2.3.1" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.3.1.tgz#eacda46bf589d857f640c53953d92e07cb688636" + dependencies: + css-animation "^1.3.0" + +rc-slider@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-5.2.1.tgz#58e4526a5296cf40c2a1515fe8fda3736fb611af" + dependencies: + babel-runtime "6.x" + classnames "^2.2.5" + rc-tooltip "^3.4.2" + rc-util "^3.2.1" + warning "^3.0.0" + +rc-tooltip@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.4.2.tgz#c5c89c347ed790f7f290ef825d5e24e8fb599166" + dependencies: + rc-trigger "1.x" + +rc-trigger@1.x: + version "1.7.3" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-1.7.3.tgz#0f34afbc975b3ccd6c2bbb46a8b000749ed4cbf7" + dependencies: + babel-runtime "6.x" + rc-align "2.x" + rc-animate "2.x" + rc-util "^3.3.x" + +rc-util@3.x, rc-util@^3.2.1, rc-util@^3.3.x: + version "3.4.1" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-3.4.1.tgz#4b7e0b0c7593bdbcff8ed045d88fbbc773a7b061" + dependencies: + add-dom-event-listener "1.x" + classnames "2.x" + shallowequal "0.2.x" + +rc@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~1.0.4" + +"react-addons-css-transition-group@^15.0.0 || ^16.0.0": + version "15.3.2" + resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.3.2.tgz#d8fa52bec9bb61bdfde8b9e4652b80297cbff667" + +react-addons-perf@^15.3.2, react-addons-perf@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-addons-perf/-/react-addons-perf-15.4.1.tgz#c6dd5a7011f43cd3222f47b7cb1aebe9d4174cb0" + +"react-addons-shallow-compare@^0.14.0 || ^15.0.0", react-addons-shallow-compare@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.4.1.tgz#b68103dd4d13144cb221065f6021de1822bd435a" + +"react-addons-update@^0.14.0 || <15.4.0": + version "15.3.2" + resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.3.2.tgz#b6385c4db1e5df371825e0615b04360ed94430fe" + +react-addons-update@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.4.1.tgz#00c07f45243aa9715e1706bbfd1f23d3d8d80bd1" + +react-bootstrap-datetimepicker@^0.0.22: + version "0.0.22" + resolved "https://registry.yarnpkg.com/react-bootstrap-datetimepicker/-/react-bootstrap-datetimepicker-0.0.22.tgz#07e448d993157d049ad0876d0f9a3c9c5029d9c5" + dependencies: + babel-runtime "^5.6.18" + classnames "^2.1.2" + moment "^2.8.2" + +react-bootstrap-table@3.0.0-beta.7: + version "3.0.0-beta.7" + resolved "https://registry.yarnpkg.com/react-bootstrap-table/-/react-bootstrap-table-3.0.0-beta.7.tgz#1ba4ba9b75c9c281e6d0980b8a8141d8af17478d" + dependencies: + "@allenfang/react-toastr" "2.8.2" + classnames "^2.1.2" + react-modal "^1.4.0" + +react-bootstrap@^0.30.0-rc.2: + version "0.30.6" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.30.6.tgz#288662a245f9dbb79f7740ee595e4ec931d6a4a9" + dependencies: + babel-runtime "^6.11.6" + classnames "^2.2.5" + dom-helpers "^2.4.0" + invariant "^2.2.1" + keycode "^2.1.2" + react-overlays "^0.6.10" + react-prop-types "^0.4.0" + uncontrollable "^4.0.1" + warning "^3.0.0" + +react-color@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.4.0.tgz#58005b9093bb8b51b43bc6b91b4410c8c0a2d236" + dependencies: + babel-jest "^16.0.0" + lodash "^4.0.1" + material-colors "^1.0.0" + merge "^1.2.0" + react-addons-shallow-compare "^0.14.0 || ^15.0.0" + reactcss "^1.0.6" + tinycolor2 "^1.1.2" + +react-dnd-html5-backend@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.1.2.tgz#bcff5866629c335b310b1062fe6537af35073c66" + dependencies: + lodash "^4.2.0" + +react-dnd@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.1.4.tgz#dd2afeddddd5ff4507d795a5bd44361c84374c0f" + dependencies: + disposables "^1.0.1" + dnd-core "^2.0.1" + invariant "^2.1.0" + lodash "^4.2.0" + +"react-dom@^0.14.0 || <15.4.0": + version "15.3.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.2.tgz#c46b0aa5380d7b838e7a59c4a7beff2ed315531f" + +"react-dom@^15.0.0 || ^16.0.0", react-dom@^15.3.2, react-dom@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.1.tgz#d54c913261aaedb17adc20410d029dcc18a1344a" + dependencies: + fbjs "^0.8.1" + loose-envify "^1.1.0" + object-assign "^4.1.0" + +react-dropzone@^3.5.3: + version "3.7.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-3.7.2.tgz#e6e17e39eb0f2741beff200a4f9cdb796a9b0125" + dependencies: + attr-accept "^1.0.3" + +react-file-download@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/react-file-download/-/react-file-download-0.3.2.tgz#369c66b20dcf05670c521c10a778237d5c86d735" + dependencies: + react "^0.14.7" + +react-helmet@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-3.1.0.tgz#63486194682f33004826f3687dc49a138b557050" + dependencies: + deep-equal "1.0.1" + object-assign "^4.0.1" + react-side-effect "1.0.2" + shallowequal "0.2.2" + warning "2.1.0" + +react-input-autosize@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.1.0.tgz#3fe1ac832387d8abab85f6051ceab1c9e5570853" + +react-leaflet@^0.12.1: + version "0.12.3" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-0.12.3.tgz#db37372e2f9bca7c45cc6d8c2e720bee7788845b" + dependencies: + lodash "^4.0.0" + warning "^3.0.0" + +react-modal@^1.4.0: + version "1.6.5" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-1.6.5.tgz#f720d99bd81b1def5c2c32e0ffaa48bdaf484862" + dependencies: + element-class "^0.2.0" + exenv "1.2.0" + lodash.assign "^4.2.0" + +react-overlays@^0.6.10: + version "0.6.10" + resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.6.10.tgz#e7e52dad47f00a0fc784eb044428c3a9e874bfa3" + dependencies: + classnames "^2.2.5" + dom-helpers "^2.4.0" + react-prop-types "^0.4.0" + warning "^3.0.0" + +react-prop-types@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0" + dependencies: + warning "^3.0.0" + +react-pure-render@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-pure-render/-/react-pure-render-1.0.2.tgz#9d8a928c7f2c37513c2d064e57b3e3c356e9fabb" + +react-redux@^4.4.0: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-4.4.5.tgz#f509a2981be2252d10c629ef7c559347a4aec457" + dependencies: + hoist-non-react-statics "^1.0.3" + invariant "^2.0.0" + lodash "^4.2.0" + loose-envify "^1.1.0" + +react-router-bootstrap@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/react-router-bootstrap/-/react-router-bootstrap-0.20.1.tgz#fc2059660e268b022b05abc69db5281ca32b63b3" + +react-router-redux@^4.0.0, react-router-redux@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.6.tgz#10cf98dce911d7dd912a05bdb07fee4d3c563dee" + +react-router@^3.0.0, react-router@^3.0.0-alpha.1: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.0.0.tgz#3f313e4dbaf57048c48dd0a8c3cac24d93667dff" + dependencies: + history "^3.0.0" + hoist-non-react-statics "^1.2.0" + invariant "^2.2.1" + loose-envify "^1.2.0" + warning "^3.0.0" + +react-select@^1.0.0-beta14: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.0.0-rc.2.tgz#9fc11b149a3dc1ac831289d21b40a59742f82f8d" + dependencies: + classnames "^2.2.4" + react-input-autosize "^1.1.0" + +react-side-effect@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.0.2.tgz#98e354decdbf0281e4223d87852d33e345eda561" + dependencies: + fbjs "0.1.0-alpha.10" + +react-sidebar@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-sidebar/-/react-sidebar-2.2.1.tgz#a8faf6a3c62ddc562c70680d5d016fe9741b585f" + +react-toggle@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-3.0.0.tgz#4929b2a0903c67f3d3db0cb68ee42be88c36ffaa" + dependencies: + classnames "^2.2.5" + +react-virtualized-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-virtualized-select/-/react-virtualized-select-2.1.0.tgz#dbb05a700e198fcf0f99e59166065750f63c5685" + dependencies: + babel-runtime "^6.11.6" + react-select "^1.0.0-beta14" + react-virtualized "^8.0.5" + +react-virtualized@^8.0.5, react-virtualized@^8.5.0: + version "8.5.1" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-8.5.1.tgz#52e38f9382d46fecd6b016c6935eb2c5cf9b3e5f" + dependencies: + babel-runtime "^6.11.6" + classnames "^2.2.3" + dom-helpers "^2.4.0" + +"react@^0.14.0 || <15.4.0", react@^0.14.7: + version "0.14.8" + resolved "https://registry.yarnpkg.com/react/-/react-0.14.8.tgz#078dfa454d4745bcc54a9726311c2bf272c23684" + dependencies: + envify "^3.0.0" + fbjs "^0.6.1" + +"react@^15.0.0 || ^16.0.0", react@^15.3.2, react@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react/-/react-15.4.1.tgz#498e918602677a3983cd0fd206dfe700389a0dd6" + dependencies: + fbjs "^0.8.4" + loose-envify "^1.1.0" + object-assign "^4.1.0" + +reactcss@^1.0.4, reactcss@^1.0.6: + version "1.0.9" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.0.9.tgz#adb38e1d75640433ac99ef4933b8116d8a369fd4" + dependencies: + classnames "^2.2.0" + lodash "^4.0.1" + merge "^1.2.0" + object-assign "^4.1.0" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + dependencies: + pify "^2.3.0" + +read-only-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" + dependencies: + readable-stream "^2.0.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.27-1: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.1.0, readable-stream@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@^2.0.2, readable-stream@~2.0.0, readable-stream@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +recast@^0.10.0: + version "0.10.43" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f" + dependencies: + ast-types "0.8.15" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + source-map "~0.5.0" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redeyed@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" + dependencies: + esprima "~3.0.0" + +reduce-css-calc@^1.2.6, reduce-css-calc@^1.2.7: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +reduce-reducers@^0.1.0, reduce-reducers@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" + +redux-actions@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-0.13.0.tgz#34bf77c5698e86d86b0edb533ea23a1c0658f4fe" + dependencies: + lodash "^4.13.1" + reduce-reducers "^0.1.0" + +redux-logger@^2.6.1, redux-logger@^2.7.4: + version "2.7.4" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-2.7.4.tgz#891e5d29e7f111d08b5781a237b9965b5858c7f8" + dependencies: + deep-diff "0.3.4" + +redux-merge-reducers@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/redux-merge-reducers/-/redux-merge-reducers-0.0.2.tgz#2d83da42de18636bd526be183de19e9a3a0d647b" + +redux-thunk@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.1.0.tgz#c724bfee75dbe352da2e3ba9bc14302badd89a98" + +redux@^3.2.0, redux@^3.3.1, redux@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.2" + +regenerate@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" + +regenerator-runtime@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb" + +regenerator-transform@0.9.8: + version "0.9.8" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.8.tgz#0f88bb2bc03932ddb7b6b7312e68078f01026d6c" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remarkable@^1.6.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.1.tgz#aaca4972100b66a642a63a1021ca4bac1be3bff6" + dependencies: + argparse "~0.1.15" + autolinker "~0.15.0" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2, repeat-string@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@^2.55.0, request@^2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +request@^2.67.0: + version "2.76.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.76.0.tgz#be44505afef70360a0436955106be3945d95560e" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + node-uuid "~1.4.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +reqwest@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/reqwest/-/reqwest-2.0.5.tgz#00fb15ac4918c419ca82b43f24c78882e66039a1" + +reqwest@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/reqwest/-/reqwest-1.1.6.tgz#4b6894d29596bf8e824a25f34975df15562ee813" + +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve@1.1.7, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +resolve@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c" + +resp-modifier@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/resp-modifier/-/resp-modifier-6.0.2.tgz#b124de5c4fbafcba541f48ffa73970f4aa456b4f" + dependencies: + debug "^2.2.0" + minimatch "^3.0.2" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +rgb-hex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-1.0.0.tgz#bfaf8cd9cd9164b5a26d71eb4f15a0965324b3c1" + +rgb@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +right-now@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/right-now/-/right-now-1.0.0.tgz#6e89609deebd7dcdaf8daecc9aea39cf585a0918" + +right-pad@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + dependencies: + glob "^7.0.5" + +ripemd160@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +run-parallel@^1.1.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.6.tgz#29003c9a2163e01e2d2dfc90575f2c6c1d61a039" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + +sane@~1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sane/-/sane-1.4.1.tgz#88f763d74040f5f0c256b6163db399bf110ac715" + dependencies: + exec-sh "^0.2.0" + fb-watchman "^1.8.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + +sax@1.1.5, sax@>=0.6.0, sax@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.5.tgz#1da50a8d00cdecd59405659f5ff85349fe773743" + +selectn@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/selectn/-/selectn-0.10.0.tgz#8ad4fabdd6338677f519a28711792c45a3a14c39" + +selectn@^1.0.20, selectn@^1.0.5: + version "1.1.1" + resolved "https://registry.yarnpkg.com/selectn/-/selectn-1.1.1.tgz#94271b1cc10b82801a032f7276eb076f70c8b7f8" + dependencies: + brackets2dots "^1.1.0" + curry2 "^1.0.0" + debug "^2.2.0" + dotsplit.js "^1.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.5.1" + mime "1.3.4" + ms "0.7.2" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +sentence-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.0.tgz#d592fbed457fd1a59e3af0ee17e99f6fd70d7efd" + dependencies: + no-case "^2.2.0" + upper-case-first "^1.1.2" + +serve-static@^1.10.0: + version "1.11.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.14.2" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setprototypeof@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.1.tgz#52009b27888c4dc48f591949c0a8275834c1ca7e" + +setprototypeof@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" + +sha.js@^2.3.6, sha.js@~2.4.4: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" + +shallow-copy@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" + +shallowequal@0.2.2, shallowequal@0.2.x: + version "0.2.2" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e" + dependencies: + lodash.keys "^3.1.2" + +shasum@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" + dependencies: + json-stable-stringify "~0.0.0" + sha.js "~2.4.4" + +shell-quote@^1.4.2, shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +shelljs@0.7.5, shelljs@^0.7.0, shelljs@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.5.tgz#2eef7a50a21e1ccf37da00df767ec69e30ad0675" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.0.tgz#66afd47b6a12932d9071cbfd98a52e785cd0ba14" + +shp-write@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/shp-write/-/shp-write-0.3.2.tgz#cac3e84bcfdc953c4fc273a1b2f2843d6db8f69d" + dependencies: + dbf "0.1.4" + jszip "2.5.0" + +shpjs@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/shpjs/-/shpjs-3.3.2.tgz#f8ca857a4c70ee9526965a280d229a46775fb553" + dependencies: + jszip "^2.4.0" + lie "^3.0.1" + lru-cache "^2.7.0" + parsedbf "~0.1.2" + proj4 "^2.1.4" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +simple-html-index@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/simple-html-index/-/simple-html-index-1.5.0.tgz#2c93eeaebac001d8a135fc0022bd4ade8f58996f" + dependencies: + from2-string "^1.1.0" + +sizzle@^2.0.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sizzle/-/sizzle-2.3.3.tgz#4eb078c37231a56b52e4193f701e7ef8937e606b" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + dependencies: + no-case "^2.2.0" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +source-map-support@^0.4.2: + version "0.4.6" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.6.tgz#32552aa64b458392a85eab3b0b5ee61527167aeb" + dependencies: + source-map "^0.5.3" + +"source-map@>= 0.1.2", source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +source-map@^0.4.2, source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@~0.1.33: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + dependencies: + amdefine ">=0.0.4" + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + dependencies: + amdefine ">=0.0.4" + +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +split2@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/split2/-/split2-0.2.1.tgz#02ddac9adc03ec0bb78c1282ec079ca6e85ae900" + dependencies: + through2 "~0.6.1" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stacked@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stacked/-/stacked-1.1.1.tgz#2c7fa38cc7e37a3411a77cd8e792de448f9f6975" + +standard-engine@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/standard-engine/-/standard-engine-5.3.0.tgz#fa254d7e068d92de8019d9945d420286d1ce04c9" + dependencies: + deglob "^2.1.0" + find-root "^1.0.0" + get-stdin "^5.0.1" + home-or-tmp "^2.0.0" + minimist "^1.1.0" + pkg-config "^1.0.1" + +standard-engine@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/standard-engine/-/standard-engine-5.2.0.tgz#400660ae5acce8afd4db60ff2214a9190ad790a3" + dependencies: + deglob "^2.0.0" + find-root "^1.0.0" + get-stdin "^5.0.1" + home-or-tmp "^2.0.0" + minimist "^1.1.0" + pkg-config "^1.0.1" + +standard@^8.3.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/standard/-/standard-8.6.0.tgz#635132be7bfb567c2921005f30f9e350e4752aad" + dependencies: + eslint "~3.10.2" + eslint-config-standard "6.2.1" + eslint-config-standard-jsx "3.2.0" + eslint-plugin-promise "~3.4.0" + eslint-plugin-react "~6.7.1" + eslint-plugin-standard "~2.0.1" + standard-engine "~5.2.0" + +static-eval@~0.2.0: + version "0.2.4" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-0.2.4.tgz#b7d34d838937b969f9641ca07d48f8ede263ea7b" + dependencies: + escodegen "~0.0.24" + +static-module@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/static-module/-/static-module-1.3.1.tgz#79071d340e4419e4ab5ce87976a9eb67250c8493" + dependencies: + concat-stream "~1.4.5" + duplexer2 "~0.0.2" + escodegen "~1.3.2" + falafel "^1.0.0" + has "^1.0.0" + object-inspect "~0.4.0" + quote-stream "~0.0.0" + readable-stream "~1.0.27-1" + shallow-copy "~0.0.1" + static-eval "~0.2.0" + through2 "~0.4.1" + +statuses@1, "statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +"statuses@>= 1.3.0 < 2": + version "1.3.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.0.tgz#8e55758cb20e7682c1f4fce8dcab30bf01d1e07a" + +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-http@^2.0.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.1.0" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.2" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-to-js@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/string-to-js/-/string-to-js-0.0.1.tgz#bf153c760636faa30769b804a0195552ba7ad80f" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + +string.prototype.codepointat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" + +string_decoder@~0.10.0, string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220" + dependencies: + ansi-regex "^0.2.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +strip-json-comments@~1.0.1, strip-json-comments@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + +supports-color@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.3.1.tgz#15758df09d8ff3b4acc307539fabe27095e1042d" + +supports-color@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.2, supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +swap-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3" + dependencies: + lower-case "^1.1.1" + upper-case "^1.1.1" + +symbol-observable@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + +"symbol-tree@>= 3.1.0 < 4.0.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.1.tgz#8549dd1d01fa9f893c18cc9ab0b106b4d9b168cb" + +syntax-error@^1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.1.6.tgz#b4549706d386cc1c1dc7c2423f18579b6cade710" + dependencies: + acorn "^2.7.0" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tar-pack@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" + dependencies: + debug "~2.2.0" + fstream "~1.0.10" + fstream-ignore "~1.0.5" + once "~1.3.3" + readable-stream "~2.1.4" + rimraf "~2.5.1" + tar "~2.2.1" + uid-number "~0.0.6" + +tar@~2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +term-color@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/term-color/-/term-color-1.0.1.tgz#38e192553a473e35e41604ff5199846bf8117a3a" + dependencies: + ansi-styles "2.0.1" + supports-color "1.3.1" + +test-exclude@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-2.1.3.tgz#a8d8968e1da83266f9864f2852c55e220f06434a" + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + +test-exclude@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-3.3.0.tgz#7a17ca1239988c98367b0621456dbb7d4bc38977" + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +throat@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-3.0.0.tgz#e7c64c867cbb3845f10877642f7b60055b8ec0d6" + +through2@^2.0.0, through2@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" + dependencies: + readable-stream "~2.0.0" + xtend "~4.0.0" + +through2@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" + dependencies: + readable-stream "~1.0.17" + xtend "~2.1.1" + +through2@~0.6.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +"through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.7, through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +through@~2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/through/-/through-2.2.7.tgz#6e8e21200191d4eb6a99f6f010df46aa1c6eb2bd" + +timers-browserify@^1.0.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" + dependencies: + process "~0.11.0" + +tiny-lr@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-0.2.1.tgz#b3fdba802e5d56a33c2f6f10794b32e477ac729d" + dependencies: + body-parser "~1.14.0" + debug "~2.2.0" + faye-websocket "~0.10.0" + livereload-js "^2.2.0" + parseurl "~1.3.0" + qs "~5.1.0" + +tinycolor2@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + +title-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.0.tgz#c68ccb4232079ded64f94b91b4941ade91391979" + dependencies: + no-case "^2.2.0" + upper-case "^1.0.3" + +tmp@^0.0.29: + version "0.0.29" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" + dependencies: + os-tmpdir "~1.0.1" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" + +tough-cookie@^2.3.1, tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim@0.0.1, trim@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + +truncate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/truncate/-/truncate-2.0.0.tgz#09d5bc4163f3e257cd687351241071c24f14112f" + +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + +tty-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +turf-along@^3.0.10: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-along/-/turf-along-3.0.12.tgz#e622bde7a4bd138c09647d4b14aa0ea700485de6" + dependencies: + turf-bearing "^3.0.12" + turf-destination "^3.0.12" + turf-distance "^3.0.12" + turf-helpers "^3.0.12" + +turf-area@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-area/-/turf-area-3.0.12.tgz#9b7e469ef9fb558fd147bb0c214823263bdbf13c" + dependencies: + geojson-area "^0.2.1" + +turf-bbox-polygon@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-bbox-polygon/-/turf-bbox-polygon-3.0.12.tgz#330dc0bb38322d61545df966ce6c80f685acf4f2" + dependencies: + turf-helpers "^3.0.12" + +turf-bearing@^3.0.10, turf-bearing@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-bearing/-/turf-bearing-3.0.12.tgz#65f609dd850e7364c7771aa6ded87b0e1917fd20" + dependencies: + turf-invariant "^3.0.12" + +turf-destination@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-destination/-/turf-destination-3.0.12.tgz#7dd6fbf97e86f831a26c83ef2d5a2f8d1d8a6de2" + dependencies: + turf-helpers "^3.0.12" + turf-invariant "^3.0.12" + +turf-distance@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-distance/-/turf-distance-3.0.12.tgz#fb97b8705facd993b145e014b41862610eeca449" + dependencies: + turf-helpers "^3.0.12" + turf-invariant "^3.0.12" + +turf-extent@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/turf-extent/-/turf-extent-1.0.4.tgz#9f28ce11af916ada88862a062074658ba3fe0f01" + dependencies: + turf-meta "^1.0.2" + +turf-helpers@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-helpers/-/turf-helpers-3.0.12.tgz#dd4272e74b3ad7c96eecb7ae5c57fe8eca544b7b" + +turf-invariant@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-invariant/-/turf-invariant-3.0.12.tgz#3b95253953991ebd962dd35d4f6704c287de8ebe" + +turf-line-distance@^3.0.10: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-line-distance/-/turf-line-distance-3.0.12.tgz#7108f5b26907f7b8c2dd1b3997866dd3a60e8f5f" + dependencies: + turf-distance "^3.0.12" + turf-helpers "^3.0.12" + +turf-line-slice@^3.0.11: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-line-slice/-/turf-line-slice-3.0.12.tgz#f5f1accc92adae69ea1ac0b29f07529a28dde916" + dependencies: + turf-bearing "^3.0.12" + turf-destination "^3.0.12" + turf-distance "^3.0.12" + turf-helpers "^3.0.12" + turf-point-on-line "^3.0.12" + +turf-linestring@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/turf-linestring/-/turf-linestring-1.0.2.tgz#604a63a7c54c9346ab9f39f273bbb7ece05a3096" + +turf-meta@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/turf-meta/-/turf-meta-1.0.2.tgz#74d4c495938eb24772a70b31a157ee3d4fd6ba1b" + +turf-point-on-line@^3.0.11, turf-point-on-line@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/turf-point-on-line/-/turf-point-on-line-3.0.12.tgz#1d8663354e70372db1863e6253e9040c47127b0f" + dependencies: + turf-bearing "^3.0.12" + turf-destination "^3.0.12" + turf-distance "^3.0.12" + turf-helpers "^3.0.12" + +turf-point@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/turf-point/-/turf-point-2.0.1.tgz#a2dcc30a2d20f44cf5c6271df7bae2c0e2146069" + dependencies: + minimist "^1.1.0" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.3.tgz#3da382f670f25ded78d7b3d1792119bca0b7132d" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.10, type-is@~1.6.13: + version "1.6.13" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.13.tgz#6e83ba7bc30cd33a7bb0b7fb00737a2085bf9d08" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.11" + +typedarray@^0.0.6, typedarray@~0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +ua-parser-js@^0.7.9: + version "0.7.10" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.10.tgz#917559ddcce07cbc09ece7d80495e4c268f4ef9f" + +uglify-js@2.x.x, uglify-js@^2.6: + version "2.7.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8" + dependencies: + async "~0.2.6" + source-map "~0.5.1" + uglify-to-browserify "~1.0.0" + yargs "~3.10.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyify@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/uglifyify/-/uglifyify-3.0.4.tgz#487e080a5a7798880e68e90def9b06681fb13bd2" + dependencies: + convert-source-map "~1.1.0" + extend "^1.2.1" + minimatch "^3.0.2" + through "~2.3.4" + uglify-js "2.x.x" + +uid-number@~0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +umd@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e" + +uncontrollable@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-4.0.3.tgz#06ec76cb9e02914756085d9cea0354fc746b09b4" + dependencies: + invariant "^2.1.0" + +underscore.string@~2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.4.0.tgz#8cdd8fbac4e2d2ea1e7e2e8097c42f442280f85b" + +underscore@^1.6.0: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +underscore@~1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.5.2.tgz#1335c5e4f5e6d33bbb4b006ba8c86a00f556de08" + +underscore@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +units-css@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" + dependencies: + isnumeric "^0.2.0" + viewport-dimensions "^0.2.0" + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +upper-case-first@^1.1.0, upper-case-first@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" + dependencies: + upper-case "^1.1.1" + +upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + +url-trim@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-trim/-/url-trim-1.0.0.tgz#40057e2f164b88e5daca7269da47e6d1dd837adc" + +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +url@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@~0.10.1: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728" + +uuid@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +validator@^5.5.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-5.7.0.tgz#7a87a58146b695ac486071141c0c49d67da05e5c" + +vary@^1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +viewport-dimensions@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" + +vm-browserify@~0.0.1: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +warning@2.1.0, warning@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-2.1.0.tgz#21220d9c63afc77a8c92111e011af705ce0c6901" + dependencies: + loose-envify "^1.0.0" + +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + +watch@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" + +watchify-middleware@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchify-middleware/-/watchify-middleware-1.6.0.tgz#6db6e28f0279de1ca1209ae4f1a7f063745877c4" + dependencies: + concat-stream "^1.5.0" + debounce "^1.0.0" + events "^1.0.2" + object-assign "^4.0.1" + strip-ansi "^3.0.0" + watchify "^3.3.1" + +watchify@^3.3.1, watchify@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/watchify/-/watchify-3.9.0.tgz#f075fd2e8a86acde84cedba6e5c2a0bedd523d9e" + dependencies: + anymatch "^1.3.0" + browserify "^14.0.0" + chokidar "^1.0.0" + defined "^1.0.0" + outpipe "^1.1.0" + through2 "^2.0.0" + xtend "^4.0.0" + +webidl-conversions@^3.0.0, webidl-conversions@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + +websocket-driver@>=0.5.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + dependencies: + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" + +wgs84@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/wgs84/-/wgs84-0.0.0.tgz#34fdc555917b6e57cf2a282ed043710c049cdc76" + +whatwg-encoding@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" + dependencies: + iconv-lite "0.4.13" + +whatwg-fetch@>=0.10.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.0.0.tgz#01c2ac4df40e236aaa18480e3be74bd5c8eb798e" + +whatwg-fetch@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" + +whatwg-url@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.3.0.tgz#92aaee21f4f2a642074357d70ef8500a7cbb171a" + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which@^1.0.5, which@^1.1.1, which@^1.2.12, which@^1.2.4: + version "1.2.12" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" + dependencies: + isexe "^1.1.1" + +wide-align@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" + dependencies: + string-width "^1.0.1" + +winchan@0.1.4, winchan@^0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.1.4.tgz#88fa12411cd542eb626018c38a196bcbb17993bb" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +word-wrap@^1.0.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.1.tgz#248f459b465d179a17bc407c854d3151d07e45d8" + +wordwrap@0.0.2, wordwrap@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +worker-farm@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.3.1.tgz#4333112bb49b17aa050b87895ca6b2cacf40e5ff" + dependencies: + errno ">=0.1.1 <0.2.0-0" + xtend ">=4.0.0 <4.1.0-0" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +"xml-name-validator@>= 2.0.1 < 3.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" + +xml2js@0.4.15: + version "0.4.15" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.15.tgz#95cd03ff2dd144ec28bc6273bf2b2890c581ad0c" + dependencies: + sax ">=0.6.0" + xmlbuilder ">=2.4.6" + +xmlbuilder@2.6.2, xmlbuilder@>=2.4.6: + version "2.6.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-2.6.2.tgz#f916f6d10d45dc171b1be2e6e673fb6e0cc35d0a" + dependencies: + lodash "~3.5.0" + +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + dependencies: + object-keys "~0.4.0" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yamljs@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.2.8.tgz#ef23fb006e62f6ae07b406aa2a949561f336ea5c" + dependencies: + argparse "^1.0.7" + glob "^7.0.5" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs@^6.3.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0"