Class: GeoModel

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/geo_model.rb

Direct Known Subclasses

SearchModel

Constant Summary collapse

@@default_client_srid =

default client_srid

21781
@@default_rgeo_factory =

default geo 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')

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.area_field(client_srid = nil) ⇒ Object

NOTE: area in client_srid units



64
65
66
67
# File 'app/models/geo_model.rb', line 64

def self.area_field(client_srid=nil)
  # transform geometry to client_srid first
  "ST_Area(#{transform_geom_sql(geometry_field, srid, client_srid)}) AS area"
end

.bbox_filter(params) ⇒ Object

based on mapfish_filter



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'app/models/geo_model.rb', line 155

def self.bbox_filter(params)
  filter = scoped

  if params['bbox']
    x1, y1, x2, y2 = params['bbox'].split(',').collect(&:to_f)
    box = [[x1, y1], [x2, y2]]
    filter_geom = "'BOX3D(#{box[0].join(" ")},#{box[1].join(" ")})'::box3d"
  elsif params['polygon']
    filter_geom = "ST_GeomFromText('#{params['polygon']}')"
  end

  if filter_geom
    # transform filter geom to srid
    client_srid = params['srid'].blank? ? default_client_srid : params['srid'].to_i
    filter_geom = transform_geom_sql("ST_SetSRID(#{filter_geom}, #{client_srid})", client_srid, srid)
    filter = filter.where("ST_Intersects(#{geometry_field}, #{filter_geom})")
  end

  filter.limit(1000)
end

.can_edit?(ability) ⇒ Boolean

Returns:

  • (Boolean)


278
279
280
281
282
283
284
285
286
287
288
289
# File 'app/models/geo_model.rb', line 278

def self.can_edit?(ability)
  @layers ||= Layer.where(:table => self.table_name).all
  can_edit = false
  @layers.each do |layer|
    # check if any layer with this table is editable
    if ability.can?(:edit, layer)
      can_edit = true
      break
    end
  end
  can_edit
end

.default_client_sridObject



477
478
479
# File 'app/models/geo_model.rb', line 477

def self.default_client_srid
  @@default_client_srid
end

.default_rgeo_factoryObject



489
490
491
# File 'app/models/geo_model.rb', line 489

def self.default_rgeo_factory
  @@default_rgeo_factory
end

.extent_field(client_srid = nil) ⇒ Object



58
59
60
61
# File 'app/models/geo_model.rb', line 58

def self.extent_field(client_srid=nil)
  # transform geometry to client_srid first
  "ST_Envelope(#{transform_geom_sql(geometry_field, srid, client_srid)}) AS extent"
end

.forbidden(ability) ⇒ Object



303
304
305
306
307
308
309
310
# File 'app/models/geo_model.rb', line 303

def self.forbidden(ability)
  if ability.nil?
    logger.info "----> Edit access forbidden without login"
  else
    logger.info "----> Edit access forbidden with roles #{ability.roles.collect(&:name).join('+')}!"
  end
  where('1=0') # No access
end

.geo_factoryObject



493
494
495
496
497
498
499
500
501
# File 'app/models/geo_model.rb', line 493

def self.geo_factory
  if self.rgeo_factory_generator == RGeo::ActiveRecord::DEFAULT_FACTORY_GENERATOR
    self.rgeo_factory_generator = RGeo::Geos.factory_generator
    rgeo_factory_settings.set_column_factory(table_name, geometry_column_name,
      default_rgeo_factory
    )
  end
  rgeo_factory_for_column(geometry_column_name)
end

.geojson_decode(json) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'app/models/geo_model.rb', line 176

def self.geojson_decode(json)
  geojson = JSON.parse(json)

  # get client_srid from GeoJSON CRS
  if geojson['crs'].blank?
    client_srid = default_client_srid
  else
    client_srid = geojson['crs']['properties']['name'].split(':').last rescue default_client_srid
    # use EPSG:4326 for 'urn:ogc:def:crs:OGC:1.3:CRS84'
    client_srid = 4326 if client_srid == 'CRS84'
  end

  # NOTE: use dummy factory to set client_srid for update_attributes_from_geojson_feature()
  RGeo::GeoJSON.decode(geojson, :geo_factory => RGeo::Cartesian.factory(:srid => client_srid))
end

.geometry_columnObject



30
31
32
33
34
# File 'app/models/geo_model.rb', line 30

def self.geometry_column
  col = columns_hash[geometry_column_name]
  col.instance_eval { @srid = srid }
  col
end

.geometry_column_infoObject



11
12
13
14
15
16
# File 'app/models/geo_model.rb', line 11

