rcap 1.3.1 → 2.0.0

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 (88) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/README.md +78 -151
  3. data/Rakefile +1 -1
  4. data/lib/rcap/alert.rb +2 -2
  5. data/lib/rcap/base/alert.rb +446 -0
  6. data/lib/rcap/base/area.rb +228 -0
  7. data/lib/rcap/base/circle.rb +121 -0
  8. data/lib/rcap/base/event_code.rb +6 -0
  9. data/lib/rcap/base/geocode.rb +6 -0
  10. data/lib/rcap/base/info.rb +498 -0
  11. data/lib/rcap/base/parameter.rb +88 -0
  12. data/lib/rcap/base/point.rb +87 -0
  13. data/lib/rcap/base/polygon.rb +120 -0
  14. data/lib/rcap/base/resource.rb +168 -0
  15. data/lib/rcap/cap_1_0/alert.rb +59 -342
  16. data/lib/rcap/cap_1_0/area.rb +13 -188
  17. data/lib/rcap/cap_1_0/circle.rb +2 -100
  18. data/lib/rcap/cap_1_0/event_code.rb +8 -3
  19. data/lib/rcap/cap_1_0/geocode.rb +8 -3
  20. data/lib/rcap/cap_1_0/info.rb +16 -468
  21. data/lib/rcap/cap_1_0/parameter.rb +9 -67
  22. data/lib/rcap/cap_1_0/point.rb +2 -61
  23. data/lib/rcap/cap_1_0/polygon.rb +5 -95
  24. data/lib/rcap/cap_1_0/resource.rb +4 -144
  25. data/lib/rcap/cap_1_1/alert.rb +7 -412
  26. data/lib/rcap/cap_1_1/area.rb +13 -188
  27. data/lib/rcap/cap_1_1/circle.rb +2 -100
  28. data/lib/rcap/cap_1_1/event_code.rb +8 -3
  29. data/lib/rcap/cap_1_1/geocode.rb +7 -3
  30. data/lib/rcap/cap_1_1/info.rb +127 -386
  31. data/lib/rcap/cap_1_1/parameter.rb +4 -76
  32. data/lib/rcap/cap_1_1/point.rb +2 -61
  33. data/lib/rcap/cap_1_1/polygon.rb +5 -95
  34. data/lib/rcap/cap_1_1/resource.rb +37 -143
  35. data/lib/rcap/cap_1_2/alert.rb +8 -413
  36. data/lib/rcap/cap_1_2/area.rb +13 -188
  37. data/lib/rcap/cap_1_2/circle.rb +2 -100
  38. data/lib/rcap/cap_1_2/event_code.rb +8 -3
  39. data/lib/rcap/cap_1_2/geocode.rb +8 -3
  40. data/lib/rcap/cap_1_2/info.rb +132 -419
  41. data/lib/rcap/cap_1_2/parameter.rb +4 -76
  42. data/lib/rcap/cap_1_2/point.rb +2 -61
  43. data/lib/rcap/cap_1_2/polygon.rb +5 -92
  44. data/lib/rcap/cap_1_2/resource.rb +31 -134
  45. data/lib/rcap/config.rb +3 -0
  46. data/lib/{extensions → rcap/extensions}/array.rb +1 -1
  47. data/lib/rcap/extensions/date.rb +11 -0
  48. data/lib/{extensions → rcap/extensions}/date_time.rb +2 -5
  49. data/lib/{extensions → rcap/extensions}/string.rb +1 -1
  50. data/lib/{extensions → rcap/extensions}/time.rb +2 -4
  51. data/lib/rcap/utilities.rb +11 -11
  52. data/lib/rcap/validations.rb +7 -2
  53. data/lib/rcap/version.rb +1 -1
  54. data/lib/rcap.rb +21 -4
  55. data/spec/alert_spec.rb +69 -37
  56. data/spec/cap_1_0/alert_spec.rb +46 -61
  57. data/spec/cap_1_0/area_spec.rb +77 -37
  58. data/spec/cap_1_0/circle_spec.rb +26 -6
  59. data/spec/cap_1_0/event_code_spec.rb +10 -3
  60. data/spec/cap_1_0/geocode_spec.rb +11 -4
  61. data/spec/cap_1_0/info_spec.rb +74 -77
  62. data/spec/cap_1_0/parameter_spec.rb +18 -5
  63. data/spec/cap_1_0/point_spec.rb +9 -2
  64. data/spec/cap_1_0/polygon_spec.rb +52 -9
  65. data/spec/cap_1_0/resource_spec.rb +28 -21
  66. data/spec/cap_1_1/alert_spec.rb +47 -60
  67. data/spec/cap_1_1/area_spec.rb +66 -43
  68. data/spec/cap_1_1/circle_spec.rb +29 -6
  69. data/spec/cap_1_1/event_code_spec.rb +11 -3
  70. data/spec/cap_1_1/geocode_spec.rb +11 -3
  71. data/spec/cap_1_1/info_spec.rb +262 -118
  72. data/spec/cap_1_1/parameter_spec.rb +12 -3
  73. data/spec/cap_1_1/point_spec.rb +8 -2
  74. data/spec/cap_1_1/polygon_spec.rb +57 -18
  75. data/spec/cap_1_1/resource_spec.rb +70 -20
  76. data/spec/cap_1_2/alert_spec.rb +97 -110
  77. data/spec/cap_1_2/area_spec.rb +59 -41
  78. data/spec/cap_1_2/circle_spec.rb +15 -8
  79. data/spec/cap_1_2/event_code_spec.rb +11 -3
  80. data/spec/cap_1_2/geocode_spec.rb +11 -3
  81. data/spec/cap_1_2/info_spec.rb +266 -119
  82. data/spec/cap_1_2/parameter_spec.rb +11 -3
  83. data/spec/cap_1_2/point_spec.rb +10 -3
  84. data/spec/cap_1_2/polygon_spec.rb +25 -10
  85. data/spec/cap_1_2/resource_spec.rb +33 -28
  86. data/spec/{utilities_spec.rb → extensions_spec.rb} +0 -0
  87. data/spec/validations_spec.rb +18 -2
  88. metadata +20 -46
