aixm 0.3.1 → 0.3.2

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