aixm 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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