aixm 0.1.0 → 0.1.3

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/Guardfile +1 -1
  4. data/README.md +146 -17
  5. data/aixm.gemspec +3 -1
  6. data/lib/aixm.rb +12 -10
  7. data/lib/aixm/component/base.rb +6 -0
  8. data/lib/aixm/component/class_layer.rb +49 -0
  9. data/lib/aixm/component/geometry.rb +73 -0
  10. data/lib/aixm/component/geometry/arc.rb +53 -0
  11. data/lib/aixm/component/geometry/border.rb +49 -0
  12. data/lib/aixm/component/geometry/circle.rb +56 -0
  13. data/lib/aixm/component/geometry/point.rb +42 -0
  14. data/lib/aixm/component/schedule.rb +45 -0
  15. data/lib/aixm/{vertical/limits.rb → component/vertical_limits.rb} +9 -14
  16. data/lib/aixm/document.rb +30 -19
  17. data/lib/aixm/feature/airspace.rb +60 -29
  18. data/lib/aixm/refinements.rb +49 -2
  19. data/lib/aixm/shortcuts.rb +30 -0
  20. data/lib/aixm/version.rb +1 -1
  21. data/spec/factory.rb +42 -25
  22. data/spec/lib/aixm/component/class_layer_spec.rb +74 -0
  23. data/spec/lib/aixm/{horizontal → component/geometry}/arc_spec.rb +11 -11
  24. data/spec/lib/aixm/component/geometry/border_spec.rb +30 -0
  25. data/spec/lib/aixm/{horizontal → component/geometry}/circle_spec.rb +8 -8
  26. data/spec/lib/aixm/{horizontal → component/geometry}/point_spec.rb +7 -7
  27. data/spec/lib/aixm/{geometry_spec.rb → component/geometry_spec.rb} +39 -40
  28. data/spec/lib/aixm/component/schedule_spec.rb +33 -0
  29. data/spec/lib/aixm/{vertical/limits_spec.rb → component/vertical_limits_spec.rb} +10 -10
  30. data/spec/lib/aixm/document_spec.rb +97 -36
  31. data/spec/lib/aixm/feature/airspace_spec.rb +230 -71
  32. data/spec/lib/aixm/refinements_spec.rb +52 -12
  33. metadata +30 -23
  34. data/lib/aixm/constants.rb +0 -6
  35. data/lib/aixm/geometry.rb +0 -71
  36. data/lib/aixm/horizontal/arc.rb +0 -50
  37. data/lib/aixm/horizontal/border.rb +0 -45
  38. data/lib/aixm/horizontal/circle.rb +0 -53
  39. data/lib/aixm/horizontal/point.rb +0 -39
  40. data/spec/lib/aixm/horizontal/border_spec.rb +0 -47
