aixm 0.1.0 → 0.1.3

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