Skip to content

Commit

Permalink
begin server side clustering
Browse files Browse the repository at this point in the history
  • Loading branch information
Sébastien Gruhier committed May 13, 2012
1 parent 61a7315 commit 2550d48
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 27 deletions.
5 changes: 3 additions & 2 deletions app/controllers/medications_controller.rb
@@ -1,6 +1,6 @@
class MedicationsController < ApplicationController
before_filter :authenticate_user!, :only => [:create, :new]
before_filter :authenticate_admin, :only => [:list, :map, :edit, :update, :destroy]
before_filter :authenticate_admin, :only => [:list, :edit, :update, :destroy]

# GET /medications
# GET /medications.json
Expand Down Expand Up @@ -111,7 +111,8 @@ def list
end

def map
@prescriptions = Prescription.where(:path => {"$ne" => nil})
@medication = Medication.find_by_slug(params[:id])
@prescriptions = @medication.prescriptions.where(:path => {"$ne" => nil})
end

end
1 change: 1 addition & 0 deletions app/views/medications/_list.html.erb
Expand Up @@ -27,6 +27,7 @@

<td>
<%= link_to t(:show), medication_path(medication.slug) %> -
<%= link_to t(:map), map_medication_path(medication.slug) %> -
<%= link_to t(:"medications.add_effects"), new_medication_secondary_effect_path(medication.slug) %>
<% if admin_mode %>
Expand Down
8 changes: 0 additions & 8 deletions app/views/medications/list.html.erb
@@ -1,12 +1,4 @@
<h2>Medication listing</h2>
<ul class="nav nav-tabs">
<li class="active">
<%= link_to "List", list_medications_path %>
</li>
<li>
<%= link_to "Map", map_medications_path %>
</li>
</ul>

<%= render 'list', admin_mode: true %>
<%= link_to 'Or add a new medication', new_medication_path, :class => "link" %>
10 changes: 1 addition & 9 deletions app/views/medications/map.html.erb
@@ -1,12 +1,4 @@
<h2>Medication listing</h2>
<ul class="nav nav-tabs">
<li>
<%= link_to "List", list_medications_path %>
</li>
<li class="active">
<%= link_to "Map", map_medications_path %>
</li>
</ul>
<h2>Medication map of <%= @medication.name %></h2>
<div class="container-fluid">
<div class="row-fluid">
<div class="span2">
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Expand Up @@ -12,6 +12,8 @@
collection do
get :search
get :list
end
member do
get :map
end
resources :secondary_effects
Expand Down
106 changes: 106 additions & 0 deletions lib/bbphealth/clustering.rb
@@ -0,0 +1,106 @@
require 'bbphealth/mercator_projection'
module BBPHealth
module Clustering
MAP_METHOD = <<-MAP
emit(this.path.substr(0, resolution),
{sw_lat: this.lat,
sw_lng: this.lng,
ne_lat: this.lat,
ne_lng: this.lng,
count: 1 });
MAP

REDUCE_METHOD = <<-REDUCE
function reduce(key, values) {
var value = values[0], result = { count: 0, sw_lat: value.sw_lat, sw_lng: value.sw_lng, ne_lat: value.ne_lat, ne_lng: value.ne_lng};
for (var i = values.length - 1; i>=0; i--) {
var value = values[i];
result.sw_lat = Math.min(result.sw_lat, value.sw_lat);
result.sw_lng = Math.min(result.sw_lng, value.sw_lng);
result.ne_lat = Math.max(result.ne_lat, value.ne_lat);
result.ne_lng = Math.max(result.ne_lng, value.ne_lng);
result.count += value.count;
}
return result;
}
REDUCE

FINALIZE_METHOD = <<-FINALIZE
function(obj, val) {
if (val.count == 1) {
return {count: 1, id: val.id, lat: val.sw_lat, lng: sw_lng}
} else {
return {
lat: (val.sw_lat + val.ne_lat) / 2,
lng: (val.sw_lng + val.ne_lng) / 2,
count: val.count ,
sw_lat: val.sw_lat,
sw_lng: val.sw_lng,
ne_lat: val.ne_lat,
ne_lng: val.ne_lng
}
}
}
FINALIZE

def clusterize_response(params)
viewport = params["viewport"].split(',').map &:to_i
ne = params["ne"].split(',').map &:to_f
sw = params["sw"].split(',').map &:to_f

projection = MercatorProjection.new(MercatorProjection.zoom_for(ne[1] - sw[1], viewport[0]))
grouping_distance = (params["groupingDistance"] || 20).to_i
query = bounds_conditions(sw, ne)
result = Prescription.collection.map_reduce(MAP_METHOD, REDUCE_METHOD,
finalize: FINALIZE_METHOD,
out: {inline: true},
raw: true,
query: query,
scope: {resolution: projection.resolution_for(grouping_distance)})
result["results"].map! { |p| p["value"] }
puts projection.resolution_for(grouping_distance).inspect
puts result.inspect
response = perform_further_grouping(projection, grouping_distance, result["results"])
response[:success] = true
response
end

private
def bounds_conditions(sw, ne)
{ "lat" => { "$gte" => sw[0] - 0.00001, "$lte" => ne[0] + 0.00001 }, "lng" => { "$gte" => sw[1] - 0.00001, "$lte" => ne[1] + 0.00001 } }
end