@@ -0,0 +1,53 @@
1
+ module AIXM
2
+ module Component
3
+ class Geometry
4
+
5
+ ##
6
+ # Arcs are +clockwise+ (true/false) circle sectors around +center_xy+ and
7
+ # starting at +xy+.
8
+ class Arc < Point
9
+ using AIXM::Refinements
10
+
11
+ attr_reader :center_xy
12
+
13
+ def initialize(xy:, center_xy:, clockwise:)
14
+ super(xy: xy)
15
+ fail(ArgumentError, "invalid center xy") unless center_xy.is_a? AIXM::XY
16
+ fail(ArgumentError, "clockwise must be true or false") unless [true, false].include? clockwise
17
+ @center_xy, @clockwise = center_xy, clockwise
18
+ end
19
+
20
+ ##
21
+ # Whether the arc is going clockwise (true) or not (false)
22
+ def clockwise?
23
+ @clockwise
24
+ end
25
+
26
+ ##
27
+ # Digest to identify the payload
28
+ def to_digest
29
+ [xy.lat, xy.long, center_xy.lat, center_xy.long, clockwise?].to_digest
30
+ end
31
+
32
+ ##
33
+ # Render AIXM
34
+ #
35
+ # Extensions:
36
+ # * +:OFM+ - Open Flightmaps
37
+ def to_xml(*extensions)
38
+ format = extensions >> :OFM ? :OFM : :AIXM
39
+ builder = Builder::XmlMarkup.new(indent: 2)
40
+ builder.Avx do |avx|
41
+ avx.codeType(clockwise? ? 'CWA' : 'CCA')
42
+ avx.geoLat(xy.lat(format))
43
+ avx.geoLong(xy.long(format))
44
+ avx.codeDatum('WGE')
45
+ avx.geoLatArc(center_xy.lat(format))
46
+ avx.geoLongArc(center_xy.long(format))
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ module AIXM
2
+ module Component
3
+ class Geometry
4
+
5
+ ##
6
+ # Borders are following natural or artifical border lines referenced by
7
+ # +name+ and starting at +xy+.
8
+ class Border < Point
9
+ using AIXM::Refinements
10
+
11
+ attr_reader :name
12
+
13
+ def initialize(xy:, name:)
14
+ super(xy: xy)
15
+ @name = name
16
+ end
17
+
18
+ ##
19
+ # Digest to identify the payload
20
+ def to_digest
21
+ [xy.lat, xy.long, name].to_digest
22
+ end
23
+
24
+ ##
25
+ # Render AIXM
26
+ #
27
+ # Extensions:
28
+ # * +:OFM+ - Open Flightmaps
29
+ def to_xml(*extensions)
30
+ format = extensions >> :OFM ? :OFM : :AIXM
31
+ builder = Builder::XmlMarkup.new(indent: 2)
32
+ builder.Avx do |avx|
33
+ avx.codeType('FNT')
34
+ avx.geoLat(xy.lat(format))
35
+ avx.geoLong(xy.long(format))
36
+ avx.codeDatum('WGE')
37
+ # TODO: Find examples how to do this with vanilla AIXM
38
+ if extensions >> :OFM
39
+ avx.GbrUid do |gbruid|
40
+ gbruid.txtName(name.to_s)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ module AIXM
2
+ module Component
3
+ class Geometry
4
+
5
+ ##
6
+ # Circles are defined by a +center_xy+ and a +radius+ in kilometers.
7
+ class Circle
8
+ using AIXM::Refinements
9
+
10
+ attr_reader :center_xy, :radius
11
+
12
+ def initialize(center_xy:, radius:)
13
+ fail(ArgumentError, "invalid center xy") unless center_xy.is_a? AIXM::XY
14
+ @center_xy, @radius = center_xy, radius
15
+ end
16
+
17
+ ##
18
+ # Digest to identify the payload
19
+ def to_digest
20
+ [center_xy.lat, center_xy.long, radius].to_digest
21
+ end
22
+
23
+ ##
24
+ # Render AIXM
25
+ #
26
+ # Extensions:
27
+ # * +:OFM+ - Open Flightmaps
28
+ def to_xml(*extensions)
29
+ format = extensions >> :OFM ? :OFM : :AIXM
30
+ builder = Builder::XmlMarkup.new(indent: 2)
31
+ builder.Avx do |avx|
32
+ avx.codeType('CWA')
33
+ avx.geoLat(north_xy.lat(format))
34
+ avx.geoLong(north_xy.long(format))
35
+ avx.codeDatum('WGE')
36
+ avx.geoLatArc(center_xy.lat(format))
37
+ avx.geoLongArc(center_xy.long(format))
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ ##
44
+ # Coordinates of the point which is both strictly north of the center
45
+ # and on the circumference of the circle
46
+ def north_xy
47
+ AIXM.xy(
48
+ lat: center_xy.lat + radius.to_f / 6371 * 180 / Math::PI,
49
+ long: center_xy.long
50
+ )
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ module AIXM
2
+ module Component
3
+ class Geometry
4
+
5
+ ##
6
+ # Points are defined by +xy+ coordinates.
7
+ class Point
8
+ using AIXM::Refinements
9
+
10
+ attr_reader :xy
11
+
12
+ def initialize(xy:)
13
+ fail(ArgumentError, "invalid xy") unless xy.is_a? AIXM::XY
14
+ @xy = xy
15
+ end
16
+
17
+ ##
18
+ # Digest to identify the payload
19
+ def to_digest
20
+ [xy.lat, xy.long].to_digest
21
+ end
22
+
23
+ ##
24
+ # Render AIXM
25
+ #
26
+ # Extensions:
27
+ # * +:OFM+ - Open Flightmaps
28
+ def to_xml(*extensions)
29
+ format = extensions >> :OFM ? :OFM : :AIXM
30
+ builder = Builder::XmlMarkup.new(indent: 2)
31
+ builder.Avx do |avx|
32
+ avx.codeType('GRC')
33
+ avx.geoLat(xy.lat(format))
34
+ avx.geoLong(xy.long(format))
35
+ avx.codeDatum('WGE')
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ module AIXM
2
+ module Component
3
+
4
+ ##
5
+ # Schedules define activity time windows. As of now, only predefined
6
+ # schedules are imlemented either by use of explicit (e.g. +:continuous+)
7
+ # or short codes (e.g. +:H24+) as listed by the +CODES+ constant.
8
+ #
9
+ # Shortcuts:
10
+ # * +AIXM::H24+ - continuous 24/7
11
+ class Schedule < Base
12
+ using AIXM::Refinements
13
+
14
+ CODES = {
15
+ continuous: :H24,
16
+ sunrise_to_sunset: :HJ,
17
+ sunset_to_sunrise: :HN,
18
+ unspecified: :HX,
19
+ operational_request: :HO,
20
+ notam: :NOTAM
21
+ }.freeze
22
+
23
+ attr_reader :code
24
+
25
+ def initialize(code:)
26
+ @code = code&.to_sym
27
+ @code = CODES[code] unless CODES.has_value? code
28
+ fail(ArgumentError, "code `#{code}' not recognized") unless @code
29
+ end
30
+
31
+ ##
32
+ # Digest to identify the payload
33
+ def to_digest
34
+ [code].to_digest
35
+ end
36
+
37
+ ##
38
+ # Render AIXM
39
+ def to_xml(*extensions)
40
+ Builder::XmlMarkup.new(indent: 2).codeWorkHr(code.to_s)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -1,18 +1,18 @@
1
1
  module AIXM
