swiss-netex 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +77 -0
- data/LICENSE +21 -0
- data/README.md +237 -0
- data/exe/swiss-netex +6 -0
- data/lib/swiss_netex/catalog.rb +41 -0
- data/lib/swiss_netex/cli.rb +546 -0
- data/lib/swiss_netex/dataset_page.rb +90 -0
- data/lib/swiss_netex/download.rb +239 -0
- data/lib/swiss_netex/error.rb +5 -0
- data/lib/swiss_netex/extract.rb +56 -0
- data/lib/swiss_netex/filter/common_frame.rb +194 -0
- data/lib/swiss_netex/filter/frame_worker.rb +188 -0
- data/lib/swiss_netex/filter/frames.rb +126 -0
- data/lib/swiss_netex/filter/refs.rb +106 -0
- data/lib/swiss_netex/filter/service_lines.rb +139 -0
- data/lib/swiss_netex/filter/timetable.rb +387 -0
- data/lib/swiss_netex/filter/timetable_batch.rb +330 -0
- data/lib/swiss_netex/filter/xml_sieve.rb +189 -0
- data/lib/swiss_netex/filter.rb +346 -0
- data/lib/swiss_netex/http.rb +130 -0
- data/lib/swiss_netex/lines.rb +161 -0
- data/lib/swiss_netex/operators.rb +254 -0
- data/lib/swiss_netex/package.rb +145 -0
- data/lib/swiss_netex/package_source.rb +30 -0
- data/lib/swiss_netex/resource_frame.rb +73 -0
- data/lib/swiss_netex/tsv.rb +53 -0
- data/lib/swiss_netex/version.rb +5 -0
- data/lib/swiss_netex.rb +20 -0
- data/swiss-netex.gemspec +40 -0
- metadata +104 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module SwissNetex
|
|
6
|
+
class Filter
|
|
7
|
+
# Per-frame rewrite rules once journey refs are known.
|
|
8
|
+
# Each method returns XmlSieve::Result (tempfile IO + counters).
|
|
9
|
+
#
|
|
10
|
+
# Unknown large entity types are listed as filterable and denied by default so
|
|
11
|
+
# a future national export that adds Route / JourneyPattern / ServiceLink does
|
|
12
|
+
# not pass millions of unreferenced entities through.
|
|
13
|
+
module Frames
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
SERVICE_ENTITIES = %w[
|
|
17
|
+
Line DestinationDisplay ScheduledStopPoint PassengerStopAssignment
|
|
18
|
+
Notice DefaultConnection SiteConnection Direction
|
|
19
|
+
Route JourneyPattern ServiceLink ServiceJourneyPattern TimingLink
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
SITE_ENTITIES = %w[StopPlace TopographicPlace Quay].freeze
|
|
23
|
+
|
|
24
|
+
RESOURCE_ENTITIES = %w[
|
|
25
|
+
Operator ResponsibilitySet TypeOfProductCategory VehicleType TypeOfNotice
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
CALENDAR_ENTITIES = %w[AvailabilityCondition DayType DayTypeAssignment].freeze
|
|
29
|
+
|
|
30
|
+
def service(source, refs)
|
|
31
|
+
rewrite(source, SERVICE_ENTITIES) { |entity| keep_service_entity?(entity, refs) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def site(source, refs)
|
|
35
|
+
rewrite(source, SITE_ENTITIES) do |entity|
|
|
36
|
+
case entity.local_name
|
|
37
|
+
when "TopographicPlace"
|
|
38
|
+
true # small fixed/shared set in Swiss exports
|
|
39
|
+
when "StopPlace"
|
|
40
|
+
# Nested quays travel with kept StopPlace outer_xml; no extra ref buckets.
|
|
41
|
+
refs.include?(:stop_places, entity.id)
|
|
42
|
+
else
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resource(source, refs)
|
|
49
|
+
rewrite(source, RESOURCE_ENTITIES) do |entity|
|
|
50
|
+
case entity.local_name
|
|
51
|
+
when "TypeOfNotice" then true # small catalogue
|
|
52
|
+
when "Operator" then refs.include?(:operators, entity.id)
|
|
53
|
+
when "ResponsibilitySet" then refs.include?(:responsibility_sets, entity.id)
|
|
54
|
+
when "TypeOfProductCategory" then refs.include?(:type_of_product_categories, entity.id)
|
|
55
|
+
when "VehicleType" then refs.include?(:vehicle_types, entity.id)
|
|
56
|
+
else false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def common(source, refs)
|
|
62
|
+
CommonFrame.new(refs).call(source)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def service_calendar(source, refs)
|
|
66
|
+
rewrite(source, CALENDAR_ENTITIES) do |entity|
|
|
67
|
+
case entity.local_name
|
|
68
|
+
when "AvailabilityCondition" then refs.include?(:availability_conditions, entity.id)
|
|
69
|
+
when "DayType" then refs.include?(:day_types, entity.id)
|
|
70
|
+
when "DayTypeAssignment" then day_type_assignment_keep?(entity.outer_xml, refs)
|
|
71
|
+
else false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def rewrite(source, filterable, &)
|
|
77
|
+
XmlSieve.new(filterable: filterable).call(source, &)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def keep_service_entity?(entity, refs)
|
|
81
|
+
case entity.local_name
|
|
82
|
+
when "Line" then refs.include?(:lines, entity.id)
|
|
83
|
+
when "DestinationDisplay" then refs.include?(:destination_displays, entity.id)
|
|
84
|
+
when "ScheduledStopPoint" then refs.include?(:scheduled_stop_points, entity.id)
|
|
85
|
+
when "Notice" then refs.include?(:notices, entity.id)
|
|
86
|
+
when "Direction" then true # small fixed set in Swiss exports
|
|
87
|
+
when "PassengerStopAssignment" then ssp_ref_kept?(entity.outer_xml, refs)
|
|
88
|
+
when "DefaultConnection" then stop_place_ref_kept?(entity.outer_xml, refs)
|
|
89
|
+
when "SiteConnection" then site_connection_kept?(entity.outer_xml, refs)
|
|
90
|
+
else false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ssp_ref_kept?(xml, refs)
|
|
95
|
+
ref = first_ref(xml, "ScheduledStopPointRef")
|
|
96
|
+
ref && refs.include?(:scheduled_stop_points, ref)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def stop_place_ref_kept?(xml, refs)
|
|
100
|
+
ref = first_ref(xml, "StopPlaceRef")
|
|
101
|
+
ref && refs.include?(:stop_places, ref)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Keep walk transfers only when both ends are in the filtered stop set.
|
|
105
|
+
def site_connection_kept?(xml, refs)
|
|
106
|
+
refs_list = all_refs(xml, "StopPlaceRef")
|
|
107
|
+
!refs_list.empty? && refs_list.all? { |ref| refs.include?(:stop_places, ref) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def first_ref(xml, element_name)
|
|
111
|
+
all_refs(xml, element_name).first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def all_refs(xml, element_name)
|
|
115
|
+
text = xml.to_s
|
|
116
|
+
text.scan(/<#{element_name}\b[^>]*\bref="([^"]+)"/).flatten +
|
|
117
|
+
text.scan(/<[^:>]+:#{element_name}\b[^>]*\bref="([^"]+)"/).flatten
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def day_type_assignment_keep?(xml, refs)
|
|
121
|
+
ref = first_ref(xml, "DayTypeRef")
|
|
122
|
+
ref && refs.include?(:day_types, ref)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwissNetex
|
|
4
|
+
class Filter
|
|
5
|
+
# Mutable sets of NeTEx ids collected while filtering, used for transitive keep.
|
|
6
|
+
class Refs
|
|
7
|
+
ATTRS = %i[
|
|
8
|
+
operators
|
|
9
|
+
lines
|
|
10
|
+
scheduled_stop_points
|
|
11
|
+
stop_places
|
|
12
|
+
destination_displays
|
|
13
|
+
notices
|
|
14
|
+
availability_conditions
|
|
15
|
+
day_types
|
|
16
|
+
responsibility_sets
|
|
17
|
+
type_of_product_categories
|
|
18
|
+
type_of_services
|
|
19
|
+
vehicle_types
|
|
20
|
+
service_facility_sets
|
|
21
|
+
train_numbers
|
|
22
|
+
journeys
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
REF_BUCKETS = {
|
|
26
|
+
"OperatorRef" => :operators,
|
|
27
|
+
"LineRef" => :lines,
|
|
28
|
+
"ScheduledStopPointRef" => :scheduled_stop_points,
|
|
29
|
+
"StopPlaceRef" => :stop_places,
|
|
30
|
+
"DestinationDisplayRef" => :destination_displays,
|
|
31
|
+
"NoticeRef" => :notices,
|
|
32
|
+
"AvailabilityConditionRef" => :availability_conditions,
|
|
33
|
+
"DayTypeRef" => :day_types,
|
|
34
|
+
"TypeOfProductCategoryRef" => :type_of_product_categories,
|
|
35
|
+
"TypeOfServiceRef" => :type_of_services,
|
|
36
|
+
"VehicleTypeRef" => :vehicle_types,
|
|
37
|
+
"ServiceFacilitySetRef" => :service_facility_sets,
|
|
38
|
+
"TrainNumberRef" => :train_numbers,
|
|
39
|
+
"ResponsibilitySetRef" => :responsibility_sets
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
attr_reader(*ATTRS)
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
ATTRS.each { |name| instance_variable_set(:"@#{name}", {}) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add(collection, *ids)
|
|
49
|
+
set = public_send(collection)
|
|
50
|
+
ids.flatten.each do |id|
|
|
51
|
+
text = id.to_s.strip
|
|
52
|
+
set[text] = true unless text.empty?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def include?(collection, id)
|
|
57
|
+
public_send(collection).key?(id.to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def size(collection)
|
|
61
|
+
public_send(collection).size
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def merge_from_xml(xml)
|
|
65
|
+
xml.to_s.scan(/<([A-Za-z0-9:]+)[^>]*\bref="([^"]+)"/) do |raw_name, ref|
|
|
66
|
+
bucket = REF_BUCKETS[local_name(raw_name)]
|
|
67
|
+
add(bucket, ref) if bucket
|
|
68
|
+
end
|
|
69
|
+
extract_responsibility_set(xml)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Compact export for IPC (fork workers) / merging batch deltas.
|
|
73
|
+
def export
|
|
74
|
+
ATTRS.each_with_object({}) do |name, hash|
|
|
75
|
+
keys = public_send(name).keys
|
|
76
|
+
hash[name] = keys unless keys.empty?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def merge_export(data)
|
|
81
|
+
return self unless data
|
|
82
|
+
|
|
83
|
+
data.each do |name, ids|
|
|
84
|
+
add(name.to_sym, ids) if ATTRS.include?(name.to_sym)
|
|
85
|
+
end
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def counts
|
|
90
|
+
ATTRS.to_h { |name| [name, public_send(name).size] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def local_name(name)
|
|
96
|
+
name.to_s.split(":", 2).last
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def extract_responsibility_set(xml)
|
|
100
|
+
xml.to_s.scan(/\bresponsibilitySetRef="([^"]+)"/) do |ref,|
|
|
101
|
+
add(:responsibility_sets, ref)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module SwissNetex
|
|
7
|
+
class Filter
|
|
8
|
+
# First SERVICE pass: choose Line ids for the selected operators (and optional
|
|
9
|
+
# line aliases) without rewriting the frame yet.
|
|
10
|
+
class ServiceLines
|
|
11
|
+
Line = Data.define(
|
|
12
|
+
:id,
|
|
13
|
+
:operator_id,
|
|
14
|
+
:public_code,
|
|
15
|
+
:short_name,
|
|
16
|
+
:slnid,
|
|
17
|
+
:name
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def initialize(operator_ids:, line_aliases: [], refs: nil)
|
|
21
|
+
@operator_ids = operator_ids.to_h { |id| [id.to_s, true] }
|
|
22
|
+
@line_aliases = normalize_aliases(line_aliases)
|
|
23
|
+
@refs = refs
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(source)
|
|
27
|
+
kept = {}
|
|
28
|
+
each_line_xml(source) do |xml|
|
|
29
|
+
line = parse_line(xml)
|
|
30
|
+
next unless line && keep_line?(line)
|
|
31
|
+
|
|
32
|
+
kept[line.id] = line
|
|
33
|
+
remember(line.id, xml)
|
|
34
|
+
end
|
|
35
|
+
kept
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def remember(id, xml)
|
|
41
|
+
return unless @refs
|
|
42
|
+
|
|
43
|
+
@refs.add(:lines, id)
|
|
44
|
+
@refs.merge_from_xml(xml)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def keep_line?(line)
|
|
48
|
+
return false unless @operator_ids.key?(line.operator_id)
|
|
49
|
+
return true if @line_aliases.empty?
|
|
50
|
+
|
|
51
|
+
line_matches_alias?(line)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def line_matches_alias?(line)
|
|
55
|
+
[
|
|
56
|
+
line.id,
|
|
57
|
+
line.public_code,
|
|
58
|
+
line.short_name,
|
|
59
|
+
line.slnid,
|
|
60
|
+
line.name
|
|
61
|
+
].compact.map { |value| normalize(value) }.any? { |key| @line_aliases.key?(key) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def each_line_xml(source)
|
|
65
|
+
reader = Nokogiri::XML::Reader(to_io(source))
|
|
66
|
+
while reader.read
|
|
67
|
+
next unless line_element?(reader)
|
|
68
|
+
|
|
69
|
+
xml = reader.outer_xml
|
|
70
|
+
yield xml if xml && !xml.empty?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def line_element?(reader)
|
|
75
|
+
reader.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT &&
|
|
76
|
+
reader.local_name == "Line"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_line(xml)
|
|
80
|
+
root = Nokogiri::XML(xml).root
|
|
81
|
+
return unless root
|
|
82
|
+
|
|
83
|
+
id = root["id"].to_s.strip
|
|
84
|
+
return if id.empty?
|
|
85
|
+
|
|
86
|
+
Line.new(
|
|
87
|
+
id: id,
|
|
88
|
+
operator_id: attr_ref(root, "OperatorRef"),
|
|
89
|
+
public_code: text_at(root, "PublicCode"),
|
|
90
|
+
short_name: text_at(root, "ShortName"),
|
|
91
|
+
slnid: key_value(root, "SLNID"),
|
|
92
|
+
name: text_at(root, "Name")
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def attr_ref(node, element_name)
|
|
97
|
+
child = node.at_xpath(".//*[local-name()=#{element_name.inspect}]")
|
|
98
|
+
blank_to_nil(child&.[]("ref")&.strip)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def text_at(node, element_name)
|
|
102
|
+
child = node.at_xpath(".//*[local-name()=#{element_name.inspect}]")
|
|
103
|
+
blank_to_nil(child&.text&.strip)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def key_value(node, key_name)
|
|
107
|
+
node.xpath(".//*[local-name()='KeyValue']").each do |kv|
|
|
108
|
+
key = kv.at_xpath(".//*[local-name()='Key']")&.text&.strip
|
|
109
|
+
next unless key&.casecmp?(key_name)
|
|
110
|
+
|
|
111
|
+
return blank_to_nil(kv.at_xpath(".//*[local-name()='Value']")&.text&.strip)
|
|
112
|
+
end
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def blank_to_nil(text)
|
|
117
|
+
text.nil? || text.empty? ? nil : text
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def normalize_aliases(aliases)
|
|
121
|
+
Array(aliases).each_with_object({}) do |value, hash|
|
|
122
|
+
key = normalize(value)
|
|
123
|
+
hash[key] = true unless key.empty?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize(value)
|
|
128
|
+
value.to_s.strip.downcase
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def to_io(source)
|
|
132
|
+
return StringIO.new(source) if source.is_a?(String)
|
|
133
|
+
return source if source.respond_to?(:read)
|
|
134
|
+
|
|
135
|
+
raise Error, "ServiceLines expects a String or IO-like object"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|