aixm 0.3.1 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da3e3c10902d2c8b47bea41e1056a5447890e1ebeb9d7fb651115cc894f10067
4
- data.tar.gz: dc2edb38a94804aa7c2c89d2cfcad6872a909598e77ac266fafebf2c39f7deb0
3
+ metadata.gz: d3f7a1a1f47a40725a1f7444d97ca6f11acf3c1805d3d2521d9c79c72494abe5
4
+ data.tar.gz: a45d00f0667b808e4e44f5a065392c4d0f6a00fbcd7e085c8bab072288fcdf7b
5
5
  SHA512:
6
- metadata.gz: 70a1af692e7141c60c49b3c2426be8037f030acc7808a3ec12029bc8df5bc62681396eb3b8e42add5aff92c10d010156a4cc920f8febf0b70a576a1fb5bd7ac2
7
- data.tar.gz: c5dc5a92ebef4b7dea61928c794dd2d38c64c9d6a83fc305cb6b6105dff3adb7cb34b4c5eccb95501a14dbb89317c311924c5928a84fa333c165112cda63f820
6
+ metadata.gz: e58d749b2c1face5c6ec67c1e0f23e5fba2ebca6e3d111cc5075ce561c6c7d6b022585f2cdf0ace8e9320f7b1684385f7ab7bb7c1ca80ecf485afb19e7257cba
7
+ data.tar.gz: 5888395288986381ed501d7b7a488729e6be98dc65ed1e77c1bb9522022b95cc7d62f13031272edb86c86a3ce41c301917c30e5cce1e88756d88688b8ca2bd87
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.3.2
2
+
3
+ #### Additions
4
+ * Obstacle and obstacle group features
5
+ * `AIXM::D` (distance)
6
+
7
+ #### Breaking Changes
8
+ * All distances (circle geometry radius, helipad and runway length/width) must
9
+ be `AIXM::D`.
10
+ * `AIXM::XY#distance` now returns `AIXM::D`
11
+ * Removed obsolete refinement `Float#to_km` (use `AIXM::D#to_km` instead)
12
+
1
13
  ## 0.3.1
2
14
 
3
15
  #### Additions
@@ -9,7 +21,7 @@
9
21
  #### Breaking Changes
10
22
  * Renamed `Airport#code` to `Airport#id`
11
23
  * Renamed `Airspace#short_name` to `Airspace#local_type`
12
- * Moved `region` attribute from features to Document
24
+ * Moved `region` attribute from features to Document
13
25
 
14
26
  #### Changes
15
27
  * Be more permissive on `Airport#id` in order to accomodate generated codes
@@ -27,7 +39,7 @@
27
39
  * Removed `Array#to_digest`
28
40
  * Removed `Document#complete?`
29
41
  * Renamed Schedule to Timetable
30
- * Timetable and remarks moved from Airspace to to Layer (formerly known as class layer)
42
+ * Timetable and remarks moved from Airspace to Layer (formerly known as class layer)
31
43
 
32
44
  #### Additions
33
45
  * Organization and Unit features
@@ -69,7 +81,7 @@
69
81
  * Symbols such as `:qnh`, `:ofm` or `:mhz` are downcased now
70
82
 
71
83
  #### Additions
72
- * Frequency
84
+ * `AIXM::F` (frequency)
73
85
  * Navigational aids features
74
86
  * `AIXM::Z#qfe?` and friends
75
87
 
@@ -111,11 +123,11 @@
111
123
  ## 0.1.0
112
124
 
113
125
  #### Initial Implementation
114
- * XY Coordinate
115
- * Z Altitude
126
+ * `AIXM::XY` (coordinates)
127
+ * `AIXM::Z` (altitude or elevation)
116
128
  * AIXM-Snapshot 4.5 Document
117
129
  * Airspace feature
118
- * Vertical Limits
130
+ * Vertical limits
119
131
  * Geometry
120
132
  * Point
121
133
  * Arc
