aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/models/chouette/access_link.rb65
-rw-r--r--app/models/chouette/access_point.rb162
-rw-r--r--app/models/chouette/access_point_type.rb50
-rw-r--r--app/models/chouette/active_record.rb66
-rw-r--r--app/models/chouette/area_type.rb56
-rw-r--r--app/models/chouette/command.rb94
-rw-r--r--app/models/chouette/command_line_support.rb35
-rw-r--r--app/models/chouette/company.rb14
-rw-r--r--app/models/chouette/connection_link.rb47
-rw-r--r--app/models/chouette/connection_link_type.rb51
-rw-r--r--app/models/chouette/direction.rb60
-rw-r--r--app/models/chouette/exporter.rb32
-rw-r--r--app/models/chouette/file_validator.rb47
-rw-r--r--app/models/chouette/footnote.rb6
-rw-r--r--app/models/chouette/for_alighting_enumerations.rb8
-rw-r--r--app/models/chouette/for_boarding_enumerations.rb8
-rw-r--r--app/models/chouette/group_of_line.rb28
-rw-r--r--app/models/chouette/journey_frequency.rb36
-rw-r--r--app/models/chouette/journey_pattern.rb109
-rw-r--r--app/models/chouette/journey_pattern_section.rb20
-rw-r--r--app/models/chouette/line.rb74
-rw-r--r--app/models/chouette/link_orientation_type.rb49
-rw-r--r--app/models/chouette/loader.rb110
-rw-r--r--app/models/chouette/network.rb46
-rw-r--r--app/models/chouette/object_id.rb36
-rw-r--r--app/models/chouette/pt_link.rb37
-rw-r--r--app/models/chouette/route.rb183
-rw-r--r--app/models/chouette/route_section.rb82
-rw-r--r--app/models/chouette/source_type.rb56
-rw-r--r--app/models/chouette/stop_area.rb323
-rw-r--r--app/models/chouette/stop_point.rb41
-rw-r--r--app/models/chouette/time_table.rb450
-rw-r--r--app/models/chouette/time_table_date.rb14
-rw-r--r--app/models/chouette/time_table_period.rb40
-rw-r--r--app/models/chouette/timeband.rb30
-rw-r--r--app/models/chouette/transport_mode.rb71
-rw-r--r--app/models/chouette/trident_active_record.rb132
-rw-r--r--app/models/chouette/vehicle_journey.rb114
-rw-r--r--app/models/chouette/vehicle_journey_at_stop.rb48
-rw-r--r--app/models/chouette/vehicle_journey_frequency.rb69
-rw-r--r--app/models/chouette/wayback.rb50
-rw-r--r--app/presenters/chouette/geometry/access_link_presenter.rb11
-rw-r--r--app/presenters/chouette/geometry/access_point_presenter.rb11
-rw-r--r--app/presenters/chouette/geometry/connection_link_presenter.rb11
-rw-r--r--app/presenters/chouette/geometry/general_presenter.rb20
-rw-r--r--app/presenters/chouette/geometry/line_presenter.rb42
-rw-r--r--app/presenters/chouette/geometry/route_presenter.rb22
-rw-r--r--app/presenters/chouette/geometry/stop_area_presenter.rb13
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