af83/chouette-core

View on GitHub
app/models/export/netex_generic.rb

Summary

Maintainability
F
4 days
Test Coverage
class Export::NetexGeneric < Export::Base
  include LocalExportSupport

  option :profile, enumerize: %w[none french european idfm/line idfm/full], default: :none
  option :duration
  option :from, serialize: ActiveModel::Type::Date
  option :to, serialize: ActiveModel::Type::Date
  option :line_ids, serialize: :map_ids
  option :company_ids, serialize: :map_ids
  option :line_provider_ids, serialize: :map_ids
  option :period, default_value: 'all_periods', enumerize: %w[all_periods only_next_days static_day_period]
  option :exported_lines, default_value: 'all_line_ids', enumerize: %w[line_ids company_ids line_provider_ids all_line_ids]

  validate :ensure_is_valid_period

  def ensure_is_valid_period
    return unless period == 'static_day_period'

    if from.blank? || to.blank? || from > to
      errors.add(:from, :invalid)
      errors.add(:to, :invalid)
    end
  end

  def target
    @target ||= Netex::Target.build export_file, profile: netex_profile, validity_periods: [export_scope.validity_period]
  end
  attr_writer :target

  def profile?
    ! [nil, 'none'].include? profile
  end

  def netex_profile
    @netex_profile ||= Netex::Profile.create(profile) if profile?
  end

  def content_type
    profile? ? 'application/zip' : 'text/xml'
  end

  def file_extension
    profile? ? "zip" : 'xml'
  end

  delegate :stop_area_referential, to: :workgroup
  delegate :shape_referential, to: :workgroup

  def stop_areas
    @stop_areas ||=
      ::Query::StopArea.new(stop_area_referential.stop_areas).
        self_referents_and_ancestors(export_scope.stop_areas)
  end

  def entrances
    # Must unscope the entrances to find entrances associated with all exported Stop Areas
    # (including parent Stop Areas)
    stop_area_referential.entrances.where(stop_area: stop_areas)
  end

  def point_of_interests
    shape_referential.point_of_interests
  end

  def resource_tagger
    @resource_tagger ||= ResourceTagger.new
  end

  def export_file
    @export_file ||= Tempfile.new(["export#{id}",'.zip'])
  end

  def generate_export_file
    part_classes = [
      Entrances,
      Quays,
      StopPlaces,
      Companies,
      Networks,
      Lines,
      # Export StopPoints before Routes to detect local references
      StopPoints,
      Routes,
      RoutingConstraintZones,
      JourneyPatterns,
      TimeTables,
      VehicleJourneyAtStops,
      VehicleJourneys,
      VehicleJourneyStopAssignments,
      Organisations,
      PointOfInterests
    ]

    part_classes.each_with_index do |part_class, index|
      part_class.new(self).export_part
    end

    target.close
    export_file.close

    export_file
  end

  class TaggedTarget
    def initialize(target, tags = {})
      @target = target
      @tags = tags
    end

    def add(resource)
      resource.tags = @tags
      @target << resource
    end
    alias << add
  end

  class Part
    attr_reader :export

    def initialize(export, options = {})
      @export = export
      options.each { |k,v| send "#{k}=", v }
    end

    delegate :target, :resource_tagger, :export_scope, :workgroup, to: :export

    def part_name
      @part_name ||= self.class.name.demodulize.underscore
    end

    def export_part
      Chouette::Benchmark.measure part_name do
        export!
      end
    end
  end

  class ResourceTagger

    # Returns tags for several lines.
    # Returns only uniq values accross all given lines
    def tags_for_lines line_ids
      tags = Hash.new { |h,k| h[k] = Set.new }

      line_ids.each do |line_id|
        tags_for(line_id).each do |key, value|
          tags[key] << value
        end
      end

      # Remove multiple values
      tags.map do |key, set|
        [ key, set.first ] if set.size == 1
      end.compact.to_h
    end

    def tags_for line_id
      tag_index[line_id]
    end

    def register_tag_for(line)
      tag_index[line.id] = {
        line_id: line.objectid,
        line_name: line.name,
        operator_id: line.company&.objectid,
        operator_name: line.company&.name
      }
    end

    protected

    def tag_index
      @tag_index ||= Hash.new { |h,k| h[k] = {} }
    end
  end

  class AlternateIdentifiersExtractor

    def initialize(model)
      @model = model
    end
    attr_reader :model

    delegate :registration_number, :codes, to: :model

    def registration_number_value
      if has_registration_number?
        [[ "external", registration_number ]]
      else
        []
      end
    end

    def has_registration_number?
      model.respond_to?(:registration_number) && registration_number.present?
    end

    def has_codes?
      model.respond_to? :codes
    end

    def codes_values
      if has_codes?
        codes.map do |code|
          [ code.code_space.short_name, code.value ]
        end
      else
        []
      end
    end

    def alternate_identifiers_values
      registration_number_value + codes_values
    end

    def alternate_identifiers
      alternate_identifiers_values.map do |key, value|
        Netex::KeyValue.new key: key, value: value, type_of_key: "ALTERNATE_IDENTIFIER"
      end
    end

  end

  class CustomFieldExtractor

    def initialize(model)
      @model = model
    end
    attr_reader :model

    delegate :custom_field_values, to: :model, allow_nil: true

    def custom_field_identifiers
      return [] unless custom_field_values.present?

      custom_field_values.map do |key, value|
        Netex::KeyValue.new key: key, value: value, type_of_key: "chouette::custom-field"
      end
    end

  end

  module Accessibility
    extend ActiveSupport::Concern

    included do
      def accessibility_assessment
        return unless accessibility_assessment?

        Netex::AccessibilityAssessment.new(
          id: netex_identifier&.change(type: 'AccessibilityAssessment').to_s,
          mobility_impaired_access: netex_value(mobility_impaired_accessibility),
          limitations: [accessibility_limitation].compact,
          validity_conditions: [availability_condition].compact
        )
      end

      def accessibility_limitation
        return unless accessibility_limitation?

        Netex::AccessibilityLimitation.new(
          wheelchair_access: netex_value(wheelchair_accessibility),
          step_free_access: netex_value(step_free_accessibility),
          escalator_free_access: netex_value(escalator_free_accessibility),
          lift_free_access: netex_value(lift_free_accessibility),
          audible_signals_available: netex_value(audible_signals_availability),
          visual_signs_available: netex_value(visual_signs_availability)
        )
      end

      def netex_value(value)
        case value
        when 'yes'
          'true'
        when 'no'
          'false'
        else
          value
        end
      end

      def availability_condition
        return unless accessibility_limitation_description.present?

        Netex::AvailabilityCondition.new(
          id: netex_identifier.change(type: 'AvailabilityCondition').to_s,
          description: accessibility_limitation_description
        )
      end

      def accessibility_assessment?
        accessibility_limitation? || availability_condition.present? || mobility_impaired_accessibility != 'unknown'
      end

      def accessibility_limitation?
        %i[
          wheelchair_accessibility step_free_accessibility escalator_free_accessibility
          lift_free_accessibility audible_signals_availability visual_signs_availability
        ].any? do |attribute|
          send(attribute) != :unknown
        end
      end
    end
  end

  class StopDecorator < SimpleDelegator
    include Accessibility

    def netex_attributes # rubocop:disable Metrics/MethodLength
      {
        id: netex_identifier,
        derived_from_object_ref: derived_from_object_ref,
        name: name,
        public_code: public_code,
        centroid: centroid,
        raw_xml: import_xml,
        key_list: key_list,
        accessibility_assessment: accessibility_assessment,
        postal_address: postal_address,
        url: url
      }.tap do |attributes|
        unless netex_quay?
          attributes[:parent_site_ref] = parent_site_ref
          attributes[:place_types] = place_types
        end
      end
    end

    def netex_identifier
      @netex_identifier ||= Netex::ObjectId.parse(objectid)
    end

    def parent_objectid
      parent&.objectid
    end

    def derived_from_object_ref
      referent&.objectid
    end

    def key_list
      netex_alternate_identifiers + netex_custom_field_identifiers
    end

    def netex_alternate_identifiers
      AlternateIdentifiersExtractor.new(self).alternate_identifiers
    end

    def netex_custom_field_identifiers
      CustomFieldExtractor.new(self).custom_field_identifiers
    end

    def centroid
      Netex::Point.new(location: Netex::Location.new(longitude: longitude, latitude: latitude))
    end

    def parent_site_ref
      Netex::Reference.new(parent_objectid, type: 'StopPlace') if parent_objectid
    end

    def place_types
      [Netex::Reference.new(type_of_place, type: String)]
    end

    def type_of_place
      case area_type
      when Chouette::AreaType::QUAY
        'quay'
      when 'zdlp'
        'monomodalStopPlace'
      when 'lda'
        'generalStopPlace'
      when 'gdl'
        'groupOfStopPlaces'
      end
    end

    def postal_address_objectid
      netex_identifier&.change(type: 'PostalAddress').to_s
    end

    def postal_address
      Netex::PostalAddress.new(
        id: postal_address_objectid,
        address_line_1: street_name,
        post_code: zip_code,
        town: city_name,
        postal_region: postal_region,
        country_name: country_name
      )
    end

    def netex_resource
      netex_resource_class.new(netex_attributes).tap do |stop|
        if netex_quay?
          stop.with_tag parent_id: parent_objectid
        end
      end
    end

    def netex_quay?
      area_type&.to_sym == Chouette::AreaType::QUAY
    end

    def netex_resource_class
      netex_quay? ? Netex::Quay : Netex::StopPlace
    end
  end

  class Quays < Part

    delegate :stop_areas, to: :export

    def export!
      stop_areas.where(area_type: Chouette::AreaType::QUAY).includes(:codes, :parent, :referent).find_each do |stop_area|
        netex_resource = StopDecorator.new(stop_area).netex_resource
        target << netex_resource
      end
    end

  end

  class StopPlaces < Part

    delegate :stop_areas, to: :export

    def export!
      stop_areas.where.not(area_type: Chouette::AreaType::QUAY).includes(:codes, :entrances, :parent, :referent).find_each do |stop_area|
        stop_place = StopDecorator.new(stop_area).netex_resource
        target << stop_place
      end
    end

  end

  class Entrances < Part

    delegate :entrances, to: :export

    def export!
      entrances.includes(:raw_import).find_each do |entrance|
        decorated_entrance = Decorator.new(entrance)
        target << decorated_entrance.netex_resource
      end
    end

    class Decorator < SimpleDelegator

      def netex_attributes
        {
          id: netex_identifier,
          name: name,
          short_name: short_name,
          description: description,
          centroid: centroid,
          postal_address: postal_address,
          entrance_type: entrance_type,
          is_entry: entry?,
          is_exit: exit?,
          raw_xml: raw_xml,
        }
      end

      def netex_resource
        Netex::StopPlaceEntrance.new(netex_attributes).with_tag(parent_id: parent_objectid)
      end

      def netex_identifier
        @netex_identifier ||= Netex::ObjectId.parse(objectid)
      end

      def centroid
        Netex::Point.new(
          location: Netex::Location.new(longitude: longitude, latitude: latitude)
        )
      end

      def postal_address_objectid
        netex_identifier.change(type: 'PostalAddress').to_s
      end

      def postal_address
        Netex::PostalAddress.new(
          id: postal_address_objectid,
          address_line_1: address_line_1,
          post_code: zip_code,
          town: city_name,
          country_name: country
        )
      end

      def parent_objectid
        stop_area&.objectid
      end

      def raw_xml
        raw_import&.content
      end
    end
  end

  class PointOfInterests < Part

    def export!
      point_of_interests.find_each do |point_of_interest|
        decorated_point_of_interest = Decorator.new(point_of_interest)
        target << decorated_point_of_interest.netex_resource
      end
    end

    def point_of_interests
      export.point_of_interests
        .includes(:codes, :point_of_interest_hours)
        .joins(:point_of_interest_category)
        .select(
          "point_of_interests.*",
          "point_of_interest_categories.name AS category_name"
        )
    end

    class Decorator < SimpleDelegator

      def netex_attributes
        {
          id: uuid,
          name: name,
          url: url,
          centroid: centroid,
          postal_address: postal_address,
          key_list: key_list,
          operating_organisation_view: operating_organisation_view,
          classifications: classifications,
          validity_conditions: validity_conditions,
        }
      end

      def netex_resource
        Netex::PointOfInterest.new(netex_attributes)
      end

      def centroid
        return unless longitude || latitude

        Netex::Point.new(
          location: Netex::Location.new(longitude: longitude, latitude: latitude)
        )
      end

      def postal_address
        Netex::PostalAddress.new(
          id: "Address:#{uuid}",
          address_line_1: address_line_1,
          post_code: zip_code,
          town: city_name,
          postal_region: postal_region,
          country_name: country
        )
      end

      def operating_organisation_view
        Netex::OperatingOrganisationView.new(
          contact_details: Netex::ContactDetails.new(
            phone: phone,
            email: email
          )
        )
      end

      def classifications
        [ Netex::PointOfInterestClassificationView.new(name: category_name) ]
      end

      def key_list
        AlternateIdentifiersExtractor.new(self).alternate_identifiers
      end

      def validity_conditions
        [].tap do |validity_conditions|
          point_of_interest_hours.find_each do |hour|
            validity_condition = ValidityCondition.new(hour, uuid)
            validity_conditions << Netex::AvailabilityCondition.new(
              day_types: validity_condition.day_types,
              timebands: validity_condition.timebands
            )
          end
        end
      end

      class ValidityCondition
        def initialize(hour, uuid)
          @hour = hour
          @uuid = uuid
        end
        attr_accessor :hour, :uuid

        def timebands
          [
            Netex::Timeband.new(
              id: id,
              start_time: start_time,
              end_time: end_time
            )
          ]
        end

        def day_types
          [
            Netex::DayType.new(
              id: id,
              properties: properties
            )
          ]
        end

        private

        def id
          "#{uuid}-#{hour.id}"
        end

        def start_time
          hour.opening_time_of_day.to_hms
        end

        def end_time
          hour.closing_time_of_day.to_hms
        end

        def properties
          [ Netex::PropertyOfDay.new(days_of_week: days_of_week) ]
        end

        def days_of_week
          all_days
            .select{ |day| contains_day?(day) }
            .map{ |day| day.to_s.capitalize }
            .join(' ')
        end

        def all_days
          @all_days ||= Cuckoo::Timetable::DaysOfWeek.all.days
        end

        def contains_day?(day)
          hour.week_days.send("#{day}?")
        end
      end
    end
  end

  class Lines < Part

    delegate :lines, to: :export_scope

    def export!
      lines.includes(:company).find_each do |line|
        resource_tagger.register_tag_for line
        tags = resource_tagger.tags_for(line.id)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_line = Decorator.new(line)
        tagged_target << decorated_line.netex_resource
      end
    end

    class Decorator < SimpleDelegator
      include Accessibility

      def netex_attributes
        {
          id: objectid,
          name: netex_name,
          transport_mode: transport_mode,
          transport_submode: netex_transport_submode,
          operator_ref: operator_ref,
          public_code: number,
          represented_by_group_ref: represented_by_group_ref,
          presentation: presentation,
          additional_operators: additional_operators,
          key_list: netex_alternate_identifiers,
          accessibility_assessment: accessibility_assessment,
          status: status,
          valid_between: valid_between,
          raw_xml: import_xml
        }
      end

      def netex_alternate_identifiers
        AlternateIdentifiersExtractor.new(self).alternate_identifiers
      end

      def netex_identifier
        @netex_identifier ||= Netex::ObjectId.parse(objectid)
      end

      def netex_name
        name || published_name
      end

      def valid_between
        return unless active_from.present? || active_until.present?

        from_date = active_from.present? ? active_from.beginning_of_day : nil
        to_date = active_until.present? ? (active_until + 1).beginning_of_day : nil

        Netex::ValidBetween.new(
          from_date: from_date,
          to_date: to_date
        )
      end

      def additional_operators
        secondary_companies.map do |company|
          Netex::Reference.new(company&.objectid, type: 'OperatorRef')
        end
      end

      def status
        deactivated ? 'inactive' : ''
      end

      def netex_transport_submode
        transport_submode&.to_s unless transport_submode == :undefined
      end

      def presentation
        Netex::Presentation.new(text_colour: text_color&.downcase, colour: color&.downcase)
      end

      def netex_resource
        Netex::Line.new netex_attributes
      end

      def operator_ref
        Netex::Reference.new(company.objectid, type: 'OperatorRef') if company
      end

      def represented_by_group_ref
        Netex::Reference.new(network.objectid, type: 'NetworkRef') if network
      end

    end
  end

  class Companies < Part

    delegate :companies, to: :export_scope

    def export!
      companies.find_each do |company|
        decorated_company = Decorator.new(company)
        target << decorated_company.netex_resource
      end
    end

    class Decorator < SimpleDelegator

      def netex_attributes
        {
          id: objectid,
          name: name,
          raw_xml: import_xml
        }.tap do |attributes|
          if netex_alternate_identifiers.present?
            attributes[:key_list] = netex_alternate_identifiers
          end
        end
      end

      def netex_resource
        Netex::Operator.new netex_attributes
      end

      def netex_alternate_identifiers
        AlternateIdentifiersExtractor.new(self).alternate_identifiers
      end
    end

  end

  class Networks < Part

    delegate :networks, to: :export_scope

    def export!
      networks.find_each do |network|
        Rails.logger.debug { "Export Network #{network.inspect}" }
        decorated_network = Decorator.new(network)
        target << decorated_network.netex_resource
      end
    end

    class Decorator < SimpleDelegator

      def netex_attributes
        {
          id: objectid,
          name: name,
          raw_xml: import_xml
        }
      end

      def netex_resource
        Netex::Network.new netex_attributes
      end

    end

  end

  class StopPointDecorator < SimpleDelegator

    attr_accessor :journey_pattern_id, :route
    def initialize(stop_point, journey_pattern_id: nil, route: nil)
      super stop_point
      @journey_pattern_id = journey_pattern_id
      @route = route
    end

    def point_on_route
      Netex::PointOnRoute.new point_on_route_attributes
    end

    def point_on_route_attributes
      {
        id: point_on_route_id,
        order: netex_order,
        route_point_ref: route_point_ref
      }
    end

    def netex_order
      position+1
    end

    def netex_identifier
      @netex_identifier ||= Netex::ObjectId.parse(objectid)
    end

    def point_on_route_id
      netex_identifier.change(type: 'PointOnRoute').to_s
    end

    def route_point_ref
      Netex::Reference.new(route_point_ref_id, type: 'RoutePointRef')
    end

    def route_point_ref_id
      netex_identifier.change(type: 'RoutePoint').to_s
    end

    def scheduled_stop_point
      Netex::ScheduledStopPoint.new(scheduled_stop_point_attributes)
    end

    def scheduled_stop_point_attributes
      {
        id: scheduled_stop_point_id,
        data_source_ref: route_data_source_ref,
      }
    end

    def route_data_source_ref
      __getobj__.try(:route_data_source_ref) || route&.data_source_ref
    end

    def stop_area_objectid
      __getobj__.try(:stop_area_objectid) || stop_area&.objectid
    end

    def stop_area_area_type
      __getobj__.try(:stop_area_area_type) || stop_area&.area_type
    end

    def scheduled_stop_point_id
      @scheduled_stop_point_id ||= netex_identifier.change(type: 'ScheduledStopPoint').to_s if netex_identifier
    end

    def netex_quay?
      stop_area_area_type&.to_sym == Chouette::AreaType::QUAY
    end

    def passenger_stop_assignment
      Netex::PassengerStopAssignment.new(passenger_stop_assignment_attributes).tap do |passenger_stop_assignment|
        if netex_quay?
          passenger_stop_assignment.quay_ref = quay_ref
        else
          passenger_stop_assignment.stop_place_ref = stop_place_ref
        end
      end
    end

    def passenger_stop_assignment_attributes
      {
        id: passenger_stop_assignment_id,
        data_source_ref: route_data_source_ref,
        order: 0,
        scheduled_stop_point_ref: scheduled_stop_point_ref
      }
    end

    def passenger_stop_assignment_id
      netex_identifier.change(type: 'PassengerStopAssignment').to_s if netex_identifier
    end

    def scheduled_stop_point_ref
      Netex::Reference.new(scheduled_stop_point_id, type: 'ScheduledStopPointRef')
    end

    def quay_ref
      Netex::Reference.new(stop_area_objectid, type: 'QuayRef')
    end

    def stop_place_ref
      Netex::Reference.new(stop_area_objectid, type: 'StopPlaceRef')
    end

    def route_point
      Netex::RoutePoint.new(route_point_attributes)
    end

    def route_point_attributes
      {
        id: route_point_id,
        projections: point_projection,
        data_source_ref: route_data_source_ref
      }
    end

    def route_point_id
      netex_identifier.change(type: 'RoutePoint').to_s
    end

    def point_projection
      [Netex::PointProjection.new(point_projection_attributes)]
    end

    def point_projection_attributes
      {
        id: point_projection_id,
        project_to_point_ref: project_to_point_ref
      }
    end

    def point_projection_id
      netex_identifier.change(type: 'PointProjection').to_s
    end

    def project_to_point_ref
      # Netex::Reference.new(scheduled_stop_point_id, type: 'ProjectToPointRef')
      Netex::Reference.new(scheduled_stop_point_id, type: 'ScheduledStopPoint')
    end

    def stop_point_in_journey_pattern
      Netex::StopPointInJourneyPattern.new stop_point_in_journey_pattern_attributes
    end

    def stop_point_in_journey_pattern_attributes
      {
        id: stop_point_in_journey_pattern_id,
        order: position+1,
        scheduled_stop_point_ref: scheduled_stop_point_ref,
        for_boarding: netex_for_boarding,
        for_alighting: netex_for_alighting
      }
    end

    def netex_for_boarding
      for_boarding == "normal"
    end

    def netex_for_alighting
      for_alighting == "normal"
    end

    def self.stop_point_in_journey_pattern_id(stop_point_objectid, journey_pattern_objectid)
      merged_object_id = Netex::ObjectId.merge(journey_pattern_objectid, stop_point_objectid, type: "StopPointInJourneyPattern")

      if merged_object_id
        merged_object_id.to_s
      else
        "#{journey_pattern_objectid}-#{stop_point_objectid}"
      end
    end

    def stop_point_in_journey_pattern_id
      self.class.stop_point_in_journey_pattern_id(netex_identifier, journey_pattern_id)
    end
  end

  class Routes < Part

    delegate :routes, to: :export_scope

    def export!
      routes.includes(:line, :stop_points).find_each do |route|
        tags = resource_tagger.tags_for(route.line_id)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_route = Decorator.new(route)
        # Export Direction before the Route to detect local reference
        tagged_target << decorated_route.direction if decorated_route.direction
        tagged_target << decorated_route.netex_resource

        decorated_route.routing_constraint_zones.each do |zone|
          tagged_target << zone
        end
      end
    end

    class Decorator < SimpleDelegator

      delegate :line_routing_constraint_zones, to: :line

      def netex_attributes
        {
          id: objectid,
          data_source_ref: data_source_ref,
          name: netex_name,
          line_ref: line_ref,
          direction_ref: direction_ref,
          direction_type: direction_type,
          points_in_sequence: points_in_sequence
        }.tap do |attributes|
          attributes[:direction_ref] = direction_ref if published_name.present?
        end
      end

      def netex_resource
        Netex::Route.new netex_attributes
      end

      def netex_name
        published_name.presence || name
      end

      def netex_identifier
        @netex_identifier ||= Netex::ObjectId.parse(objectid)
      end

      def direction_id
        netex_identifier.change(type: 'Direction').to_s
      end

      def direction
        @direction ||= Netex::Direction.new(
          id: direction_id,
          data_source_ref: data_source_ref,
          name: published_name
        ) if published_name
      end

      def direction_ref
        Netex::Reference.new(direction_id, type: 'DirectionRef') if direction
      end

      def direction_type
        wayback.to_s
      end

      def line_ref
        Netex::Reference.new(line.objectid, type: Netex::Line) if line
      end

      def points_in_sequence
        decorated_stop_points.map(&:point_on_route)
      end

      def decorated_stop_points
        @decorated_stop_points ||= stop_points.map do |stop_point|
          StopPointDecorator.new stop_point, route: self
        end
      end

      def stop_area_ids
        @stop_area_ids ||= stop_points.map(&:stop_area_id)
      end

      def filter_line_routing_constraint_zones
        line_routing_constraint_zones.select do |line_routing_constraint_zone|
          (line_routing_constraint_zone.stop_area_ids & stop_area_ids).many?
        end
      end

      def routing_constraint_zones
        filter_line_routing_constraint_zones.map do |line_routing_constraint_zone|
          LineRoutingConstraintZoneDecorator.new(line_routing_constraint_zone, self).netex_resource
        end
      end

      class LineRoutingConstraintZoneDecorator < SimpleDelegator
        delegate :line, to: :route

        attr_accessor :route

        def initialize(line_routing_constraint_zone, route)
          super line_routing_constraint_zone
          @route = route
        end

        def netex_attributes
          {
            id: Netex::ObjectId.merge(route.objectid, id, type: "RoutingConstraintZone"),
            name: name,
            members: scheduled_stop_point_refs,
            lines: line_refs,
            zone_use: zone_use
          }
        end

        def zone_use
          "cannotBoardAndAlightInSameZone"
        end

        def line_refs
          [Netex::Reference.new(line.objectid, type: 'LineRef')]
        end

        def stop_points
          route.stop_points.select { |stop_point| stop_area_ids.include? stop_point.stop_area_id }
        end

        def scheduled_stop_point_refs
          stop_points.map do |stop_point|
            StopPointDecorator.new(stop_point).scheduled_stop_point_ref
          end
        end

        def netex_resource
          Netex::RoutingConstraintZone.new netex_attributes
        end
      end
    end

  end

  class StopPoints < Part

    delegate :stop_points, to: :export_scope

    def export!
      stop_points.joins(:route, :stop_area).select(selected).find_each_light do |stop_point|
        tags = resource_tagger.tags_for(stop_point.line_id)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_stop_point = StopPointDecorator.new(stop_point)
        tagged_target << decorated_stop_point.scheduled_stop_point
        tagged_target << decorated_stop_point.passenger_stop_assignment
        tagged_target << decorated_stop_point.route_point
      end
    end

    private

    def selected
      [
        'stop_points.*',
        'stop_areas.objectid AS stop_area_objectid',
        'stop_areas.area_type AS stop_area_area_type',
        'routes.line_id AS line_id',
        'routes.data_source_ref AS route_data_source_ref',
      ]
    end
  end

  class RoutingConstraintZones < Part

    delegate :routing_constraint_zones, to: :export_scope

    def export!
      routing_constraint_zones.includes(route: :line).find_each do |routing_constraint_zone|
        tags = resource_tagger.tags_for(routing_constraint_zone.route.line_id)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_zone = Decorator.new(routing_constraint_zone)
        tagged_target << decorated_zone.netex_resource
      end
    end

    class Decorator < SimpleDelegator

      def netex_attributes
        {
          id: objectid,
          data_source_ref: data_source_ref,
          name: name,
          members: scheduled_stop_point_refs,
          lines: line_refs,
          zone_use: zone_use
        }
      end

      def scheduled_stop_point_refs
        decorated_stop_points.map(&:scheduled_stop_point_ref)
      end

      def line
        route&.line
      end

      def line_refs
        [ Netex::Reference.new(line.objectid, type: 'LineRef') ] if line
      end

      def netex_resource
        Netex::RoutingConstraintZone.new netex_attributes
      end

      def zone_use
        "cannotBoardAndAlightInSameZone"
      end

      def decorated_stop_points
        stop_points.map do |stop_point|
          StopPointDecorator.new stop_point
        end
      end
    end
  end

  class JourneyPatterns < Part

    delegate :journey_patterns, to: :export_scope

    def export!
      journey_patterns.includes(:route, stop_points: :stop_area).find_each do |journey_pattern|
        tags = resource_tagger.tags_for(journey_pattern.route.line_id)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_journey_pattern = Decorator.new(journey_pattern)
        # Export Destination Displays before the JourneyPattern to detect local reference
        tagged_target << decorated_journey_pattern.destination_display if journey_pattern.published_name.present?
        tagged_target << decorated_journey_pattern.netex_resource
      end
    end

    class Decorator < SimpleDelegator

      def netex_attributes
        {
          id: objectid,
          data_source_ref: data_source_ref,
          name: name,
          route_ref: route_ref,
          points_in_sequence: points_in_sequence
        }.tap do |attributes|
          attributes[:destination_display_ref] = destination_display_ref if published_name.present?
        end
      end

      def netex_resource
        Netex::ServiceJourneyPattern.new netex_attributes
      end

      def route_ref
        Netex::Reference.new(route.objectid, type: 'RouteRef')
      end

      def netex_identifier
        @netex_identifier ||= Netex::ObjectId.parse(objectid)
      end

      def destination_display_id
        netex_identifier.change(type: 'DestinationDisplay').to_s
      end

      def destination_display
        @destination_display ||= Netex::DestinationDisplay.new(
          id: destination_display_id,
          data_source_ref: data_source_ref,
          front_text: published_name
        )
      end

      def destination_display_ref
        Netex::Reference.new(destination_display_id, type: Netex::DestinationDisplay)
      end

      def points_in_sequence
        decorated_stop_points.map(&:stop_point_in_journey_pattern)
      end

      def decorated_stop_points
        @decorated_stop_points ||= stop_points.map do |stop_point|
          StopPointDecorator.new(stop_point, journey_pattern_id: objectid)
        end
      end
    end

  end

  class VehicleJourneyAtStops < Part
    def export!
      vehicle_journey_at_stops.find_each_light do |light_vehicle_journey_at_stop|
        decorated_vehicle_journey_at_stop = Decorator.new(light_vehicle_journey_at_stop)
        target << decorated_vehicle_journey_at_stop.netex_resource
      end
    end

    def vehicle_journey_at_stops
      export_scope.vehicle_journey_at_stops
        .joins(stop_point: :stop_area, vehicle_journey: { journey_pattern: :route })
        .order(:vehicle_journey_id, "stop_points.position": :asc)
        .select(
          "vehicle_journey_at_stops.*",
          "journey_patterns.objectid AS journey_pattern_objectid",
          "vehicle_journeys.objectid AS vehicle_journey_objectid",
          "stop_points.objectid AS stop_point_objectid",
          "stop_areas.time_zone AS time_zone",
        )
    end

    class Decorator < SimpleDelegator
      def netex_attributes
        {
          departure_time: stop_time_departure_time,
          arrival_time: stop_time_arrival_time,
          departure_day_offset: departure_day_offset,
          arrival_day_offset: arrival_day_offset,
          stop_point_in_journey_pattern_ref: stop_point_in_journey_pattern_ref,
        }
      end

      def netex_resource
        Netex::TimetabledPassingTime.new(netex_attributes).with_tag(parent_id: parent_id)
      end

      def netex_identifier
        @netex_identifier ||= Netex::ObjectId.parse(stop_point_objectid)
      end

      def parent_id
        vehicle_journey_objectid
      end

      def journey_pattern_objectid
        __getobj__.try(:journey_pattern_objectid) || journey_pattern&.objectid
      end

      def stop_point_objectid
        __getobj__.try(:stop_point_objectid) || stop_point&.objectid
      end

      def stop_point_in_journey_pattern_id
        StopPointDecorator.stop_point_in_journey_pattern_id(stop_point_objectid, journey_pattern_objectid)
      end

      def stop_point_in_journey_pattern_ref
        Netex::Reference.new(stop_point_in_journey_pattern_id, type: Netex::StopPointInJourneyPattern)
      end

      def netex_time time_of_day
        Netex::Time.new time_of_day.hour, time_of_day.minute, time_of_day.second
      end

      def stop_time_arrival_time
        netex_time arrival_local_time_of_day if arrival_local_time_of_day
      end

      def stop_time_departure_time
        netex_time departure_local_time_of_day if departure_local_time_of_day
      end
    end
  end

  class VehicleJourneys < Part
    def export!
      vehicle_journeys.find_each(batch_size: 10_000) do |vehicle_journey|
        tags = resource_tagger.tags_for(vehicle_journey.line_id)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_vehicle_journey = Decorator.new(vehicle_journey, code_space_keys)
        tagged_target << decorated_vehicle_journey.netex_resource
      end
    end

    def vehicle_journeys
      Query.new(export_scope.vehicle_journeys).scope
    end

    def code_space_keys
      @code_space_keys ||= workgroup.code_spaces.pluck(:id, :short_name).to_h
    end

    class Query

      def initialize(vehicle_journeys)
        @vehicle_journeys = vehicle_journeys
      end
      attr_accessor :vehicle_journeys

      def scope
        scope = vehicle_journeys.joins(journey_pattern: :route).select(selected)

        scope = scope.left_joins(:codes).select(vehicle_journey_codes).group(group_by)
        scope.joins(:time_tables).select(time_table_objectids)
      end

      private

      def selected
        <<~SQL
          vehicle_journeys.*,
          routes.line_id AS line_id,
          journey_patterns.objectid AS journey_pattern_objectid
        SQL
      end

      def time_table_objectids
        <<~SQL
          array_agg(time_tables.objectid) AS time_table_objectids
        SQL
      end

      def vehicle_journey_codes
        <<~SQL
          array_agg(
            jsonb_build_object(
              'id', referential_codes.code_space_id,
              'value', referential_codes.value
            )
          ) AS vehicle_journey_codes
        SQL
      end

      def group_by
        <<~SQL
          vehicle_journeys.id,
          routes.line_id,
          journey_patterns.objectid
        SQL
      end
    end

    class Decorator < SimpleDelegator

      def initialize(vehicle_journey, code_space_keys = nil)
        super vehicle_journey
        @code_space_keys = code_space_keys
      end
      attr_accessor :code_space_keys

      def netex_attributes
        {
          id: objectid,
          data_source_ref: data_source_ref,
          name: published_journey_name,
          journey_pattern_ref: journey_pattern_ref,
          public_code: published_journey_identifier,
          day_types: day_types
        }.tap do |attributes|
          if netex_alternate_identifiers
            attributes[:key_list] = netex_alternate_identifiers
          end
        end
      end

      def netex_resource
        Netex::ServiceJourney.new netex_attributes
      end

      def code_space_key(code_space_id)
        code_space_keys[code_space_id]
      end

      def netex_alternate_identifiers
        return if code_space_keys.blank? || try(:vehicle_journey_codes).blank?

        # Avoid duplicated vehicle_journey_codes
        uniq_vehicle_journey_codes.map do |vehicle_journey_code|
          Netex::KeyValue.new({
                                key: code_space_key(vehicle_journey_code['id'].to_i),
                                value: vehicle_journey_code['value'],
                                type_of_key: 'ALTERNATE_IDENTIFIER'
                              })
        end
      end

      def uniq_vehicle_journey_codes
        try(:vehicle_journey_codes)&.uniq { |c| [c['id'], c['value']] } || []
      end

      def journey_pattern_ref
        Netex::Reference.new(journey_pattern_objectid, type: 'JourneyPatternRef')
      end

      def journey_pattern_objectid
        __getobj__.try(:journey_pattern_objectid) || journey_pattern&.objectid
      end

      def day_types
        objectids = try(:time_table_objectids) || time_tables.pluck(:objectid)
        objectids.map do |objectid|
          Netex::Reference.new(objectid, type: 'DayTypeRef')
        end
      end
    end
  end

  class VehicleJourneyStopAssignments < Part
    def export!
      vehicle_journey_at_stops.find_each do |vehicle_journey_at_stop|
        tags = resource_tagger.tags_for(vehicle_journey_at_stop.line_id)
        tagged_target = TaggedTarget.new(target, tags)

        netex_resource = Decorator.new(vehicle_journey_at_stop).netex_resource
        tagged_target << netex_resource
      end
    end

    def vehicle_journey_at_stops
      export_scope.vehicle_journey_at_stops.where.not(stop_area: nil)
                  .joins(:stop_point, :stop_area, vehicle_journey: :route)
                  .select(*selected_columns)
    end

    def selected_columns
      ['vehicle_journey_at_stops.*',
       'vehicle_journeys.objectid AS vehicle_journey_objectid',
       "COALESCE(vehicle_journeys.data_source_ref, 'none') AS vehicle_journey_data_source_ref",
       'stop_points.objectid AS stop_point_objectid',
       'stop_areas.objectid AS stop_area_objectid',
       'stop_points.position AS stop_point_position',
       'routes.line_id as line_id'
      ]
    end

    class Decorator < SimpleDelegator
      def netex_attributes
        {
          id: objectid,
          data_source_ref: vehicle_journey_data_source_ref,
          scheduled_stop_point_ref: scheduled_stop_point_ref,
          stop_place_ref: stop_place_ref,
          quay_ref: quay_ref,
          vehicle_journey_refs: vehicle_journey_refs
        }
      end

      def netex_resource
        Netex::VehicleJourneyStopAssignment.new(netex_attributes)
      end

      def objectid
        Netex::ObjectId.merge(vehicle_journey_objectid, stop_point_position, type: 'VehicleJourneyStopAssignment').to_s
      end

      def stop_point_position
        __getobj__.try(:stop_point_position) || stop_point&.position
      end

      def stop_point_objectid
        __getobj__.try(:stop_point_objectid) || stop_point&.objectid
      end

      def stop_area_objectid
        __getobj__.try(:stop_area_objectid) || stop_area&.objectid
      end

      def vehicle_journey_objectid
        __getobj__.try(:vehicle_journey_objectid) || vehicle_journey&.objectid
      end

      def vehicle_journey_data_source_ref
        loaded_value = __getobj__.try(:vehicle_journey_data_source_ref)
        return nil if loaded_value == 'none'

        loaded_value || vehicle_journey&.data_source_ref
      end

      def scheduled_stop_point_ref
        Netex::Reference.new(stop_point_objectid, type: 'ScheduledStopPointRef')
      end

      def stop_place_ref
        Netex::Reference.new(stop_area_objectid, type: 'StopPlaceRef')
      end

      def quay_ref
        Netex::Reference.new(stop_area_objectid, type: 'QuayRef')
      end

      def vehicle_journey_refs
        [Netex::Reference.new(vehicle_journey_objectid, type: 'ServiceJourney')]
      end
    end
  end

  class PeriodDecorator < SimpleDelegator

    attr_accessor :day_type_ref, :time_table
    def initialize(period, day_type_ref)
      super period
      @day_type_ref = day_type_ref

      @time_table = period.time_table
    end

    def netex_identifier
      @netex_identifier ||= Netex::ObjectId.parse(time_table.objectid)
    end

    def operating_period
      Netex::OperatingPeriod.new operating_period_attributes
    end

    def operating_period_id
      netex_identifier.merge(id, type: 'OperatingPeriod').to_s
    end

    def operating_period_attributes
      {
        id: operating_period_id,
        data_source_ref: time_table.data_source_ref,
        from_date: period_start,
        to_date: period_end
      }
    end

    def day_type_assignment_id
      netex_identifier.merge("p#{id}", type: 'DayTypeAssignment').to_s
    end

    def day_type_assignment
      Netex::DayTypeAssignment.new day_type_assignment_attributes
    end

    def day_type_assignment_attributes
      {
        id: day_type_assignment_id,
        data_source_ref: time_table.data_source_ref,
        operating_period_ref: operating_period_ref,
        day_type_ref: day_type_ref,
        order: 0
      }
    end

    def operating_period_ref
      Netex::Reference.new(operating_period_id, type: 'OperatingPeriodRef')
    end

  end

  class DateDecorator < SimpleDelegator

    attr_accessor :day_type_ref
    def initialize(date, day_type_ref)
      super date
      @day_type_ref = day_type_ref
      @time_table = date.time_table
    end

    def netex_identifier
      @netex_identifier ||= Netex::ObjectId.parse(time_table.objectid)
    end

    def day_type_assignment
      Netex::DayTypeAssignment.new day_type_assignment_attributes
    end

    def date_type_assignment_id
      netex_identifier.merge("d#{id}", type: 'DayTypeAssignment').to_s
    end

    def day_type_assignment_attributes
      {
        id: date_type_assignment_id,
        data_source_ref: time_table.data_source_ref,
        date: date,
        is_available: in_out,
        day_type_ref: day_type_ref,
        order: 0
      }
    end

  end

  class TimeTables < Part
    delegate :time_tables, to: :export_scope
    delegate :validity_period, to: :export_scope

    def export!
      time_tables.includes(:periods, :dates, :lines).find_each do |time_table|
        decorated_time_table = Decorator.new(time_table, validity_period)

        tags = resource_tagger.tags_for_lines(time_table.line_ids)
        tagged_target = TaggedTarget.new(target, tags)

        decorated_time_table.netex_resources.each do |resource|
          tagged_target << resource
        end
      end
    end

    class Decorator < SimpleDelegator

      def initialize(time_table, validity_period=nil)
        super time_table
        @validity_period = validity_period
      end
      attr_accessor :validity_period

      def netex_resources
        return [] unless day_type_assignment?

        [day_type, exported_periods, exported_dates].flatten
      end

      def day_type_assignment?
        decorated_dates.present? || decorated_periods.present?
      end

      def day_type
        Netex::DayType.new day_type_attributes
      end

      def day_type_attributes
        {
          id: objectid,
          data_source_ref: data_source_ref,
          name: comment,
          properties: properties
        }
      end

      def day_type_ref
        @day_type_ref ||= Netex::Reference.new(objectid, type: 'DayTypeRef')
      end

      def properties
        [Netex::PropertyOfDay.new(days_of_week: days_of_week)]
      end

      DAYS = %w{monday tuesday wednesday thursday friday saturday sunday}
      def days_of_week
        DAYS.map { |day| day.capitalize if send(day) }.compact.join(' ')
      end

      def exported_periods
        decorated_periods.map(&:operating_period) + decorated_periods.map(&:day_type_assignment)
      end

      def candidate_periods
        @candidate_periods ||= periods.select { |period| period.intersect?(validity_period) }
      end

      def decorated_periods
        @decorated_periods ||= candidate_periods.map do |period|
          PeriodDecorator.new(period, day_type_ref)
        end
      end

      def candidate_excluded_dates
        dates.select(&:excluded?).select do |date|
          candidate_periods.any? { |period| period.include? date.date }
        end
      end

      def candidate_included_dates
        dates.select(&:included?).select { |date| validity_period.include? date.date }
      end

      def candidate_dates
        candidate_excluded_dates + candidate_included_dates
      end

      def exported_dates
        decorated_dates.map(&:day_type_assignment)
      end

      def decorated_dates
        @decorated_dates ||= candidate_dates.map do |date|
          DateDecorator.new(date, day_type_ref)
        end
      end
    end
  end

  class Organisations < Part
    delegate :organisations, to: :export_scope

    def export!
      organisations.find_each do |o|
        target << Decorator.new(o).netex_resource
      end
    end

    class Decorator < SimpleDelegator

      def netex_resource
        Netex::GeneralOrganisation.new(id: code, name: name)
      end
    end
  end

end