diff options
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 |
