aixm 1.0.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +62 -13
  4. data/README.md +34 -21
  5. data/lib/aixm/a.rb +110 -78
  6. data/lib/aixm/association.rb +39 -28
  7. data/lib/aixm/classes.rb +9 -2
  8. data/lib/aixm/{feature → component}/address.rb +27 -18
  9. data/lib/aixm/component/approach_lighting.rb +139 -0
  10. data/lib/aixm/component/fato.rb +93 -65
  11. data/lib/aixm/component/frequency.rb +34 -22
  12. data/lib/aixm/component/geometry/arc.rb +17 -4
  13. data/lib/aixm/component/geometry/border.rb +9 -2
  14. data/lib/aixm/component/geometry/circle.rb +17 -5
  15. data/lib/aixm/component/geometry/point.rb +9 -2
  16. data/lib/aixm/component/geometry/rhumb_line.rb +9 -2
  17. data/lib/aixm/component/geometry.rb +23 -12
  18. data/lib/aixm/component/helipad.rb +67 -56
  19. data/lib/aixm/component/layer.rb +36 -23
  20. data/lib/aixm/component/lighting.rb +26 -28
  21. data/lib/aixm/component/runway.rb +108 -72
  22. data/lib/aixm/component/service.rb +23 -20
  23. data/lib/aixm/component/surface.rb +60 -27
  24. data/lib/aixm/component/timesheet.rb +178 -0
  25. data/lib/aixm/component/timetable.rb +34 -15
  26. data/lib/aixm/component/vasis.rb +135 -0
  27. data/lib/aixm/component/vertical_limit.rb +29 -7
  28. data/lib/aixm/component.rb +13 -0
  29. data/lib/aixm/concerns/hash_equality.rb +21 -0
  30. data/lib/aixm/concerns/intensity.rb +30 -0
  31. data/lib/aixm/concerns/marking.rb +21 -0
  32. data/lib/aixm/concerns/remarks.rb +21 -0
  33. data/lib/aixm/concerns/timetable.rb +22 -0
  34. data/lib/aixm/config.rb +2 -0
  35. data/lib/aixm/d.rb +32 -25
  36. data/lib/aixm/document.rb +32 -6
  37. data/lib/aixm/f.rb +30 -18
  38. data/lib/aixm/feature/airport.rb +125 -55
  39. data/lib/aixm/feature/airspace.rb +31 -5
  40. data/lib/aixm/feature/navigational_aid/designated_point.rb +8 -1
  41. data/lib/aixm/feature/navigational_aid/dme.rb +41 -12
  42. data/lib/aixm/feature/navigational_aid/marker.rb +11 -4
  43. data/lib/aixm/feature/navigational_aid/ndb.rb +15 -3
  44. data/lib/aixm/feature/navigational_aid/tacan.rb +3 -2
  45. data/lib/aixm/feature/navigational_aid/vor.rb +52 -16
  46. data/lib/aixm/feature/navigational_aid.rb +30 -21
  47. data/lib/aixm/feature/obstacle.rb +111 -35
  48. data/lib/aixm/feature/obstacle_group.rb +9 -9
  49. data/lib/aixm/feature/organisation.rb +24 -14
  50. data/lib/aixm/feature/unit.rb +25 -12
  51. data/lib/aixm/feature.rb +25 -3
  52. data/lib/aixm/memoize.rb +27 -11
  53. data/lib/aixm/p.rb +22 -15
  54. data/lib/aixm/payload_hash.rb +6 -3
  55. data/lib/aixm/r.rb +65 -0
  56. data/lib/aixm/refinements.rb +43 -3
  57. data/lib/aixm/schedule/date.rb +181 -0
  58. data/lib/aixm/schedule/day.rb +114 -0
  59. data/lib/aixm/schedule/time.rb +276 -0
  60. data/lib/aixm/shortcuts.rb +3 -0
  61. data/lib/aixm/version.rb +1 -1
  62. data/lib/aixm/w.rb +21 -13
  63. data/lib/aixm/xy.rb +37 -26
  64. data/lib/aixm/z.rb +28 -15
  65. data/lib/aixm.rb +23 -5
  66. data/schemas/ofmx/0.1/OFMX-DataTypes.xsd +6 -0
  67. data/schemas/ofmx/0.1/OFMX-Snapshot.xsd +5 -0
  68. data.tar.gz.sig +0 -0
  69. metadata +27 -14
  70. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f4efdae60e160732c8f30262673f01b49d6dd53d5c5e9bc2d8c395cf696ccae
4
- data.tar.gz: 7b129a858e6c6d0eb7545d4a32910243e7a8ecd00a8895698f4ae69fcaed33f2
3
+ metadata.gz: ce948465612571ae319c68d7f0eff0664d41c696440fb190298ac475b0e3105d
4
+ data.tar.gz: 402358be48d240b24baef522ace4c501b1c685073ef28be35ce70985a5eaa9e8
5
5
  SHA512:
