ruby-macrodroid 0.8.9 → 0.9.1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/ruby-macrodroid.rb +28 -758
- data/lib/ruby-macrodroid/actions.rb +142 -34
- data/lib/ruby-macrodroid/constraints.rb +1338 -0
- data/lib/ruby-macrodroid/macro.rb +630 -0
- data/lib/ruby-macrodroid/triggers.rb +42 -9
- metadata +12 -10
- metadata.gz.sig +0 -0
@@ -0,0 +1,630 @@
|
|
1
|
+
# file: ruby-macrodroid/macro.rb
|
2
|
+
|
3
|
+
|
4
|
+
# This file contains the following classes:
|
5
|
+
#
|
6
|
+
# ## Macro class
|
7
|
+
#
|
8
|
+
# Macro
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
VAR_TYPES = {
|
13
|
+
String: [2, :string_value],
|
14
|
+
TrueClass: [0, :boolean_value],
|
15
|
+
TrueClass: [0, :boolean_value],
|
16
|
+
Integer: [1, :int_value],
|
17
|
+
Float: [3, :decimal_value]
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
class Macro
|
22
|
+
using ColouredText
|
23
|
+
using Params
|
24
|
+
|
25
|
+
attr_reader :local_variables, :triggers, :actions, :constraints,
|
26
|
+
:guid, :deviceid
|
27
|
+
attr_accessor :title, :description, :remote_url
|
28
|
+
|
29
|
+
def initialize(name=nil, geofences: nil, deviceid: nil, remote_url: nil,
|
30
|
+
debug: false)
|
31
|
+
|
32
|
+
@title, @geofences, @deviceid, @debug = name, geofences, deviceid, debug
|
33
|
+
@remote_url = remote_url
|
34
|
+
|
35
|
+
puts 'inside Macro#initialize' if @debug
|
36
|
+
|
37
|
+
@local_variables, @triggers, @actions, @constraints = [], [], [], []
|
38
|
+
@h = {}
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(obj)
|
43
|
+
|
44
|
+
if obj.kind_of? Trigger then
|
45
|
+
|
46
|
+
puts 'trigger found' if @debug
|
47
|
+
@triggers << obj
|
48
|
+
|
49
|
+
elsif obj.kind_of? Action
|
50
|
+
|
51
|
+
puts 'action found' if @debug
|
52
|
+
@actions << obj
|
53
|
+
|
54
|
+
elsif obj.kind_of? Constraint
|
55
|
+
|
56
|
+
puts 'constraint found' if @debug
|
57
|
+
@constraints << obj
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_h()
|
64
|
+
|
65
|
+
h = {
|
66
|
+
local_variables: varify(@local_variables),
|
67
|
+
m_trigger_list: @triggers.map(&:to_h),
|
68
|
+
m_action_list: @actions.map(&:to_h),
|
69
|
+
m_category: @category,
|
70
|
+
m_constraint_list: @constraints.map(&:to_h),
|
71
|
+
m_description: '',
|
72
|
+
m_name: title(),
|
73
|
+
m_excludeLog: false,
|
74
|
+
m_GUID: guid(),
|
75
|
+
m_isOrCondition: false,
|
76
|
+
m_enabled: false,
|
77
|
+
m_descriptionOpen: false,
|
78
|
+
m_headingColor: 0
|
79
|
+
}
|
80
|
+
|
81
|
+
puts 'h: ' + h.inspect if @debug
|
82
|
+
|
83
|
+
@h.merge(h)
|
84
|
+
end
|
85
|
+
|
86
|
+
def import_h(h)
|
87
|
+
|
88
|
+
if @debug then
|
89
|
+
puts 'inside import_h'
|
90
|
+
puts 'h:' + h.inspect
|
91
|
+
end
|
92
|
+
|
93
|
+
@category = h[:category]
|
94
|
+
@title = h[:name]
|
95
|
+
@description = h[:description]
|
96
|
+
|
97
|
+
# fetch the local variables
|
98
|
+
if h[:local_variables].any? and h[:local_variables].first.any? then
|
99
|
+
|
100
|
+
@local_variables = h[:local_variables].map do |var|
|
101
|
+
|
102
|
+
val = case var[:type]
|
103
|
+
when 0 # boolean
|
104
|
+
var[:boolean_value]
|
105
|
+
when 1 # integer
|
106
|
+
var[:int_value]
|
107
|
+
when 2 # string
|
108
|
+
var[:string_value]
|
109
|
+
when 3 # decimal
|
110
|
+
var[:decimal_Value]
|
111
|
+
end
|
112
|
+
|
113
|
+
[var[:name], val]
|
114
|
+
|
115
|
+
end.to_h
|
116
|
+
end
|
117
|
+
|
118
|
+
# fetch the triggers
|
119
|
+
@triggers = h[:trigger_list].map do |trigger|
|
120
|
+
puts 'trigger: ' + trigger.inspect
|
121
|
+
#exit
|
122
|
+
object(trigger.to_snake_case)
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
@actions = h[:action_list].map do |action|
|
127
|
+
object(action.to_snake_case)
|
128
|
+
end
|
129
|
+
puts 'before fetch constraints' if @debug
|
130
|
+
# fetch the constraints
|
131
|
+
@constraints = h[:constraint_list].map do |constraint|
|
132
|
+
object(constraint.to_snake_case)
|
133
|
+
end
|
134
|
+
puts 'after fetch constraints' if @debug
|
135
|
+
@h = h
|
136
|
+
|
137
|
+
%i(local_variables m_trigger_list m_action_list m_constraint_list)\
|
138
|
+
.each {|x| @h[x] = [] }
|
139
|
+
puts 'after @h set' if @debug
|
140
|
+
@h
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
def import_xml(node)
|
145
|
+
|
146
|
+
if @debug then
|
147
|
+
puts 'inside Macro#import_xml'
|
148
|
+
puts 'node: ' + node.xml.inspect
|
149
|
+
end
|
150
|
+
|
151
|
+
if node.element('triggers') then
|
152
|
+
|
153
|
+
# level 2
|
154
|
+
|
155
|
+
@title = node.attributes[:name]
|
156
|
+
@category = node.attributes[:category]
|
157
|
+
@description = node.attributes[:description]
|
158
|
+
|
159
|
+
|
160
|
+
# get all the triggers
|
161
|
+
@triggers = node.xpath('triggers/*').map do |e|
|
162
|
+
|
163
|
+
puts 'e.name: ' + e.name.inspect if @debug
|
164
|
+
{timer: TimerTrigger}[e.name.to_sym].new(e.attributes.to_h)
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
# get all the actions
|
169
|
+
@actions = node.xpath('actions/*').map do |e|
|
170
|
+
|
171
|
+
if e.name == 'notification' then
|
172
|
+
|
173
|
+
case e.attributes[:type].to_sym
|
174
|
+
when :popup
|
175
|
+
e.attributes.delete :type
|
176
|
+
ToastAction.new e.attributes.to_h
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
# get all the constraints
|
184
|
+
@constraints = node.xpath('constraints/*').map do |e|
|
185
|
+
|
186
|
+
puts 'e.name: ' + e.name.inspect if @debug
|
187
|
+
{airplanemode: AirplaneModeConstraint}[e.name.to_sym].new(e.attributes.to_h)
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
else
|
192
|
+
|
193
|
+
# Level 1
|
194
|
+
|
195
|
+
puts 'import_xml: inside level 1' if @debug
|
196
|
+
|
197
|
+
@title = node.text('macro') || node.attributes[:name]
|
198
|
+
|
199
|
+
@local_variables = node.xpath('variable').map do |e|
|
200
|
+
|
201
|
+
label, v = e.text.to_s.split(/: */,2)
|
202
|
+
|
203
|
+
value = if v.to_f.to_s == v
|
204
|
+
v.to_f
|
205
|
+
elsif v.downcase == 'true'
|
206
|
+
true
|
207
|
+
elsif v.downcase == 'false'
|
208
|
+
false
|
209
|
+
elsif v.to_i.to_s == v
|
210
|
+
v.to_i
|
211
|
+
else
|
212
|
+
v
|
213
|
+
end
|
214
|
+
|
215
|
+
[label, value]
|
216
|
+
end
|
217
|
+
|
218
|
+
#@description = node.attributes[:description]
|
219
|
+
|
220
|
+
tp = TriggersNlp.new(self)
|
221
|
+
|
222
|
+
@triggers = node.xpath('trigger').flat_map do |e|
|
223
|
+
|
224
|
+
r = tp.find_trigger e.text
|
225
|
+
|
226
|
+
puts 'found trigger ' + r.inspect if @debug
|
227
|
+
|
228
|
+
item = e.element('item')
|
229
|
+
if item then
|
230
|
+
|
231
|
+
if item.element('description') then
|
232
|
+
|
233
|
+
item.xpath('description').map do |description|
|
234
|
+
|
235
|
+
inner_lines = description.text.to_s.strip.lines
|
236
|
+
puts 'inner_lines: ' + inner_lines.inspect if @debug
|
237
|
+
|
238
|
+
trigger = if e.text.to_s.strip.empty? then
|
239
|
+
inner_lines.shift.strip
|
240
|
+
else
|
241
|
+
e.text.strip
|
242
|
+
end
|
243
|
+
|
244
|
+
puts 'trigger: ' + trigger.inspect if @debug
|
245
|
+
|
246
|
+
r = tp.find_trigger trigger
|
247
|
+
puts 'r: ' + r.inspect if @debug
|
248
|
+
o = r[0].new([description, self]) if r
|
249
|
+
puts 'after o' if @debug
|
250
|
+
o
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
else
|
255
|
+
|
256
|
+
trigger = e.text.strip
|
257
|
+
r = tp.find_trigger trigger
|
258
|
+
|
259
|
+
a = e.xpath('item/*')
|
260
|
+
|
261
|
+
h = if a.any? then
|
262
|
+
a.map {|node| [node.name.to_sym, node.text.to_s]}.to_h
|
263
|
+
else
|
264
|
+
{}
|
265
|
+
end
|
266
|
+
|
267
|
+
r = tp.find_trigger trigger
|
268
|
+
r[0].new(h) if r
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
else
|
273
|
+
|
274
|
+
trigger = e.text.strip
|
275
|
+
r = tp.find_trigger trigger
|
276
|
+
r[0].new(r[1]) if r
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
ap = ActionsNlp.new self
|
283
|
+
|
284
|
+
@actions = node.xpath('action').flat_map do |e|
|
285
|
+
|
286
|
+
puts 'action e: ' + e.xml.inspect if @debug
|
287
|
+
puts 'e.text ' + e.text if @debug
|
288
|
+
|
289
|
+
item = e.element('item')
|
290
|
+
if item then
|
291
|
+
|
292
|
+
if item.element('description') then
|
293
|
+
|
294
|
+
item.xpath('description').map do |description|
|
295
|
+
|
296
|
+
inner_lines = description.text.to_s.strip.lines
|
297
|
+
puts 'inner_lines: ' + inner_lines.inspect if @debug
|
298
|
+
|
299
|
+
action = if e.text.to_s.strip.empty? then
|
300
|
+
inner_lines.shift.strip
|
301
|
+
else
|
302
|
+
e.text.strip
|
303
|
+
end
|
304
|
+
|
305
|
+
puts 'action: ' + action.inspect if @debug
|
306
|
+
|
307
|
+
r = ap.find_action action
|
308
|
+
puts 'r: ' + r.inspect if @debug
|
309
|
+
o = r[0].new([description, self]) if r
|
310
|
+
puts 'after o' if @debug
|
311
|
+
o
|
312
|
+
|
313
|
+
end
|
314
|
+
|
315
|
+
else
|
316
|
+
|
317
|
+
action = e.text.strip
|
318
|
+
r = ap.find_action action
|
319
|
+
|
320
|
+
a = e.xpath('item/*')
|
321
|
+
|
322
|
+
h = if a.any? then
|
323
|
+
a.map {|node| [node.name.to_sym, node.text.to_s]}.to_h
|
324
|
+
else
|
325
|
+
{}
|
326
|
+
end
|
327
|
+
|
328
|
+
r = ap.find_action action
|
329
|
+
r[0].new(h) if r
|
330
|
+
|
331
|
+
end
|
332
|
+
|
333
|
+
else
|
334
|
+
|
335
|
+
action = e.text.strip
|
336
|
+
r = ap.find_action action
|
337
|
+
r[0].new(r[1]) if r
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
cp = ConstraintsNlp.new
|
344
|
+
|
345
|
+
@constraints = node.xpath('constraint').map do |e|
|
346
|
+
|
347
|
+
r = cp.find_constraint e.text
|
348
|
+
puts 'found constraint ' + r.inspect if @debug
|
349
|
+
|
350
|
+
if r then
|
351
|
+
r[0].new(r[1])
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
end
|
357
|
+
|
358
|
+
self
|
359
|
+
|
360
|
+
end
|
361
|
+
|
362
|
+
def match?(triggerx, detail={time: $env[:time]}, model=nil )
|
363
|
+
|
364
|
+
if @triggers.any? {|x| x.type == triggerx and x.match?(detail, model) } then
|
365
|
+
|
366
|
+
if @debug then
|
367
|
+
puts 'checking constraints ...'
|
368
|
+
puts '@constraints: ' + @constraints.inspect
|
369
|
+
end
|
370
|
+
|
371
|
+
if @constraints.all? {|x| x.match?($env.merge(detail), model) } then
|
372
|
+
|
373
|
+
true
|
374
|
+
|
375
|
+
else
|
376
|
+
|
377
|
+
return false
|
378
|
+
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
|
385
|
+
# invokes the actions
|
386
|
+
#
|
387
|
+
def run()
|
388
|
+
@actions.map(&:invoke)
|
389
|
+
end
|
390
|
+
|
391
|
+
# prepares the environment in order for triggers to test fire successfully
|
392
|
+
# Used for testing
|
393
|
+
#
|
394
|
+
def set_env()
|
395
|
+
@triggers.each(&:set_env)
|
396
|
+
end
|
397
|
+
|
398
|
+
def to_pc()
|
399
|
+
|
400
|
+
heading = '# ' + @title
|
401
|
+
heading += '\n# ' + @description if @description
|
402
|
+
condition = @triggers.first.to_pc
|
403
|
+
actions = @actions.map(&:to_pc).join("\n")
|
404
|
+
|
405
|
+
<<EOF
|
406
|
+
#{heading}
|
407
|
+
|
408
|
+
if #{condition} then
|
409
|
+
#{actions}
|
410
|
+
end
|
411
|
+
EOF
|
412
|
+
end
|
413
|
+
|
414
|
+
def to_s(colour: false)
|
415
|
+
|
416
|
+
indent = 0 #@actions.map(&:to_s).join.lines.length > 0 ? 1 : 0
|
417
|
+
|
418
|
+
a = []
|
419
|
+
a << '# ' + @category + "\n" if @category
|
420
|
+
a << (colour ? "m".bg_cyan.gray.bold : 'm') + ': ' + @title
|
421
|
+
|
422
|
+
|
423
|
+
if @description and @description.length >= 1 then
|
424
|
+
a << (colour ? "d".bg_gray.gray.bold : 'd') + ': ' \
|
425
|
+
+ @description.gsub(/\n/,"\n ")
|
426
|
+
end
|
427
|
+
|
428
|
+
if @local_variables.length >= 1 then
|
429
|
+
|
430
|
+
vars = @local_variables.map do |k,v|
|
431
|
+
label = colour ? 'v'.bg_magenta : 'v'
|
432
|
+
label += ': '
|
433
|
+
label + "%s: %s" % [k,v]
|
434
|
+
end
|
435
|
+
|
436
|
+
a << vars.join("\n")
|
437
|
+
end
|
438
|
+
|
439
|
+
puts 'before triggers' if @debug
|
440
|
+
|
441
|
+
a << @triggers.map do |x|
|
442
|
+
|
443
|
+
puts 'x: ' + x.inspect if @debug
|
444
|
+
|
445
|
+
s =-x.to_s(colour: colour)
|
446
|
+
puts 's: ' + s.inspect if @debug
|
447
|
+
|
448
|
+
s2 = if s.lines.length > 1 then
|
449
|
+
"\n" + s.lines.map {|x| x.prepend (' ' * (indent+1)) }.join
|
450
|
+
else
|
451
|
+
' ' + s
|
452
|
+
end
|
453
|
+
|
454
|
+
puts 's2: ' + s2.inspect if @debug
|
455
|
+
|
456
|
+
#s.lines > 1 ? "\n" + x : x
|
457
|
+
(colour ? "t".bg_red.gray.bold : 't') + ":" + s2
|
458
|
+
end.join("\n")
|
459
|
+
|
460
|
+
puts 'before actions' if @debug
|
461
|
+
actions = @actions.map do |x|
|
462
|
+
|
463
|
+
puts 'x: ' + x.inspect if @debug
|
464
|
+
raise 'Macro#to_s action cannot be nil' if x.nil?
|
465
|
+
s = x.to_s(colour: colour)
|
466
|
+
#puts 's: ' + s.inspect
|
467
|
+
|
468
|
+
|
469
|
+
|
470
|
+
r = if indent <= 0 then
|
471
|
+
|
472
|
+
lines = s.lines
|
473
|
+
|
474
|
+
if lines.length > 1 then
|
475
|
+
s = lines.map {|x| x.prepend (' ' * (indent+1)) }.join
|
476
|
+
end
|
477
|
+
|
478
|
+
s2 = s.lines.length > 1 ? "\n" + s : ' ' + s
|
479
|
+
|
480
|
+
if colour then
|
481
|
+
"a".bg_blue.gray.bold + ":" + s2
|
482
|
+
else
|
483
|
+
"a:" + s2
|
484
|
+
end
|
485
|
+
|
486
|
+
elsif indent > 0
|
487
|
+
|
488
|
+
if s =~ /^Else/ then
|
489
|
+
(' ' * (indent-1)) + "%s" % s
|
490
|
+
elsif s =~ /^End/
|
491
|
+
indent -= 1
|
492
|
+
(' ' * indent) + "%s" % s
|
493
|
+
else
|
494
|
+
s2 = s.lines[0] + s.lines[1..-1].map {|x| (' ' * indent) + x }.join
|
495
|
+
(' ' * indent) + "%s" % s2
|
496
|
+
end
|
497
|
+
|
498
|
+
end
|
499
|
+
|
500
|
+
if s =~ /^(?:If|DO \/ WHILE)/i then
|
501
|
+
|
502
|
+
if indent < 1 then
|
503
|
+
|
504
|
+
r = if colour then
|
505
|
+
"a".bg_blue.gray.bold + ":\n %s" % s
|
506
|
+
else
|
507
|
+
"a:\n %s" % s
|
508
|
+
end
|
509
|
+
|
510
|
+
indent += 1
|
511
|
+
else
|
512
|
+
r = (' ' * indent) + "%s" % s
|
513
|
+
end
|
514
|
+
|
515
|
+
indent += 1
|
516
|
+
end
|
517
|
+
|
518
|
+
r
|
519
|
+
|
520
|
+
end.join("\n")
|
521
|
+
|
522
|
+
|
523
|
+
|
524
|
+
|
525
|
+
a << actions
|
526
|
+
|
527
|
+
|
528
|
+
puts 'before constraints' if @debug
|
529
|
+
if @constraints.any? then
|
530
|
+
a << @constraints.map do |x|
|
531
|
+
(colour ? "c".bg_green.gray.bold : 'c') + ": %s" % x
|
532
|
+
end.join("\n")
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
|
537
|
+
|
538
|
+
|
539
|
+
a.join("\n") + "\n"
|
540
|
+
|
541
|
+
end
|
542
|
+
|
543
|
+
def to_summary(colour: false)
|
544
|
+
|
545
|
+
if colour then
|
546
|
+
|
547
|
+
a = [
|
548
|
+
'm'.bg_cyan.gray.bold + ': ' + @title,
|
549
|
+
't'.bg_red.gray.bold + ': ' + @triggers.map \
|
550
|
+
{|x| x.to_summary(colour: false)}.join(", "),
|
551
|
+
'a'.bg_blue.gray.bold + ': ' + @actions.map \
|
552
|
+
{|x| x.to_summary(colour: false)}.join(", ")
|
553
|
+
]
|
554
|
+
|
555
|
+
if @constraints.any? then
|
556
|
+
a << 'c'.bg_green.gray.bold + ': ' + @constraints.map \
|
557
|
+
{|x| x.to_summary(colour: false)}.join(", ")
|
558
|
+
end
|
559
|
+
|
560
|
+
else
|
561
|
+
|
562
|
+
a = [
|
563
|
+
'm: ' + @title,
|
564
|
+
't: ' + @triggers.map {|x| x.to_summary(colour: false)}.join(", "),
|
565
|
+
'a: ' + @actions.map {|x| x.to_summary(colour: false)}.join(", ")
|
566
|
+
]
|
567
|
+
|
568
|
+
if @constraints.any? then
|
569
|
+
a << 'c: ' + @constraints.map \
|
570
|
+
{|x| x.to_summary(colour: false)}.join(", ")
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
|
575
|
+
|
576
|
+
a.join("\n") + "\n"
|
577
|
+
|
578
|
+
end
|
579
|
+
|
580
|
+
private
|
581
|
+
|
582
|
+
def guid()
|
583
|
+
'-' + rand(1..9).to_s + 18.times.map { rand 9 }.join
|
584
|
+
end
|
585
|
+
|
586
|
+
def object(h={})
|
587
|
+
|
588
|
+
puts ('inside object h:' + h.inspect).debug if @debug
|
589
|
+
klass = Object.const_get h[:class_type]
|
590
|
+
puts klass.inspect.highlight if $debug
|
591
|
+
|
592
|
+
if klass == GeofenceTrigger then
|
593
|
+
puts 'GeofenceTrigger found'.highlight if $debug
|
594
|
+
GeofenceTrigger.new(h, geofences: @geofences)
|
595
|
+
else
|
596
|
+
puts 'before klass'
|
597
|
+
h2 = h.merge( macro: self)
|
598
|
+
puts 'h2: ' + h2.inspect
|
599
|
+
r = klass.new h2
|
600
|
+
puts 'r:' + r.inspect
|
601
|
+
r
|
602
|
+
|
603
|
+
end
|
604
|
+
|
605
|
+
end
|
606
|
+
|
607
|
+
def varify(local_variables)
|
608
|
+
|
609
|
+
|
610
|
+
local_variables.map do |key, value|
|
611
|
+
|
612
|
+
puts 'value ' + value.class.to_s.to_sym.inspect
|
613
|
+
puts 'VAR_TYPES: ' + VAR_TYPES.inspect
|
614
|
+
type = VAR_TYPES[value.class.to_s.to_sym]
|
615
|
+
puts 'type: ' + type.inspect
|
616
|
+
h = {
|
617
|
+
boolean_value: false,
|
618
|
+
decimal_value: 0.0,
|
619
|
+
int_value: 0,
|
620
|
+
name: key,
|
621
|
+
string_value: '',
|
622
|
+
type: type[0]
|
623
|
+
}
|
624
|
+
h[type[1]] = value
|
625
|
+
h
|
626
|
+
end
|
627
|
+
|
628
|
+
end
|
629
|
+
|
630
|
+
end
|