diff options
| author | Xinhui | 2016-02-22 14:17:18 +0100 | 
|---|---|---|
| committer | Xinhui | 2016-02-22 14:17:18 +0100 | 
| commit | d49f47b4ac1db2cd88b96d830772bb7773924601 (patch) | |
| tree | 191c7b9cda9edf939792e7780df4e7460d685a4a /app | |
| parent | 633004afc5861a6e8158948ddfecd73bf4dd86a8 (diff) | |
| download | chouette-core-d49f47b4ac1db2cd88b96d830772bb7773924601.tar.bz2 | |
Merge model from ninoxe gem
Diffstat (limited to 'app')
48 files changed, 3179 insertions, 0 deletions
| diff --git a/app/models/chouette/access_link.rb b/app/models/chouette/access_link.rb new file mode 100644 index 000000000..b43dcfb7f --- /dev/null +++ b/app/models/chouette/access_link.rb @@ -0,0 +1,65 @@ +module Chouette +  class AccessLink < TridentActiveRecord +    # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +    self.primary_key = "id" + +    attr_accessor :access_link_type, :link_orientation_type, :link_key + +    belongs_to :access_point, :class_name => 'Chouette::AccessPoint' +    belongs_to :stop_area, :class_name => 'Chouette::StopArea' + +    validates_presence_of :name +    validates_presence_of :link_orientation + +    def self.nullable_attributes +      [:link_distance, :default_duration, :frequent_traveller_duration, :occasional_traveller_duration, +        :mobility_restricted_traveller_duration, :link_type] +    end + +    def access_link_type +      link_type && Chouette::ConnectionLinkType.new(link_type.underscore) +    end + +    def access_link_type=(access_link_type) +      self.link_type = (access_link_type ? access_link_type.camelcase : nil) +    end + +    @@access_link_types = nil +    def self.access_link_types +      @@access_link_types ||= Chouette::ConnectionLinkType.all +    end + +    def link_orientation_type +      link_orientation && Chouette::LinkOrientationType.new(link_orientation.underscore) +    end + +    def link_orientation_type=(link_orientation_type) +      self.link_orientation = (link_orientation_type ? link_orientation_type.camelcase : nil) +    end + +    @@link_orientation_types = nil +    def self.link_orientation_types +      @@link_orientation_types ||= Chouette::LinkOrientationType.all +    end + +    def geometry +      GeoRuby::SimpleFeatures::LineString.from_points( [ access_point.geometry, stop_area.geometry], 4326) if access_point.geometry and stop_area.geometry +    end + +    def link_key +      Chouette::AccessLink.build_link_key(access_point,stop_area,link_orientation_type) +    end +     +    def self.build_link_key(access_point,stop_area,link_orientation_type) +      if link_orientation_type == "access_point_to_stop_area" +        "A_#{access_point.id}-S_#{stop_area.id}" +      else   +        "S_#{stop_area.id}-A_#{access_point.id}" +      end +    end + +    def geometry_presenter +      Chouette::Geometry::AccessLinkPresenter.new self +    end +  end +end diff --git a/app/models/chouette/access_point.rb b/app/models/chouette/access_point.rb new file mode 100644 index 000000000..43c8e1b3a --- /dev/null +++ b/app/models/chouette/access_point.rb @@ -0,0 +1,162 @@ +require 'geokit' +require 'geo_ruby' + +class Chouette::AccessPoint < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" +  include Geokit::Mappable +  has_many :access_links, :dependent => :destroy +  belongs_to :stop_area +   +  attr_accessor :access_point_type +  attr_writer :coordinates +   +  validates_presence_of :name +  validates_presence_of :access_type + +  validates_presence_of :latitude, :if => :longitude +  validates_presence_of :longitude, :if => :latitude +  validates_numericality_of :latitude, :less_than_or_equal_to => 90, :greater_than_or_equal_to => -90, :allow_nil => true +  validates_numericality_of :longitude, :less_than_or_equal_to => 180, :greater_than_or_equal_to => -180, :allow_nil => true + +  validates_format_of :coordinates, :with => %r{\A *-?(0?[0-9](\.[0-9]*)?|[0-8][0-9](\.[0-9]*)?|90(\.[0]*)?) *\, *-?(0?[0-9]?[0-9](\.[0-9]*)?|1[0-7][0-9](\.[0-9]*)?|180(\.[0]*)?) *\Z}, :allow_nil => true, :allow_blank => true + +  def self.nullable_attributes +    [:street_name, :country_code, :comment, :long_lat_type, :zip_code, :city_name] +  end + +  before_save :coordinates_to_lat_lng + +  def combine_lat_lng +    if self.latitude.nil? || self.longitude.nil? +      "" +    else +      self.latitude.to_s+","+self.longitude.to_s  +    end +  end +   +  def coordinates +      @coordinates || combine_lat_lng +  end +   +  def coordinates_to_lat_lng +    if ! @coordinates.nil? +      if @coordinates.empty? +        self.latitude = nil +        self.longitude = nil +      else +        self.latitude = BigDecimal.new(@coordinates.split(",").first) +        self.longitude = BigDecimal.new(@coordinates.split(",").last) +      end +      @coordinates = nil  +    end +  end   + +  def to_lat_lng +    Geokit::LatLng.new(latitude, longitude) if latitude and longitude +  end + +  def geometry +    GeoRuby::SimpleFeatures::Point.from_lon_lat(longitude, latitude, 4326) if latitude and longitude +  end + +  def geometry=(geometry) +    geometry = geometry.to_wgs84 +    self.latitude, self.longitude, self.long_lat_type = geometry.lat, geometry.lng, "WGS84" +  end + +  def position  +    geometry +  end + +  def position=(position) +    position = nil if String === position && position == "" +    position = Geokit::LatLng.normalize(position), 4326 if String === position +    self.latitude = position.lat +    self.longitude = position.lng +  end + +  def default_position  +    stop_area.geometry or stop_area.default_position +  end + + +  def access_point_type +    access_type && Chouette::AccessPointType.new(access_type.underscore) +  end + +  def access_point_type=(access_point_type)    +    self.access_type = (access_point_type ? access_point_type.camelcase : nil) +  end + +  @@access_point_types = nil +  def self.access_point_types +    @@access_point_types ||= Chouette::AccessPointType.all.select do |access_point_type| +      access_point_type.to_i >= 0 +    end +  end + +  def generic_access_link_matrix +     matrix = Array.new +     hash = Hash.new +     access_links.each do |link| +        hash[link.link_key] = link +     end +     key=Chouette::AccessLink.build_link_key(self,stop_area,"access_point_to_stop_area") +     if hash.has_key?(key) +       matrix << hash[key] +     else +       link = Chouette::AccessLink.new +       link.access_point = self +       link.stop_area = stop_area +       link.link_orientation_type = "access_point_to_stop_area" +       matrix << link +     end +     key=Chouette::AccessLink.build_link_key(self,stop_area,"stop_area_to_access_point") +     if hash.has_key?(key) +       matrix << hash[key] +     else +       link = Chouette::AccessLink.new +       link.access_point = self +       link.stop_area = stop_area +       link.link_orientation_type = "stop_area_to_access_point" +       matrix << link +     end +     matrix +  end + +  def detail_access_link_matrix +     matrix = Array.new +     hash = Hash.new +     access_links.each do |link| +        hash[link.link_key] = link +     end +     stop_area.children_at_base.each do |child| +       key=Chouette::AccessLink.build_link_key(self,child,"access_point_to_stop_area") +       if hash.has_key?(key) +         matrix << hash[key] +       else +         link = Chouette::AccessLink.new +         link.access_point = self +         link.stop_area = child +         link.link_orientation_type = "access_point_to_stop_area" +         matrix << link +       end +       key=Chouette::AccessLink.build_link_key(self,child,"stop_area_to_access_point") +       if hash.has_key?(key) +         matrix << hash[key] +       else +         link = Chouette::AccessLink.new +         link.access_point = self +         link.stop_area = child +         link.link_orientation_type = "stop_area_to_access_point" +         matrix << link +       end +     end +     matrix +  end + +  def geometry_presenter +    Chouette::Geometry::AccessPointPresenter.new self +  end +end diff --git a/app/models/chouette/access_point_type.rb b/app/models/chouette/access_point_type.rb new file mode 100644 index 000000000..94d28e5ae --- /dev/null +++ b/app/models/chouette/access_point_type.rb @@ -0,0 +1,50 @@ +class Chouette::AccessPointType < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    camelize +  end + +  @@definitions = [ +    ["in", 0], +    ["out", 1], +    ["in_out", 2] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end diff --git a/app/models/chouette/active_record.rb b/app/models/chouette/active_record.rb new file mode 100644 index 000000000..59052075e --- /dev/null +++ b/app/models/chouette/active_record.rb @@ -0,0 +1,66 @@ +#require "active_record" +require 'deep_cloneable' +module Chouette +  class ActiveRecord < ::ActiveRecord::Base + +    self.abstract_class = true + +    before_save :nil_if_blank + +    # to be overrided to set nullable attrs when empty +    def self.nullable_attributes +      [] +    end + +    def nil_if_blank +      self.class.nullable_attributes.each { |attr| self[attr] = nil if self[attr].blank? } +    end + + +    def human_attribute_name(*args) +      self.class.human_attribute_name(*args) +    end + +    # class << self +    #   alias_method :create_reflection_without_chouette_naming, :create_reflection + +    #   def create_reflection(macro, name, options, active_record) +    #     options =  +    #       Reflection.new(macro, name, options, active_record).options_with_default + +    #     create_reflection_without_chouette_naming(macro, name, options, active_record) +    #   end +    # end + + + +    # class Reflection + +    #   attr_reader :macro, :name, :options, :active_record + +    #   def initialize(macro, name, options, active_record) +    #     @macro, @name, @options, @active_record = macro, name.to_s, options, active_record +    #   end + +    #   def collection? +    #     macro == :has_many +    #   end + +    #   def singular_name +    #     collection? ? name.singularize : name +    #   end + +    #   def class_name +    #     "Chouette::#{singular_name.camelize}" +    #   end + +    #   def options_with_default +    #     options.dup.tap do |options| +    #       options[:class_name] ||= class_name +    #     end +    #   end + +    # end + +  end +end diff --git a/app/models/chouette/area_type.rb b/app/models/chouette/area_type.rb new file mode 100644 index 000000000..af614dc55 --- /dev/null +++ b/app/models/chouette/area_type.rb @@ -0,0 +1,56 @@ +class Chouette::AreaType < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    if (to_s == 'itl')  +      to_s.upcase +    else +      camelize +    end +  end + +  @@definitions = [ +    ["boarding_position", 0], +    ["quay", 1], +    ["commercial_stop_point", 2], +    ["stop_place", 3], +    ["itl", 4] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end diff --git a/app/models/chouette/command.rb b/app/models/chouette/command.rb new file mode 100644 index 000000000..d2995a000 --- /dev/null +++ b/app/models/chouette/command.rb @@ -0,0 +1,94 @@ +require 'tmpdir' + +#if RUBY_PLATFORM == "java" +  # FIXME disable remove_entry_secure because incompatible with jruby ?!  +  # See http://jira.codehaus.org/browse/JRUBY-4082 +  module FileUtils +    def self.remove_entry_secure(*args) +      self.remove_entry *args +    end +  end +#end + +class Chouette::Command + +  include Chouette::CommandLineSupport + +  @@command = "chouette" +  cattr_accessor :command + +  attr_accessor :database, :schema, :host, :user, :password, :port + +  def initialize(options = {}) +    database_options_from_active_record.merge(options).each do |k,v| +      send "#{k}=", v +    end +  end + +  def database_options_from_active_record +    config = Chouette::ActiveRecord.connection_pool.spec.config +    {  +      :database => config[:database],  +      :user => config[:username], +      :password => config[:password], +      :port => config[:port], +      :host => (config[:host] or "localhost") +    } +  end + + +  def run!(options = {}) +    Dir.mktmpdir do |config_dir| +      chouette_properties = File.join(config_dir, "chouette.properties") +      open(chouette_properties, "w") do |f| +        f.puts "database.name = #{database}" +        f.puts "database.schema = #{schema}" +        #f.puts "database.showsql = true" +        f.puts "hibernate.username = #{user}" +        f.puts "hibernate.password = #{password}" +        f.puts "jdbc.url=jdbc:postgresql://#{host}:#{port}/#{database}" +        f.puts "jdbc.username = #{user}" +        f.puts "jdbc.password = #{password}" +        #f.puts "database.hbm2ddl.auto=update" +      end + +      logger.debug "Chouette properties: #{File.readlines(chouette_properties).collect(&:strip).join(', ')}" + +      command_line = "#{command} -classpath #{config_dir} #{command_options(options)}" +      logger.debug "Execute '#{command_line}'" + +      execute! command_line +    end +  end + +  class Option + +    attr_accessor :key, :value + +    def initialize(key, value) +      @key, @value = key.to_s, value +    end + +    def command_key +      key.camelize(:lower)  +    end + +    def to_s +      unless value == true +        "-#{command_key} #{value}" +      else +        "-#{command_key}" +      end +    end + +  end + +  def command_options(options) +    options.collect do |key, value| +      Option.new(key, value) +    end.sort_by(&:key).join(' ') +  end + +   + +end diff --git a/app/models/chouette/command_line_support.rb b/app/models/chouette/command_line_support.rb new file mode 100644 index 000000000..99c61fa5b --- /dev/null +++ b/app/models/chouette/command_line_support.rb @@ -0,0 +1,35 @@ +module Chouette::CommandLineSupport + +  class ExecutionError < StandardError; end + +  def available_loggers +    [].tap do |logger| +      logger << Chouette::ActiveRecord.logger   +      logger << Rails.logger if defined?(Rails) +      logger << Logger.new($stdout) +    end.compact +  end + +  def logger +    @logger ||= available_loggers.first +  end + +  def max_output_length +    2000 +  end + +  def execute!(command) +    logger.debug "execute '#{command}'" + +    output = `#{command} 2>&1` +    output = "[...] #{output[-max_output_length,max_output_length]}" if output.length > max_output_length +    logger.info output unless output.empty? + +    if $? != 0 +      raise ExecutionError.new("Command failed: #{command} (error code #{$?})") +    end + +    true +  end + +end diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb new file mode 100644 index 000000000..d0375b2e6 --- /dev/null +++ b/app/models/chouette/company.rb @@ -0,0 +1,14 @@ +class Chouette::Company < Chouette::TridentActiveRecord +  has_many :lines + +  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 + +  def self.nullable_attributes +    [:organizational_unit, :operating_department_name, :code, :phone, :fax, :email, :url, :time_zone] +  end + + +end + diff --git a/app/models/chouette/connection_link.rb b/app/models/chouette/connection_link.rb new file mode 100644 index 000000000..045f7c1d9 --- /dev/null +++ b/app/models/chouette/connection_link.rb @@ -0,0 +1,47 @@ +class Chouette::ConnectionLink < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  attr_accessor :connection_link_type + +  belongs_to :departure, :class_name => 'Chouette::StopArea' +  belongs_to :arrival, :class_name => 'Chouette::StopArea' + +  validates_presence_of :name + +  def self.nullable_attributes +    [:link_distance, :default_duration, :frequent_traveller_duration, :occasional_traveller_duration, +      :mobility_restricted_traveller_duration, :link_type] +  end + +  def connection_link_type +    link_type && Chouette::ConnectionLinkType.new( link_type.underscore) +  end + +  def connection_link_type=(connection_link_type) +    self.link_type = (connection_link_type ? connection_link_type.camelcase : nil) +  end + +  @@connection_link_types = nil +  def self.connection_link_types +    @@connection_link_types ||= Chouette::ConnectionLinkType.all +  end + +  def possible_areas +    Chouette::StopArea.where("area_type != 'ITL'") +  end + +  def stop_areas +    Chouette::StopArea.where(:id => [self.departure_id,self.arrival_id]) +  end + +  def geometry +    GeoRuby::SimpleFeatures::LineString.from_points( [ departure.geometry, arrival.geometry], 4326) if departure.geometry and arrival.geometry +  end + +  def geometry_presenter +    Chouette::Geometry::ConnectionLinkPresenter.new self +  end + +end + diff --git a/app/models/chouette/connection_link_type.rb b/app/models/chouette/connection_link_type.rb new file mode 100644 index 000000000..41635f48c --- /dev/null +++ b/app/models/chouette/connection_link_type.rb @@ -0,0 +1,51 @@ +class Chouette::ConnectionLinkType < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    camelize +  end + +  @@definitions = [ +    ["underground", 0], +    ["mixed", 1], +    ["overground", 2] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end + diff --git a/app/models/chouette/direction.rb b/app/models/chouette/direction.rb new file mode 100644 index 000000000..93bc1318b --- /dev/null +++ b/app/models/chouette/direction.rb @@ -0,0 +1,60 @@ +class Chouette::Direction < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    to_s +  end + +  @@definitions = [ +    ["straight_forward", 0], +    ["backward", 1], +    ["clock_wise", 2], +    ["counter_clock_wise", 3], +    ["north", 4], +    ["north_west", 5], +    ["west", 6], +    ["south_west", 7], +    ["south", 8], +    ["south_east", 9], +    ["east", 10], +    ["north_east", 11] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end + diff --git a/app/models/chouette/exporter.rb b/app/models/chouette/exporter.rb new file mode 100644 index 000000000..df85a9dde --- /dev/null +++ b/app/models/chouette/exporter.rb @@ -0,0 +1,32 @@ +class Chouette::Exporter + +  attr_reader :schema + +  def initialize(schema) +    @schema = schema +  end + +  def chouette_command +    @chouette_command ||= Chouette::Command.new(:schema => schema) +  end + +  def export(file, options = {}) +    options = { +      :format => :neptune +    }.merge(options) + +    command_options = { +      :c => "export",  +      :o => "line",  +      :format => options.delete(:format).to_s.upcase,  +      :output_file => File.expand_path(file),  +      :optimize_memory => true +    }.merge(options) + +    logger.info "Export #{file} in schema #{schema}" +    chouette_command.run! command_options +  end + +  include Chouette::CommandLineSupport + +end diff --git a/app/models/chouette/file_validator.rb b/app/models/chouette/file_validator.rb new file mode 100644 index 000000000..513648a62 --- /dev/null +++ b/app/models/chouette/file_validator.rb @@ -0,0 +1,47 @@ +class Chouette::FileValidator + +  attr_reader :schema, :database, :user, :password, :host + +  def initialize(schema) +    @schema = schema + +    Chouette::ActiveRecord.connection_pool.spec.config.tap do |config| +      @database = config[:database] +      @user = config[:username] +      @password = config[:password] +      @host = (config[:host] or "localhost") +    end +  end + +  def self.chouette_command=(command) +    Chouette::Command.command = command +  end + +  class << self +    deprecate :chouette_command= => "Use Chouette::Command.command =" +  end + +  def chouette_command +    @chouette_command ||= Chouette::Command.new(:schema => schema) +  end + +  def validate(file, options = {}) +    options = { +      :format => :neptune +    }.merge(options) + +    command_options = { +      :c => "validate",  +      :o => "line",  +      :input_file => File.expand_path(file),  +      :optimize_memory => true +    }.merge(options) + +    logger.info "Validate #{file}" +    chouette_command.run! command_options +  end + + +  include Chouette::CommandLineSupport + +end diff --git a/app/models/chouette/footnote.rb b/app/models/chouette/footnote.rb new file mode 100644 index 000000000..de427b249 --- /dev/null +++ b/app/models/chouette/footnote.rb @@ -0,0 +1,6 @@ +class Chouette::Footnote < Chouette::ActiveRecord +  belongs_to :line, inverse_of: :footnotes +  has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney' + +  validates_presence_of :line +end diff --git a/app/models/chouette/for_alighting_enumerations.rb b/app/models/chouette/for_alighting_enumerations.rb new file mode 100644 index 000000000..4f34927d3 --- /dev/null +++ b/app/models/chouette/for_alighting_enumerations.rb @@ -0,0 +1,8 @@ +module Chouette +  module ForAlightingEnumerations +    extend Enumerize +    extend ActiveModel::Naming +     +    enumerize :for_alighting, in: %w[normal forbidden request_stop is_flexible] +  end +end diff --git a/app/models/chouette/for_boarding_enumerations.rb b/app/models/chouette/for_boarding_enumerations.rb new file mode 100644 index 000000000..48f8762c2 --- /dev/null +++ b/app/models/chouette/for_boarding_enumerations.rb @@ -0,0 +1,8 @@ +module Chouette +  module ForBoardingEnumerations +    extend Enumerize +    extend ActiveModel::Naming + +    enumerize :for_boarding, in: %w[normal forbidden request_stop is_flexible] +  end +end diff --git a/app/models/chouette/group_of_line.rb b/app/models/chouette/group_of_line.rb new file mode 100644 index 000000000..1c1ae5f4c --- /dev/null +++ b/app/models/chouette/group_of_line.rb @@ -0,0 +1,28 @@ +class Chouette::GroupOfLine < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" +   +  has_and_belongs_to_many :lines, :class_name => 'Chouette::Line', :order => 'lines.name' + +  validates_presence_of :name + +  attr_reader :line_tokens +   +  def self.nullable_attributes +    [:comment] +  end +   +  def commercial_stop_areas +    Chouette::StopArea.joins(:children => [:stop_points => [:route => [:line => :group_of_lines] ] ]).where(:group_of_lines => {:id => self.id}).uniq +  end + +  def stop_areas +    Chouette::StopArea.joins(:stop_points => [:route => [:line => :group_of_lines] ]).where(:group_of_lines => {:id => self.id}) +  end +   +  def line_tokens=(ids) +    self.line_ids = ids.split(",") +  end + +end + diff --git a/app/models/chouette/journey_frequency.rb b/app/models/chouette/journey_frequency.rb new file mode 100644 index 000000000..45b8aea8c --- /dev/null +++ b/app/models/chouette/journey_frequency.rb @@ -0,0 +1,36 @@ +module Chouette + +  class JourneyFrequencyValidator < ActiveModel::Validator +    def validate(record) +      timeband = record.timeband +      if timeband +        first_departure_time = record.first_departure_time.utc.strftime( "%H%M%S%N" ) +        last_departure_time  = record.last_departure_time.utc.strftime( "%H%M%S%N" ) +        timeband_start_time  = timeband.start_time.utc.strftime( "%H%M%S%N" ) +        timeband_end_time    = timeband.end_time.utc.strftime( "%H%M%S%N" ) + +        unless first_departure_time.between? timeband_start_time, timeband_end_time +          record.errors[:first_departure_time] << I18n.t('activerecord.errors.models.journey_frequency.start_must_be_after_timeband') +        end +        unless last_departure_time.between? timeband_start_time, timeband_end_time +          record.errors[:last_departure_time] << I18n.t('activerecord.errors.models.journey_frequency.end_must_be_before_timeband') +        end +      end +      if record.first_departure_time == record.last_departure_time +        record.errors[:last_departure_time] << I18n.t('activerecord.errors.models.journey_frequency.end_must_be_different_from_first') +      end +      if record.scheduled_headway_interval.blank? || (record.scheduled_headway_interval.strftime( "%H%M%S%N" ) == Time.current.midnight.strftime( "%H%M%S%N" )) +        record.errors[:scheduled_headway_interval] << I18n.t('activerecord.errors.models.journey_frequency.scheduled_headway_interval_greater_than_zero') +      end +    end +  end + +  class JourneyFrequency < ActiveRecord +    belongs_to :vehicle_journey_frequency, foreign_key: 'vehicle_journey_id' +    belongs_to :timeband +    validates :first_departure_time, presence: true +    validates :last_departure_time,  presence: true +    validates :scheduled_headway_interval, presence: true +    validates_with JourneyFrequencyValidator +  end +end diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb new file mode 100644 index 000000000..d48733edb --- /dev/null +++ b/app/models/chouette/journey_pattern.rb @@ -0,0 +1,109 @@ +class Chouette::JourneyPattern < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  belongs_to :route +  has_many :vehicle_journeys, :dependent => :destroy +  has_many :vehicle_journey_at_stops, :through => :vehicle_journeys +  has_and_belongs_to_many :stop_points, -> { order("stop_points.position") }, :before_add => :vjas_add, :before_remove => :vjas_remove, :after_add => :shortcuts_update_for_add, :after_remove => :shortcuts_update_for_remove +  has_many :journey_pattern_sections +  has_many :route_sections, through: :journey_pattern_sections, dependent: :destroy + +  validates_presence_of :route + +  enum section_status: { todo: 0, completed: 1, control: 2 } + +  attr_accessor  :control_checked +  after_update :control_route_sections, :unless => "control_checked" + +  # TODO: this a workarround +  # otherwise, we loose the first stop_point +  # when creating a new journey_pattern +  def special_update +    bck_sp = self.stop_points.map {|s| s} +    self.update_attributes :stop_points => [] +    self.update_attributes :stop_points => bck_sp +  end + +  def departure_stop_point +    return unless departure_stop_point_id +    Chouette::StopPoint.find( departure_stop_point_id) +  end + +  def arrival_stop_point +    return unless arrival_stop_point_id +    Chouette::StopPoint.find( arrival_stop_point_id) +  end + +  def shortcuts_update_for_add( stop_point) +    stop_points << stop_point unless stop_points.include?( stop_point) + +    ordered_stop_points = stop_points +    ordered_stop_points = ordered_stop_points.sort { |a,b| a.position <=> b.position} unless ordered_stop_points.empty? + +    self.update_attributes( :departure_stop_point_id => (ordered_stop_points.first && ordered_stop_points.first.id), +                             :arrival_stop_point_id => (ordered_stop_points.last && ordered_stop_points.last.id)) +  end + +  def shortcuts_update_for_remove( stop_point) +    stop_points.delete( stop_point) if stop_points.include?( stop_point) + +    ordered_stop_points = stop_points +    ordered_stop_points = ordered_stop_points.sort { |a,b| a.position <=> b.position} unless ordered_stop_points.empty? + +    self.update_attributes( :departure_stop_point_id => (ordered_stop_points.first && ordered_stop_points.first.id), +                             :arrival_stop_point_id => (ordered_stop_points.last && ordered_stop_points.last.id)) +  end + +  def vjas_add( stop_point) +    return if new_record? + +    vehicle_journeys.each do |vj| +      vjas = vj.vehicle_journey_at_stops.create :stop_point_id => stop_point.id +    end +  end + +  def vjas_remove( stop_point) +    return if new_record? + +    vehicle_journey_at_stops.where( :stop_point_id => stop_point.id).each do |vjas| +      vjas.destroy +    end +  end + +  def control_route_sections +    stop_area_ids = self.stop_points.map(&:stop_area_id) +    control_route_sections_by_stop_areas(stop_area_ids) +  end + +  def control_route_sections_by_stop_areas(stop_area_ids) +    journey_pattern_section_all +    i = 0 +    to_control = false +    stop_area_ids.each_cons(2) do |a| +      jps = @route_sections_orders[i] +      i += 1 +      unless jps +        to_control = true +        next +      end +      unless [jps.route_section.departure.id, jps.route_section.arrival.id] == a +        jps.destroy +        to_control = true +      end +    end +    self.control_checked = true +    to_control ? self.control! : self.completed! +  end + +  protected + +  def journey_pattern_section_all +    @route_sections_orders = {} +    self.journey_pattern_sections.all.map do |journey_pattern_section| +      @route_sections_orders[journey_pattern_section.rank] = journey_pattern_section +    end +  end + +end + diff --git a/app/models/chouette/journey_pattern_section.rb b/app/models/chouette/journey_pattern_section.rb new file mode 100644 index 000000000..3ccba8ec0 --- /dev/null +++ b/app/models/chouette/journey_pattern_section.rb @@ -0,0 +1,20 @@ +class Chouette::JourneyPatternSection < Chouette::ActiveRecord +  belongs_to :journey_pattern +  belongs_to :route_section + +  validates :journey_pattern_id, presence: true +  validates :route_section_id, presence: true +  validates :rank, presence: true, numericality: :only_integer +  validates :journey_pattern_id, uniqueness: { scope: [:route_section_id, :rank] } + +  default_scope { order(:rank) } + +  def self.update_by_journey_pattern_rank(journey_pattern_id, route_section_id, rank) +    jps = self.find_or_initialize_by(journey_pattern_id: journey_pattern_id, rank: rank) +    if route_section_id.present? +      jps.update_attributes(route_section_id: route_section_id) +    else +      jps.destroy +    end +  end +end diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb new file mode 100644 index 000000000..d69203233 --- /dev/null +++ b/app/models/chouette/line.rb @@ -0,0 +1,74 @@ +class Chouette::Line < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  belongs_to :company +  belongs_to :network +  has_many :routes, :dependent => :destroy +  has_many :journey_patterns, :through => :routes +  has_many :vehicle_journeys, :through => :journey_patterns + +  has_and_belongs_to_many :group_of_lines, :class_name => 'Chouette::GroupOfLine', :order => 'group_of_lines.name' + +  has_many :footnotes, :inverse_of => :line, :validate => :true, :dependent => :destroy +  accepts_nested_attributes_for :footnotes, :reject_if => :all_blank, :allow_destroy => true + +  attr_reader :group_of_line_tokens +  attr_accessor :transport_mode + +  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 :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 +  validates_format_of :text_color, :with => %r{\A[0-9a-fA-F]{6}\Z}, :allow_nil => true, :allow_blank => true + +  validates_presence_of :name + +  def self.nullable_attributes +    [:published_name, :number, :comment, :url, :color, :text_color, :stable_id] +  end + +  def geometry_presenter +    Chouette::Geometry::LinePresenter.new self +  end + +  def transport_mode +    # return nil if transport_mode_name is nil +    transport_mode_name && Chouette::TransportMode.new( transport_mode_name.underscore) +  end + +  def transport_mode=(transport_mode) +    self.transport_mode_name = (transport_mode ? transport_mode.camelcase : nil) +  end + +  @@transport_modes = nil +  def self.transport_modes +    @@transport_modes ||= Chouette::TransportMode.all.select do |transport_mode| +      transport_mode.to_i > 0 +    end +  end + +  def commercial_stop_areas +    Chouette::StopArea.joins(:children => [:stop_points => [:route => :line] ]).where(:lines => {:id => self.id}).uniq +  end + +  def stop_areas +    Chouette::StopArea.joins(:stop_points => [:route => :line]).where(:lines => {:id => self.id}) +  end + +  def stop_areas_last_parents +    Chouette::StopArea.joins(:stop_points => [:route => :line]).where(:lines => {:id => self.id}).collect(&:root).flatten.uniq +  end + +  def group_of_line_tokens=(ids) +    self.group_of_line_ids = ids.split(",") +  end + +  def vehicle_journey_frequencies? +    self.vehicle_journeys.unscoped.where(journey_category: 1).count > 0 +  end + +end diff --git a/app/models/chouette/link_orientation_type.rb b/app/models/chouette/link_orientation_type.rb new file mode 100644 index 000000000..ec279aba3 --- /dev/null +++ b/app/models/chouette/link_orientation_type.rb @@ -0,0 +1,49 @@ +class Chouette::LinkOrientationType < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    camelize +  end + +  @@definitions = [ +    ["access_point_to_stop_area", 0], +    ["stop_area_to_access_point", 1] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end diff --git a/app/models/chouette/loader.rb b/app/models/chouette/loader.rb new file mode 100644 index 000000000..40a2be4ee --- /dev/null +++ b/app/models/chouette/loader.rb @@ -0,0 +1,110 @@ +class Chouette::Loader + +  attr_reader :schema, :database, :user, :password, :host + +  def initialize(schema) +    @schema = schema + +    Chouette::ActiveRecord.connection_pool.spec.config.tap do |config| +      @database = config[:database] +      @user = config[:username] +      @password = config[:password] +      @host = (config[:host] or "localhost") +    end +  end + +  # Load dump where datas are in schema 'chouette' +  def load_dump(file) +    logger.info "Load #{file} in schema #{schema}" +    with_pg_password do +      execute!("sed -e 's/ chouette/ \"#{schema}\"/' -e 's/ OWNER TO .*;/ OWNER TO #{user};/' #{file} | psql #{pg_options} --set ON_ERROR_ROLLBACK=1 --set ON_ERROR_STOP=1") +    end +    self +  end + +  def self.chouette_command=(command) +    Chouette::Command.command = command +  end + +  class << self +    deprecate :chouette_command= => "Use Chouette::Command.command =" +  end + +  def chouette_command +    @chouette_command ||= Chouette::Command.new(:schema => schema) +  end + +  def import(file, options = {}) +    options = { +      :format => :neptune +    }.merge(options) + +    command_options = { +      :c => "import",  +      :o => "line",  +      :format => options.delete(:format).to_s.upcase,  +      :input_file => File.expand_path(file),  +      :optimize_memory => true +    }.merge(options) + +    logger.info "Import #{file} in schema #{schema}" +    chouette_command.run! command_options +  end + +  def backup(file) +    logger.info "Backup schema #{schema} in #{file}" + +    with_pg_password do +      execute!("pg_dump -n #{schema} -f #{file} #{pg_options}") +    end + +    self +  end + +  def pg_options +    [].tap do |options| +      options << "-U #{user}" if user +      options << "-h #{host}" if host +      options << database +    end.join(" ") +  end + +  def create +    logger.info "Create schema #{schema}" +    with_pg_password do +      execute!("psql -c 'CREATE SCHEMA \"#{schema}\";' #{pg_options}") +    end +    self +  end + +  def drop +    logger.info "Drop schema #{schema}" +    with_pg_password do +      execute!("psql -c 'DROP SCHEMA \"#{schema}\" CASCADE;' #{pg_options}") +    end +    self +  end + +  def with_pg_password(&block) +    ENV['PGPASSWORD'] = password.to_s if password +    begin +      yield +    ensure +      ENV['PGPASSWORD'] = nil +    end +  end + +  @@binarisation_command = "binarisation" +  cattr_accessor :binarisation_command + +  def binarize(period, target_directory) +    # TODO check these computed daybefore/dayafter +    day_before = Date.today - period.begin +    day_after = period.end - period.begin + +    execute! "#{binarisation_command} --host=#{host} --dbname=#{database} --user=#{user} --password=#{password} --schema=#{schema} --daybefore=#{day_before} --dayafter=#{day_after} --targetdirectory=#{target_directory}" +  end + +  include Chouette::CommandLineSupport + +end diff --git a/app/models/chouette/network.rb b/app/models/chouette/network.rb new file mode 100644 index 000000000..a631d70ec --- /dev/null +++ b/app/models/chouette/network.rb @@ -0,0 +1,46 @@ +class Chouette::Network < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  has_many :lines + +  attr_accessor :source_type_name + +  validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true +  validates_presence_of :name + +  def self.object_id_key +    "PTNetwork" +  end + +  def self.nullable_attributes +    [:source_name, :source_type, :source_identifier, :comment] +  end + +  def commercial_stop_areas +    Chouette::StopArea.joins(:children => [:stop_points => [:route => [:line => :network] ] ]).where(:networks => {:id => self.id}).uniq +  end + +  def stop_areas +    Chouette::StopArea.joins(:stop_points => [:route => [:line => :network] ]).where(:networks => {:id => self.id}) +  end + +  def source_type_name +    # return nil if source_type is nil +    source_type && Chouette::SourceType.new( source_type.underscore) +  end + +  def source_type_name=(source_type_name) +    self.source_type = (source_type_name ? source_type_name.camelcase : nil) +  end + +  @@source_type_names = nil +  def self.source_type_names +    @@source_type_names ||= Chouette::SourceType.all.select do |source_type_name| +      source_type_name.to_i > 0 +    end +  end + + +end + diff --git a/app/models/chouette/object_id.rb b/app/models/chouette/object_id.rb new file mode 100644 index 000000000..4f58048e1 --- /dev/null +++ b/app/models/chouette/object_id.rb @@ -0,0 +1,36 @@ +class Chouette::ObjectId < String + +  def valid? +    parts.present? +  end +  alias_method :objectid?, :valid? + +  @@format = /^([0-9A-Za-z_]+):([A-Za-z]+):([0-9A-Za-z_-]+)$/  +  cattr_reader :format + +  def parts +    match(format).try(:captures) +  end + +  def system_id +    parts.try(:first) +  end + +  def object_type +    parts.try(:second) +  end + +  def local_id +    parts.try(:third) +  end +   +  def self.create(system_id, object_type, local_id) +    new [system_id, object_type, local_id].join(":") +  end + +  def self.new(string) +    string ||= "" +    self === string ? string : super +  end + +end diff --git a/app/models/chouette/pt_link.rb b/app/models/chouette/pt_link.rb new file mode 100644 index 000000000..8a4e368ea --- /dev/null +++ b/app/models/chouette/pt_link.rb @@ -0,0 +1,37 @@ +require 'geokit' + +class Chouette::PtLink < Chouette::ActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  include Geokit::Mappable + +  def geometry +    the_geom +  end + +  def self.import_csv +    csv_file = Rails.root + "chouette_pt_links.csv" +    if File.exists?( csv_file) +      csv = CSV::Reader.parse(File.read(csv_file)) + +      slug = csv.shift.first + +      Network::Base.find_by_slug( slug).tune_connection + +      csv.each do |row| +        origin = Chouette::StopArea.find_by_objectid( row[0]) +        destination = Chouette::StopArea.find_by_objectid( row[1]) + +        raise "unknown origin #{row[0]}" unless origin +        raise "unknown destination #{row[1]}" unless destination + +        Chouette::PtLink.create( :departure_id => origin.id, +                                 :arrival_id => destination.id, +                                 :the_geom => GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb( row[2])) +      end +    end +     +  end + +end diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb new file mode 100644 index 000000000..d5e39ac12 --- /dev/null +++ b/app/models/chouette/route.rb @@ -0,0 +1,183 @@ +class Chouette::Route < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  attr_accessor :wayback_code +  attr_accessor :direction_code + +  def self.nullable_attributes +    [:published_name, :comment, :number, :name] +  end + +  belongs_to :line + +  has_many :journey_patterns, :dependent => :destroy +  has_many :vehicle_journeys, :dependent => :destroy do +    def timeless +      Chouette::Route.vehicle_journeys_timeless(proxy_association.owner.journey_patterns.pluck( :departure_stop_point_id)) +    end +  end +  has_many :vehicle_journey_frequencies, :dependent => :destroy do +    # Todo : I think there is a better way to do this. +    def timeless +      Chouette::Route.vehicle_journeys_timeless(proxy_association.owner.journey_patterns.pluck( :departure_stop_point_id)) +    end +  end +  belongs_to :opposite_route, :class_name => 'Chouette::Route', :foreign_key => :opposite_route_id +  has_many :stop_points, -> { order('position ASC') }, :dependent => :destroy do +    def find_by_stop_area(stop_area) +      stop_area_ids = Integer === stop_area ? [stop_area] : (stop_area.children_in_depth + [stop_area]).map(&:id) +      where( :stop_area_id => stop_area_ids).first or +        raise ActiveRecord::RecordNotFound.new("Can't find a StopArea #{stop_area.inspect} in Route #{proxy_owner.id.inspect}'s StopPoints") +    end + +    def between(departure, arrival) +      between_positions = [departure, arrival].collect do |endpoint| +        case endpoint +        when Chouette::StopArea +          find_by_stop_area(endpoint).position +        when  Chouette::StopPoint +          endpoint.position +        when Integer +          endpoint +        else +          raise ActiveRecord::RecordNotFound.new("Can't determine position in route #{proxy_owner.id} with #{departure.inspect}") +        end +      end +      where(" position between ? and ? ", between_positions.first, between_positions.last) +    end +  end +  has_many :stop_areas, -> { order('stop_points.position ASC') }, :through => :stop_points do +    def between(departure, arrival) +      departure, arrival = [departure, arrival].collect do |endpoint| +        String === endpoint ? Chouette::StopArea.find_by_objectid(endpoint) : endpoint +      end +      proxy_owner.stop_points.between(departure, arrival).includes(:stop_area).collect(&:stop_area) +    end +  end +  accepts_nested_attributes_for :stop_points, :allow_destroy => :true + +  # validates_presence_of :name +  validates_presence_of :line +  # validates_presence_of :direction_code +  # validates_presence_of :wayback_code + +  before_destroy :dereference_opposite_route + +  after_commit :journey_patterns_control_route_sections + +  def geometry_presenter +    Chouette::Geometry::RoutePresenter.new self +  end + +  def dereference_opposite_route +    self.line.routes.each do |r| +      r.update_attributes( :opposite_route => nil) if r.opposite_route == self +    end +  end + +  def geometry +    points = stop_areas.map(&:to_lat_lng).compact.map do |loc| +      [loc.lng, loc.lat] +    end +    GeoRuby::SimpleFeatures::LineString.from_coordinates( points, 4326) +  end + +  def time_tables +    self.vehicle_journeys.joins(:time_tables).map(&:"time_tables").flatten.uniq +  end + +  def sorted_vehicle_journeys(journey_category_model) +    send(journey_category_model) +        .joins(:journey_pattern, :vehicle_journey_at_stops) +        .where("vehicle_journey_at_stops.stop_point_id=journey_patterns.departure_stop_point_id") +        .order( "vehicle_journey_at_stops.departure_time") +  end + +  def self.direction_binding +    { "A" => "straight_forward", +      "R" => "backward", +      "ClockWise" => "clock_wise", +      "CounterClockWise" => "counter_clock_wise", +      "North" => "north", +      "NorthWest" => "north_west", +      "West" => "west", +      "SouthWest" => "south_west", +      "South" => "south", +      "SouthEast" => "south_east", +      "East" => "east", +      "NorthEast" => "north_east"} +  end +  def direction_code +    return nil if self.class.direction_binding[direction].nil? +    Chouette::Direction.new( self.class.direction_binding[direction]) +  end +  def direction_code=(direction_code) +    self.direction = nil +    self.class.direction_binding.each do |k,v| +      self.direction = k if v==direction_code +    end +  end +  @@directions = nil +  def self.directions +    @@directions ||= Chouette::Direction.all +  end +  def self.wayback_binding +    { "A" => "straight_forward", "R" => "backward"} +  end +  def wayback_code +    return nil if self.class.wayback_binding[wayback].nil? +    Chouette::Wayback.new( self.class.wayback_binding[wayback]) +  end +  def wayback_code=(wayback_code) +    self.wayback = nil +    self.class.wayback_binding.each do |k,v| +      self.wayback = k if v==wayback_code +    end +  end +  @@waybacks = nil +  def self.waybacks +    @@waybacks ||= Chouette::Wayback.all +  end + +  def stop_point_permutation?( stop_point_ids) +    stop_points.map(&:id).map(&:to_s).sort == stop_point_ids.map(&:to_s).sort +  end + +  def reorder!( stop_point_ids) +    return false unless stop_point_permutation?( stop_point_ids) + +    stop_area_id_by_stop_point_id = {} +    stop_points.each do |sp| +      stop_area_id_by_stop_point_id.merge!( sp.id => sp.stop_area_id) +    end + +    reordered_stop_area_ids = [] +    stop_point_ids.each do |stop_point_id| +      reordered_stop_area_ids << stop_area_id_by_stop_point_id[ stop_point_id.to_i] +    end + +    stop_points.each_with_index do |sp, index| +      if sp.stop_area_id.to_s != reordered_stop_area_ids[ index].to_s +        #result = sp.update_attributes( :stop_area_id => reordered_stop_area_ids[ index]) +        sp.stop_area_id = reordered_stop_area_ids[ index] +        result = sp.save! +      end +    end + +    return true +  end + +  def journey_patterns_control_route_sections +    self.journey_patterns.each do |jp| +      jp.control_route_sections +    end +  end + +  protected + +  def self.vehicle_journeys_timeless(stop_point_id) +    all( :conditions => ['vehicle_journeys.id NOT IN (?)', Chouette::VehicleJourneyAtStop.where(stop_point_id: stop_point_id).pluck(:vehicle_journey_id)] ) +  end + +end diff --git a/app/models/chouette/route_section.rb b/app/models/chouette/route_section.rb new file mode 100644 index 000000000..99f1e776f --- /dev/null +++ b/app/models/chouette/route_section.rb @@ -0,0 +1,82 @@ +class Chouette::RouteSection < Chouette::TridentActiveRecord +  belongs_to :departure, class_name: 'Chouette::StopArea' +  belongs_to :arrival, class_name: 'Chouette::StopArea' +  has_many :journey_pattern_sections +  has_many :journey_patterns, through: :journey_pattern_sections, dependent: :destroy + +  validates :departure, :arrival, presence: true +  validates :processed_geometry, presence: true + +  scope :by_endpoint_name, ->(endpoint, name) do +    joins("INNER JOIN stop_areas #{endpoint} ON #{endpoint}.id = route_sections.#{endpoint}_id").where(["#{endpoint}.name ilike ?", "%#{name}%"]) +  end +  scope :by_line_id, ->(line_id) do +    joins(:journey_pattern_sections, :journey_patterns).joins('INNER JOIN routes ON journey_patterns.route_id = routes.id').where("routes.line_id = #{line_id}") +  end + +  def stop_areas +    [departure, arrival].compact +  end + +  def default_geometry +    points = stop_areas.collect(&:geometry).compact +    GeoRuby::SimpleFeatures::LineString.from_points(points) if points.many? +  end + +  def name +    stop_areas.map do |stop_area| +      stop_area.try(:name) or '?' +    end.join(' - ') + " (#{geometry_description})" +  end + +  def via_count +    input_geometry ? [ input_geometry.points.count - 2, 0 ].max : 0 +  end + +  def geometry_description +    if input_geometry || processed_geometry +      [ "#{distance.to_i}m" ].tap do |parts| +        parts << "#{via_count} #{'via'.pluralize(via_count)}" if via_count > 0 +      end.join(' - ') +    else +      "-" +    end +  end + +  DEFAULT_PROCESSOR = Proc.new { |section| section.input_geometry || section.default_geometry.try(:to_rgeo) } + +  @@processor = DEFAULT_PROCESSOR +  cattr_accessor :processor + +  def instance_processor +    no_processing || processor.nil? ? DEFAULT_PROCESSOR : processor +  end + +  def process_geometry +    if input_geometry_changed? || processed_geometry.nil? +      self.processed_geometry = instance_processor.call(self) +      self.distance = processed_geometry.to_georuby.to_wgs84.spherical_distance if processed_geometry +    end + +    true +  end +  before_validation :process_geometry + +  def editable_geometry=(geometry) +    self.input_geometry = geometry +  end + +  def editable_geometry +    input_geometry.try(:to_georuby) or default_geometry +  end + +  def editable_geometry_before_type_cast +    editable_geometry.to_ewkt +  end + +  def geometry(mode = nil) +    mode ||= :processed +    mode == :editable ? editable_geometry : processed_geometry.to_georuby +  end + +end diff --git a/app/models/chouette/source_type.rb b/app/models/chouette/source_type.rb new file mode 100644 index 000000000..124a6c433 --- /dev/null +++ b/app/models/chouette/source_type.rb @@ -0,0 +1,56 @@ +class Chouette::SourceType < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    camelize +  end + +  @@definitions = [ +    ["public_and_private_utilities", 0], +    ["road_authorities", 1], +    ["transit_operator", 2], +    ["public_transport", 3], +    ["passenger_transport_coordinating_authority", 4], +    ["travel_information_service_provider", 5], +    ["travel_agency", 6], +    ["individual_subject_of_travel_itinerary", 7], +    ["other_information", 8] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb new file mode 100644 index 000000000..b7cdd313a --- /dev/null +++ b/app/models/chouette/stop_area.rb @@ -0,0 +1,323 @@ +require 'geokit' +require 'geo_ruby' + +class Chouette::StopArea < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" +  include Geokit::Mappable +  has_many :stop_points, :dependent => :destroy +  has_many :access_points, :dependent => :destroy +  has_many :access_links, :dependent => :destroy +  has_and_belongs_to_many :routing_lines, :class_name => 'Chouette::Line', :foreign_key => "stop_area_id", :association_foreign_key => "line_id", :join_table => "routing_constraints_lines", :order => "lines.number" +  has_and_belongs_to_many :routing_stops, :class_name => 'Chouette::StopArea', :foreign_key => "parent_id", :association_foreign_key => "child_id", :join_table => "stop_areas_stop_areas", :order => "stop_areas.name" + +  acts_as_tree :foreign_key => 'parent_id',:order => "name" + +  attr_accessor :stop_area_type +  attr_accessor :children_ids +  attr_writer :coordinates + +  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_presence_of :name +  validates_presence_of :area_type + +  validates_presence_of :latitude, :if => :longitude +  validates_presence_of :longitude, :if => :latitude +  validates_numericality_of :latitude, :less_than_or_equal_to => 90, :greater_than_or_equal_to => -90, :allow_nil => true +  validates_numericality_of :longitude, :less_than_or_equal_to => 180, :greater_than_or_equal_to => -180, :allow_nil => true + +  validates_format_of :coordinates, :with => %r{\A *-?(0?[0-9](\.[0-9]*)?|[0-8][0-9](\.[0-9]*)?|90(\.[0]*)?) *\, *-?(0?[0-9]?[0-9](\.[0-9]*)?|1[0-7][0-9](\.[0-9]*)?|180(\.[0]*)?) *\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 + +  def self.nullable_attributes +    [:registration_number, :street_name, :country_code, :fare_code, +     :nearest_topic_name, :comment, :long_lat_type, :zip_code, :city_name, :url, :time_zone] +  end + +  after_update :clean_invalid_access_links + +  before_save :coordinates_to_lat_lng + +  def combine_lat_lng +    if self.latitude.nil? || self.longitude.nil? +      "" +    else +      self.latitude.to_s+","+self.longitude.to_s +    end +  end + +  def coordinates +      @coordinates || combine_lat_lng +  end + +  def coordinates_to_lat_lng +    if ! @coordinates.nil? +      if @coordinates.empty? +        self.latitude = nil +        self.longitude = nil +      else +        self.latitude = BigDecimal.new(@coordinates.split(",").first) +        self.longitude = BigDecimal.new(@coordinates.split(",").last) +      end +      @coordinates = nil +    end +  end + +  def children_in_depth +    return [] if self.children.empty? + +    self.children + self.children.map do |child| +      child.children_in_depth +    end.flatten.compact +  end + +  def possible_children +    case area_type +      when "BoardingPosition" then [] +      when "Quay" then [] +      when "CommercialStopPoint" then Chouette::StopArea.where(:area_type => ['Quay', 'BoardingPosition']) - [self] +      when "StopPlace" then Chouette::StopArea.where(:area_type => ['StopPlace', 'CommercialStopPoint']) - [self] +      when "ITL" then Chouette::StopArea.where(:area_type => ['Quay', 'BoardingPosition', 'StopPlace', 'CommercialStopPoint']) +    end + +  end + +  def possible_parents +    case area_type +      when "BoardingPosition" then Chouette::StopArea.where(:area_type => "CommercialStopPoint")  - [self] +      when "Quay" then Chouette::StopArea.where(:area_type => "CommercialStopPoint") - [self] +      when "CommercialStopPoint" then Chouette::StopArea.where(:area_type => "StopPlace") - [self] +      when "StopPlace" then Chouette::StopArea.where(:area_type => "StopPlace") - [self] +    end +  end + +  def geometry_presenter +    Chouette::Geometry::StopAreaPresenter.new self +  end + +  def lines +    if (area_type == 'CommercialStopPoint') +      self.children.collect(&:stop_points).flatten.collect(&:route).flatten.collect(&:line).flatten.uniq +    else +      self.stop_points.collect(&:route).flatten.collect(&:line).flatten.uniq +    end +  end + +  def routes +    self.stop_points.collect(&:route).flatten.uniq +  end + +  def self.commercial +    where :area_type => "CommercialStopPoint" +  end + +  def self.stop_place +    where :area_type => "StopPlace" +  end + +  def self.physical +    where :area_type => [ "BoardingPosition", "Quay" ] +  end + +  def self.itl +    where :area_type => "ITL" +  end + +  def to_lat_lng +    Geokit::LatLng.new(latitude, longitude) if latitude and longitude +  end + +  def geometry +    GeoRuby::SimpleFeatures::Point.from_lon_lat(longitude, latitude, 4326) if latitude and longitude +  end + +  def geometry=(geometry) +    geometry = geometry.to_wgs84 +    self.latitude, self.longitude, self.long_lat_type = geometry.lat, geometry.lng, "WGS84" +  end + +  def position +    geometry +  end + +  def position=(position) +    position = nil if String === position && position == "" +    position = Geokit::LatLng.normalize(position), 4326 if String === position +    self.latitude = position.lat +    self.longitude = position.lng +  end + +  def default_position +    # for first StopArea ... the bounds is nil :( +    Chouette::StopArea.bounds and Chouette::StopArea.bounds.center +  end + +  def self.near(origin, distance = 0.3) +    origin = origin.to_lat_lng + +    lat_degree_units = units_per_latitude_degree(:kms) +    lng_degree_units = units_per_longitude_degree(origin.lat, :kms) + +    where "SQRT(POW(#{lat_degree_units}*(#{origin.lat}-latitude),2)+POW(#{lng_degree_units}*(#{origin.lng}-longitude),2)) <= #{distance}" +  end + +  def self.bounds +    # Give something like : +    # [["113.5292500000000000", "22.1127580000000000", "113.5819330000000000", "22.2157050000000000"]] +    min_and_max = connection.select_rows("select min(longitude) as min_lon, min(latitude) as min_lat, max(longitude) as max_lon, max(latitude) as max_lat from #{table_name} where latitude is not null and longitude is not null").first +    return nil unless min_and_max + +    # Ignore [nil, nil, nil, nil] +    min_and_max.compact! +    return nil unless min_and_max.size == 4 + +    min_and_max.collect! { |n| n.to_f } + +    # We need something like : +    # [[113.5292500000000000, 22.1127580000000000], [113.5819330000000000, 22.2157050000000000]] +    coordinates = min_and_max.each_slice(2).to_a +    GeoRuby::SimpleFeatures::Envelope.from_coordinates coordinates +  end + +  def stop_area_type +    area_type && Chouette::AreaType.new(area_type.underscore) +  end + +  def stop_area_type=(stop_area_type) +    self.area_type = (stop_area_type ? stop_area_type.camelcase : nil) +    if self.area_type == 'Itl' +      self.area_type = 'ITL' +    end +  end + +  @@stop_area_types = nil +  def self.stop_area_types +    @@stop_area_types ||= Chouette::AreaType.all.select do |stop_area_type| +      stop_area_type.to_i >= 0 +    end +  end + +  def children_ids=(children_ids) +    children = children_ids.split(',').uniq +    # remove unset children +    self.children.each do |child| +      if (! children.include? child.id) +        child.update_attribute :parent_id, nil +      end +    end +    # add new children +    Chouette::StopArea.find(children).each do |child| +       child.update_attribute :parent_id, self.id +    end +  end + +  def routing_stop_ids=(routing_stop_ids) +    stops = routing_stop_ids.split(',').uniq +    self.routing_stops.clear +    Chouette::StopArea.find(stops).each do |stop| +      self.routing_stops << stop +    end +  end + +  def routing_line_ids=(routing_line_ids) +    lines = routing_line_ids.split(',').uniq +    self.routing_lines.clear +    Chouette::Line.find(lines).each do |line| +      self.routing_lines << line +    end +  end + +  def self.without_geometry +    where("latitude is null or longitude is null") +  end + +  def self.with_geometry +    where("latitude is not null and longitude is not null") +  end + +  def self.default_geometry! +    count = 0 +    where(nil).find_each do |stop_area| +      Chouette::StopArea.unscoped do +        count += 1 if stop_area.default_geometry! +      end +    end +    count +  end + +  def default_geometry! +    new_geometry = default_geometry +    update_attribute :geometry, new_geometry if new_geometry +  end + +  def default_geometry +    children_geometries = children.with_geometry.map(&:geometry).uniq +    GeoRuby::SimpleFeatures::Point.centroid children_geometries if children_geometries.present? +  end + +  def generic_access_link_matrix +     matrix = Array.new +     access_points.each do |access_point| +       matrix += access_point.generic_access_link_matrix +     end +     matrix +  end + +  def detail_access_link_matrix +     matrix = Array.new +     access_points.each do |access_point| +       matrix += access_point.detail_access_link_matrix +     end +     matrix +  end + +  def children_at_base +    list = Array.new +    children_in_depth.each do |child| +      if child.area_type == 'Quay' || child.area_type == 'BoardingPosition' +        list << child +      end +    end +    list +  end + +  def parents +    list = Array.new +    if !parent.nil? +      list << parent +      list += parent.parents +    end +    list +  end + +  def clean_invalid_access_links +    stop_parents = parents +    access_links.each do |link| +      unless stop_parents.include? link.access_point.stop_area +        link.delete +      end +    end +    children.each do |child| +      child.clean_invalid_access_links +    end +  end + +  def duplicate +    sa = self.deep_clone :except => [:object_version, :parent_id, :registration_number] +    sa.uniq_objectid +    sa.name = I18n.t("activerecord.copy", :name => self.name) +    sa +  end + +  def journey_patterns_control_route_sections +    if self.changed_attributes['latitude'] || self.changed_attributes['longitude'] +      self.stop_points.each do |stop_point| +        stop_point.route.journey_patterns.completed.map{ |jp| jp.control! } +      end +    end +  end + +end diff --git a/app/models/chouette/stop_point.rb b/app/models/chouette/stop_point.rb new file mode 100644 index 000000000..b77189fc1 --- /dev/null +++ b/app/models/chouette/stop_point.rb @@ -0,0 +1,41 @@ +module Chouette +  class StopPoint < TridentActiveRecord +    include ForBoardingEnumerations +    include ForAlightingEnumerations +     +    # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +    self.primary_key = "id" + +    belongs_to :stop_area +    belongs_to :route, inverse_of: :stop_points +    has_many :vehicle_journey_at_stops, :dependent => :destroy +    has_many :vehicle_journeys, -> {uniq}, :through => :vehicle_journey_at_stops + +    acts_as_list :scope => :route, top_of_list: 0 + +    validates_presence_of :stop_area +    validate :stop_area_id_validation + +    scope :default_order, order("position") + +    before_destroy :remove_dependent_journey_pattern_stop_points +    def remove_dependent_journey_pattern_stop_points +      route.journey_patterns.each do |jp| +        if jp.stop_point_ids.include?( id) +          jp.stop_point_ids = jp.stop_point_ids - [id] +        end +      end +    end   + +    def stop_area_id_validation +      if stop_area_id.nil? +        errors.add(:stop_area_id, I18n.t("errors.messages.empty")) +      end +    end + +    def self.area_candidates +      Chouette::StopArea.where( :area_type => ['Quay', 'BoardingPosition']) +    end + +  end +end diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb new file mode 100644 index 000000000..033c39f1c --- /dev/null +++ b/app/models/chouette/time_table.rb @@ -0,0 +1,450 @@ +class Chouette::TimeTable < Chouette::TridentActiveRecord +  # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +  self.primary_key = "id" + +  acts_as_taggable +   +  attr_accessor :monday,:tuesday,:wednesday,:thursday,:friday,:saturday,:sunday   +  attr_accessor :tag_search + +  def self.ransackable_attributes auth_object = nil +    (column_names + ['tag_search']) + _ransackers.keys +  end + +  has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney' + +  has_many :dates, -> {order(:date)}, inverse_of: :time_table, :validate => :true, :class_name => "Chouette::TimeTableDate", :dependent => :destroy +  has_many :periods, -> {order(:period_start)}, inverse_of: :time_table, :validate => :true, :class_name => "Chouette::TimeTablePeriod", :dependent => :destroy + +  after_save :save_shortcuts + +  def self.object_id_key +    "Timetable" +  end + +  accepts_nested_attributes_for :dates, :allow_destroy => :true +  accepts_nested_attributes_for :periods, :allow_destroy => :true + +  validates_presence_of :comment +  validates_associated :dates +  validates_associated :periods + +  def self.start_validity_period +    [Chouette::TimeTable.minimum(:start_date)].compact.min +  end +  def self.end_validity_period +    [Chouette::TimeTable.maximum(:end_date)].compact.max +  end + +  def save_shortcuts +      shortcuts_update +      self.update_column(:start_date, start_date) +      self.update_column(:end_date, end_date) +  end + +  def shortcuts_update(date=nil) +    dates_array = bounding_dates +    #if new_record? +      if dates_array.empty? +        self.start_date=nil +        self.end_date=nil +      else +        self.start_date=dates_array.min +        self.end_date=dates_array.max +      end +    #else +     # if dates_array.empty? +     #   update_attributes :start_date => nil, :end_date => nil +     # else +     #   update_attributes :start_date => dates_array.min, :end_date => dates_array.max +     # end +    #end +  end + +  def validity_out_from_on?(expected_date) +    return false unless self.end_date +    self.end_date <= expected_date +  end +  def validity_out_between?(starting_date, ending_date) +    return false unless self.start_date +    starting_date < self.end_date  && +      self.end_date <= ending_date +  end +  def self.validity_out_from_on?(expected_date,limit=0) +    if limit==0 +      Chouette::TimeTable.where("end_date <= ?", expected_date) +    else +      Chouette::TimeTable.where("end_date <= ?", expected_date).limit( limit) +    end +  end +  def self.validity_out_between?(start_date, end_date,limit=0) +    if limit==0 +      Chouette::TimeTable.where( "? < end_date", start_date).where( "end_date <= ?", end_date) +    else +      Chouette::TimeTable.where( "? < end_date", start_date).where( "end_date <= ?", end_date).limit( limit) +    end +  end + +  # Return days which intersects with the time table dates and periods +  def intersects(days) +    [].tap do |intersect_days| +      days.each do |day| +        intersect_days << day if include_day?(day) +      end +    end +  end + +  def include_day?(day) +    include_in_dates?(day) || include_in_periods?(day) +  end + +  def include_in_dates?(day) +    self.dates.any?{ |d| d.date === day && d.in_out == true } +  end + +  def excluded_date?(day) +    self.dates.any?{ |d| d.date === day && d.in_out == false } +  end + +  def include_in_periods?(day) +    self.periods.any?{ |period| period.period_start <= day && +                                day <= period.period_end && +                                valid_days.include?(day.cwday) && +                                ! excluded_date?(day) } +  end + +  def include_in_overlap_dates?(day) +    return false if self.excluded_date?(day) + +    counter = self.dates.select{ |d| d.date === day}.size + self.periods.select{ |period| period.period_start <= day && day <= period.period_end && valid_days.include?(day.cwday) }.size +    counter <= 1 ? false : true +   end + +  def periods_max_date +    return nil if self.periods.empty? + +    min_start = self.periods.map(&:period_start).compact.min +    max_end = self.periods.map(&:period_end).compact.max +    result = nil + +    if max_end && min_start +      max_end.downto( min_start) do |date| +        if self.valid_days.include?(date.cwday) && !self.excluded_date?(date) +            result = date +            break +        end +      end +    end +    result +  end +  def periods_min_date +    return nil if self.periods.empty? + +    min_start = self.periods.map(&:period_start).compact.min +    max_end = self.periods.map(&:period_end).compact.max +    result = nil + +    if max_end && min_start +      min_start.upto(max_end) do |date| +        if self.valid_days.include?(date.cwday) && !self.excluded_date?(date) +            result = date +            break +        end +      end +    end +    result +  end +  def bounding_dates +    bounding_min = self.dates.select{|d| d.in_out}.map(&:date).compact.min +    bounding_max = self.dates.select{|d| d.in_out}.map(&:date).compact.max + +    unless self.periods.empty? +      bounding_min = periods_min_date if periods_min_date && +          (bounding_min.nil? || (periods_min_date < bounding_min)) + +      bounding_max = periods_max_date if periods_max_date && +          (bounding_max.nil? || (bounding_max < periods_max_date)) +    end + +    [bounding_min, bounding_max].compact +  end + +  def day_by_mask(flag) +    int_day_types & flag == flag +  end + +  def self.day_by_mask(int_day_types,flag) +    int_day_types & flag == flag +  end + + +  def valid_days +    # Build an array with day of calendar week (1-7, Monday is 1). +    [].tap do |valid_days| +      valid_days << 1  if monday +      valid_days << 2  if tuesday +      valid_days << 3  if wednesday +      valid_days << 4  if thursday +      valid_days << 5  if friday +      valid_days << 6  if saturday +      valid_days << 7  if sunday +    end +  end + +  def self.valid_days(int_day_types) +    # Build an array with day of calendar week (1-7, Monday is 1). +    [].tap do |valid_days| +      valid_days << 1  if day_by_mask(int_day_types,4) +      valid_days << 2  if day_by_mask(int_day_types,8) +      valid_days << 3  if day_by_mask(int_day_types,16) +      valid_days << 4  if day_by_mask(int_day_types,32) +      valid_days << 5  if day_by_mask(int_day_types,64) +      valid_days << 6  if day_by_mask(int_day_types,128) +      valid_days << 7  if day_by_mask(int_day_types,256) +    end +  end + +  def monday +    day_by_mask(4) +  end +  def tuesday +    day_by_mask(8) +  end +  def wednesday +    day_by_mask(16) +  end +  def thursday +    day_by_mask(32) +  end +  def friday +    day_by_mask(64) +  end +  def saturday +    day_by_mask(128) +  end +  def sunday +    day_by_mask(256) +  end + +  def set_day(day,flag) +    if day == '1' || day == true +      self.int_day_types |= flag +    else +      self.int_day_types &= ~flag +    end +    shortcuts_update +  end + +  def monday=(day) +    set_day(day,4) +  end +  def tuesday=(day) +    set_day(day,8) +  end +  def wednesday=(day) +    set_day(day,16) +  end +  def thursday=(day) +    set_day(day,32) +  end +  def friday=(day) +    set_day(day,64) +  end +  def saturday=(day) +    set_day(day,128) +  end +  def sunday=(day) +    set_day(day,256) +  end + +  def effective_days_of_period(period,valid_days=self.valid_days) +    days = [] +      period.period_start.upto(period.period_end) do |date| +        if valid_days.include?(date.cwday) && !self.excluded_date?(date) +            days << date +        end +      end +    days +  end + +  def effective_days(valid_days=self.valid_days) +    days=self.effective_days_of_periods(valid_days) +    self.dates.each do |d| +      days |= [d.date] if d.in_out +    end +    days.sort +  end + +  def effective_days_of_periods(valid_days=self.valid_days) +    days = [] +    self.periods.each { |p| days |= self.effective_days_of_period(p,valid_days)} +    days.sort +  end + +  def clone_periods +    periods = [] +    self.periods.each { |p| periods << p.copy} +    periods +  end + +  def included_days +    days = [] +    self.dates.each do |d| +      days |= [d.date] if d.in_out +    end +    days.sort +  end + +  def excluded_days +    days = [] +    self.dates.each do |d| +      days |= [d.date] unless d.in_out +    end +    days.sort +  end + + +  # produce a copy of periods without anyone overlapping or including another +  def optimize_periods +    periods = self.clone_periods +    optimized = [] +    i=0 +    while i < periods.length +      p1 = periods[i] +      optimized << p1 +      j= i+1 +      while j < periods.length +        p2 = periods[j] +        if p1.contains? p2 +          periods.delete p2 +        elsif p1.overlap? p2 +          p1.period_start = [p1.period_start,p2.period_start].min +          p1.period_end = [p1.period_end,p2.period_end].max +          periods.delete p2 +        else +          j += 1 +        end +      end +      i+= 1 +    end +    optimized.sort { |a,b| a.period_start <=> b.period_start} +  end + +  # add a peculiar day or switch it from excluded to included +  def add_included_day(d) +    if self.excluded_date?(d) +       self.dates.each do |date| +         if date.date === d +           date.in_out = true +         end +       end +    elsif !self.include_in_dates?(d) +       self.dates << Chouette::TimeTableDate.new(:date => d, :in_out => true) +    end +  end + +  # merge effective days from another timetable +  def merge!(another_tt) +    transaction do +    # if one tt has no period, just merge lists +    if self.periods.empty? || another_tt.periods.empty? +      if !another_tt.periods.empty? +        # copy periods +        self.periods = another_tt.clone_periods +        # set valid_days +        self.int_day_types = another_tt.int_day_types +      end +      # merge dates +      self.dates ||= [] +      another_tt.included_days.each do |d| +        add_included_day d +      end +    else +      # check if periods can be kept +      common_day_types = self.int_day_types & another_tt.int_day_types & 508 +      # if common day types : merge periods +      if common_day_types != 0 +        periods = self.optimize_periods +        another_periods = another_tt.optimize_periods +        # add not common days of both periods as peculiar days +        self.effective_days_of_periods(self.class.valid_days(self.int_day_types ^ common_day_types)).each do |d| +          self.dates |= [Chouette::TimeTableDate.new(:date => d, :in_out => true)] +        end +        another_tt.effective_days_of_periods(self.class.valid_days(another_tt.int_day_types ^ common_day_types)).each do |d| +          add_included_day d +        end +        # merge periods +        self.periods = periods | another_periods +        self.int_day_types = common_day_types +        self.periods = self.optimize_periods +      else +        # convert all period in days +        self.effective_days_of_periods.each do |d| +          self.dates << Chouette::TimeTableDate.new(:date => d, :in_out => true) unless self.include_in_dates?(d) +        end +        another_tt.effective_days_of_periods.each do |d| +          add_included_day d +        end +      end +    end +    # if remained excluded dates are valid in other tt , remove it from result +    self.dates.each do |date| +      date.in_out = true if date.in_out == false && another_tt.include_day?(date.date) +    end + +    # if peculiar dates are valid in new periods, remove them +    if !self.periods.empty? +      days_in_period = self.effective_days_of_periods +      dates = [] +      self.dates.each do |date| +        dates << date unless date.in_out && days_in_period.include?(date.date) +      end +      self.dates = dates +    end +    self.dates.to_a.sort! { |a,b| a.date <=> b.date} +    self.save! +    end +  end + +  # remove dates form tt which aren't in another_tt +  def intersect!(another_tt) +    transaction do +       +      # transform tt as effective dates and get common ones +      days = another_tt.intersects(self.effective_days) & self.intersects(another_tt.effective_days) +      self.dates.clear +      days.each {|d| self.dates << Chouette::TimeTableDate.new( :date =>d, :in_out => true)} +      self.periods.clear +      self.int_day_types = 0 +      self.dates.to_a.sort! { |a,b| a.date <=> b.date} +      self.save! +    end +  end + + +  def disjoin!(another_tt) +    transaction do +      # remove days from another calendar +      days_to_exclude = self.intersects(another_tt.effective_days) +      days = self.effective_days - days_to_exclude +      self.dates.clear +      self.periods.clear +      self.int_day_types = 0 + +      days.each {|d| self.dates << Chouette::TimeTableDate.new( :date =>d, :in_out => true)} + +      self.dates.to_a.sort! { |a,b| a.date <=> b.date} +      self.periods.to_a.sort! { |a,b| a.period_start <=> b.period_start} +      self.save! +    end +  end + +  def duplicate +    tt = self.deep_clone :include => [:periods, :dates], :except => :object_version +    tt.uniq_objectid +    tt.comment = I18n.t("activerecord.copy", :name => self.comment) +    tt +  end + +end + diff --git a/app/models/chouette/time_table_date.rb b/app/models/chouette/time_table_date.rb new file mode 100644 index 000000000..4624ae88e --- /dev/null +++ b/app/models/chouette/time_table_date.rb @@ -0,0 +1,14 @@ +class Chouette::TimeTableDate < Chouette::ActiveRecord +  self.primary_key = "id" +  belongs_to :time_table, inverse_of: :dates +  acts_as_list :scope => 'time_table_id = #{time_table_id}',:top_of_list => 0 + +  validates_presence_of :date +  validates_uniqueness_of :date, :scope => :time_table_id + +  def self.model_name +    ActiveModel::Name.new Chouette::TimeTableDate, Chouette, "TimeTableDate" +  end + +end + diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb new file mode 100644 index 000000000..0a75c18c3 --- /dev/null +++ b/app/models/chouette/time_table_period.rb @@ -0,0 +1,40 @@ +class Chouette::TimeTablePeriod < Chouette::ActiveRecord +  self.primary_key = "id" +  belongs_to :time_table, inverse_of: :periods +  acts_as_list :scope => 'time_table_id = #{time_table_id}',:top_of_list => 0 + +  validates_presence_of :period_start +  validates_presence_of :period_end +   +  validate :start_must_be_before_end + +   +  def self.model_name +    ActiveModel::Name.new Chouette::TimeTablePeriod, Chouette, "TimeTablePeriod" +  end +   +  def start_must_be_before_end +    # security against nil values +    if period_end.nil? || period_start.nil? +      return +    end +    if period_end <= period_start +      errors.add(:period_end,I18n.t("activerecord.errors.models.time_table_period.start_must_be_before_end")) +    end +  end +     +  def copy +    Chouette::TimeTablePeriod.new(:period_start => self.period_start,:period_end => self.period_end) +  end +   +  # Test to see if a period overlap this period +  def overlap?(p) +     (p.period_start >= self.period_start && p.period_start <= self.period_end) || (p.period_end >= self.period_start && p.period_end <= self.period_end) +  end + +  # Test to see if a period is included in this period +  def contains?(p) +     (p.period_start >= self.period_start && p.period_end <= self.period_end) +  end +   +end diff --git a/app/models/chouette/timeband.rb b/app/models/chouette/timeband.rb new file mode 100644 index 000000000..9844dd1b1 --- /dev/null +++ b/app/models/chouette/timeband.rb @@ -0,0 +1,30 @@ +module Chouette + +  class TimebandValidator < ActiveModel::Validator +    def validate(record) +      if record.end_time <= record.start_time +        record.errors[:end_time] << I18n.t('activerecord.errors.models.timeband.start_must_be_before_end') +      end +    end +  end + +  class Timeband < Chouette::TridentActiveRecord +    self.primary_key = "id" + +    validates :start_time, :end_time, presence: true +    validates_with TimebandValidator + +    default_scope { order(:start_time) } + +    def self.object_id_key +      "Timeband" +    end + +    def fullname +      fullname = "#{I18n.l(self.start_time, format: :hour)}-#{I18n.l(self.end_time, format: :hour)}" +      "#{self.name} (#{fullname})" if self.name +    end + +  end + +end diff --git a/app/models/chouette/transport_mode.rb b/app/models/chouette/transport_mode.rb new file mode 100644 index 000000000..825ef15b8 --- /dev/null +++ b/app/models/chouette/transport_mode.rb @@ -0,0 +1,71 @@ +class Chouette::TransportMode < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    camelize +  end + +  @@definitions = [ +    ["interchange", -1], +    ["unknown", 0], +    ["coach", 1], +    ["air", 2], +    ["waterborne", 3], +    ["bus", 4], +    ["ferry", 5], +    ["walk", 6], +    ["metro", 7], +    ["shuttle", 8], +    ["rapid_transit", 9], +    ["taxi", 10], +    ["local_train", 11], +    ["train", 12], +    ["long_distance_train", 13], +    ["tramway", 14], +    ["trolleybus", 15], +    ["private_vehicle", 16], +    ["bicycle", 17], +    ["other", 18] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +  def public_transport? +    not interchange? +  end + +end diff --git a/app/models/chouette/trident_active_record.rb b/app/models/chouette/trident_active_record.rb new file mode 100644 index 000000000..b89b85863 --- /dev/null +++ b/app/models/chouette/trident_active_record.rb @@ -0,0 +1,132 @@ +class Chouette::TridentActiveRecord < Chouette::ActiveRecord +    before_validation :prepare_auto_columns +    after_validation :reset_auto_columns +     +    after_save :build_objectid + +    self.abstract_class = true +    # +    # triggers to generate objectId and objectVersion  +    # TODO setting prefix in referential object +     +    def self.object_id_key +      model_name +    end +     +    def prefix +      "NINOXE" +    end +    def prepare_auto_columns +      # logger.info 'calling before_validation' +      # logger.info 'start before_validation : '+self.objectid.to_s +      if self.objectid.nil? || self.objectid.blank? +        # if empty, generate a pending objectid which will be completed after creation +        if self.id.nil? +           self.objectid = "#{prefix}:#{self.class.object_id_key}:__pending_id__#{rand(1000)}" +        else +           self.objectid = "#{prefix}:#{self.class.object_id_key}:#{self.id}" +           fix_uniq_objectid +        end +      elsif not self.objectid.include? ':' +        # if one token : technical token : completed by prefix and key +        self.objectid = "#{prefix}:#{self.class.object_id_key}:#{self.objectid}" +      end +      # logger.info 'end before_validation : '+self.objectid +      # initialize or update version +      if self.object_version.nil? +        self.object_version = 1 +      else +        self.object_version += 1 +      end +      self.creation_time = Time.now +      self.creator_id = 'chouette' +    end +     +    def reset_auto_columns +      clean_object_id unless self.errors.nil? || self.errors.empty? +    end +     +    def clean_object_id +      if self.objectid.include?("__pending_id__") +        self.objectid=nil +      end +    end   +     +    def fix_uniq_objectid +        base_objectid = self.objectid.rpartition(":").first +        self.objectid = "#{base_objectid}:#{self.id}" +        if !self.valid? +          base_objectid="#{self.objectid}_" +          cnt=1 +          while !self.valid?  +            self.objectid = "#{base_objectid}#{cnt}" +            cnt += 1 +          end +        end +       +    end +     +    def build_objectid +      #logger.info 'start after_create : '+self.objectid +      if self.objectid.include? ':__pending_id__' +        fix_uniq_objectid +        self.update_attributes( :objectid => self.objectid, :object_version => (self.object_version - 1) ) +      end +      #logger.info 'end after_create : '+self.objectid +    end + +  validates_presence_of :objectid +  validates_uniqueness_of :objectid +  validates_numericality_of :object_version +  validate :objectid_format_compliance + +  def objectid_format_compliance +    if !self.objectid.valid? +      #errors.add(:objectid, "is not a valid ObjectId object") +      errors.add(:objectid,I18n.t("activerecord.errors.models.trident.invalid_object_id",:type => self.class.object_id_key)) +#    else +#      unless self.objectid.object_type==self.class.object_id_key +#        errors.add(:objectid,I18n.t("activerecord.errors.models.trident.invalid_object_id_type",:type => self.class.object_id_key)) +#      end +    end +  end +   +  def uniq_objectid +    i = 0 +    baseobjectid = self.objectid +    while self.class.exists?(:objectid => self.objectid)  +      i += 1 +      self.objectid = baseobjectid+"_"+i.to_s +    end +  end + +  def self.model_name +    ActiveModel::Name.new self, Chouette, self.name.demodulize +  end + +  def objectid +    Chouette::ObjectId.new read_attribute(:objectid) +  end + +#  def version +#    self.object_version +#  end + +#  def version=(version) +#    self.object_version = version +#  end + +  before_validation :default_values, :on => :create +  def default_values +    self.object_version ||= 1 +  end + +  def timestamp_attributes_for_update #:nodoc: +    [:creation_time] +  end +   +  def timestamp_attributes_for_create #:nodoc: +    [:creation_time] +  end + +end diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb new file mode 100644 index 000000000..44a9f8975 --- /dev/null +++ b/app/models/chouette/vehicle_journey.rb @@ -0,0 +1,114 @@ +module Chouette +  class VehicleJourney < TridentActiveRecord + +    # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +    self.primary_key = "id" + +    enum journey_category: { timed: 0, frequency: 1 } + +    default_scope { where(journey_category: journey_categories[:timed]) } + +    attr_accessor :transport_mode_name +    attr_reader :time_table_tokens + +    def self.nullable_attributes +      [:transport_mode, :published_journey_name, :vehicle_type_identifier, :published_journey_identifier, :comment, :status_value] +    end + +    belongs_to :company +    belongs_to :route +    belongs_to :journey_pattern + +    has_and_belongs_to_many :footnotes, :class_name => 'Chouette::Footnote' + +    validates_presence_of :route +    validates_presence_of :journey_pattern + +    has_many :vehicle_journey_at_stops, -> { includes(:stop_point).order("stop_points.position") }, :dependent => :destroy +    has_and_belongs_to_many :time_tables, :class_name => 'Chouette::TimeTable', :foreign_key => "vehicle_journey_id", :association_foreign_key => "time_table_id" +    has_many :stop_points, -> { order("stop_points.position") }, :through => :vehicle_journey_at_stops + +    validate :increasing_times +    validates_presence_of :number + +    before_validation :set_default_values +    def set_default_values +      if number.nil? +        self.number = 0 +      end +    end + +    scope :without_any_time_table, -> { joins('LEFT JOIN "time_tables_vehicle_journeys" ON "time_tables_vehicle_journeys"."vehicle_journey_id" = "vehicle_journeys"."id" LEFT JOIN "time_tables" ON "time_tables"."id" = "time_tables_vehicle_journeys"."time_table_id"').where(:time_tables => { :id => nil}) } +    scope :without_any_passing_time, -> { joins('LEFT JOIN "vehicle_journey_at_stops" ON "vehicle_journey_at_stops"."vehicle_journey_id" = "vehicle_journeys"."id"').where(vehicle_journey_at_stops: { id: nil }) } + +    accepts_nested_attributes_for :vehicle_journey_at_stops, :allow_destroy => true + +    def transport_mode_name +      # return nil if transport_mode is nil +      transport_mode && Chouette::TransportMode.new( transport_mode.underscore) +    end + +    def transport_mode_name=(transport_mode_name) +      self.transport_mode = (transport_mode_name ? transport_mode_name.camelcase : nil) +    end + +    @@transport_mode_names = nil +    def self.transport_mode_names +      @@transport_mode_names ||= Chouette::TransportMode.all.select do |transport_mode_name| +        transport_mode_name.to_i > 0 +      end +    end + +    def increasing_times +      previous = nil +      vehicle_journey_at_stops.select{|vjas| vjas.departure_time && vjas.arrival_time}.each do |vjas| +        errors.add( :vehicle_journey_at_stops, 'time gap overflow') unless vjas.increasing_times_validate( previous) +        previous = vjas +      end +    end + +    def missing_stops_in_relation_to_a_journey_pattern(selected_journey_pattern) +      selected_journey_pattern.stop_points - self.stop_points +    end +    def extra_stops_in_relation_to_a_journey_pattern(selected_journey_pattern) +      self.stop_points - selected_journey_pattern.stop_points +    end +    def extra_vjas_in_relation_to_a_journey_pattern(selected_journey_pattern) +      extra_stops = self.extra_stops_in_relation_to_a_journey_pattern(selected_journey_pattern) +      self.vehicle_journey_at_stops.select { |vjas| extra_stops.include?( vjas.stop_point)} +    end +    def time_table_tokens=(ids) +      self.time_table_ids = ids.split(",") +    end +    def bounding_dates +      dates = [] + +      time_tables.each do |tm| +        dates << tm.start_date if tm.start_date +        dates << tm.end_date if tm.end_date +      end + +      dates.empty? ? [] : [dates.min, dates.max] +    end + +    def update_journey_pattern( selected_journey_pattern) +      return unless selected_journey_pattern.route_id==self.route_id + +      missing_stops_in_relation_to_a_journey_pattern(selected_journey_pattern).each do |sp| +        self.vehicle_journey_at_stops.build( :stop_point => sp) +      end +      extra_vjas_in_relation_to_a_journey_pattern(selected_journey_pattern).each do |vjas| +        vjas._destroy = true +      end +    end + +    def self.matrix(vehicle_journeys) +      {}.tap do |hash| +        vehicle_journeys.map{ |vj| +          vj.vehicle_journey_at_stops.map{ |vjas |hash[ "#{vj.id}-#{vjas.stop_point_id}"] = vjas } +        } +      end +    end + +  end +end diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb new file mode 100644 index 000000000..7d6414f55 --- /dev/null +++ b/app/models/chouette/vehicle_journey_at_stop.rb @@ -0,0 +1,48 @@ +module Chouette +  class VehicleJourneyAtStop < ActiveRecord +    include ForBoardingEnumerations +    include ForAlightingEnumerations + +    # FIXME http://jira.codehaus.org/browse/JRUBY-6358 +    self.primary_key = "id" + +    belongs_to :stop_point +    belongs_to :vehicle_journey + +    attr_accessor :_destroy + +    validate :arrival_must_be_before_departure +    def arrival_must_be_before_departure +      # security against nil values +      return unless arrival_time && departure_time + +      if exceeds_gap?( arrival_time, departure_time) +        errors.add(:arrival_time,I18n.t("activerecord.errors.models.vehicle_journey_at_stop.arrival_must_be_before_departure")) +      end +    end + +    after_initialize :set_virtual_attributes +    def set_virtual_attributes +      @_destroy = false +    end + +    def increasing_times_validate( previous) +      result = true +      return result unless previous + +      if exceeds_gap?( previous.departure_time, departure_time) +        result = false +        errors.add( :departure_time, 'departure time gap overflow') +      end +      if exceeds_gap?( previous.arrival_time, arrival_time) +        result = false +        errors.add( :arrival_time, 'arrival time gap overflow') +      end +      result +    end +    def exceeds_gap?(first, second) +      (4 * 3600) < ((second - first) % (3600 * 24)) +    end + +  end +end diff --git a/app/models/chouette/vehicle_journey_frequency.rb b/app/models/chouette/vehicle_journey_frequency.rb new file mode 100644 index 000000000..41ba49082 --- /dev/null +++ b/app/models/chouette/vehicle_journey_frequency.rb @@ -0,0 +1,69 @@ +module Chouette +  class VehicleJourneyFrequency < VehicleJourney + +    after_initialize :fill_journey_category + +    default_scope { where(journey_category: journey_categories[:frequency]) } + +    has_many :journey_frequencies, dependent: :destroy, foreign_key: 'vehicle_journey_id' +    accepts_nested_attributes_for :journey_frequencies, allow_destroy: true + +    validate :require_at_least_one_frequency + +    def self.matrix(vehicle_journeys) +      hash = {} +      vehicle_journeys.each do |vj| +        vj.journey_frequencies.each do |jf| +          next if jf.scheduled_headway_interval.hour == 0 && jf.scheduled_headway_interval.min == 0 +          interval = jf.scheduled_headway_interval.hour.hours + jf.scheduled_headway_interval.min.minutes +          first_departure_time = jf.first_departure_time +          while first_departure_time <= jf.last_departure_time +            hash[first_departure_time] = vj +            first_departure_time += interval +          end +        end +      end +      hash.sort.to_h +    end + +    def self.matrix_interval(matrix) +      hash = prepare_matrix(matrix) +      matrix.each do |departure_time, vj| +        @base_departure_time = departure_time +        vj.vehicle_journey_at_stops.each_cons(2) { |vjas, vjas_next| +          vjas_dup = vjas.dup +          vjas_dup.departure_time = @base_departure_time +          hash[vjas.stop_point.stop_area.name][departure_time.to_i] = vjas_dup +          @base_departure_time = vjas_dup.departure_time + (vjas_next.departure_time - vjas.departure_time) +          @last_vjas_next = vjas_next.dup +        } +        # Add last stop_point +        @last_vjas_next.departure_time = @base_departure_time +        hash[@last_vjas_next.stop_point.stop_area.name][departure_time.to_i] = @last_vjas_next +      end +      hash +    end + +    private + +    def self.prepare_matrix(matrix) +      matrix.map{ |departure_time, vj| +        Hash[ +          vj.vehicle_journey_at_stops.map{ |sp| +            [ +              sp.stop_point.stop_area.name, Hash[matrix.map{ |departure_time2, vj2| [departure_time2.to_i, nil] }] +            ] +          } +        ] +      }.inject(&:merge) +    end + +    def fill_journey_category +      self.journey_category = :frequency +    end + +    def require_at_least_one_frequency +      errors.add(:base, I18n.t('vehicle_journey_frequency.require_at_least_one_frequency')) unless journey_frequencies.size > 0 +    end +  end +end diff --git a/app/models/chouette/wayback.rb b/app/models/chouette/wayback.rb new file mode 100644 index 000000000..b2950449d --- /dev/null +++ b/app/models/chouette/wayback.rb @@ -0,0 +1,50 @@ +class Chouette::Wayback < ActiveSupport::StringInquirer + +  def initialize(text_code, numerical_code) +    super text_code.to_s +    @numerical_code = numerical_code +  end + +  def self.new(text_code, numerical_code = nil) +    if text_code and numerical_code +      super +    elsif self === text_code  +      text_code +    else +      if Fixnum === text_code +        text_code, numerical_code = definitions.rassoc(text_code) +      else +        text_code, numerical_code = definitions.assoc(text_code.to_s) +      end + +      super text_code, numerical_code +    end +  end + +  def to_i +    @numerical_code +  end + +  def inspect +    "#{to_s}/#{to_i}" +  end + +  def name +    to_s +  end + +  @@definitions = [ +    ["straight_forward", 0], +    ["backward", 1] +  ] +  cattr_reader :definitions + +  @@all = nil +  def self.all +    @@all ||= definitions.collect do |text_code, numerical_code| +      new(text_code, numerical_code) +    end +  end + +end + diff --git a/app/presenters/chouette/geometry/access_link_presenter.rb b/app/presenters/chouette/geometry/access_link_presenter.rb new file mode 100644 index 000000000..b0f1d5f94 --- /dev/null +++ b/app/presenters/chouette/geometry/access_link_presenter.rb @@ -0,0 +1,11 @@ +class Chouette::Geometry::AccessLinkPresenter +  include Chouette::Geometry::GeneralPresenter + +  def initialize(access_link) +    @access_link = access_link +  end + +  def geometry +    to_line_string_feature( [ @access_link.stop_area , @access_link.access_point ] ) +  end +end diff --git a/app/presenters/chouette/geometry/access_point_presenter.rb b/app/presenters/chouette/geometry/access_point_presenter.rb new file mode 100644 index 000000000..d3ecb6128 --- /dev/null +++ b/app/presenters/chouette/geometry/access_point_presenter.rb @@ -0,0 +1,11 @@ +class Chouette::Geometry::AccessPointPresenter +  include Chouette::Geometry::GeneralPresenter + +  def initialize(access_point) +    @access_point = access_point +  end + +  def geometry +    to_point_feature( @access_point) +  end +end diff --git a/app/presenters/chouette/geometry/connection_link_presenter.rb b/app/presenters/chouette/geometry/connection_link_presenter.rb new file mode 100644 index 000000000..10f7fcd17 --- /dev/null +++ b/app/presenters/chouette/geometry/connection_link_presenter.rb @@ -0,0 +1,11 @@ +class Chouette::Geometry::ConnectionLinkPresenter +  include Chouette::Geometry::GeneralPresenter + +  def initialize(connection_link) +    @connection_link = connection_link +  end + +  def geometry +    to_line_string_feature( @connection_link.stop_areas) +  end +end diff --git a/app/presenters/chouette/geometry/general_presenter.rb b/app/presenters/chouette/geometry/general_presenter.rb new file mode 100644 index 000000000..3f0dd0031 --- /dev/null +++ b/app/presenters/chouette/geometry/general_presenter.rb @@ -0,0 +1,20 @@ +module Chouette::Geometry::GeneralPresenter + +  def to_line_string_feature( stop_areas) +    points = stop_areas.collect(&:geometry).compact +    GeoRuby::SimpleFeatures::LineString.from_points(points) +  end + +  def to_multi_point_feature( stop_areas) +    points = stop_areas.collect(&:geometry).compact +    GeoRuby::SimpleFeatures::MultiPoint.from_points( points ) +  end + +  def to_point_feature( stop_area) +    return nil unless stop_area.longitude && stop_area.latitude +    GeoRuby::SimpleFeatures::Point.from_lon_lat( stop_area.longitude, stop_area.latitude, 4326) +  end + +end + + diff --git a/app/presenters/chouette/geometry/line_presenter.rb b/app/presenters/chouette/geometry/line_presenter.rb new file mode 100644 index 000000000..89ab39185 --- /dev/null +++ b/app/presenters/chouette/geometry/line_presenter.rb @@ -0,0 +1,42 @@ +class Chouette::Geometry::LinePresenter +  include Chouette::Geometry::GeneralPresenter + +  def initialize(line) +    @line = line +  end + +  # return line geometry based on CommercialStopPoint +  # +  def geometry +    features = commercial_links.map { |link| to_line_string_feature(link) } +    GeoRuby::SimpleFeatures::MultiLineString.from_line_strings( features, 4326) +  end +  # +  # return line's stop_areas cloud geometry +  # +  def stop_areas_geometry +    to_multi_point_feature( @line.commercial_stop_areas) +  end + +  def commercial_links +    link_keys = [] +    [].tap do |stop_area_links| +      @line.routes.each do |route| +        previous_commercial = nil +        routes_localized_commercials(route).each do |commercial| +          if previous_commercial && !link_keys.include?( "#{previous_commercial.id}-#{commercial.id}") +            stop_area_links << [ previous_commercial, commercial] +            link_keys << "#{previous_commercial.id}-#{commercial.id}" +            link_keys << "#{commercial.id}-#{previous_commercial.id}" +          end +          previous_commercial = commercial +        end +      end +    end +  end + +  def routes_localized_commercials(route) +    route.stop_areas.map { |sa| sa.parent}.compact.select { |sa| sa.latitude && sa.longitude} +  end + +end diff --git a/app/presenters/chouette/geometry/route_presenter.rb b/app/presenters/chouette/geometry/route_presenter.rb new file mode 100644 index 000000000..292548c91 --- /dev/null +++ b/app/presenters/chouette/geometry/route_presenter.rb @@ -0,0 +1,22 @@ +class Chouette::Geometry::RoutePresenter +  include Chouette::Geometry::GeneralPresenter + +  def initialize(route) +    @route = route +  end + +  # return route's stop_areas cloud geometry +  # +  def stop_areas_geometry +    to_multi_point_feature( @route.stop_areas.with_geometry ) +  end + +  # return route geometry based on BoardingPosition or Quay +  # +  def geometry +    to_line_string_feature( @route.stop_areas.with_geometry ) +  end + + +end + diff --git a/app/presenters/chouette/geometry/stop_area_presenter.rb b/app/presenters/chouette/geometry/stop_area_presenter.rb new file mode 100644 index 000000000..195405eab --- /dev/null +++ b/app/presenters/chouette/geometry/stop_area_presenter.rb @@ -0,0 +1,13 @@ +class Chouette::Geometry::StopAreaPresenter +  include Chouette::Geometry::GeneralPresenter + +  def initialize(stop_area) +    @stop_area = stop_area +  end + +  # return line geometry based on CommercialStopPoint +  # +  def geometry +    to_point_feature( @stop_area) +  end +end | 