6
- metadata.gz: 98115e7b0f93f82e976235045afdff88b3b6a99b256620de733b4b4741ffa7fb769e6bb92aae86657d6ae57b5744aef5df82a634b22adb5ae81b9db18a7c1531
7
- data.tar.gz: fd3bf40b77d4cfef51465b4e1806382465a4eee86abd8c74c3639763a627653a7832696395bab9673dd6433458bef8474871b7dcd121533891df3c911a429baf
6
+ metadata.gz: ee675af09c87b12e15bd50e2b2a2c8f00b457cb6ec53c8ee3d16d1b1b5965e410687c2f24f51c49a9673cef00d882f2ef6101e6af780161d33f6f898ff903f9f
7
+ data.tar.gz: da8135812e7dd27baeb8068b8eefd6004467370763170737c18258b09cdc520bf09aad70fd6eb4c567dd39d5920c46ac1fd87a0ca5e30895c33b684fdf52ab73
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,10 +1,59 @@
1
+ ## Main
2
+
3
+ Nothing so far
4
+
5
+ ## 1.2.1
6
+
7
+ #### Additions
8
+ * Rounding of `AIXM::Schedule::Time`
9
+
10
+ ## 1.2.0
11
+
12
+ #### Additions
13
+ * `Timesheet` to add custom schedules to `Timetable`
14
+ * `AIXM::Schedule::(Date|Day|Time)` for custom timetables
15
+ * Interface to allow most class instances as Hash keys
16
+
17
+ #### Fixes
18
+ * Fix typo in `GUESSED_UNIT_TYPES_MAP`
19
+
20
+ ## 1.1.0
21
+
22
+ #### Breaking Changes
23
+ * `AIXM::Association:Array#duplicates` now returns an array of arrays which
24
+ group all duplicates together.
25
+ * `VOR#associate_dme` and `VOR#associate_tacan` no longer take the channel
26
+ as argument but calculate it from the (ghost) frequency of the VOR.
27
+ * Replaced `#length`/`#width` with `#dimensions` on `Runway`, `Helipad` and `FATO`
28
+ * Renamed `AIXM::D#dist` to `AIXM::D#dim`
29
+ * Renamed `TLOF#helicopter_class` to `TLOF#performance_class`
30
+ * Renamed `#geographic_orientation` and `#magnetic_orientation` to more familiar
31
+ `#geographic_bearing` and `#magnetic_bearing` on `Runway` and `FATO`
32
+ * Re-implementation of `AIXM::A` without precision
33
+ * Demoted `Address` to component
34
+ * Fixed typo in `Service` type `:vdf_direction_finding_service`
35
+
36
+ #### Additions
37
+ * Associations from `Service` to `Airport` and `Airspace`
38
+ * `AIXM::R` (rectangle)
39
+ * `Runway#marking`
40
+ * `ApproachLighting` on `Runway::Direction` and `FATO::Direction`
41
+ * `VASIS` on `Runway::Direction` and `FATO::Direction`
42
+ * `#meta` on every feature and component
43
+ * `Document#regions` which is added to the root element for OFMX
44
+
45
+ #### Changes
46
+ * Nested memoization of the same method is now allowed and won't reset the
47
+ memoization cache anymore.
48
+ * Remove unit "mhz" from `Address` of type `:radio_frequency`.
49
+
1
50
  ## 1.0.0
2
51
 
3
52
  #### Breaking Changes
4
53
  * Move `Ase->txtLocalType` up into `AseUid` for OFMX
5
54
 
6
55
  #### Additions
7
- * Add `AIXM::Component::Geometry::RhumbLine`
56
+ * Add rhumb line geometry
8
57
 
9
58
  ## 0.3.11
10
59
 
@@ -19,21 +68,21 @@
19
68
  #### Additions
20
69
  * Add `f#voice?` and `AIXM.config.voice_channel_separation` to check whether a
21
70
  frequency belongs to the voice communication airband and use it to validate
22
- `AIXM::Frequency`
71
+ `Frequency`
23
72
 
24
73
  ## 0.3.10
25
74
 
26
75
  #### Additions
27
76
  * Proper `has_many` and `has_one` associations
28
- * `AIXM::Association:Array#find_by|find|duplicate` on `has_many` associations
77
+ * `AIXM::Association:Array#find_by|find|duplicates` on `has_many` associations
29
78
  * `AIXM.config.mid` now defines whether `mid` attributes are inserted or not
30
79
  provided the selected schema is OFMX
31
80
  * `AIXM::Memoize` module
32
81
  * `AIXM::PayloadHash` class
33
82
  * `mkmid` executable to insert `mid` attributes into valid OFMX file
