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.
@@ -0,0 +1,104 @@
1
+ using AIXM::Refinements
2
+
3
+ module AIXM
4
+ class Feature
5
+
6
+ # Groups of obstacles which consist of either linked (e.g. power line
7
+ # towers) or unlinked (e.g. wind turbines) members.
8
+ #
9
+ # ===Cheat Sheet in Pseudo Code:
10
+ # obstacle_group = AIXM.obstacle_group(
11
+ # source: String or nil # see remarks below
12
+ # name: String or nil
13
+ # )
14
+ # obstacle_group.add_obstacle( # add an obstacle to the group
15
+ # AIXM.obstacle
16
+ # )
17
+ # obstacle_group.add_obstacle( # add an obstacle to the group and link
18
+ # AIXM.obstacle, # it to the obstacle last added to the group
19
+ # linked_to: :previous,
20
+ # link_type: LINK_TYPES
21
+ # )
22
+ # obstacle_group.add_obstacle( # add an obstacle to the group and link
23
+ # AIXM.obstacle, # it to any obstacle already in the group
24
+ # linked_to: AIXM.obstacle,
25
+ # link_type: LINK_TYPES
26
+ # )
27
+ # obstacle_group.id # UUID v3 calculated from the group payload
28
+ #
29
+ # As soon as an obstacle is added to a group, it's extended with new the
30
+ # following attributes:
31
+ # * group - the group this object belongs to
32
+ # * linked_to - obstacle this one is linked to (if any)
33
+ # * link_type - type of link between the two obstacles (if any)
34
+ #
35
+ # The source set on the group is handed down to each of it's obstacles and
36
+ # will be used there unless the individual obstacle overrides it with a
37
+ # different source of it's own.
38
+ #
39
+ # @see https://github.com/openflightmaps/ofmx/wiki/Obstacle
40
+ class ObstacleGroup < Feature
41
+ public_class_method :new
42
+
43
+ LINK_TYPES = {
44
+ CABLE: :cable,
45
+ SOLID: :solid,
46
+ OTHER: :other
47
+ }.freeze
48
+
49
+ # @return [String] group name
50
+ attr_reader :name
51
+
52
+ # @return [Array<AIXM::Feature::Obstacle>] obstacles in this group
53
+ attr_reader :obstacles
54
+
55
+ def initialize(source: nil, name: nil)
56
+ super(source: source)
57
+ self.name = name
58
+ @obstacles = []
59
+ end
60
+
61
+ # @return [String]
62
+ def inspect
63
+ %Q(#<#{self.class} #{@obstacles.count} obstacle(s)>)
64
+ end
65
+
66
+ def name=(value)
67
+ fail(ArgumentError, "invalid name") unless value.nil? || value.is_a?(String)
68
+ @name = value&.uptrans
69
+ end
70
+
71
+ # Add an obstacle to the group and optionally link it to another obstacle
72
+ # from the group.
73
+ #
74
+ # @param obstacle [AIXM::Feature::Obstacle] obstacle instance
75
+ # @param linked_to [Symbol, AIXM::Feature::Obstacle, nil] Either:
76
+ # * :previous - link to the obstacle last added to the group
77
+ # * AIXM::Feature::Obstacle - link to this specific obstacle
78
+ # @param link_type [Symbol, nil] type of link (see {LINK_TYPES})
79
+ # @return [self]
80
+ def add_obstacle(obstacle, linked_to: nil, link_type: :other)
81
+ obstacle.extend AIXM::Feature::Obstacle::Grouped
82
+ obstacle.send(:group=, self)
83
+ if linked_to && link_type
84
+ obstacle.send(:linked_to=, linked_to == :previous ? @obstacles.last : linked_to)
85
+ obstacle.send(:link_type=, link_type)
86
+ end
87
+ @obstacles << obstacle
88
+ self
89
+ end
90
+
91
+ # @return [String] UUID version 3 group identifier
92
+ def id
93
+ ([name] + @obstacles.map { |o| o.xy.to_s }).to_uuid
94
+ end
95
+ alias_method :to_uid, :id # features need "to_uid" for "==" to work
96
+
97
+ # @return [String] AIXM or OFMX markup
98
+ def to_xml
99
+ @obstacles.map { |o| o.to_xml }.join
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -13,13 +13,6 @@ module AIXM
13
13
  "Ø" => "Oe"
14
14
  }.freeze
