aixm 0.3.11 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +64 -12
  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 +12 -4
  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 +18 -6
  15. data/lib/aixm/component/geometry/point.rb +12 -4
  16. data/lib/aixm/component/geometry/rhumb_line.rb +61 -0
  17. data/lib/aixm/component/geometry.rb +30 -17
  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 +36 -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 +4 -2
  35. data/lib/aixm/d.rb +32 -25
  36. data/lib/aixm/document.rb +36 -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 +33 -6
  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 +138 -46
  48. data/lib/aixm/feature/obstacle_group.rb +15 -18
  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 +255 -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 +45 -26
  64. data/lib/aixm/z.rb +28 -15
  65. data/lib/aixm.rb +25 -6
  66. data/schemas/ofmx/{0 → 0.1}/OFMX-CSV-Obstacle.json +0 -0
  67. data/schemas/ofmx/{0 → 0.1}/OFMX-CSV.json +0 -0
  68. data/schemas/ofmx/{0 → 0.1}/OFMX-DataTypes.xsd +58 -2
  69. data/schemas/ofmx/{0 → 0.1}/OFMX-Features.xsd +119 -40
  70. data/schemas/ofmx/{0 → 0.1}/OFMX-Snapshot.xsd +5 -0
  71. data.tar.gz.sig +0 -0
  72. metadata +33 -19
  73. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d86771a98ab156be9da1487e7e2e11615bcd63590824108717a51f07c044e241
4
- data.tar.gz: b6fc6d7c2803169145153d434f1675f161be6ae731b54eb1d53b6d2fbcf4807b
3
+ metadata.gz: a1dc4f9cb88ab4e481f835ca4d309142c6a17910b396f4293a6d9e7d3f946ec6
4
+ data.tar.gz: 244adc808983bc3e810fb5847051a89dbd0893df3d2c709943d2c7231acfb434
5
5
  SHA512:
6
- metadata.gz: e537adc538ba520a2f0e891da01ccb13e768a5e183c8a042fbe3bf55f63bfc9f2a66125cf6a19b1882e7d646d8c9b0f848fc551f095a24323f24216d9a8036a1
7
- data.tar.gz: 5a3884e00a329f243835648bce2bc13f1cd0ae701113dc89a86f23e83ebe9a82abf4af7a5c6aa87a8f22b1d7c49bc94fd222b2752a5a9bef42276936def12e60
6
+ metadata.gz: ac00f735cca36dc6b1b0ec00c671dcbcbdb1e14c0d051fc8d865737e9ac1363d5648f5df69098b0daf721688c541a79188cac3603a07e01f8719fde3c8070d91
7
+ data.tar.gz: 59d6ffb78fcb04104838bc4fb0ba50e7f3c5d526e921410e4300d08dafa08a2aa9cd1d637d374d7e5428d05c56e5f66186377a2f32fb6bff6c14ce5539044884
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,3 +1,55 @@
1
+ ## Main
2
+
3
+ Nothing so far
4
+
5
+ ## 1.2.0
6
+
7
+ #### Additions
8
+ * `Timesheet` to add custom schedules to `Timetable`
9
+ * `AIXM::Schedule::(Date|Day|Time)` for custom timetables
10
+ * Interface to allow most class instances as Hash keys
11
+
12
+ #### Fixes
13
+ * Fix typo in `GUESSED_UNIT_TYPES_MAP`
14
+
15
+ ## 1.1.0
16
+
17
+ #### Breaking Changes
18
+ * `AIXM::Association:Array#duplicates` now returns an array of arrays which
19
+ group all duplicates together.
20
+ * `VOR#associate_dme` and `VOR#associate_tacan` no longer take the channel
21
+ as argument but calculate it from the (ghost) frequency of the VOR.
22
+ * Replaced `#length`/`#width` with `#dimensions` on `Runway`, `Helipad` and `FATO`
23
+ * Renamed `AIXM::D#dist` to `AIXM::D#dim`
24
+ * Renamed `TLOF#helicopter_class` to `TLOF#performance_class`
25
+ * Renamed `#geographic_orientation` and `#magnetic_orientation` to more familiar
26
+ `#geographic_bearing` and `#magnetic_bearing` on `Runway` and `FATO`
27
+ * Re-implementation of `AIXM::A` without precision
28
+ * Demoted `Address` to component
29
+ * Fixed typo in `Service` type `:vdf_direction_finding_service`
30
+
31
+ #### Additions
32
+ * Associations from `Service` to `Airport` and `Airspace`
33
+ * `AIXM::R` (rectangle)
34
+ * `Runway#marking`
35
+ * `ApproachLighting` on `Runway::Direction` and `FATO::Direction`
36
+ * `VASIS` on `Runway::Direction` and `FATO::Direction`
37
+ * `#meta` on every feature and component
38
+ * `Document#regions` which is added to the root element for OFMX
39
+
40
+ #### Changes
41
+ * Nested memoization of the same method is now allowed and won't reset the
42
+ memoization cache anymore.
43
+ * Remove unit "mhz" from `Address` of type `:radio_frequency`.
44
+
45
+ ## 1.0.0
46
+
47
+ #### Breaking Changes
48
+ * Move `Ase->txtLocalType` up into `AseUid` for OFMX
49
+
50
+ #### Additions
51
+ * Add rhumb line geometry
52
+
1
53
  ## 0.3.11
