Class: Georeference
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Georeference
- Includes:
- Housekeeping, Shared::Citations, Shared::Confidences, Shared::DataAttributes, Shared::DwcOccurrenceHooks, Shared::IsData, Shared::Maps, Shared::Notes, Shared::ProtocolRelationships, Shared::Tags, SoftValidation
- Defined in:
- app/models/georeference.rb
Overview
A Georeference is an assertion that some shape, as derived from some method, describes the location of some CollectingEvent.
A georeference contains three components:
1) A reference to a CollectingEvent (who, where, when, how)
2) A reference to a GeographicItem (a shape)
3) A method by which the shape was associated with the collecting event (via `type` subclassing).
If a georeference was published its Source can be provided. This is not equivalent to providing a method for deriving the georeference.
Contains information about a location on the face of the Earth, consisting of:
st_centroid() of the geographic item.
Defined Under Namespace
Classes: Exif, GPX, GeoLocate, GoogleMap, Leaflet, Point, VerbatimData, Wkt
Constant Summary
Constants included from SoftValidation
SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS
Instance Attribute Summary collapse
-
#api_request ⇒ String
The text of the GeoLocation request (::GeoLocate), or the verbatim data (VerbatimData).
-
#collecting_event_id ⇒ Integer
The id of a CollectingEvent which represents the event of this georeference definition.
- #day_georeferenced ⇒ Integer?
-
#error_depth ⇒ Integer
The distance in meters of the radius of the area of vertical uncertainty of the accuracy of the location of this georeference definition.
-
#error_geographic_item_id ⇒ Integer
The id of a GeographicItem which represents the (error) representation of this georeference definition.
-
#error_radius ⇒ Integer
the radius of the area of horizontal uncertainty of the accuracy of the location of this georeference definition.
-
#geographic_item_id ⇒ Integer
The id of a GeographicItem which represents the (non-error) representation of this georeference definition.
-
#iframe_response ⇒ Object
used to handle the geolocate from Tulane response.
-
#is_median_z ⇒ Boolean
True if this georeference represents an average vertical distance, otherwise false.
-
#is_public ⇒ Boolean
True if this georeference can be shared, otherwise false.
-
#is_undefined_z ⇒ Boolean
True if this georeference cannot be located vertically, otherwise false.
- #month_georeferenced ⇒ Integer?
-
#no_cached ⇒ Boolean
When true, cascading cached values (e.g. in CollectingEvent) are not built.
-
#position ⇒ Integer
An arbitrary ordering mechanism, the first georeference is routinely defaulted to in the application.
-
#project_id ⇒ Integer
the project ID.
-
#type ⇒ String
The type name of the this georeference definition.
-
#year_georeferenced ⇒ Integer?
4 digit year the georeference was first created/captured.
Class Method Summary collapse
-
.batch_create_from_georeference_matcher(arguments) ⇒ Array
TODO: not yet sure what the params are going to look like.
-
.filter_by(params) ⇒ Scope
Of selected georeferences.
- .point_type ⇒ Object
-
.with_geographic_area(geographic_area) ⇒ Scope
returns all georeferences which have collecting_events which have geographic_areas which match geographic_areas as a GeographicArea TODO: or, (in the future) a string matching a geographic_area.name.
-
.with_locality(string) ⇒ Scope
return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which equals String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”).
-
.with_locality_as(string, like) ⇒ Scope
Bool = true if ‘contains’ Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”) TODO: Arelize.
-
.with_locality_like(string) ⇒ Scope
includes String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”).
-
.within_radius_of_item(geographic_item_id, distance) ⇒ Scope
Georeferences all georeferences within some distance of a geographic_item, by id.
Instance Method Summary collapse
-
#add_err_geo_item_inside_err_radius ⇒ Boolean
protected
True iff error_radius contains error_geographic_item.
-
#add_error_depth ⇒ Boolean
protected
True iff error_depth is less than 8.8 kilometers (5.5 miles).
-
#add_error_geo_item_inside_area ⇒ Boolean
protected
True iff collecting_event area contains georeference error_geographic_item.
-
#add_error_geo_item_intersects_area ⇒ Boolean
protected
True iff collecting_event area intersects georeference error_geographic_item.
-
#add_error_radius ⇒ Boolean
protected
True iff error_radius is less than 10 kilometers (6.6 miles).
-
#add_error_radius_inside_area ⇒ Boolean
protected
True iff collecting_event area contains georeference error_radius bounding box.
-
#add_obj_inside_area ⇒ Boolean
protected
True iff collecting_event contains georeference geographic_item.
-
#add_obj_inside_err_geo_item ⇒ Boolean
protected
True iff error_geographic_item contains geographic_item.
-
#add_obj_inside_err_radius ⇒ Boolean
protected
True iff error_radius contains geographic_item.
-
#check_err_geo_item_inside_err_radius ⇒ Boolean
protected
True if error_geographic_item is completely contained in error_box.
-
#check_error_geo_item_inside_area ⇒ Boolean
protected
collecting_event.geographic_area.default_geographic_item.
-
#check_error_geo_item_intersects_area ⇒ Boolean
protected
collecting_event.geographic_area.default_geographic_item.
-
#check_error_radius_inside_area ⇒ Boolean
protected
collecting_event.geographic_area.default_geographic_item.
-
#check_obj_inside_area ⇒ Boolean
protected
.default_geographic_item.
-
#check_obj_inside_err_geo_item ⇒ Boolean
protected
True if geographic_item is completely contained in error_geographic_item.
-
#check_obj_inside_err_radius ⇒ Boolean
protected
True if geographic_item is completely contained in error_box.
-
#dwc_georeference_attributes(h = {}) ⇒ Hash
The interface to DwcOccurrence writiing for Georeference based values.
- #dwc_occurrences ⇒ Object
-
#error_box ⇒ GeographicItem?
TODO: cleanup, subclass, and calculate with SQL?.
-
#error_radius_buffer_polygon ⇒ Rgeo::polygon?
A polygon representing the buffer.
- #geographic_item_present_if_error_radius_provided ⇒ Object protected
-
#heading(from_lat_, from_lon_, to_lat_, to_lon_) ⇒ Double
protected
TODO: Should be in lib/utilities/geo.rb.
- #latitude ⇒ Float
- #longitude ⇒ Float
-
#method_name ⇒ String?
The underscored version of the type, e.g.
-
#radius_from_error_shape ⇒ Object
Calculate the radius from error shapes !! Used to retroactively rebuild the radius from the polygon shape.
- #round_error_radius ⇒ Object private
- #set_cached ⇒ Object protected
- #set_cached_collecting_event ⇒ Object protected
-
#to_geo_json_feature ⇒ Hash
Called by Gis::GeoJSON.feature_collection.
-
#to_simple_json_feature ⇒ JSON Feature
TODO: parametrize to include gazeteer i.e.
Methods included from Shared::IsData
#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_destroyable?, #is_editable?, #is_in_use?, #is_in_users_projects?, #metamorphosize, #similar
Methods included from Shared::Confidences
Methods included from Shared::DataAttributes
#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes
Methods included from Shared::Citations
#cited?, #mark_citations_for_destruction, #nomenclature_date, #origin_citation_source_id, #reject_citations, #requires_citation?, #sources_by_topic_id
Methods included from Shared::ProtocolRelationships
#protocolled?, #reject_protocols
Methods included from Shared::Tags
#reject_tags, #tag_with, #tagged?, #tagged_with?
Methods included from Shared::Notes
#concatenated_notes_string, #reject_notes
Methods included from SoftValidation
#clear_soft_validations, #fix_for, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations, #soft_validators
Methods included from Housekeeping
#has_polymorphic_relationship?
Methods inherited from ApplicationRecord
Instance Attribute Details
#api_request ⇒ String
The text of the GeoLocation request (::GeoLocate), or the verbatim data (VerbatimData).
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#collecting_event_id ⇒ Integer
The id of a CollectingEvent which represents the event of this georeference definition.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#day_georeferenced ⇒ Integer?
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#error_depth ⇒ Integer
The distance in meters of the radius of the area of vertical uncertainty of the accuracy of the location of this georeference definition.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#error_geographic_item_id ⇒ Integer
The id of a GeographicItem which represents the (error) representation of this georeference definition.
Generally, it will represent a polygon.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#error_radius ⇒ Integer
the radius of the area of horizontal uncertainty of the accuracy of the location of this georeference definition. Measured in meters. Corresponding error areas are draw from the
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#geographic_item_id ⇒ Integer
The id of a GeographicItem which represents the (non-error) representation of this georeference definition. Generally, it will represent a point.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#iframe_response ⇒ Object
used to handle the geolocate from Tulane response
90 91 92 |
# File 'app/models/georeference.rb', line 90 def iframe_response @iframe_response end |
#is_median_z ⇒ Boolean
True if this georeference represents an average vertical distance, otherwise false.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#is_public ⇒ Boolean
True if this georeference can be shared, otherwise false.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#is_undefined_z ⇒ Boolean
True if this georeference cannot be located vertically, otherwise false.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#month_georeferenced ⇒ Integer?
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#no_cached ⇒ Boolean
Returns When true, cascading cached values (e.g. in CollectingEvent) are not built.
130 131 132 |
# File 'app/models/georeference.rb', line 130 def no_cached @no_cached end |
#position ⇒ Integer
An arbitrary ordering mechanism, the first georeference is routinely defaulted to in the application.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#project_id ⇒ Integer
the project ID
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#type ⇒ String
The type name of the this georeference definition.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
#year_georeferenced ⇒ Integer?
Returns 4 digit year the georeference was first created/captured.
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 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 379 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 422 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 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/models/georeference.rb', line 75 class Georeference < ApplicationRecord include Housekeeping include SoftValidation include Shared::Notes include Shared::Tags include Shared::ProtocolRelationships include Shared::Citations include Shared::DataAttributes include Shared::Confidences # qualitative, not spatial include Shared::Maps include Shared::DwcOccurrenceHooks include Shared::IsData include Shared::Maps attr_accessor :iframe_response # used to handle the geolocate from Tulane response acts_as_list scope: [:collecting_event_id, :project_id], add_new_at: :top belongs_to :collecting_event, inverse_of: :georeferences belongs_to :error_geographic_item, class_name: 'GeographicItem', inverse_of: :georeferences_through_error_geographic_item belongs_to :geographic_item, inverse_of: :georeferences has_many :collection_objects, through: :collecting_event, inverse_of: :georeferences has_many :otus, through: :collection_objects, source: 'otus' has_many :georeferencer_roles, class_name: 'Georeferencer', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :georeference_authors, -> { order('roles.position ASC') }, through: :georeferencer_roles, source: :person # , inverse_of: :georeferences validates :year_georeferenced, date_year: {min_year: 1000, max_year: Time.now.year } validates :month_georeferenced, date_month: true validates :day_georeferenced, date_day: {year_sym: :year_georeferenced, month_sym: :month_georeferenced}, unless: -> { year_georeferenced.nil? || month_georeferenced.nil? } validates :collecting_event, presence: true validates :collecting_event_id, uniqueness: { scope: [:type, :geographic_item_id, :project_id] } validates :geographic_item, presence: true validates :type, presence: true # TODO: technically not needed validate :add_err_geo_item_inside_err_radius validate :add_error_depth validate :add_error_geo_item_intersects_area validate :add_error_radius validate :add_error_radius_inside_area validate :add_obj_inside_area validate :add_obj_inside_err_geo_item validate :add_obj_inside_err_radius validate :geographic_item_present_if_error_radius_provided # validate :add_error_geo_item_inside_area accepts_nested_attributes_for :geographic_item, :error_geographic_item # @return [Boolean] # When true, cascading cached values (e.g. in CollectingEvent) are not built attr_accessor :no_cached before_validation :round_error_radius after_save :set_cached, unless: -> { self.no_cached || (self.collecting_event && self.collecting_event.no_cached == true) } after_destroy :set_cached_collecting_event def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end # @param [Array] of parameters in the style of 'params' # @return [Scope] of selected georeferences def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end # @param [Integer] geographic_item_id # @param [Integer] distance # @return [Scope] georeferences # all georeferences within some distance of a geographic_item, by id def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end # @param [String] locality string # @return [Scope] Georeferences # all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality_like(string) with_locality_as(string, true) end # @param [String] locality string # @return [Scope] Georeferences # return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # equals String somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) def self.with_locality(string) with_locality_as(string, false) end # @param [GeographicArea] # @return [Scope] Georeferences # returns all georeferences which have collecting_events which have geographic_areas which match # geographic_areas as a GeographicArea # TODO: or, (in the future) a string matching a geographic_area.name def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end # TODO: not yet sure what the params are going to look like. what is below just represents a guess # @param [ActionController::Parameters] arguments from _collecting_event_selection form # @return [Array] of georeferences def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end # @param [String, Boolean] String to find in collecting_event.verbatim_locality, Bool = false for 'Starts with', # Bool = true if 'contains' # @return [Scope] Georeferences which are attached to a CollectingEvent which has a verbatim_locality which # includes, or is equal to 'string' somewhere # Joins collecting_event.rb and matches %String% against verbatim_locality # .where(id in CollectingEvent.where{verbatim_locality like "%var%"}) # TODO: Arelize def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end # @return [Hash] # The interface to DwcOccurrence writiing for Georeference based values. # See subclasses for super extensions. def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end # @return [String, nil] # the underscored version of the type, e.g. Georeference::GoogleMap => 'google_map' def method_name return nil if type.blank? type.demodulize.underscore end # @return [GeographicItem, nil] # a square which represents either the bounding box of the # circle represented by the error_radius, or the bounding box of the error_geographic_item # !! We assume the radius calculation is always larger (TODO: do we? discuss with Jim) # TODO: cleanup, subclass, and calculate with SQL? def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end # @return [Rgeo::polygon, nil] # a polygon representing the buffer def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end # Called by Gis::GeoJSON.feature_collection # @return [Hash] formed as a GeoJSON 'Feature' def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end # @return [Float] def latitude geographic_item.center_coords[0] end # @return [Float] def longitude geographic_item.center_coords[1] end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') # @return [JSON Feature] def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end # Calculate the radius from error shapes # !! Used to retroactively rebuild the radius from the polygon shape def radius_from_error_shape error_geographic_item&.radius end protected def set_cached_collecting_event collecting_event.send(:set_cached) end def set_cached collecting_event.send(:set_cached_geographic_names) end # validation methods # @return [Boolean] # true if geographic_item is completely contained in error_geographic_item def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] # true if geographic_item is completely contained in error_box # def check_obj_inside_err_radius # # case 2 # retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object # retval = error_box.contains?(geographic_item.geo_object) # end # retval # end # @return [Boolean] # true if geographic_item is completely contained in error_box def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item is completely contained in error_box def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end # @return [Boolean] true if error_box is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end # @return [Boolean] true if error_geographic_item.geo_object is completely contained in # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] true if error_geographic_item.geo_object intersects (overlaps) # collecting_event.geographic_area.default_geographic_item def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end # @return [Boolean] # true if geographic_item.geo_object is completely contained in collecting_event.geographic_area # .default_geographic_item def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end # @return [Boolean] true iff collecting_event contains georeference geographic_item. def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end # @return [Boolean] true iff collecting_event area contains georeference error_geographic_item. def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area intersects georeference error_geographic_item. def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end # @return [Boolean] true iff collecting_event area contains georeference error_radius bounding box. def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end # @return [Boolean] true iff error_radius contains error_geographic_item. def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end # @return [Boolean] true iff error_radius contains geographic_item. def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end # @return [Boolean] true iff error_geographic_item contains geographic_item. def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end # @return [Boolean] true iff error_depth is less than 8.8 kilometers (5.5 miles). def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end # @return [Boolean] true iff error_radius is less than 10 kilometers (6.6 miles). def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end # TODO: Should be in lib/utilities/geo.rb. # @param [Double] from_lat_ # @param [Double] from_lon_ # @param [Double] to_lat_ # @param [Double] to_lon_ # @return [Double] Heading is returned as an angle in degrees clockwise from North. def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end private def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end end |
Class Method Details
.batch_create_from_georeference_matcher(arguments) ⇒ Array
TODO: not yet sure what the params are going to look like. what is below just represents a guess
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'app/models/georeference.rb', line 202 def self.batch_create_from_georeference_matcher(arguments) gr = Georeference.find(arguments[:georeference_id].to_param) result = [] collecting_event_list = arguments[:checked_ids] unless collecting_event_list.nil? collecting_event_list.each do |event_id| new_gr = Georeference.new( collecting_event_id: event_id.to_i, geographic_item_id: gr.geographic_item_id, error_radius: gr.error_radius, error_depth: gr.error_depth, error_geographic_item_id: gr.error_geographic_item_id, type: gr.type, is_public: gr.is_public, api_request: gr.api_request, is_undefined_z: gr.is_undefined_z, is_median_z: gr.is_median_z) if new_gr.valid? # generally, this catches the case of multiple identical georeferences per collecting_event. new_gr.save! result.push new_gr end end end result end |
.filter_by(params) ⇒ Scope
Returns of selected georeferences.
144 145 146 147 148 149 |
# File 'app/models/georeference.rb', line 144 def self.filter_by(params) collecting_events = CollectingEvent.filter_by(params) georeferences = Georeference.where('collecting_event_id in (?)', collecting_events.ids) georeferences end |
.point_type ⇒ Object
138 139 140 |
# File 'app/models/georeference.rb', line 138 def self.point_type joins(:geographic_item).where(geographic_items: {type: 'GeographicItem::Point'}) end |
.with_geographic_area(geographic_area) ⇒ Scope
returns all georeferences which have collecting_events which have geographic_areas which match geographic_areas as a GeographicArea TODO: or, (in the future) a string matching a geographic_area.name
193 194 195 196 197 |
# File 'app/models/georeference.rb', line 193 def self.with_geographic_area(geographic_area) partials = CollectingEvent.where(geographic_area:) partial_gr = Georeference.where('collecting_event_id in (?)', partials.pluck(:id)) partial_gr end |
.with_locality(string) ⇒ Scope
return all Georeferences which are attached to a CollectingEvent which has a verbatim_locality which equals String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”)
184 185 186 |
# File 'app/models/georeference.rb', line 184 def self.with_locality(string) with_locality_as(string, false) end |
.with_locality_as(string, like) ⇒ Scope
Bool = true if ‘contains’ Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”) TODO: Arelize
238 239 240 241 242 243 |
# File 'app/models/georeference.rb', line 238 def self.with_locality_as(string, like) likeness = like ? '%' : '' query = "verbatim_locality #{like ? 'ilike' : '='} '#{likeness}#{string}#{likeness}'" Georeference.where(collecting_event: CollectingEvent.where(query)) end |
.with_locality_like(string) ⇒ Scope
includes String somewhere Joins collecting_event.rb and matches %String% against verbatim_locality .where(id in CollectingEvent.wherelike “%var%”)
174 175 176 |
# File 'app/models/georeference.rb', line 174 def self.with_locality_like(string) with_locality_as(string, true) end |
.within_radius_of_item(geographic_item_id, distance) ⇒ Scope
Returns georeferences all georeferences within some distance of a geographic_item, by id.
155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'app/models/georeference.rb', line 155 def self.within_radius_of_item(geographic_item_id, distance) return where(id: -1) if geographic_item_id.nil? || distance.nil? # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) # => "name='foo''bar' and group_id=4" q1 = "ST_Distance(#{GeographicItem::GEOGRAPHY_SQL}, " \ "(#{GeographicItem.select_geography_sql(geographic_item_id)})) < #{distance}" # q2 = ActiveRecord::Base.send(:sanitize_sql_array, ['ST_Distance(?, (?)) < ?', # GeographicItem::GEOGRAPHY_SQL, # GeographicItem.select_geography_sql(geographic_item_id), # distance]) Georeference.joins(:geographic_item).where(q1) end |
Instance Method Details
#add_err_geo_item_inside_err_radius ⇒ Boolean (protected)
Returns true iff error_radius contains error_geographic_item.
545 546 547 548 549 550 551 |
# File 'app/models/georeference.rb', line 545 def add_err_geo_item_inside_err_radius unless check_err_geo_item_inside_err_radius # rubocop:disable Style/GuardClause problem = 'error_radius must contain error_geographic_item.' errors.add(:error_radius, problem) errors.add(:error_geographic_item, problem) end end |
#add_error_depth ⇒ Boolean (protected)
Returns true iff error_depth is less than 8.8 kilometers (5.5 miles).
564 565 566 567 |
# File 'app/models/georeference.rb', line 564 def add_error_depth errors.add(:error_depth, 'error_depth must be less than 8.8 kilometers (5.5 miles).') if error_depth && error_depth > 8_800 # 8,800 meters end |
#add_error_geo_item_inside_area ⇒ Boolean (protected)
Returns true iff collecting_event area contains georeference error_geographic_item.
518 519 520 521 522 523 524 |
# File 'app/models/georeference.rb', line 518 def add_error_geo_item_inside_area unless check_error_geo_item_inside_area problem = 'collecting_event geographic area must contain georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end |
#add_error_geo_item_intersects_area ⇒ Boolean (protected)
Returns true iff collecting_event area intersects georeference error_geographic_item.
527 528 529 530 531 532 533 |
# File 'app/models/georeference.rb', line 527 def add_error_geo_item_intersects_area unless check_error_geo_item_intersects_area problem = 'collecting_event geographic area must intersect georeference error_geographic_item.' errors.add(:error_geographic_item, problem) errors.add(:collecting_event, problem) end end |
#add_error_radius ⇒ Boolean (protected)
Returns true iff error_radius is less than 10 kilometers (6.6 miles).
570 571 572 573 574 |
# File 'app/models/georeference.rb', line 570 def add_error_radius if error_radius.present? && error_radius > 10_000 # 10 km errors.add(:error_radius, ' must be less than 10 kilometers (6.6 miles).') end end |
#add_error_radius_inside_area ⇒ Boolean (protected)
Returns true iff collecting_event area contains georeference error_radius bounding box.
536 537 538 539 540 541 542 |
# File 'app/models/georeference.rb', line 536 def add_error_radius_inside_area unless check_error_radius_inside_area problem = 'collecting_event geographic area must contain georeference error_radius bounding box.' errors.add(:error_radius, problem) errors.add(:collecting_event, problem) # probably don't need error here end end |
#add_obj_inside_area ⇒ Boolean (protected)
Returns true iff collecting_event contains georeference geographic_item.
506 507 508 509 510 511 512 513 514 515 |
# File 'app/models/georeference.rb', line 506 def add_obj_inside_area unless check_obj_inside_area errors.add( :geographic_item, 'for georeference is not contained in the geographic area bound to the collecting event') errors.add( :collecting_event, 'is assigned a geographic area which does not contain the supplied georeference/geographic item') end end |
#add_obj_inside_err_geo_item ⇒ Boolean (protected)
Returns true iff error_geographic_item contains geographic_item.
559 560 561 |
# File 'app/models/georeference.rb', line 559 def add_obj_inside_err_geo_item errors.add(:error_geographic_item, 'must contain geographic_item.') unless check_obj_inside_err_geo_item end |
#add_obj_inside_err_radius ⇒ Boolean (protected)
Returns true iff error_radius contains geographic_item.
554 555 556 |
# File 'app/models/georeference.rb', line 554 def add_obj_inside_err_radius errors.add(:error_radius, 'must contain geographic_item.') unless check_obj_inside_err_radius end |
#check_err_geo_item_inside_err_radius ⇒ Boolean (protected)
Returns true if error_geographic_item is completely contained in error_box.
424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'app/models/georeference.rb', line 424 def check_err_geo_item_inside_err_radius # case 3 retval = true unless error_radius.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_box.contains?(error_geographic_item.geo_object) end end end retval end |
#check_error_geo_item_inside_area ⇒ Boolean (protected)
collecting_event.geographic_area.default_geographic_item
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 |
# File 'app/models/georeference.rb', line 454 def check_error_geo_item_inside_area # case 5 retval = true unless collecting_event.nil? unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false unless collecting_event.geographic_area.nil? retval = collecting_event.geographic_area.default_geographic_item .contains?(error_geographic_item.geo_object) end end end end retval end |
#check_error_geo_item_intersects_area ⇒ Boolean (protected)
collecting_event.geographic_area.default_geographic_item
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 |
# File 'app/models/georeference.rb', line 472 def check_error_geo_item_intersects_area # case 5.5 retval = true if collecting_event.present? if error_geographic_item.present? if error_geographic_item.geo_object.present? if collecting_event.geographic_area.present? # !! TODO: check fir nil case retval = collecting_event.geographic_area.default_geographic_item .intersects?(error_geographic_item.geo_object) end end end end retval end |
#check_error_radius_inside_area ⇒ Boolean (protected)
collecting_event.geographic_area.default_geographic_item
439 440 441 442 443 444 445 446 447 448 449 450 |
# File 'app/models/georeference.rb', line 439 def check_error_radius_inside_area # case 4 retval = true if collecting_event ga_gi = collecting_event.geographic_area_default_geographic_item eb = error_box if error_radius.present? # rubocop:disable Style/IfUnlessModifier retval = ga_gi.contains?(eb) if ga_gi && eb end end retval end |
#check_obj_inside_area ⇒ Boolean (protected)
.default_geographic_item
492 493 494 495 496 497 498 499 500 501 502 503 |
# File 'app/models/georeference.rb', line 492 def check_obj_inside_area # case 6 retval = true if collecting_event.present? if geographic_item.present? && collecting_event.geographic_area.present? if geographic_item.geo_object && collecting_event.geographic_area.default_geographic_item.present? retval = collecting_event.geographic_area.default_geographic_item.contains?(geographic_item.geo_object) end end end retval end |
#check_obj_inside_err_geo_item ⇒ Boolean (protected)
Returns true if geographic_item is completely contained in error_geographic_item.
380 381 382 383 384 385 386 387 388 389 390 391 |
# File 'app/models/georeference.rb', line 380 def check_obj_inside_err_geo_item # case 1 retval = true unless geographic_item.nil? || !geographic_item.geo_object unless error_geographic_item.nil? if error_geographic_item.geo_object # is NOT false retval = error_geographic_item.contains?(geographic_item.geo_object) end end end retval end |
#check_obj_inside_err_radius ⇒ Boolean (protected)
Returns true if geographic_item is completely contained in error_box.
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# File 'app/models/georeference.rb', line 406 def check_obj_inside_err_radius # case 2 retval = true # if !error_radius.blank? && geographic_item && geographic_item.geo_object if error_radius.present? if geographic_item.present? if geographic_item.geo_object.present? val = error_box if val.present? retval = val.contains?(geographic_item.geo_object) end end end end retval end |
#dwc_georeference_attributes(h = {}) ⇒ Hash
Returns The interface to DwcOccurrence writiing for Georeference based values. See subclasses for super extensions.
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'app/models/georeference.rb', line 256 def dwc_georeference_attributes(h = {}) georeferenced_by = if .any? .collect{|a| a.cached}.join('|') else creator.name end h.merge!( footprintWKT: geographic_item.to_wkt, georeferenceVerificationStatus: confidences&.collect{|c| c.name}.join('; ').presence, georeferencedBy: georeferenced_by, georeferencedDate: created_at, georeferenceProtocol: protocols.collect{|p| p.name}.join('|') ) if geographic_item.type == 'GeographicItem::Point' b = geographic_item.to_a h[:decimalLongitude] = b.first h[:decimalLatitude] = b.second h[:coordinateUncertaintyInMeters] = error_radius end h end |
#dwc_occurrences ⇒ Object
245 246 247 248 249 250 251 |
# File 'app/models/georeference.rb', line 245 def dwc_occurrences DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .joins('JOIN georeferences g on co.collecting_event_id = g.collecting_event_id') .where(g: {id:}) .distinct end |
#error_box ⇒ GeographicItem?
TODO: cleanup, subclass, and calculate with SQL?
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'app/models/georeference.rb', line 292 def error_box retval = nil if error_radius.nil? retval = error_geographic_item.dup unless error_geographic_item.nil? else unless geographic_item.nil? if geographic_item.geo_object_type case geographic_item.geo_object_type when :point retval = Utilities::Geo.error_box_for_point(geographic_item.geo_object, error_radius) when :polygon, :multi_polygon retval = geographic_item.geo_object end end end end retval end |
#error_radius_buffer_polygon ⇒ Rgeo::polygon?
Returns a polygon representing the buffer.
314 315 316 317 318 319 320 321 322 323 |
# File 'app/models/georeference.rb', line 314 def error_radius_buffer_polygon return nil if error_radius.nil? || geographic_item.nil? sql_str = ActivRecord::Base.send( :sanitize_sql_array, ['SELECT ST_Buffer(?, ?)', geographic_item.geo_object.to_s, (error_radius / Utilities::Geo::ONE_WEST_MEAN)]) value = GeographicItem.connection.select_all(sql_str).first['st_buffer'] Gis::FACTORY.parse_wkb(value) end |
#geographic_item_present_if_error_radius_provided ⇒ Object (protected)
576 577 578 579 580 581 582 |
# File 'app/models/georeference.rb', line 576 def geographic_item_present_if_error_radius_provided if error_radius.present? && geographic_item_id.blank? && # provide existing geographic_item.blank? # provide new errors.add(:error_radius, 'can only be provided when geographic item is provided') end end |
#heading(from_lat_, from_lon_, to_lat_, to_lon_) ⇒ Double (protected)
TODO: Should be in lib/utilities/geo.rb.
590 591 592 593 594 595 596 597 598 |
# File 'app/models/georeference.rb', line 590 def heading(from_lat_, from_lon_, to_lat_, to_lon_) from_lat_rad_ = RADIANS_PER_DEGREE * from_lat_ to_lat_rad_ = RADIANS_PER_DEGREE * to_lat_ delta_lon_rad_ = RADIANS_PER_DEGREE * (to_lon_ - from_lon_) y_ = ::Math.sin(delta_lon_rad_) * ::Math.cos(to_lat_rad_) x_ = ::Math.cos(from_lat_rad_) * ::Math.sin(to_lat_rad_) - ::Math.sin(from_lat_rad_) * ::Math.cos(to_lat_rad_) * ::Math.cos(delta_lon_rad_) DEGREES_PER_RADIAN * ::Math.atan2(y_, x_) end |
#latitude ⇒ Float
339 340 341 |
# File 'app/models/georeference.rb', line 339 def latitude geographic_item.center_coords[0] end |
#longitude ⇒ Float
344 345 346 |
# File 'app/models/georeference.rb', line 344 def longitude geographic_item.center_coords[1] end |
#method_name ⇒ String?
Returns the underscored version of the type, e.g. Georeference::GoogleMap => ‘google_map’.
282 283 284 285 |
# File 'app/models/georeference.rb', line 282 def method_name return nil if type.blank? type.demodulize.underscore end |
#radius_from_error_shape ⇒ Object
Calculate the radius from error shapes !! Used to retroactively rebuild the radius from the polygon shape
362 363 364 |
# File 'app/models/georeference.rb', line 362 def radius_from_error_shape error_geographic_item&.radius end |
#round_error_radius ⇒ Object (private)
602 603 604 605 606 |
# File 'app/models/georeference.rb', line 602 def round_error_radius if error_radius.present? write_attribute(:error_radius, error_radius.round) end end |
#set_cached ⇒ Object (protected)
372 373 374 |
# File 'app/models/georeference.rb', line 372 def set_cached collecting_event.send(:set_cached_geographic_names) end |
#set_cached_collecting_event ⇒ Object (protected)
368 369 370 |
# File 'app/models/georeference.rb', line 368 def set_cached_collecting_event collecting_event.send(:set_cached) end |
#to_geo_json_feature ⇒ Hash
Called by Gis::GeoJSON.feature_collection
327 328 329 330 331 332 333 334 335 336 |
# File 'app/models/georeference.rb', line 327 def to_geo_json_feature to_simple_json_feature.merge( 'properties' => { 'georeference' => { 'id' => id, 'tag' => "Georeference ID = #{id}" } } ) end |
#to_simple_json_feature ⇒ JSON Feature
TODO: parametrize to include gazeteer
i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
351 352 353 354 355 356 357 358 |
# File 'app/models/georeference.rb', line 351 def to_simple_json_feature geometry = RGeo::GeoJSON.encode(geographic_item.geo_object) { 'type' => 'Feature', 'geometry' => geometry, 'properties' => {} } end |