def perform_further_grouping(projection, distance, data)
data.sort! {|x,y| x["lat"] <=> y["lat"] }
i = 0
while i < data.length
j = i - 1
current = data[i]
while j >= 0
previous = data[j]
j -= 1
break if projection.vertical_distance(previous["lat"], current["lat"]) > distance
next if projection.horizontal_distance(previous["lng"], current["lng"]) > distance
add_marker(current, previous)
data.delete(previous)
end
i += 1
end
data
end

def merge_marker(source, dest)
# Update count
source["count"] += dest["count"]
source["lat"] = (source["lat"] * source["count"] + dest["lat"] * dest["count"]) / (source["count"] + dest["count"])
source["lng"] = (source["lng"] * source["count"] + dest["lng"] * dest["count"]) / (source["count"] + dest["count"])

source["sw_lat"] = [source["sw_lat"] || source["lat"], dest["sw_lat"] || dest["lat"]].min
source["sw_lng"] = [source["sw_lng"] || source["lng"], dest["sw_lng"] || dest["lng"]].min
source["ne_lat"] = [source["ne_lat"] || source["lat"], dest["ne_lat"] || dest["lat"]].max
source["ne_lng"] = [source["ne_lng"] || source["lng"], dest["ne_lng"] || dest["lng"]].max

source.delete("id")
end
end
end
2 changes: 2 additions & 0 deletions lib/bbphealth/mercator_projection.rb
@@ -1,3 +1,4 @@
module BBPHealth
class MercatorProjection
TILE_SIZE_PX = 256
MAX_LAT = 85.05113
Expand Down Expand Up @@ -38,3 +39,4 @@ def lat_to_y(lat)
@earth_half_radius_px * Math.log((1 + x) / (1 - x))
end
end
end
34 changes: 34 additions & 0 deletions spec/lib/clustering_spec.rb
@@ -0,0 +1,34 @@
require 'spec_helper'
require 'bbphealth/clustering'

describe BBPHealth::Clustering do
class Engine
include BBPHealth::Clustering
end

describe "clusterize" do
before(:each) do
@medication = create(:medication)
@prescription1 = create(:prescription, medication: @medication, :lat => 10, :lng => 10),
@prescription2 = create(:prescription, medication: @medication, :lat => 12, :lng => 12),

@engine = Engine.new
@params = {"ne"=>"89.988124,179.999999", "sw"=>"-89.97661,-179.999999", "viewport" => "600,600", "callback"=>"com.maptimize.callbacks._0", "groupingDistance"=>"30"}
end

it "should clusterize properties #1" do
response = @engine.clusterize_response @params
response[:success].should be_true
response[:markers].should == []
response[:clusters].should == [{:coords=>"11.333333333333334, 11.333333333333334", :count=>2.0, :bounds=>{:ne=>"12.0, 12.0", :sw=>"10.0, 10.0"}}]
end

it "should clusterize properties #2" do
response = @engine.clusterize_response @params.merge("sw"=>"9,9", "ne"=>"13, 13")
response[:success].should be_true
response[:markers].should =~ [{:coords => "10.0, 10.0", :id => @prescription1.id},
{:coords => "12.0, 12.0", :id => @prescription2.id}]
response[:clusters].should == []
end
end
end
17 changes: 9 additions & 8 deletions spec/lib/mercator_projection_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
require 'bbphealth/mercator_projection'

describe MercatorProjection do
describe BBPHealth::MercatorProjection do
it "should get resolution for grouping distance zoom 0" do
p = MercatorProjection.new(0)
p = BBPHealth::MercatorProjection.new(0)
p.resolution_for(256).should == 0
p.resolution_for(192).should == 0
p.resolution_for(168).should == 0
Expand All @@ -15,7 +16,7 @@
end

it "should get resolution for grouping distance zoom 3" do
p = MercatorProjection.new(3)
p = BBPHealth::MercatorProjection.new(3)
p.resolution_for(256).should == 3
p.resolution_for(192).should == 3
p.resolution_for(168).should == 3
Expand All @@ -28,22 +29,22 @@
end

it "should get zoom from bouds/viewport with" do
MercatorProjection.zoom_for(0.49713134765625, 1446).should == 12
BBPHealth::MercatorProjection.zoom_for(0.49713134765625, 1446).should == 12
end

it "should get horizontal distance" do
p = MercatorProjection.new(12)
p = BBPHealth::MercatorProjection.new(12)
p.horizontal_distance(7.008769, 7.505901).should == 1448

p = MercatorProjection.new(10)
p = BBPHealth::MercatorProjection.new(10)
p.horizontal_distance(6.263072, 8.251598).should == 1448
end

it "should get vertical distance" do
p = MercatorProjection.new(12)
p = BBPHealth::MercatorProjection.new(12)
p.vertical_distance(43.641667, 43.79105).should == 602

p = MercatorProjection.new(10)
p = BBPHealth::MercatorProjection.new(10)
p.vertical_distance(43.416896, 44.014424).should == 602
end
end

0 comments on commit 2550d48

Please sign in to comment.