diff --git a/app/assets/javascripts/map_marker.js.coffee b/app/assets/javascripts/map_marker.js.coffee new file mode 100644 index 00000000..146a805c --- /dev/null +++ b/app/assets/javascripts/map_marker.js.coffee @@ -0,0 +1,41 @@ +window.map_marker = (map, options = {}) -> + pos = new google.maps.LatLng(options.lat, options.lng) + + if options.radius > 0 + new google.maps.Circle + map: map + strokeColor: '#FF0000' + strokeOpacity: 0.8 + strokeWeight: 2 + fillColor: '#FF0000' + fillOpacity: 0.35 + center: pos + radius: options.radius + else + new google.maps.Marker + map: map + position: pos + title: 'Recorded Location' + + if options.course + p1 = new LatLon(pos.lat(), pos.lng()) + speed = options.speed ? 1 + p2 = p1.destinationPoint(options.course, Math.max(0.2, speed) * 0.1) + + lineCoordinates = [ + pos + new google.maps.LatLng(p2.lat(), p2.lon()) + ] + + lineSymbol = + path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW + + new google.maps.Polyline + map: map + path: lineCoordinates + icons: [ + { + icon: lineSymbol + offset: '100%' + } + ] diff --git a/app/models/agents/user_location_agent.rb b/app/models/agents/user_location_agent.rb index 2575e800..e3164131 100644 --- a/app/models/agents/user_location_agent.rb +++ b/app/models/agents/user_location_agent.rb @@ -65,8 +65,10 @@ module Agents private def handle_payload(payload) - if payload[:latitude].present? && payload[:longitude].present? - create_event payload: payload, lat: payload[:latitude].to_f, lng: payload[:longitude].to_f + location = Location.new(payload) + + if location.present? + create_event payload: payload, location: location end end end diff --git a/app/models/event.rb b/app/models/event.rb index f534ef05..321fc233 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,3 +1,5 @@ +require 'location' + # Events are how Huginn Agents communicate and log information about the world. Events can be emitted and received by # Agents. They contain a serialized `payload` of arbitrary JSON data, as well as optional `lat`, `lng`, and `expires_at` # fields. @@ -5,7 +7,7 @@ class Event < ActiveRecord::Base include JSONSerializedField include LiquidDroppable - attr_accessible :lat, :lng, :payload, :user_id, :user, :expires_at + attr_accessible :lat, :lng, :location, :payload, :user_id, :user, :expires_at acts_as_mappable @@ -28,6 +30,42 @@ class Event < ActiveRecord::Base where("expires_at IS NOT NULL AND expires_at < ?", Time.now) } + scope :with_location, -> { + where.not(lat: nil).where.not(lng: nil) + } + + def location + @location ||= Location.new( + # lat and lng are BigDecimal, but converted to Float by the Location class + lat: lat, + lng: lng, + radius: + begin + h = payload[:horizontal_accuracy].presence + v = payload[:vertical_accuracy].presence + if h && v + (h.to_f + v.to_f) / 2 + else + (h || v || payload[:accuracy]).to_f + end + end, + course: payload[:course], + speed: payload[:speed].presence) + end + + def location=(location) + case location + when nil + self.lat = self.lng = nil + return + when Location + else + location = Location.new(location) + end + self.lat, self.lng = location.lat, location.lng + location + end + # Emit this event again, as a new Event. def reemit! agent.create_event :payload => payload, :lat => lat, :lng => lng @@ -79,4 +117,8 @@ class EventDrop @object.created_at } end + + def _location_ + @object.location + end end diff --git a/app/views/agents/agent_views/user_location_agent/_show.html.erb b/app/views/agents/agent_views/user_location_agent/_show.html.erb index f1756ad8..37555cd9 100644 --- a/app/views/agents/agent_views/user_location_agent/_show.html.erb +++ b/app/views/agents/agent_views/user_location_agent/_show.html.erb @@ -1,8 +1,11 @@ - +<% content_for :head do -%> +<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?sensor=false" %> +<%= javascript_include_tag "map_marker" %> +<% end -%>
No events found. diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 8bfa52ce..636e8254 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -16,7 +16,10 @@
<% if @event.lat && @event.lng %> - + <% content_for :head do -%> +<%= javascript_include_tag "https://maps.googleapis.com/maps/api/js?sensor=false" %> +<%= javascript_include_tag "map_marker" %> + <% end -%>
Lat:
@@ -36,9 +39,9 @@
};
var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
-
- <%= render "shared/map_marker", event: @event %>
+ map_marker(map, <%= Utils.jsonify(@event.location) %>);
+
<% end %>
diff --git a/app/views/shared/_map_marker.html.erb b/app/views/shared/_map_marker.html.erb
deleted file mode 100644
index e288a591..00000000
--- a/app/views/shared/_map_marker.html.erb
+++ /dev/null
@@ -1,61 +0,0 @@
-
\ No newline at end of file
diff --git a/config/environments/production.rb b/config/environments/production.rb
index a43a0775..51d0dc99 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -61,7 +61,7 @@ Huginn::Application.configure do
end
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added)
- config.assets.precompile += %w( diagram.js graphing.js user_credentials.js )
+ config.assets.precompile += %w( diagram.js graphing.js map_marker.js user_credentials.js )
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
diff --git a/lib/location.rb b/lib/location.rb
new file mode 100644
index 00000000..5e64cdcf
--- /dev/null
+++ b/lib/location.rb
@@ -0,0 +1,110 @@
+require 'liquid'
+
+Location = Struct.new(:lat, :lng, :radius, :speed, :course)
+
+class Location
+ include LiquidDroppable
+
+ protected :[]=
+
+ def initialize(data = {})
+ super()
+
+ case data
+ when Array
+ raise ArgumentError, 'unsupported location data' unless data.size == 2
+ self.lat, self.lng = data
+ when Hash, Location
+ data.each { |key, value|
+ case key.to_sym
+ when :lat, :latitude
+ self.lat = value
+ when :lng, :longitude
+ self.lng = value
+ when :radius
+ self.radius = value
+ when :speed
+ self.speed = value
+ when :course
+ self.course = value
+ end
+ }
+ else
+ raise ArgumentError, 'unsupported location data'
+ end
+
+ yield self if block_given?
+ end
+
+ def lat=(value)
+ self[:lat] = floatify(value) { |f|
+ if f.abs <= 90
+ f
+ else
+ raise ArgumentError, 'out of bounds'
+ end
+ }
+ end
+
+ alias latitude lat
+ alias latitude= lat=
+
+ def lng=(value)
+ self[:lng] = floatify(value) { |f|
+ if f.abs <= 180
+ f
+ else
+ raise ArgumentError, 'out of bounds'
+ end
+ }
+ end
+
+ alias longitude lng
+ alias longitude= lng=
+
+ def radius=(value)
+ self[:radius] = floatify(value) { |f| f if f >= 0 }
+ end
+
+ def speed=(value)
+ self[:speed] = floatify(value) { |f| f if f >= 0 }
+ end
+
+ def course=(value)
+ self[:course] = floatify(value) { |f| f if (0..360).cover?(f) }
+ end
+
+ def present?
+ lat && lng
+ end
+
+ def empty?
+ !present?
+ end
+
+ private
+
+ def floatify(value)
+ case value
+ when nil, ''
+ return nil
+ else
+ float = Float(value)
+ if block_given?
+ yield(float)
+ else
+ float
+ end
+ end
+ end
+end
+
+class LocationDrop
+ KEYS = Location.members.map(&:to_s).concat(%w[latitude longitude])
+
+ def before_method(key)
+ if KEYS.include?(key)
+ @object.__send__(key)
+ end
+ end
+end
diff --git a/spec/lib/location_spec.rb b/spec/lib/location_spec.rb
new file mode 100644
index 00000000..ea42fa40
--- /dev/null
+++ b/spec/lib/location_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Location do
+ let(:location) {
+ Location.new(
+ lat: BigDecimal.new('2.0'),
+ lng: BigDecimal.new('3.0'),
+ radius: 300,
+ speed: 2,
+ course: 30)
+ }
+
+ it "converts values to Float" do
+ expect(location.lat).to be_a Float
+ expect(location.lat).to eq 2.0
+ expect(location.lng).to be_a Float
+ expect(location.lng).to eq 3.0
+ expect(location.radius).to be_a Float
+ expect(location.radius).to eq 300.0
+ expect(location.speed).to be_a Float
+ expect(location.speed).to eq 2.0
+ expect(location.course).to be_a Float
+ expect(location.course).to eq 30.0
+ end
+
+ it "provides hash-style access to its properties with both symbol and string keys" do
+ expect(location[:lat]).to be_a Float
+ expect(location[:lat]).to eq 2.0
+ expect(location['lat']).to be_a Float
+ expect(location['lat']).to eq 2.0
+ end
+
+ it "does not allow hash-style assignment" do
+ expect {
+ location[:lat] = 2.0
+ }.to raise_error
+ end
+
+ it "ignores invalid values" do
+ location2 = Location.new(
+ lat: 2,
+ lng: 3,
+ radius: -1,
+ speed: -1,
+ course: -1)
+ expect(location2.radius).to be_nil
+ expect(location2.speed).to be_nil
+ expect(location2.course).to be_nil
+ end
+
+ it "considers a location empty if either latitude or longitude is missing" do
+ expect(Location.new.empty?).to be_truthy
+ expect(Location.new(lat: 2, radius: 1).present?).to be_falsy
+ expect(Location.new(lng: 3, radius: 1).present?).to be_falsy
+ end
+
+ it "is droppable" do
+ {
+ '{{location.lat}}' => '2.0',
+ '{{location.latitude}}' => '2.0',
+ '{{location.lng}}' => '3.0',
+ '{{location.longitude}}' => '3.0',
+ }.each { |template, result|
+ expect(Liquid::Template.parse(template).render('location' => location.to_liquid)).to eq(result),
+ "expected #{template.inspect} to expand to #{result.inspect}"
+ }
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index c2e40c03..e7fd4bb8 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -1,6 +1,50 @@
require 'spec_helper'
describe Event do
+ describe ".with_location" do
+ it "selects events with location" do
+ event = events(:bob_website_agent_event)
+ event.lat = 2
+ event.lng = 3
+ event.save!
+ Event.with_location.pluck(:id).should == [event.id]
+
+ event.lat = nil
+ event.save!
+ Event.with_location.should be_empty
+ end
+ end
+
+ describe "#location" do
+ it "returns a default hash when an event does not have a location" do
+ event = events(:bob_website_agent_event)
+ event.location.should == Location.new(
+ lat: nil,
+ lng: nil,
+ radius: 0.0,
+ speed: nil,
+ course: nil)
+ end
+
+ it "returns a hash containing location information" do
+ event = events(:bob_website_agent_event)
+ event.lat = 2
+ event.lng = 3
+ event.payload = {
+ radius: 300,
+ speed: 0.5,
+ course: 90.0,
+ }
+ event.save!
+ event.location.should == Location.new(
+ lat: 2.0,
+ lng: 3.0,
+ radius: 0.0,
+ speed: 0.5,
+ course: 90.0)
+ end
+ end
+
describe "#reemit" do
it "creates a new event identical to itself" do
events(:bob_website_agent_event).lat = 2
@@ -130,6 +174,8 @@ describe EventDrop do
'title' => 'some title',
'url' => 'http://some.site.example.org/',
}
+ @event.lat = 2
+ @event.lng = 3
@event.save!
end
@@ -166,4 +212,9 @@ describe EventDrop do
t = '{{created_at | date:"%FT%T%z" }}'
interpolate(t, @event).should eq(@event.created_at.strftime("%FT%T%z"))
end
+
+ it 'should have _location_' do
+ t = '{{_location_.lat}},{{_location_.lng}}'
+ interpolate(t, @event).should eq("2.0,3.0")
+ end
end