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.
@@ -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)