2
- module Vertical
2
+ module Component
3
3
 
4
4
  ##
5
- # Vertical limits
6
- #
7
- # Normally noted as:
5
+ # Vertical limits define a 3D airspace vertically. They are normally noted
6
+ # as follows:
8
7
  #
9
8
  # +upper z+ (or +max_z+ whichever is higher)
10
9
  # ---------
11
10
  # +lower_z+ (or +min_z+ whichever is lower)
12
11
  #
13
- # Use +AIXM::GROUND+ as a shortcut for surface aka zero height.
14
- class Limits
15
-
12
+ # Shortcuts:
13
+ # * +AIXM::GROUND+ - surface (aka: 0ft QFE)
14
+ # * +AIXM::UNLIMITED+ - no upper limit (aka: FL 999)
15
+ class VerticalLimits < Base
16
16
  using AIXM::Refinements
17
17
 
18
18
  TAGS = { upper: :Upper, lower: :Lower, max: :Max, min: :Mnm }.freeze
@@ -20,13 +20,7 @@ module AIXM
20
20
 
21
21
  attr_reader :upper_z, :lower_z, :max_z, :min_z
22
22
 
23
- ##
24
- # Defines vertical limits +upper_z+ and +lower_z+
25
- #
26
- # Options:
27
- # * +max_z+ - alternative upper limit "whichever is higher"
28
- # * +min_z+ - alternative lower limit "whichever is lower"
29
- def initialize(upper_z:, lower_z:, max_z: nil, min_z: nil)
23
+ def initialize(max_z: nil, upper_z:, lower_z:, min_z: nil)
30
24
  fail(ArgumentError, "invalid upper_z") unless upper_z.is_a? AIXM::Z
31
25
  fail(ArgumentError, "invalid lower_z") unless lower_z.is_a? AIXM::Z
32
26
  fail(ArgumentError, "invalid max_z") unless max_z.nil? || max_z.is_a?(AIXM::Z)
@@ -55,5 +49,6 @@ module AIXM
55
49
  end.target! # see https://github.com/jimweirich/builder/issues/42
56
50
  end
57
51
  end
52
+
58
53
  end
59
54
  end
@@ -1,29 +1,32 @@
1
1
  module AIXM
2
2
  class Document
3
3
 
4
- include Enumerable
5
- extend Forwardable
6
4
  using AIXM::Refinements
7
5
 
8
- def_delegators :@result_array, :each, :<<
9
-
10
6
  attr_reader :created_at, :effective_at
7
+ attr_accessor :features
11
8
 
12
9
  ##
13
10
  # Define a AIXM-Snapshot document
14
11
  #
15
12
  # Options:
