app/models/export/ara.rb
# frozen_string_literal: true
# Export a dataset into a Ara CSV file
require 'ara/file'
class Export::Ara < Export::Base
include LocalExportSupport
# FIXME: Should be shared
option :line_ids, serialize: :map_ids
option :company_ids, serialize: :map_ids
option :line_provider_ids, serialize: :map_ids
option :exported_lines, default_value: 'all_line_ids',
enumerize: %w[line_ids company_ids line_provider_ids all_line_ids]
option :duration # Ignored by this export .. but required by Export::Scope builder
option :include_stop_visits
skip_empty_exports
def content_type
'application/csv'
end
def file_extension
'csv'
end
# TODO: Should be shared
def export_file
@export_file ||= Tempfile.new(["export#{id}", ".#{file_extension}"])
end
def target
@target ||= ::Ara::File::Target.new export_file
end
attr_writer :target
def period
@period ||=
begin
today = Time.zone.today
today..today + 5
end
end
alias include_stop_visits? include_stop_visits
def parts
@parts ||= [Stops, Lines, Companies, VehicleJourneys].tap do |parts|
parts << StopVisits if include_stop_visits?
end
end
def export_scope_options
# Disable stateful Export::Scope to make DailyScope works correctly
super.merge(stateful: false)
end
def generate_export_file
period.each do |day|
# For each day, a scope selects models to be exported
daily_scope = DailyScope.new self, day
Rails.logger.tagged(day) do
target.model_name(day) do |model_name|
# For each day, each kind of model is exported
parts.each do |part|
part.new(context: Context.new(self), export_scope: daily_scope, target: model_name).export
end
end
end
end
target.close
export_file.close
export_file
end
# Provides (restricted) access to Export resources in Part
class Context
def initialize(export)
@export = export
end
private
attr_reader :export
end
# Use Export::Scope::Scheduled which scopes all models according to vehicle_journeys
class DailyScope < Export::Scope::Base
def initialize(export, day)
super export.build_export_scope
@export = export
@day = day
current_scope.final_scope = self if current_scope.respond_to?(:final_scope=)
end
attr_reader :export, :day
delegate :stop_area_referential, :line_referential, to: :export
def vehicle_journeys
@vehicle_journeys ||= current_scope.vehicle_journeys.scheduled_on(day)
end
def stop_areas
@stop_areas ||= ::Query::StopArea.new(stop_area_referential.stop_areas)
.self_referents_and_ancestors(current_scope.stop_areas)
end
def lines
@lines ||= ::Query::Line.new(line_referential.lines)
.self_and_referents(current_scope.lines)
end
end
# TODO: To be shared
module CodeProvider
# Manage all CodeSpace for a Model class (StopArea, VehicleJourney, ...)
class Model
def initialize(scope:, model_class:)
@scope = scope
@model_class = model_class
end
attr_reader :scope, :model_class
# For the given model (StopArea, VehicleJourney, ..), returns all codes which are uniq.
def unique_codes(model)
unique_codes = {}
if model.respond_to?(:codes)
code_spaces = model.codes.map(&:code_space).uniq
unique_codes = code_spaces.map do |code_space|
code_provider = code_providers[code_space]
unique_code = code_provider.unique_code(model)
[code_provider.short_name, unique_code] if unique_code
end.compact.to_h
end
# Use registration_number as legacy mode
if model.respond_to?(:registration_number) &&
!unique_codes.key?(registration_number_provider.short_name)
unique_code = registration_number_provider.unique_code(model)
unique_codes[registration_number_provider.short_name] = unique_code if unique_code
end
unique_codes['external'] ||= model.objectid if model.respond_to?(:objectid) && !unique_codes.key?('external')
unique_codes
end
# Provide (unique) value from registration provider
def registration_number_provider
@registration_number_provider ||=
CodeProvider::RegistrationNumber.new scope: scope,
model_class: model_class
end
# Provide (unique) value for each Code Space
def code_providers
@code_providers ||= Hash.new do |h, code_space|
h[code_space] =
CodeProvider::CodeSpace.new scope: scope,
code_space: code_space,
model_class: model_class
end
end
# Returns a default implementation which simply returns all model codes
# Can be used instead of a real CodeProvider::Model when the context is not ready
def self.null
@null ||= Null.new
end
class Null
def unique_codes(model)
model.codes.map do |code|
[code.code_space.short_name, code.value]
end.to_h
end
end
end
# Manage registration number attribute as a code space
class RegistrationNumber
def initialize(scope:, model_class:)
@scope = scope
@model_class = model_class
end
attr_reader :scope, :model_class
def short_name
'external'
end
def unique_code(model)
candidate_value = model.registration_number
return nil if candidate_value.blank?
return nil if duplicated?(candidate_value)
candidate_value
end
def duplicated?(code_value)
duplicated_registration_numbers.include? code_value
end
def model_collection
model_class.model_name.plural
end
def models
scope.send model_collection
end
def duplicated_registration_numbers
# CHOUETTE-1787 Use model_class to load models before grouping it
@duplicated_registration_numbers ||=
begin
registration_numbers = model_class
.where(id: models.select(:id))
.where.not(registration_number: nil)
.group(:registration_number)
.having("count(#{model_class.model_name.plural}.id) > 1")
.pluck(:registration_number)
SortedSet.new registration_numbers
end
end
end
# Manage a single CodeSpace for a Model class
# TODO To be used in Export::Gtfs
class CodeSpace
def initialize(scope:, code_space:, model_class:)
@scope = scope
@code_space = code_space
@model_class = model_class
end
attr_reader :scope, :code_space, :model_class
delegate :short_name, to: :code_space
# Returns the code value for the given Resource if uniq
def unique_code(model)
candidates = candidate_codes(model)
return nil unless candidates.one?
candidate_value = candidates.first.value
return nil if duplicated?(candidate_value)
candidate_value
end
def candidate_codes(model)
model.codes.select { |code| code.code_space_id == code_space.id }
end
def duplicated?(code_value)
duplicated_code_values.include? code_value
end
def model_collection
model_class.model_name.plural
end
def models
scope.send model_collection
end
def model_codes
codes.where(code_space: code_space, resource: models)
end
def codes
# FIXME
if model_class == Chouette::VehicleJourney
scope.referential_codes
else
scope.codes
end
end
def duplicated_code_values
@duplicated_code_values ||=
SortedSet.new(model_codes.select(:value,
:resource_id).group(:value).having('count(resource_id) > 1').pluck(:value))
end
end
end
class Part
attr_reader :context, :export_scope, :target
def initialize(export_scope:, target:, context: nil)
@context = context
@export_scope = export_scope
@target = target
end
def name
@name ||= self.class.name.demodulize.underscore
end
def export
Chouette::Benchmark.measure name do
Rails.logger.tagged(name) do
export!
end
end
end
end
delegate :stop_area_referential, :line_referential, to: :referential
class Stops < Part
delegate :stop_areas, to: :export_scope
def export!
stop_areas.includes(:parent, :referent, :lines, codes: :code_space).find_each do |stop_area|
target << Decorator.new(stop_area, code_provider: code_provider).ara_model
end
end
def code_provider
@code_provider ||= CodeProvider::Model.new scope: export_scope, model_class: Chouette::StopArea
end
# Creates an Ara::StopArea from a StopArea
class Decorator < SimpleDelegator
def initialize(stop_area, code_provider: nil)
super stop_area
@code_provider = code_provider
end
# TODO: To be shared
def code_provider
@code_provider ||= CodeProvider::Model.null
end
def ara_attributes
{
id: uuid,
name: name,
codes: ara_codes,
parent_id: parent_uuid,
line_ids: line_uuids,
collect_children: ara_collect_children?,
referent_id: referent_uuid
}
end
def ara_collect_children?
!quay?
end
def line_uuids
lines.map { |line| line.get_objectid&.local_id }
end
def parent_uuid
parent&.get_objectid&.local_id
end
def referent_uuid
referent&.get_objectid&.local_id
end
def ara_model
Ara::File::StopArea.new ara_attributes
end
# TODO: To be shared
def uuid
get_objectid&.local_id
end
# TODO: To be shared
def ara_codes
code_provider.unique_codes __getobj__
end
end
end
class StopVisits < Part
def vehicle_journey_at_stops
sql_query = export_scope.vehicle_journey_at_stops.departure_arrival_base_query
export_scope.vehicle_journey_at_stops.select('*').from(sql_query)
end
def export!
included = { vehicle_journey: [:company, { route: { line: :company } }], stop_point: :stop_area }
vehicle_journey_at_stops.includes(included).find_each(batch_size: 10_000) do |stop_visit|
target << Decorator.new(stop_visit, day: export_scope.day).ara_model
end
end
class Decorator < SimpleDelegator
EXPORT_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%:z'
def initialize(stop_visit, day:)
super stop_visit
@day = day
end
attr_accessor :day
def vehicle_journey_at_stop
__getobj__
end
def arrival?
if vehicle_journey_at_stop.respond_to?(:arrival?)
vehicle_journey_at_stop.arrival?
else
vehicle_journey&.vehicle_journey_at_stops&.last == vehicle_journey_at_stop
end
end
def departure?
if vehicle_journey_at_stop.respond_to?(:departure?)
vehicle_journey_at_stop.departure?
else
vehicle_journey&.vehicle_journey_at_stops&.first == vehicle_journey_at_stop
end
end
def arrival_time
super unless departure?
end
def departure_time
super unless arrival?
end
def ara_attributes
{
id: uuid,
codes: ara_codes,
stop_area_id: stop_area_id,
vehicle_journey_id: vehicle_journey_id,
passage_order: passage_order,
schedules: schedules,
references: references
}
end
def ara_model
Ara::File::StopVisit.new ara_attributes
end
def stop_area_id
stop_point&.stop_area&.get_objectid&.local_id
end
def vehicle_journey_id
vehicle_journey&.get_objectid&.local_id
end
def references
return unless operator_code
{
"OperatorRef": {
'Type': 'OperatorRef',
'Code': operator_code
}
}
end
def operator_code
return unless company
return { 'external' => company.registration_number } if company.registration_number
code = company.codes.first
return unless code
{ code.code_space.short_name => code.value }
end
def company
vehicle_journey&.company || line&.company
end
delegate :line, to: :vehicle_journey, allow_nil: true
def passage_order
pos = stop_point&.position
return '' if pos.nil?
(pos + 1).to_s
end
def schedules
return unless arrival_time || departure_time
[ { 'Kind': 'aimed', 'ArrivalTime': ara_arrival_time, 'DepartureTime': ara_departure_time }.compact ]
end
def uuid
@uuid ||= SecureRandom.uuid
end
def ara_codes
{ external: uuid }
end
delegate :time_zone, to: :vehicle_journey_at_stop
def ara_departure_time
departure_local_time_of_day&.to_time(day, time_zone: time_zone)&.strftime(EXPORT_TIME_FORMAT)
end
def ara_arrival_time
arrival_local_time_of_day&.to_time(day, time_zone: time_zone)&.strftime(EXPORT_TIME_FORMAT)
end
end
end
class Lines < Part
delegate :lines, to: :export_scope
def export!
lines.includes(codes: :code_space).find_each do |line|
target << Decorator.new(line, code_provider: code_provider).ara_model
end
end
def code_provider
@code_provider ||= CodeProvider::Model.new scope: export_scope, model_class: Chouette::Line
end
# Creates an Ara::StopArea from a StopArea
class Decorator < SimpleDelegator
def initialize(line, code_provider: nil)
super line
@code_provider = code_provider
end
# TODO: To be shared
def code_provider
@code_provider ||= CodeProvider::Model.null
end
def ara_attributes
{
id: uuid,
name: name,
number: number,
codes: ara_codes,
referent_id: referent_uuid
}
end
def ara_model
Ara::File::Line.new ara_attributes
end
# TODO: To be shared
def uuid
get_objectid.local_id
end
def referent_uuid
referent&.get_objectid&.local_id
end
# TODO: To be shared
def ara_codes
code_provider.unique_codes __getobj__
end
end
end
class Companies < Part
delegate :companies, to: :export_scope
def export!
companies.find_each do |company|
target << Decorator.new(company, code_provider: code_provider).ara_model
end
end
def code_provider
@code_provider ||= CodeProvider::Model.new scope: export_scope, model_class: Chouette::Company
end
# Creates an Ara::StopArea from a StopArea
class Decorator < SimpleDelegator
def initialize(company, code_provider: nil)
super company
@code_provider = code_provider
end
# TODO: To be shared
def code_provider
@code_provider ||= CodeProvider::Model.null
end
def ara_attributes
{
id: uuid,
name: name,
codes: ara_codes
}
end
def ara_model
Ara::File::Operator.new ara_attributes
end
# TODO: To be shared
def uuid
get_objectid&.local_id
end
# TODO: To be shared
def ara_codes
code_provider.unique_codes __getobj__
end
end
end
class VehicleJourneys < Part
delegate :vehicle_journeys, to: :export_scope
def export!
vehicle_journeys.includes(codes: :code_space, route: :line).find_each do |vehicle_journey|
target << Decorator.new(vehicle_journey, code_provider: code_provider).ara_model
end
end
def code_provider
@code_provider ||= CodeProvider::Model.new scope: export_scope, model_class: Chouette::VehicleJourney
end
# Creates an Ara::VehicleJourney from a VehicleJourney
class Decorator < SimpleDelegator
def initialize(vehicle_journey, code_provider: nil)
super vehicle_journey
@code_provider = code_provider
end
# TODO: To be shared
def code_provider
@code_provider ||= CodeProvider::Model.null
end
def ara_attributes
{
id: uuid,
name: published_journey_name,
codes: ara_codes,
line_id: line.get_objectid.local_id,
direction_type: route.wayback,
attributes: {
"VehicleMode": line.transport_mode
}
}
end
def ara_model
Ara::File::VehicleJourney.new ara_attributes
end
# TODO: To be shared
def uuid
get_objectid.local_id
end
# TODO: To be shared
def ara_codes
code_provider.unique_codes __getobj__
end
end
end
end