34
83
  * `ckmid` executable to check `mid` attributes in an OFMX file
35
- * `AIXM::Component::Geometry#point?|circle?|polygon?`
36
- * `AIXM::Component::Layer#services`
84
+ * Geometries respond to `#point?`, `#circle?` and `#polygon?`
85
+ * `Layer#services`
37
86
 
38
87
  #### Breaking Changes
39
88
  * Require Ruby 2.7
@@ -63,20 +112,20 @@
63
112
  ## 0.3.7
64
113
 
65
114
  #### Additions
66
- * `AIXM::Document#select_features`
67
- * `AIXM::Document#group_obstacles!`
115
+ * `Document#select_features`
116
+ * `Document#group_obstacles!`
68
117
 
69
118
  ## 0.3.6
70
119
 
71
120
  #### Additions
72
- * `AIXM::Component::FATO`
73
- * `AIXM::Component::Helipad#helicopter_class` and `AIXM::Component::Helipad#marking`
121
+ * `FATO`
122
+ * `Helipad#helicopter_class` and `Helipad#marking`
74
123
  * `AIXM::XY#seconds?` to detect possibly rounded or estimated coordinates
75
- * `AIXM::Features::Airport#operator`
124
+ * `Airport#operator`
76
125
  * `AIXM::W` (weight)
77
126
  * `AIXM::P` (pressure)
78
- * `AIXM::Component::Lighting` for use with runways, helipads and FATOs
79
- * Surface details `siwl_weight`, `siwl_tire_pressure` and `auw_weight`
127
+ * `Lighting` for use with runways, helipads and FATOs
128
+ * Surface details `#siwl_weight`, `#siwl_tire_pressure` and `#auw_weight`
80
129
 
81
130
  #### Changes
82
131
  * Generate `Airport#id` from region and `Airport#name`
@@ -242,7 +291,7 @@
242
291
 
243
292
  #### Changes
244
293
  * `Document#created_at` and `#effective_at` accept Time, Date, String or *nil*
245
- * Separate `AIXM::Document#valid?` from `#complete?`
294
+ * Separate `Document#valid?` from `#complete?`
246
295
  * Write coordinates in DD if extension `:OFM` is set
247
296
  * `Array#to_digest` returns Integer which fits in signed 32bit
