gb_mapfish_appserver 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|