16
- # * +created_at+ - creation date (default: now)
17
- # * +effective_at+ - snapshot effective after date (default: now)
13
+ # * +created_at+ - creation date and time (default: now)
14
+ # * +effective_at+ - snapshot effective after date and time (default: now)
18
15
  def initialize(created_at: nil, effective_at: nil)
19
- @created_at, @effective_at = created_at, effective_at
20
- @result_array = []
16
+ @created_at, @effective_at = parse_time(created_at), parse_time(effective_at)
17
+ @features = []
21
18
  end
22
19
 
23
20
  ##
24
- # Array of features defined by this document
25
- def features
26
- @result_array
21
+ # Check whether the document is complete (extensions excluded)
22
+ def complete?
23
+ features.any? && features.none? { |f| !f.complete? }
24
+ end
25
+
26
+ ##
27
+ # Validate atainst the XSD and return +true+ if no errors were found
28
+ def valid?
29
+ errors.none?
27
30
  end
28
31
 
29
32
  ##
@@ -33,12 +36,6 @@ module AIXM
33
36
  xsd.validate(Nokogiri::XML(to_xml))
34
37
  end
35
38
 
36
- ##
37
- # Check whether the document is valid (extensions excluded)
38
- def valid?
39
- any? && reduce(true) { |b, f| b && f.valid? } && errors.none?
40
- end
41
-
42
39
  ##
43
40
  # Render AIXM
44
41
  #
@@ -53,12 +50,26 @@ module AIXM
53
50
  created: @created_at&.xmlschema || now,
54
51
  effective: @effective_at&.xmlschema || now
55
52
  }
56
- meta[:version] += ' + OFM extensions of version 0.1' if extensions.include?(:OFM)
53
+ meta[:version] += ' + OFM extensions of version 0.1' if extensions >> :OFM
57
54
  builder = Builder::XmlMarkup.new(indent: 2)
58
55
  builder.instruct!
59
56
  builder.tag!('AIXM-Snapshot', meta) do |aixm_snapshot|
60
- aixm_snapshot << @result_array.map { |f| f.to_xml(extensions) }.join.indent(2)
57
+ aixm_snapshot << features.map { |f| f.to_xml(*extensions) }.join.indent(2)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def parse_time(value)
64
+ case value
65
+ when String then Time.parse(value)
66
+ when Date then value.to_time
67
+ when Time then value
68
+ when nil then nil
69
+ else fail ArgumentError
61
70
  end
71
+ rescue ArgumentError
72
+ raise(ArgumentError, "`#{value}' is not a valid date and time")
62
73
  end
63
74
 
64
75
  end
@@ -4,38 +4,43 @@ module AIXM
4
4
 
5
5
  using AIXM::Refinements
6
6
 
7
- attr_reader :name, :type
8
- attr_reader :vertical_limits
9
- attr_accessor :geometry, :remarks
7
+ attr_reader :name, :short_name, :type
8
+ attr_reader :schedule
9
+ attr_accessor :geometry, :class_layers, :remarks
10
10
 
11
11
  ##
12
12
  # Airspace feature
13
13
  #
14
14
  # Options:
15
- # * +name+ - name of the airspace (will be converted to uppercase)
15
+ # * +name+ - full name of the airspace (will be converted to uppercase,
16
+ # e.g. +LF P 81+)
17
+ # * +short_name+ - short name of the airspace (will be converted to
18
+ # uppercase, e.g. +LF P 81 CHERBOURG+)
16
19
  # * +type+ - airspace type (e.g. +TMA+ or +P+)
17
- def initialize(name:, type:)
18
- @geometry = AIXM::Geometry.new
19
- @name, @type = name.upcase, type
20
+ def initialize(name:, short_name: nil, type:)
21
+ @name, @short_name, @type = name.uptrans, short_name&.uptrans, type
22
+ @schedule = nil
23
+ @geometry = AIXM.geometry
24
+ @class_layers = []
20
25
  end
21
26
 
22
27
  ##
23
- # Assign a +Vertical::Limits+ object
24
- def vertical_limits=(value)
25
- fail(ArgumentError, "invalid vertical limit") unless value.is_a?(AIXM::Vertical::Limits)
26
- @vertical_limits = value
28
+ # Assign a +Schedule+ object or +nil+
29
+ def schedule=(value)
30
+ fail(ArgumentError, "invalid schedule") unless value.nil? || value.is_a?(AIXM::Component::Schedule)
31
+ @schedule = value
27
32
  end
