aixm 0.3.5 → 0.3.6
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 +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +4 -0
- data/CHANGELOG.md +15 -0
- data/README.md +7 -7
- data/aixm.gemspec +1 -0
- data/{Gemfile → gems.rb} +0 -0
- data/lib/aixm.rb +4 -0
- data/lib/aixm/component/fato.rb +255 -0
- data/lib/aixm/component/helipad.rb +59 -4
- data/lib/aixm/component/layer.rb +2 -2
- data/lib/aixm/component/lighting.rb +133 -0
- data/lib/aixm/component/runway.rb +31 -6
- data/lib/aixm/component/surface.rb +45 -2
- data/lib/aixm/d.rb +3 -3
- data/lib/aixm/document.rb +3 -1
- data/lib/aixm/feature/airport.rb +39 -5
- data/lib/aixm/feature/airspace.rb +7 -1
- data/lib/aixm/feature/unit.rb +3 -1
- data/lib/aixm/p.rb +88 -0
- data/lib/aixm/refinements.rb +15 -15
- data/lib/aixm/shortcuts.rb +4 -0
- data/lib/aixm/version.rb +1 -1
- data/lib/aixm/w.rb +86 -0
- data/lib/aixm/xy.rb +6 -0
- data/{Rakefile → rakefile.rb} +10 -0
- data/spec/factory.rb +64 -6
- data/spec/lib/aixm/component/fato_spec.rb +260 -0
- data/spec/lib/aixm/component/helipad_spec.rb +68 -3
- data/spec/lib/aixm/component/lighting_spec.rb +88 -0
- data/spec/lib/aixm/component/runway_spec.rb +63 -3
- data/spec/lib/aixm/component/surface_spec.rb +36 -0
- data/spec/lib/aixm/d_spec.rb +2 -2
- data/spec/lib/aixm/document_spec.rb +280 -2
- data/spec/lib/aixm/feature/airport_spec.rb +161 -8
- data/spec/lib/aixm/p_spec.rb +189 -0
- data/spec/lib/aixm/refinements_spec.rb +16 -16
- data/spec/lib/aixm/w_spec.rb +150 -0
- data/spec/lib/aixm/xy_spec.rb +11 -0
- data/spec/macros/marking.rb +12 -0
- metadata +32 -4
@@ -8,7 +8,7 @@ module AIXM
|
|
8
8
|
# ===Cheat Sheet in Pseudo Code:
|
9
9
|
# airspace = AIXM.airspace(
|
10
10
|
# source: String or nil
|
11
|
-
# id: String
|
11
|
+
# id: String or nil # nil is converted to an 8 character digest
|
12
12
|
# type: String or Symbol
|
13
13
|
# local_type: String or nil
|
14
14
|
# name: String or nil
|
@@ -16,6 +16,10 @@ module AIXM
|
|
16
16
|
# airspace.geometry << AIXM.point or AIXM.arc or AIXM.border or AIXM.circle
|
17
17
|
# airspace.layers << AIXM.layer
|
18
18
|
#
|
19
|
+
# The +id+ is mandatory, however, you may omit it when initializing a new
|
20
|
+
# airspace or assign +nil+ to an existing airspace which will generate a 8
|
21
|
+
# character digest from +type+, +local_type+ and +name+.
|
22
|
+
#
|
19
23
|
# Some regions define additional airspace types. In LF (France) for
|
20
24
|
# intance, the types RMZ (radio mandatory zone) and TMZ (transponder
|
21
25
|
# mandatory zone) exist. Such airspaces are usually specified together
|
@@ -105,6 +109,8 @@ module AIXM
|
|
105
109
|
%Q(#<#{self.class} type=#{type.inspect} name=#{name.inspect}>)
|
106
110
|
end
|
107
111
|
|
112
|
+
# The +id+ is mandatory, however, you may assign +nil+ which will generate
|
113
|
+
# an 8 character digest from +type+, +local_type+ and +name+.
|
108
114
|
def id=(value)
|
109
115
|
fail(ArgumentError, "invalid id") unless value.nil? || value.is_a?(String)
|
110
116
|
@id = value&.uptrans || [type, local_type, name].to_digest.upcase
|
data/lib/aixm/feature/unit.rb
CHANGED
@@ -100,7 +100,7 @@ module AIXM
|
|
100
100
|
|
101
101
|
# @return [String]
|
102
102
|
def inspect
|
103
|
-
%Q(#<#{
|
103
|
+
%Q(#<#{original_class} name=#{name.inspect} type=#{type.inspect}>)
|
104
104
|
end
|
105
105
|
|
106
106
|
def organisation=(value)
|
@@ -118,7 +118,9 @@ module AIXM
|
|
118
118
|
end
|
119
119
|
|
120
120
|
# @!attribute class
|
121
|
+
# @note Use +original_class+ to query the Ruby object class.
|
121
122
|
# @return [Symbol] class of unit (see {CLASSES})
|
123
|
+
alias_method :original_class, :class
|
122
124
|
def class
|
123
125
|
@klass
|
124
126
|
end
|
data/lib/aixm/p.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
using AIXM::Refinements
|
2
|
+
|
3
|
+
module AIXM
|
4
|
+
|
5
|
+
# pressure
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# AIXM.d(14, :bar)
|
9
|
+
class P
|
10
|
+
include Comparable
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
UNITS = {
|
14
|
+
p: { mpa: 0.000001, psi: 0.000145037738, bar: 0.00001, torr: 0.0075006 },
|
15
|
+
mpa: { p: 1_000_000, psi: 145.037738, bar: 10, torr: 7500.6 },
|
16
|
+
psi: { p: 6894.75729, mpa: 0.00689475729, bar: 0.0689475729, torr: 51.714816529374 },
|
17
|
+
bar: { p: 100000, mpa: 0.1, psi: 14.5037738, torr: 750.06 },
|
18
|
+
torr: { p: 133.322, mpa: 0.000133322, psi: 0.019336721305636, bar: 0.00133322 }
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# @!method zero?
|
22
|
+
# @return [Boolean] whether pressure is zero
|
23
|
+
def_delegator :@pres, :zero?
|
24
|
+
|
25
|
+
# @return [Float] pressure
|
26
|
+
attr_reader :pres
|
27
|
+
|
28
|
+
# @return [Symbol] unit (see {UNITS})
|
29
|
+
attr_reader :unit
|
30
|
+
|
31
|
+
def initialize(pres, unit)
|
32
|
+
self.pres, self.unit = pres, unit
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [String]
|
36
|
+
def inspect
|
37
|
+
%Q(#<#{self.class} #{to_s}>)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [String] human readable representation (e.g. "14 bar")
|
41
|
+
def to_s
|
42
|
+
[pres, unit].join(' ')
|
43
|
+
end
|
44
|
+
|
45
|
+
def pres=(value)
|
46
|
+
fail(ArgumentError, "invalid pres") unless value.is_a?(Numeric) && value >= 0
|
47
|
+
@pres = value.to_f
|
48
|
+
end
|
49
|
+
|
50
|
+
def unit=(value)
|
51
|
+
fail(ArgumentError, "invalid unit") unless value.respond_to? :to_sym
|
52
|
+
@unit = value.to_sym.downcase
|
53
|
+
fail(ArgumentError, "invalid unit") unless UNITS.has_key? @unit
|
54
|
+
end
|
55
|
+
|
56
|
+
# @!method to_p
|
57
|
+
# @!method to_mpa
|
58
|
+
# @!method to_psi
|
59
|
+
# @!method to_bar
|
60
|
+
# @!method to_torr
|
61
|
+
# @return [AIXM::P] convert pressure
|
62
|
+
UNITS.each_key do |target_unit|
|
63
|
+
define_method "to_#{target_unit}" do
|
64
|
+
return self if unit == target_unit
|
65
|
+
self.class.new((pres * UNITS[unit][target_unit]).round(8), target_unit)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @see Object#<=>
|
70
|
+
# @return [Integer]
|
71
|
+
def <=>(other)
|
72
|
+
pres <=> other.send(:"to_#{unit}").pres
|
73
|
+
end
|
74
|
+
|
75
|
+
# @see Object#==
|
76
|
+
# @return [Boolean]
|
77
|
+
def ==(other)
|
78
|
+
self.class === other && (self <=> other).zero?
|
79
|
+
end
|
80
|
+
alias_method :eql?, :==
|
81
|
+
|
82
|
+
# @see Object#hash
|
83
|
+
# @return [Integer]
|
84
|
+
def hash
|
85
|
+
to_s.hash
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/aixm/refinements.rb
CHANGED
@@ -15,21 +15,6 @@ module AIXM
|
|
15
15
|
"Ø" => "Oe"
|
16
16
|
}.freeze
|
17
17
|
|
18
|
-
# @!method then_if
|
19
|
-
# Same as +Object#then+ but only applied if the condition is true.
|
20
|
-
#
|
21
|
-
# @example
|
22
|
-
# "foobar".then_if(false) { |s| s.gsub(/o/, 'i') } # => "foobar"
|
23
|
-
# "foobar".then_if(true) { |s| s.gsub(/o/, 'i') } # => "fiibar"
|
24
|
-
#
|
25
|
-
# @note This is a refinement for +Object+
|
26
|
-
# @return [Object]
|
27
|
-
refine Object do
|
28
|
-
def then_if(condition, &block)
|
29
|
-
condition ? self.then(&block) : self
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
18
|
# @!method to_digest
|
34
19
|
# Builds a 4 byte hex digest from the Array payload.
|
35
20
|
#
|
@@ -149,6 +134,21 @@ module AIXM
|
|
149
134
|
end
|
150
135
|
end
|
151
136
|
|
137
|
+
# @!method then_if
|
138
|
+
# Same as +Object#then+ but only applied if the condition is true.
|
139
|
+
#
|
140
|
+
# @example
|
141
|
+
# "foobar".then_if(false) { |s| s.gsub(/o/, 'i') } # => "foobar"
|
142
|
+
# "foobar".then_if(true) { |s| s.gsub(/o/, 'i') } # => "fiibar"
|
143
|
+
#
|
144
|
+
# @note This is a refinement for +Object+
|
145
|
+
# @return [Object]
|
146
|
+
refine Object do
|
147
|
+
def then_if(condition, &block)
|
148
|
+
condition ? self.then(&block) : self
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
152
|
# @!method decapture
|
153
153
|
# Replace all groups with non-caputuring groups
|
154
154
|
#
|
data/lib/aixm/shortcuts.rb
CHANGED
@@ -8,6 +8,8 @@ module AIXM
|
|
8
8
|
d: D,
|
9
9
|
f: F,
|
10
10
|
a: A,
|
11
|
+
w: W,
|
12
|
+
p: P,
|
11
13
|
address: Feature::Address,
|
12
14
|
organisation: Feature::Organisation,
|
13
15
|
unit: Feature::Unit,
|
@@ -15,8 +17,10 @@ module AIXM
|
|
15
17
|
frequency: Component::Frequency,
|
16
18
|
airport: Feature::Airport,
|
17
19
|
runway: Component::Runway,
|
20
|
+
fato: Component::FATO,
|
18
21
|
helipad: Component::Helipad,
|
19
22
|
surface: Component::Surface,
|
23
|
+
lighting: Component::Lighting,
|
20
24
|
airspace: Feature::Airspace,
|
21
25
|
layer: Component::Layer,
|
22
26
|
geometry: Component::Geometry,
|
data/lib/aixm/version.rb
CHANGED
data/lib/aixm/w.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
using AIXM::Refinements
|
2
|
+
|
3
|
+
module AIXM
|
4
|
+
|
5
|
+
# Weight
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# AIXM.w(2.9, :t)
|
9
|
+
class W
|
10
|
+
include Comparable
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
UNITS = {
|
14
|
+
kg: { t: 0.001, lb: 2.204622622, ton: 0.00110231131 },
|
15
|
+
t: { kg: 1000, lb: 2204.622622, ton: 1.10231131 },
|
16
|
+
lb: { kg: 0.45359237, t: 0.00045359237, ton: 0.000499999999581 },
|
17
|
+
ton: { kg: 907.18474, t: 0.90718474, lb: 2000.00000013718828 }
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# @!method zero?
|
21
|
+
# @return [Boolean] whether weight is zero
|
22
|
+
def_delegator :@wgt, :zero?
|
23
|
+
|
24
|
+
# @return [Float] weight
|
25
|
+
attr_reader :wgt
|
26
|
+
|
27
|
+
# @return [Symbol] unit (see {UNITS})
|
28
|
+
attr_reader :unit
|
29
|
+
|
30
|
+
def initialize(wgt, unit)
|
31
|
+
self.wgt, self.unit = wgt, unit
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String]
|
35
|
+
def inspect
|
36
|
+
%Q(#<#{self.class} #{to_s}>)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String] human readable representation (e.g. "123 t")
|
40
|
+
def to_s
|
41
|
+
[wgt, unit].join(' ')
|
42
|
+
end
|
43
|
+
|
44
|
+
def wgt=(value)
|
45
|
+
fail(ArgumentError, "invalid wgt") unless value.is_a?(Numeric) && value >= 0
|
46
|
+
@wgt = value.to_f
|
47
|
+
end
|
48
|
+
|
49
|
+
def unit=(value)
|
50
|
+
fail(ArgumentError, "invalid unit") unless value.respond_to? :to_sym
|
51
|
+
@unit = value.to_sym.downcase
|
52
|
+
fail(ArgumentError, "invalid unit") unless UNITS.has_key? @unit
|
53
|
+
end
|
54
|
+
|
55
|
+
# @!method to_kg
|
56
|
+
# @!method to_t
|
57
|
+
# @!method to_lb
|
58
|
+
# @!method to_ton
|
59
|
+
# @return [AIXM::W] convert weight
|
60
|
+
UNITS.each_key do |target_unit|
|
61
|
+
define_method "to_#{target_unit}" do
|
62
|
+
return self if unit == target_unit
|
63
|
+
self.class.new((wgt * UNITS[unit][target_unit]).round(8), target_unit)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @see Object#<=>
|
68
|
+
# @return [Integer]
|
69
|
+
def <=>(other)
|
70
|
+
wgt <=> other.send(:"to_#{unit}").wgt
|
71
|
+
end
|
72
|
+
|
73
|
+
# @see Object#==
|
74
|
+
# @return [Boolean]
|
75
|
+
def ==(other)
|
76
|
+
self.class === other && (self <=> other).zero?
|
77
|
+
end
|
78
|
+
alias_method :eql?, :==
|
79
|
+
|
80
|
+
# @see Object#hash
|
81
|
+
# @return [Integer]
|
82
|
+
def hash
|
83
|
+
to_s.hash
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/aixm/xy.rb
CHANGED
@@ -69,6 +69,12 @@ module AIXM
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
+
# @return [Boolean] +false+ if both longitude and latitude have zero DMS
|
73
|
+
# seconds which may indicate rounded or estimated coordinates
|
74
|
+
def seconds?
|
75
|
+
!(long.to_dms[-6,5].to_f.zero? && lat.to_dms[-6,5].to_f.zero?)
|
76
|
+
end
|
77
|
+
|
72
78
|
# @return [AIXM::Component::Geometry::Point] convert to point
|
73
79
|
def to_point
|
74
80
|
AIXM.point(xy: self)
|
data/{Rakefile → rakefile.rb}
RENAMED
@@ -9,4 +9,14 @@ Rake::TestTask.new do |t|
|
|
9
9
|
t.warning = true
|
10
10
|
end
|
11
11
|
|
12
|
+
desc "Run local YARD documentation server"
|
13
|
+
task :yard do
|
14
|
+
`rm -rf ./.yardoc`
|
15
|
+
Thread.new do
|
16
|
+
sleep 2
|
17
|
+
`open http://localhost:8808`
|
18
|
+
end
|
19
|
+
`yard server -r`
|
20
|
+
end
|
21
|
+
|
12
22
|
task default: :test
|
data/spec/factory.rb
CHANGED
@@ -24,6 +24,14 @@ module AIXM
|
|
24
24
|
AIXM.a('34L')
|
25
25
|
end
|
26
26
|
|
27
|
+
def w
|
28
|
+
AIXM.w(1.5, :t)
|
29
|
+
end
|
30
|
+
|
31
|
+
def p
|
32
|
+
AIXM.p(0.5, :mpa)
|
33
|
+
end
|
34
|
+
|
27
35
|
# Components
|
28
36
|
|
29
37
|
def address
|
@@ -36,11 +44,22 @@ module AIXM
|
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
47
|
+
def lighting
|
48
|
+
AIXM.lighting(
|
49
|
+
position: :aiming_point
|
50
|
+
).tap do |lighting|
|
51
|
+
lighting.description = "omnidirectional"
|
52
|
+
lighting.intensity = :medium
|
53
|
+
lighting.color = :green
|
54
|
+
lighting.remarks = "lighting remarks"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
39
58
|
def timetable
|
40
59
|
AIXM.timetable(
|
41
60
|
code: :sunrise_to_sunset
|
42
61
|
).tap do |timetable|
|
43
|
-
timetable.remarks =
|
62
|
+
timetable.remarks = "timetable remarks"
|
44
63
|
end
|
45
64
|
end
|
46
65
|
|
@@ -317,9 +336,12 @@ module AIXM
|
|
317
336
|
airport.z = AIXM.z(146, :qnh)
|
318
337
|
airport.declination = 1.08
|
319
338
|
airport.transition_z = AIXM.z(10_000, :qnh)
|
339
|
+
airport.operator = "Municipality of Pujaut"
|
320
340
|
airport.remarks = "Restricted access"
|
321
341
|
airport.add_runway(runway)
|
342
|
+
airport.add_fato(fato)
|
322
343
|
airport.add_helipad(helipad)
|
344
|
+
airport.helipads.first.fato = airport.fatos.first # necessary when using factories only
|
323
345
|
airport.add_usage_limitation :permitted
|
324
346
|
airport.add_usage_limitation(:reservation_required) do |reservation_required|
|
325
347
|
reservation_required.add_condition { |c| c.aircraft = :glider }
|
@@ -339,6 +361,9 @@ module AIXM
|
|
339
361
|
runway.surface.preparation = :paved
|
340
362
|
runway.surface.condition = :good
|
341
363
|
runway.surface.pcn = "59/F/A/W/T"
|
364
|
+
runway.surface.siwl_weight = AIXM.w(1500, :kg)
|
365
|
+
runway.surface.siwl_tire_pressure = AIXM.p(0.5, :mpa)
|
366
|
+
runway.surface.auw_weight = AIXM.w(30, :t)
|
342
367
|
runway.surface.remarks = "Paved shoulder on 2.5m on each side of the RWY."
|
343
368
|
runway.status = :closed
|
344
369
|
runway.remarks = "Markings eroded"
|
@@ -348,18 +373,46 @@ module AIXM
|
|
348
373
|
runway.forth.geographic_orientation = AIXM.a(165)
|
349
374
|
runway.forth.vfr_pattern = :left_or_right
|
350
375
|
runway.forth.remarks = "forth remarks"
|
376
|
+
runway.forth.add_lighting(lighting)
|
351
377
|
runway.back.xy = AIXM.xy(lat: %q(43°59'25.31"N), long: %q(004°45'23.24"E))
|
352
378
|
runway.back.z = AIXM.z(147, :qnh)
|
353
379
|
runway.back.displaced_threshold = AIXM.xy(lat: %q(43°59'31.84"N), long: %q(004°45'20.85"E))
|
354
380
|
runway.back.geographic_orientation = AIXM.a(345)
|
355
381
|
runway.back.vfr_pattern = :left
|
356
382
|
runway.back.remarks = "back remarks"
|
383
|
+
runway.back.add_lighting(lighting)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def fato
|
388
|
+
AIXM.fato(name: 'H1').tap do |fato|
|
389
|
+
fato.length = AIXM.d(35, :m)
|
390
|
+
fato.width = AIXM.d(35, :m)
|
391
|
+
fato.surface.composition = :concrete
|
392
|
+
fato.surface.preparation = :paved
|
393
|
+
fato.surface.condition = :fair
|
394
|
+
fato.surface.pcn = "30/F/A/W/U"
|
395
|
+
fato.surface.siwl_weight = AIXM.w(1500, :kg)
|
396
|
+
fato.surface.siwl_tire_pressure = AIXM.p(0.5, :mpa)
|
397
|
+
fato.surface.auw_weight = AIXM.w(8, :t)
|
398
|
+
fato.surface.remarks = "Cracks near the center"
|
399
|
+
fato.profile = "Northwest from RWY 12/30"
|
400
|
+
fato.marking = "Dashed white lines"
|
401
|
+
fato.status = :other
|
402
|
+
fato.remarks = "Authorizaton by AD operator required"
|
403
|
+
fato.add_direction(name: '35') do |direction|
|
404
|
+
direction.geographic_orientation = AIXM.a(355)
|
405
|
+
direction.remarks = "Avoid flight over residental area"
|
406
|
+
direction.add_lighting(lighting)
|
407
|
+
end
|
357
408
|
end
|
358
409
|
end
|
359
410
|
|
360
411
|
def helipad
|
361
|
-
AIXM.helipad(
|
362
|
-
|
412
|
+
AIXM.helipad(
|
413
|
+
name: 'H1',
|
414
|
+
xy: AIXM.xy(lat: %q(43°59'56.94"N), long: %q(004°45'05.56"E))
|
415
|
+
).tap do |helipad|
|
363
416
|
helipad.z = AIXM.z(141, :qnh)
|
364
417
|
helipad.length = AIXM.d(20, :m)
|
365
418
|
helipad.width = AIXM.d(20, :m)
|
@@ -367,9 +420,15 @@ module AIXM
|
|
367
420
|
helipad.surface.preparation = :paved
|
368
421
|
helipad.surface.condition = :fair
|
369
422
|
helipad.surface.pcn = "30/F/A/W/U"
|
370
|
-
helipad.surface.
|
423
|
+
helipad.surface.siwl_weight = AIXM.w(1500, :kg)
|
424
|
+
helipad.surface.siwl_tire_pressure = AIXM.p(0.5, :mpa)
|
425
|
+
helipad.surface.auw_weight = AIXM.w(8, :t)
|
426
|
+
helipad.surface.remarks = "Cracks near the center"
|
427
|
+
helipad.marking = "Continuous white lines"
|
428
|
+
helipad.helicopter_class = 1
|
371
429
|
helipad.status = :other
|
372
430
|
helipad.remarks = "Authorizaton by AD operator required"
|
431
|
+
helipad.add_lighting(lighting)
|
373
432
|
end
|
374
433
|
end
|
375
434
|
|
@@ -470,11 +529,10 @@ module AIXM
|
|
470
529
|
# Document
|
471
530
|
|
472
531
|
def document
|
473
|
-
time = Time.parse('2018-01-01 12:00:00 +0100')
|
474
532
|
AIXM.document(
|
475
533
|
region: 'LF',
|
476
534
|
namespace: '00000000-0000-0000-0000-000000000000',
|
477
|
-
created_at: time,
|
535
|
+
created_at: (time = Time.parse('2018-01-01 12:00:00 +0100')),
|
478
536
|
effective_at: time
|
479
537
|
).tap do |document|
|
480
538
|
document.features << organisation
|