data/README.md CHANGED
@@ -74,6 +74,7 @@ AIXM.schema(:version) # => 0
74
74
  * [Document](http://www.rubydoc.info/gems/aixm/AIXM/Document.html)
75
75
  * [XY (longitude and latitude)](http://www.rubydoc.info/gems/aixm/AIXM/XY.html)
76
76
  * [Z (height, elevation or altitude)](http://www.rubydoc.info/gems/aixm/AIXM/Z.html)
77
+ * [D (distance or length)](http://www.rubydoc.info/gems/aixm/AIXM/D.html)
77
78
  * [F (frequency)](http://www.rubydoc.info/gems/aixm/AIXM/F.html)
78
79
 
79
80
  ### Features
@@ -88,6 +89,7 @@ AIXM.schema(:version) # => 0
88
89
  * [NDB](http://www.rubydoc.info/gems/aixm/AIXM/Feature/NDB.html)
89
90
  * [TACAN](http://www.rubydoc.info/gems/aixm/AIXM/Feature/TACAN.html)
90
91
  * [VOR](http://www.rubydoc.info/gems/aixm/AIXM/Feature/VOR.html)
92
+ * [Obstacle and obstacle group](http://www.rubydoc.info/gems/aixm/AIXM/Feature/Obstacle.html)
91
93
 
92
94
  ### Components
93
95
  * [Service](http://www.rubydoc.info/gems/aixm/AIXM/Component/Service.html)
data/lib/aixm.rb CHANGED
@@ -14,6 +14,7 @@ require_relative 'aixm/errors'
14
14
  require_relative 'aixm/document'
15
15
  require_relative 'aixm/xy'
16
16
  require_relative 'aixm/z'
17
+ require_relative 'aixm/d'
17
18
  require_relative 'aixm/f'
18
19
 
19
20
  require_relative 'aixm/component'
@@ -42,5 +43,7 @@ require_relative 'aixm/feature/navigational_aid/marker'
42
43
  require_relative 'aixm/feature/navigational_aid/ndb'
43
44
  require_relative 'aixm/feature/navigational_aid/tacan'
44
45
  require_relative 'aixm/feature/navigational_aid/vor'
46
+ require_relative 'aixm/feature/obstacle'
47
+ require_relative 'aixm/feature/obstacle_group'
45
48
 
46
49
  require_relative 'aixm/shortcuts'
@@ -4,12 +4,12 @@ module AIXM
4
4
  class Component
5
5
  class Geometry
6
6
 
7
- # Circles are defined by a {#center_xy} and a {#radius} in kilometers.
7
+ # Circles are defined by a {#center_xy} and a {#radius}.
8
8
  #
9
9
  # ===Cheat Sheet in Pseudo Code:
10
10
  # circle = AIXM.circle(
11
11
  # center_xy: AIXM.xy
12
- # radius: Numeric # kilometers
12
+ # radius: AIXM.d
13
13
  # )
14
14
  #
15
15
  # @see https://github.com/openflightmaps/ofmx/wiki/Airspace#circle
@@ -17,7 +17,7 @@ module AIXM
17
17
  # @return [AIXM::XY] center point
18
18
  attr_reader :center_xy
19
19
 
20
- # @return [Integer] circle radius
20
+ # @return [AIXM::D] circle radius
21
21
  attr_reader :radius
22
22
 
23
23
  def initialize(center_xy:, radius:)
@@ -35,8 +35,8 @@ module AIXM
35
35
  end
36
36
 
37
37
  def radius=(value)
38
- fail(ArgumentError, "invalid radius") unless value.is_a?(Numeric) && value > 0
39
- @radius = value.to_f
38
+ fail(ArgumentError, "invalid radius") unless value.is_a?(AIXM::D) && value.dist > 0
39
+ @radius = value
40
40
  end
41
41
 
42
42
  # @return [String] AIXM or OFMX markup
@@ -58,7 +58,7 @@ module AIXM
58
58
  # and on the circumference of the circle.
59
59
  def north_xy
60
60
  AIXM.xy(
61
- lat: center_xy.lat + radius.to_f / (AIXM::XY::EARTH_RADIUS / 1000) * 180 / Math::PI,
61
+ lat: center_xy.lat + radius.to_km.dist / (AIXM::XY::EARTH_RADIUS / 1000) * 180 / Math::PI,
62
62
  long: center_xy.long
63
63
  )
64
64
  end
@@ -11,8 +11,8 @@ module AIXM
11
11
  # )
12
12
  # helipad.xy = AIXM.xy
13
13
  # helipad.z = AIXM.z or nil
14
- # helipad.length = Integer or nil # meters
15
- # helipad.width = Integer or nil # meters
14
+ # helipad.length = AIXM.d or nil # must use same unit as width
15
+ # helipad.width = AIXM.d or nil # must use same unit as length
16
16
  # helipad.composition = COMPOSITIONS or nil
17
17
  # helipad.status = STATUSES or nil
18
18
  # helipad.remarks = String or nil
@@ -50,13 +50,13 @@ module AIXM
50
50
  # @return [AIXM::XY] center point
51
51
  attr_reader :xy
52
52
 
53
- # @return [AIXM:Z, nil] elevation in +:qnh+
53
+ # @return [AIXM::Z, nil] elevation in +:qnh+
54
54
  attr_reader :z
55
55
 
56
- # @return [Integer, nil] length in meters
56
+ # @return [AIXM::D, nil] length
57
57
  attr_reader :length
58
58
 
59
- # @return [Integer, nil] width in meters
59
+ # @return [AIXM::D, nil] width
60
60
  attr_reader :width
61
61
 
62
62
  # @return [Symbol, nil] composition of the surface (see {COMPOSITIONS})
@@ -99,13 +99,19 @@ module AIXM
99
99
  end
100
100
 
101
101
  def length=(value)
102
- fail(ArgumentError, "invalid length") unless value.nil? || (value.is_a?(Numeric) && value > 0)
103
- @length = value.nil? ? nil : value.to_i
102
+ @length = if value
103
+ fail(ArgumentError, "invalid length") unless value.is_a?(AIXM::D) && value.dist > 0
104
+ fail(ArgumentError, "invalid length unit") if width && width.unit != value.unit
105
+ @length = value
106
+ end
104
107
  end
105
108
 
106
109
  def width=(value)
107
- fail(ArgumentError, "invalid width") unless value.nil? || (value.is_a?(Numeric) && value > 0)
108
- @width = value.nil? ? nil : value.to_i
110
+ @width = if value
111
+ fail(ArgumentError, "invalid width") unless value.is_a?(AIXM::D) && value.dist > 0
112
+ fail(ArgumentError, "invalid width unit") if length && length.unit != value.unit
113
+ @width = value
114
+ end
109
115
  end
110
116
 
111
117
  def composition=(value)
@@ -141,9 +147,10 @@ module AIXM
141
147
  tla.valElev(z.alt)
142
148
  tla.uomDistVer(z.unit.upcase.to_s)
143
149
  end
144
- tla.valLen(length) if length
145
- tla.valWid(width) if width
146
- tla.uomDim('M') if length || width
150
+ tla.valLen(length.dist.trim) if length
151
+ tla.valWid(width.dist.trim) if width
152
+ tla.uomDim(length.unit.to_s.upcase) if length
153
+ tla.uomDim(width.unit.to_s.upcase) if width && !length
147
154
  tla.codeComposition(COMPOSITIONS.key(composition).to_s) if composition
148
155
  tla.codeSts(STATUSES.key(status).to_s) if status
149
156
  tla.txtRmk(remarks) if remarks
@@ -16,8 +16,8 @@ module AIXM
16
16
  # runway = AIXM.runway(
17
17
  # name: String
18
18
  # )
19
- # runway.length = Integer or nil # meters
20
- # runway.width = Integer or nil # meters
19
+ # runway.length = AIXM.d or nil # must use same unit as width
20
+ # runway.width = AIXM.d or nil # must use same unit as length
21
21
  # runway.composition = COMPOSITIONS or nil
22
22
  # runway.status = STATUSES or nil
23
23
  # runway.remarks = String or nil
@@ -25,7 +25,7 @@ module AIXM
25
25
  # runway.forth.geographic_orientation = Integer or nil # degrees
26
26
  # runway.forth.xy = AIXM.xy
27
27
  # runway.forth.z = AIXM.z or nil
28
- # runway.forth.displaced_threshold = Integer or nil # meters
28
+ # runway.forth.displaced_threshold = AIXM.xy or AIXM.d or nil
29
29
  # runway.forth.remarks = String or nil
30
30
  #
31
31
  # @example Bidirectional runway
@@ -73,10 +73,10 @@ module AIXM
73
73
  # @return [String] full name of runway (e.g. "12/30" or "16L/34R")
74
74
  attr_reader :name
75
75
 
76
- # @return [Integer, nil] length in meters
76
+ # @return [AIXM::D, nil] length
77
77
  attr_reader :length
78
78
 
79
- # @return [Integer, nil] width in meters
79
+ # @return [AIXM::D, nil] width
80
80
  attr_reader :width
81
81
 
82
82
  # @return [Symbol, nil] composition of the surface (see {COMPOSITIONS})
@@ -119,13 +119,19 @@ module AIXM
119
119
  end
120
120
 
121
121
  def length=(value)
122
- fail(ArgumentError, "invalid length") unless value.nil? || (value.is_a?(Numeric) && value > 0)
123
- @length = value.nil? ? nil : value.to_i
122
+ @length = if value
123
+ fail(ArgumentError, "invalid length") unless value.is_a?(AIXM::D) && value.dist > 0
124
+ fail(ArgumentError, "invalid length unit") if width && width.unit != value.unit
125
+ @length = value
126
+ end
124
127
  end
125
128
 
126
129
  def width=(value)
127
- fail(ArgumentError, "invalid width") unless value.nil? || (value.is_a?(Numeric) && value > 0)
128
- @width = value.nil? ? nil : value.to_i
130
+ @width = if value
131
+ fail(ArgumentError, "invalid width") unless value.is_a?(AIXM::D) && value.dist > 0
132
+ fail(ArgumentError, "invalid width unit") if length && length.unit != value.unit
133
+ @width = value
134
+ end
129
135
  end
130
136
 
131
137
  def composition=(value)
@@ -154,9 +160,10 @@ module AIXM
154
160
  builder = Builder::XmlMarkup.new(indent: 2)
155
161
  builder.Rwy do |rwy|
156
162
  rwy << to_uid.indent(2)
157
- rwy.valLen(length) if length
158
- rwy.valWid(width) if width
159
- rwy.uomDimRwy('M') if length || width
163
+ rwy.valLen(length.dist.trim) if length
164
+ rwy.valWid(width.dist.trim) if width
165
+ rwy.uomDimRwy(length.unit.to_s.upcase) if length
166
+ rwy.uomDimRwy(width.unit.to_s.upcase) if width && !length
160
167
  rwy.codeComposition(COMPOSITIONS.key(composition).to_s) if composition
161
168
  rwy.codeSts(STATUSES.key(status).to_s) if status
162
169
  rwy.txtRmk(remarks) if remarks
@@ -188,9 +195,9 @@ module AIXM
188
195
  # @return [AIXM::Z, nil] elevation of the touch down zone in +qnh+
189
196
  attr_reader :z
190
197
 
191
- # @return [AIXM::XY, Integer, nil] displaced threshold point either as
192
- # coordinates (AIXM::XY) or distance (Integer) in meters from the
193
- # beginning point
198
+ # @return [AIXM::XY, AIXM::D, nil] displaced threshold point either as
199
+ # coordinates (AIXM::XY) or distance (AIXM::D) from the beginning
200
+ # point
194
201
  attr_reader :displaced_threshold
195
202
 
196
203
  # @return [String, nil] free text remarks
@@ -234,11 +241,16 @@ module AIXM
234
241
  end
235
242
 
236
243
  def displaced_threshold=(value)
237
- @displaced_threshold = case value
238
- when AIXM::XY then @xy.distance(value).to_i
239
- when Numeric then value.to_i
240
- when NilClass then nil
241
- else fail(ArgumentError, "invalid displaced threshold")
244
+ case value
245
+ when AIXM::XY
246
+ @displaced_threshold = @xy.distance(value)
247
+ when AIXM::D
248
+ fail(ArgumentError, "invalid displaced threshold") unless value.dist > 0
249
+ @displaced_threshold = value
250
+ when NilClass
251
+ @displaced_threshold = nil
252
+ else
253
+ fail(ArgumentError, "invalid displaced threshold")
242
254
  end
243
255
  end
244
256
 
@@ -281,8 +293,8 @@ module AIXM
281
293
  rdd_uid.codeType('DPLM')
282
294
  rdd_uid.codeDayPeriod('A')
283
295
  end
284
- rdd.valDist(displaced_threshold)
285
- rdd.uomDist('M')
296
+ rdd.valDist(displaced_threshold.dist.trim)
297
+ rdd.uomDist(displaced_threshold.unit.to_s.upcase)
286
298
  rdd.txtRmk(remarks) if remarks
287
299
  end
288
300
  end
data/lib/aixm/d.rb ADDED
@@ -0,0 +1,66 @@
1
+ using AIXM::Refinements
2
+
3
+ module AIXM
4
+
5
+ # Distance or length
6
+ #
7
+ # @example
8
+ # AIXM.d(123, :m)
9
+ class D
10
+ include Comparable
11
+
12
+ UNITS = {
13
+ ft: { km: 0.0003048, m: 0.3048, nm: 0.000164578833554 },
14
+ km: { ft: 3280.839895, m: 1000, nm: 0.539956803 },
15
+ m: { ft: 3.280839895, km: 0.001, nm: 0.000539956803 },
16
+ nm: { ft: 6076.11548554, km: 1.852, m: 1852 }
17
+ }.freeze
18
+
19
+ # @return [Float] distance
20
+ attr_reader :dist
21
+
22
+ # @return [Symbol] unit (see {UNITS})
23
+ attr_reader :unit
24
+
25
+ def initialize(dist, unit)
26
+ self.dist, self.unit = dist, unit
27
+ end
28
+
29
+ # @return [String]
30
+ def inspect
31
+ %Q(#<#{self.class} #{to_s}>)
32
+ end
33
+
34
+ # @return [String] human readable representation (e.g. "123 m")
35
+ def to_s
36
+ [dist, unit].join(' ')
37
+ end
38
+
39
+ def dist=(value)
40
+ fail(ArgumentError, "invalid dist") unless value.is_a?(Numeric) && value >= 0
41
+ @dist = value.to_f
42
+ end
43
+
44
+ def unit=(value)
45
+ fail(ArgumentError, "invalid unit") unless value.respond_to? :to_sym
46
+ @unit = value.to_sym.downcase
47
+ fail(ArgumentError, "invalid unit") unless UNITS.has_key? @unit
48
+ end
49
+
50
+ def <=>(other)
51
+ to_m.dist <=> other.to_m.dist
52
+ end
53
+
54
+ # @!method to_ft
55
+ # @!method to_km
56
+ # @!method to_m
57
+ # @!method to_nm
58
+ # @return [AIXM::d] convert distance
59
+ UNITS.each_key do |target_unit|
60
+ define_method "to_#{target_unit}" do
61
+ return self if unit == target_unit
62
+ self.class.new((dist * UNITS[unit][target_unit]).round(8), target_unit)
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/aixm/document.rb CHANGED
@@ -53,11 +53,11 @@ module AIXM
53
53
  end
54
54
 
55
55
  def created_at=(value)
56
- @created_at = parse_time(value) || effective_at || Time.now
56
+ @created_at = value&.to_time || effective_at || Time.now
57
57
  end
58
58
 
59
59
  def effective_at=(value)
60
- @effective_at = parse_time(value) || created_at || Time.now
60
+ @effective_at = value&.to_time || created_at || Time.now
61
61
  end
62
62
 
63
63
  # Validate the generated AIXM or OFMX atainst it's XSD.
@@ -96,17 +96,5 @@ module AIXM
96
96
  end
97
97
  end
98
98
 
99
- private
100
-
101
- def parse_time(value)
102
- case value
103
- when String then Time.parse(value)
104
- when Date then value.to_time
105
- when Time then value
106
- when nil then nil
107
- else fail(ArgumentError, "invalid date or time")
108
- end
109
- end
110
-
111
99
  end
112
100
  end
@@ -125,6 +125,7 @@ module AIXM
125
125
  # however, this may be overridden by setting an alternative value, most
126
126
  # notably +:landing_site+.
127
127
  #
128
+ # @!attribute type
128
129
  # @return [Symbol] type of airport (see {TYPES})
129
130
  def type
130
131
  @type = case
@@ -0,0 +1,288 @@
1
+ using AIXM::Refinements
2
+
3
+ module AIXM
4
+ class Feature
5
+
6
+ # Obstacles are individual objects described as cylindrical volume with
7
+ # circular base and height.
8
+ #
9
+ # ===Cheat Sheet in Pseudo Code:
10
+ # obstacle = AIXM.obstacle(
11
+ # source: String or nil
12
+ # name: String or nil
13
+ # type: TYPES
14
+ # xy: AIXM.xy
15
+ # radius: AIXM.d
16
+ # z: AIXM.z
17
+ # )
18
+ # obstacle.lighting = true or false (default for AIXM) or nil (means: unknown, default for OFMX)
19
+ # obstacle.lighting_remarks = String or nil
20
+ # obstacle.marking = true or false or nil (means: unknown, default)
21
+ # obstacle.marking_remarks = String or nil
22
+ # obstacle.height = AIXM.d or nil
23
+ # obstacle.xy_accuracy = AIXM.d or nil
24
+ # obstacle.z_accuracy = AIXM.d or nil
25
+ # obstacle.height_accurate = true or false or nil (means: unknown, default)
26
+ # obstacle.valid_from = Time or Date or String or nil
27
+ # obstacle.valid_until = Time or Date or String or nil
28
+ # obstacle.remarks = String or nil
29
+ #
30
+ # @see https://github.com/openflightmaps/ofmx/wiki/Obstacle
31
+ class Obstacle < Feature
32
+ public_class_method :new
33
+
34
+ TYPES = {
35
+ ANTENNA: :antenna,
36
+ BUILDING: :building,
37
+ CHIMNEY: :chimney,
38
+ CRANE: :crane,
39
+ MAST: :mast,
40
+ TOWER: :tower,
41
+ WINDTURBINE: :wind_turbine,
42
+ OTHER: :other # specify in remarks
43
+ }.freeze
44
+
45
+ # @return [String] full name
46
+ attr_reader :name
47
+
48
+ # @return [Symbol] type of obstacle
49
+ attr_reader :type
50
+
51
+ # @return [AIXM::XY] circular base center point
52
+ attr_reader :xy
53
+
54
+ # @return [AIXM::D] circular base radius
55
+ attr_reader :radius
56
+
57
+ # @return [AIXM::Z] elevation of the top point in +:qnh+
58
+ attr_reader :z
59
+
60
+ # @return [Boolean, nil] lighting (e.g. strobes)
61
+ # true => lighting present, false => no lighting, nil => unknown
62
+ attr_reader :lighting
63
+
64
+ # @return [String, nil] detailed description of the lighting
65
+ attr_reader :lighting_remarks
66
+
67
+ # @return [Boolean, nil] marking (e.g. red/white paint)
68
+ # true => marking present, false => no marking, nil => unknown
69
+ attr_reader :marking
70
+
71
+ # @return [String, nil] detailed description of the marking
72
+ attr_reader :marking_remarks
73
+
74
+ # @return [AIXM::D, nil] height from ground to top point
75
+ attr_reader :height
76
+
77
+ # @return [AIXM::D, nil] margin of error for circular base center point
78
+ attr_reader :xy_accuracy
79
+
80
+ # @return [AIXM::D, nil] margin of error for top point
81
+ attr_reader :z_accuracy
82
+
83
+ # @return [Boolean, nil] height accuracy
84
+ # true => height measured, false => height estimated, nil => unknown
85
+ attr_reader :height_accurate
86
+
87
+ # @return [Time, Date, String, nil] effective after this point in time
88
+ attr_reader :valid_from
89
+
90
+ # @return [Time, Date, String, nil] effective until this point in time
91
+ attr_reader :valid_until
92
+
93
+ # @return [String, nil] free text remarks
94
+ attr_reader :remarks
95
+
96
+ def initialize(source: nil, name: nil, type:, xy:, radius:, z:)
97
+ super(source: source)
98
+ self.name, self.type, self.xy, self.radius, self.z = name, type, xy, radius, z
99
+ @lighting = @marking = @height_accurate = false
100
+ end
101
+
102
+ # @return [String]
103
+ def inspect
104
+ %Q(#<#{self.class} xy="#{xy.to_s}" type=#{type.inspect}>)
105
+ end
106
+
107
+ def name=(value)
108
+ fail(ArgumentError, "invalid name") unless value.nil? || value.is_a?(String)
109
+ @name = value&.uptrans
110
+ end
111
+
112
+ def type=(value)
113
+ @type = TYPES.lookup(value&.to_s&.to_sym, nil) || fail(ArgumentError, "invalid type")
114
+ end
115
+
116
+ def xy=(value)
117
+ fail(ArgumentError, "invalid xy") unless value.is_a? AIXM::XY
118
+ @xy = value
119
+ end
120
+
121
+ def radius=(value)
122
+ fail(ArgumentError, "invalid radius") unless value.is_a?(AIXM::D) && value.dist > 0
123
+ @radius = value
124
+ end
125
+
126
+ def z=(value)
127
+ fail(ArgumentError, "invalid z") unless value.is_a?(AIXM::Z) && value.qnh?
128
+ @z = value
129
+ end
130
+
131
+ def lighting=(value)
132
+ fail(ArgumentError, "invalid lighting") unless [true, false, nil].include? value
133
+ @lighting = value
134
+ end
135
+
136
+ def lighting_remarks=(value)
137
+ @lighting_remarks = value&.to_s
138
+ end
139
+
140
+ def marking=(value)
141
+ fail(ArgumentError, "invalid marking") unless [true, false, nil].include? value
142
+ @marking = value
143
+ end
144
+
145
+ def marking_remarks=(value)
146
+ @marking_remarks = value&.to_s
147
+ end
148
+
149
+ def height=(value)
150
+ fail(ArgumentError, "invalid height") unless value.nil? || (value.is_a?(AIXM::D) && value.dist > 0)
151
+ @height = value
152
+ end
153
+
154
+ def xy_accuracy=(value)
155
+ fail(ArgumentError, "invalid xy accuracy") unless value.nil? || value.is_a?(AIXM::D)
156
+ @xy_accuracy = value
157
+ end
158
+
159
+ def z_accuracy=(value)
160
+ fail(ArgumentError, "invalid z accuracy") unless value.nil? || value.is_a?(AIXM::D)
161
+ @z_accuracy = value
162
+ end
163
+
164
+ def height_accurate=(value)
165
+ fail(ArgumentError, "invalid height accurate") unless [true, false, nil].include? value
166
+ @height_accurate = value
167
+ end
168
+
169
+ def valid_from=(value)
170
+ @valid_from = value&.to_time
171
+ end
172
+
173
+ def valid_until=(value)
174
+ @valid_until = value&.to_time
175
+ end
176
+
177
+ def remarks=(value)
178
+ @remarks = value&.to_s
179
+ end
180
+
181
+ # @return [Boolean] estimation whether one obstacle (e.g. single tree) or
182
+ # a cluster of obstacles (e.g. a few trees)
183
+ def clustered?
184
+ height && radius > height
185
+ end
186
+
187
+ # @return [Boolean] whether part of an obstacle group
188
+ def grouped?
189
+ respond_to? :group
190
+ end
191
+
192
+ # @return [String] UID markup
193
+ def to_uid(as: :ObsUid)
194
+ builder = Builder::XmlMarkup.new(indent: 2)
195
+ builder.tag!(as) do |tag|
196
+ tag.geoLat((xy.lat(AIXM.schema)))
197
+ tag.geoLong((xy.long(AIXM.schema)))
198
+ end
199
+ end
200
+
201
+ # @return [String] AIXM or OFMX markup
202
+ def to_xml
203
+ builder = Builder::XmlMarkup.new(indent: 2)
204
+ builder.comment! "Obstacle: [#{type}] #{xy.to_s} #{name}".strip
205
+ builder.Obs({ source: ((source || (group.source if grouped?)) if AIXM.ofmx?) }.compact) do |obs|
206
+ obs << to_uid.indent(2)
207
+ obs.txtName(name) if name
208
+ if AIXM.ofmx?
209
+ obs.codeType(TYPES.key(type).to_s)
210
+ else
211
+ obs.txtDescrType(TYPES.key(type).to_s)
212
+ obs.codeGroup(grouped? || clustered? ? 'Y' : 'N')
213
+ end
214
+ if AIXM.ofmx?
215
+ obs.codeLgt(lighting ? 'Y' : 'N') unless lighting.nil?
216
+ obs.codeMarking(marking ? 'Y' : 'N') unless marking.nil?
217
+ else
218
+ obs.codeLgt(lighting ? 'Y' : 'N')
219
+ end
220
+ obs.txtDescrLgt(lighting_remarks) if lighting_remarks
221
+ obs.txtDescrMarking(marking_remarks) if marking_remarks
222
+ obs.codeDatum('WGE')
223
+ if xy_accuracy
224
+ obs.valGeoAccuracy(xy_accuracy.dist.trim)
225
+ obs.uomGeoAccuracy(xy_accuracy.unit.upcase.to_s)
226
+ end
227
+ obs.valElev(z.alt)
228
+ obs.valElevAccuracy(z_accuracy.to_ft.dist.round) if z_accuracy
229
+ obs.valHgt(height.to_ft.dist.round) if height
230
+ if AIXM.ofmx? && !height_accurate.nil?
231
+ obs.codeHgtAccuracy(height_accurate ? 'Y' : 'N')
232
+ end
233
+ obs.uomDistVer('FT')
234
+ if AIXM.ofmx?
235
+ obs.valRadius(radius.dist.trim)
236
+ obs.uomRadius(radius.unit.upcase.to_s)
237
+ if grouped?
238
+ obs.codeGroupId(group.id)
239
+ obs.txtGroupName(group.name) if group.name
240
+ if linked?
241
+ obs << linked_to.to_uid(as: :ObsUidLink).indent(2)
242
+ obs.codeLinkType(AIXM::Feature::ObstacleGroup::LINK_TYPES.key(link_type).to_s)
243
+ end
244
+ end
245
+ obs.datetimeValidWef(valid_from.xmlschema) if valid_from
246
+ obs.datetimeValidTil(valid_until.xmlschema) if valid_until
247
+ end
248
+ obs.txtRmk(remarks) if remarks
249
+ end
250
+ end
251
+
252
+ # Extensions used by ObstacleGroup feature
253
+ module Grouped
254
+ # @return [AIXM::Feature::ObstacleGroup] group this obstacle belongs to
255
+ attr_reader :group
256
+
257
+ # @return [AIXM::Feature::Obstacle] another obstacle this one is linked to
258
+ attr_reader :linked_to
259
+
260
+ # @return [Symbol] type of link between two obstacles
261
+ attr_reader :link_type
262
+
263
+ def group=(value)
264
+ fail(ArgumentError, "invalid group") unless value.is_a?(AIXM::Feature::ObstacleGroup)
265
+ @group = value
266
+ end
267
+ private :group=
268
+
269
+ # @return [Boolean] whether obstacle is linked to another one
270
+ def linked?
271
+ !!linked_to
272
+ end
273
+
274
+ def linked_to=(value)
275
+ fail(ArgumentError, "invalid linked to") unless value.is_a?(AIXM::Feature::Obstacle)
276
+ @linked_to = value
277
+ end
278
+ private :linked_to=
279
+
280
+ def link_type=(value)
281
+ @link_type = AIXM::Feature::ObstacleGroup::LINK_TYPES.lookup(value&.to_s&.to_sym, nil) || fail(ArgumentError, "invalid link type")
282
+ end
283
+ private :link_type=
284
+ end
285
+
286
+ end
287
+ end
288
+ end