15
15
 
16
- KM_FACTORS = {
17
- km: 1,
18
- m: 0.001,
19
- nm: 1.852,
20
- ft: 0.0003048
21
- }.freeze
22
-
23
16
  # @!method to_digest
24
17
  # Builds a 4 byte hex digest from the Array payload.
25
18
  #
@@ -35,6 +28,83 @@ module AIXM
35
28
  end
36
29
  end
37
30
 
31
+ # @!method to_uuid
32
+ # Builds a UUID version 3 digest from the Array payload.
33
+ #
34
+ # @example
35
+ # ['foo', :bar, nil, [123]].to_uuid
36
+ # # => "f3920098"
37
+ #
38
+ # @note This is a refinement for +Array+
39
+ # @return [String] UUID version 3
40
+ refine Array do
41
+ def to_uuid
42
+ ::Digest::MD5.hexdigest(flatten.map(&:to_s).join('|')).unpack("a8a4a4a4a12").join("-")
43
+ end
44
+ end
45
+
46
+
47
+ # @!method to_dms(padding=3)
48
+ # Convert DD angle to DMS with the degrees zero padded to +padding+
49
+ # length.
50
+ #
51
+ # @example
52
+ # 43.22164444444445.to_dms(2)
53
+ # # => "43°12'77.92\""
54
+ # 43.22164444444445.to_dms
55
+ # # => "043°12'77.92\""
56
+ #
57
+ # @note This is a refinement for +Float+
58
+ # @param padding [Integer] number of digits for the degree part
59
+ # @return [String] angle in DMS notation +{-}D°MM'SS.SS"+
60
+ refine Float do
61
+ def to_dms(padding=3)
62
+ degrees = self.abs.floor
63
+ minutes = ((self.abs - degrees) * 60).floor
64
+ seconds = (self.abs - degrees - minutes.to_f / 60) * 3600
65
+ minutes, seconds = minutes + 1, 0 if seconds.round(2) == 60
66
+ degrees, minutes = degrees + 1, 0 if minutes == 60
67
+ %Q(%s%0#{padding}d°%02d'%05.2f") % [
68
+ ('-' if self.negative?),
69
+ self.abs.truncate,
70
+ minutes.abs.truncate,
71
+ seconds.abs
72
+ ]
73
+ end
74
+ end
75
+
76
+ # @!method to_rad
77
+ # Convert an angle from degree to radian.
78
+ #
79
+ # @example
80
+ # 45.to_rad
81
+ # # => 0.7853981633974483
82
+ #
83
+ # @note This is a refinement for +Float+
84
+ # @return [Float] radian angle
85
+ refine Float do
86
+ def to_rad
87
+ self * Math::PI / 180
88
+ end
89
+ end
90
+
91
+ # @!method trim
92
+ # Convert whole numbers to Integer and leave all other untouched.
93
+ #
94
+ # @example
95
+ # 3.0.trim
96
+ # # => 3
97
+ # 3.3.trim
98
+ # # => 3.3
99
+ #
100
+ # @note This is a refinement for +Float+
101
+ # @return [Integer, Float] converted Float
102
+ refine Float do
103
+ def trim
104
+ (self % 1).zero? ? self.to_i : self
105
+ end
106
+ end
107
+
38
108
  # @!method lookup(key_or_value, fallback=omitted=true)
39
109
  # Fetch a value from the hash, but unlike +Hash#fetch+, if +key_or_value+