28
33
 
29
34
  ##
30
- # Check whether the airspace is valid
31
- def valid?
32
- name && type && vertical_limits && geometry.valid?
35
+ # Check whether the airspace is complete
36
+ def complete?
37
+ !!name && !!type && class_layers.any? && geometry.complete?
33
38
  end
34
39
 
35
40
  ##
36
41
  # Digest to identify the payload
37
42
  def to_digest
38
- [name, type, vertical_limits.to_digest, geometry.to_digest, remarks].to_digest
43
+ [name, short_name, type, schedule&.to_digest, class_layers.map(&:to_digest), geometry.to_digest, remarks].to_digest
39
44
  end
40
45
 
41
46
  ##
@@ -46,28 +51,54 @@ module AIXM
46
51
  def to_xml(*extensions)
47
52
  mid = to_digest
48
53
  builder = Builder::XmlMarkup.new(indent: 2)
49
- builder.Ase(extensions.include?(:OFM) ? { xt_classLayersAvail: false } : {}) do |ase|
50
- ase.AseUid(extensions.include?(:OFM) ? { mid: mid, newEntity: true } : { mid: mid }) do |aseuid|
51
- aseuid.codeType(type)
52
- aseuid.codeId(mid) # TODO: verify
54
+ builder.comment! "Airspace: [#{type}] #{name}"
55
+ builder.Ase({ xt_classLayersAvail: ((class_layers.count > 1) if extensions >> :OFM) }.compact) do |ase|
56
+ ase.AseUid({ mid: mid, newEntity: (true if extensions >> :OFM) }.compact) do |aseuid|
57
+ aseuid.codeType(type.to_s)
58
+ aseuid.codeId(mid)
53
59
  end
54
- ase.txtName(name)
55
- ase << vertical_limits.to_xml(extensions).indent(2)
56
- ase.txtRmk(remarks) if remarks
57
- if extensions.include?(:OFM)
58
- ase.xt_txtRmk(remarks)
59
- ase.xt_selAvail(false)
60
+ ase.txtLocalType(short_name.to_s) if short_name && short_name != name
61
+ ase.txtName(name.to_s)
62
+ ase << class_layers.first.to_xml(*extensions).indent(2)
63
+ if schedule
64
+ ase.Att do |att|
65
+ att << schedule.to_xml(*extensions).indent(4)
66
+ end
60
67
  end
68
+ ase.txtRmk(remarks.to_s) if remarks
69
+ ase.xt_selAvail(false) if extensions >> :OFM
61
70
  end
62
71
  builder.Abd do |abd|
63
72
  abd.AbdUid do |abduid|
64
- abduid.AseUid(extensions.include?(:OFM) ? { mid: mid, newEntity: true } : { mid: mid }) do |aseuid|
65
- aseuid.codeType(type)
66
- aseuid.codeId(mid) # TODO: verify
73
+ abduid.AseUid({ mid: mid, newEntity: (true if extensions >> :OFM) }.compact) do |aseuid|
74
+ aseuid.codeType(type.to_s)
75
+ aseuid.codeId(mid)
76
+ end
77
+ end
78
+ abd << geometry.to_xml(*extensions).indent(2)
79
+ end
80
+ if class_layers.count > 1
81
+ builder.Adg do |adg|
82
+ class_layers.each.with_index do |class_layer, index|
83
+ adg.AdgUid do |adguid|
84
+ adguid.AseUid(mid: "#{mid}.#{index + 1}") do |aseuid|
85
+ aseuid.codeType("CLASS")
86
+ end
87
+ end
88
+ end
89
+ adg.AseUidSameExtent(mid: mid)
90
+ end
91
+ class_layers.each.with_index do |class_layer, index|
92
+ builder.Ase do |ase|
93
+ ase.AseUid(mid: "#{mid}.#{index + 1}") do |aseuid|
94
+ aseuid.codeType("CLASS")
95
+ end
96
+ ase.txtName(name.to_s)
97
+ ase << class_layers[index].to_xml(*extensions).indent(2)
67
98
  end
68
99
  end
69
- abd << geometry.to_xml(extensions).indent(2)
70
100
  end
101
+ builder.target! # see https://github.com/jimweirich/builder/issues/42
71
102
  end
72
103
  end
73
104
  end