def self.geometry_column_info
  #spatial_column_info returns key/value list with geometry_column name as key
  #value example: {:srid=>21781, :type=>"MULTIPOLYGON", :dimension=>2, :has_z=>false, :has_m=>false, :name=>"the_geom"}
  #We take the first matching entry
  @geometry_column_info ||= connection.spatial_column_info(table_name).values.first
end

.geometry_column_nameObject



18
19
20
# File 'app/models/geo_model.rb', line 18

def self.geometry_column_name
  geometry_column_info[:name]
end

.geometry_fieldObject



54
55
56
# File 'app/models/geo_model.rb', line 54

def self.geometry_field
  "#{table_name}.#{connection.quote_column_name(geometry_column_name)}"
end

.geometry_typeObject



22
23
24
# File 'app/models/geo_model.rb', line 22

def self.geometry_type
  geometry_column_info[:type]
end

.identify_filter(searchgeo, radius, nearest = false, client_srid = nil) ⇒ Object

NOTE: radius in srid units



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'app/models/geo_model.rb', line 70

def self.identify_filter(searchgeo, radius, nearest=false, client_srid=nil)
  filter = scoped

  client_srid ||= default_client_srid

  if searchgeo[0..3] == "POLY"
    logger.debug "*** POLY-query: #{searchgeo} ***"
    search_geom = "ST_GeomFromText('#{searchgeo}', #{client_srid})"
    center = "ST_Centroid(#{search_geom})"
  else
    if searchgeo.split(',').length == 3
      logger.debug "*** CIRCLE-query: #{searchgeo} ***"
      x1, y1, r  = searchgeo.split(',').collect(&:to_f)
      center = "ST_GeomFromText('POINT(#{x1} #{y1})', #{client_srid})"
      # NOTE: circle as buffer with radius in client_srid units
      search_geom = "ST_Buffer(#{center}, #{r}, 32)"
      radius = 0
    else
      logger.debug "*** BBOX-query: #{searchgeo} ***"
      x1, y1, x2, y2 = searchgeo.split(',').collect(&:to_f)
      search_geom = "ST_GeomFromText('POINT(#{x1+(x2-x1)/2} #{y1+(y2-y1)/2})', #{client_srid})"
      center = search_geom
    end
  end

  # transform search geometry to srid
  search_geom = transform_geom_sql(search_geom, client_srid, srid)

  # get features within radius in srid units
  filter = filter.where("ST_DWithin(#{geometry_field}, #{search_geom}, #{radius})")

  if nearest
    logger.debug "*** query nearest ***"
    # transform center to srid
    center = transform_geom_sql(center, client_srid, srid)
    # get min dist
    min_dist = filter.select("Min(ST_Distance(#{geometry_field}, #{center})) AS min_dist").first
    unless min_dist.nil?
      logger.debug "*** min_dist = #{min_dist.min_dist} ***"
      if min_dist.min_dist.to_f == 0
        # center of the search geometry is within a feature (may be overlapping features)
        filter = filter.where("ST_Within(#{center}, #{geometry_field})")
      else
        # get the feature nearest to the center of the search geometry
        filter = filter.order("ST_Distance(#{geometry_field}, #{center})").limit(1)
      end
    end
    # else no features in filter
  else
    # order by distance to center
    center = transform_geom_sql(center, client_srid, srid)
    filter = filter.order("ST_Distance(#{geometry_field}, #{center})")
  end

  filter
end

.select_geojson_geom(client_srid = nil) ⇒ Object

select transformed geometry as GeoJSON



193
194
195
196
197
198
# File 'app/models/geo_model.rb', line 193

def self.select_geojson_geom(client_srid=nil)
  # transform geometry to client_srid
  geom_sql = transform_geom_sql("#{geometry_field}", srid, client_srid)
  # select geometry as GeoJSON
  scoped.select("ST_AsGeoJSON(#{geom_sql}) AS geojson_geom, 'EPSG:' || #{client_srid || srid} AS geojson_srid")
end

.set_default_client_srid(client_srid) ⇒ Object



473
474
475
# File 'app/models/geo_model.rb', line 473

def self.set_default_client_srid(client_srid)
  @@default_client_srid = client_srid
end

.set_default_rgeo_factory(factory) ⇒ Object



485
486
487
# File 'app/models/geo_model.rb', line 485

def self.set_default_rgeo_factory(factory)
  @@default_rgeo_factory = factory
end

.sridObject



26
27
28
# File 'app/models/geo_model.rb', line 26

def self.srid
  geometry_column_info[:srid]
end

.transform_geom_sql(geom_sql, geom_srid, target_srid) ⇒ Object

generate SQL fragment for transforming input geometry geom_sql from geom_srid to target_srid



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/models/geo_model.rb', line 37