40
110
  # is no hash key, check whether +key_or_value+ is a hash value and if so
@@ -79,32 +149,6 @@ module AIXM
79
149
  end
80
150
  end
81
151
 
82
- # @!method uptrans
83
- # Upcase and transliterate to match the reduced character set for
84
- # AIXM names and titles.
85
- #
86
- # See {UPTRANS_MAP} for supported diacryts and {UPTRANS_FILTER} for the
87
- # list of allowed characters in the returned value.
88
- #
89
- # @example
90
- # "Nîmes-Alès".uptrans
91
- # # => "NIMES-ALES"
92
- # "Zürich".uptrans
93
- # # => "ZUERICH"
94
- #
95
- # @note This is a refinement for +String+
96
- # @return [String] upcased and transliterated String
97
- refine String do
98
- def uptrans
99
- self.dup.tap do |string|
100
- string.upcase!
101
- string.gsub!(/(#{UPTRANS_MAP.keys.join('|')})/, UPTRANS_MAP)
102
- string.unicode_normalize!(:nfd)
103
- string.gsub!(UPTRANS_FILTER, '')
104
- end
105
- end
106
- end
107
-
108
152
  # @!method to_dd
109
153
  # Convert DMS angle to DD or +nil+ if the notation is not recognized.
110
154
  #
@@ -129,85 +173,44 @@ module AIXM
129
173
  end
130
174
  end
131
175
 
132
- # @!method trim
133
- # Convert whole numbers to Integer and leave all other untouched.
134
- #
135
- # @example
136
- # 3.0.trim
137
- # # => 3
138
- # 3.3.trim
139
- # # => 3.3
140
- #
141
- # @note This is a refinement for +Float+
142
- # @return [Integer, Float] converted Float
143
- refine Float do
144
- def trim
145
- (self % 1).zero? ? self.to_i : self
146
- end
147
- end
148
-
149
- # @!method to_rad
150
- # Convert an angle from degree to radian.
176
+ # @!method to_time
177
+ # Parse string to date and time.
151
178
  #
152
179
  # @example
153
- # 45.to_rad
154
- # # => 0.7853981633974483
180
+ # '2018-01-01 15:00'.to_time
181
+ # # => 2018-01-01 15:00:00 +0100
155
182
  #
156
- # @note This is a refinement for +Float+
157
- # @return [Float] radian angle
158
- refine Float do
159
- def to_rad
160
- self * Math::PI / 180
183
+ # @note This is a refinement for +String+
184
+ # @return [Time] date and time
185
+ refine String do
186
+ def to_time
187
+ Time.parse(self)
161
188
  end
162
189
  end
163
190
 
164
- # @!method to_dms(padding=3)
165
- # Convert DD angle to DMS with the degrees zero padded to +padding+
166
- # length.
167
- #
168
- # @example
169
- # 43.22164444444445.to_dms(2)
170
- # # => "43°12'77.92\""
171
- # 43.22164444444445.to_dms
172
- # # => "043°12'77.92\""
191
+ # @!method uptrans
192
+ # Upcase and transliterate to match the reduced character set for
193
+ # AIXM names and titles.
173
194
  #
174
- # @note This is a refinement for +Float+
175
- # @param padding [Integer] number of digits for the degree part
176
- # @return [String] angle in DMS notation +{-}D°MM'SS.SS"+
177
- refine Float do
178
- def to_dms(padding=3)
179
- degrees = self.abs.floor
180
- minutes = ((self.abs - degrees) * 60).floor
181
- seconds = (self.abs - degrees - minutes.to_f / 60) * 3600
182
- minutes, seconds = minutes + 1, 0 if seconds.round(2) == 60
183
- degrees, minutes = degrees + 1, 0 if minutes == 60
184
- %Q(%s%0#{padding}d°%02d'%05.2f") % [
185
- ('-' if self.negative?),
186
- self.abs.truncate,
187
- minutes.abs.truncate,
188
- seconds.abs
189
- ]
190
- end
191
- end
192
-
193
- # @!method to_km(from:)
194
- # Convert a distance from the source unit +from+ to kilometers.
195
+ # See {UPTRANS_MAP} for supported diacryts and {UPTRANS_FILTER} for the
196
+ # list of allowed characters in the returned value.
195
197
  #
196
198
  # @example
197
- # 10.to_km(from: :nm)
198
- # # => 18.52
199
- # 10.to_km(from: :foobar)
200
- # # => ArgumentError
199
+ # "Nîmes-Alès".uptrans
200
+ # # => "NIMES-ALES"
201
+ # "Zürich".uptrans
202
+ # # => "ZUERICH"
201
203
  #
202
- # @note This is a refinement for +Float+
203
- # @param from [Symbol] source unit (see {KM_FACTORS})
204
- # @raise [ArgumentError] if the specified unit is not supported
205
- # @return [Float] value converted to kilometers
206
- refine Float do
207
- def to_km(from:)
208
- self * KM_FACTORS.fetch(from.downcase.to_sym)
209
- rescue KeyError
210
- raise(ArgumentError, "unit `#{from}' not supported")
204
+ # @note This is a refinement for +String+
205
+ # @return [String] upcased and transliterated String
206
+ refine String do
207
+ def uptrans
208
+ self.dup.tap do |string|
209
+ string.upcase!
210
+ string.gsub!(/(#{UPTRANS_MAP.keys.join('|')})/, UPTRANS_MAP)
211
+ string.unicode_normalize!(:nfd)
212
+ string.gsub!(UPTRANS_FILTER, '')
213
+ end
211
214
  end
212
215
  end
213
216
  end
@@ -4,6 +4,7 @@ module AIXM
4
4
  document: Document,
5
5
  xy: XY,
6
6
  z: Z,
7
+ d: D,
7
8
  f: F,
8
9
  organisation: Feature::Organisation,
9
10
  unit: Feature::Unit,
@@ -26,6 +27,8 @@ module AIXM
26
27
  tacan: Feature::NavigationalAid::TACAN,
27
28
  ndb: Feature::NavigationalAid::NDB,
28
29
  vor: Feature::NavigationalAid::VOR,
30
+ obstacle: Feature::Obstacle,
31
+ obstacle_group: Feature::ObstacleGroup,
29
32
  timetable: Component::Timetable
30
33
  }.freeze
31
34
 
data/lib/aixm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module AIXM
2
- VERSION = "0.3.1".freeze
2
+ VERSION = "0.3.2".freeze
3
3
  end
data/lib/aixm/xy.rb CHANGED
@@ -68,19 +68,20 @@ module AIXM
68
68
  other.is_a?(self.class) && lat == other.lat && long == other.long
69
69
  end
70
70
 
71
- # @return [Float] distance in meters as calculated by use of the Haversine formula
71
+ # @return [AIXM::D] distance as calculated by use of the Haversine formula
72
72
  def distance(other)
73
73
  if self == other
74
- 0
74
+ AIXM.d(0, :m)
75
75
  else
76
- 2 * EARTH_RADIUS * Math.asin(
76
+ value = 2 * EARTH_RADIUS * Math.asin(
77
77
  Math.sqrt(
78
78
  Math.sin((other.lat.to_rad - lat.to_rad) / 2) ** 2 +
79
79
  Math.cos(lat.to_rad) * Math.cos(other.lat.to_rad) *
80
80
  Math.sin((other.long.to_rad - long.to_rad) / 2) ** 2
81
81
  )
82
82
  )
83
- end.round
83
+ AIXM.d(value.round, :m)
84
+ end
84
85
  end
85
86
 
86
87
  private
data/lib/aixm/z.rb CHANGED
@@ -5,8 +5,8 @@ module AIXM
5
5
  # Height, elevation or altitude
6
6
  #
7
7
  # @example
8
- # AIXM.z(1000, :qfe) # height: 1000 ft above ground
9
- # AIXM.z(2000, :qnh) # elevation or altitude: 2000 ft above mean sea level
8
+ # AIXM.z(1000, :qfe) # height (ft): 1000 ft above ground
9
+ # AIXM.z(2000, :qnh) # elevation or altitude (ft): 2000 ft above mean sea level
10
10
  # AIXM.z(45, :qne) # altitude: flight level 45
11
11
  #
12
12
  # ===Shortcuts:
@@ -15,7 +15,7 @@ module AIXM
15
15
  class Z
16
16
  CODES = %i(qfe qnh qne).freeze
17
17
 
18
- # @return [Integer] elevation or altitude value
18
+ # @return [Integer] altitude or elevation value
19
19
  attr_reader :alt
20
20
 
21
21
  # @return [Symbol] Q code - either +:qfe+ (height in feet), +:qnh+ (altitude in feet or +:qne+ (altitude as flight level)
data/spec/factory.rb CHANGED
@@ -12,6 +12,10 @@ module AIXM
12
12
  AIXM.z(1000, :qnh)
13
13
  end
14
14
 
15
+ def d
16
+ AIXM.d(123, :m)
17
+ end
18
+
15
19
  def f
16
20
  AIXM.f(123.35, :mhz)
17
21
  end
@@ -69,7 +73,7 @@ module AIXM
69
73
  AIXM.geometry.tap do |geometry|
70
74
  geometry << AIXM.circle(
71
75
  center_xy: AIXM.xy(lat: %q(47°35'00"N), long: %q(004°53'00"E)),
72
- radius: 10
76
+ radius: AIXM.d(10, :km)
73
77
  )
74
78
  end
75
79
  end
@@ -313,8 +317,8 @@ module AIXM
313
317
 
314
318
  def runway
315
319
  AIXM.runway(name: '16L/34R').tap do |runway|
316
- runway.length = 650
317
- runway.width = 80
320
+ runway.length = AIXM.d(650, :m)
321
+ runway.width = AIXM.d(80, :m)
318
322
  runway.composition = :graded_earth
319
323
  runway.status = :closed
320
324
  runway.remarks = "Markings eroded"
@@ -335,14 +339,110 @@ module AIXM
335
339
  AIXM.helipad(name: 'H1').tap do |helipad|
336
340
  helipad.xy = AIXM.xy(lat: %q(43°59'56.94"N), long: %q(004°45'05.56"E))
337
341
  helipad.z = AIXM.z(141, :qnh)
338
- helipad.length = 20
339
- helipad.width = 20
342
+ helipad.length = AIXM.d(20, :m)
343
+ helipad.width = AIXM.d(20, :m)
340
344
  helipad.composition = :grass
341
345
  helipad.status = :other
342
346
  helipad.remarks = "Authorizaton by AD operator required"
343
347
  end
344
348
  end
345
349
 
350
+ # Obstacle
351
+
352
+ def obstacle
353
+ AIXM.obstacle(
354
+ name: "Eiffel Tower",
355
+ type: :tower,
356
+ xy: AIXM.xy(lat: %q(48°51'29.7"N), long: %q(002°17'40.52"E)),
357
+ radius: AIXM.d(88, :m),
358
+ z: AIXM.z(1187 , :qnh)
359
+ ).tap do |obstacle|
360
+ obstacle.lighting = true
361
+ obstacle.lighting_remarks = "red strobes"
362
+ obstacle.marking = nil
363
+ obstacle.marking_remarks = nil
364
+ obstacle.height = AIXM.d(324, :m)
365
+ obstacle.xy_accuracy = AIXM.d(2, :m)
366
+ obstacle.z_accuracy = AIXM.d(1, :m)
367
+ obstacle.height_accurate = true
368
+ obstacle.valid_from = Time.parse('2018-01-01 12:00:00 +0100')
369
+ obstacle.valid_until = Time.parse('2019-01-01 12:00:00 +0100')
370
+ obstacle.remarks = "Temporary light installations (white strobes, gyro light etc)"
371
+ end
372
+ end
373
+
374
+ def unlinked_obstacle_group
375
+ AIXM.obstacle_group(
376
+ name: "Mirmande éoliennes"
377
+ ).tap do |obstacle_group|
378
+ obstacle_group.add_obstacle(
379
+ AIXM.obstacle(
380
+ name: "La Teissonière 1",
381
+ type: :wind_turbine,
382
+ xy: AIXM.xy(lat: %q(44°40'30.05"N), long: %q(004°52'21.24"E)),
383
+ radius: AIXM.d(80, :m),
384
+ z: AIXM.z(1764, :qnh)
385
+ ).tap do |obstacle|
386
+ obstacle.height = AIXM.d(80, :m)
387
+ obstacle.xy_accuracy = AIXM.d(50, :m)
388
+ obstacle.z_accuracy = AIXM.d(10, :m)
389
+ obstacle.height_accurate = false
390
+ end
391
+ )
392
+ obstacle_group.add_obstacle(
393
+ AIXM.obstacle(
394
+ name: "La Teissonière 2",
395
+ type: :wind_turbine,
396
+ xy: AIXM.xy(lat: %q(44°40'46.08"N), long: %q(004°52'25.72"E)),
397
+ radius: AIXM.d(80, :m),
398
+ z: AIXM.z(1738 , :qnh)
399
+ ).tap do |obstacle|
400
+ obstacle.height = AIXM.d(80, :m)
401
+ obstacle.xy_accuracy = AIXM.d(50, :m)
402
+ obstacle.z_accuracy = AIXM.d(10, :m)
403
+ obstacle.height_accurate = false
404
+ end
405
+ )
406
+ end
407
+ end
408
+
409
+ def linked_obstacle_group
410
+ AIXM.obstacle_group(
411
+ name: "Droitwich longwave antenna"
412
+ ).tap do |obstacle_group|
413
+ obstacle_group.add_obstacle(
414
+ AIXM.obstacle(
415
+ name: "Droitwich LW north",
416
+ type: :mast,
417
+ xy: AIXM.xy(lat: %q(52°17'47.03"N), long: %q(002°06'24.31"W)),
418
+ radius: AIXM.d(200, :m),
419
+ z: AIXM.z(848 , :qnh)
420
+ ).tap do |obstacle|
421
+ obstacle.height = AIXM.d(700, :ft)
422
+ obstacle.xy_accuracy = AIXM.d(0, :m)
423
+ obstacle.z_accuracy = AIXM.d(0, :ft)
424
+ obstacle.height_accurate = true
425
+ end
426
+ )
427
+ obstacle_group.add_obstacle(
428
+ AIXM.obstacle(
429
+ name: "Droitwich LW north",
430
+ type: :mast,
431
+ xy: AIXM.xy(lat: %q(52°17'40.48"N), long: %q(002°06'20.47"W)),
432
+ radius: AIXM.d(200, :m),
433
+ z: AIXM.z(848 , :qnh)
434
+ ).tap do |obstacle|
435
+ obstacle.height = AIXM.d(700, :ft)
436
+ obstacle.xy_accuracy = AIXM.d(0, :m)
437
+ obstacle.z_accuracy = AIXM.d(0, :ft)
438
+ obstacle.height_accurate = true
439
+ end,
440
+ linked_to: :previous,
441
+ link_type: :cable
442
+ )
443
+ end
444
+ end
445
+
346
446
  # Document
347
447
 
348
448
  def document
@@ -366,6 +466,9 @@ module AIXM
366
466
  document.features << vor
367
467
  document.features << vordme
368
468
  document.features << vortac
469
+ document.features << obstacle
470
+ document.features << unlinked_obstacle_group
471
+ document.features << linked_obstacle_group
369
472
  end
370
473
  end
371
474