@@ -1,3 +1,5 @@ |
||
| 1 |
+require 'location' |
|
| 2 |
+ |
|
| 1 | 3 |
# Events are how Huginn Agents communicate and log information about the world. Events can be emitted and received by |
| 2 | 4 |
# Agents. They contain a serialized `payload` of arbitrary JSON data, as well as optional `lat`, `lng`, and `expires_at` |
| 3 | 5 |
# fields. |
@@ -33,10 +35,10 @@ class Event < ActiveRecord::Base |
||
| 33 | 35 |
} |
| 34 | 36 |
|
| 35 | 37 |
def location |
| 36 |
- @location ||= {
|
|
| 37 |
- # lat and lng are BigDecimal, so convert them to Float |
|
| 38 |
- lat: (lat.to_f if lat), |
|
| 39 |
- lng: (lng.to_f if lng), |
|
| 38 |
+ @location ||= Location.new( |
|
| 39 |
+ # lat and lng are BigDecimal, but converted to Float by the Location class |
|
| 40 |
+ lat: lat, |
|
| 41 |
+ lng: lng, |
|
| 40 | 42 |
radius: |
| 41 | 43 |
begin |
| 42 | 44 |
h = payload[:horizontal_accuracy].presence |
@@ -47,21 +49,8 @@ class Event < ActiveRecord::Base |
||
| 47 | 49 |
(h || v || payload[:accuracy]).to_f |
| 48 | 50 |
end |
| 49 | 51 |
end, |
| 50 |
- course: |
|
| 51 |
- begin |
|
| 52 |
- if (v = payload[:course].presence) && |
|
| 53 |
- (v = v.to_f) >= 0 |
|
| 54 |
- v |
|
| 55 |
- end |
|
| 56 |
- end, |
|
| 57 |
- speed: |
|
| 58 |
- begin |
|
| 59 |
- if (v = payload[:speed].presence) && |
|
| 60 |
- (v = v.to_f) >= 0 |
|
| 61 |
- v |
|
| 62 |
- end |
|
| 63 |
- end, |
|
| 64 |
- } |
|
| 52 |
+ course: payload[:course], |
|
| 53 |
+ speed: payload[:speed].presence) |
|
| 65 | 54 |
end |
| 66 | 55 |
|
| 67 | 56 |
# Emit this event again, as a new Event. |
@@ -0,0 +1,82 @@ |
||
| 1 |
+Location = Struct.new(:lat, :lng, :radius, :speed, :course) |
|
| 2 |
+ |
|
| 3 |
+class Location |
|
| 4 |
+ protected :[]= |
|
| 5 |
+ |
|
| 6 |
+ def initialize(data = {})
|
|
| 7 |
+ super() |
|
| 8 |
+ |
|
| 9 |
+ case data |
|
| 10 |
+ when Array |
|
| 11 |
+ raise ArgumentError, 'unsupported location data' unless data.size == 2 |
|
| 12 |
+ self.lat, self.lng = data |
|
| 13 |
+ when Hash, Location |
|
| 14 |
+ data.each { |key, value|
|
|
| 15 |
+ begin |
|
| 16 |
+ __send__("#{key}=", value)
|
|
| 17 |
+ rescue NameError |
|
| 18 |
+ end |
|
| 19 |
+ } |
|
| 20 |
+ else |
|
| 21 |
+ raise ArgumentError, 'unsupported location data' |
|
| 22 |
+ end |
|
| 23 |
+ |
|
| 24 |
+ yield self if block_given? |
|
| 25 |
+ end |
|
| 26 |
+ |
|
| 27 |
+ def lat=(value) |
|
| 28 |
+ self[:lat] = floatify(value) { |f|
|
|
| 29 |
+ if f.abs <= 90 |
|
| 30 |
+ f |
|
| 31 |
+ else |
|
| 32 |
+ raise ArgumentError, 'out of bounds' |
|
| 33 |
+ end |
|
| 34 |
+ } |
|
| 35 |
+ end |
|
| 36 |
+ |
|
| 37 |
+ def lng=(value) |
|
| 38 |
+ self[:lng] = floatify(value) { |f|
|
|
| 39 |
+ if f.abs <= 180 |
|
| 40 |
+ f |
|
| 41 |
+ else |
|
| 42 |
+ raise ArgumentError, 'out of bounds' |
|
| 43 |
+ end |
|
| 44 |
+ } |
|
| 45 |
+ end |
|
| 46 |
+ |
|
| 47 |
+ def radius=(value) |
|
| 48 |
+ self[:radius] = floatify(value) { |f| f if f >= 0 }
|
|
| 49 |
+ end |
|
| 50 |
+ |
|
| 51 |
+ def speed=(value) |
|
| 52 |
+ self[:speed] = floatify(value) { |f| f if f >= 0 }
|
|
| 53 |
+ end |
|
| 54 |
+ |
|
| 55 |
+ def course=(value) |
|
| 56 |
+ self[:course] = floatify(value) { |f| f if (0..360).cover?(f) }
|
|
| 57 |
+ end |
|
| 58 |
+ |
|
| 59 |
+ def present? |
|
| 60 |
+ lat && lng |
|
| 61 |
+ end |
|
| 62 |
+ |
|
| 63 |
+ def empty? |
|
| 64 |
+ !present? |
|
| 65 |
+ end |
|
| 66 |
+ |
|
| 67 |
+ private |
|
| 68 |
+ |
|
| 69 |
+ def floatify(value) |
|
| 70 |
+ case value |
|
| 71 |
+ when nil, '' |
|
| 72 |
+ return nil |
|
| 73 |
+ else |
|
| 74 |
+ float = Float(value) |
|
| 75 |
+ if block_given? |
|
| 76 |
+ yield(float) |
|
| 77 |
+ else |
|
| 78 |
+ float |
|
| 79 |
+ end |
|
| 80 |
+ end |
|
| 81 |
+ end |
|
| 82 |
+end |
@@ -0,0 +1,49 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe Location do |
|
| 4 |
+ let(:location) {
|
|
| 5 |
+ Location.new( |
|
| 6 |
+ lat: BigDecimal.new('2.0'),
|
|
| 7 |
+ lng: BigDecimal.new('3.0'),
|
|
| 8 |
+ radius: 300, |
|
| 9 |
+ speed: 2, |
|
| 10 |
+ course: 30) |
|
| 11 |
+ } |
|
| 12 |
+ |
|
| 13 |
+ it "converts values to Float" do |
|
| 14 |
+ expect(location.lat).to equal 2.0 |
|
| 15 |
+ expect(location.lng).to equal 3.0 |
|
| 16 |
+ expect(location.radius).to equal 300.0 |
|
| 17 |
+ expect(location.speed).to equal 2.0 |
|
| 18 |
+ expect(location.course).to equal 30.0 |
|
| 19 |
+ end |
|
| 20 |
+ |
|
| 21 |
+ it "provides hash-style access to its properties with both symbol and string keys" do |
|
| 22 |
+ expect(location[:lat]).to equal 2.0 |
|
| 23 |
+ expect(location['lat']).to equal 2.0 |
|
| 24 |
+ end |
|
| 25 |
+ |
|
| 26 |
+ it "does not allow hash-style assignment" do |
|
| 27 |
+ expect {
|
|
| 28 |
+ location[:lat] = 2.0 |
|
| 29 |
+ }.to raise_error |
|
| 30 |
+ end |
|
| 31 |
+ |
|
| 32 |
+ it "ignores invalid values" do |
|
| 33 |
+ location2 = Location.new( |
|
| 34 |
+ lat: 2, |
|
| 35 |
+ lng: 3, |
|
| 36 |
+ radius: -1, |
|
| 37 |
+ speed: -1, |
|
| 38 |
+ course: -1) |
|
| 39 |
+ expect(location2.radius).to be_nil |
|
| 40 |
+ expect(location2.speed).to be_nil |
|
| 41 |
+ expect(location2.course).to be_nil |
|
| 42 |
+ end |
|
| 43 |
+ |
|
| 44 |
+ it "considers a location empty if either latitude or longitude is missing" do |
|
| 45 |
+ expect(Location.new.empty?).to be_truthy |
|
| 46 |
+ expect(Location.new(lat: 2, radius: 1).present?).to be_falsy |
|
| 47 |
+ expect(Location.new(lng: 3, radius: 1).present?).to be_falsy |
|
| 48 |
+ end |
|
| 49 |
+end |
@@ -18,13 +18,12 @@ describe Event do |
||
| 18 | 18 |
describe "#location" do |
| 19 | 19 |
it "returns a default hash when an event does not have a location" do |
| 20 | 20 |
event = events(:bob_website_agent_event) |
| 21 |
- event.location.should == {
|
|
| 21 |
+ event.location.should == Location.new( |
|
| 22 | 22 |
lat: nil, |
| 23 | 23 |
lng: nil, |
| 24 | 24 |
radius: 0.0, |
| 25 | 25 |
speed: nil, |
| 26 |
- course: nil, |
|
| 27 |
- } |
|
| 26 |
+ course: nil) |
|
| 28 | 27 |
end |
| 29 | 28 |
|
| 30 | 29 |
it "returns a hash containing location information" do |
@@ -37,31 +36,12 @@ describe Event do |
||
| 37 | 36 |
course: 90.0, |
| 38 | 37 |
} |
| 39 | 38 |
event.save! |
| 40 |
- event.location.should == {
|
|
| 39 |
+ event.location.should == Location.new( |
|
| 41 | 40 |
lat: 2.0, |
| 42 | 41 |
lng: 3.0, |
| 43 | 42 |
radius: 0.0, |
| 44 | 43 |
speed: 0.5, |
| 45 |
- course: 90.0, |
|
| 46 |
- } |
|
| 47 |
- end |
|
| 48 |
- |
|
| 49 |
- it "ignores invalid speed and course" do |
|
| 50 |
- event = events(:bob_website_agent_event) |
|
| 51 |
- event.lat = 2 |
|
| 52 |
- event.lng = 3 |
|
| 53 |
- event.payload = {
|
|
| 54 |
- speed: -1, |
|
| 55 |
- course: -1, |
|
| 56 |
- } |
|
| 57 |
- event.save! |
|
| 58 |
- event.location.should == {
|
|
| 59 |
- lat: 2.0, |
|
| 60 |
- lng: 3.0, |
|
| 61 |
- radius: 0.0, |
|
| 62 |
- speed: nil, |
|
| 63 |
- course: nil, |
|
| 64 |
- } |
|
| 44 |
+ course: 90.0) |
|
| 65 | 45 |
end |
| 66 | 46 |
end |
| 67 | 47 |
|