Skip to content

Commit c36f0cd

Browse files
committed
projects API: use ?include=geometry to get the geojson #94
- also: use ST_AsGeoJson(geom) to let the DB do the to-JSON conversion, hoping that this is faster than RGeo. I also integrated that into the issues API.
1 parent aa7a09e commit c36f0cd

File tree

12 files changed

+149
-19
lines changed

12 files changed

+149
-19
lines changed

app/views/issues/index.api.rsb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :l
2121
api.estimated_hours issue.estimated_hours
2222

2323
if issue.geom
24-
api.geojson issue.geojson
24+
api.geojson issue.geojson.to_json
2525
else
2626
api.geojson ""
2727
end

app/views/projects/index.api.rsb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ api.array :projects, api_meta(:total_count => @project_count, :offset => @offset
99
api.status project.status
1010
api.is_public project.is_public?
1111

12-
if project.geom
13-
api.geojson project.geojson
14-
else
15-
api.geojson ""
12+
if @include_geometry
13+
if project.geom
14+
api.geojson project.geojson.to_json
15+
else
16+
api.geojson ""
17+
end
1618
end
1719

1820
render_api_custom_values project.visible_custom_field_values, api

lib/redmine_gtt/conversions.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ def call(wkb)
4848
end
4949
end
5050

51+
def self.to_feature(geometry, properties: {})
52+
geometry = JSON.parse geometry if geometry.is_a?(String)
53+
{
54+
'type' => 'Feature',
55+
'geometry' => geometry,
56+
'properties' => properties
57+
}
58+
end
5159

5260
# Turns database WKB into geometry attribute string
5361
def self.wkb_to_json(wkb, id: nil, properties: nil)

lib/redmine_gtt/patches/geojson_attribute.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@ module GeojsonAttribute
77
def self.prepended(base)
88
base.extend ClassMethods
99
base.class_eval do
10+
# if this wouldnt break count, this would be really nice:
11+
#default_scope ->{ select "#{table_name}.*, ST_AsGeoJson(#{table_name}.geom) as geojson" }
1012
scope :geojson, ->(include_properties: false){
1113
data = []
1214
where.not(geom: nil).
13-
find_in_batches.each{|group| group.each{|o|
15+
select("#{table_name}.*, #{geojson_attribute_select}").
16+
find_each{|o|
1417
data << o.geojson_params(include_properties)
15-
}}
18+
}
1619
Conversions::GeomToJson.new.collection_to_json data
1720
}
1821
end
1922
end
2023

2124
module ClassMethods
25+
26+
def geojson_attribute_select
27+
"ST_AsGeoJson(#{table_name}.geom) as db_geojson"
28+
end
29+
2230
def array_to_geojson(array, include_properties: false)
2331
Conversions::GeomToJson.new.collection_to_json(
2432
array.map{ |o|
@@ -47,9 +55,12 @@ def geojson_additional_properties(include_properties = false)
4755

4856
# returns the geojson attribute for reading / writing to the DB
4957
def geojson
50-
@geojson ||= if geom.present?
51-
Conversions.geom_to_json geom
52-
end
58+
@geojson ||= if respond_to?(:db_geojson) && db_geojson.present?
59+
# use the value returned by ST_AsGeoJson
60+
Conversions.to_feature db_geojson
61+
elsif geom.present?
62+
Conversions.geom_to_json geom
63+
end
5364
end
5465

5566
# sets the geojson attribute for reading / writing to the DB

lib/redmine_gtt/patches/issue_patch.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ def self.apply
77
unless Issue < self
88
Issue.prepend self
99
Issue.prepend GeojsonAttribute
10+
Issue.extend ClassMethods
1011
Issue.class_eval do
1112
attr_reader :distance
1213
safe_attributes "geojson",
1314
if: ->(issue, user){
1415
perm = issue.new_record? ? :add_issues : :edit_issues
1516
user.allowed_to? perm, issue.project
1617
}
18+
1719
end
1820
end
1921
end
@@ -24,6 +26,23 @@ def map
2426
bounds: (new_record? ? project.map.json : json)
2527
end
2628

29+
module ClassMethods
30+
def load_geojson(issues)
31+
if issues.any?
32+
geometries_by_id = Hash[
33+
Issue.
34+
where(id: issues.map(&:id)).
35+
pluck(:id, Issue.geojson_attribute_select)
36+
]
37+
issues.each do |issue|
38+
issue.instance_variable_set(
39+
"@geojson", Conversions.to_feature(geometries_by_id[issue.id])
40+
)
41+
end
42+
end
43+
end
44+
end
45+
2746
end
2847

2948
end

lib/redmine_gtt/patches/issue_query_patch.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
module RedmineGtt
24
module Patches
35
module IssueQueryPatch
@@ -13,6 +15,9 @@ def self.prepended(base)
1315

1416
def issues(*_)
1517
super.tap do |issues|
18+
if load_geojson? || has_column?(:geom)
19+
Issue.load_geojson(issues)
20+
end
1621
if center = find_center_point
1722
load_distances(issues, center)
1823
end
@@ -21,6 +26,13 @@ def issues(*_)
2126
raise ::Query::StatementInvalid.new(e.message)
2227
end
2328

29+
def load_geojson
30+
@load_geojson = true
31+
end
32+
def load_geojson?
33+
!!@load_geojson
34+
end
35+
2436
def available_columns
2537
return @available_columns if @available_columns
2638

@@ -75,7 +87,7 @@ def sql_for_distance_field(field, operator, value)
7587
# or ['meters_min', 'meters_max', 'lng', 'lat'] if op == '><'
7688
lng, lat = value.last(2).map(&:to_f)
7789
distance = value.first.to_i
78-
sql = "ST_Distance_Sphere(#{Issue.table_name}.geom, ST_GeomFromText('POINT(#{lng} #{lat})',4326))"
90+
sql = +"ST_Distance_Sphere(#{Issue.table_name}.geom, ST_GeomFromText('POINT(#{lng} #{lat})',4326))"
7991
if operator == '><'
8092
distance_max = value[1].to_i
8193
sql << " BETWEEN #{distance} AND #{distance_max}"

lib/redmine_gtt/patches/issues_controller_patch.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def show
2222
end
2323
end
2424

25+
def retrieve_query(*_)
26+
return @query if @query
27+
super
28+
end
29+
private :retrieve_query
2530

2631
def index
2732
retrieve_query
@@ -36,6 +41,7 @@ def index
3641
:filename => "issues.geojson"
3742
)
3843
}
44+
format.api { @query.load_geojson; super }
3945
format.any { super }
4046
end
4147
else

lib/redmine_gtt/patches/projects_controller_patch.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ def self.apply
1010
def index
1111
respond_to do |format|
1212
format.api {
13-
scope = RedmineGtt::SpatialProjectsQuery.new(
13+
@include_geometry = params[:include] == 'geometry'
14+
query = RedmineGtt::SpatialProjectsQuery.new(
1415
contains: params[:contains],
16+
geometry: @include_geometry,
1517
projects: Project.visible.sorted
16-
).scope
17-
@project_count = scope.count
18+
)
19+
scope = query.scope
20+
@project_count = query.count
1821
@offset, @limit = api_offset_and_limit
1922
@projects = scope.offset(@offset).limit(@limit).to_a
2023
}

lib/redmine_gtt/spatial_projects_query.rb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,43 @@
1+
# frozen_string_literal: true
2+
13
module RedmineGtt
24
class SpatialProjectsQuery
3-
def initialize(contains: nil, projects: Project.active.visible)
5+
def initialize(contains: nil, projects: Project.active.visible, geometry: true)
46
@contains = contains.presence
57
@projects = projects
8+
@include_geometry = geometry
69
end
710

8-
QUERY_SQL = (<<-SQL
11+
QUERY_SQL = <<-SQL
912
#{Project.table_name}.geom IS NOT NULL AND
1013
ST_Intersects(#{Project.table_name}.geom, ST_GeomFromText('%s', 4326))
1114
SQL
12-
).freeze
15+
16+
VIRTUAL_GEOJSON_ATTRIBUTE_SELECT = <<-SQL
17+
#{Project.table_name}.*, #{Project.geojson_attribute_select}
18+
SQL
19+
20+
def count
21+
# we have to pass :all, otherwise we hit this rails issue due to the
22+
# select call in build_scope: https://github.com/rails/rails/issues/15138
23+
scope.count :all
24+
end
1325

1426
def scope
27+
@scope ||= build_scope
28+
end
29+
30+
private
31+
32+
def build_scope
1533
if @contains
1634
@projects = @projects.where(
1735
Project.send(:sanitize_sql_array, [ QUERY_SQL, @contains ])
1836
)
1937
end
38+
if @include_geometry
39+
@projects = @projects.select VIRTUAL_GEOJSON_ATTRIBUTE_SELECT
40+
end
2041
@projects
2142
end
2243

test/integration/projects_api_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,39 @@ class ProjectsApiTest < Redmine::IntegrationTest
4747
assert projects = xml.xpath('/projects/project')
4848
assert_equal 1, projects.size, xml.to_s
4949
assert_equal 'ecookbook', projects.xpath('identifier').text
50+
assert_equal 0, projects.xpath('geojson').size, projects.to_s
5051

5152
end
5253

54+
test 'should include geojson on demand' do
55+
geo = {
56+
'type' => 'Feature',
57+
'geometry' => {
58+
'type' => 'Polygon',
59+
'coordinates' => [
60+
[[123.269691,9.305099], [123.279691,9.305099],[123.279691,9.405099],[123.269691,9.405099], [123.269691,9.305099]]
61+
]
62+
}
63+
}
64+
geojson = geo.to_json
65+
66+
@project.update_attribute :geojson, geojson
67+
get '/projects.xml?include=geometry'
68+
assert_response :success
69+
xml = xml_data
70+
assert projects = xml.xpath('/projects/project')
71+
assert json = projects.xpath('geojson').text
72+
assert json.present?
73+
assert_match(/123\.269691/, json)
74+
assert_equal geo['geometry'], JSON.parse(json)['geometry'], json
75+
76+
get '/projects.json?include=geometry'
77+
assert_response :success
78+
assert json = JSON.parse(@response.body)
79+
hsh = JSON.parse json['projects'].detect{|p|p['id'] == @project.id}['geojson']
80+
assert_equal geo['geometry'], hsh['geometry']
81+
end
82+
5383
def xml_data
5484
Nokogiri::XML(@response.body)
5585
end

0 commit comments

Comments
 (0)