@@ -0,0 +1,446 @@
1
+ module RCAP
2
+ module Base
3
+ class Alert
4
+ include Validation
5
+
6
+ STATUS_ACTUAL = "Actual"
7
+ STATUS_EXERCISE = "Exercise"
8
+ STATUS_SYSTEM = "System"
9
+ STATUS_TEST = "Test"
10
+ # Valid values for status
11
+ VALID_STATUSES = [ STATUS_ACTUAL, STATUS_EXERCISE, STATUS_SYSTEM, STATUS_TEST ]
12
+
13
+ MSG_TYPE_ALERT = "Alert"
14
+ MSG_TYPE_UPDATE = "Update"
15
+ MSG_TYPE_CANCEL = "Cancel"
16
+ MSG_TYPE_ACK = "Ack"
17
+ MSG_TYPE_ERROR = "Error"
18
+ # Valid values for msg_type
19
+ VALID_MSG_TYPES = [ MSG_TYPE_ALERT, MSG_TYPE_UPDATE, MSG_TYPE_CANCEL, MSG_TYPE_ACK, MSG_TYPE_ERROR ]
20
+
21
+ SCOPE_PUBLIC = "Public"
22
+ SCOPE_RESTRICTED = "Restricted"
23
+ SCOPE_PRIVATE = "Private"
24
+ # Valid values for scope
25
+ VALID_SCOPES = [ SCOPE_PUBLIC, SCOPE_PRIVATE, SCOPE_RESTRICTED ]
26
+
27
+ # @return [String] If not set a UUID will be set by default on object initialisation
28
+ attr_accessor( :identifier)
29
+ # @return [String]
30
+ attr_accessor( :sender )
31
+ # @return [DateTime] If not set will be time of creation.
32
+ attr_accessor( :sent )
33
+ # @return [String] Can only be one of {VALID_STATUSES}
34
+ attr_accessor( :status )
35
+ # @return [String] Can only be one of {VALID_MSG_TYPES}
36
+ attr_accessor( :msg_type )
37
+ # @return [String] Can only be one of {VALID_SCOPES}
38
+ attr_accessor( :scope )
39
+ # @return [String]
40
+ attr_accessor( :source )
41
+ # @return [String ] Required if scope is {SCOPE_RESTRICTED}
42
+ attr_accessor( :restriction )
43
+ # @return [String]
44
+ attr_accessor( :note )
45
+
46
+ # @return [Array<String>] Collection of address strings. Depends on scope being {SCOPE_PRIVATE}
47
+ attr_reader( :addresses )
48
+ # @return [Array<String>]
49
+ attr_reader( :codes )
50
+ # @return [Array<String>] Collection of references to previous alerts
51
+ # @see #to_reference
52
+ attr_reader( :references)
53
+ # @return [Array<String>] Collection of incident strings
54
+ attr_reader( :incidents )
55
+ # @return [Array<Info>]
56
+ attr_reader( :infos )
57
+
58
+ validates_presence_of( :identifier, :sender, :sent, :status, :msg_type, :scope )
59
+
60
+ validates_inclusion_of( :status, :in => VALID_STATUSES )
61
+ validates_inclusion_of( :msg_type, :in => VALID_MSG_TYPES )
62
+ validates_inclusion_of( :scope, :in => VALID_SCOPES )
63
+
64
+ validates_format_of( :identifier, :with => ALLOWED_CHARACTERS )
65
+ validates_format_of( :sender , :with => ALLOWED_CHARACTERS )
66
+
67
+ validates_conditional_presence_of( :addresses, :when => :scope, :is => SCOPE_PRIVATE )
68
+ validates_conditional_presence_of( :restriction, :when => :scope, :is => SCOPE_RESTRICTED )
69
+
70
+ validates_collection_of( :infos )
71
+
72
+ # Initialises a new Alert object. Yields the initialised alert to a block.
73
+ #
74
+ # @example
75
+ # alert = Alert.new do |alert|
76
+ # alert.sender = alerts@example.org
77
+ # alert.status = Alert::STATUS_ACTUAL
78
+ # alert.msg_type = Alert::MSG_TYPE_ALERT
79
+ # alert.scope = Alert::SCOPE_PUBLIC
80
+ # end
81
+ #
82
+ # @yieldparam alert [Alert] The newly initialised Alert.
83
+ def initialize
84
+ @identifier = RCAP.generate_identifier
85
+ @addresses = []
86
+ @codes = []
87
+ @references = []
88
+ @incidents = []
89
+ @infos = []
90
+ yield( self ) if block_given?
91
+ end
92
+
93
+ # Creates a new {Info} object and adds it to the {#infos} array.
94
+ #
95
+ # @see Info#initialize
96
+ # @yield [Info] The newly initialised Info object.
97
+ # @return [Info] The initialised Info object after being yielded to the block
98
+ def add_info
99
+ self.info_class.new.tap do |info|
100
+ yield( info ) if block_given?
101
+ @infos << info
102
+ end
103
+ end
104
+
105
+ XML_ELEMENT_NAME = 'alert'
106
+ IDENTIFIER_ELEMENT_NAME = 'identifier'
107
+ SENDER_ELEMENT_NAME = 'sender'
108
+ SENT_ELEMENT_NAME = 'sent'
109
+ STATUS_ELEMENT_NAME = 'status'
110
+ MSG_TYPE_ELEMENT_NAME = 'msgType'
111
+ SOURCE_ELEMENT_NAME = 'source'
112
+ SCOPE_ELEMENT_NAME = 'scope'
113
+ RESTRICTION_ELEMENT_NAME = 'restriction'
114
+ ADDRESSES_ELEMENT_NAME = 'addresses'
115
+ CODE_ELEMENT_NAME = 'code'
116
+ NOTE_ELEMENT_NAME = 'note'
117
+ REFERENCES_ELEMENT_NAME = 'references'
118
+ INCIDENTS_ELEMENT_NAME = 'incidents'
119
+
120
+ # @return [REXML::Element]
121
+ def to_xml_element
122
+ xml_element = REXML::Element.new( XML_ELEMENT_NAME )
123
+ xml_element.add_namespace( self.class::XMLNS )
124
+ xml_element.add_element( IDENTIFIER_ELEMENT_NAME ).add_text( @identifier ) if @identifier
125
+ xml_element.add_element( SENDER_ELEMENT_NAME ).add_text( @sender ) if @sender
126
+ xml_element.add_element( SENT_ELEMENT_NAME ).add_text( @sent.to_s_for_cap ) if @sent
127
+ xml_element.add_element( STATUS_ELEMENT_NAME ).add_text( @status ) if @status
128
+ xml_element.add_element( MSG_TYPE_ELEMENT_NAME ).add_text( @msg_type ) if @msg_type
129
+ xml_element.add_element( SOURCE_ELEMENT_NAME ).add_text( @source ) if @source
130
+ xml_element.add_element( SCOPE_ELEMENT_NAME ).add_text( @scope ) if @scope
131
+ xml_element.add_element( RESTRICTION_ELEMENT_NAME ).add_text( @restriction ) if @restriction
132
+ if @addresses.any?
133
+ xml_element.add_element( ADDRESSES_ELEMENT_NAME ).add_text( @addresses.to_s_for_cap )
134
+ end
135
+ @codes.each do |code|
136
+ xml_element.add_element( CODE_ELEMENT_NAME ).add_text( code )
137
+ end
138
+ xml_element.add_element( NOTE_ELEMENT_NAME ).add_text( @note ) if @note
139
+ if @references.any?
140
+ xml_element.add_element( REFERENCES_ELEMENT_NAME ).add_text( @references.join( ' ' ))
141
+ end
142
+ if @incidents.any?
143
+ xml_element.add_element( INCIDENTS_ELEMENT_NAME ).add_text( @incidents.join( ' ' ))
144
+ end
145
+ @infos.each do |info|
146
+ xml_element.add_element( info.to_xml_element )
147
+ end
148
+ xml_element
149
+ end
150
+
151
+ # @return [REXML::Document]
152
+ def to_xml_document
153
+ xml_document = REXML::Document.new
154
+ xml_document.add( REXML::XMLDecl.new )
155
+ xml_document.add( self.to_xml_element )
156
+ xml_document
157
+ end
158
+
159
+ # Returns a string containing the XML representation of the alert.
160
+ #
161
+ # @param [true,false] pretty_print Pretty print output
162
+ # @return [String]
163
+ def to_xml( pretty_print = false )
164
+ if pretty_print
165
+ xml_document = ""
166
+ RCAP::XML_PRETTY_PRINTER.write( self.to_xml_document, xml_document )
167
+ xml_document
168
+ else
169
+ self.to_xml_document.to_s
170
+ end
171
+ end
172
+
173
+ # Returns a string representation of the alert suitable for usage as a reference in a CAP message of the form
174
+ # sender,identifier,sent
175
+ #
176
+ # @return [String]
177
+ def to_reference
178
+ "#{ @sender },#{ @identifier },#{ @sent }"
179
+ end
180
+
181
+ # @return [String]
182
+ def inspect
183
+ alert_inspect = [ "CAP Version: #{ self.class::CAP_VERSION }",
184
+ "Identifier: #{ @identifier }",
185
+ "Sender: #{ @sender }",
186
+ "Sent: #{ @sent }",
187
+ "Status: #{ @status }",
188
+ "Message Type: #{ @msg_type }",
189
+ "Source: #{ @source }",
190
+ "Scope: #{ @scope }",
191
+ "Restriction: #{ @restriction }",
192
+ "Addresses: #{ @addresses.to_s_for_cap }",
193
+ "Codes:",
194
+ @codes.map{ |code| " " + code }.join("\n")+"",
195
+ "Note: #{ @note }",
196
+ "References: #{ @references.join( ' ' )}",
197
+ "Incidents: #{ @incidents.join( ' ')}",
198
+ "Information:",
199
+ @infos.map{ |info| " " + info.to_s }.join( "\n" )].join( "\n" )
200
+ RCAP.format_lines_for_inspect( 'ALERT', alert_inspect )
201
+ end
202
+
203
+ # Returns a string representation of the alert of the form
204
+ # sender/identifier/sent
205
+ # See {#to_reference} for another string representation suitable as a CAP reference.
206
+ #
207
+ # @return [String]
208
+ def to_s
209
+ "#{ @sender }/#{ @identifier }/#{ @sent }"
210
+ end
211
+
212
+ XPATH = 'cap:alert'
213
+ IDENTIFIER_XPATH = "cap:#{ IDENTIFIER_ELEMENT_NAME }"
214
+ SENDER_XPATH = "cap:#{ SENDER_ELEMENT_NAME }"
215
+ SENT_XPATH = "cap:#{ SENT_ELEMENT_NAME }"
216
+ STATUS_XPATH = "cap:#{ STATUS_ELEMENT_NAME }"
217
+ MSG_TYPE_XPATH = "cap:#{ MSG_TYPE_ELEMENT_NAME }"
218
+ SOURCE_XPATH = "cap:#{ SOURCE_ELEMENT_NAME }"
219
+ SCOPE_XPATH = "cap:#{ SCOPE_ELEMENT_NAME }"
220
+ RESTRICTION_XPATH = "cap:#{ RESTRICTION_ELEMENT_NAME }"
221
+ ADDRESSES_XPATH = "cap:#{ ADDRESSES_ELEMENT_NAME }"
222
+ CODE_XPATH = "cap:#{ CODE_ELEMENT_NAME }"
223
+ NOTE_XPATH = "cap:#{ NOTE_ELEMENT_NAME }"
224
+ REFERENCES_XPATH = "cap:#{ REFERENCES_ELEMENT_NAME }"
225
+ INCIDENTS_XPATH = "cap:#{ INCIDENTS_ELEMENT_NAME }"
226
+
227
+ # @param [REXML::Element] alert_xml_element
228
+ # @return [RCAP::CAP_1_0::Alert]
229
+ def self.from_xml_element( alert_xml_element )
230
+ self.new do |alert|
231
+ alert.identifier = RCAP.xpath_text( alert_xml_element, IDENTIFIER_XPATH, alert.xmlns )
232
+ alert.sender = RCAP.xpath_text( alert_xml_element, SENDER_XPATH, alert.xmlns )
233
+ alert.sent = (( sent = RCAP.xpath_first( alert_xml_element, SENT_XPATH, alert.xmlns )) ? DateTime.parse( sent.text ) : nil )
234
+ alert.status = RCAP.xpath_text( alert_xml_element, STATUS_XPATH, alert.xmlns )
235
+ alert.msg_type = RCAP.xpath_text( alert_xml_element, MSG_TYPE_XPATH, alert.xmlns )
236
+ alert.source = RCAP.xpath_text( alert_xml_element, SOURCE_XPATH, alert.xmlns )
237
+ alert.scope = RCAP.xpath_text( alert_xml_element, SCOPE_XPATH, alert.xmlns )
238
+ alert.restriction = RCAP.xpath_text( alert_xml_element, RESTRICTION_XPATH, alert.xmlns )
239
+
240
+ (( address = RCAP.xpath_text( alert_xml_element, ADDRESSES_XPATH, alert.xmlns )) ? address.unpack_cap_list : [] ).each do |address|
241
+ alert.addresses << address
242
+ end
243
+ RCAP.xpath_match( alert_xml_element, CODE_XPATH, alert.xmlns ).each do |element|
244
+ alert.codes << element.text
245
+ end
246
+ alert.note = RCAP.xpath_text( alert_xml_element, NOTE_XPATH, alert.xmlns )
247
+ (( references = RCAP.xpath_text( alert_xml_element, REFERENCES_XPATH, alert.xmlns )) ? references.split( ' ' ) : []).each do |reference|
248
+ alert.references << reference
249
+ end
250
+
251
+ (( incidents = RCAP.xpath_text( alert_xml_element, INCIDENTS_XPATH, alert.xmlns )) ? incidents.split( ' ' ) : []).each do |incident|
252
+ alert.incidents << incident
253
+ end
254
+
255
+ RCAP.xpath_match( alert_xml_element, Info::XPATH, alert.xmlns ).each do |element|
256
+ alert.infos << alert.info_class.from_xml_element( element )
257
+ end
258
+ end
259
+ end
260
+
261
+ # @param [REXML::Document] xml_document
262
+ # @return [Alert]
263
+ def self.from_xml_document( xml_document )
264
+ self.from_xml_element( xml_document.root )
265
+ end
266
+
267
+ # Initialise an Alert object from an XML string. Any object that is a subclass of IO (e.g. File) can be passed in.
268
+ #
269
+ # @param [String] xml
270
+ # @return [Alert]
271
+ def self.from_xml( xml )
272
+ self.from_xml_document( REXML::Document.new( xml ))
273
+ end
274
+
275
+ CAP_VERSION_YAML = "CAP Version"
276
+ IDENTIFIER_YAML = "Identifier"
277
+ SENDER_YAML = "Sender"
278
+ SENT_YAML = "Sent"
279
+ STATUS_YAML = "Status"
280
+ MSG_TYPE_YAML = "Message Type"
281
+ SOURCE_YAML = "Source"
282
+ SCOPE_YAML = "Scope"
283
+ RESTRICTION_YAML = "Restriction"
284
+ ADDRESSES_YAML = "Addresses"
285
+ CODES_YAML = "Codes"
286
+ NOTE_YAML = "Note"
287
+ REFERENCES_YAML = "References"
288
+ INCIDENTS_YAML = "Incidents"
289
+ INFOS_YAML = "Information"
290
+
291
+ # Returns a string containing the YAML representation of the alert.
292
+ #
293
+ # @return [String]
294
+ def to_yaml( options = {} )
295
+ RCAP.attribute_values_to_hash( [ CAP_VERSION_YAML, self.class::CAP_VERSION ],
296
+ [ IDENTIFIER_YAML, @identifier ],
297
+ [ SENDER_YAML, @sender ],
298
+ [ SENT_YAML, @sent ],
299
+ [ STATUS_YAML, @status ],
300
+ [ MSG_TYPE_YAML, @msg_type ],
301
+ [ SOURCE_YAML, @source ],
302
+ [ SCOPE_YAML, @scope ],
303
+ [ RESTRICTION_YAML, @restriction ],
304
+ [ ADDRESSES_YAML, @addresses ],
305
+ [ CODES_YAML, @codes ],
306
+ [ NOTE_YAML, @note ],
307
+ [ REFERENCES_YAML, @references ],
308
+ [ INCIDENTS_YAML, @incidents ],
309
+ [ INFOS_YAML, @infos ]).to_yaml( options )
310
+ end
311
+
312
+ # Initialise an Alert object from a YAML string. Any object that is a subclass of IO (e.g. File) can be passed in.
313
+ #
314
+ # @param [String] yaml
315
+ # @return [Alert]
316
+ def self.from_yaml( yaml )
317
+ self.from_yaml_data( YAML.load( yaml ))
318
+ end
319
+
320
+ # Initialise an Alert object from a hash reutrned from YAML.load.
321
+ #
322
+ # @param [hash] alert_yaml_data
323
+ # @return [Alert]
324
+ def self.from_yaml_data( alert_yaml_data )
325
+ self.new do |alert|
326
+ alert.identifier = alert_yaml_data[ IDENTIFIER_YAML ]
327
+ alert.sender = alert_yaml_data[ SENDER_YAML ]
328
+ alert.sent = ( sent = alert_yaml_data[ SENT_YAML ]).blank? ? nil : DateTime.parse( sent.to_s )
329
+ alert.status = alert_yaml_data[ STATUS_YAML ]
330
+ alert.msg_type = alert_yaml_data[ MSG_TYPE_YAML ]
331
+ alert.source = alert_yaml_data[ SOURCE_YAML ]
332
+ alert.scope = alert_yaml_data[ SCOPE_YAML ]
333
+ alert.restriction = alert_yaml_data[ RESTRICTION_YAML ]
334
+ Array( alert_yaml_data[ ADDRESSES_YAML ]).each do |address|
335
+ alert.addresses << address
336
+ end
337
+ Array( alert_yaml_data[ CODES_YAML ]).each do |code|
338
+ alert.codes << code
339
+ end
340
+ alert.note = alert_yaml_data[ NOTE_YAML ]
341
+ Array( alert_yaml_data[ REFERENCES_YAML ]).each do |reference|
342
+ alert.references << reference
343
+ end
344
+ Array( alert_yaml_data[ INCIDENTS_YAML ]).each do |incident|
345
+ alert.incidents << incident
346
+ end
347
+ Array( alert_yaml_data[ INFOS_YAML ]).each do |info_yaml_data|
348
+ alert.infos << alert.info_class.from_yaml_data( info_yaml_data )
349
+ end
350
+ end
351
+ end
352
+
353
+ CAP_VERSION_KEY = 'cap_version'
354
+ IDENTIFIER_KEY = 'identifier'
355
+ SENDER_KEY = 'sender'
356
+ SENT_KEY = 'sent'
357
+ STATUS_KEY = 'status'
358
+ MSG_TYPE_KEY = 'msg_type'
359
+ SOURCE_KEY = 'source'
360
+ SCOPE_KEY = 'scope'
361
+ RESTRICTION_KEY = 'restriction'
362
+ ADDRESSES_KEY = 'addresses'
363
+ CODES_KEY = 'codes'
364
+ NOTE_KEY = 'note'
365
+ REFERENCES_KEY = 'references'
366
+ INCIDENTS_KEY = 'incidents'
367
+ INFOS_KEY = 'infos'
368
+
369
+ # Returns a Hash representation of an Alert object
370
+ #
371
+ # @return [Hash]
372
+ def to_h
373
+ RCAP.attribute_values_to_hash( [ CAP_VERSION_KEY, self.class::CAP_VERSION ],
374
+ [ IDENTIFIER_KEY, @identifier ],
375
+ [ SENDER_KEY, @sender ],
376
+ [ SENT_KEY, RCAP.to_s_for_cap( @sent )],
377
+ [ STATUS_KEY, @status ],
378
+ [ MSG_TYPE_KEY, @msg_type ],
379
+ [ SOURCE_KEY, @source ],
380
+ [ SCOPE_KEY, @scope ],
381
+ [ RESTRICTION_KEY, @restriction ],
382
+ [ ADDRESSES_KEY, @addresses ],
383
+ [ CODES_KEY, @codes ],
384
+ [ NOTE_KEY, @note ],
385
+ [ REFERENCES_KEY, @references ],
386
+ [ INCIDENTS_KEY, @incidents ],
387
+ [ INFOS_KEY, @infos.map{ |info| info.to_h }])
388
+ end
389
+
390
+ # Initialises an Alert object from a Hash produced by Alert#to_h
391
+ #
392
+ # @param [Hash] alert_hash
393
+ # @return [RCAP::CAP_1_0::Alert]
394
+ def self.from_h( alert_hash )
395
+ self.new do |alert|
396
+ alert.identifier = alert_hash[ IDENTIFIER_KEY ]
397
+ alert.sender = alert_hash[ SENDER_KEY ]
398
+ alert.sent = RCAP.parse_datetime( alert_hash[ SENT_KEY ])
399
+ alert.status = alert_hash[ STATUS_KEY ]
400
+ alert.msg_type = alert_hash[ MSG_TYPE_KEY ]
401
+ alert.source = alert_hash[ SOURCE_KEY ]
402
+ alert.scope = alert_hash[ SCOPE_KEY ]
403
+ alert.restriction = alert_hash[ RESTRICTION_KEY ]
404
+ Array( alert_hash[ ADDRESSES_KEY ]).each do |address|
405
+ alert.addresses << address
406
+ end
407
+ Array( alert_hash[ CODES_KEY ]).each do |code|
408
+ alert.codes << code
409
+ end
410
+ alert.note = alert_hash[ NOTE_KEY ]
411
+ Array( alert_hash[ REFERENCES_KEY ]).each do |reference|
412
+ alert.references << reference
413
+ end
414
+
415
+ Array( alert_hash[ INCIDENTS_KEY ]).each do |incident|
416
+ alert.incidents << incident
417
+ end
418
+
419
+ Array( alert_hash[ INFOS_KEY ]).each do |info_hash|
420
+ alert.infos << alert.info_class.from_h( info_hash )
421
+ end
422
+ end
423
+ end
424
+
425
+ # Returns a JSON string representation of an Alert object
426
+ #
427
+ # @param [true,false] pretty_print
428
+ # @return [String]
429
+ def to_json( pretty_print = false )
430
+ if pretty_print
431
+ JSON.pretty_generate( self.to_h )
432
+ else
433
+ self.to_h.to_json
434
+ end
435
+ end
436
+
437
+ # Initialises an Alert object from a JSON string produced by Alert#to_json
438
+ #
439
+ # @param [String] json_string
440
+ # @return [Alert]
441
+ def self.from_json( json_string )
442
+ self.from_h( JSON.parse( json_string ))
443
+ end
444
+ end
445
+ end
446
+ end
@@ -0,0 +1,228 @@
1
+ module RCAP
2
+ module Base
3
+ class Area
4
+ include Validation
5
+
6
+ # @return [String] Textual description of area.
7
+ attr_accessor( :area_desc )
8
+ # @return [Numeric] Expressed in feet above sea level
9
+ attr_accessor( :altitude )
10
+ # @return [Numeric] Expressed in feet above sea level.
11
+ attr_accessor( :ceiling )
12
+ # @return [Array<Circle>]
13
+ attr_reader( :circles )
14
+ # @return [Array<Geocode>]
15
+ attr_reader( :geocodes )
16
+ # @return [Array<Polygon>]
17
+ attr_reader( :polygons )
18
+
19
+ validates_presence_of( :area_desc )
20
+ validates_collection_of( :circles, :geocodes, :polygons, allow_empty: true )
21
+ validates_dependency_of( :ceiling, on: :altitude )
22
+
23
+ XML_ELEMENT_NAME = 'area'
24
+ AREA_DESC_ELEMENT_NAME = 'areaDesc'
25
+ ALTITUDE_ELEMENT_NAME = 'altitude'
26
+ CEILING_ELEMENT_NAME = 'ceiling'
27
+
28
+ XPATH = "cap:#{ XML_ELEMENT_NAME }"
29
+ AREA_DESC_XPATH = "cap:#{ AREA_DESC_ELEMENT_NAME }"
30
+ ALTITUDE_XPATH = "cap:#{ ALTITUDE_ELEMENT_NAME }"
31
+ CEILING_XPATH = "cap:#{ CEILING_ELEMENT_NAME }"
32
+
33
+ # @example
34
+ # Area.new do |area|
35
+ # area.area_desc = 'Cape Town CVD'
36
+ # end
37
+ def initialize
38
+ @area_desc = nil
39
+ @altitude = nil
40
+ @ceiling = nil
41
+
42
+ @circles = []
43
+ @geocodes = []
44
+ @polygons = []
45
+ yield( self ) if block_given?
46
+ end
47
+
48
+ # Creates a new {Polygon} object and adds it to the {#polygons} array.
49
+ #
50
+ # @return [Polygon]
51
+ def add_polygon
52
+ self.polygon_class.new.tap do |polygon|
53
+ yield( polygon ) if block_given?
54
+ @polygons << polygon
55
+ end
56
+ end
57
+
58
+ # Creates a new {Circle} object and adds it to the {#circles} array.
59
+ #
60
+ # @return [Circle]
61
+ def add_circle
62
+ self.circle_class.new.tap do |circle|
63
+ yield( circle ) if block_given?
64
+ @circles << circle
65
+ end
66
+ end
67
+
68
+ # Creates a new {Geocode} object and adds it to the {#geocodes} array.
69
+ #
70
+ # @return [Geocode]
71
+ def add_geocode
72
+ self.geocode_class.new do |geocode|
73
+ yield( geocode ) if block_given?
74
+ @geocodes << geocode
75
+ end
76
+ end
77
+
78
+ def self.from_xml_element( area_xml_element )
79
+ self.new do |area|
80
+ area.area_desc = RCAP.xpath_text( area_xml_element, AREA_DESC_XPATH, area.xmlns )
81
+ area.altitude = (( alt = RCAP.xpath_text( area_xml_element, ALTITUDE_XPATH, area.xmlns )) ? alt.to_f : nil )
82
+ area.ceiling = (( ceil = RCAP.xpath_text( area_xml_element, CEILING_XPATH, area.xmlns )) ? ceil.to_f : nil )
83
+
84
+ RCAP.xpath_match( area_xml_element, area.circle_class::XPATH, area.xmlns ).each do |circle_element|
85
+ area.circles << area.circle_class.from_xml_element( circle_element )
86
+ end
87
+
88
+ RCAP.xpath_match( area_xml_element, area.geocode_class::XPATH, area.xmlns ).each do |geocode_element|
89
+ area.geocodes << area.geocode_class.from_xml_element( geocode_element )
90
+ end
91
+
92
+ RCAP.xpath_match( area_xml_element, area.polygon_class::XPATH, area.xmlns ).each do |polygon_element|
93
+ area.polygons << area.polygon_class.from_xml_element( polygon_element )
94
+ end
95
+ end
96
+ end
97
+
98
+ # @return [REXML::Element]
99
+ def to_xml_element
100
+ xml_element = REXML::Element.new( XML_ELEMENT_NAME )
101
+ xml_element.add_element( AREA_DESC_ELEMENT_NAME ).add_text( @area_desc.to_s )
102
+ add_to_xml_element = lambda do |element, object|
103
+ element.add_element( object.to_xml_element )
104
+ element
105
+ end
106
+ @polygons.inject( xml_element, &add_to_xml_element )
107
+ @circles.inject( xml_element, &add_to_xml_element )
108
+ @geocodes.inject( xml_element, &add_to_xml_element )
109
+ xml_element.add_element( ALTITUDE_ELEMENT_NAME ).add_text( @altitude.to_s ) unless @altitude.blank?
110
+ xml_element.add_element( CEILING_ELEMENT_NAME ).add_text( @ceiling.to_s ) unless @altitude.blank?
111
+ xml_element
112
+ end
113
+
114
+ # @return [String] XML representation of the Area
115
+ def to_xml
116
+ self.to_xml_element.to_s
117
+ end
118
+
119
+ # Implements an equality operator for the Area object. Two Area objects are equal if all their attributes are equal.
120
+ #
121
+ # @param [Area] other Area object to compare
122
+ # @return [true,false]
123
+ def ==( other )
124
+ comparison_attributes = lambda{ |area| [ area.area_desc, area.altitude, area.ceiling, area.circles, area.geocodes, area.polygons ]}
125
+ comparison_attributes.call( self ) == comparison_attributes.call( other )
126
+ end
127
+
128
+ # @return [String]
129
+ def inspect
130
+ area_inspect = "Area Description: #{ @area_desc }\n"+
131
+ "Polygons:\n"+
132
+ @polygons.map{ |polygon| " " + polygon.inspect }.join("\n" )+"\n"+
133
+ "Circles: #{ @circles.inspect }\n"+
134
+ "Geocodes: #{ @geocodes.inspect }\n"
135
+ RCAP.format_lines_for_inspect( 'AREA', area_inspect )
136
+ end
137
+
138
+ # Returns the area description
139
+ #
140
+ # @return [String]
141
+ def to_s
142
+ @area_desc
143
+ end
144
+
145
+
146
+ AREA_DESC_YAML = 'Area Description'
147
+ ALTITUDE_YAML = 'Altitude'
148
+ CEILING_YAML = 'Ceiling'
149
+ CIRCLES_YAML = 'Circles'
150
+ GEOCODES_YAML = 'Geocodes'
151
+ POLYGONS_YAML = 'Polygons'
152
+
153
+ # @return [String] YAML representation of object
154
+ def to_yaml( options = {} )
155
+ RCAP.attribute_values_to_hash( [ AREA_DESC_YAML, @area_desc ],
156
+ [ ALTITUDE_YAML, @altitude ],
157
+ [ CEILING_YAML, @ceiling ],
158
+ [ CIRCLES_YAML, @circles.map{ |circle| circle.to_a }],
159
+ [ GEOCODES_YAML, @geocodes.inject({}){|h,geocode| h.merge( geocode.name => geocode.value )}],
160
+ [ POLYGONS_YAML, @polygons ]).to_yaml( options )
161
+ end
162
+
163
+ # @param [Hash] area_yaml_data
164
+ # @return [Area]
165
+ def self.from_yaml_data( area_yaml_data )
166
+ self.new do |area|
167
+ area.area_desc = area_yaml_data[ AREA_DESC_YAML ]
168
+ area.altitude = area_yaml_data[ ALTITUDE_YAML ]
169
+ area.ceiling = area_yaml_data[ CEILING_YAML ]
170
+
171
+ Array( area_yaml_data[ CIRCLES_YAML ]).each do |circle_yaml_data|
172
+ area.circles << area.circle_class.from_yaml_data( circle_yaml_data )
173
+ end
174
+
175
+ Array( area_yaml_data[ GEOCODES_YAML ]).each do |name, value|
176
+ area.add_geocode do |geocode|
177
+ geocode.name = name
178
+ geocode.value = value
179
+ end
180
+ end
181
+
182
+ Array( area_yaml_data[ POLYGONS_YAML ]).each do |polyon_yaml_data|
183
+ area.polygons << area.polygon_class.from_yaml_data( polyon_yaml_data )
184
+ end
185
+ end
186
+ end
187
+
188
+ AREA_DESC_KEY = 'area_desc'
189
+ ALTITUDE_KEY = 'altitude'
190
+ CEILING_KEY = 'ceiling'
191
+ CIRCLES_KEY = 'circles'
192
+ GEOCODES_KEY = 'geocodes'
193
+ POLYGONS_KEY = 'polygons'
194
+
195
+ # @param [Hash] area_hash
196
+ # @return [Area]
197
+ def self.from_h( area_hash )
198
+ self.new do |area|
199
+ area.area_desc = area_hash[ AREA_DESC_KEY ]
200
+ area.altitude = area_hash[ ALTITUDE_KEY ]
201
+ area.ceiling = area_hash[ CEILING_KEY ]
202
+
203
+ Array( area_hash[ CIRCLES_KEY ]).each do |circle_array|
204
+ area.circles << area.circle_class.from_a( circle_array )
205
+ end
206
+
207
+ Array( area_hash[ GEOCODES_KEY ]).each do |geocode_hash|
208
+ area.geocodes << area.geocode_class.from_h( geocode_hash )
209
+ end
210
+
211
+ Array( area_hash[ POLYGONS_KEY ]).each do |polygon_hash|
212
+ area.polygons << area.polygon_class.from_h( polygon_hash )
213
+ end
214
+ end
215
+ end
216
+
217
+ # @return [Hash]
218
+ def to_h
219
+ RCAP.attribute_values_to_hash( [ AREA_DESC_KEY, @area_desc ],
220
+ [ ALTITUDE_KEY, @altitude ],
221
+ [ CEILING_KEY, @ceiling ],
222
+ [ CIRCLES_KEY, @circles.map{ |circle| circle.to_a }],
223
+ [ GEOCODES_KEY, @geocodes.map{ |geocode| geocode.to_h }],
224
+ [ POLYGONS_KEY, @polygons.map{ |polygon| polygon.to_h }])
225
+ end
226
+ end
227
+ end
228
+ end