def self.transform_geom_sql(geom_sql, geom_srid, target_srid)
  if geom_srid.nil? || target_srid.nil? || geom_srid == target_srid
    # no transformation
  else
    # transform to target SRID
    if target_srid == 2056 && geom_srid == 21781
      geom_sql = "ST_GeomFromEWKB(ST_Fineltra(#{geom_sql}, 'chenyx06_triangles', 'geom_lv03', 'geom_lv95'))"
    elsif target_srid == 21781 && geom_srid == 2056
      geom_sql = "ST_GeomFromEWKB(ST_Fineltra(#{geom_sql}, 'chenyx06_triangles', 'geom_lv95', 'geom_lv03'))"
    else
      geom_sql = "ST_Transform(#{geom_sql}, #{target_srid})"
    end
  end

  geom_sql
end

.user_filter(ability) ⇒ Object

apply user filter for editing Override in descendant classes



293
294
295
296
297
298
299
300
301
# File 'app/models/geo_model.rb', line 293

def self.user_filter(ability)
  if ability.nil?
    forbidden(ability)
  elsif can_edit?(ability)
    scoped #No filter
  else
    forbidden(ability)
  end
end

.validate_feature(feature, geojson_data) ⇒ Object



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'app/models/geo_model.rb', line 380

def self.validate_feature(feature, geojson_data)
  if feature.nil? || (feature.is_a?(RGeo::GeoJSON::Feature) && feature.geometry.is_empty?)
    geojson = JSON.parse(geojson_data)
    if geojson['type'].blank? || geojson['type'] != 'Feature'
      # not a GeoJSON
      return {
        :error => "Invalid GeoJSON"
      }
    else
      # invalid geometry
      # NOTE: RGeo::GeoJSON.decode is nil or feature geometry is empty if geometry is invalid
      errors = []
      begin
        errors = validate_geometry(geojson)
      rescue => err
        logger.error "Error while checking GeoJSON geometries:\n#{err.message}"
      end

      return {
        :error => "Invalid geometry",
        :geometry_errors => errors
      }
    end
  elsif !feature.is_a? RGeo::GeoJSON::Feature
    # not a GeoJSON Feature
    return {
      :error => "GeoJSON is not a Feature"
    }
  end

  # validate geometry type
  error = validate_geometry_type(feature)
  unless error.nil?
    return {
      :error => "Invalid geometry type",
      :geometry_errors => [error]
    }
  end

  # validations OK
  return nil
end

.validate_feature_collection(feature_collection, geojson_data) ⇒ Object

GeoJSON validations



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'app/models/geo_model.rb', line 326

def self.validate_feature_collection(feature_collection, geojson_data)
  if feature_collection.nil?
    # not a GeoJSON
    return {
      :error => "Invalid GeoJSON"
    }
  elsif !feature_collection.is_a? RGeo::GeoJSON::FeatureCollection
    # not a GeoJSON FeatureCollection
    return {
      :error => "GeoJSON is not a FeatureCollection"
    }
  elsif !feature_collection.any? || feature_collection.select {|feature| feature.geometry.is_empty?}.any?
    # no features or invalid geometries
    # NOTE: RGeo::GeoJSON.decode or feature geometry is empty if geometry is invalid
    errors = []
    begin
      geojson = JSON.parse(geojson_data)
      if geojson['features'].blank?
        return {
          :error => "No GeoJSON features found"
        }
      end

      geojson['features'].each do |feature|
        feature_errors = validate_geometry(feature)
        errors += feature_errors if feature_errors.any?
      end
    rescue => err
      logger.error "Error while checking GeoJSON geometries:\n#{err.message}"
    end

    return {
      :error => "Invalid geometry",
      :geometry_errors => errors
    }
  end

  # validate geometry type
  errors = []
  feature_collection.each do |feature|
    error = validate_geometry_type(feature)
    errors << error unless error.nil?
  end
  if errors.any?
    return {
      :error => "Invalid geometry type",
      :geometry_errors => errors
    }
  end

  # validations OK
  return nil
end

.validate_geometry(feature) ⇒ Object



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'app/models/geo_model.rb', line 423

def self.validate_geometry(feature)
  errors = []

  # validate geometry
  wkt_geom = ""
  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)"
  sql = send :sanitize_sql, [sql, feature['geometry'].to_json]
  results = connection.execute(sql)
  results.each do |result|
    if result['valid'] == 'f'
      error = {}
      error[:id] = feature['id'] unless feature['id'].blank?
      error[:reason] = result['reason'] unless result['reason'].blank?
      error[:location] = result['location'] unless result['location'].blank?
      errors << error
    end
    wkt_geom = result['wkt_geom']
  end

  # check for repeated vertices
  wkt_geom.gsub(/(?<=\()([\d\.,\s]+)(?=\))/) do |m|
    vertices = m.split(',')
    vertices.each_with_index do |v, i|
      if i > 0 && vertices[i-1] == v
        errors << {
          :reason => "Duplicated point",
          :location => "POINT(#{v})"
        }
      end
    end
  end

  errors