2
54
 
3
55
  #### Breaking Changes
@@ -11,21 +63,21 @@
11
63
  #### Additions
12
64
  * Add `f#voice?` and `AIXM.config.voice_channel_separation` to check whether a
13
65
  frequency belongs to the voice communication airband and use it to validate
14
- `AIXM::Frequency`
66
+ `Frequency`
15
67
 
16
68
  ## 0.3.10
17
69
 
18
70
  #### Additions
19
71
  * Proper `has_many` and `has_one` associations
20
- * `AIXM::Association:Array#find_by|find|duplicate` on `has_many` associations
72
+ * `AIXM::Association:Array#find_by|find|duplicates` on `has_many` associations
21
73
  * `AIXM.config.mid` now defines whether `mid` attributes are inserted or not
22
74
  provided the selected schema is OFMX
23
75
  * `AIXM::Memoize` module
24
76
  * `AIXM::PayloadHash` class
25
77
  * `mkmid` executable to insert `mid` attributes into valid OFMX file
26
78
  * `ckmid` executable to check `mid` attributes in an OFMX file
27
- * `AIXM::Component::Geometry#point?|circle?|polygon?`
28
- * `AIXM::Component::Layer#services`
79
+ * Geometries respond to `#point?`, `#circle?` and `#polygon?`
80
+ * `Layer#services`
29
81
 
30
82
  #### Breaking Changes
31
83
  * Require Ruby 2.7
@@ -55,20 +107,20 @@
55
107
  ## 0.3.7
56
108
 
57
109
  #### Additions
58
- * `AIXM::Document#select_features`
59
- * `AIXM::Document#group_obstacles!`
110
+ * `Document#select_features`
111
+ * `Document#group_obstacles!`
60
112
 
61
113
  ## 0.3.6
62
114
 
63
115
  #### Additions
64
- * `AIXM::Component::FATO`
65
- * `AIXM::Component::Helipad#helicopter_class` and `AIXM::Component::Helipad#marking`
116
+ * `FATO`
117
+ * `Helipad#helicopter_class` and `Helipad#marking`
66
118
  * `AIXM::XY#seconds?` to detect possibly rounded or estimated coordinates
67
- * `AIXM::Features::Airport#operator`
119
+ * `Airport#operator`
68
120
  * `AIXM::W` (weight)
69
121
  * `AIXM::P` (pressure)
70
- * `AIXM::Component::Lighting` for use with runways, helipads and FATOs
71
- * Surface details `siwl_weight`, `siwl_tire_pressure` and `auw_weight`
122
+ * `Lighting` for use with runways, helipads and FATOs
123
+ * Surface details `#siwl_weight`, `#siwl_tire_pressure` and `#auw_weight`
72
124
 
73
125
  #### Changes
74
126
  * Generate `Airport#id` from region and `Airport#name`
@@ -234,7 +286,7 @@
234
286
 
235
287
  #### Changes
236
288
  * `Document#created_at` and `#effective_at` accept Time, Date, String or *nil*
237
- * Separate `AIXM::Document#valid?` from `#complete?`
289
+ * Separate `Document#valid?` from `#complete?`
238
290
  * Write coordinates in DD if extension `:OFM` is set
239
291
  * `Array#to_digest` returns Integer which fits in signed 32bit
240
292
 
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