248
297
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ For now, only the parts needed to automize the AIP import of [open flightmaps](h
11
11
 
12
12
  * [Homepage](https://github.com/svoop/aixm)
13
13
  * [API](https://www.rubydoc.info/gems/aixm)
14
- * Author: [Sven Schwyn - Bitcetera](http://www.bitcetera.com)
14
+ * Author: [Sven Schwyn - Bitcetera](https://bitcetera.com)
15
15
 
16
16
  ## Install
17
17
 
@@ -47,7 +47,7 @@ gem install aixm --trust-policy MediumSecurity
47
47
 
48
48
  ## Usage
49
49
 
50
- Here's how to build a document object, populate it with a simple feature and then render it as AIXM:
50
+ Here's how to build a document object, populate it with a simple feature and then render it as AIXM or OFMX:
51
51
 
52
52
  ```ruby
53
53
  document = AIXM.document(
@@ -139,17 +139,16 @@ AIXM.config.ignored_errors = /invalid date/i
139
139
 
140
140
  ### Fundamentals
141
141
  * [Document](https://www.rubydoc.info/gems/aixm/AIXM/Document.html)
142
+ * [A (angle)](https://www.rubydoc.info/gems/aixm/AIXM/A.html)
143
+ * [D (dimension, distance or length)](https://www.rubydoc.info/gems/aixm/AIXM/D.html)
144
+ * [F (frequency)](https://www.rubydoc.info/gems/aixm/AIXM/F.html)
145
+ * [P (pressure)](https://www.rubydoc.info/gems/aixm/AIXM/P.html)
146
+ * [R (rectangle)](https://www.rubydoc.info/gems/aixm/AIXM/R.html)
142
147
  * [XY (longitude and latitude)](https://www.rubydoc.info/gems/aixm/AIXM/XY.html)
143
148
  * [Z (height, elevation or altitude)](https://www.rubydoc.info/gems/aixm/AIXM/Z.html)
144
- * [D (distance or length)](https://www.rubydoc.info/gems/aixm/AIXM/D.html)
145
- * [F (frequency)](https://www.rubydoc.info/gems/aixm/AIXM/F.html)
146
- * [A (angle)](https://www.rubydoc.info/gems/aixm/AIXM/A.html)
147
149
 
148
150
  ### Features
149
151
  * [Address](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Address.html)
150
- * [Organisation](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Organisation.html)
151
- * [Unit](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Unit.html)
152
- * [Service](https://www.rubydoc.info/gems/aixm/AIXM/Component/Service.html)
153
152
  * [Airport](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Airport.html)
154
153
  * [Airspace](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Airspace.html)
155
154
  * [Navigational aid](https://www.rubydoc.info/gems/aixm/AIXM/NavigationalAid.html)
@@ -159,22 +158,33 @@ AIXM.config.ignored_errors = /invalid date/i
159
158
  * [NDB](https://www.rubydoc.info/gems/aixm/AIXM/Feature/NDB.html)
160
159
  * [TACAN](https://www.rubydoc.info/gems/aixm/AIXM/Feature/TACAN.html)
161
160
  * [VOR](https://www.rubydoc.info/gems/aixm/AIXM/Feature/VOR.html)
162
- * [Obstacle and obstacle group](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Obstacle.html)
161
+ * [Obstacle](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Obstacle.html)
162
+ * [Obstacle group](https://www.rubydoc.info/gems/aixm/AIXM/Feature/ObstacleGroup.html)
163
+ * [Organisation](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Organisation.html)
164
+ * [Service](https://www.rubydoc.info/gems/aixm/AIXM/Component/Service.html)
165
+ * [Unit](https://www.rubydoc.info/gems/aixm/AIXM/Feature/Unit.html)
163
166
 
164
167
  ### Components
168
+
169
+ * [ApproachLighting](https://www.rubydoc.info/gems/aixm/AIXM/Component/ApproachLighting.html)
170
+ * [FATO](https://www.rubydoc.info/gems/aixm/AIXM/Component/FATO.html)
165
171
  * [Frequency](https://www.rubydoc.info/gems/aixm/AIXM/Component/Frequency.html)
166
172
  * [Geometry](https://www.rubydoc.info/gems/aixm/AIXM/Component/Geometry.html)
167
- * [Point](https://www.rubydoc.info/gems/aixm/AIXM/Component/Point.html)
168
173
  * [Arc](https://www.rubydoc.info/gems/aixm/AIXM/Component/Arc.html)
169
174
  * [Border](https://www.rubydoc.info/gems/aixm/AIXM/Component/Border.html)
170
175
  * [Circle](https://www.rubydoc.info/gems/aixm/AIXM/Component/Circle.html)
171
- * [Runway](https://www.rubydoc.info/gems/aixm/AIXM/Component/Runway.html)
176
+ * [Point](https://www.rubydoc.info/gems/aixm/AIXM/Component/Point.html)
177
+ * [RhumbLine](https://www.rubydoc.info/gems/aixm/AIXM/Component/RhumbLine.html)
172
178
  * [Helipad](https://www.rubydoc.info/gems/aixm/AIXM/Component/Helipad.html)
173
- * [FATO](https://www.rubydoc.info/gems/aixm/AIXM/Component/FATO.html)
174
- * [Surface](https://www.rubydoc.info/gems/aixm/AIXM/Component/Surface.html)
175
179
  * [Layer](https://www.rubydoc.info/gems/aixm/AIXM/Component/Layer.html)
176
- * [Vertical limit](https://www.rubydoc.info/gems/aixm/AIXM/Component/VerticalLimit.html)
180
+ * [Lighting](https://www.rubydoc.info/gems/aixm/AIXM/Component/Lighting.html)
181
+ * [Runway](https://www.rubydoc.info/gems/aixm/AIXM/Component/Runway.html)
182
+ * [Service](https://www.rubydoc.info/gems/aixm/AIXM/Component/Service.html)
183
+ * [Surface](https://www.rubydoc.info/gems/aixm/AIXM/Component/Surface.html)
177
184
  * [Timetable](https://www.rubydoc.info/gems/aixm/AIXM/Component/Timetable.html)
185
+ * [Timesheet](https://www.rubydoc.info/gems/aixm/AIXM/Component/Timesheet.html)
186
+ * [VASIS](https://www.rubydoc.info/gems/aixm/AIXM/Component/VASIS.html)
187
+ * [Vertical limit](https://www.rubydoc.info/gems/aixm/AIXM/Component/VerticalLimit.html)
178
188
 
179
189
  ## Associations
180
190
 
@@ -204,6 +214,14 @@ document.features.find(airport) # => [#<AIXM::Feature::Airport>]
204
214
 
205
215
  This may seem redundant at first, but keep in mind that two instances of +AIXM::CLASSES+ which implement `#to_uid` are considered equal if they are instances of the same class and both their UIDs as calculated by `#to_uid` are equal. Attributes which are not part of the `#to_uid` calculation are irrelevant!
206
216
 
217
+ ### meta
218
+
219
+ You can write arbitrary meta information to any feature or component. It won't be used when building the AIXM or OFMX document, in fact, it is not used by this gem at all. But you can store e.g. foreign keys and then later use them to find a feature or component like so:
220
+
221
+ ```ruby
222
+ document.features.find_by(:airport, meta: 1234) # 1234 is the foreign key
223
+ ```
224
+
207
225
  ### duplicates
208
226
 
209
227
  Equally on `has_many` associations, use `duplicates` to find identical or equal associations:
@@ -257,6 +275,7 @@ ckmid --help
257
275
  ### AIXM
258
276
  * [AIXM](http://aixm.aero)
259
277
  * [AICM 4.5 documentation](https://openflightmaps.gitlab.io/ofmx/aixm/4.5/manual/aicm/)
278
+ * [AICM 4.5 manual](https://www.aixm.aero/sites/aixm.aero/files/imce/library/aicm_manual_4-5.pdf)
260
279
  * [AIXM 4.5 specification](http://aixm.aero/document/aixm-45-specification)
261
280
 
262
281
  ### OFMX
@@ -282,13 +301,7 @@ bundle exec rake # run tests once
282
301
  bundle exec guard # run tests whenever files are modified
283
302
  ```
284
303
 
285
- Please submit issues on:
286
-
287
- https://github.com/svoop/aixm/issues
288
-
289
- To contribute code, fork the project on Github, add your code and submit a pull request:
290
-
291
- https://help.github.com/articles/fork-a-repo
304
+ You're welcome to [submit issues](https://github.com/svoop/aixm/issues) and contribute code by [forking the project and submitting pull requests](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
292
305
 
293
306
  ## License
294
307
 
data/lib/aixm/a.rb CHANGED
@@ -2,53 +2,61 @@ using AIXM::Refinements
2
2
 
3
3
  module AIXM
4
4
 
5
- # Angle from 0 to 359 degrees with an optional suffix used for azimuths,
6
- # bearings, headings, courses etc.
5
+ # Angle in the range of -360 < angle < 360 degrees (used for azimuths or
6
+ # courses) and with an optional one-letter suffix (used for runways).
7
7
  #
8
- # @example Initialized with Numeric
9
- # a = AIXM.a(12) # 12 degrees, 1 degree precision, no suffix
10
- # a.precision # => 3 (three digits = steps of 1 degree)
11
- # a.to_s # => "012"
12
- # a.suffix # => nil
13
- # a.deg # => 12
14
- # a.deg += 7 # => 19
15
- # a.deg += 341 # => 0 - deg is always within (0..359)
16
- # a.to_s # => "000" - to_s is always within ("000".."359")
8
+ # @example Initialization
9
+ # AIXM.a(-36.9) # => #<AIXM::A -36.9° "32">
10
+ # AIXM.a(12) # => #<AIXM::A 12° "01">
11
+ # AIXM.a("12L") # => #<AIXM::A 120° "12L">
12
+ # AIXM.a(360) # => #<AIXM::A 0° "36">
13
+ # AIXM.a(-400) # => #<AIXM::A -40° "32">
17
14
  #
18
- # @example Initialized with String
19
- # a = AIXM.a('06L') # 60 degrees, 10 degree precision, suffix :L
20
- # a.precision # => 2 (two digits = steps of 10 degrees)
21
- # a.to_s # => "06L"
22
- # a.suffix # => :L
23
- # a.deg # => 60
24
- # a.deg += 7 # => 70
25
- # a.deg += 190 # => 0 - deg is always within (0..359)
26
- # a.to_s # => "36L" - to_s converts to ("01".."36")
15
+ # @example Calculations
16
+ # a = AIXM.a("02L")
17
+ # a += 5 # => #<AIXM::A 25° "03L">
18
+ # a -= AIXM.a(342.8) # => #<AIXM::A -317.8° "04L">
19
+ # a.to_s # => "-317.8°"
20
+ # a.to_s(:runway) # => "04L"
21
+ # a.to_s(:bearing) # => "042.2000"
22
+ # a.to_f # => 42.2
23
+ # a.to_i # => 42
24
+ # a.invert # => #<AIXM::A -137.8° "22R">
25
+ # a.to_s(:runway) # => "22R"
27
26
  class A
27
+ include AIXM::Concerns::HashEquality
28
+
28
29
  SUFFIX_INVERSIONS = {
29
30
  R: :L,
30
31
  L: :R
31
32
  }.freeze
32
33
 
33
- # @return [Integer] angle
34
- attr_reader :deg
34
+ RUNWAY_RE = /\A(0[1-9]|[12]\d|3[0-6])([A-Z])?\z/
35
35
 
36
- # @return [Integer] precision: +2+ (10 degree steps) or +3+ (1 degree steps)
37
- attr_reader :precision
36
+ # Angle in the range of -360 < angle < 360.
37
+ #
38
+ # @overload deg
39
+ # @return [Integer]
40
+ # @overload deg=(value)
41
+ # @param value [Integer]
42
+ attr_reader :deg
38
43
 
39
- # @return [Symbol, nil] suffix
44
+ # One-letter suffix.
45
+ #
46
+ # @overload suffix
47
+ # @return [Symbol, nil]
48
+ # @overload suffix=(value)
49
+ # @param value [Symbol, nil]
40
50
  attr_reader :suffix
41
51
 
42
- def initialize(deg_and_suffix)
43
- case deg_and_suffix
44
- when Numeric
45
- self.deg, @precision = deg_and_suffix, 3
52
+ # See the {overview}[AIXM::A] for examples.
53
+ def initialize(value)
54
+ case value
46
55
  when String
47
- fail(ArgumentError, "invalid angle") unless deg_and_suffix.to_s =~ /\A(\d+)([A-Z]+)?\z/
48
- self.deg, @precision, self.suffix = $1.to_i * 10, 2, $2
49
- when Symbol # used only by private build method
50
- fail(ArgumentError, "invalid precision") unless %i(2 3).include? deg_and_suffix
51
- @deg, @precision = 0, deg_and_suffix.to_s.to_i
56
+ fail(ArgumentError, "invalid angle") unless value =~ RUNWAY_RE
57
+ self.deg, self.suffix = $1.to_i * 10, $2
58
+ when Numeric
59
+ self.deg = value
52
60
  else
53
61
  fail(ArgumentError, "invalid angle")
54
62
  end
@@ -56,42 +64,69 @@ module AIXM
56
64
 
57
65
  # @return [String]
58
66
  def inspect
59
- %Q(#<#{self.class}[precision=#{precision}] #{to_s}>)
67
+ %Q(#<#{self.class} #{to_s} #{to_s(:runway).inspect}>)
60
68
  end
61
69
 
62
- # @return [String] human readable representation according to precision
63
- def to_s
64
- if precision == 2
65
- [('%02d' % ((deg / 10 + 35) % 36 + 1)), suffix].map(&:to_s).join
66
- else
67
- ('%03d' % deg)
70
+ # Degrees in the range of 0..359
71
+ #
72
+ # @return [Integer]
73
+ def to_i
74
+ (deg.round + 360) % 360
75
+ end
76
+
77
+ # Degrees in the range of 0.0...360.0
78
+ #
79
+ # @return [Float]
80
+ def to_f
81
+ ((deg + 360) % 360).to_f
82
+ end
83
+
84
+ # Degrees as formatted string
85
+ #
86
+ # Types are:
87
+ # * :human - degrees within -359.9~..359.9~ as D.D° (default)
88
+ # * :bearing - degrees within 0.0..359.9~ as DDD.DDDD
89
+ # * :runway - degrees within "01".."36" plus optional suffix
90
+ #
91
+ # @param type [Symbol, nil] either :runway, :bearing or nil
92
+ # @param unit [String] unit to postfix
93
+ # @return [String]
94
+ def to_s(type=:human)
95
+ return '' unless deg
96
+ case type
97
+ when :runway then [('%02d' % (((deg / 10).round + 35) % 36 + 1)), suffix].join
98
+ when :bearing then '%08.4f' % to_f.round(4)
99
+ when :human then [deg.to_s('F').sub(/\.0$/, ''), '°'].join
100
+ else fail ArgumentError
68
101
  end
69
102
  end
70
103
 
71
104
  def deg=(value)
72
- fail(ArgumentError, "invalid deg `#{value}'") unless value.is_a?(Numeric) && value.round.between?(0, 360)
73
- @deg = (precision == 2 ? (value.to_f / 10).round * 10 : value.round) % 360
105
+ fail(ArgumentError, "invalid deg `#{value}'") unless value.is_a? Numeric
106
+ normalized_value = value.abs % 360
107
+ sign = '-' if value.negative? && normalized_value.nonzero?
108
+ @deg = BigDecimal("#{sign}#{normalized_value}")
74
109
  end
75
110
 
76
111
  def suffix=(value)
77
- fail(RuntimeError, "suffix only allowed when precision is 2") unless value.nil? || precision == 2
78
- fail(ArgumentError, "invalid suffix") unless value.nil? || value.to_s =~ /\A[A-Z]+\z/
112
+ fail(ArgumentError, "invalid suffix") unless value.nil? || value.to_s =~ /\A[A-Z]\z/
79
113
  @suffix = value&.to_s&.to_sym
80
114
  end
81
115
 
82
- # Invert an angle by 180 degrees
116
+ # Invert an angle by 180 degrees.
83
117
  #
84
118
  # @example
85
- # AIXM.a(120).invert # => AIXM.a(300)
86
- # AIXM.a("34L").invert # => AIXM.a("16R")
87
- # AIXM.a("33X").invert # => AIXM.a("33X")
119
+ # AIXM.a(120).invert # (300°)
120
+ # AIXM.a("34L").invert # (160° suffix "R")
88
121
  #
89
- # @return [AIXM::A] inverted angle
122
+ # @return [AIXM::A]
90
123
  def invert
91
- build(precision: precision, deg: (deg + 180) % 360, suffix: SUFFIX_INVERSIONS.fetch(suffix, suffix))
124
+ self.class.new(deg.negative? ? deg - 180 : deg + 180).tap do |angle|
125
+ angle.suffix = SUFFIX_INVERSIONS.fetch(suffix, suffix)
126
+ end
92
127
  end
93
128
 
94
- # Check whether +other+ angle is the inverse
129
+ # Whether other angle is the inverse.
95
130
  #
96
131
  # @example
97
132
  # AIXM.a(120).inverse_of? AIXM.a(300) # => true
@@ -99,52 +134,49 @@ module AIXM
99
134
  # AIXM.a("33X").inverse_of? AIXM.a("33X") # => true
100
135
  # AIXM.a("16R").inverse_of? AIXM.a("16L") # => false
101
136
  #
102
- # @return [Boolean] whether the inverted angle or not
137
+ # @return [Boolean]
103
138
  def inverse_of?(other)
104
139
  invert == other
105
140
  end
106
141
 
107
- # Add degrees
142
+ # Negate degrees.
108
143
  #
109
144
  # @return [AIXM::A]
110
- def +(numeric_or_angle)
111
- fail ArgumentError unless numeric_or_angle.respond_to? :round
112
- build(precision: precision, deg: (deg + numeric_or_angle.round) % 360, suffix: suffix)
145
+ def -@
146
+ deg.zero? ? self : self.class.new(-deg).tap { _1.suffix = suffix }
113
147
  end
114
148
 
115
- # Subtract degrees
149
+ # Add degrees.
116
150
  #
151
+ # @param value [Numeric, AIXM::A]
117
152
  # @return [AIXM::A]
118
- def -(numeric_or_angle)
119
- fail ArgumentError unless numeric_or_angle.respond_to? :round
120
- build(precision: precision, deg: (deg - numeric_or_angle.round + 360) % 360, suffix: suffix)
153
+ def +(value)
154
+ case value
155
+ when Numeric
156
+ value.zero? ? self : self.class.new(deg + value).tap { _1.suffix = suffix }
157
+ when AIXM::A
158
+ value.deg.zero? ? self : self.class.new(deg + value.deg).tap { _1.suffix = suffix }
159
+ else
160
+ fail ArgumentError
161
+ end
121
162
  end
122
163
 
123
- # @private
124
- def round
125
- deg
164
+ # Subtract degrees.
165
+ #
166
+ # @param value [Numeric, AIXM::A]
167
+ # @return [AIXM::A]
168
+ def -(value)
169
+ self + -value
126
170
  end
127
171
 
128
172
  # @see Object#==
129
- # @return [Boolean]
130
173
  def ==(other)
131
- self.class === other && deg == other.deg && precision == other.precision && suffix == other.suffix
174
+ self.class === other && deg == other.deg && suffix == other.suffix
132
175
  end
133
- alias_method :eql?, :==
134
176
 
135
177
  # @see Object#hash
136
- # @return [Integer]
137
178
  def hash
138
- to_s.hash
139
- end
140
-
141
- private
142
-
143
- def build(precision:, deg:, suffix: nil)
144
- self.class.new(precision.to_s.to_sym).tap do |a|
145
- a.deg = deg
146
- a.suffix = suffix
147
- end
179
+ [self.class, deg, suffix].join.hash
148
180
  end
149
181
  end
150
182
 
@@ -23,18 +23,18 @@ module AIXM
23
23
  # end
24
24
  # blog, post = Blog.new, Post.new
25
25
  # # --either--
26
- # blog.add_post(post)
26
+ # blog.add_post(post) # => Blog
27
27
  # blog.posts.count # => 1
28
28
  # blog.posts.first == post # => true
29
29
  # post.blog == blog # => true
30
- # blog.remove_post(post)
30
+ # blog.remove_post(post) # => Blog
31
31
  # blog.posts.count # => 0
32
32
  # # --or--
33
- # post.blog = blog
33
+ # post.blog = blog # => Blog
34
34
  # blog.posts.count # => 1
35
35
  # blog.posts.first == post # => true
36
36
  # post.blog == blog # => true
37
- # post.blog = nil
37
+ # post.blog = nil # => nil
38
38
  # blog.posts.count # => 0
39
39
  # # --or--
40
40
  # post_2 = Post.new
@@ -53,19 +53,21 @@ module AIXM
53
53
  # end
54
54
  # blog, post = Blog.new, Post.new
55
55
  # # --either--
56
- # blog.post = post
57
- # blog.post == post # => true
58
- # post.blog == blog # => true
59
- # blog.post = nil
60
- # blog.post # => nil
61
- # post.blog # => nil
56
+ # blog.post = post # => Post (standard assignment)
57
+ # blog.add_post(post) # => Blog (alternative for chaining)
58
+ # blog.post == post # => true
59
+ # post.blog == blog # => true
60
+ # blog.post = nil # => nil
61
+ # blog.post # => nil
62
+ # post.blog # => nil
62
63
  # # --or--
63
- # post.blog = blog
64
- # post.blog == blog # => true
65
- # blog.post == post # => true
66
- # post.blog = nil
67
- # post.blog # => nil
68
- # blog.post # => nil
64
+ # post.blog = blog # => Blog (standard assignment)
65
+ # post.add_blog(blog) # => Post (alternative for chaining)
66
+ # post.blog == blog # => true
67
+ # blog.post == post # => true
68
+ # post.blog = nil # => nil
69
+ # post.blog # => nil
70
+ # blog.post # => nil
69
71
  #
70
72
  # @example Association with readonly +belongs_to+ (idem for +has_one+)
71
73
  # class Blog
@@ -177,7 +179,7 @@ module AIXM
177
179
  (@has_many_attributes ||= []) << attribute
178
180
  # features
179
181
  define_method(attribute) do
180
- instance_eval("@#{attribute} ||= AIXM::Association::Array.new")
182
+ instance_variable_get(:"@#{attribute}") || AIXM::Association::Array.new
181
183
  end
182
184
  # add_feature
183
185
  define_method(:"add_#{association}") do |object=nil, **options, &add_block|
@@ -188,6 +190,7 @@ module AIXM
188
190
  end
189
191
  instance_exec(object, **options, &association_block) if association_block
190
192
  fail(ArgumentError, "#{object.__class__} not allowed") unless class_names.any? { |c| object.is_a?(c.to_class) }
193
+ instance_eval("@#{attribute} ||= AIXM::Association::Array.new")
191
194
  send(attribute).send(:push, object)
192
195
  object.instance_variable_set(:"@#{inversion}", self)
193
196
  self
@@ -218,14 +221,19 @@ module AIXM
218
221
  (@has_one_attributes ||= []) << attribute
219
222
  # feature
220
223
  attr_reader attribute
221
- # feature= / add_feature
224
+ # feature=
222
225
  define_method(:"#{association}=") do |object|
223
226
  fail(ArgumentError, "#{object.__class__} not allowed") unless class_names.any? { |c| object.is_a?(c.to_class) }
224
227
  instance_variable_get(:"@#{attribute}")&.instance_variable_set(:"@#{inversion}", nil)
225
228
  instance_variable_set(:"@#{attribute}", object)
226
229
  object&.instance_variable_set(:"@#{inversion}", self)
230
+ object
231
+ end
232
+ # add_feature
233
+ define_method(:"add_#{association}") do |object|
234
+ send("#{association}=", object)
235
+ self
227
236
  end
228
- alias_method(:"add_#{association}", :"#{association}=")
229
237
  # remove_feature
230
238
  define_method(:"remove_#{association}") do |_|
231
239
  send(:"#{association}=", nil)
@@ -239,11 +247,17 @@ module AIXM
239
247
  (@belongs_to_attributes ||= []) << attribute
240
248
  # feature
241
249
  attr_reader attribute
242
- # feature=
243
250
  unless readonly
251
+ # feature=
244
252
  define_method(:"#{attribute}=") do |object|
245
253
  instance_variable_get(:"@#{attribute}")&.send(:"remove_#{inversion}", self)
246
254
  object&.send(:"add_#{inversion}", self)
255
+ object
256
+ end
257
+ # add_feature
258
+ define_method(:"add_#{attribute}") do |object|
259
+ send("#{attribute}=", object)
260
+ self
247
261
  end
248
262
  end
249
263
  end
@@ -349,17 +363,14 @@ module AIXM
349
363
  # belongs_to :blog
350
364
  # end
351
365
  # blog, post = Blog.new, Post.new
352
- # blog.add_posts([post, post])
353
- # blog.posts.duplicates # => [post]
366
+ # duplicate_post = post.dup
367
+ # blog.add_posts([post, duplicate_post])
368
+ # blog.posts.duplicates # => [[post, duplicate_post]]
354
369
  #
355
- # @return [AIXM::Association::Array]
370
+ # @return [Array<Array<AIXM::Feature>>]
356
371
  def duplicates
357
372
  AIXM::Memoize.method :to_uid do
358
- self.class.new(
359
- select.with_index do |element, index|
360
- index != self.index(element)
361
- end
362
- )
373
+ group_by(&:to_uid).select { |_, a| a.count > 1 }.map(&:last)
363
374
  end
364
375
  end
365
376
  end