end

.validate_geometry_type(feature) ⇒ Object



458
459
460
461
462
463
464
465
466
467
# File 'app/models/geo_model.rb', line 458

def self.validate_geometry_type(feature)
  if geometry_type != 'GEOMETRY' && feature.geometry.geometry_type.to_s.upcase != geometry_type
    error = {}
    error[:id] = feature.feature_id unless feature.feature_id.blank?
    error.merge({
      :reason => "Invalid geometry type: #{feature.geometry.geometry_type}",
      :location => feature.geometry.as_text
    })
  end
end

Instance Method Details

#bbox(client_srid = nil) ⇒ Object

Custom identify query def self.identify_query(layer, query_topic, searchgeom, ability, user, client_srid=nil)

# default layer query
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(',')
features = scoped.identify_filter(searchgeom, layer.searchdistance, nil, client_srid).where(layer.where_filter).select(query_fields)
features.all

end



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/models/geo_model.rb', line 135

def bbox(client_srid=nil)
  if respond_to?('extent')
    # use extent from select(extent_field)
    envelope = GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(extent).envelope
    [envelope.lower_corner.x, envelope.lower_corner.y, envelope.upper_corner.x, envelope.upper_corner.y]
  else
    # get Box2D for this feature
    # transform geometry to client_srid first
    box_query = "Box2D(#{self.class.transform_geom_sql(self.class.geometry_field, self.class.srid, client_srid)})"
    extent = self.class.select("ST_XMin(#{box_query}), ST_YMin(#{box_query}), ST_XMax(#{box_query}), ST_Ymax(#{box_query})").find(id)
    [
      extent.st_xmin.to_f,
      extent.st_ymin.to_f,
      extent.st_xmax.to_f,
      extent.st_ymax.to_f
    ]
  end
end

#csv_headerObject

header for CSV export



313
314
315
316
# File 'app/models/geo_model.rb', line 313

def csv_header
  #empty by default
  []
end

#csv_rowObject

row values for CSV export



319
320
321
322
# File 'app/models/geo_model.rb', line 319

def csv_row
  #empty by default
  []
end

#customize_geojson(geojson, options = {}) ⇒ Object

customize GeoJSON contents, e.g. to add custom properties or fields override in descendant classes



202
203
204
# File 'app/models/geo_model.rb', line 202

def customize_geojson(geojson, options={})
  geojson
end

#modified_by(user) ⇒ Object

update modification attributes (changed_by, etc.) Override in descendant classes



274
275
276
# File 'app/models/geo_model.rb', line 274

def modified_by(user)
  #none by default
end

#to_geojson(options = {}) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'app/models/geo_model.rb', line 206

def to_geojson(options={})
  only = options.delete(:only)
  geojson = { :type => 'Feature' }
  geojson[:properties] = attributes.delete_if do |name, value|
    if name == self.class.geometry_column_name
      geojson[:geometry] = value
      true
    elsif name == 'geojson_geom' || name == 'geojson_srid'
      # skip helper fields
      true
    elsif name == self.class.primary_key then
      geojson[:id] = value
      true
    elsif only
      !only.include?(name.to_sym)
    end
  end

  geojson = customize_geojson(geojson, options)

  if attributes.has_key?('geojson_geom')
    # dummy geometry (ignore value from geometry column)
    geojson[:geometry] = {}

    unless options[:skip_feature_crs]
      # add GeoJSON CRS unless part of a FeatureCollection
      geojson[:crs] = {
        :type => 'name',
        :properties => {
          :name => attributes['geojson_srid']
        }
      }
    end

    # convert to JSON and replace geometry with GeoJSON field from query
    geojson.to_json.sub(/"geometry":{}/, "\"geometry\":#{attributes['geojson_geom']}")
  else
    geojson.to_json
  end
end

#update_attributes_from_geojson_feature(feature, user) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'app/models/geo_model.rb', line 247

def update_attributes_from_geojson_feature(feature, user)
  attr = feature.properties

  unless feature.geometry.nil?
    # get client_srid from RGeo geometry
    client_srid = feature.geometry.srid
    if client_srid != self.class.srid
      # transform feature geometry to srid
      geom_sql = self.class.transform_geom_sql("ST_GeomFromText('#{feature.geometry.as_text}', #{client_srid})", client_srid, self.class.srid)
      sql = "SELECT ST_AsText(#{geom_sql}) AS wkt_geom"
      results = connection.execute(sql)
      results.each do |result|
        attr[self.class.geometry_column_name] = self.class.geo_factory.parse_wkt(result['wkt_geom'])
      end
    else
      # no transformation
      attr[self.class.geometry_column_name] = feature.geometry
    end
  end

  ok = update_attributes(attr)
  modified_by(user)
  ok
end