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.
@@ -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 v1.1.0 or later no longer uses the gb_mapfish_print gem, but uses a
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-2015 Sourcepole AG & Canton of Zurich*
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
- @current_roles ||= Ability::Roles.new(current_user, host_zone(request.host))
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
- @feature = geo_model.user_filter(current_ability).json_filter.find(params[:id])
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 update
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 destroy
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 'RMagick'
5
+ require 'rmagick'
6
6
  rescue LoadError
7
- ActionController::Base.logger.info "Couldn't find RMagick. Image export not supported"
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
- response = Net::HTTP.get_response(url)
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 = SEARCHRULES[params[: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
- result = @rule.model.query(@rule.fields, params)
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
- location = rule.locate(params['locations'])
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
- logger.info "Forward request: #{url.scheme}://#{url.host}#{path}"
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(url.host, path, url.port)
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(url.host, url.port)
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
@@ -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
- def self.extent_field
34
- "ST_Envelope(#{table_name}.#{geometry_column_name}) AS extent"
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.area_field
38
- "ST_Area(#{table_name}.#{geometry_column.name}) AS area"
54
+ def self.geometry_field
55
+ "#{table_name}.#{connection.quote_column_name(geometry_column_name)}"
39
56
  end
40
57
 
41
- def self.identify_filter(searchgeo, radius, nearest=false)
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
- polygon = "ST_GeomFromText('#{searchgeo}', #{srid})"
46
- filter = filter.where("ST_DWithin(#{table_name}.#{geometry_column_name}, #{polygon}, #{radius})")
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})', #{srid})"
53
- filter = filter.where("ST_DWithin(#{table_name}.#{geometry_column_name}, #{center}, #{r})")
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
- center = "ST_GeomFromText('POINT(#{x1+(x2-x1)/2} #{y1+(y2-y1)/2})', #{srid})"
58
- filter = filter.where("ST_DWithin(#{table_name}.#{geometry_column_name}, #{center}, #{radius})")
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
- min_dist = filter.select("Min(ST_Distance(#{table_name}.#{geometry_column_name}, #{center})) AS min_dist").first
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}, #{table_name}.#{geometry_column_name})")
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(#{table_name}.#{geometry_column_name}, #{center})").limit(1)
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(bbox, radius)
83
- # scoped.select().where()....
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 #TODO: replace with rgeo
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
- box_query = "Box2D(#{connection.quote_column_name(self.class.geometry_column_name)})"
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
- filter = filter.where("ST_Intersects(#{table_name}.#{connection.quote_column_name(geometry_column_name)}, ST_SetSRID(#{filter_geom}, #{srid}))")
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
- RGeo::GeoJSON.decode(json, :json_parser => :json, :geo_factory => geo_factory)
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
- def to_geojson(options = {})
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
- geoson = { :type => 'Feature' }
130
- geoson[:properties] = attributes.delete_if do |name, value|
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
- geoson[:geometry] = value
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
- geoson[:id] = value
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
- geoson.to_json
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
- attr[self.class.geometry_column_name] = feature.geometry unless feature.geometry.nil?
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
- 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')
497
+ default_rgeo_factory
210
498
  )
211
499
  end
212
500
  rgeo_factory_for_column(geometry_column_name)