gb_mapfish_appserver 1.1.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +5 -2
- data/app/controllers/application_controller.rb +1 -1
- data/app/controllers/apps_controller.rb +3 -1
- data/app/controllers/geo_controller.rb +164 -4
- data/app/controllers/print_controller.rb +9 -3
- data/app/controllers/search_controller.rb +15 -3
- data/app/controllers/topics_controller.rb +7 -4
- data/app/controllers/wms_controller.rb +6 -5
- data/app/models/geo_model.rb +319 -31
- data/app/models/group.rb +1 -1
- data/app/models/layer.rb +9 -9
- data/app/models/locate_rule.rb +3 -3
- data/app/models/search_model.rb +3 -3
- data/app/models/topic.rb +2 -2
- data/app/models/user.rb +2 -2
- data/app/views/groups_users/show_group.html.erb +1 -1
- data/config/initializers/devise.rb +0 -2
- data/gb_mapfish_appserver.gemspec +7 -3
- data/lib/gb_mapfish_appserver/core_extensions.rb +22 -1
- data/lib/gb_mapfish_appserver/engine.rb +21 -0
- data/lib/gb_mapfish_appserver/version.rb +1 -1
- data/lib/generators/mapfish/install/templates/search_rules.rb +2 -0
- data/test/dummy/config/environments/development.rb +1 -0
- data/test/dummy/config/environments/production.rb +1 -0
- data/test/dummy/config/environments/test.rb +1 -0
- data/test/dummy/config/initializers/mapfish.rb +5 -0
- data/test/dummy/config/initializers/search_rules.rb +2 -0
- metadata +112 -72
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4c0bd8f62eddc656b9b0e8d45fa7b24bc2e60ed8
|
4
|
+
data.tar.gz: e3a565123819e15f9b4392cacdfab3962042911c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c5d4d4cb3e680b9eaf524938a5d464d7047ca588b902f2900aac056b9a306692982f74b0867591d056b0cae6b7c1871cf0a1768af44a5012475d8c61441a13cd
|
7
|
+
data.tar.gz: 1e5e2b251baf0d7658a1f67b1ae475214ece3edd9aabce9a17b37b6cd3f65d71f8cc2f19623239b8909a8f39c9a3532b27b124f9b9fa9b7701c41dff6e39d834
|
data/README.md
CHANGED
@@ -17,6 +17,7 @@ Mapfish Appserver comes with the following out-of-the box features:
|
|
17
17
|
- Fully customizable legends and feature infos
|
18
18
|
- Creation of complex custom searches
|
19
19
|
- Rich digitizing and editing functionality
|
20
|
+
- Support for differing client and server spatial reference systems
|
20
21
|
- Role-based access control on topic, layer and attribute level
|
21
22
|
- Access control for WMS and WFS
|
22
23
|
- Rich library of ExtJS 4 based map components
|
@@ -31,7 +32,9 @@ Documentation
|
|
31
32
|
|
32
33
|
For more documentation see the [mapfish-appserver.github.io](http://mapfish-appserver.github.io/)
|
33
34
|
|
34
|
-
Note: Mapfish Appserver
|
35
|
+
Note: Mapfish Appserver v2.0.0 requires Ruby 2.3 or greater
|
36
|
+
|
37
|
+
Note: Mapfish Appserver v1.1.0 or greater no longer uses the gb_mapfish_print gem, but uses a
|
35
38
|
separately installed Mapfish Print v3 for printing (see documentation)
|
36
39
|
|
37
40
|
Authors and License
|
@@ -42,4 +45,4 @@ Stefan Zinggeler and Adrian Herzog, Canton of Zurich.
|
|
42
45
|
|
43
46
|
New BSD License
|
44
47
|
|
45
|
-
*Copyright (c) 2009-
|
48
|
+
*Copyright (c) 2009-2017 Sourcepole AG & Canton of Zurich*
|
@@ -43,7 +43,7 @@ class ApplicationController < ActionController::Base
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def current_roles
|
46
|
-
@
|
46
|
+
@current_roles_ ||= Ability::Roles.new(current_user, host_zone(request.host))
|
47
47
|
end
|
48
48
|
|
49
49
|
# Overide CanCan method
|
@@ -17,6 +17,8 @@ class AppsController < ApplicationController
|
|
17
17
|
@x = params['x'].nil? ? DEFAULT_X : params['x'].to_f
|
18
18
|
@y = params['y'].nil? ? DEFAULT_Y : params['y'].to_f
|
19
19
|
|
20
|
+
@client_srid = params[:srid].blank? ? GeoModel.default_client_srid : params[:srid].to_i
|
21
|
+
|
20
22
|
@zoom = params['zoom'].nil? ? DEFAULT_ZOOM : params['zoom'].to_i # for mobile
|
21
23
|
@gbapp = params['gbapp'].nil? ? 'default' : params['gbapp'] # for mobile
|
22
24
|
|
@@ -40,7 +42,7 @@ class AppsController < ApplicationController
|
|
40
42
|
if rule.nil?
|
41
43
|
logger.info "Locate rule not found: {params['locate']}"
|
42
44
|
else
|
43
|
-
location = rule.locate(params['locations'])
|
45
|
+
location = rule.locate(params['locations'], @client_srid)
|
44
46
|
unless location.nil?
|
45
47
|
@seltopic = location[:selection][:topic] || @topic_name
|
46
48
|
@sellayer = location[:selection][:layer]
|
@@ -7,12 +7,14 @@ class GeoController < ApplicationController
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def index
|
10
|
+
client_srid = params[:srid].blank? ? geo_model.default_client_srid : params[:srid].to_i
|
11
|
+
|
10
12
|
@features = geo_model.bbox_filter(params)
|
11
13
|
@features = @features.user_filter(current_ability)
|
12
14
|
respond_to do |format|
|
13
15
|
# NOTE: return GeoJSON by default (OpenLayers.Protocol.HTTP PUT does not work with '.json' format selection)
|
14
16
|
format.html {
|
15
|
-
render :json => @features.json_filter.to_geojson
|
17
|
+
render :json => @features.json_filter.select_geojson_geom(client_srid).to_geojson
|
16
18
|
}
|
17
19
|
format.csv {
|
18
20
|
send_csv_excel(@features.csv_filter)
|
@@ -21,11 +23,161 @@ class GeoController < ApplicationController
|
|
21
23
|
end
|
22
24
|
|
23
25
|
def show
|
24
|
-
|
26
|
+
client_srid = params[:srid].blank? ? geo_model.default_client_srid : params[:srid].to_i
|
27
|
+
|
28
|
+
@feature = geo_model.user_filter(current_ability).json_filter.select_geojson_geom(client_srid).find(params[:id])
|
25
29
|
render :json => [@feature].to_geojson
|
26
30
|
end
|
27
31
|
|
28
32
|
def create
|
33
|
+
if params[:editv2]
|
34
|
+
create_v2
|
35
|
+
else
|
36
|
+
create_v1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def update
|
41
|
+
if params[:editv2]
|
42
|
+
update_v2
|
43
|
+
else
|
44
|
+
update_v1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def destroy
|
49
|
+
if params[:editv2]
|
50
|
+
destroy_v2
|
51
|
+
else
|
52
|
+
destroy_v1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_v2
|
57
|
+
unless geo_model.can_edit?(current_ability)
|
58
|
+
render :json => {
|
59
|
+
:error => "Forbidden"
|
60
|
+
}, :status => :forbidden
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
geojson_data = request.raw_post
|
65
|
+
begin
|
66
|
+
feature_collection = geo_model.geojson_decode(geojson_data)
|
67
|
+
rescue => err
|
68
|
+
# JSON parse error
|
69
|
+
render :json => {
|
70
|
+
:error => "Invalid JSON: #{err.message}"
|
71
|
+
}, :status => :bad_request
|
72
|
+
return
|
73
|
+
end
|
74
|
+
error = geo_model.validate_feature_collection(feature_collection, geojson_data)
|
75
|
+
unless error.nil?
|
76
|
+
render :json => error, :status => :bad_request
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
@features = []
|
81
|
+
feature_collection.each do |feature|
|
82
|
+
if feature.feature_id.is_a? Integer
|
83
|
+
begin
|
84
|
+
new_feature = geo_model.find(feature.feature_id)
|
85
|
+
rescue ActiveRecord::RecordNotFound => err
|
86
|
+
render :json => {
|
87
|
+
:error => "Feature ID not found"
|
88
|
+
}, :status => :not_found
|
89
|
+
return
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if new_feature.nil?
|
93
|
+
new_feature = geo_model.new
|
94
|
+
end
|
95
|
+
|
96
|
+
logger.info "#{geo_model.table_name}.update_attributes_from_feature: #{feature.inspect}"
|
97
|
+
if new_feature.update_attributes_from_geojson_feature(feature, current_user)
|
98
|
+
# transform geometry to SRID from GeoJSON feature
|
99
|
+
client_srid = feature.geometry.nil? ? geo_model.default_client_srid : feature.geometry.srid
|
100
|
+
new_feature = geo_model.json_filter.select_geojson_geom(client_srid).find(new_feature.id)
|
101
|
+
|
102
|
+
@features << new_feature
|
103
|
+
else
|
104
|
+
render :json => {
|
105
|
+
:error => "Feature validation failed",
|
106
|
+
:validation_errors => new_feature.errors.full_messages
|
107
|
+
}, :status => :unprocessable_entity
|
108
|
+
return
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
render :json => @features.to_geojson, :status => :created
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_v2
|
116
|
+
unless geo_model.can_edit?(current_ability)
|
117
|
+
render :json => {
|
118
|
+
:error => "Forbidden"
|
119
|
+
}, :status => :forbidden
|
120
|
+
return
|
121
|
+
end
|
122
|
+
|
123
|
+
geojson_data = request.raw_post
|
124
|
+
begin
|
125
|
+
feature = geo_model.geojson_decode(geojson_data)
|
126
|
+
rescue => err
|
127
|
+
# JSON parse error
|
128
|
+
render :json => {
|
129
|
+
:error => "Invalid JSON: #{err.message}"
|
130
|
+
}, :status => :bad_request
|
131
|
+
return
|
132
|
+
end
|
133
|
+
error = geo_model.validate_feature(feature, geojson_data)
|
134
|
+
unless error.nil?
|
135
|
+
render :json => error, :status => :bad_request
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
if feature.feature_id.is_a? Integer
|
140
|
+
begin
|
141
|
+
# NOTE: user_filter may limit editable features; find raises RecordNotFound if feature cannot be found
|
142
|
+
@feature = geo_model.user_filter(current_ability).find(feature.feature_id)
|
143
|
+
rescue ActiveRecord::RecordNotFound => err
|
144
|
+
render :json => {
|
145
|
+
:error => "Feature ID not found"
|
146
|
+
}, :status => :not_found
|
147
|
+
return
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
if @feature.update_attributes_from_geojson_feature(feature, current_user)
|
152
|
+
# transform geometry to SRID from GeoJSON feature
|
153
|
+
client_srid = feature.geometry.nil? ? geo_model.default_client_srid : feature.geometry.srid
|
154
|
+
@feature = geo_model.json_filter.select_geojson_geom(client_srid).find(@feature.id)
|
155
|
+
|
156
|
+
render :json => @feature.to_geojson, :status => :created
|
157
|
+
else
|
158
|
+
render :json => {
|
159
|
+
:error => "Feature validation failed",
|
160
|
+
:validation_errors => @feature.errors.full_messages
|
161
|
+
}, :status => :unprocessable_entity
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def destroy_v2
|
166
|
+
begin
|
167
|
+
@feature = geo_model.user_filter(current_ability).find(params[:id])
|
168
|
+
rescue ActiveRecord::RecordNotFound => err
|
169
|
+
render :json => {
|
170
|
+
:error => "Feature ID not found"
|
171
|
+
}, :status => :not_found
|
172
|
+
return
|
173
|
+
end
|
174
|
+
@feature.destroy
|
175
|
+
render :json => {
|
176
|
+
:message => "Feature removed"
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
def create_v1
|
29
181
|
@features = []
|
30
182
|
feature_collection = geo_model.geojson_decode(request.raw_post)
|
31
183
|
if feature_collection.nil? || !geo_model.can_edit?(current_ability)
|
@@ -43,6 +195,10 @@ class GeoController < ApplicationController
|
|
43
195
|
|
44
196
|
logger.info "#{geo_model.table_name}.update_attributes_from_feature: #{feature.inspect}"
|
45
197
|
if new_feature.update_attributes_from_geojson_feature(feature, current_user)
|
198
|
+
# transform geometry to SRID from GeoJSON feature
|
199
|
+
client_srid = feature.geometry.nil? ? geo_model.default_client_srid : feature.geometry.srid
|
200
|
+
new_feature = geo_model.json_filter.select_geojson_geom(client_srid).find(new_feature.id)
|
201
|
+
|
46
202
|
@features << new_feature
|
47
203
|
else
|
48
204
|
head :unprocessable_entity
|
@@ -53,7 +209,7 @@ class GeoController < ApplicationController
|
|
53
209
|
render :json => @features.to_geojson, :status => :created
|
54
210
|
end
|
55
211
|
|
56
|
-
def
|
212
|
+
def update_v1
|
57
213
|
# NOTE: user_filter checks if model is editable
|
58
214
|
feature = geo_model.user_filter(current_ability).geojson_decode(request.raw_post)
|
59
215
|
if feature.nil?
|
@@ -67,13 +223,17 @@ class GeoController < ApplicationController
|
|
67
223
|
end
|
68
224
|
|
69
225
|
if @feature.update_attributes_from_geojson_feature(feature, current_user)
|
226
|
+
# transform geometry to SRID from GeoJSON feature
|
227
|
+
client_srid = feature.geometry.nil? ? geo_model.default_client_srid : feature.geometry.srid
|
228
|
+
@feature = geo_model.json_filter.select_geojson_geom(client_srid).find(@feature.id)
|
229
|
+
|
70
230
|
render :json => @feature.to_geojson, :status => :created
|
71
231
|
else
|
72
232
|
head :unprocessable_entity
|
73
233
|
end
|
74
234
|
end
|
75
235
|
|
76
|
-
def
|
236
|
+
def destroy_v1
|
77
237
|
@feature = geo_model.user_filter(current_ability).find(params[:id])
|
78
238
|
@feature.destroy
|
79
239
|
head :no_content
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
class PrintController < ApplicationController
|
4
4
|
begin
|
5
|
-
require '
|
5
|
+
require 'rmagick'
|
6
6
|
rescue LoadError
|
7
|
-
ActionController::Base.logger.info "Couldn't find
|
7
|
+
ActionController::Base.logger.info "Couldn't find rmagick. Image export not supported"
|
8
8
|
end
|
9
9
|
|
10
10
|
skip_before_filter :verify_authenticity_token, :only => :create # allow /print/create with POST
|
@@ -392,6 +392,7 @@ class PrintController < ApplicationController
|
|
392
392
|
# convert PDF to image if not supported by Mapfish Print
|
393
393
|
pdf = Magick::Image.read(temp_mapfish) { self.density = print_params['dpi'] }.first
|
394
394
|
temp_img = "#{TMP_PREFIX}#{temp_id.to_s}.#{output_format}"
|
395
|
+
pdf.alpha(Magick::RemoveAlphaChannel)
|
395
396
|
pdf.write(temp_img)
|
396
397
|
File.delete(temp_mapfish)
|
397
398
|
temp = temp_img
|
@@ -409,9 +410,13 @@ class PrintController < ApplicationController
|
|
409
410
|
when 'GET'
|
410
411
|
# add params to URL
|
411
412
|
url = URI.parse("#{url}?#{print_params.to_param}") unless print_params.nil?
|
412
|
-
|
413
|
+
http = Net::HTTP.new(url.host, url.port)
|
414
|
+
http.read_timeout = 300
|
415
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
416
|
+
response = http.request(req)
|
413
417
|
when 'POST'
|
414
418
|
http = Net::HTTP.new(url.host, url.port)
|
419
|
+
http.read_timeout = 300
|
415
420
|
req = Net::HTTP::Post.new(url.path)
|
416
421
|
req.set_form_data(print_params)
|
417
422
|
response = http.request(req)
|
@@ -573,6 +578,7 @@ class PrintController < ApplicationController
|
|
573
578
|
end
|
574
579
|
call_params[:MAP_BBOX] = page["extent"].join(',')
|
575
580
|
call_params[:MAP_CENTER] = page["center"].join(',')
|
581
|
+
call_params[:MAP_SRS] = request.parameters["srs"]
|
576
582
|
end
|
577
583
|
request.parameters.each do |name, val|
|
578
584
|
if name =~ /^REP_/
|
@@ -1,14 +1,18 @@
|
|
1
1
|
class SearchController < ApplicationController
|
2
2
|
|
3
3
|
def index
|
4
|
-
@rule =
|
4
|
+
@rule = SEARCHRULES[params[:rule]]
|
5
5
|
if @rule.nil? then
|
6
6
|
respond_to do |format|
|
7
7
|
format.html # index.html.erb
|
8
8
|
format.json { render :json => {:success => false, :quality => -9999, :msg => "ERROR: #{params[:rule]} model missing"} }
|
9
9
|
end
|
10
10
|
else
|
11
|
-
|
11
|
+
# transform geometry fields to client SRID
|
12
|
+
client_srid = params[:srid].blank? ? GeoModel.default_client_srid : params[:srid].to_i
|
13
|
+
fields = transformed_geom_fields(@rule.fields, @rule.model.srid, client_srid)
|
14
|
+
|
15
|
+
result = @rule.model.query(fields, params)
|
12
16
|
@features = result[:features]
|
13
17
|
@quality = result[:quality]
|
14
18
|
@success = @quality >= 0
|
@@ -30,7 +34,8 @@ class SearchController < ApplicationController
|
|
30
34
|
if rule.nil?
|
31
35
|
render :json => {:success => false, :msg => "ERROR: #{params[:rule]} model missing"}
|
32
36
|
else
|
33
|
-
|
37
|
+
client_srid = params[:srid].blank? ? GeoModel.default_client_srid : params[:srid].to_i
|
38
|
+
location = rule.locate(params['locations'], client_srid)
|
34
39
|
unless location.nil?
|
35
40
|
location[:success] = true
|
36
41
|
render :json => location
|
@@ -76,6 +81,13 @@ class SearchController < ApplicationController
|
|
76
81
|
|
77
82
|
private
|
78
83
|
|
84
|
+
# replace "*geom*" in fields with SQL for transformed geometry
|
85
|
+
def transformed_geom_fields(fields, geom_srid, target_srid)
|
86
|
+
fields.collect do |field|
|
87
|
+
field.gsub(/\*(\w+)\*/) { |m| GeoModel.transform_geom_sql($1, geom_srid, target_srid) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
79
91
|
def features_for_json_reader(features)
|
80
92
|
# convert feature list for display in grid panel:
|
81
93
|
#
|
@@ -31,6 +31,9 @@ class TopicsController < ApplicationController
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def query
|
34
|
+
client_srid = params[:srid].blank? ? GeoModel.default_client_srid : params[:srid].to_i
|
35
|
+
@client_srid = client_srid
|
36
|
+
|
34
37
|
# optional parameter to return only the feature nearest to the center of the search geometry, if no custom layer query is used
|
35
38
|
# use layer setting by default
|
36
39
|
nearest = params['nearest'].nil? ? nil : params['nearest'] == 'true'
|
@@ -42,15 +45,15 @@ class TopicsController < ApplicationController
|
|
42
45
|
authorize! :show, topic
|
43
46
|
query_topic['topicobj'] = topic
|
44
47
|
if params['bbox']
|
45
|
-
query_topic['results'] = topic.query(current_ability, query_topic, params['bbox'], nearest, current_user)
|
48
|
+
query_topic['results'] = topic.query(current_ability, query_topic, params['bbox'], nearest, current_user, client_srid)
|
46
49
|
elsif params['rect']
|
47
50
|
x1, y1, x2, y2 = params['rect'].split(',').collect(&:to_f)
|
48
51
|
rect = "POLYGON((#{x1} #{y1}, #{x1} #{y2}, #{x2} #{y2}, #{x2} #{y1} ,#{x1} #{y1}))"
|
49
|
-
query_topic['results'] = topic.query(current_ability, query_topic, rect, nearest, current_user)
|
52
|
+
query_topic['results'] = topic.query(current_ability, query_topic, rect, nearest, current_user, client_srid)
|
50
53
|
elsif params['circle']
|
51
|
-
query_topic['results'] = topic.query(current_ability, query_topic, params['circle'], nearest, current_user)
|
54
|
+
query_topic['results'] = topic.query(current_ability, query_topic, params['circle'], nearest, current_user, client_srid)
|
52
55
|
elsif params['poly']
|
53
|
-
query_topic['results'] = topic.query(current_ability, query_topic, params['poly'], nearest, current_user)
|
56
|
+
query_topic['results'] = topic.query(current_ability, query_topic, params['poly'], nearest, current_user, client_srid)
|
54
57
|
else
|
55
58
|
# problem
|
56
59
|
end
|
@@ -12,10 +12,10 @@ class WmsController < ApplicationController
|
|
12
12
|
add_filter(topic_name)
|
13
13
|
|
14
14
|
#Send redirect for public services
|
15
|
-
if request.get? && public?(topic_name, host_zone(request.host))
|
15
|
+
if MAPSERV_REDIRECT && request.get? && public?(topic_name, host_zone(request.host))
|
16
16
|
url, path = mapserv_request_url(request)
|
17
17
|
#expires_in 2.minutes, :public => true #FIXME: cache_path "wms-public-#{topic_name}-#{host_zone(request.host)}"
|
18
|
-
redirect_to "#{url.scheme}://#{url.host}#{path}"
|
18
|
+
redirect_to "#{url.scheme}://#{url.host}:#{url.port}#{path}"
|
19
19
|
return
|
20
20
|
end
|
21
21
|
|
@@ -89,14 +89,15 @@ class WmsController < ApplicationController
|
|
89
89
|
|
90
90
|
def call_wms(request)
|
91
91
|
url, path = mapserv_request_url(request)
|
92
|
-
|
92
|
+
uri = URI.parse("#{url.scheme}://#{url.host}:#{url.port}#{path}")
|
93
|
+
logger.info "Forward request: #{uri}"
|
93
94
|
|
94
95
|
if request.get?
|
95
|
-
result = Net::HTTP.get_response(
|
96
|
+
result = Net::HTTP.get_response(uri)
|
96
97
|
send_data result.body, :status => result.code, :type => result.content_type, :disposition => 'inline'
|
97
98
|
else
|
98
99
|
#POST
|
99
|
-
http = Net::HTTP.new(
|
100
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
100
101
|
req = Net::HTTP::Post.new(path)
|
101
102
|
post_params = []
|
102
103
|
post_params += url.query.split(/&|=/) if url.query
|
data/app/models/geo_model.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'georuby'
|
2
|
+
require 'geo_ruby/ewk'
|
3
|
+
|
1
4
|
class GeoModel < ActiveRecord::Base
|
2
5
|
establish_connection(GEODB)
|
3
6
|
|
@@ -30,67 +33,114 @@ class GeoModel < ActiveRecord::Base
|
|
30
33
|
col
|
31
34
|
end
|
32
35
|
|
33
|
-
|
34
|
-
|
36
|
+
# generate SQL fragment for transforming input geometry geom_sql from geom_srid to target_srid
|
37
|
+
def self.transform_geom_sql(geom_sql, geom_srid, target_srid)
|
38
|
+
if geom_srid.nil? || target_srid.nil? || geom_srid == target_srid
|
39
|
+
# no transformation
|
40
|
+
else
|
41
|
+
# transform to target SRID
|
42
|
+
if target_srid == 2056 && geom_srid == 21781
|
43
|
+
geom_sql = "ST_GeomFromEWKB(ST_Fineltra(#{geom_sql}, 'chenyx06_triangles', 'geom_lv03', 'geom_lv95'))"
|
44
|
+
elsif target_srid == 21781 && geom_srid == 2056
|
45
|
+
geom_sql = "ST_GeomFromEWKB(ST_Fineltra(#{geom_sql}, 'chenyx06_triangles', 'geom_lv95', 'geom_lv03'))"
|
46
|
+
else
|
47
|
+
geom_sql = "ST_Transform(#{geom_sql}, #{target_srid})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
geom_sql
|
35
52
|
end
|
36
53
|
|
37
|
-
def self.
|
38
|
-
"
|
54
|
+
def self.geometry_field
|
55
|
+
"#{table_name}.#{connection.quote_column_name(geometry_column_name)}"
|
39
56
|
end
|
40
57
|
|
41
|
-
def self.
|
58
|
+
def self.extent_field(client_srid=nil)
|
59
|
+
# transform geometry to client_srid first
|
60
|
+
"ST_Envelope(#{transform_geom_sql(geometry_field, srid, client_srid)}) AS extent"
|
61
|
+
end
|
62
|
+
|
63
|
+
# NOTE: area in client_srid units
|
64
|
+
def self.area_field(client_srid=nil)
|
65
|
+
# transform geometry to client_srid first
|
66
|
+
"ST_Area(#{transform_geom_sql(geometry_field, srid, client_srid)}) AS area"
|
67
|
+
end
|
68
|
+
|
69
|
+
# NOTE: radius in srid units
|
70
|
+
def self.identify_filter(searchgeo, radius, nearest=false, client_srid=nil)
|
42
71
|
filter = scoped
|
72
|
+
|
73
|
+
client_srid ||= default_client_srid
|
74
|
+
|
43
75
|
if searchgeo[0..3] == "POLY"
|
44
76
|
logger.debug "*** POLY-query: #{searchgeo} ***"
|
45
|
-
|
46
|
-
|
47
|
-
center = "ST_Centroid(#{polygon})"
|
77
|
+
search_geom = "ST_GeomFromText('#{searchgeo}', #{client_srid})"
|
78
|
+
center = "ST_Centroid(#{search_geom})"
|
48
79
|
else
|
49
80
|
if searchgeo.split(',').length == 3
|
50
81
|
logger.debug "*** CIRCLE-query: #{searchgeo} ***"
|
51
82
|
x1, y1, r = searchgeo.split(',').collect(&:to_f)
|
52
|
-
center = "ST_GeomFromText('POINT(#{x1} #{y1})', #{
|
53
|
-
|
83
|
+
center = "ST_GeomFromText('POINT(#{x1} #{y1})', #{client_srid})"
|
84
|
+
# NOTE: circle as buffer with radius in client_srid units
|
85
|
+
search_geom = "ST_Buffer(#{center}, #{r}, 32)"
|
86
|
+
radius = 0
|
54
87
|
else
|
55
88
|
logger.debug "*** BBOX-query: #{searchgeo} ***"
|
56
89
|
x1, y1, x2, y2 = searchgeo.split(',').collect(&:to_f)
|
57
|
-
|
58
|
-
|
90
|
+
search_geom = "ST_GeomFromText('POINT(#{x1+(x2-x1)/2} #{y1+(y2-y1)/2})', #{client_srid})"
|
91
|
+
center = search_geom
|
59
92
|
end
|
60
93
|
end
|
61
94
|
|
95
|
+
# transform search geometry to srid
|
96
|
+
search_geom = transform_geom_sql(search_geom, client_srid, srid)
|
97
|
+
|
98
|
+
# get features within radius in srid units
|
99
|
+
filter = filter.where("ST_DWithin(#{geometry_field}, #{search_geom}, #{radius})")
|
100
|
+
|
62
101
|
if nearest
|
63
102
|
logger.debug "*** query nearest ***"
|
64
|
-
|
103
|
+
# transform center to srid
|
104
|
+
center = transform_geom_sql(center, client_srid, srid)
|
105
|
+
# get min dist
|
106
|
+
min_dist = filter.select("Min(ST_Distance(#{geometry_field}, #{center})) AS min_dist").first
|
65
107
|
unless min_dist.nil?
|
66
108
|
logger.debug "*** min_dist = #{min_dist.min_dist} ***"
|
67
109
|
if min_dist.min_dist.to_f == 0
|
68
110
|
# center of the search geometry is within a feature (may be overlapping features)
|
69
|
-
filter = filter.where("ST_Within(#{center}, #{
|
111
|
+
filter = filter.where("ST_Within(#{center}, #{geometry_field})")
|
70
112
|
else
|
71
113
|
# get the feature nearest to the center of the search geometry
|
72
|
-
filter = filter.order("ST_Distance(#{
|
114
|
+
filter = filter.order("ST_Distance(#{geometry_field}, #{center})").limit(1)
|
73
115
|
end
|
74
116
|
end
|
75
117
|
# else no features in filter
|
118
|
+
else
|
119
|
+
# order by distance to center
|
120
|
+
center = transform_geom_sql(center, client_srid, srid)
|
121
|
+
filter = filter.order("ST_Distance(#{geometry_field}, #{center})")
|
76
122
|
end
|
77
123
|
|
78
124
|
filter
|
79
125
|
end
|
80
126
|
|
81
127
|
#Custom identify query
|
82
|
-
#def self.identify_query(
|
83
|
-
#
|
128
|
+
#def self.identify_query(layer, query_topic, searchgeom, ability, user, client_srid=nil)
|
129
|
+
# # default layer query
|
130
|
+
# query_fields = (["#{self.table_name}.#{self.primary_key}"] + layer.ident_fields_for(ability).split(',') + [self.extent_field(client_srid), self.area_field(client_srid)]).join(',')
|
131
|
+
# features = scoped.identify_filter(searchgeom, layer.searchdistance, nil, client_srid).where(layer.where_filter).select(query_fields)
|
132
|
+
# features.all
|
84
133
|
#end
|
85
134
|
|
86
|
-
def bbox
|
135
|
+
def bbox(client_srid=nil)
|
87
136
|
if respond_to?('extent')
|
88
137
|
# use extent from select(extent_field)
|
89
|
-
envelope = GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(extent).envelope
|
138
|
+
envelope = GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(extent).envelope
|
90
139
|
[envelope.lower_corner.x, envelope.lower_corner.y, envelope.upper_corner.x, envelope.upper_corner.y]
|
91
140
|
else
|
92
141
|
# get Box2D for this feature
|
93
|
-
|
142
|
+
# transform geometry to client_srid first
|
143
|
+
box_query = "Box2D(#{self.class.transform_geom_sql(self.class.geometry_field, self.class.srid, client_srid)})"
|
94
144
|
extent = self.class.select("ST_XMin(#{box_query}), ST_YMin(#{box_query}), ST_XMax(#{box_query}), ST_Ymax(#{box_query})").find(id)
|
95
145
|
[
|
96
146
|
extent.st_xmin.to_f,
|
@@ -114,37 +164,106 @@ class GeoModel < ActiveRecord::Base
|
|
114
164
|
end
|
115
165
|
|
116
166
|
if filter_geom
|
117
|
-
|
167
|
+
# transform filter geom to srid
|
168
|
+
client_srid = params['srid'].blank? ? default_client_srid : params['srid'].to_i
|
169
|
+
filter_geom = transform_geom_sql("ST_SetSRID(#{filter_geom}, #{client_srid})", client_srid, srid)
|
170
|
+
filter = filter.where("ST_Intersects(#{geometry_field}, #{filter_geom})")
|
118
171
|
end
|
119
172
|
|
120
173
|
filter.limit(1000)
|
121
174
|
end
|
122
175
|
|
123
176
|
def self.geojson_decode(json)
|
124
|
-
|
177
|
+
geojson = JSON.parse(json)
|
178
|
+
|
179
|
+
# get client_srid from GeoJSON CRS
|
180
|
+
if geojson['crs'].blank?
|
181
|
+
client_srid = default_client_srid
|
182
|
+
else
|
183
|
+
client_srid = geojson['crs']['properties']['name'].split(':').last rescue default_client_srid
|
184
|
+
# use EPSG:4326 for 'urn:ogc:def:crs:OGC:1.3:CRS84'
|
185
|
+
client_srid = 4326 if client_srid == 'CRS84'
|
186
|
+
end
|
187
|
+
|
188
|
+
# NOTE: use dummy factory to set client_srid for update_attributes_from_geojson_feature()
|
189
|
+
RGeo::GeoJSON.decode(geojson, :geo_factory => RGeo::Cartesian.factory(:srid => client_srid))
|
125
190
|
end
|
126
191
|
|
127
|
-
|
192
|
+
# select transformed geometry as GeoJSON
|
193
|
+
def self.select_geojson_geom(client_srid=nil)
|
194
|
+
# transform geometry to client_srid
|
195
|
+
geom_sql = transform_geom_sql("#{geometry_field}", srid, client_srid)
|
196
|
+
# select geometry as GeoJSON
|
197
|
+
scoped.select("ST_AsGeoJSON(#{geom_sql}) AS geojson_geom, 'EPSG:' || #{client_srid || srid} AS geojson_srid")
|
198
|
+
end
|
199
|
+
|
200
|
+
# customize GeoJSON contents, e.g. to add custom properties or fields
|
201
|
+
# override in descendant classes
|
202
|
+
def customize_geojson(geojson, options={})
|
203
|
+
geojson
|
204
|
+
end
|
205
|
+
|
206
|
+
def to_geojson(options={})
|
128
207
|
only = options.delete(:only)
|
129
|
-
|
130
|
-
|
131
|
-
# TODO: support for multiple geometry columns
|
208
|
+
geojson = { :type => 'Feature' }
|
209
|
+
geojson[:properties] = attributes.delete_if do |name, value|
|
132
210
|
if name == self.class.geometry_column_name
|
133
|
-
|
211
|
+
geojson[:geometry] = value
|
212
|
+
true
|
213
|
+
elsif name == 'geojson_geom' || name == 'geojson_srid'
|
214
|
+
# skip helper fields
|
134
215
|
true
|
135
216
|
elsif name == self.class.primary_key then
|
136
|
-
|
217
|
+
geojson[:id] = value
|
137
218
|
true
|
138
219
|
elsif only
|
139
220
|
!only.include?(name.to_sym)
|
140
221
|
end
|
141
222
|
end
|
142
|
-
|
223
|
+
|
224
|
+
geojson = customize_geojson(geojson, options)
|
225
|
+
|
226
|
+
if attributes.has_key?('geojson_geom')
|
227
|
+
# dummy geometry (ignore value from geometry column)
|
228
|
+
geojson[:geometry] = {}
|
229
|
+
|
230
|
+
unless options[:skip_feature_crs]
|
231
|
+
# add GeoJSON CRS unless part of a FeatureCollection
|
232
|
+
geojson[:crs] = {
|
233
|
+
:type => 'name',
|
234
|
+
:properties => {
|
235
|
+
:name => attributes['geojson_srid']
|
236
|
+
}
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
# convert to JSON and replace geometry with GeoJSON field from query
|
241
|
+
geojson.to_json.sub(/"geometry":{}/, "\"geometry\":#{attributes['geojson_geom']}")
|
242
|
+
else
|
243
|
+
geojson.to_json
|
244
|
+
end
|
143
245
|
end
|
144
246
|
|
145
247
|
def update_attributes_from_geojson_feature(feature, user)
|
146
248
|
attr = feature.properties
|
147
|
-
|
249
|
+
|
250
|
+
unless feature.geometry.nil?
|
251
|
+
# get client_srid from RGeo geometry
|
252
|
+
client_srid = feature.geometry.srid
|
253
|
+
if client_srid != self.class.srid
|
254
|
+
# transform feature geometry to srid
|
255
|
+
geom_sql = self.class.transform_geom_sql("ST_GeomFromText('#{feature.geometry.as_text}', #{client_srid})", client_srid, self.class.srid)
|
256
|
+
sql = "SELECT ST_AsText(#{geom_sql}) AS wkt_geom"
|
257
|
+
results = connection.execute(sql)
|
258
|
+
results.each do |result|
|
259
|
+
attr[self.class.geometry_column_name] = self.class.geo_factory.parse_wkt(result['wkt_geom'])
|
260
|
+
end
|
261
|
+
else
|
262
|
+
# no transformation
|
263
|
+
attr[self.class.geometry_column_name] = feature.geometry
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
148
267
|
ok = update_attributes(attr)
|
149
268
|
modified_by(user)
|
150
269
|
ok
|
@@ -202,11 +321,180 @@ class GeoModel < ActiveRecord::Base
|
|
202
321
|
[]
|
203
322
|
end
|
204
323
|
|
324
|
+
# GeoJSON validations
|
325
|
+
|
326
|
+
def self.validate_feature_collection(feature_collection, geojson_data)
|
327
|
+
if feature_collection.nil?
|
328
|
+
# not a GeoJSON
|
329
|
+
return {
|
330
|
+
:error => "Invalid GeoJSON"
|
331
|
+
}
|
332
|
+
elsif !feature_collection.is_a? RGeo::GeoJSON::FeatureCollection
|
333
|
+
# not a GeoJSON FeatureCollection
|
334
|
+
return {
|
335
|
+
:error => "GeoJSON is not a FeatureCollection"
|
336
|
+
}
|
337
|
+
elsif !feature_collection.any? || feature_collection.select {|feature| feature.geometry.is_empty?}.any?
|
338
|
+
# no features or invalid geometries
|
339
|
+
# NOTE: RGeo::GeoJSON.decode or feature geometry is empty if geometry is invalid
|
340
|
+
errors = []
|
341
|
+
begin
|
342
|
+
geojson = JSON.parse(geojson_data)
|
343
|
+
if geojson['features'].blank?
|
344
|
+
return {
|
345
|
+
:error => "No GeoJSON features found"
|
346
|
+
}
|
347
|
+
end
|
348
|
+
|
349
|
+
geojson['features'].each do |feature|
|
350
|
+
feature_errors = validate_geometry(feature)
|
351
|
+
errors += feature_errors if feature_errors.any?
|
352
|
+
end
|
353
|
+
rescue => err
|
354
|
+
logger.error "Error while checking GeoJSON geometries:\n#{err.message}"
|
355
|
+
end
|
356
|
+
|
357
|
+
return {
|
358
|
+
:error => "Invalid geometry",
|
359
|
+
:geometry_errors => errors
|
360
|
+
}
|
361
|
+
end
|
362
|
+
|
363
|
+
# validate geometry type
|
364
|
+
errors = []
|
365
|
+
feature_collection.each do |feature|
|
366
|
+
error = validate_geometry_type(feature)
|
367
|
+
errors << error unless error.nil?
|
368
|
+
end
|
369
|
+
if errors.any?
|
370
|
+
return {
|
371
|
+
:error => "Invalid geometry type",
|
372
|
+
:geometry_errors => errors
|
373
|
+
}
|
374
|
+
end
|
375
|
+
|
376
|
+
# validations OK
|
377
|
+
return nil
|
378
|
+
end
|
379
|
+
|
380
|
+
def self.validate_feature(feature, geojson_data)
|
381
|
+
if feature.nil? || (feature.is_a?(RGeo::GeoJSON::Feature) && feature.geometry.is_empty?)
|
382
|
+
geojson = JSON.parse(geojson_data)
|
383
|
+
if geojson['type'].blank? || geojson['type'] != 'Feature'
|
384
|
+
# not a GeoJSON
|
385
|
+
return {
|
386
|
+
:error => "Invalid GeoJSON"
|
387
|
+
}
|
388
|
+
else
|
389
|
+
# invalid geometry
|
390
|
+
# NOTE: RGeo::GeoJSON.decode is nil or feature geometry is empty if geometry is invalid
|
391
|
+
errors = []
|
392
|
+
begin
|
393
|
+
errors = validate_geometry(geojson)
|
394
|
+
rescue => err
|
395
|
+
logger.error "Error while checking GeoJSON geometries:\n#{err.message}"
|
396
|
+
end
|
397
|
+
|
398
|
+
return {
|
399
|
+
:error => "Invalid geometry",
|
400
|
+
:geometry_errors => errors
|
401
|
+
}
|
402
|
+
end
|
403
|
+
elsif !feature.is_a? RGeo::GeoJSON::Feature
|
404
|
+
# not a GeoJSON Feature
|
405
|
+
return {
|
406
|
+
:error => "GeoJSON is not a Feature"
|
407
|
+
}
|
408
|
+
end
|
409
|
+
|
410
|
+
# validate geometry type
|
411
|
+
error = validate_geometry_type(feature)
|
412
|
+
unless error.nil?
|
413
|
+
return {
|
414
|
+
:error => "Invalid geometry type",
|
415
|
+
:geometry_errors => [error]
|
416
|
+
}
|
417
|
+
end
|
418
|
+
|
419
|
+
# validations OK
|
420
|
+
return nil
|
421
|
+
end
|
422
|
+
|
423
|
+
def self.validate_geometry(feature)
|
424
|
+
errors = []
|
425
|
+
|
426
|
+
# validate geometry
|
427
|
+
wkt_geom = ""
|
428
|
+
sql = "WITH feature AS (SELECT ST_GeomFromGeoJSON(?) AS geom) SELECT valid, reason, ST_AsText(location) AS location, ST_AsText(geom) AS wkt_geom FROM feature, ST_IsValidDetail(geom)"
|
429
|
+
sql = send :sanitize_sql, [sql, feature['geometry'].to_json]
|
430
|
+
results = connection.execute(sql)
|
431
|
+
results.each do |result|
|
432
|
+
if result['valid'] == 'f'
|
433
|
+
error = {}
|
434
|
+
error[:id] = feature['id'] unless feature['id'].blank?
|
435
|
+
error[:reason] = result['reason'] unless result['reason'].blank?
|
436
|
+
error[:location] = result['location'] unless result['location'].blank?
|
437
|
+
errors << error
|
438
|
+
end
|
439
|
+
wkt_geom = result['wkt_geom']
|
440
|
+
end
|
441
|
+
|
442
|
+
# check for repeated vertices
|
443
|
+
wkt_geom.gsub(/(?<=\()([\d\.,\s]+)(?=\))/) do |m|
|
444
|
+
vertices = m.split(',')
|
445
|
+
vertices.each_with_index do |v, i|
|
446
|
+
if i > 0 && vertices[i-1] == v
|
447
|
+
errors << {
|
448
|
+
:reason => "Duplicated point",
|
449
|
+
:location => "POINT(#{v})"
|
450
|
+
}
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
errors
|
456
|
+
end
|
457
|
+
|
458
|
+
def self.validate_geometry_type(feature)
|
459
|
+
if geometry_type != 'GEOMETRY' && feature.geometry.geometry_type.to_s.upcase != geometry_type
|
460
|
+
error = {}
|
461
|
+
error[:id] = feature.feature_id unless feature.feature_id.blank?
|
462
|
+
error.merge({
|
463
|
+
:reason => "Invalid geometry type: #{feature.geometry.geometry_type}",
|
464
|
+
:location => feature.geometry.as_text
|
465
|
+
})
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# default client_srid
|
470
|
+
|
471
|
+
@@default_client_srid = 21781
|
472
|
+
|
473
|
+
def self.set_default_client_srid(client_srid)
|
474
|
+
@@default_client_srid = client_srid
|
475
|
+
end
|
476
|
+
|
477
|
+
def self.default_client_srid
|
478
|
+
@@default_client_srid
|
479
|
+
end
|
480
|
+
|
481
|
+
# default geo factory
|
482
|
+
|
483
|
+
@@default_rgeo_factory = RGeo::Cartesian.factory(:srid => 21781, :proj4 => '+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel +towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs')
|
484
|
+
|
485
|
+
def self.set_default_rgeo_factory(factory)
|
486
|
+
@@default_rgeo_factory = factory
|
487
|
+
end
|
488
|
+
|
489
|
+
def self.default_rgeo_factory
|
490
|
+
@@default_rgeo_factory
|
491
|
+
end
|
492
|
+
|
205
493
|
def self.geo_factory
|
206
494
|
if self.rgeo_factory_generator == RGeo::ActiveRecord::DEFAULT_FACTORY_GENERATOR
|
207
495
|
self.rgeo_factory_generator = RGeo::Geos.factory_generator
|
208
496
|
rgeo_factory_settings.set_column_factory(table_name, geometry_column_name,
|
209
|
-
|
497
|
+
default_rgeo_factory
|
210
498
|
)
|
211
499
|
end
|
212
500
|
rgeo_factory_for_column(geometry_column_name)
|