aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuc Donnet2018-04-10 17:30:55 +0200
committerGitHub2018-04-10 17:30:55 +0200
commit537a5078657ead0b89aa5220c05dfbc01ae94dca (patch)
tree54072303cdbab71e6005e7fc5c2af6db6a4f9d0b
parentdad504f5794a36be8dac97a257cdecd87704763b (diff)
parent22e7844c1d2392e2a651a33bf83c32664a879619 (diff)
downloadchouette-core-537a5078657ead0b89aa5220c05dfbc01ae94dca.tar.bz2
Merge pull request #438 from af83/6368-gtfs-import
GTFS import (first step)
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock9
-rw-r--r--app/models/chouette/company.rb2
-rw-r--r--app/models/chouette/line.rb2
-rw-r--r--app/models/chouette/stop_area.rb2
-rw-r--r--app/models/concerns/application_days_support.rb8
-rw-r--r--app/models/import/gtfs.rb302
-rw-r--r--app/models/import/workbench.rb21
-rw-r--r--app/models/referential.rb8
-rw-r--r--app/workers/gtfs_import_worker.rb7
-rw-r--r--db/migrate/20180403100007_add_registration_number_indexes.rb7
-rw-r--r--lib/gtfs/time.rb28
-rw-r--r--spec/fixtures/google-sample-feed.zipbin0 -> 3217 bytes
-rw-r--r--spec/lib/gtfs/time_spec.rb27
-rw-r--r--spec/models/import/gtfs_spec.rb276
15 files changed, 673 insertions, 28 deletions
diff --git a/Gemfile b/Gemfile
index 2da189f2b..cca55be61 100644
--- a/Gemfile
+++ b/Gemfile
@@ -143,6 +143,8 @@ gem 'puma', '~> 3.10.0'
gem 'newrelic_rpm'
gem 'letter_opener'
+gem 'gtfs'
+
group :development do
gem 'capistrano', '2.13.5'
gem 'capistrano-ext'
diff --git a/Gemfile.lock b/Gemfile.lock
index 0c1243593..4fb77eeb9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -258,6 +258,10 @@ GEM
google-analytics-rails (1.1.0)
gretel (3.0.9)
rails (>= 3.1.0)
+ gtfs (0.2.5)
+ multi_json
+ rake
+ rubyzip (~> 1.1)
has_scope (0.7.0)
actionpack (>= 4.1, < 5.1)
activesupport (>= 4.1, < 5.1)
@@ -321,7 +325,7 @@ GEM
minitest (5.11.3)
money (6.10.1)
i18n (>= 0.6.4, < 1.0)
- multi_json (1.12.1)
+ multi_json (1.13.1)
multi_test (0.1.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
@@ -420,7 +424,7 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
- rake (12.3.0)
+ rake (12.3.1)
ransack (1.8.3)
actionpack (>= 3.0)
activerecord (>= 3.0)
@@ -633,6 +637,7 @@ DEPENDENCIES
georuby-ext (= 0.0.5)
google-analytics-rails
gretel
+ gtfs
has_array_of!
htmlbeautifier
i18n-js
diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb
index cb2266a3d..9d5737a6c 100644
--- a/app/models/chouette/company.rb
+++ b/app/models/chouette/company.rb
@@ -9,7 +9,7 @@ module Chouette
has_many :lines
- validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true
+ # validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true
validates_presence_of :name
validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true
diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb
index 51851fc2e..4b5d1a68d 100644
--- a/app/models/chouette/line.rb
+++ b/app/models/chouette/line.rb
@@ -29,7 +29,7 @@ module Chouette
# validates_presence_of :network
# validates_presence_of :company
- validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true
+ # validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true
validates_format_of :stable_id, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true
validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true
validates_format_of :color, :with => %r{\A[0-9a-fA-F]{6}\Z}, :allow_nil => true, :allow_blank => true
diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb
index 9f28b7ee6..4ddc7403b 100644
--- a/app/models/chouette/stop_area.rb
+++ b/app/models/chouette/stop_area.rb
@@ -33,7 +33,7 @@ module Chouette
after_update :journey_patterns_control_route_sections,
if: Proc.new { |stop_area| ['boarding_position', 'quay'].include? stop_area.stop_area_type }
- validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_blank => true
+ # validates_format_of :registration_number, :with => %r{\A[\d\w_:\-]+\Z}, :allow_blank => true
validates_presence_of :name
validates_presence_of :kind
validates_presence_of :latitude, :if => :longitude
diff --git a/app/models/concerns/application_days_support.rb b/app/models/concerns/application_days_support.rb
index 2d00b5847..6086d9580 100644
--- a/app/models/concerns/application_days_support.rb
+++ b/app/models/concerns/application_days_support.rb
@@ -10,8 +10,10 @@ module ApplicationDaysSupport
SUNDAY = 256
EVERYDAY = MONDAY | TUESDAY | WEDNESDAY | THURSDAY | FRIDAY | SATURDAY | SUNDAY
+ ALL_DAYS = %w(monday tuesday wednesday thursday friday saturday sunday).freeze
+
def display_day_types
- %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ')
+ ALL_DAYS.select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ')
end
def day_by_mask(flag)
@@ -39,6 +41,10 @@ module ApplicationDaysSupport
def self.day_by_mask(int_day_types,flag)
int_day_types & flag == flag
end
+
+ def self.all_days
+ ALL_DAYS
+ end
end
def valid_days
diff --git a/app/models/import/gtfs.rb b/app/models/import/gtfs.rb
index 03cf49e60..70f448132 100644
--- a/app/models/import/gtfs.rb
+++ b/app/models/import/gtfs.rb
@@ -1,36 +1,296 @@
-require 'net/http'
class Import::Gtfs < Import::Base
- before_destroy :destroy_non_ready_referential
+ after_commit :launch_worker, :on => :create
- after_commit :launch_java_import, on: :create
- before_save def abort_unless_referential
- self.status = 'aborted' unless referential
+ def launch_worker
+ GtfsImportWorker.perform_async id
end
- def launch_java_import
- return if self.class.finished_statuses.include?(status)
- threaded_call_boiv_iev
+ def import
+ update status: 'running', started_at: Time.now
+
+ import_without_status
+ update status: 'successful', ended_at: Time.now
+ rescue Exception => e
+ update status: 'failed', ended_at: Time.now
+ Rails.logger.error "Error in GTFS import: #{e} #{e.backtrace.join('\n')}"
+ ensure
+ notify_parent
+ referential&.update ready: true
+ end
+
+ def self.accept_file?(file)
+ Zip::File.open(file) do |zip_file|
+ zip_file.glob('agency.txt').size == 1
+ end
+ rescue Exception => e
+ Rails.logger.debug "Error in testing GTFS file: #{e}"
+ return false
+ end
+
+ def create_referential
+ self.referential ||= Referential.create!(
+ name: "GTFS Import",
+ organisation_id: workbench.organisation_id,
+ workbench_id: workbench.id,
+ metadatas: [referential_metadata]
+ )
+ end
+
+ def referential_metadata
+ registration_numbers = source.routes.map(&:id)
+ line_ids = line_referential.lines.where(registration_number: registration_numbers).pluck(:id)
+
+ start_dates, end_dates = source.calendars.map { |c| [c.start_date, c.end_date ] }.transpose
+ excluded_dates = source.calendar_dates.select { |d| d.exception_type == "2" }.map(&:date)
+
+ min_date = Date.parse (start_dates + [excluded_dates.min]).compact.min
+ max_date = Date.parse (end_dates + [excluded_dates.max]).compact.max
+
+ ReferentialMetadata.new line_ids: line_ids, periodes: [min_date..max_date]
+ end
+
+ attr_accessor :local_file
+ def local_file
+ @local_file ||= download_local_file
end
- private
+ attr_accessor :download_host
+ def download_host
+ @download_host ||= Rails.application.config.rails_host.gsub("http://","")
+ end
- def destroy_non_ready_referential
- if referential && !referential.ready
- referential.destroy
+ def local_temp_directory
+ Rails.application.config.try(:import_temporary_directory) ||
+ Rails.root.join('tmp', 'imports')
+ end
+
+ def local_temp_file(&block)
+ Tempfile.open("chouette-import", local_temp_directory) do |file|
+ file.binmode
+ yield file
end
end
- def threaded_call_boiv_iev
- Thread.new(&method(:call_boiv_iev))
+ def download_path
+ Rails.application.routes.url_helpers.download_workbench_import_path(workbench, id, token: token_download)
end
- def call_boiv_iev
- Rails.logger.error("Begin IEV call for import")
- Net::HTTP.get(URI("#{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}"))
- Rails.logger.error("End IEV call for import")
- rescue Exception => e
- logger.error "IEV server error : #{e.message}"
- logger.error e.backtrace.inspect
+ def download_local_file
+ local_temp_file do |file|
+ begin
+ Net::HTTP.start(download_host) do |http|
+ http.request_get(download_path) do |response|
+ response.read_body do |segment|
+ file.write segment
+ end
+ end
+ end
+ ensure
+ file.close
+ end
+
+ file.path
+ end
+ end
+
+ def source
+ @source ||= ::GTFS::Source.build local_file
+ end
+
+ delegate :line_referential, :stop_area_referential, to: :workbench
+
+ def prepare_referential
+ import_agencies
+ import_stops
+ import_routes
+
+ create_referential
+ referential.switch
+ end
+
+ def import_without_status
+ prepare_referential
+
+ import_calendars
+ import_trips
+ import_stop_times
+ end
+
+ def import_agencies
+ Chouette::Company.transaction do
+ source.agencies.each do |agency|
+ company = line_referential.companies.find_or_initialize_by(registration_number: agency.id)
+ company.attributes = { name: agency.name }
+
+ save_model company
+ end
+ end
+ end
+
+ def import_stops
+ Chouette::StopArea.transaction do
+ source.stops.each do |stop|
+ stop_area = stop_area_referential.stop_areas.find_or_initialize_by(registration_number: stop.id)
+
+ stop_area.name = stop.name
+ stop_area.area_type = stop.location_type == "1" ? "zdlp" : "zdep"
+ stop_area.parent = stop_area_referential.stop_areas.find_by!(registration_number: stop.parent_station) if stop.parent_station.present?
+ stop_area.latitude, stop_area.longitude = stop.lat, stop.lon
+ stop_area.kind = "commercial"
+
+ # TODO correct default timezone
+
+ save_model stop_area
+ end
+ end
+ end
+
+ def import_routes
+ Chouette::Line.transaction do
+ source.routes.each do |route|
+ line = line_referential.lines.find_or_initialize_by(registration_number: route.id)
+ line.name = route.long_name.presence || route.short_name
+ line.number = route.short_name
+ line.published_name = route.long_name
+
+ line.company = line_referential.companies.find_by(registration_number: route.agency_id) if route.agency_id.present?
+
+ # TODO transport mode
+
+ line.comment = route.desc
+
+ # TODO colors
+
+ line.url = route.url
+
+ save_model line
+ end
+ end
+ end
+
+ def vehicle_journey_by_trip_id
+ @vehicle_journey_by_trip_id ||= {}
+ end
+
+ def import_trips
+ source.trips.each_slice(100) do |slice|
+ slice.each do |trip|
+ Chouette::Route.transaction do
+ line = line_referential.lines.find_by registration_number: trip.route_id
+
+ route = referential.routes.build line: line
+ route.wayback = (trip.direction_id == "0" ? :outbound : :inbound)
+ # TODO better name ?
+ name = route.published_name = trip.short_name.presence || trip.headsign.presence || route.wayback.to_s.capitalize
+ route.name = name
+ save_model route
+
+ journey_pattern = route.journey_patterns.build name: name
+ save_model journey_pattern
+
+ vehicle_journey = journey_pattern.vehicle_journeys.build route: route
+ vehicle_journey.published_journey_name = trip.headsign.presence || trip.id
+ save_model vehicle_journey
+
+ time_table = referential.time_tables.find_by(id: time_tables_by_service_id[trip.service_id]) if time_tables_by_service_id[trip.service_id]
+ if time_table
+ vehicle_journey.time_tables << time_table
+ else
+ messages.create! criticity: "warning", message_key: "gtfs.trips.unkown_service_id", message_attributes: {service_id: trip.service_id}
+ end
+
+ vehicle_journey_by_trip_id[trip.id] = vehicle_journey.id
+ end
+ end
+ end
+ end
+
+ def import_stop_times
+ source.stop_times.group_by(&:trip_id).each_slice(50) do |slice|
+ slice.each do |trip_id, stop_times|
+ Chouette::VehicleJourneyAtStop.transaction do
+ vehicle_journey = referential.vehicle_journeys.find vehicle_journey_by_trip_id[trip_id]
+ journey_pattern = vehicle_journey.journey_pattern
+ route = journey_pattern.route
+
+ stop_times.sort_by! { |s| s.stop_sequence.to_i }
+
+ stop_times.each do |stop_time|
+ stop_area = stop_area_referential.stop_areas.find_by(registration_number: stop_time.stop_id)
+
+ stop_point = route.stop_points.build stop_area: stop_area
+ save_model stop_point
+
+ journey_pattern.stop_points << stop_point
+
+ # JourneyPattern#vjas_add creates automaticaly VehicleJourneyAtStop
+ vehicle_journey_at_stop = journey_pattern.vehicle_journey_at_stops.find_by(stop_point_id: stop_point.id)
+
+ departure_time = GTFS::Time.parse(stop_time.departure_time)
+ arrival_time = GTFS::Time.parse(stop_time.arrival_time)
+
+ vehicle_journey_at_stop.departure_time = departure_time.time
+ vehicle_journey_at_stop.arrival_time = arrival_time.time
+ vehicle_journey_at_stop.departure_day_offset = departure_time.day_offset
+ vehicle_journey_at_stop.arrival_day_offset = arrival_time.day_offset
+
+ # TODO offset
+
+ save_model vehicle_journey_at_stop
+ end
+ end
+ end
+ end
+ end
+
+ def time_tables_by_service_id
+ @time_tables_by_service_id ||= {}
+ end
+
+ def import_calendars
+ source.calendars.each_slice(500) do |slice|
+ Chouette::TimeTable.transaction do
+ slice.each do |calendar|
+ time_table = referential.time_tables.build comment: "Calendar #{calendar.service_id}"
+ Chouette::TimeTable.all_days.each do |day|
+ time_table.send("#{day}=", calendar.send(day))
+ end
+ time_table.periods.build period_start: calendar.start_date, period_end: calendar.end_date
+
+ save_model time_table
+
+ time_tables_by_service_id[calendar.service_id] = time_table.id
+ end
+ end
+ end
+ end
+
+ def import_calendar_dates
+ source.calendar_dates.each_slice(500) do |slice|
+ Chouette::TimeTable.transaction do
+ slice.each do |calendar_date|
+ time_table = referential.time_tables.find time_tables_by_service_id[calendar_date.service_id]
+ date = time_table.dates.build date: Date.parse(calendar_date.date), in_out: calendar_date.exception_type == "1"
+
+ save_model date
+ end
+ end
+ end
+ end
+
+ def save_model(model)
+ unless model.save
+ Rails.logger.info "Can't save #{model.class.name} : #{model.errors.inspect}"
+ raise ActiveRecord::RecordNotSaved.new("Invalid #{model.class.name} : #{model.errors.inspect}")
+ end
+ Rails.logger.debug "Created #{model.inspect}"
+ end
+
+ def notify_parent
+ return unless parent.present?
+ return if notified_parent_at
+ parent.child_change
+ update_column :notified_parent_at, Time.now
end
end
diff --git a/app/models/import/workbench.rb b/app/models/import/workbench.rb
index f6e15cb89..124b9b0d8 100644
--- a/app/models/import/workbench.rb
+++ b/app/models/import/workbench.rb
@@ -2,6 +2,25 @@ class Import::Workbench < Import::Base
after_commit :launch_worker, :on => :create
def launch_worker
- WorkbenchImportWorker.perform_async(id)
+ unless Import::Gtfs.accept_file?(file.path)
+ WorkbenchImportWorker.perform_async(id)
+ else
+ import_gtfs
+ end
+ end
+
+ def import_gtfs
+ update_column :status, 'running'
+ update_column :started_at, Time.now
+
+ Import::Gtfs.create! parent_id: self.id, workbench: workbench, file: File.new(file.path), name: "Import GTFS", creator: "Web service"
+
+ update_column :status, 'successful'
+ update_column :ended_at, Time.now
+ rescue Exception => e
+ Rails.logger.error "Error while processing GTFS file: #{e}"
+
+ update_column :status, 'failed'
+ update_column :ended_at, Time.now
end
end
diff --git a/app/models/referential.rb b/app/models/referential.rb
index 3304108d0..1794126a2 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -168,6 +168,10 @@ class Referential < ApplicationModel
Chouette::TimeTable.all
end
+ def time_table_dates
+ Chouette::TimeTableDate.all
+ end
+
def timebands
Chouette::Timeband.all
end
@@ -184,6 +188,10 @@ class Referential < ApplicationModel
Chouette::VehicleJourneyFrequency.all
end
+ def vehicle_journey_at_stops
+ Chouette::VehicleJourneyAtStop.all
+ end
+
def routing_constraint_zones
Chouette::RoutingConstraintZone.all
end
diff --git a/app/workers/gtfs_import_worker.rb b/app/workers/gtfs_import_worker.rb
new file mode 100644
index 000000000..02f5053b0
--- /dev/null
+++ b/app/workers/gtfs_import_worker.rb
@@ -0,0 +1,7 @@
+class GtfsImportWorker
+ include Sidekiq::Worker
+
+ def perform(import_id)
+ Import::Gtfs.find(import_id).import
+ end
+end
diff --git a/db/migrate/20180403100007_add_registration_number_indexes.rb b/db/migrate/20180403100007_add_registration_number_indexes.rb
new file mode 100644
index 000000000..bc2d48329
--- /dev/null
+++ b/db/migrate/20180403100007_add_registration_number_indexes.rb
@@ -0,0 +1,7 @@
+class AddRegistrationNumberIndexes < ActiveRecord::Migration
+ def change
+ add_index :stop_areas, [:stop_area_referential_id, :registration_number], name: 'index_stop_areas_on_referential_id_and_registration_number'
+ add_index :lines, [:line_referential_id, :registration_number], name: 'index_lines_on_referential_id_and_registration_number'
+ add_index :companies, [:line_referential_id, :registration_number], name: 'index_companies_on_referential_id_and_registration_number'
+ end
+end
diff --git a/lib/gtfs/time.rb b/lib/gtfs/time.rb
new file mode 100644
index 000000000..49546532a
--- /dev/null
+++ b/lib/gtfs/time.rb
@@ -0,0 +1,28 @@
+module GTFS
+ class Time
+ attr_reader :hours, :minutes, :seconds
+ def initialize(hours, minutes, seconds)
+ @hours, @minutes, @seconds = hours, minutes, seconds
+ end
+
+ def real_hours
+ hours.modulo(24)
+ end
+
+ def time
+ @time ||= ::Time.new(2000, 1, 1, real_hours, minutes, seconds, "+00:00")
+ end
+
+ def day_offset
+ hours / 24
+ end
+
+ FORMAT = /(\d{1,2}):(\d{2}):(\d{2})/
+
+ def self.parse(definition)
+ if definition.to_s =~ FORMAT
+ new *[$1, $2, $3].map(&:to_i)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/google-sample-feed.zip b/spec/fixtures/google-sample-feed.zip
new file mode 100644
index 000000000..79819e21a
--- /dev/null
+++ b/spec/fixtures/google-sample-feed.zip
Binary files differ
diff --git a/spec/lib/gtfs/time_spec.rb b/spec/lib/gtfs/time_spec.rb
new file mode 100644
index 000000000..540d7cc79
--- /dev/null
+++ b/spec/lib/gtfs/time_spec.rb
@@ -0,0 +1,27 @@
+require "rails_helper"
+
+RSpec.describe GTFS::Time do
+
+ it "returns an UTC Time with given H:M:S" do
+ expect(GTFS::Time.parse("14:29:00").time).to eq(Time.parse("2000-01-01 14:29:00 +00"))
+ end
+
+ it "support hours with a single number" do
+ expect(GTFS::Time.parse("4:29:00").time).to eq(Time.parse("2000-01-01 04:29:00 +00"))
+ end
+
+ it "return nil for invalid format" do
+ expect(GTFS::Time.parse("abc")).to be_nil
+ end
+
+ it "removes 24 hours after 23:59:59" do
+ expect(GTFS::Time.parse("25:29:00").time).to eq(Time.parse("2000-01-01 01:29:00 +00"))
+ end
+
+ it "returns a day_offset for each 24 hours turn" do
+ expect(GTFS::Time.parse("10:00:00").day_offset).to eq(0)
+ expect(GTFS::Time.parse("30:00:00").day_offset).to eq(1)
+ expect(GTFS::Time.parse("50:00:00").day_offset).to eq(2)
+ end
+
+end
diff --git a/spec/models/import/gtfs_spec.rb b/spec/models/import/gtfs_spec.rb
new file mode 100644
index 000000000..66607c27f
--- /dev/null
+++ b/spec/models/import/gtfs_spec.rb
@@ -0,0 +1,276 @@
+require "rails_helper"
+
+RSpec.describe Import::Gtfs do
+
+ let(:referential) do
+ create :referential do |referential|
+ referential.line_referential.objectid_format = "netex"
+ referential.stop_area_referential.objectid_format = "netex"
+ end
+ end
+
+ let(:workbench) do
+ create :workbench do |workbench|
+ workbench.line_referential.objectid_format = "netex"
+ workbench.stop_area_referential.objectid_format = "netex"
+ end
+ end
+
+ def create_import(file)
+ Import::Gtfs.new workbench: workbench, local_file: fixtures_path(file)
+ end
+
+ describe "#import_agencies" do
+ let(:import) { create_import "google-sample-feed.zip" }
+ it "should create a company for each agency" do
+ import.import_agencies
+
+ expect(workbench.line_referential.companies.pluck(:registration_number, :name)).to eq([["DTA","Demo Transit Authority"]])
+ end
+ end
+
+ describe "#import_stops" do
+ let(:import) { create_import "google-sample-feed.zip" }
+ it "should create a company for each agency" do
+ import.import_stops
+
+ defined_attributes = [
+ :registration_number, :name, :parent_id, :latitude, :longitude
+ ]
+ expected_attributes = [
+ ["AMV", "Amargosa Valley (Demo)", nil, 36.641496, -116.40094],
+ ["EMSI", "E Main St / S Irving St (Demo)", nil, 36.905697, -116.76218],
+ ["DADAN", "Doing Ave / D Ave N (Demo)", nil, 36.909489, -116.768242],
+ ["NANAA", "North Ave / N A Ave (Demo)", nil, 36.914944, -116.761472],
+ ["NADAV", "North Ave / D Ave N (Demo)", nil, 36.914893, -116.76821],
+ ["STAGECOACH", "Stagecoach Hotel & Casino (Demo)", nil, 36.915682, -116.751677],
+ ["BULLFROG", "Bullfrog (Demo)", nil, 36.88108, -116.81797],
+ ["BEATTY_AIRPORT", "Nye County Airport (Demo)", nil, 36.868446, -116.784582],
+ ["FUR_CREEK_RES", "Furnace Creek Resort (Demo)", nil, 36.425288, -117.133162]
+ ]
+
+ expect(workbench.stop_area_referential.stop_areas.pluck(*defined_attributes)).to match_array(expected_attributes)
+ end
+ end
+
+ describe "#import_routes" do
+ let(:import) { create_import "google-sample-feed.zip" }
+ it "should create a line for each route" do
+ import.import_routes
+
+ defined_attributes = [
+ :registration_number, :name, :number, :published_name,
+ "companies.registration_number",
+ :comment, :url
+ ]
+ expected_attributes = [
+ ["AAMV", "Airport - Amargosa Valley", "50", "Airport - Amargosa Valley", nil, nil, nil],
+ ["CITY", "City", "40", "City", nil, nil, nil],
+ ["STBA", "Stagecoach - Airport Shuttle", "30", "Stagecoach - Airport Shuttle", nil, nil, nil],
+ ["BFC", "Bullfrog - Furnace Creek Resort", "20", "Bullfrog - Furnace Creek Resort", nil, nil, nil],
+ ["AB", "Airport - Bullfrog", "10", "Airport - Bullfrog", nil, nil, nil]
+ ]
+
+ expect(workbench.line_referential.lines.includes(:company).pluck(*defined_attributes)).to match_array(expected_attributes)
+ end
+ end
+
+ describe "#import_trips" do
+ let(:import) { create_import "google-sample-feed.zip" }
+ before do
+ import.prepare_referential
+ import.import_calendars
+ end
+
+ it "should create a Route for each trip" do
+ import.import_trips
+
+ defined_attributes = [
+ "lines.registration_number", :wayback, :name, :published_name
+ ]
+ expected_attributes = [
+ ["AB", "outbound", "to Bullfrog", "to Bullfrog"],
+ ["AB", "inbound", "to Airport", "to Airport"],
+ ["STBA", "inbound", "Shuttle", "Shuttle"],
+ ["CITY", "outbound", "Outbound", "Outbound"],
+ ["CITY", "inbound", "Inbound", "Inbound"],
+ ["BFC", "outbound", "to Furnace Creek Resort", "to Furnace Creek Resort"],
+ ["BFC", "inbound", "to Bullfrog", "to Bullfrog"],
+ ["AAMV", "outbound", "to Amargosa Valley", "to Amargosa Valley"],
+ ["AAMV", "inbound", "to Airport", "to Airport"],
+ ["AAMV", "outbound", "to Amargosa Valley", "to Amargosa Valley"],
+ ["AAMV", "inbound", "to Airport", "to Airport"]
+ ]
+
+ expect(import.referential.routes.includes(:line).pluck(*defined_attributes)).to match_array(expected_attributes)
+ end
+
+ it "should create a JourneyPattern for each trip" do
+ import.import_trips
+
+ defined_attributes = [
+ :name
+ ]
+ expected_attributes = [
+ "to Bullfrog", "to Airport", "Shuttle", "Outbound", "Inbound", "to Furnace Creek Resort", "to Bullfrog", "to Amargosa Valley", "to Airport", "to Amargosa Valley", "to Airport"
+ ]
+
+ expect(import.referential.journey_patterns.pluck(*defined_attributes)).to match_array(expected_attributes)
+ end
+
+ it "should create a VehicleJourney for each trip" do
+ import.import_trips
+
+ defined_attributes = ->(v) {
+ [v.published_journey_name, v.time_tables.first&.comment]
+ }
+ expected_attributes = [
+ ["to Bullfrog", "Calendar FULLW"],
+ ["to Airport", "Calendar FULLW"],
+ ["Shuttle", "Calendar FULLW"],
+ ["CITY1", "Calendar FULLW"],
+ ["CITY2", "Calendar FULLW"],
+ ["to Furnace Creek Resort", "Calendar FULLW"],
+ ["to Bullfrog", "Calendar FULLW"],
+ ["to Amargosa Valley", "Calendar WE"],
+ ["to Airport", "Calendar WE"],
+ ["to Amargosa Valley", "Calendar WE"],
+ ["to Airport", "Calendar WE"]
+ ]
+
+ expect(import.referential.vehicle_journeys.map(&defined_attributes)).to match_array(expected_attributes)
+ end
+ end
+
+ describe "#import_stop_times" do
+ let(:import) { create_import "google-sample-feed.zip" }
+
+ before do
+ import.prepare_referential
+ import.import_calendars
+ import.import_trips
+ end
+
+ it "should create a VehicleJourneyAtStop for each stop_time" do
+ import.import_stop_times
+
+ def t(value)
+ Time.parse(value)
+ end
+
+ defined_attributes = [
+ "stop_areas.registration_number", :position, :departure_time, :arrival_time,
+ ]
+ expected_attributes = [
+ ["STAGECOACH", 0, t("2000-01-01 06:00:00 UTC"), t("2000-01-01 06:00:00 UTC")],
+ ["BEATTY_AIRPORT", 1, t("2000-01-01 06:20:00 UTC"), t("2000-01-01 06:20:00 UTC")],
+ ["STAGECOACH", 0, t("2000-01-01 06:00:00 UTC"), t("2000-01-01 06:00:00 UTC")],
+ ["NANAA", 1, t("2000-01-01 06:07:00 UTC"), t("2000-01-01 06:05:00 UTC")],
+ ["NADAV", 2, t("2000-01-01 06:14:00 UTC"), t("2000-01-01 06:12:00 UTC")],
+ ["DADAN", 3, t("2000-01-01 06:21:00 UTC"), t("2000-01-01 06:19:00 UTC")],
+ ["EMSI", 4, t("2000-01-01 06:28:00 UTC"), t("2000-01-01 06:26:00 UTC")],
+ ["EMSI", 0, t("2000-01-01 06:30:00 UTC"), t("2000-01-01 06:28:00 UTC")],
+ ["DADAN", 1, t("2000-01-01 06:37:00 UTC"), t("2000-01-01 06:35:00 UTC")],
+ ["NADAV", 2, t("2000-01-01 06:44:00 UTC"), t("2000-01-01 06:42:00 UTC")],
+ ["NANAA", 3, t("2000-01-01 06:51:00 UTC"), t("2000-01-01 06:49:00 UTC")],
+ ["STAGECOACH", 4, t("2000-01-01 06:58:00 UTC"), t("2000-01-01 06:56:00 UTC")],
+ ["BEATTY_AIRPORT", 0, t("2000-01-01 08:00:00 UTC"), t("2000-01-01 08:00:00 UTC")],
+ ["BULLFROG", 1, t("2000-01-01 08:15:00 UTC"), t("2000-01-01 08:10:00 UTC")],
+ ["BULLFROG", 0, t("2000-01-01 12:05:00 UTC"), t("2000-01-01 12:05:00 UTC")],
+ ["BEATTY_AIRPORT", 1, t("2000-01-01 12:15:00 UTC"), t("2000-01-01 12:15:00 UTC")],
+ ["BULLFROG", 0, t("2000-01-01 08:20:00 UTC"), t("2000-01-01 08:20:00 UTC")],
+ ["FUR_CREEK_RES", 1, t("2000-01-01 09:20:00 UTC"), t("2000-01-01 09:20:00 UTC")],
+ ["FUR_CREEK_RES", 0, t("2000-01-01 11:00:00 UTC"), t("2000-01-01 11:00:00 UTC")],
+ ["BULLFROG", 1, t("2000-01-01 12:00:00 UTC"), t("2000-01-01 12:00:00 UTC")],
+ ["BEATTY_AIRPORT", 0, t("2000-01-01 08:00:00 UTC"), t("2000-01-01 08:00:00 UTC")],
+ ["AMV", 1, t("2000-01-01 09:00:00 UTC"), t("2000-01-01 09:00:00 UTC")],
+ ["AMV", 0, t("2000-01-01 10:00:00 UTC"), t("2000-01-01 10:00:00 UTC")],
+ ["BEATTY_AIRPORT", 1, t("2000-01-01 11:00:00 UTC"), t("2000-01-01 11:00:00 UTC")],
+ ["BEATTY_AIRPORT", 0, t("2000-01-01 13:00:00 UTC"), t("2000-01-01 13:00:00 UTC")],
+ ["AMV", 1, t("2000-01-01 14:00:00 UTC"), t("2000-01-01 14:00:00 UTC")],
+ ["AMV", 0, t("2000-01-01 15:00:00 UTC"), t("2000-01-01 15:00:00 UTC")],
+ ["BEATTY_AIRPORT", 1, t("2000-01-01 16:00:00 UTC"), t("2000-01-01 16:00:00 UTC")]
+ ]
+ expect(referential.vehicle_journey_at_stops.includes(stop_point: :stop_area).pluck(*defined_attributes)).to match_array(expected_attributes)
+ end
+ end
+
+ describe "#import_calendars" do
+ let(:import) { create_import "google-sample-feed.zip" }
+
+ before do
+ import.prepare_referential
+ end
+
+ it "should create a Timetable for each calendar" do
+ import.import_calendars
+
+ def d(value)
+ Date.parse(value)
+ end
+
+ defined_attributes = ->(t) {
+ [t.comment, t.valid_days, t.periods.first.period_start, t.periods.first.period_end]
+ }
+ expected_attributes = [
+ ["Calendar FULLW", [1, 2, 3, 4, 5, 6, 7], d("Mon, 01 Jan 2007"), d("Fri, 31 Dec 2010")],
+ ["Calendar WE", [6, 7], d("Mon, 01 Jan 2007"), d("Fri, 31 Dec 2010")]
+ ]
+ expect(referential.time_tables.map(&defined_attributes)).to match_array(expected_attributes)
+ end
+ end
+
+ describe "#import_calendar_dates" do
+ let(:import) { create_import "google-sample-feed.zip" }
+
+ before do
+ import.prepare_referential
+ import.import_calendars
+ end
+
+ it "should create a Timetable::Date for each calendar date" do
+ import.import_calendar_dates
+
+ def d(value)
+ Date.parse(value)
+ end
+
+ defined_attributes = ->(d) {
+ [d.time_table.comment, d.date, d.in_out]
+ }
+ expected_attributes = [
+ ["Calendar FULLW", d("Mon, 04 Jun 2007"), false]
+ ]
+ expect(referential.time_table_dates.map(&defined_attributes)).to match_array(expected_attributes)
+ end
+ end
+
+ describe "#download_local_file" do
+
+ let(:file) { "google-sample-feed.zip" }
+ let(:import) do
+ Import::Gtfs.create! name: "GTFS test", creator: "Test", workbench: workbench, file: open_fixture(file), download_host: "rails_host"
+ end
+
+ let(:download_url) { "#{import.download_host}/workbenches/#{import.workbench_id}/imports/#{import.id}/download?token=#{import.token_download}" }
+
+ before do
+ stub_request(:get, download_url).to_return(status: 200, body: read_fixture(file))
+ end
+
+ it "should download local_file" do
+ expect(File.read(import.download_local_file)).to eq(read_fixture(file))
+ end
+
+ end
+
+ describe "#download_host" do
+ it "should return host defined by Rails.application.config.rails_host (without http:// schema)" do
+ allow(Rails.application.config).to receive(:rails_host).and_return("http://download_host")
+
+ expect(Import::Gtfs.new.download_host).to eq("download_host")
+ end
+
+ end
+
+end