Class: CollectingEvent
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- CollectingEvent
- Includes:
- Housekeeping, Shared::Citations, Shared::Confidences, Shared::DataAttributes, Shared::Depictions, Shared::Documentation, Shared::HasPapertrail, Shared::HasRoles, Shared::Identifiers, Shared::IsData, Shared::Labels, Shared::Notes, Shared::Tags, SoftValidation
- Defined in:
- app/models/collecting_event.rb
Overview
A collecting event describes how something (e.g. a CollectionObject) was aquired. It is the unique combination of who, where, when, and how.
Constant Summary collapse
- NEARBY_DISTANCE =
5000
Constants included from SoftValidation
SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS
Instance Attribute Summary collapse
-
#cached ⇒ String
A string, typically sliced from verbatim_label, that represents the provided uncertainty value.
-
#cached_level0_geographic_name ⇒ String?
The auto-calculated level0 (= country in TaxonWorks) value drawn from GeographicNames, never directly user supplied.
-
#cached_level1_geographic_name ⇒ String?
The auto-calculated level1 (typically state/province) value drawn from GeographicNames, never directly user supplied.
-
#cached_level2_geographic_name ⇒ String?
The auto-calculated level2 value (e.g. county) drawn from GeographicNames, never directly user supplied.
-
#document_label ⇒ String
A print-ready expanded/clarified version of a verbatim_label intended to clarify interpretation of that label.
-
#elevation_precision ⇒ String
A float, in meters.
-
#end_date_day ⇒ Integer
The date of the month the collecting event ended on.
-
#end_date_month ⇒ Integer
The month, from 0-12, that the collecting event ended on.
-
#end_date_year ⇒ Integer
The four digit year, end of the collecting event.
-
#field_notes ⇒ String
Any/all field notes that this collecting event was derived from, or that supplement this collecting event.
-
#formation ⇒ String?
Formation sensu PBDB.
-
#geographic_area_id ⇒ Integer
The finest geo-political unit that this collecting event can be localized to, can be used for gross georeferencing when Georeference not available.
-
#group ⇒ String?
Member sensu PBDB.
-
#lithology ⇒ String?
Lithology sensu PBDB.
-
#max_ma ⇒ Decimal?
Max_ma (million years) sensu PBDB.
-
#maximum_elevation ⇒ String
A float, in meters.
-
#md5_of_verbatim_label ⇒ String
Application defined, an index to the verbatim label.
-
#member ⇒ String?
Member sensu PBDB.
-
#min_ma ⇒ Decimal?
Min_ma (million years) sensu PBDB.
-
#minimum_elevation ⇒ String
A float, in meters.
-
#no_cached ⇒ Boolean
When true, cached values are not built.
-
#no_dwc_occurrence ⇒ Boolean
When true, will not rebuild dwc_occurrence index.
-
#print_label ⇒ String
A print-formatted ready representation of this collecting event.
-
#project_id ⇒ Integer
the project ID.
-
#start_date_day ⇒ Integer
The day of the month the collecting event started on.
-
#start_date_month ⇒ Integer
The month, from 0-12, that the collecting event started on.
-
#start_date_year ⇒ Integer
The four digit year, start of the collecting event.
-
#time_end_hour ⇒ Integer
0-23.
-
#time_end_minute ⇒ Integer
0-59.
-
#time_end_second ⇒ Integer
0-59.
-
#time_start_hour ⇒ Integer
0-23.
-
#time_start_minute ⇒ Integer
0-59.
-
#time_start_second ⇒ Integer
0-59.
-
#verbatim_collectors ⇒ String
The literal string that indicates the collectors, typically taken right off the label.
-
#verbatim_date ⇒ String
The string representation, typically as taken from the label, of the date.
- #verbatim_datum ⇒ String
-
#verbatim_elevation ⇒ String
A string, typically sliced from verbatim_label, that represents all elevation data (min/max/precision) as recorded there.
-
#verbatim_geolocation_uncertainty ⇒ String
A string, typically sliced from verbatim_label, that represents the provided uncertainty value.
-
#verbatim_habitat ⇒ String
A literal string, typically taken from the printed label, tha represents assertions about the habitat.
-
#verbatim_label ⇒ String
A verbatim representation of label that defined this collecting event, typically, but not exclusively, used for retroactive data capture.
-
#verbatim_latitude ⇒ String
A string, typically sliced from verbatim_label, that represents the latitude.
-
#verbatim_locality ⇒ String
A string, typically sliced from verbatim_label, that represents the locality, including any modifiers (2 mi NE).
-
#verbatim_longitude ⇒ String
A string, typically sliced from verbatim_label, that represents the longitude.
-
#verbatim_method ⇒ String
The literal string that indicates the collecting method, typically taken right off the label.
-
#verbatim_trip_identifier ⇒ String
The literal string/identifier used by the collector(s) to identify this particular collecting event, usually part of a series particular to one trip.
-
#with_verbatim_data_georeference ⇒ Object
Returns the value of attribute with_verbatim_data_georeference.
Class Method Summary collapse
-
.contained_within(geographic_item) ⇒ Scope
TODO: use joins(:geographic_items).where(containing scope), simplied to.
-
.filter_by(params) ⇒ Scope
TODO: deprecate for lib/queries/collecting_event.
-
.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on') ⇒ Scope
TODO: remove all of this for direct call to Queries::CollectingEvent::Filter.
-
.not_including(collecting_events) ⇒ Scope
TODO: DRY, use general form of this.
-
.select_optimized(user_id, project_id) ⇒ Object
Scopes.
Instance Method Summary collapse
-
#all_geographic_items ⇒ Scope
All geographic_items associated with this collecting_event through georeferences only.
-
#build_verbatim_geographic_item ⇒ GeographicItem?
A GeographicItem instance representing a translation of the verbatim values, not saved.
- #cache_geographic_names(values = {}, tried = false) ⇒ Object
- #cached_geographic_name_classification ⇒ Object
- #cached_level0_name ⇒ Object
- #check_date_range ⇒ Object protected
- #check_elevation_range ⇒ Object protected
- #check_ma_range ⇒ Object protected
- #check_verbatim_geolocation_uncertainty ⇒ Object protected
-
#clone ⇒ CollectingEvent
The instance may not be valid!.
-
#collecting_events_contained_in_error ⇒ Scope
Find other CEs that have GRs whose GIs or EGIs are contained in the EGI.
-
#collecting_events_intersecting_with ⇒ Scope
Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self.
- #collecting_events_within_radius_of(distance) ⇒ Scope
-
#collector_names ⇒ String?
yes, it's a Helper.
-
#containing_geographic_items ⇒ Array of GeographicItems containing this target
GeographicItems are those that contain either the georeference or, if there are none, the geographic area.
-
#counties_hash ⇒ Hash
returns either: ( => [GAs] or [=> [GAs], => [GAs]]) one hash, consisting of a county name paired with an array of the corresponding GAs, or an array of all of the hashes (name/GA pairs), which are county_level, and have GIs containing the (GI and/or EGI) of this CE.
- #countries_hash ⇒ Hash
- #country_name ⇒ String
- #county_or_equivalent_name ⇒ String (also: #county_name)
-
#date_range ⇒ Array
Date_start and end if provided.
-
#distance_to(geographic_item_id) ⇒ String
See how far away we are from another gi.
-
#end_date ⇒ Time
This is for the purposes of computation, not display!.
-
#end_date_string ⇒ String?
An umabigously formatted string with missing parts indicated by ??.
-
#generate_verbatim_data_georeference(reference_self = false, no_cached: false) ⇒ Georeference::VerbatimData, false
CollectingEvent.select {|d| !(d.verbatim_latitude.nil? || d.verbatim_longitude.nil?)} .select {|ce| ce.georeferences.empty?}.
-
#geographic_area_default_geographic_item ⇒ GeographicItem?
Returns the geographic_item corresponding to the geographic area, if provided.
-
#geographic_name_classification ⇒ Hash
Classifies this collecting event into country, state, county categories.
-
#geographic_name_classification_method ⇒ Symbol?
Determines (prioritizes) the method to be used to decided the geographic name classification (string labels for country, state, county) for this collecting_event.
-
#geolocate_attributes ⇒ Hash
rubocop:disable Style/StringHashKeys.
-
#geolocate_ui_params ⇒ Hash
A complete set of params necessary to form a request string.
- #geolocate_ui_params_string ⇒ String
- #georeference_latitude ⇒ Object
- #georeference_longitude ⇒ Object
-
#get_error_radius ⇒ Integer
@TODO: See Utilities::Geo.distance_in_meters(String).
- #get_geographic_name_classification ⇒ Object
- #has_cached_geographic_names? ⇒ Boolean
- #has_collectors? ⇒ Boolean
- #has_data? ⇒ Boolean
-
#has_end_date? ⇒ Boolean
Has a fully defined date.
- #has_some_date? ⇒ Boolean
-
#has_start_date? ⇒ Boolean
Has a fully defined date.
-
#lat_long_source ⇒ Symbol?
Prioritizes and identifies the source of the latitude/longitude values that will be calculated for DWCA and primary display.
- #latitude ⇒ Object
- #level0_name ⇒ Object
- #level1_name ⇒ Object
- #level2_name ⇒ Object
- #longitude ⇒ Object
- #map_center ⇒ Rgeo::Geographic::ProjectedPointImpl?
-
#map_center_method ⇒ Symbol?
The name of the method that will return an Rgeo object that represent the “preferred” centroid for this collecting event.
- #name_from_geopolitical_hash(name_hash) ⇒ String
-
#name_hash(types) ⇒ Hash
or [=> [GAs], => [GAs]] ) one hash, consisting of a country name paired with an array of the corresponding GAs, or an array of all of the hashes (name/GA pairs), which are country_level, and have GIs containing the (GI and/or EGI) of this CE.
- #names ⇒ Object
-
#nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) ⇒ Scope
DEPRECATED for shared code.
-
#next_without_georeference ⇒ CollectingEvent
Return the next collecting event without a georeference in this collecting events project sort order 1.
- #set_cached ⇒ Object protected
- #set_times_to_nil_if_form_provided_blank ⇒ Object protected
-
#similar_lat_longs(lat, long, project_id, piece = '', include_values = true) ⇒ Scope
Of matching collecting events TODO: deprecate.
- #some_end_date? ⇒ Boolean
- #some_start_date? ⇒ Boolean
-
#start_date ⇒ Time
This is for the purposes of computation, not display!.
-
#start_date_string ⇒ String?
An umabigously formatted string with missing parts indicated by ??.
- #state_name ⇒ String
- #state_or_province_name ⇒ String
-
#states_hash ⇒ Hash
returns either: ( => [GAs] or [=> [GAs], => [GAs]]) one hash, consisting of a state name paired with an array of the corresponding GAs, or an array of all of the hashes (name/GA pairs), which are state_level, and have GIs containing the (GI and/or EGI) of this CE.
- #sv_georeference_matches_verbatim ⇒ Object protected
- #sv_minimally_check_for_a_label ⇒ Object protected
-
#time_end ⇒ String
Like 00, 00:00, or 00:00:00.
-
#time_range ⇒ Array
Time_start and end if provided.
-
#time_start ⇒ String
Like 00, 00:00, or 00:00:00.
-
#to_geo_json_feature ⇒ GeoJSON::Feature
The first geographic item of the first georeference on this collecting event.
-
#to_simple_json_feature ⇒ Object
TODO: parametrize to include gazeteer i.e.
- #update_dwc_occurrences ⇒ Object
-
#verbatim_center_coordinates ⇒ String
Coordinates for centering a Google map.
-
#verbatim_map_center(delta_z = 0.0) ⇒ RGeo::Geographic::ProjectedPointImpl?
For the verbatim latitude/longitude only.
Methods included from SoftValidation
#clear_soft_validations, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations
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::Documentation
#document_array=, #documented?, #reject_documentation, #reject_documents
Methods included from Shared::Confidences
Methods included from Shared::Labels
Methods included from Shared::Depictions
#has_depictions?, #image_array=, #reject_depictions, #reject_images
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 Shared::Identifiers
#identified?, #next_by_identifier, #previous_by_identifier, #reject_identifiers
Methods included from Shared::HasRoles
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 Housekeeping
#has_polymorphic_relationship?
Methods inherited from ApplicationRecord
Instance Attribute Details
#cached ⇒ String
A string, typically sliced from verbatim_label, that represents the provided uncertainty value.
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 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 |
# File 'app/models/collecting_event.rb', line 179 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::HasRoles include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::HasPapertrail include Shared::IsData include SoftValidation ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, will not rebuild dwc_occurrence index. # See also Shared::IsDwcOccurrence attr_accessor :no_dwc_occurrence # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # handle_asynchronously :update_dwc_occurrences, run_at: Proc.new { 20.seconds.from_now } belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy has_one :verbatim_data_georeference, class_name: 'Georeference::VerbatimData' has_one :preferred_georeference, -> { order(:position) }, class_name: 'Georeference', foreign_key: :collecting_event_id has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy has_many :collectors, through: :collector_roles, source: :person, inverse_of: :collecting_events has_many :dwc_occurrences, through: :collection_objects has_many :georeferences, dependent: :destroy, inverse_of: :collecting_event has_many :error_geographic_items, through: :georeferences, source: :error_geographic_item has_many :geographic_items, through: :georeferences # See also all_geographic_items, the union has_many :geo_locate_georeferences, class_name: '::Georeference::GeoLocate', dependent: :destroy has_many :gpx_georeferences, class_name: 'Georeference::GPX', dependent: :destroy has_many :otus, through: :collection_objects after_create do if with_verbatim_data_georeference generate_verbatim_data_georeference(true) end end before_save :set_times_to_nil_if_form_provided_blank after_save :cache_geographic_names, if: -> { !no_cached && saved_change_to_attribute?(:geographic_area_id) } after_save :set_cached, unless: -> { no_cached } after_save :update_dwc_occurrences , unless: -> { no_dwc_occurrence } def update_dwc_occurrences # reload is required! if collection_objects.count < 40 collection_objects.reload.each do |o| o.set_dwc_occurrence end end end accepts_nested_attributes_for :verbatim_data_georeference accepts_nested_attributes_for :geo_locate_georeferences accepts_nested_attributes_for :gpx_georeferences accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_ma_range validates_uniqueness_of :md5_of_verbatim_label, scope: [:project_id], unless: -> { verbatim_label.blank? } validates_presence_of :verbatim_longitude, if: -> { !verbatim_latitude.blank? } validates_presence_of :verbatim_latitude, if: -> { !verbatim_longitude.blank? } validates :geographic_area, presence: true, allow_nil: true validates :time_start_hour, time_hour: true validates :time_start_minute, time_minute: true validates :time_start_second, time_second: true validates_presence_of :time_start_minute, if: -> { !self.time_start_second.blank? } validates_presence_of :time_start_hour, if: -> { !self.time_start_minute.blank? } validates :time_end_hour, time_hour: true validates :time_end_minute, time_minute: true validates :time_end_second, time_second: true validates_presence_of :time_end_minute, if: -> { !self.time_end_second.blank? } validates_presence_of :time_end_hour, if: -> { !self.time_end_minute.blank? } validates :start_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :end_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :start_date_month, date_month: true validates :end_date_month, date_month: true validates_presence_of :start_date_month, if: -> { !start_date_day.nil? } validates_presence_of :end_date_month, if: -> { !end_date_day.nil? } validates :end_date_day, date_day: {year_sym: :end_date_year, month_sym: :end_date_month}, unless: -> { end_date_year.nil? || end_date_month.nil? } validates :start_date_day, date_day: {year_sym: :start_date_year, month_sym: :start_date_month}, unless: -> { start_date_year.nil? || start_date_month.nil? } soft_validate(:sv_minimally_check_for_a_label) soft_validate(:sv_georeference_matches_verbatim, set: :georeference, has_fix: false) # @param [String] def verbatim_label=(value) write_attribute(:verbatim_label, value) write_attribute(:md5_of_verbatim_label, Utilities::Strings.generate_md5(value)) end scope :used_recently, -> { joins(:collection_objects).includes(:collection_objects).where(collection_objects: { created_at: 1.weeks.ago..Time.now } ).order('"collection_objects"."created_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { project_id: project_id } ) } class << self # # Scopes # def select_optimized(user_id, project_id) h = { recent: (CollectingEvent.used_in_project(project_id) .where(collection_objects: {updated_by_id: user_id}) .used_recently .distinct .limit(5) .order(:cached) .to_a + CollectingEvent.where(project_id: project_id, updated_by_id: user_id, created_at: 3.hours.ago..Time.now).limit(5).to_a).uniq, pinboard: CollectingEvent.pinned_by(user_id).pinned_in_project(project_id).to_a } h[:quick] = (CollectingEvent.pinned_by(user_id).pinboard_inserted.pinned_in_project(project_id).to_a + h[:recent]).uniq h end # @param [GeographicItem] geographic_item # @return [Scope] # TODO: use joins(:geographic_items).where(containing scope), simplied to def contained_within(geographic_item) CollectingEvent.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)) end # @param [CollectingEvent Scope] collecting_events # @return [Scope] without self (if included) # TODO: DRY, use general form of this def not_including(collecting_events) where.not(id: collecting_events) end # # Other # # @param [String] search_start_date string in form 'yyyy/mm/dd' # @param [String] search_end_date string in form 'yyyy/mm/dd' # @param [String] partial_overlap 'on' or 'off' # @return [Scope] of selected collecting events # TODO: remove all of this for direct call to Queries::CollectingEvent::Filter def in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on') allow_partial = (partial_overlap.downcase == 'off' ? false : true) q = Queries::CollectingEvent::Filter.new(start_date: search_start_date, end_date: search_end_date, partial_overlap_dates: allow_partial) where(q.between_date_range.to_sql).distinct # TODO: uniq should likely not be here end # @param [ActionController::Parameters] params in the style Rails of 'params' # @return [Scope] of selected collecting_events # TODO: deprecate for lib/queries/collecting_event def filter_by(params) sql_string = '' unless params.blank? # not strictly necessary, but handy for debugging sql_string = Utilities::Dates.date_sql_from_params(params) # processing text data v_locality_fragment = params['verbatim_locality_text'] any_label_fragment = params['any_label_text'] id_fragment = params['identifier_text'] prefix = '' unless v_locality_fragment.blank? unless sql_string.blank? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' unless any_label_fragment.blank? unless sql_string.blank? prefix = 'and ' end sql_string += "#{ prefix }(verbatim_label ilike '%#{any_label_fragment}%'" sql_string += " or print_label ilike '%#{any_label_fragment}%'" sql_string += " or document_label ilike '%#{any_label_fragment}%'" sql_string += ')' end unless id_fragment.blank? # @todo this still needs to be dealt with end end # find the records if sql_string.blank? collecting_events = CollectingEvent.none else collecting_events = CollectingEvent.where(sql_string).distinct end collecting_events end end # end Class methods # @param [String] lat # @param [String] long # @param [String] piece # @param [Integer] project_id # @param [Boolean] include_values true if to include records whicgh already have verbatim lat/longs # @return [Scope] of matching collecting events # TODO: deprecate def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" unless lat.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" unless long.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" unless piece.blank? sql += ')' sql += ' and (verbatim_latitude is null or verbatim_longitude is null)' unless include_values retval = CollectingEvent.where(sql) .with_project_id(project_id) .order(:id) .where.not(id: id).distinct retval end # @return [Boolean] def has_data? CollectingEvent.data_attributes.each do |a| return true unless self.send(a).blank? end return true if georeferences.any? false end def has_some_date? !verbatim_date.blank? || some_start_date? || some_end_date? end def has_collectors? !verbatim_collectors.blank? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? !start_date_day.blank? && !start_date_month.blank? && !start_date_year.blank? end # @return [Boolean] # has a fully defined date def has_end_date? !end_date_day.blank? && !end_date_month.blank? && !end_date_year.blank? end def some_start_date? [start_date_day, start_date_month, start_date_year].compact.any? end def some_end_date? [end_date_day, end_date_month, end_date_year].compact.any? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def start_date_string Utilities::Dates.from_parts(start_date_year, start_date_month, start_date_day) if some_start_date? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def end_date_string Utilities::Dates.from_parts(end_date_year, end_date_month, end_date_day) if some_end_date? end # @return [Time] # This is for the purposes of computation, not display! def end_date Utilities::Dates.nomenclature_date(end_date_day, end_date_month, end_date_year) end # @return [Time] # This is for the purposes of computation, not display! def start_date Utilities::Dates.nomenclature_date(start_date_day, start_date_month, start_date_year) end # @return [String] # like 00, 00:00, or 00:00:00 def time_start Utilities::Dates.format_to_hours_minutes_seconds(time_start_hour, time_start_minute, time_start_second) end # @return [String] # like 00, 00:00, or 00:00:00 def time_end Utilities::Dates.format_to_hours_minutes_seconds(time_end_hour, time_end_minute, time_end_second) end # @return [Array] # time_start and end if provided def time_range [time_start, time_end].compact end # @return [Array] # date_start and end if provided def date_range [start_date_string, end_date_string].compact end # CollectingEvent.select {|d| !(d.verbatim_latitude.nil? || d.verbatim_longitude.nil?)} # .select {|ce| ce.georeferences.empty?} # @param [Boolean] reference_self # @param [Boolean] no_cached # @return [Georeference::VerbatimData, false] # generates (creates) a Georeference::VerbatimReference from verbatim_latitude and verbatim_longitude values def generate_verbatim_data_georeference(reference_self = false, no_cached: false) return false if (verbatim_latitude.nil? || verbatim_longitude.nil?) begin CollectingEvent.transaction do vg_attributes = {collecting_event_id: id.to_s, no_cached: no_cached} vg_attributes.merge!(by: self.creator.id, project_id: self.project_id) if reference_self a = Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue raise end false end # @return [GeographicItem, nil] # a GeographicItem instance representing a translation of the verbatim values, not saved def build_verbatim_geographic_item if self.verbatim_latitude && self.verbatim_longitude && !self.new_record? local_latitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude) local_longitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude) elev = Utilities::Geo.distance_in_meters(verbatim_elevation).to_f point = Gis::FACTORY.point(local_latitude, local_longitude, elev) GeographicItem.new(point: point) else nil end end # @return [Integer] # @todo figure out how to convert verbatim_geolocation_uncertainty in different units (ft, m, km, mi) into meters # @TODO: See Utilities::Geo.distance_in_meters(String) def get_error_radius return nil if verbatim_geolocation_uncertainty.blank? return verbatim_geolocation_uncertainty.to_i if is.number?(verbatim_geolocation_uncertainty) nil end # @return [Scope] # all geographic_items associated with this collecting_event through georeferences only def all_geographic_items GeographicItem. joins('LEFT JOIN georeferences g2 ON geographic_items.id = g2.error_geographic_item_id'). joins('LEFT JOIN georeferences g1 ON geographic_items.id = g1.geographic_item_id'). where(['(g1.collecting_event_id = ? OR g2.collecting_event_id = ?) AND (g1.geographic_item_id IS NOT NULL OR g2.error_geographic_item_id IS NOT NULL)', self.id, self.id]) end # @return [GeographicItem, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item try(:geographic_area).try(:default_geographic_item) end # @param [GeographicItem] # @return [String] # see how far away we are from another gi def distance_to(geographic_item_id) GeographicItem.distance_between(preferred_georeference.geographic_item_id, geographic_item_id) end # @param [Double] distance in meters # @return [Scope] def collecting_events_within_radius_of(distance) return CollectingEvent.none if !preferred_georeference geographic_item_id = preferred_georeference.geographic_item_id # geographic_item_id = preferred_georeference.try(:geographic_item_id) # geographic_item_id = Georeference.where(collecting_event_id: id).first.geographic_item_id if geographic_item_id.nil? CollectingEvent.not_self(self) .joins(:geographic_items) .where(GeographicItem.within_radius_of_item_sql(geographic_item_id, distance)) end # @return [Scope] # Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self def collecting_events_intersecting_with pieces = GeographicItem.with_collecting_event_through_georeferences.intersecting('any', self.geographic_items.first).distinct gr = [] # all collecting events for a geographic_item pieces.each { |o| gr.push(o.collecting_events_through_georeferences.to_a) gr.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo change 'id in (?)' to some other sql construct pieces = CollectingEvent.where(id: gr.flatten.map(&:id).uniq) pieces.not_including(self) end # @return [Scope] # Find other CEs that have GRs whose GIs or EGIs are contained in the EGI def collecting_events_contained_in_error # find all the GIs and EGIs associated with CEs # TODO: this will be impossibly slow in present form pieces = GeographicItem.with_collecting_event_through_georeferences.to_a me = self.error_geographic_items.first.geo_object gi = [] # collect all the GIs which are within the EGI pieces.each { |o| gi.push(o) if o.geo_object.within?(me) } # collect all the CEs which refer to these GIs ce = [] gi.each { |o| ce.push(o.collecting_events_through_georeferences.to_a) ce.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo Directly map this pieces = CollectingEvent.where(id: ce.flatten.map(&:id).uniq) pieces.not_including(self) end # DEPRECATED for shared code # @param [String, String, Integer] # @return [Scope] def nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) return CollectingEvent.none if compared_string.nil? order_str = CollectingEvent.send(:sanitize_sql_for_conditions, ["levenshtein(collecting_events.#{column}, ?)", compared_string]) CollectingEvent.where('id <> ?', id.to_s). order(Arel.sql(order_str)). limit(limit) end # @param [String] # one or more names from GeographicAreaType # @return [Hash] # ( # {'name' => [GAs]} # or # [{'name' => [GAs]}, {'name' => [GAs]}] # ) # one hash, consisting of a country name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are country_level, and have GIs containing the (GI and/or EGI) of this CE # @todo this needs more work, possibily direct AREL table manipulation. def name_hash(types) retval = {} gi_list = containing_geographic_items # there are a few ways we can end up with no GIs # unless gi_list.nil? # no references GeographicAreas or Georeferences at all, or unless gi_list.empty? # no available GeographicItems to test # map the resulting GIs to their corresponding GAs # pieces = GeographicItem.where(id: gi_list.flatten.map(&:id).uniq) # pieces = gi_list ga_list = GeographicArea.joins(:geographic_area_type, :geographic_areas_geographic_items). where(geographic_area_types: {name: types}, geographic_areas_geographic_items: {geographic_item_id: gi_list}).distinct # WAS: now find all of the GAs which have the same names as the ones we collected. # map the names to an array of results ga_list.each { |i| retval[i.name] ||= [] # if we haven't come across this name yet, set it to point to a blank array retval[i.name].push i # we now have at least a blank array, push the result into it } end # end retval end # @return [Hash] # classifies this collecting event into country, state, county categories def geographic_name_classification # if names are stored in the database, and the the geographic_area_id has not changed if has_cached_geographic_names? && !geographic_area_id_changed? return cached_geographic_name_classification else r = get_geographic_name_classification cache_geographic_names(r, true) end end def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference # quick r = preferred_georeference.geographic_item.quick_geographic_name_hierarchy # almost never the case, UI not setup to do this # slow r = preferred_georeference.geographic_item.inferred_geographic_name_hierarchy if r == {} # therefor defaults to slow when :geographic_area_with_shape # geographic_area.try(:has_shape?) # quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow r = geographic_area.default_geographic_item.inferred_geographic_name_hierarchy if r == {} when :geographic_area # elsif geographic_area # quick r = geographic_area.geographic_name_classification when :verbatim_map_center # elsif map_center # slowest r = GeographicItem.point_inferred_geographic_name_hierarchy(verbatim_map_center) end r ||= {} r end def has_cached_geographic_names? cached_geographic_name_classification != {} end def cached_geographic_name_classification h = {} h[:country] = cached_level0_geographic_name if cached_level0_geographic_name h[:state] = cached_level1_geographic_name if cached_level1_geographic_name h[:county] = cached_level2_geographic_name if cached_level2_geographic_name h end def cache_geographic_names(values = {}, tried = false) # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? && !tried return {} if values.empty? update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county] ) values end # @return [Symbol, nil] # determines (prioritizes) the method to be used to decided the geographic name classification # (string labels for country, state, county) for this collecting_event. def geographic_name_classification_method return :preferred_georeference if preferred_georeference return :geographic_area_with_shape if geographic_area.try(:has_shape?) return :geographic_area if geographic_area return :verbatim_map_center if verbatim_map_center nil end # @return [Array of GeographicItems containing this target] # GeographicItems are those that contain either the georeference or, if there are none, # the geographic area def containing_geographic_items gi_list = [] if self.georeferences.any? # gather all the GIs which contain this GI or EGI # # Struck EGI, EGI must contain GI, therefor anything that contains EGI contains GI, threfor containing GI will always be the bigger set # !! and there was no tests broken # GeographicItem.are_contained_in_item('any_poly', self.geographic_items.to_a).pluck(:id).uniq gi_list = GeographicItem.containing(*geographic_items.pluck(:id)).pluck(:id).uniq else # use geographic_area only if there are no GIs or EGIs unless self.geographic_area.nil? # unless self.geographic_area.geographic_items.empty? # we need to use the geographic_area directly gi_list = GeographicItem.are_contained_in_item('any_poly', self.geographic_area.geographic_items).pluck(:id).uniq # end end end gi_list end # @return [Hash] def countries_hash name_hash(GeographicAreaType::COUNTRY_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a state name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are state_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def states_hash name_hash(GeographicAreaType::STATE_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a county name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are county_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def counties_hash name_hash(GeographicAreaType::COUNTY_LEVEL_TYPES) end # @param [Hash] # @return [String] def name_from_geopolitical_hash(name_hash) return nil if name_hash.keys.count == 0 return name_hash.keys.first if name_hash.keys.count == 1 most_key = nil most_count = 0 name_hash.keys.sort.each do |k| # alphabetically first (keys are unordered) if name_hash[k].size > most_count most_count = name_hash[k].size most_key = k end end most_key end # @return [String] def country_name name_from_geopolitical_hash(countries_hash) end # @return [String] def state_or_province_name name_from_geopolitical_hash(states_hash) end # @return [String] def state_name state_or_province_name end # @return [String] def county_or_equivalent_name name_from_geopolitical_hash(counties_hash) end alias county_name county_or_equivalent_name # @return [Symbol, nil] # prioritizes and identifies the source of the latitude/longitude values that # will be calculated for DWCA and primary display def lat_long_source if preferred_georeference :georeference elsif verbatim_latitude && verbatim_longitude :verbatim elsif geographic_area && geographic_area.has_shape? :geographic_area else nil end end =begin # @todo @mjy: please fill in any other paths you can think of for the acquisition of information for the seven below listed items ce.georeference.geographic_item.centroid ce.georeference.error_geographic_item.centroid ce.verbatim_georeference ce.preferred_georeference ce.georeference.first ce.verbatim_lat/ee.verbatim_lng ce.verbatim_locality ce.geographic_area.geographic_item.centroid There are a number of items we can try to get data for to complete the geolocate parameter string: 'country' can come from: GeographicArea through ce.country_name 'state' can come from: GeographicArea through ce.state_or_province_name 'county' can come from: GeographicArea through ce.county_or_equivalent_name 'locality' can come from: ce.verbatim_locality 'Latitude', 'Longitude' can come from: GeographicItem through ce.georeferences.geographic_item.centroid GeographicItem through ce.georeferences.error_geographic_item.centroid GeographicArea through ce.geographic_area.geographic_area_map_focus 'Placename' can come from: ? Copy of 'locality' =end # rubocop:disable Style/StringHashKeys # @return [Hash] # parameters from collecting event that are of use to geolocate def geolocate_attributes parameters = { 'country' => country_name, 'state' => state_or_province_name, 'county' => county_or_equivalent_name, 'locality' => verbatim_locality, 'Placename' => verbatim_locality, } focus = case lat_long_source when :georeference preferred_georeference.geographic_item when :geographic_area geographic_area.geographic_area_map_focus else nil end parameters.merge!( 'Longitude' => focus.point.x, 'Latitude' => focus.point.y ) unless focus.nil? parameters end def latitude verbatim_map_center.try(:y) end def longitude verbatim_map_center.try(:x) end # @return [Hash] # a complete set of params necessary to form a request string def geolocate_ui_params Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_hash end # @return [String] def geolocate_ui_params_string Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_string end # @return [GeoJSON::Feature] # the first geographic item of the first georeference on this collecting event def to_geo_json_feature # !! avoid loading the whole geographic item, just grab the bits we need: # self.georeferences(true) # do this to to_simple_json_feature.merge({ 'properties' => { 'collecting_event' => { 'id' => self.id, 'tag' => "Collecting event #{self.id}." } } }) end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') def to_simple_json_feature base = { 'type' => 'Feature', 'properties' => {} } if geographic_items.any? geo_item_id = geographic_items.select(:id).first.id query = "ST_AsGeoJSON(#{GeographicItem::GEOMETRY_SQL.to_sql}::geometry) geo_json" base['geometry'] = JSON.parse(GeographicItem.select(query).find(geo_item_id).geo_json) end base end # rubocop:enable Style/StringHashKeys # @return [CollectingEvent] # return the next collecting event without a georeference in this collecting events project sort order # 1. verbatim_locality # 2. geography_id # 3. start_date_year # 4. updated_on # 5. id def next_without_georeference CollectingEvent.not_including(self). includes(:georeferences). where(project_id: self.project_id, georeferences: {collecting_event_id: nil}). order(:verbatim_locality, :geographic_area_id, :start_date_year, :updated_at, :id). first end # @param [Float] delta_z, will be used to fill in the z coordinate of the point # @return [RGeo::Geographic::ProjectedPointImpl, nil] # for the *verbatim* latitude/longitude only def verbatim_map_center(delta_z = 0.0) retval = nil unless verbatim_latitude.blank? or verbatim_longitude.blank? lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude.to_s) long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude.to_s) elev = Utilities::Geo.distance_in_meters(verbatim_elevation.to_s).to_f delta_z = elev unless elev == 0.0 # Meh, BAD! must be nil retval = Gis::FACTORY.point(long, lat, delta_z) end retval end # @return [Symbol, nil] # the name of the method that will return an Rgeo object that represent # the "preferred" centroid for this collecting event def map_center_method return :preferred_georeference if preferred_georeference # => { georeferenceProtocol => ? } return :verbatim_map_center if verbatim_map_center # => { } return :geographic_area if geographic_area.try(:has_shape?) nil end # @return [Rgeo::Geographic::ProjectedPointImpl, nil] def map_center case map_center_method when :preferred_georeference preferred_georeference.geographic_item.centroid when :verbatim_map_center verbatim_map_center when :geographic_area geographic_area.default_geographic_item.geo_object.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } end def georeference_latitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].latitude.to_f end retval.round(6) end def georeference_longitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].longitude.to_f end retval.round(6) end # @return [String] # coordinates for centering a Google map def verbatim_center_coordinates if self.verbatim_latitude.blank? || self.verbatim_longitude.blank? 'POINT (0.0 0.0 0.0)' else self.verbatim_map_center.to_s end end def level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:country] end def level1_name return cached_level1_name if cached_level1_name cache_geographic_names[:state] end def level2_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end def cached_level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end # @return [CollectingEvent] # the instance may not be valid! def clone a = dup a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') roles.each do |r| a.collector_roles.build(person: r.person, position: r.position) end if georeferences.load.any? not_georeference_attributes = %w{created_at updated_at project_id updated_by_id created_by_id collecting_event_id id position} georeferences.each do |g| c = g.dup.attributes.select{|c| !not_georeference_attributes.include?(c) } a.georeferences.build(c) end end a.save a end # @return [String, nil] # a string used in DWC reportedBy and ultimately label generation # TODO: include initials when we find out a clean way of producing them # yes, it's a Helper def collector_names [Utilities::Strings.(collectors.collect{|a| a.last_name}), verbatim_collectors].compact.first end protected def set_cached v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = cached_geographic_name_classification.values.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? update_columns(cached: string) end def set_times_to_nil_if_form_provided_blank matches = ['0001-01-01 00:00:00 UTC', '2000-01-01 00:00:00 UTC'] write_attribute(:time_start, nil) if matches.include?(self.time_start.to_s) write_attribute(:time_end, nil) if matches.include?(self.time_end.to_s) end def check_verbatim_geolocation_uncertainty errors.add(:verbatim_geolocation_uncertainty, 'Provide both verbatim_latitude and verbatim_longitude if you provide verbatim_uncertainty.') if !verbatim_geolocation_uncertainty.blank? && verbatim_longitude.blank? && verbatim_latitude.blank? end def check_date_range begin errors.add(:base, 'End date is earlier than start date.') if has_start_date? && has_end_date? && (start_date > end_date) rescue errors.add(:base, 'Start and/or end date invalid.') end errors.add(:base, 'End date without start date.') if (has_end_date? && !has_start_date?) end def check_ma_range errors.add(:min_ma, 'Min ma is < Max ma.') if min_ma.present? && max_ma.present? && min_ma > max_ma end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if !minimum_elevation.blank? && !maximum_elevation.blank? && maximum_elevation < minimum_elevation end def sv_georeference_matches_verbatim if a = georeferences.where(type: 'Georeference::VerbatimData').first d_lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude).to_f d_long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude).to_f if (a.latitude.to_f != d_lat) soft_validations.add( :base, "Verbatim latitude #{verbatim_latitude}: (#{d_lat}) and point geoference latitude #{a.latitude} do not match") end if (a.longitude.to_f != d_long) soft_validations.add( :base, "Verbatim longitude #{verbatim_longitude}: (#{d_long}) and point geoference longitude #{a.longitude} do not match") end end end def sv_minimally_check_for_a_label [:verbatim_label, :print_label, :document_label, :field_notes].each do |v| return true if !self.send(v).blank? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end end |
#cached_level0_geographic_name ⇒ String?
Returns the auto-calculated level0 (= country in TaxonWorks) value drawn from GeographicNames, never directly user supplied.
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 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 |
# File 'app/models/collecting_event.rb', line 179 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::HasRoles include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::HasPapertrail include Shared::IsData include SoftValidation ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, will not rebuild dwc_occurrence index. # See also Shared::IsDwcOccurrence attr_accessor :no_dwc_occurrence # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # handle_asynchronously :update_dwc_occurrences, run_at: Proc.new { 20.seconds.from_now } belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy has_one :verbatim_data_georeference, class_name: 'Georeference::VerbatimData' has_one :preferred_georeference, -> { order(:position) }, class_name: 'Georeference', foreign_key: :collecting_event_id has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy has_many :collectors, through: :collector_roles, source: :person, inverse_of: :collecting_events has_many :dwc_occurrences, through: :collection_objects has_many :georeferences, dependent: :destroy, inverse_of: :collecting_event has_many :error_geographic_items, through: :georeferences, source: :error_geographic_item has_many :geographic_items, through: :georeferences # See also all_geographic_items, the union has_many :geo_locate_georeferences, class_name: '::Georeference::GeoLocate', dependent: :destroy has_many :gpx_georeferences, class_name: 'Georeference::GPX', dependent: :destroy has_many :otus, through: :collection_objects after_create do if with_verbatim_data_georeference generate_verbatim_data_georeference(true) end end before_save :set_times_to_nil_if_form_provided_blank after_save :cache_geographic_names, if: -> { !no_cached && saved_change_to_attribute?(:geographic_area_id) } after_save :set_cached, unless: -> { no_cached } after_save :update_dwc_occurrences , unless: -> { no_dwc_occurrence } def update_dwc_occurrences # reload is required! if collection_objects.count < 40 collection_objects.reload.each do |o| o.set_dwc_occurrence end end end accepts_nested_attributes_for :verbatim_data_georeference accepts_nested_attributes_for :geo_locate_georeferences accepts_nested_attributes_for :gpx_georeferences accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_ma_range validates_uniqueness_of :md5_of_verbatim_label, scope: [:project_id], unless: -> { verbatim_label.blank? } validates_presence_of :verbatim_longitude, if: -> { !verbatim_latitude.blank? } validates_presence_of :verbatim_latitude, if: -> { !verbatim_longitude.blank? } validates :geographic_area, presence: true, allow_nil: true validates :time_start_hour, time_hour: true validates :time_start_minute, time_minute: true validates :time_start_second, time_second: true validates_presence_of :time_start_minute, if: -> { !self.time_start_second.blank? } validates_presence_of :time_start_hour, if: -> { !self.time_start_minute.blank? } validates :time_end_hour, time_hour: true validates :time_end_minute, time_minute: true validates :time_end_second, time_second: true validates_presence_of :time_end_minute, if: -> { !self.time_end_second.blank? } validates_presence_of :time_end_hour, if: -> { !self.time_end_minute.blank? } validates :start_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :end_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :start_date_month, date_month: true validates :end_date_month, date_month: true validates_presence_of :start_date_month, if: -> { !start_date_day.nil? } validates_presence_of :end_date_month, if: -> { !end_date_day.nil? } validates :end_date_day, date_day: {year_sym: :end_date_year, month_sym: :end_date_month}, unless: -> { end_date_year.nil? || end_date_month.nil? } validates :start_date_day, date_day: {year_sym: :start_date_year, month_sym: :start_date_month}, unless: -> { start_date_year.nil? || start_date_month.nil? } soft_validate(:sv_minimally_check_for_a_label) soft_validate(:sv_georeference_matches_verbatim, set: :georeference, has_fix: false) # @param [String] def verbatim_label=(value) write_attribute(:verbatim_label, value) write_attribute(:md5_of_verbatim_label, Utilities::Strings.generate_md5(value)) end scope :used_recently, -> { joins(:collection_objects).includes(:collection_objects).where(collection_objects: { created_at: 1.weeks.ago..Time.now } ).order('"collection_objects"."created_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { project_id: project_id } ) } class << self # # Scopes # def select_optimized(user_id, project_id) h = { recent: (CollectingEvent.used_in_project(project_id) .where(collection_objects: {updated_by_id: user_id}) .used_recently .distinct .limit(5) .order(:cached) .to_a + CollectingEvent.where(project_id: project_id, updated_by_id: user_id, created_at: 3.hours.ago..Time.now).limit(5).to_a).uniq, pinboard: CollectingEvent.pinned_by(user_id).pinned_in_project(project_id).to_a } h[:quick] = (CollectingEvent.pinned_by(user_id).pinboard_inserted.pinned_in_project(project_id).to_a + h[:recent]).uniq h end # @param [GeographicItem] geographic_item # @return [Scope] # TODO: use joins(:geographic_items).where(containing scope), simplied to def contained_within(geographic_item) CollectingEvent.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)) end # @param [CollectingEvent Scope] collecting_events # @return [Scope] without self (if included) # TODO: DRY, use general form of this def not_including(collecting_events) where.not(id: collecting_events) end # # Other # # @param [String] search_start_date string in form 'yyyy/mm/dd' # @param [String] search_end_date string in form 'yyyy/mm/dd' # @param [String] partial_overlap 'on' or 'off' # @return [Scope] of selected collecting events # TODO: remove all of this for direct call to Queries::CollectingEvent::Filter def in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on') allow_partial = (partial_overlap.downcase == 'off' ? false : true) q = Queries::CollectingEvent::Filter.new(start_date: search_start_date, end_date: search_end_date, partial_overlap_dates: allow_partial) where(q.between_date_range.to_sql).distinct # TODO: uniq should likely not be here end # @param [ActionController::Parameters] params in the style Rails of 'params' # @return [Scope] of selected collecting_events # TODO: deprecate for lib/queries/collecting_event def filter_by(params) sql_string = '' unless params.blank? # not strictly necessary, but handy for debugging sql_string = Utilities::Dates.date_sql_from_params(params) # processing text data v_locality_fragment = params['verbatim_locality_text'] any_label_fragment = params['any_label_text'] id_fragment = params['identifier_text'] prefix = '' unless v_locality_fragment.blank? unless sql_string.blank? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' unless any_label_fragment.blank? unless sql_string.blank? prefix = 'and ' end sql_string += "#{ prefix }(verbatim_label ilike '%#{any_label_fragment}%'" sql_string += " or print_label ilike '%#{any_label_fragment}%'" sql_string += " or document_label ilike '%#{any_label_fragment}%'" sql_string += ')' end unless id_fragment.blank? # @todo this still needs to be dealt with end end # find the records if sql_string.blank? collecting_events = CollectingEvent.none else collecting_events = CollectingEvent.where(sql_string).distinct end collecting_events end end # end Class methods # @param [String] lat # @param [String] long # @param [String] piece # @param [Integer] project_id # @param [Boolean] include_values true if to include records whicgh already have verbatim lat/longs # @return [Scope] of matching collecting events # TODO: deprecate def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" unless lat.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" unless long.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" unless piece.blank? sql += ')' sql += ' and (verbatim_latitude is null or verbatim_longitude is null)' unless include_values retval = CollectingEvent.where(sql) .with_project_id(project_id) .order(:id) .where.not(id: id).distinct retval end # @return [Boolean] def has_data? CollectingEvent.data_attributes.each do |a| return true unless self.send(a).blank? end return true if georeferences.any? false end def has_some_date? !verbatim_date.blank? || some_start_date? || some_end_date? end def has_collectors? !verbatim_collectors.blank? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? !start_date_day.blank? && !start_date_month.blank? && !start_date_year.blank? end # @return [Boolean] # has a fully defined date def has_end_date? !end_date_day.blank? && !end_date_month.blank? && !end_date_year.blank? end def some_start_date? [start_date_day, start_date_month, start_date_year].compact.any? end def some_end_date? [end_date_day, end_date_month, end_date_year].compact.any? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def start_date_string Utilities::Dates.from_parts(start_date_year, start_date_month, start_date_day) if some_start_date? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def end_date_string Utilities::Dates.from_parts(end_date_year, end_date_month, end_date_day) if some_end_date? end # @return [Time] # This is for the purposes of computation, not display! def end_date Utilities::Dates.nomenclature_date(end_date_day, end_date_month, end_date_year) end # @return [Time] # This is for the purposes of computation, not display! def start_date Utilities::Dates.nomenclature_date(start_date_day, start_date_month, start_date_year) end # @return [String] # like 00, 00:00, or 00:00:00 def time_start Utilities::Dates.format_to_hours_minutes_seconds(time_start_hour, time_start_minute, time_start_second) end # @return [String] # like 00, 00:00, or 00:00:00 def time_end Utilities::Dates.format_to_hours_minutes_seconds(time_end_hour, time_end_minute, time_end_second) end # @return [Array] # time_start and end if provided def time_range [time_start, time_end].compact end # @return [Array] # date_start and end if provided def date_range [start_date_string, end_date_string].compact end # CollectingEvent.select {|d| !(d.verbatim_latitude.nil? || d.verbatim_longitude.nil?)} # .select {|ce| ce.georeferences.empty?} # @param [Boolean] reference_self # @param [Boolean] no_cached # @return [Georeference::VerbatimData, false] # generates (creates) a Georeference::VerbatimReference from verbatim_latitude and verbatim_longitude values def generate_verbatim_data_georeference(reference_self = false, no_cached: false) return false if (verbatim_latitude.nil? || verbatim_longitude.nil?) begin CollectingEvent.transaction do vg_attributes = {collecting_event_id: id.to_s, no_cached: no_cached} vg_attributes.merge!(by: self.creator.id, project_id: self.project_id) if reference_self a = Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue raise end false end # @return [GeographicItem, nil] # a GeographicItem instance representing a translation of the verbatim values, not saved def build_verbatim_geographic_item if self.verbatim_latitude && self.verbatim_longitude && !self.new_record? local_latitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude) local_longitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude) elev = Utilities::Geo.distance_in_meters(verbatim_elevation).to_f point = Gis::FACTORY.point(local_latitude, local_longitude, elev) GeographicItem.new(point: point) else nil end end # @return [Integer] # @todo figure out how to convert verbatim_geolocation_uncertainty in different units (ft, m, km, mi) into meters # @TODO: See Utilities::Geo.distance_in_meters(String) def get_error_radius return nil if verbatim_geolocation_uncertainty.blank? return verbatim_geolocation_uncertainty.to_i if is.number?(verbatim_geolocation_uncertainty) nil end # @return [Scope] # all geographic_items associated with this collecting_event through georeferences only def all_geographic_items GeographicItem. joins('LEFT JOIN georeferences g2 ON geographic_items.id = g2.error_geographic_item_id'). joins('LEFT JOIN georeferences g1 ON geographic_items.id = g1.geographic_item_id'). where(['(g1.collecting_event_id = ? OR g2.collecting_event_id = ?) AND (g1.geographic_item_id IS NOT NULL OR g2.error_geographic_item_id IS NOT NULL)', self.id, self.id]) end # @return [GeographicItem, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item try(:geographic_area).try(:default_geographic_item) end # @param [GeographicItem] # @return [String] # see how far away we are from another gi def distance_to(geographic_item_id) GeographicItem.distance_between(preferred_georeference.geographic_item_id, geographic_item_id) end # @param [Double] distance in meters # @return [Scope] def collecting_events_within_radius_of(distance) return CollectingEvent.none if !preferred_georeference geographic_item_id = preferred_georeference.geographic_item_id # geographic_item_id = preferred_georeference.try(:geographic_item_id) # geographic_item_id = Georeference.where(collecting_event_id: id).first.geographic_item_id if geographic_item_id.nil? CollectingEvent.not_self(self) .joins(:geographic_items) .where(GeographicItem.within_radius_of_item_sql(geographic_item_id, distance)) end # @return [Scope] # Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self def collecting_events_intersecting_with pieces = GeographicItem.with_collecting_event_through_georeferences.intersecting('any', self.geographic_items.first).distinct gr = [] # all collecting events for a geographic_item pieces.each { |o| gr.push(o.collecting_events_through_georeferences.to_a) gr.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo change 'id in (?)' to some other sql construct pieces = CollectingEvent.where(id: gr.flatten.map(&:id).uniq) pieces.not_including(self) end # @return [Scope] # Find other CEs that have GRs whose GIs or EGIs are contained in the EGI def collecting_events_contained_in_error # find all the GIs and EGIs associated with CEs # TODO: this will be impossibly slow in present form pieces = GeographicItem.with_collecting_event_through_georeferences.to_a me = self.error_geographic_items.first.geo_object gi = [] # collect all the GIs which are within the EGI pieces.each { |o| gi.push(o) if o.geo_object.within?(me) } # collect all the CEs which refer to these GIs ce = [] gi.each { |o| ce.push(o.collecting_events_through_georeferences.to_a) ce.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo Directly map this pieces = CollectingEvent.where(id: ce.flatten.map(&:id).uniq) pieces.not_including(self) end # DEPRECATED for shared code # @param [String, String, Integer] # @return [Scope] def nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) return CollectingEvent.none if compared_string.nil? order_str = CollectingEvent.send(:sanitize_sql_for_conditions, ["levenshtein(collecting_events.#{column}, ?)", compared_string]) CollectingEvent.where('id <> ?', id.to_s). order(Arel.sql(order_str)). limit(limit) end # @param [String] # one or more names from GeographicAreaType # @return [Hash] # ( # {'name' => [GAs]} # or # [{'name' => [GAs]}, {'name' => [GAs]}] # ) # one hash, consisting of a country name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are country_level, and have GIs containing the (GI and/or EGI) of this CE # @todo this needs more work, possibily direct AREL table manipulation. def name_hash(types) retval = {} gi_list = containing_geographic_items # there are a few ways we can end up with no GIs # unless gi_list.nil? # no references GeographicAreas or Georeferences at all, or unless gi_list.empty? # no available GeographicItems to test # map the resulting GIs to their corresponding GAs # pieces = GeographicItem.where(id: gi_list.flatten.map(&:id).uniq) # pieces = gi_list ga_list = GeographicArea.joins(:geographic_area_type, :geographic_areas_geographic_items). where(geographic_area_types: {name: types}, geographic_areas_geographic_items: {geographic_item_id: gi_list}).distinct # WAS: now find all of the GAs which have the same names as the ones we collected. # map the names to an array of results ga_list.each { |i| retval[i.name] ||= [] # if we haven't come across this name yet, set it to point to a blank array retval[i.name].push i # we now have at least a blank array, push the result into it } end # end retval end # @return [Hash] # classifies this collecting event into country, state, county categories def geographic_name_classification # if names are stored in the database, and the the geographic_area_id has not changed if has_cached_geographic_names? && !geographic_area_id_changed? return cached_geographic_name_classification else r = get_geographic_name_classification cache_geographic_names(r, true) end end def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference # quick r = preferred_georeference.geographic_item.quick_geographic_name_hierarchy # almost never the case, UI not setup to do this # slow r = preferred_georeference.geographic_item.inferred_geographic_name_hierarchy if r == {} # therefor defaults to slow when :geographic_area_with_shape # geographic_area.try(:has_shape?) # quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow r = geographic_area.default_geographic_item.inferred_geographic_name_hierarchy if r == {} when :geographic_area # elsif geographic_area # quick r = geographic_area.geographic_name_classification when :verbatim_map_center # elsif map_center # slowest r = GeographicItem.point_inferred_geographic_name_hierarchy(verbatim_map_center) end r ||= {} r end def has_cached_geographic_names? cached_geographic_name_classification != {} end def cached_geographic_name_classification h = {} h[:country] = cached_level0_geographic_name if cached_level0_geographic_name h[:state] = cached_level1_geographic_name if cached_level1_geographic_name h[:county] = cached_level2_geographic_name if cached_level2_geographic_name h end def cache_geographic_names(values = {}, tried = false) # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? && !tried return {} if values.empty? update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county] ) values end # @return [Symbol, nil] # determines (prioritizes) the method to be used to decided the geographic name classification # (string labels for country, state, county) for this collecting_event. def geographic_name_classification_method return :preferred_georeference if preferred_georeference return :geographic_area_with_shape if geographic_area.try(:has_shape?) return :geographic_area if geographic_area return :verbatim_map_center if verbatim_map_center nil end # @return [Array of GeographicItems containing this target] # GeographicItems are those that contain either the georeference or, if there are none, # the geographic area def containing_geographic_items gi_list = [] if self.georeferences.any? # gather all the GIs which contain this GI or EGI # # Struck EGI, EGI must contain GI, therefor anything that contains EGI contains GI, threfor containing GI will always be the bigger set # !! and there was no tests broken # GeographicItem.are_contained_in_item('any_poly', self.geographic_items.to_a).pluck(:id).uniq gi_list = GeographicItem.containing(*geographic_items.pluck(:id)).pluck(:id).uniq else # use geographic_area only if there are no GIs or EGIs unless self.geographic_area.nil? # unless self.geographic_area.geographic_items.empty? # we need to use the geographic_area directly gi_list = GeographicItem.are_contained_in_item('any_poly', self.geographic_area.geographic_items).pluck(:id).uniq # end end end gi_list end # @return [Hash] def countries_hash name_hash(GeographicAreaType::COUNTRY_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a state name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are state_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def states_hash name_hash(GeographicAreaType::STATE_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a county name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are county_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def counties_hash name_hash(GeographicAreaType::COUNTY_LEVEL_TYPES) end # @param [Hash] # @return [String] def name_from_geopolitical_hash(name_hash) return nil if name_hash.keys.count == 0 return name_hash.keys.first if name_hash.keys.count == 1 most_key = nil most_count = 0 name_hash.keys.sort.each do |k| # alphabetically first (keys are unordered) if name_hash[k].size > most_count most_count = name_hash[k].size most_key = k end end most_key end # @return [String] def country_name name_from_geopolitical_hash(countries_hash) end # @return [String] def state_or_province_name name_from_geopolitical_hash(states_hash) end # @return [String] def state_name state_or_province_name end # @return [String] def county_or_equivalent_name name_from_geopolitical_hash(counties_hash) end alias county_name county_or_equivalent_name # @return [Symbol, nil] # prioritizes and identifies the source of the latitude/longitude values that # will be calculated for DWCA and primary display def lat_long_source if preferred_georeference :georeference elsif verbatim_latitude && verbatim_longitude :verbatim elsif geographic_area && geographic_area.has_shape? :geographic_area else nil end end =begin # @todo @mjy: please fill in any other paths you can think of for the acquisition of information for the seven below listed items ce.georeference.geographic_item.centroid ce.georeference.error_geographic_item.centroid ce.verbatim_georeference ce.preferred_georeference ce.georeference.first ce.verbatim_lat/ee.verbatim_lng ce.verbatim_locality ce.geographic_area.geographic_item.centroid There are a number of items we can try to get data for to complete the geolocate parameter string: 'country' can come from: GeographicArea through ce.country_name 'state' can come from: GeographicArea through ce.state_or_province_name 'county' can come from: GeographicArea through ce.county_or_equivalent_name 'locality' can come from: ce.verbatim_locality 'Latitude', 'Longitude' can come from: GeographicItem through ce.georeferences.geographic_item.centroid GeographicItem through ce.georeferences.error_geographic_item.centroid GeographicArea through ce.geographic_area.geographic_area_map_focus 'Placename' can come from: ? Copy of 'locality' =end # rubocop:disable Style/StringHashKeys # @return [Hash] # parameters from collecting event that are of use to geolocate def geolocate_attributes parameters = { 'country' => country_name, 'state' => state_or_province_name, 'county' => county_or_equivalent_name, 'locality' => verbatim_locality, 'Placename' => verbatim_locality, } focus = case lat_long_source when :georeference preferred_georeference.geographic_item when :geographic_area geographic_area.geographic_area_map_focus else nil end parameters.merge!( 'Longitude' => focus.point.x, 'Latitude' => focus.point.y ) unless focus.nil? parameters end def latitude verbatim_map_center.try(:y) end def longitude verbatim_map_center.try(:x) end # @return [Hash] # a complete set of params necessary to form a request string def geolocate_ui_params Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_hash end # @return [String] def geolocate_ui_params_string Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_string end # @return [GeoJSON::Feature] # the first geographic item of the first georeference on this collecting event def to_geo_json_feature # !! avoid loading the whole geographic item, just grab the bits we need: # self.georeferences(true) # do this to to_simple_json_feature.merge({ 'properties' => { 'collecting_event' => { 'id' => self.id, 'tag' => "Collecting event #{self.id}." } } }) end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') def to_simple_json_feature base = { 'type' => 'Feature', 'properties' => {} } if geographic_items.any? geo_item_id = geographic_items.select(:id).first.id query = "ST_AsGeoJSON(#{GeographicItem::GEOMETRY_SQL.to_sql}::geometry) geo_json" base['geometry'] = JSON.parse(GeographicItem.select(query).find(geo_item_id).geo_json) end base end # rubocop:enable Style/StringHashKeys # @return [CollectingEvent] # return the next collecting event without a georeference in this collecting events project sort order # 1. verbatim_locality # 2. geography_id # 3. start_date_year # 4. updated_on # 5. id def next_without_georeference CollectingEvent.not_including(self). includes(:georeferences). where(project_id: self.project_id, georeferences: {collecting_event_id: nil}). order(:verbatim_locality, :geographic_area_id, :start_date_year, :updated_at, :id). first end # @param [Float] delta_z, will be used to fill in the z coordinate of the point # @return [RGeo::Geographic::ProjectedPointImpl, nil] # for the *verbatim* latitude/longitude only def verbatim_map_center(delta_z = 0.0) retval = nil unless verbatim_latitude.blank? or verbatim_longitude.blank? lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude.to_s) long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude.to_s) elev = Utilities::Geo.distance_in_meters(verbatim_elevation.to_s).to_f delta_z = elev unless elev == 0.0 # Meh, BAD! must be nil retval = Gis::FACTORY.point(long, lat, delta_z) end retval end # @return [Symbol, nil] # the name of the method that will return an Rgeo object that represent # the "preferred" centroid for this collecting event def map_center_method return :preferred_georeference if preferred_georeference # => { georeferenceProtocol => ? } return :verbatim_map_center if verbatim_map_center # => { } return :geographic_area if geographic_area.try(:has_shape?) nil end # @return [Rgeo::Geographic::ProjectedPointImpl, nil] def map_center case map_center_method when :preferred_georeference preferred_georeference.geographic_item.centroid when :verbatim_map_center verbatim_map_center when :geographic_area geographic_area.default_geographic_item.geo_object.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } end def georeference_latitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].latitude.to_f end retval.round(6) end def georeference_longitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].longitude.to_f end retval.round(6) end # @return [String] # coordinates for centering a Google map def verbatim_center_coordinates if self.verbatim_latitude.blank? || self.verbatim_longitude.blank? 'POINT (0.0 0.0 0.0)' else self.verbatim_map_center.to_s end end def level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:country] end def level1_name return cached_level1_name if cached_level1_name cache_geographic_names[:state] end def level2_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end def cached_level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end # @return [CollectingEvent] # the instance may not be valid! def clone a = dup a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') roles.each do |r| a.collector_roles.build(person: r.person, position: r.position) end if georeferences.load.any? not_georeference_attributes = %w{created_at updated_at project_id updated_by_id created_by_id collecting_event_id id position} georeferences.each do |g| c = g.dup.attributes.select{|c| !not_georeference_attributes.include?(c) } a.georeferences.build(c) end end a.save a end # @return [String, nil] # a string used in DWC reportedBy and ultimately label generation # TODO: include initials when we find out a clean way of producing them # yes, it's a Helper def collector_names [Utilities::Strings.(collectors.collect{|a| a.last_name}), verbatim_collectors].compact.first end protected def set_cached v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = cached_geographic_name_classification.values.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? update_columns(cached: string) end def set_times_to_nil_if_form_provided_blank matches = ['0001-01-01 00:00:00 UTC', '2000-01-01 00:00:00 UTC'] write_attribute(:time_start, nil) if matches.include?(self.time_start.to_s) write_attribute(:time_end, nil) if matches.include?(self.time_end.to_s) end def check_verbatim_geolocation_uncertainty errors.add(:verbatim_geolocation_uncertainty, 'Provide both verbatim_latitude and verbatim_longitude if you provide verbatim_uncertainty.') if !verbatim_geolocation_uncertainty.blank? && verbatim_longitude.blank? && verbatim_latitude.blank? end def check_date_range begin errors.add(:base, 'End date is earlier than start date.') if has_start_date? && has_end_date? && (start_date > end_date) rescue errors.add(:base, 'Start and/or end date invalid.') end errors.add(:base, 'End date without start date.') if (has_end_date? && !has_start_date?) end def check_ma_range errors.add(:min_ma, 'Min ma is < Max ma.') if min_ma.present? && max_ma.present? && min_ma > max_ma end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if !minimum_elevation.blank? && !maximum_elevation.blank? && maximum_elevation < minimum_elevation end def sv_georeference_matches_verbatim if a = georeferences.where(type: 'Georeference::VerbatimData').first d_lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude).to_f d_long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude).to_f if (a.latitude.to_f != d_lat) soft_validations.add( :base, "Verbatim latitude #{verbatim_latitude}: (#{d_lat}) and point geoference latitude #{a.latitude} do not match") end if (a.longitude.to_f != d_long) soft_validations.add( :base, "Verbatim longitude #{verbatim_longitude}: (#{d_long}) and point geoference longitude #{a.longitude} do not match") end end end def sv_minimally_check_for_a_label [:verbatim_label, :print_label, :document_label, :field_notes].each do |v| return true if !self.send(v).blank? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end end |
#cached_level1_geographic_name ⇒ String?
Returns the auto-calculated level1 (typically state/province) value drawn from GeographicNames, never directly user supplied.
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 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 |
# File 'app/models/collecting_event.rb', line 179 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::HasRoles include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::HasPapertrail include Shared::IsData include SoftValidation ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, will not rebuild dwc_occurrence index. # See also Shared::IsDwcOccurrence attr_accessor :no_dwc_occurrence # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # handle_asynchronously :update_dwc_occurrences, run_at: Proc.new { 20.seconds.from_now } belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy has_one :verbatim_data_georeference, class_name: 'Georeference::VerbatimData' has_one :preferred_georeference, -> { order(:position) }, class_name: 'Georeference', foreign_key: :collecting_event_id has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy has_many :collectors, through: :collector_roles, source: :person, inverse_of: :collecting_events has_many :dwc_occurrences, through: :collection_objects has_many :georeferences, dependent: :destroy, inverse_of: :collecting_event has_many :error_geographic_items, through: :georeferences, source: :error_geographic_item has_many :geographic_items, through: :georeferences # See also all_geographic_items, the union has_many :geo_locate_georeferences, class_name: '::Georeference::GeoLocate', dependent: :destroy has_many :gpx_georeferences, class_name: 'Georeference::GPX', dependent: :destroy has_many :otus, through: :collection_objects after_create do if with_verbatim_data_georeference generate_verbatim_data_georeference(true) end end before_save :set_times_to_nil_if_form_provided_blank after_save :cache_geographic_names, if: -> { !no_cached && saved_change_to_attribute?(:geographic_area_id) } after_save :set_cached, unless: -> { no_cached } after_save :update_dwc_occurrences , unless: -> { no_dwc_occurrence } def update_dwc_occurrences # reload is required! if collection_objects.count < 40 collection_objects.reload.each do |o| o.set_dwc_occurrence end end end accepts_nested_attributes_for :verbatim_data_georeference accepts_nested_attributes_for :geo_locate_georeferences accepts_nested_attributes_for :gpx_georeferences accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_ma_range validates_uniqueness_of :md5_of_verbatim_label, scope: [:project_id], unless: -> { verbatim_label.blank? } validates_presence_of :verbatim_longitude, if: -> { !verbatim_latitude.blank? } validates_presence_of :verbatim_latitude, if: -> { !verbatim_longitude.blank? } validates :geographic_area, presence: true, allow_nil: true validates :time_start_hour, time_hour: true validates :time_start_minute, time_minute: true validates :time_start_second, time_second: true validates_presence_of :time_start_minute, if: -> { !self.time_start_second.blank? } validates_presence_of :time_start_hour, if: -> { !self.time_start_minute.blank? } validates :time_end_hour, time_hour: true validates :time_end_minute, time_minute: true validates :time_end_second, time_second: true validates_presence_of :time_end_minute, if: -> { !self.time_end_second.blank? } validates_presence_of :time_end_hour, if: -> { !self.time_end_minute.blank? } validates :start_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :end_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :start_date_month, date_month: true validates :end_date_month, date_month: true validates_presence_of :start_date_month, if: -> { !start_date_day.nil? } validates_presence_of :end_date_month, if: -> { !end_date_day.nil? } validates :end_date_day, date_day: {year_sym: :end_date_year, month_sym: :end_date_month}, unless: -> { end_date_year.nil? || end_date_month.nil? } validates :start_date_day, date_day: {year_sym: :start_date_year, month_sym: :start_date_month}, unless: -> { start_date_year.nil? || start_date_month.nil? } soft_validate(:sv_minimally_check_for_a_label) soft_validate(:sv_georeference_matches_verbatim, set: :georeference, has_fix: false) # @param [String] def verbatim_label=(value) write_attribute(:verbatim_label, value) write_attribute(:md5_of_verbatim_label, Utilities::Strings.generate_md5(value)) end scope :used_recently, -> { joins(:collection_objects).includes(:collection_objects).where(collection_objects: { created_at: 1.weeks.ago..Time.now } ).order('"collection_objects"."created_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { project_id: project_id } ) } class << self # # Scopes # def select_optimized(user_id, project_id) h = { recent: (CollectingEvent.used_in_project(project_id) .where(collection_objects: {updated_by_id: user_id}) .used_recently .distinct .limit(5) .order(:cached) .to_a + CollectingEvent.where(project_id: project_id, updated_by_id: user_id, created_at: 3.hours.ago..Time.now).limit(5).to_a).uniq, pinboard: CollectingEvent.pinned_by(user_id).pinned_in_project(project_id).to_a } h[:quick] = (CollectingEvent.pinned_by(user_id).pinboard_inserted.pinned_in_project(project_id).to_a + h[:recent]).uniq h end # @param [GeographicItem] geographic_item # @return [Scope] # TODO: use joins(:geographic_items).where(containing scope), simplied to def contained_within(geographic_item) CollectingEvent.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)) end # @param [CollectingEvent Scope] collecting_events # @return [Scope] without self (if included) # TODO: DRY, use general form of this def not_including(collecting_events) where.not(id: collecting_events) end # # Other # # @param [String] search_start_date string in form 'yyyy/mm/dd' # @param [String] search_end_date string in form 'yyyy/mm/dd' # @param [String] partial_overlap 'on' or 'off' # @return [Scope] of selected collecting events # TODO: remove all of this for direct call to Queries::CollectingEvent::Filter def in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on') allow_partial = (partial_overlap.downcase == 'off' ? false : true) q = Queries::CollectingEvent::Filter.new(start_date: search_start_date, end_date: search_end_date, partial_overlap_dates: allow_partial) where(q.between_date_range.to_sql).distinct # TODO: uniq should likely not be here end # @param [ActionController::Parameters] params in the style Rails of 'params' # @return [Scope] of selected collecting_events # TODO: deprecate for lib/queries/collecting_event def filter_by(params) sql_string = '' unless params.blank? # not strictly necessary, but handy for debugging sql_string = Utilities::Dates.date_sql_from_params(params) # processing text data v_locality_fragment = params['verbatim_locality_text'] any_label_fragment = params['any_label_text'] id_fragment = params['identifier_text'] prefix = '' unless v_locality_fragment.blank? unless sql_string.blank? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' unless any_label_fragment.blank? unless sql_string.blank? prefix = 'and ' end sql_string += "#{ prefix }(verbatim_label ilike '%#{any_label_fragment}%'" sql_string += " or print_label ilike '%#{any_label_fragment}%'" sql_string += " or document_label ilike '%#{any_label_fragment}%'" sql_string += ')' end unless id_fragment.blank? # @todo this still needs to be dealt with end end # find the records if sql_string.blank? collecting_events = CollectingEvent.none else collecting_events = CollectingEvent.where(sql_string).distinct end collecting_events end end # end Class methods # @param [String] lat # @param [String] long # @param [String] piece # @param [Integer] project_id # @param [Boolean] include_values true if to include records whicgh already have verbatim lat/longs # @return [Scope] of matching collecting events # TODO: deprecate def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" unless lat.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" unless long.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" unless piece.blank? sql += ')' sql += ' and (verbatim_latitude is null or verbatim_longitude is null)' unless include_values retval = CollectingEvent.where(sql) .with_project_id(project_id) .order(:id) .where.not(id: id).distinct retval end # @return [Boolean] def has_data? CollectingEvent.data_attributes.each do |a| return true unless self.send(a).blank? end return true if georeferences.any? false end def has_some_date? !verbatim_date.blank? || some_start_date? || some_end_date? end def has_collectors? !verbatim_collectors.blank? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? !start_date_day.blank? && !start_date_month.blank? && !start_date_year.blank? end # @return [Boolean] # has a fully defined date def has_end_date? !end_date_day.blank? && !end_date_month.blank? && !end_date_year.blank? end def some_start_date? [start_date_day, start_date_month, start_date_year].compact.any? end def some_end_date? [end_date_day, end_date_month, end_date_year].compact.any? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def start_date_string Utilities::Dates.from_parts(start_date_year, start_date_month, start_date_day) if some_start_date? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def end_date_string Utilities::Dates.from_parts(end_date_year, end_date_month, end_date_day) if some_end_date? end # @return [Time] # This is for the purposes of computation, not display! def end_date Utilities::Dates.nomenclature_date(end_date_day, end_date_month, end_date_year) end # @return [Time] # This is for the purposes of computation, not display! def start_date Utilities::Dates.nomenclature_date(start_date_day, start_date_month, start_date_year) end # @return [String] # like 00, 00:00, or 00:00:00 def time_start Utilities::Dates.format_to_hours_minutes_seconds(time_start_hour, time_start_minute, time_start_second) end # @return [String] # like 00, 00:00, or 00:00:00 def time_end Utilities::Dates.format_to_hours_minutes_seconds(time_end_hour, time_end_minute, time_end_second) end # @return [Array] # time_start and end if provided def time_range [time_start, time_end].compact end # @return [Array] # date_start and end if provided def date_range [start_date_string, end_date_string].compact end # CollectingEvent.select {|d| !(d.verbatim_latitude.nil? || d.verbatim_longitude.nil?)} # .select {|ce| ce.georeferences.empty?} # @param [Boolean] reference_self # @param [Boolean] no_cached # @return [Georeference::VerbatimData, false] # generates (creates) a Georeference::VerbatimReference from verbatim_latitude and verbatim_longitude values def generate_verbatim_data_georeference(reference_self = false, no_cached: false) return false if (verbatim_latitude.nil? || verbatim_longitude.nil?) begin CollectingEvent.transaction do vg_attributes = {collecting_event_id: id.to_s, no_cached: no_cached} vg_attributes.merge!(by: self.creator.id, project_id: self.project_id) if reference_self a = Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue raise end false end # @return [GeographicItem, nil] # a GeographicItem instance representing a translation of the verbatim values, not saved def build_verbatim_geographic_item if self.verbatim_latitude && self.verbatim_longitude && !self.new_record? local_latitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude) local_longitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude) elev = Utilities::Geo.distance_in_meters(verbatim_elevation).to_f point = Gis::FACTORY.point(local_latitude, local_longitude, elev) GeographicItem.new(point: point) else nil end end # @return [Integer] # @todo figure out how to convert verbatim_geolocation_uncertainty in different units (ft, m, km, mi) into meters # @TODO: See Utilities::Geo.distance_in_meters(String) def get_error_radius return nil if verbatim_geolocation_uncertainty.blank? return verbatim_geolocation_uncertainty.to_i if is.number?(verbatim_geolocation_uncertainty) nil end # @return [Scope] # all geographic_items associated with this collecting_event through georeferences only def all_geographic_items GeographicItem. joins('LEFT JOIN georeferences g2 ON geographic_items.id = g2.error_geographic_item_id'). joins('LEFT JOIN georeferences g1 ON geographic_items.id = g1.geographic_item_id'). where(['(g1.collecting_event_id = ? OR g2.collecting_event_id = ?) AND (g1.geographic_item_id IS NOT NULL OR g2.error_geographic_item_id IS NOT NULL)', self.id, self.id]) end # @return [GeographicItem, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item try(:geographic_area).try(:default_geographic_item) end # @param [GeographicItem] # @return [String] # see how far away we are from another gi def distance_to(geographic_item_id) GeographicItem.distance_between(preferred_georeference.geographic_item_id, geographic_item_id) end # @param [Double] distance in meters # @return [Scope] def collecting_events_within_radius_of(distance) return CollectingEvent.none if !preferred_georeference geographic_item_id = preferred_georeference.geographic_item_id # geographic_item_id = preferred_georeference.try(:geographic_item_id) # geographic_item_id = Georeference.where(collecting_event_id: id).first.geographic_item_id if geographic_item_id.nil? CollectingEvent.not_self(self) .joins(:geographic_items) .where(GeographicItem.within_radius_of_item_sql(geographic_item_id, distance)) end # @return [Scope] # Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self def collecting_events_intersecting_with pieces = GeographicItem.with_collecting_event_through_georeferences.intersecting('any', self.geographic_items.first).distinct gr = [] # all collecting events for a geographic_item pieces.each { |o| gr.push(o.collecting_events_through_georeferences.to_a) gr.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo change 'id in (?)' to some other sql construct pieces = CollectingEvent.where(id: gr.flatten.map(&:id).uniq) pieces.not_including(self) end # @return [Scope] # Find other CEs that have GRs whose GIs or EGIs are contained in the EGI def collecting_events_contained_in_error # find all the GIs and EGIs associated with CEs # TODO: this will be impossibly slow in present form pieces = GeographicItem.with_collecting_event_through_georeferences.to_a me = self.error_geographic_items.first.geo_object gi = [] # collect all the GIs which are within the EGI pieces.each { |o| gi.push(o) if o.geo_object.within?(me) } # collect all the CEs which refer to these GIs ce = [] gi.each { |o| ce.push(o.collecting_events_through_georeferences.to_a) ce.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo Directly map this pieces = CollectingEvent.where(id: ce.flatten.map(&:id).uniq) pieces.not_including(self) end # DEPRECATED for shared code # @param [String, String, Integer] # @return [Scope] def nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) return CollectingEvent.none if compared_string.nil? order_str = CollectingEvent.send(:sanitize_sql_for_conditions, ["levenshtein(collecting_events.#{column}, ?)", compared_string]) CollectingEvent.where('id <> ?', id.to_s). order(Arel.sql(order_str)). limit(limit) end # @param [String] # one or more names from GeographicAreaType # @return [Hash] # ( # {'name' => [GAs]} # or # [{'name' => [GAs]}, {'name' => [GAs]}] # ) # one hash, consisting of a country name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are country_level, and have GIs containing the (GI and/or EGI) of this CE # @todo this needs more work, possibily direct AREL table manipulation. def name_hash(types) retval = {} gi_list = containing_geographic_items # there are a few ways we can end up with no GIs # unless gi_list.nil? # no references GeographicAreas or Georeferences at all, or unless gi_list.empty? # no available GeographicItems to test # map the resulting GIs to their corresponding GAs # pieces = GeographicItem.where(id: gi_list.flatten.map(&:id).uniq) # pieces = gi_list ga_list = GeographicArea.joins(:geographic_area_type, :geographic_areas_geographic_items). where(geographic_area_types: {name: types}, geographic_areas_geographic_items: {geographic_item_id: gi_list}).distinct # WAS: now find all of the GAs which have the same names as the ones we collected. # map the names to an array of results ga_list.each { |i| retval[i.name] ||= [] # if we haven't come across this name yet, set it to point to a blank array retval[i.name].push i # we now have at least a blank array, push the result into it } end # end retval end # @return [Hash] # classifies this collecting event into country, state, county categories def geographic_name_classification # if names are stored in the database, and the the geographic_area_id has not changed if has_cached_geographic_names? && !geographic_area_id_changed? return cached_geographic_name_classification else r = get_geographic_name_classification cache_geographic_names(r, true) end end def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference # quick r = preferred_georeference.geographic_item.quick_geographic_name_hierarchy # almost never the case, UI not setup to do this # slow r = preferred_georeference.geographic_item.inferred_geographic_name_hierarchy if r == {} # therefor defaults to slow when :geographic_area_with_shape # geographic_area.try(:has_shape?) # quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow r = geographic_area.default_geographic_item.inferred_geographic_name_hierarchy if r == {} when :geographic_area # elsif geographic_area # quick r = geographic_area.geographic_name_classification when :verbatim_map_center # elsif map_center # slowest r = GeographicItem.point_inferred_geographic_name_hierarchy(verbatim_map_center) end r ||= {} r end def has_cached_geographic_names? cached_geographic_name_classification != {} end def cached_geographic_name_classification h = {} h[:country] = cached_level0_geographic_name if cached_level0_geographic_name h[:state] = cached_level1_geographic_name if cached_level1_geographic_name h[:county] = cached_level2_geographic_name if cached_level2_geographic_name h end def cache_geographic_names(values = {}, tried = false) # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? && !tried return {} if values.empty? update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county] ) values end # @return [Symbol, nil] # determines (prioritizes) the method to be used to decided the geographic name classification # (string labels for country, state, county) for this collecting_event. def geographic_name_classification_method return :preferred_georeference if preferred_georeference return :geographic_area_with_shape if geographic_area.try(:has_shape?) return :geographic_area if geographic_area return :verbatim_map_center if verbatim_map_center nil end # @return [Array of GeographicItems containing this target] # GeographicItems are those that contain either the georeference or, if there are none, # the geographic area def containing_geographic_items gi_list = [] if self.georeferences.any? # gather all the GIs which contain this GI or EGI # # Struck EGI, EGI must contain GI, therefor anything that contains EGI contains GI, threfor containing GI will always be the bigger set # !! and there was no tests broken # GeographicItem.are_contained_in_item('any_poly', self.geographic_items.to_a).pluck(:id).uniq gi_list = GeographicItem.containing(*geographic_items.pluck(:id)).pluck(:id).uniq else # use geographic_area only if there are no GIs or EGIs unless self.geographic_area.nil? # unless self.geographic_area.geographic_items.empty? # we need to use the geographic_area directly gi_list = GeographicItem.are_contained_in_item('any_poly', self.geographic_area.geographic_items).pluck(:id).uniq # end end end gi_list end # @return [Hash] def countries_hash name_hash(GeographicAreaType::COUNTRY_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a state name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are state_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def states_hash name_hash(GeographicAreaType::STATE_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a county name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are county_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def counties_hash name_hash(GeographicAreaType::COUNTY_LEVEL_TYPES) end # @param [Hash] # @return [String] def name_from_geopolitical_hash(name_hash) return nil if name_hash.keys.count == 0 return name_hash.keys.first if name_hash.keys.count == 1 most_key = nil most_count = 0 name_hash.keys.sort.each do |k| # alphabetically first (keys are unordered) if name_hash[k].size > most_count most_count = name_hash[k].size most_key = k end end most_key end # @return [String] def country_name name_from_geopolitical_hash(countries_hash) end # @return [String] def state_or_province_name name_from_geopolitical_hash(states_hash) end # @return [String] def state_name state_or_province_name end # @return [String] def county_or_equivalent_name name_from_geopolitical_hash(counties_hash) end alias county_name county_or_equivalent_name # @return [Symbol, nil] # prioritizes and identifies the source of the latitude/longitude values that # will be calculated for DWCA and primary display def lat_long_source if preferred_georeference :georeference elsif verbatim_latitude && verbatim_longitude :verbatim elsif geographic_area && geographic_area.has_shape? :geographic_area else nil end end =begin # @todo @mjy: please fill in any other paths you can think of for the acquisition of information for the seven below listed items ce.georeference.geographic_item.centroid ce.georeference.error_geographic_item.centroid ce.verbatim_georeference ce.preferred_georeference ce.georeference.first ce.verbatim_lat/ee.verbatim_lng ce.verbatim_locality ce.geographic_area.geographic_item.centroid There are a number of items we can try to get data for to complete the geolocate parameter string: 'country' can come from: GeographicArea through ce.country_name 'state' can come from: GeographicArea through ce.state_or_province_name 'county' can come from: GeographicArea through ce.county_or_equivalent_name 'locality' can come from: ce.verbatim_locality 'Latitude', 'Longitude' can come from: GeographicItem through ce.georeferences.geographic_item.centroid GeographicItem through ce.georeferences.error_geographic_item.centroid GeographicArea through ce.geographic_area.geographic_area_map_focus 'Placename' can come from: ? Copy of 'locality' =end # rubocop:disable Style/StringHashKeys # @return [Hash] # parameters from collecting event that are of use to geolocate def geolocate_attributes parameters = { 'country' => country_name, 'state' => state_or_province_name, 'county' => county_or_equivalent_name, 'locality' => verbatim_locality, 'Placename' => verbatim_locality, } focus = case lat_long_source when :georeference preferred_georeference.geographic_item when :geographic_area geographic_area.geographic_area_map_focus else nil end parameters.merge!( 'Longitude' => focus.point.x, 'Latitude' => focus.point.y ) unless focus.nil? parameters end def latitude verbatim_map_center.try(:y) end def longitude verbatim_map_center.try(:x) end # @return [Hash] # a complete set of params necessary to form a request string def geolocate_ui_params Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_hash end # @return [String] def geolocate_ui_params_string Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_string end # @return [GeoJSON::Feature] # the first geographic item of the first georeference on this collecting event def to_geo_json_feature # !! avoid loading the whole geographic item, just grab the bits we need: # self.georeferences(true) # do this to to_simple_json_feature.merge({ 'properties' => { 'collecting_event' => { 'id' => self.id, 'tag' => "Collecting event #{self.id}." } } }) end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') def to_simple_json_feature base = { 'type' => 'Feature', 'properties' => {} } if geographic_items.any? geo_item_id = geographic_items.select(:id).first.id query = "ST_AsGeoJSON(#{GeographicItem::GEOMETRY_SQL.to_sql}::geometry) geo_json" base['geometry'] = JSON.parse(GeographicItem.select(query).find(geo_item_id).geo_json) end base end # rubocop:enable Style/StringHashKeys # @return [CollectingEvent] # return the next collecting event without a georeference in this collecting events project sort order # 1. verbatim_locality # 2. geography_id # 3. start_date_year # 4. updated_on # 5. id def next_without_georeference CollectingEvent.not_including(self). includes(:georeferences). where(project_id: self.project_id, georeferences: {collecting_event_id: nil}). order(:verbatim_locality, :geographic_area_id, :start_date_year, :updated_at, :id). first end # @param [Float] delta_z, will be used to fill in the z coordinate of the point # @return [RGeo::Geographic::ProjectedPointImpl, nil] # for the *verbatim* latitude/longitude only def verbatim_map_center(delta_z = 0.0) retval = nil unless verbatim_latitude.blank? or verbatim_longitude.blank? lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude.to_s) long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude.to_s) elev = Utilities::Geo.distance_in_meters(verbatim_elevation.to_s).to_f delta_z = elev unless elev == 0.0 # Meh, BAD! must be nil retval = Gis::FACTORY.point(long, lat, delta_z) end retval end # @return [Symbol, nil] # the name of the method that will return an Rgeo object that represent # the "preferred" centroid for this collecting event def map_center_method return :preferred_georeference if preferred_georeference # => { georeferenceProtocol => ? } return :verbatim_map_center if verbatim_map_center # => { } return :geographic_area if geographic_area.try(:has_shape?) nil end # @return [Rgeo::Geographic::ProjectedPointImpl, nil] def map_center case map_center_method when :preferred_georeference preferred_georeference.geographic_item.centroid when :verbatim_map_center verbatim_map_center when :geographic_area geographic_area.default_geographic_item.geo_object.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } end def georeference_latitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].latitude.to_f end retval.round(6) end def georeference_longitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].longitude.to_f end retval.round(6) end # @return [String] # coordinates for centering a Google map def verbatim_center_coordinates if self.verbatim_latitude.blank? || self.verbatim_longitude.blank? 'POINT (0.0 0.0 0.0)' else self.verbatim_map_center.to_s end end def level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:country] end def level1_name return cached_level1_name if cached_level1_name cache_geographic_names[:state] end def level2_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end def cached_level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end # @return [CollectingEvent] # the instance may not be valid! def clone a = dup a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') roles.each do |r| a.collector_roles.build(person: r.person, position: r.position) end if georeferences.load.any? not_georeference_attributes = %w{created_at updated_at project_id updated_by_id created_by_id collecting_event_id id position} georeferences.each do |g| c = g.dup.attributes.select{|c| !not_georeference_attributes.include?(c) } a.georeferences.build(c) end end a.save a end # @return [String, nil] # a string used in DWC reportedBy and ultimately label generation # TODO: include initials when we find out a clean way of producing them # yes, it's a Helper def collector_names [Utilities::Strings.(collectors.collect{|a| a.last_name}), verbatim_collectors].compact.first end protected def set_cached v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = cached_geographic_name_classification.values.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? update_columns(cached: string) end def set_times_to_nil_if_form_provided_blank matches = ['0001-01-01 00:00:00 UTC', '2000-01-01 00:00:00 UTC'] write_attribute(:time_start, nil) if matches.include?(self.time_start.to_s) write_attribute(:time_end, nil) if matches.include?(self.time_end.to_s) end def check_verbatim_geolocation_uncertainty errors.add(:verbatim_geolocation_uncertainty, 'Provide both verbatim_latitude and verbatim_longitude if you provide verbatim_uncertainty.') if !verbatim_geolocation_uncertainty.blank? && verbatim_longitude.blank? && verbatim_latitude.blank? end def check_date_range begin errors.add(:base, 'End date is earlier than start date.') if has_start_date? && has_end_date? && (start_date > end_date) rescue errors.add(:base, 'Start and/or end date invalid.') end errors.add(:base, 'End date without start date.') if (has_end_date? && !has_start_date?) end def check_ma_range errors.add(:min_ma, 'Min ma is < Max ma.') if min_ma.present? && max_ma.present? && min_ma > max_ma end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if !minimum_elevation.blank? && !maximum_elevation.blank? && maximum_elevation < minimum_elevation end def sv_georeference_matches_verbatim if a = georeferences.where(type: 'Georeference::VerbatimData').first d_lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude).to_f d_long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude).to_f if (a.latitude.to_f != d_lat) soft_validations.add( :base, "Verbatim latitude #{verbatim_latitude}: (#{d_lat}) and point geoference latitude #{a.latitude} do not match") end if (a.longitude.to_f != d_long) soft_validations.add( :base, "Verbatim longitude #{verbatim_longitude}: (#{d_long}) and point geoference longitude #{a.longitude} do not match") end end end def sv_minimally_check_for_a_label [:verbatim_label, :print_label, :document_label, :field_notes].each do |v| return true if !self.send(v).blank? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end end |
#cached_level2_geographic_name ⇒ String?
Returns the auto-calculated level2 value (e.g. county) drawn from GeographicNames, never directly user supplied.
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 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 |
# File 'app/models/collecting_event.rb', line 179 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::HasRoles include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::HasPapertrail include Shared::IsData include SoftValidation ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, will not rebuild dwc_occurrence index. # See also Shared::IsDwcOccurrence attr_accessor :no_dwc_occurrence # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # handle_asynchronously :update_dwc_occurrences, run_at: Proc.new { 20.seconds.from_now } belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy has_one :verbatim_data_georeference, class_name: 'Georeference::VerbatimData' has_one :preferred_georeference, -> { order(:position) }, class_name: 'Georeference', foreign_key: :collecting_event_id has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy has_many :collectors, through: :collector_roles, source: :person, inverse_of: :collecting_events has_many :dwc_occurrences, through: :collection_objects has_many :georeferences, dependent: :destroy, inverse_of: :collecting_event has_many :error_geographic_items, through: :georeferences, source: :error_geographic_item has_many :geographic_items, through: :georeferences # See also all_geographic_items, the union has_many :geo_locate_georeferences, class_name: '::Georeference::GeoLocate', dependent: :destroy has_many :gpx_georeferences, class_name: 'Georeference::GPX', dependent: :destroy has_many :otus, through: :collection_objects after_create do if with_verbatim_data_georeference generate_verbatim_data_georeference(true) end end before_save :set_times_to_nil_if_form_provided_blank after_save :cache_geographic_names, if: -> { !no_cached && saved_change_to_attribute?(:geographic_area_id) } after_save :set_cached, unless: -> { no_cached } after_save :update_dwc_occurrences , unless: -> { no_dwc_occurrence } def update_dwc_occurrences # reload is required! if collection_objects.count < 40 collection_objects.reload.each do |o| o.set_dwc_occurrence end end end accepts_nested_attributes_for :verbatim_data_georeference accepts_nested_attributes_for :geo_locate_georeferences accepts_nested_attributes_for :gpx_georeferences accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_ma_range validates_uniqueness_of :md5_of_verbatim_label, scope: [:project_id], unless: -> { verbatim_label.blank? } validates_presence_of :verbatim_longitude, if: -> { !verbatim_latitude.blank? } validates_presence_of :verbatim_latitude, if: -> { !verbatim_longitude.blank? } validates :geographic_area, presence: true, allow_nil: true validates :time_start_hour, time_hour: true validates :time_start_minute, time_minute: true validates :time_start_second, time_second: true validates_presence_of :time_start_minute, if: -> { !self.time_start_second.blank? } validates_presence_of :time_start_hour, if: -> { !self.time_start_minute.blank? } validates :time_end_hour, time_hour: true validates :time_end_minute, time_minute: true validates :time_end_second, time_second: true validates_presence_of :time_end_minute, if: -> { !self.time_end_second.blank? } validates_presence_of :time_end_hour, if: -> { !self.time_end_minute.blank? } validates :start_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :end_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :start_date_month, date_month: true validates :end_date_month, date_month: true validates_presence_of :start_date_month, if: -> { !start_date_day.nil? } validates_presence_of :end_date_month, if: -> { !end_date_day.nil? } validates :end_date_day, date_day: {year_sym: :end_date_year, month_sym: :end_date_month}, unless: -> { end_date_year.nil? || end_date_month.nil? } validates :start_date_day, date_day: {year_sym: :start_date_year, month_sym: :start_date_month}, unless: -> { start_date_year.nil? || start_date_month.nil? } soft_validate(:sv_minimally_check_for_a_label) soft_validate(:sv_georeference_matches_verbatim, set: :georeference, has_fix: false) # @param [String] def verbatim_label=(value) write_attribute(:verbatim_label, value) write_attribute(:md5_of_verbatim_label, Utilities::Strings.generate_md5(value)) end scope :used_recently, -> { joins(:collection_objects).includes(:collection_objects).where(collection_objects: { created_at: 1.weeks.ago..Time.now } ).order('"collection_objects"."created_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { project_id: project_id } ) } class << self # # Scopes # def select_optimized(user_id, project_id) h = { recent: (CollectingEvent.used_in_project(project_id) .where(collection_objects: {updated_by_id: user_id}) .used_recently .distinct .limit(5) .order(:cached) .to_a + CollectingEvent.where(project_id: project_id, updated_by_id: user_id, created_at: 3.hours.ago..Time.now).limit(5).to_a).uniq, pinboard: CollectingEvent.pinned_by(user_id).pinned_in_project(project_id).to_a } h[:quick] = (CollectingEvent.pinned_by(user_id).pinboard_inserted.pinned_in_project(project_id).to_a + h[:recent]).uniq h end # @param [GeographicItem] geographic_item # @return [Scope] # TODO: use joins(:geographic_items).where(containing scope), simplied to def contained_within(geographic_item) CollectingEvent.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)) end # @param [CollectingEvent Scope] collecting_events # @return [Scope] without self (if included) # TODO: DRY, use general form of this def not_including(collecting_events) where.not(id: collecting_events) end # # Other # # @param [String] search_start_date string in form 'yyyy/mm/dd' # @param [String] search_end_date string in form 'yyyy/mm/dd' # @param [String] partial_overlap 'on' or 'off' # @return [Scope] of selected collecting events # TODO: remove all of this for direct call to Queries::CollectingEvent::Filter def in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on') allow_partial = (partial_overlap.downcase == 'off' ? false : true) q = Queries::CollectingEvent::Filter.new(start_date: search_start_date, end_date: search_end_date, partial_overlap_dates: allow_partial) where(q.between_date_range.to_sql).distinct # TODO: uniq should likely not be here end # @param [ActionController::Parameters] params in the style Rails of 'params' # @return [Scope] of selected collecting_events # TODO: deprecate for lib/queries/collecting_event def filter_by(params) sql_string = '' unless params.blank? # not strictly necessary, but handy for debugging sql_string = Utilities::Dates.date_sql_from_params(params) # processing text data v_locality_fragment = params['verbatim_locality_text'] any_label_fragment = params['any_label_text'] id_fragment = params['identifier_text'] prefix = '' unless v_locality_fragment.blank? unless sql_string.blank? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' unless any_label_fragment.blank? unless sql_string.blank? prefix = 'and ' end sql_string += "#{ prefix }(verbatim_label ilike '%#{any_label_fragment}%'" sql_string += " or print_label ilike '%#{any_label_fragment}%'" sql_string += " or document_label ilike '%#{any_label_fragment}%'" sql_string += ')' end unless id_fragment.blank? # @todo this still needs to be dealt with end end # find the records if sql_string.blank? collecting_events = CollectingEvent.none else collecting_events = CollectingEvent.where(sql_string).distinct end collecting_events end end # end Class methods # @param [String] lat # @param [String] long # @param [String] piece # @param [Integer] project_id # @param [Boolean] include_values true if to include records whicgh already have verbatim lat/longs # @return [Scope] of matching collecting events # TODO: deprecate def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" unless lat.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" unless long.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" unless piece.blank? sql += ')' sql += ' and (verbatim_latitude is null or verbatim_longitude is null)' unless include_values retval = CollectingEvent.where(sql) .with_project_id(project_id) .order(:id) .where.not(id: id).distinct retval end # @return [Boolean] def has_data? CollectingEvent.data_attributes.each do |a| return true unless self.send(a).blank? end return true if georeferences.any? false end def has_some_date? !verbatim_date.blank? || some_start_date? || some_end_date? end def has_collectors? !verbatim_collectors.blank? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? !start_date_day.blank? && !start_date_month.blank? && !start_date_year.blank? end # @return [Boolean] # has a fully defined date def has_end_date? !end_date_day.blank? && !end_date_month.blank? && !end_date_year.blank? end def some_start_date? [start_date_day, start_date_month, start_date_year].compact.any? end def some_end_date? [end_date_day, end_date_month, end_date_year].compact.any? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def start_date_string Utilities::Dates.from_parts(start_date_year, start_date_month, start_date_day) if some_start_date? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def end_date_string Utilities::Dates.from_parts(end_date_year, end_date_month, end_date_day) if some_end_date? end # @return [Time] # This is for the purposes of computation, not display! def end_date Utilities::Dates.nomenclature_date(end_date_day, end_date_month, end_date_year) end # @return [Time] # This is for the purposes of computation, not display! def start_date Utilities::Dates.nomenclature_date(start_date_day, start_date_month, start_date_year) end # @return [String] # like 00, 00:00, or 00:00:00 def time_start Utilities::Dates.format_to_hours_minutes_seconds(time_start_hour, time_start_minute, time_start_second) end # @return [String] # like 00, 00:00, or 00:00:00 def time_end Utilities::Dates.format_to_hours_minutes_seconds(time_end_hour, time_end_minute, time_end_second) end # @return [Array] # time_start and end if provided def time_range [time_start, time_end].compact end # @return [Array] # date_start and end if provided def date_range [start_date_string, end_date_string].compact end # CollectingEvent.select {|d| !(d.verbatim_latitude.nil? || d.verbatim_longitude.nil?)} # .select {|ce| ce.georeferences.empty?} # @param [Boolean] reference_self # @param [Boolean] no_cached # @return [Georeference::VerbatimData, false] # generates (creates) a Georeference::VerbatimReference from verbatim_latitude and verbatim_longitude values def generate_verbatim_data_georeference(reference_self = false, no_cached: false) return false if (verbatim_latitude.nil? || verbatim_longitude.nil?) begin CollectingEvent.transaction do vg_attributes = {collecting_event_id: id.to_s, no_cached: no_cached} vg_attributes.merge!(by: self.creator.id, project_id: self.project_id) if reference_self a = Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue raise end false end # @return [GeographicItem, nil] # a GeographicItem instance representing a translation of the verbatim values, not saved def build_verbatim_geographic_item if self.verbatim_latitude && self.verbatim_longitude && !self.new_record? local_latitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude) local_longitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude) elev = Utilities::Geo.distance_in_meters(verbatim_elevation).to_f point = Gis::FACTORY.point(local_latitude, local_longitude, elev) GeographicItem.new(point: point) else nil end end # @return [Integer] # @todo figure out how to convert verbatim_geolocation_uncertainty in different units (ft, m, km, mi) into meters # @TODO: See Utilities::Geo.distance_in_meters(String) def get_error_radius return nil if verbatim_geolocation_uncertainty.blank? return verbatim_geolocation_uncertainty.to_i if is.number?(verbatim_geolocation_uncertainty) nil end # @return [Scope] # all geographic_items associated with this collecting_event through georeferences only def all_geographic_items GeographicItem. joins('LEFT JOIN georeferences g2 ON geographic_items.id = g2.error_geographic_item_id'). joins('LEFT JOIN georeferences g1 ON geographic_items.id = g1.geographic_item_id'). where(['(g1.collecting_event_id = ? OR g2.collecting_event_id = ?) AND (g1.geographic_item_id IS NOT NULL OR g2.error_geographic_item_id IS NOT NULL)', self.id, self.id]) end # @return [GeographicItem, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item try(:geographic_area).try(:default_geographic_item) end # @param [GeographicItem] # @return [String] # see how far away we are from another gi def distance_to(geographic_item_id) GeographicItem.distance_between(preferred_georeference.geographic_item_id, geographic_item_id) end # @param [Double] distance in meters # @return [Scope] def collecting_events_within_radius_of(distance) return CollectingEvent.none if !preferred_georeference geographic_item_id = preferred_georeference.geographic_item_id # geographic_item_id = preferred_georeference.try(:geographic_item_id) # geographic_item_id = Georeference.where(collecting_event_id: id).first.geographic_item_id if geographic_item_id.nil? CollectingEvent.not_self(self) .joins(:geographic_items) .where(GeographicItem.within_radius_of_item_sql(geographic_item_id, distance)) end # @return [Scope] # Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self def collecting_events_intersecting_with pieces = GeographicItem.with_collecting_event_through_georeferences.intersecting('any', self.geographic_items.first).distinct gr = [] # all collecting events for a geographic_item pieces.each { |o| gr.push(o.collecting_events_through_georeferences.to_a) gr.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo change 'id in (?)' to some other sql construct pieces = CollectingEvent.where(id: gr.flatten.map(&:id).uniq) pieces.not_including(self) end # @return [Scope] # Find other CEs that have GRs whose GIs or EGIs are contained in the EGI def collecting_events_contained_in_error # find all the GIs and EGIs associated with CEs # TODO: this will be impossibly slow in present form pieces = GeographicItem.with_collecting_event_through_georeferences.to_a me = self.error_geographic_items.first.geo_object gi = [] # collect all the GIs which are within the EGI pieces.each { |o| gi.push(o) if o.geo_object.within?(me) } # collect all the CEs which refer to these GIs ce = [] gi.each { |o| ce.push(o.collecting_events_through_georeferences.to_a) ce.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo Directly map this pieces = CollectingEvent.where(id: ce.flatten.map(&:id).uniq) pieces.not_including(self) end # DEPRECATED for shared code # @param [String, String, Integer] # @return [Scope] def nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) return CollectingEvent.none if compared_string.nil? order_str = CollectingEvent.send(:sanitize_sql_for_conditions, ["levenshtein(collecting_events.#{column}, ?)", compared_string]) CollectingEvent.where('id <> ?', id.to_s). order(Arel.sql(order_str)). limit(limit) end # @param [String] # one or more names from GeographicAreaType # @return [Hash] # ( # {'name' => [GAs]} # or # [{'name' => [GAs]}, {'name' => [GAs]}] # ) # one hash, consisting of a country name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are country_level, and have GIs containing the (GI and/or EGI) of this CE # @todo this needs more work, possibily direct AREL table manipulation. def name_hash(types) retval = {} gi_list = containing_geographic_items # there are a few ways we can end up with no GIs # unless gi_list.nil? # no references GeographicAreas or Georeferences at all, or unless gi_list.empty? # no available GeographicItems to test # map the resulting GIs to their corresponding GAs # pieces = GeographicItem.where(id: gi_list.flatten.map(&:id).uniq) # pieces = gi_list ga_list = GeographicArea.joins(:geographic_area_type, :geographic_areas_geographic_items). where(geographic_area_types: {name: types}, geographic_areas_geographic_items: {geographic_item_id: gi_list}).distinct # WAS: now find all of the GAs which have the same names as the ones we collected. # map the names to an array of results ga_list.each { |i| retval[i.name] ||= [] # if we haven't come across this name yet, set it to point to a blank array retval[i.name].push i # we now have at least a blank array, push the result into it } end # end retval end # @return [Hash] # classifies this collecting event into country, state, county categories def geographic_name_classification # if names are stored in the database, and the the geographic_area_id has not changed if has_cached_geographic_names? && !geographic_area_id_changed? return cached_geographic_name_classification else r = get_geographic_name_classification cache_geographic_names(r, true) end end def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference # quick r = preferred_georeference.geographic_item.quick_geographic_name_hierarchy # almost never the case, UI not setup to do this # slow r = preferred_georeference.geographic_item.inferred_geographic_name_hierarchy if r == {} # therefor defaults to slow when :geographic_area_with_shape # geographic_area.try(:has_shape?) # quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow r = geographic_area.default_geographic_item.inferred_geographic_name_hierarchy if r == {} when :geographic_area # elsif geographic_area # quick r = geographic_area.geographic_name_classification when :verbatim_map_center # elsif map_center # slowest r = GeographicItem.point_inferred_geographic_name_hierarchy(verbatim_map_center) end r ||= {} r end def has_cached_geographic_names? cached_geographic_name_classification != {} end def cached_geographic_name_classification h = {} h[:country] = cached_level0_geographic_name if cached_level0_geographic_name h[:state] = cached_level1_geographic_name if cached_level1_geographic_name h[:county] = cached_level2_geographic_name if cached_level2_geographic_name h end def cache_geographic_names(values = {}, tried = false) # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? && !tried return {} if values.empty? update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county] ) values end # @return [Symbol, nil] # determines (prioritizes) the method to be used to decided the geographic name classification # (string labels for country, state, county) for this collecting_event. def geographic_name_classification_method return :preferred_georeference if preferred_georeference return :geographic_area_with_shape if geographic_area.try(:has_shape?) return :geographic_area if geographic_area return :verbatim_map_center if verbatim_map_center nil end # @return [Array of GeographicItems containing this target] # GeographicItems are those that contain either the georeference or, if there are none, # the geographic area def containing_geographic_items gi_list = [] if self.georeferences.any? # gather all the GIs which contain this GI or EGI # # Struck EGI, EGI must contain GI, therefor anything that contains EGI contains GI, threfor containing GI will always be the bigger set # !! and there was no tests broken # GeographicItem.are_contained_in_item('any_poly', self.geographic_items.to_a).pluck(:id).uniq gi_list = GeographicItem.containing(*geographic_items.pluck(:id)).pluck(:id).uniq else # use geographic_area only if there are no GIs or EGIs unless self.geographic_area.nil? # unless self.geographic_area.geographic_items.empty? # we need to use the geographic_area directly gi_list = GeographicItem.are_contained_in_item('any_poly', self.geographic_area.geographic_items).pluck(:id).uniq # end end end gi_list end # @return [Hash] def countries_hash name_hash(GeographicAreaType::COUNTRY_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a state name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are state_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def states_hash name_hash(GeographicAreaType::STATE_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a county name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are county_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def counties_hash name_hash(GeographicAreaType::COUNTY_LEVEL_TYPES) end # @param [Hash] # @return [String] def name_from_geopolitical_hash(name_hash) return nil if name_hash.keys.count == 0 return name_hash.keys.first if name_hash.keys.count == 1 most_key = nil most_count = 0 name_hash.keys.sort.each do |k| # alphabetically first (keys are unordered) if name_hash[k].size > most_count most_count = name_hash[k].size most_key = k end end most_key end # @return [String] def country_name name_from_geopolitical_hash(countries_hash) end # @return [String] def state_or_province_name name_from_geopolitical_hash(states_hash) end # @return [String] def state_name state_or_province_name end # @return [String] def county_or_equivalent_name name_from_geopolitical_hash(counties_hash) end alias county_name county_or_equivalent_name # @return [Symbol, nil] # prioritizes and identifies the source of the latitude/longitude values that # will be calculated for DWCA and primary display def lat_long_source if preferred_georeference :georeference elsif verbatim_latitude && verbatim_longitude :verbatim elsif geographic_area && geographic_area.has_shape? :geographic_area else nil end end =begin # @todo @mjy: please fill in any other paths you can think of for the acquisition of information for the seven below listed items ce.georeference.geographic_item.centroid ce.georeference.error_geographic_item.centroid ce.verbatim_georeference ce.preferred_georeference ce.georeference.first ce.verbatim_lat/ee.verbatim_lng ce.verbatim_locality ce.geographic_area.geographic_item.centroid There are a number of items we can try to get data for to complete the geolocate parameter string: 'country' can come from: GeographicArea through ce.country_name 'state' can come from: GeographicArea through ce.state_or_province_name 'county' can come from: GeographicArea through ce.county_or_equivalent_name 'locality' can come from: ce.verbatim_locality 'Latitude', 'Longitude' can come from: GeographicItem through ce.georeferences.geographic_item.centroid GeographicItem through ce.georeferences.error_geographic_item.centroid GeographicArea through ce.geographic_area.geographic_area_map_focus 'Placename' can come from: ? Copy of 'locality' =end # rubocop:disable Style/StringHashKeys # @return [Hash] # parameters from collecting event that are of use to geolocate def geolocate_attributes parameters = { 'country' => country_name, 'state' => state_or_province_name, 'county' => county_or_equivalent_name, 'locality' => verbatim_locality, 'Placename' => verbatim_locality, } focus = case lat_long_source when :georeference preferred_georeference.geographic_item when :geographic_area geographic_area.geographic_area_map_focus else nil end parameters.merge!( 'Longitude' => focus.point.x, 'Latitude' => focus.point.y ) unless focus.nil? parameters end def latitude verbatim_map_center.try(:y) end def longitude verbatim_map_center.try(:x) end # @return [Hash] # a complete set of params necessary to form a request string def geolocate_ui_params Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_hash end # @return [String] def geolocate_ui_params_string Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_string end # @return [GeoJSON::Feature] # the first geographic item of the first georeference on this collecting event def to_geo_json_feature # !! avoid loading the whole geographic item, just grab the bits we need: # self.georeferences(true) # do this to to_simple_json_feature.merge({ 'properties' => { 'collecting_event' => { 'id' => self.id, 'tag' => "Collecting event #{self.id}." } } }) end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') def to_simple_json_feature base = { 'type' => 'Feature', 'properties' => {} } if geographic_items.any? geo_item_id = geographic_items.select(:id).first.id query = "ST_AsGeoJSON(#{GeographicItem::GEOMETRY_SQL.to_sql}::geometry) geo_json" base['geometry'] = JSON.parse(GeographicItem.select(query).find(geo_item_id).geo_json) end base end # rubocop:enable Style/StringHashKeys # @return [CollectingEvent] # return the next collecting event without a georeference in this collecting events project sort order # 1. verbatim_locality # 2. geography_id # 3. start_date_year # 4. updated_on # 5. id def next_without_georeference CollectingEvent.not_including(self). includes(:georeferences). where(project_id: self.project_id, georeferences: {collecting_event_id: nil}). order(:verbatim_locality, :geographic_area_id, :start_date_year, :updated_at, :id). first end # @param [Float] delta_z, will be used to fill in the z coordinate of the point # @return [RGeo::Geographic::ProjectedPointImpl, nil] # for the *verbatim* latitude/longitude only def verbatim_map_center(delta_z = 0.0) retval = nil unless verbatim_latitude.blank? or verbatim_longitude.blank? lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude.to_s) long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude.to_s) elev = Utilities::Geo.distance_in_meters(verbatim_elevation.to_s).to_f delta_z = elev unless elev == 0.0 # Meh, BAD! must be nil retval = Gis::FACTORY.point(long, lat, delta_z) end retval end # @return [Symbol, nil] # the name of the method that will return an Rgeo object that represent # the "preferred" centroid for this collecting event def map_center_method return :preferred_georeference if preferred_georeference # => { georeferenceProtocol => ? } return :verbatim_map_center if verbatim_map_center # => { } return :geographic_area if geographic_area.try(:has_shape?) nil end # @return [Rgeo::Geographic::ProjectedPointImpl, nil] def map_center case map_center_method when :preferred_georeference preferred_georeference.geographic_item.centroid when :verbatim_map_center verbatim_map_center when :geographic_area geographic_area.default_geographic_item.geo_object.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } end def georeference_latitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].latitude.to_f end retval.round(6) end def georeference_longitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].longitude.to_f end retval.round(6) end # @return [String] # coordinates for centering a Google map def verbatim_center_coordinates if self.verbatim_latitude.blank? || self.verbatim_longitude.blank? 'POINT (0.0 0.0 0.0)' else self.verbatim_map_center.to_s end end def level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:country] end def level1_name return cached_level1_name if cached_level1_name cache_geographic_names[:state] end def level2_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end def cached_level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end # @return [CollectingEvent] # the instance may not be valid! def clone a = dup a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') roles.each do |r| a.collector_roles.build(person: r.person, position: r.position) end if georeferences.load.any? not_georeference_attributes = %w{created_at updated_at project_id updated_by_id created_by_id collecting_event_id id position} georeferences.each do |g| c = g.dup.attributes.select{|c| !not_georeference_attributes.include?(c) } a.georeferences.build(c) end end a.save a end # @return [String, nil] # a string used in DWC reportedBy and ultimately label generation # TODO: include initials when we find out a clean way of producing them # yes, it's a Helper def collector_names [Utilities::Strings.(collectors.collect{|a| a.last_name}), verbatim_collectors].compact.first end protected def set_cached v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = cached_geographic_name_classification.values.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? update_columns(cached: string) end def set_times_to_nil_if_form_provided_blank matches = ['0001-01-01 00:00:00 UTC', '2000-01-01 00:00:00 UTC'] write_attribute(:time_start, nil) if matches.include?(self.time_start.to_s) write_attribute(:time_end, nil) if matches.include?(self.time_end.to_s) end def check_verbatim_geolocation_uncertainty errors.add(:verbatim_geolocation_uncertainty, 'Provide both verbatim_latitude and verbatim_longitude if you provide verbatim_uncertainty.') if !verbatim_geolocation_uncertainty.blank? && verbatim_longitude.blank? && verbatim_latitude.blank? end def check_date_range begin errors.add(:base, 'End date is earlier than start date.') if has_start_date? && has_end_date? && (start_date > end_date) rescue errors.add(:base, 'Start and/or end date invalid.') end errors.add(:base, 'End date without start date.') if (has_end_date? && !has_start_date?) end def check_ma_range errors.add(:min_ma, 'Min ma is < Max ma.') if min_ma.present? && max_ma.present? && min_ma > max_ma end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if !minimum_elevation.blank? && !maximum_elevation.blank? && maximum_elevation < minimum_elevation end def sv_georeference_matches_verbatim if a = georeferences.where(type: 'Georeference::VerbatimData').first d_lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude).to_f d_long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude).to_f if (a.latitude.to_f != d_lat) soft_validations.add( :base, "Verbatim latitude #{verbatim_latitude}: (#{d_lat}) and point geoference latitude #{a.latitude} do not match") end if (a.longitude.to_f != d_long) soft_validations.add( :base, "Verbatim longitude #{verbatim_longitude}: (#{d_long}) and point geoference longitude #{a.longitude} do not match") end end end def sv_minimally_check_for_a_label [:verbatim_label, :print_label, :document_label, :field_notes].each do |v| return true if !self.send(v).blank? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end end |
#document_label ⇒ String
A print-ready expanded/clarified version of a verbatim_label intended to clarify interpretation of that label. To be used, for example, when reporting Holotype labels.
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 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 |
# File 'app/models/collecting_event.rb', line 179 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::HasRoles include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::HasPapertrail include Shared::IsData include SoftValidation ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, will not rebuild dwc_occurrence index. # See also Shared::IsDwcOccurrence attr_accessor :no_dwc_occurrence # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # handle_asynchronously :update_dwc_occurrences, run_at: Proc.new { 20.seconds.from_now } belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy has_one :verbatim_data_georeference, class_name: 'Georeference::VerbatimData' has_one :preferred_georeference, -> { order(:position) }, class_name: 'Georeference', foreign_key: :collecting_event_id has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy has_many :collectors, through: :collector_roles, source: :person, inverse_of: :collecting_events has_many :dwc_occurrences, through: :collection_objects has_many :georeferences, dependent: :destroy, inverse_of: :collecting_event has_many :error_geographic_items, through: :georeferences, source: :error_geographic_item has_many :geographic_items, through: :georeferences # See also all_geographic_items, the union has_many :geo_locate_georeferences, class_name: '::Georeference::GeoLocate', dependent: :destroy has_many :gpx_georeferences, class_name: 'Georeference::GPX', dependent: :destroy has_many :otus, through: :collection_objects after_create do if with_verbatim_data_georeference generate_verbatim_data_georeference(true) end end before_save :set_times_to_nil_if_form_provided_blank after_save :cache_geographic_names, if: -> { !no_cached && saved_change_to_attribute?(:geographic_area_id) } after_save :set_cached, unless: -> { no_cached } after_save :update_dwc_occurrences , unless: -> { no_dwc_occurrence } def update_dwc_occurrences # reload is required! if collection_objects.count < 40 collection_objects.reload.each do |o| o.set_dwc_occurrence end end end accepts_nested_attributes_for :verbatim_data_georeference accepts_nested_attributes_for :geo_locate_georeferences accepts_nested_attributes_for :gpx_georeferences accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_ma_range validates_uniqueness_of :md5_of_verbatim_label, scope: [:project_id], unless: -> { verbatim_label.blank? } validates_presence_of :verbatim_longitude, if: -> { !verbatim_latitude.blank? } validates_presence_of :verbatim_latitude, if: -> { !verbatim_longitude.blank? } validates :geographic_area, presence: true, allow_nil: true validates :time_start_hour, time_hour: true validates :time_start_minute, time_minute: true validates :time_start_second, time_second: true validates_presence_of :time_start_minute, if: -> { !self.time_start_second.blank? } validates_presence_of :time_start_hour, if: -> { !self.time_start_minute.blank? } validates :time_end_hour, time_hour: true validates :time_end_minute, time_minute: true validates :time_end_second, time_second: true validates_presence_of :time_end_minute, if: -> { !self.time_end_second.blank? } validates_presence_of :time_end_hour, if: -> { !self.time_end_minute.blank? } validates :start_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :end_date_year, date_year: {min_year: 1000, max_year: Time.now.year + 5} validates :start_date_month, date_month: true validates :end_date_month, date_month: true validates_presence_of :start_date_month, if: -> { !start_date_day.nil? } validates_presence_of :end_date_month, if: -> { !end_date_day.nil? } validates :end_date_day, date_day: {year_sym: :end_date_year, month_sym: :end_date_month}, unless: -> { end_date_year.nil? || end_date_month.nil? } validates :start_date_day, date_day: {year_sym: :start_date_year, month_sym: :start_date_month}, unless: -> { start_date_year.nil? || start_date_month.nil? } soft_validate(:sv_minimally_check_for_a_label) soft_validate(:sv_georeference_matches_verbatim, set: :georeference, has_fix: false) # @param [String] def verbatim_label=(value) write_attribute(:verbatim_label, value) write_attribute(:md5_of_verbatim_label, Utilities::Strings.generate_md5(value)) end scope :used_recently, -> { joins(:collection_objects).includes(:collection_objects).where(collection_objects: { created_at: 1.weeks.ago..Time.now } ).order('"collection_objects"."created_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { project_id: project_id } ) } class << self # # Scopes # def select_optimized(user_id, project_id) h = { recent: (CollectingEvent.used_in_project(project_id) .where(collection_objects: {updated_by_id: user_id}) .used_recently .distinct .limit(5) .order(:cached) .to_a + CollectingEvent.where(project_id: project_id, updated_by_id: user_id, created_at: 3.hours.ago..Time.now).limit(5).to_a).uniq, pinboard: CollectingEvent.pinned_by(user_id).pinned_in_project(project_id).to_a } h[:quick] = (CollectingEvent.pinned_by(user_id).pinboard_inserted.pinned_in_project(project_id).to_a + h[:recent]).uniq h end # @param [GeographicItem] geographic_item # @return [Scope] # TODO: use joins(:geographic_items).where(containing scope), simplied to def contained_within(geographic_item) CollectingEvent.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)) end # @param [CollectingEvent Scope] collecting_events # @return [Scope] without self (if included) # TODO: DRY, use general form of this def not_including(collecting_events) where.not(id: collecting_events) end # # Other # # @param [String] search_start_date string in form 'yyyy/mm/dd' # @param [String] search_end_date string in form 'yyyy/mm/dd' # @param [String] partial_overlap 'on' or 'off' # @return [Scope] of selected collecting events # TODO: remove all of this for direct call to Queries::CollectingEvent::Filter def in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on') allow_partial = (partial_overlap.downcase == 'off' ? false : true) q = Queries::CollectingEvent::Filter.new(start_date: search_start_date, end_date: search_end_date, partial_overlap_dates: allow_partial) where(q.between_date_range.to_sql).distinct # TODO: uniq should likely not be here end # @param [ActionController::Parameters] params in the style Rails of 'params' # @return [Scope] of selected collecting_events # TODO: deprecate for lib/queries/collecting_event def filter_by(params) sql_string = '' unless params.blank? # not strictly necessary, but handy for debugging sql_string = Utilities::Dates.date_sql_from_params(params) # processing text data v_locality_fragment = params['verbatim_locality_text'] any_label_fragment = params['any_label_text'] id_fragment = params['identifier_text'] prefix = '' unless v_locality_fragment.blank? unless sql_string.blank? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' unless any_label_fragment.blank? unless sql_string.blank? prefix = 'and ' end sql_string += "#{ prefix }(verbatim_label ilike '%#{any_label_fragment}%'" sql_string += " or print_label ilike '%#{any_label_fragment}%'" sql_string += " or document_label ilike '%#{any_label_fragment}%'" sql_string += ')' end unless id_fragment.blank? # @todo this still needs to be dealt with end end # find the records if sql_string.blank? collecting_events = CollectingEvent.none else collecting_events = CollectingEvent.where(sql_string).distinct end collecting_events end end # end Class methods # @param [String] lat # @param [String] long # @param [String] piece # @param [Integer] project_id # @param [Boolean] include_values true if to include records whicgh already have verbatim lat/longs # @return [Scope] of matching collecting events # TODO: deprecate def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" unless lat.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" unless long.blank? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" unless piece.blank? sql += ')' sql += ' and (verbatim_latitude is null or verbatim_longitude is null)' unless include_values retval = CollectingEvent.where(sql) .with_project_id(project_id) .order(:id) .where.not(id: id).distinct retval end # @return [Boolean] def has_data? CollectingEvent.data_attributes.each do |a| return true unless self.send(a).blank? end return true if georeferences.any? false end def has_some_date? !verbatim_date.blank? || some_start_date? || some_end_date? end def has_collectors? !verbatim_collectors.blank? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? !start_date_day.blank? && !start_date_month.blank? && !start_date_year.blank? end # @return [Boolean] # has a fully defined date def has_end_date? !end_date_day.blank? && !end_date_month.blank? && !end_date_year.blank? end def some_start_date? [start_date_day, start_date_month, start_date_year].compact.any? end def some_end_date? [end_date_day, end_date_month, end_date_year].compact.any? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def start_date_string Utilities::Dates.from_parts(start_date_year, start_date_month, start_date_day) if some_start_date? end # @return [String, nil] # an umabigously formatted string with missing parts indicated by ?? def end_date_string Utilities::Dates.from_parts(end_date_year, end_date_month, end_date_day) if some_end_date? end # @return [Time] # This is for the purposes of computation, not display! def end_date Utilities::Dates.nomenclature_date(end_date_day, end_date_month, end_date_year) end # @return [Time] # This is for the purposes of computation, not display! def start_date Utilities::Dates.nomenclature_date(start_date_day, start_date_month, start_date_year) end # @return [String] # like 00, 00:00, or 00:00:00 def time_start Utilities::Dates.format_to_hours_minutes_seconds(time_start_hour, time_start_minute, time_start_second) end # @return [String] # like 00, 00:00, or 00:00:00 def time_end Utilities::Dates.format_to_hours_minutes_seconds(time_end_hour, time_end_minute, time_end_second) end # @return [Array] # time_start and end if provided def time_range [time_start, time_end].compact end # @return [Array] # date_start and end if provided def date_range [start_date_string, end_date_string].compact end # CollectingEvent.select {|d| !(d.verbatim_latitude.nil? || d.verbatim_longitude.nil?)} # .select {|ce| ce.georeferences.empty?} # @param [Boolean] reference_self # @param [Boolean] no_cached # @return [Georeference::VerbatimData, false] # generates (creates) a Georeference::VerbatimReference from verbatim_latitude and verbatim_longitude values def generate_verbatim_data_georeference(reference_self = false, no_cached: false) return false if (verbatim_latitude.nil? || verbatim_longitude.nil?) begin CollectingEvent.transaction do vg_attributes = {collecting_event_id: id.to_s, no_cached: no_cached} vg_attributes.merge!(by: self.creator.id, project_id: self.project_id) if reference_self a = Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue raise end false end # @return [GeographicItem, nil] # a GeographicItem instance representing a translation of the verbatim values, not saved def build_verbatim_geographic_item if self.verbatim_latitude && self.verbatim_longitude && !self.new_record? local_latitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude) local_longitude = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude) elev = Utilities::Geo.distance_in_meters(verbatim_elevation).to_f point = Gis::FACTORY.point(local_latitude, local_longitude, elev) GeographicItem.new(point: point) else nil end end # @return [Integer] # @todo figure out how to convert verbatim_geolocation_uncertainty in different units (ft, m, km, mi) into meters # @TODO: See Utilities::Geo.distance_in_meters(String) def get_error_radius return nil if verbatim_geolocation_uncertainty.blank? return verbatim_geolocation_uncertainty.to_i if is.number?(verbatim_geolocation_uncertainty) nil end # @return [Scope] # all geographic_items associated with this collecting_event through georeferences only def all_geographic_items GeographicItem. joins('LEFT JOIN georeferences g2 ON geographic_items.id = g2.error_geographic_item_id'). joins('LEFT JOIN georeferences g1 ON geographic_items.id = g1.geographic_item_id'). where(['(g1.collecting_event_id = ? OR g2.collecting_event_id = ?) AND (g1.geographic_item_id IS NOT NULL OR g2.error_geographic_item_id IS NOT NULL)', self.id, self.id]) end # @return [GeographicItem, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item try(:geographic_area).try(:default_geographic_item) end # @param [GeographicItem] # @return [String] # see how far away we are from another gi def distance_to(geographic_item_id) GeographicItem.distance_between(preferred_georeference.geographic_item_id, geographic_item_id) end # @param [Double] distance in meters # @return [Scope] def collecting_events_within_radius_of(distance) return CollectingEvent.none if !preferred_georeference geographic_item_id = preferred_georeference.geographic_item_id # geographic_item_id = preferred_georeference.try(:geographic_item_id) # geographic_item_id = Georeference.where(collecting_event_id: id).first.geographic_item_id if geographic_item_id.nil? CollectingEvent.not_self(self) .joins(:geographic_items) .where(GeographicItem.within_radius_of_item_sql(geographic_item_id, distance)) end # @return [Scope] # Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self def collecting_events_intersecting_with pieces = GeographicItem.with_collecting_event_through_georeferences.intersecting('any', self.geographic_items.first).distinct gr = [] # all collecting events for a geographic_item pieces.each { |o| gr.push(o.collecting_events_through_georeferences.to_a) gr.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo change 'id in (?)' to some other sql construct pieces = CollectingEvent.where(id: gr.flatten.map(&:id).uniq) pieces.not_including(self) end # @return [Scope] # Find other CEs that have GRs whose GIs or EGIs are contained in the EGI def collecting_events_contained_in_error # find all the GIs and EGIs associated with CEs # TODO: this will be impossibly slow in present form pieces = GeographicItem.with_collecting_event_through_georeferences.to_a me = self.error_geographic_items.first.geo_object gi = [] # collect all the GIs which are within the EGI pieces.each { |o| gi.push(o) if o.geo_object.within?(me) } # collect all the CEs which refer to these GIs ce = [] gi.each { |o| ce.push(o.collecting_events_through_georeferences.to_a) ce.push(o.collecting_events_through_georeference_error_geographic_item.to_a) } # @todo Directly map this pieces = CollectingEvent.where(id: ce.flatten.map(&:id).uniq) pieces.not_including(self) end # DEPRECATED for shared code # @param [String, String, Integer] # @return [Scope] def nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) return CollectingEvent.none if compared_string.nil? order_str = CollectingEvent.send(:sanitize_sql_for_conditions, ["levenshtein(collecting_events.#{column}, ?)", compared_string]) CollectingEvent.where('id <> ?', id.to_s). order(Arel.sql(order_str)). limit(limit) end # @param [String] # one or more names from GeographicAreaType # @return [Hash] # ( # {'name' => [GAs]} # or # [{'name' => [GAs]}, {'name' => [GAs]}] # ) # one hash, consisting of a country name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are country_level, and have GIs containing the (GI and/or EGI) of this CE # @todo this needs more work, possibily direct AREL table manipulation. def name_hash(types) retval = {} gi_list = containing_geographic_items # there are a few ways we can end up with no GIs # unless gi_list.nil? # no references GeographicAreas or Georeferences at all, or unless gi_list.empty? # no available GeographicItems to test # map the resulting GIs to their corresponding GAs # pieces = GeographicItem.where(id: gi_list.flatten.map(&:id).uniq) # pieces = gi_list ga_list = GeographicArea.joins(:geographic_area_type, :geographic_areas_geographic_items). where(geographic_area_types: {name: types}, geographic_areas_geographic_items: {geographic_item_id: gi_list}).distinct # WAS: now find all of the GAs which have the same names as the ones we collected. # map the names to an array of results ga_list.each { |i| retval[i.name] ||= [] # if we haven't come across this name yet, set it to point to a blank array retval[i.name].push i # we now have at least a blank array, push the result into it } end # end retval end # @return [Hash] # classifies this collecting event into country, state, county categories def geographic_name_classification # if names are stored in the database, and the the geographic_area_id has not changed if has_cached_geographic_names? && !geographic_area_id_changed? return cached_geographic_name_classification else r = get_geographic_name_classification cache_geographic_names(r, true) end end def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference # quick r = preferred_georeference.geographic_item.quick_geographic_name_hierarchy # almost never the case, UI not setup to do this # slow r = preferred_georeference.geographic_item.inferred_geographic_name_hierarchy if r == {} # therefor defaults to slow when :geographic_area_with_shape # geographic_area.try(:has_shape?) # quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow r = geographic_area.default_geographic_item.inferred_geographic_name_hierarchy if r == {} when :geographic_area # elsif geographic_area # quick r = geographic_area.geographic_name_classification when :verbatim_map_center # elsif map_center # slowest r = GeographicItem.point_inferred_geographic_name_hierarchy(verbatim_map_center) end r ||= {} r end def has_cached_geographic_names? cached_geographic_name_classification != {} end def cached_geographic_name_classification h = {} h[:country] = cached_level0_geographic_name if cached_level0_geographic_name h[:state] = cached_level1_geographic_name if cached_level1_geographic_name h[:county] = cached_level2_geographic_name if cached_level2_geographic_name h end def cache_geographic_names(values = {}, tried = false) # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? && !tried return {} if values.empty? update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county] ) values end # @return [Symbol, nil] # determines (prioritizes) the method to be used to decided the geographic name classification # (string labels for country, state, county) for this collecting_event. def geographic_name_classification_method return :preferred_georeference if preferred_georeference return :geographic_area_with_shape if geographic_area.try(:has_shape?) return :geographic_area if geographic_area return :verbatim_map_center if verbatim_map_center nil end # @return [Array of GeographicItems containing this target] # GeographicItems are those that contain either the georeference or, if there are none, # the geographic area def containing_geographic_items gi_list = [] if self.georeferences.any? # gather all the GIs which contain this GI or EGI # # Struck EGI, EGI must contain GI, therefor anything that contains EGI contains GI, threfor containing GI will always be the bigger set # !! and there was no tests broken # GeographicItem.are_contained_in_item('any_poly', self.geographic_items.to_a).pluck(:id).uniq gi_list = GeographicItem.containing(*geographic_items.pluck(:id)).pluck(:id).uniq else # use geographic_area only if there are no GIs or EGIs unless self.geographic_area.nil? # unless self.geographic_area.geographic_items.empty? # we need to use the geographic_area directly gi_list = GeographicItem.are_contained_in_item('any_poly', self.geographic_area.geographic_items).pluck(:id).uniq # end end end gi_list end # @return [Hash] def countries_hash name_hash(GeographicAreaType::COUNTRY_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a state name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are state_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def states_hash name_hash(GeographicAreaType::STATE_LEVEL_TYPES) end # returns either: ( {'name' => [GAs]} or [{'name' => [GAs]}, {'name' => [GAs]}]) # one hash, consisting of a county name paired with an array of the corresponding GAs, or # an array of all of the hashes (name/GA pairs), # which are county_level, and have GIs containing the (GI and/or EGI) of this CE # @return [Hash] def counties_hash name_hash(GeographicAreaType::COUNTY_LEVEL_TYPES) end # @param [Hash] # @return [String] def name_from_geopolitical_hash(name_hash) return nil if name_hash.keys.count == 0 return name_hash.keys.first if name_hash.keys.count == 1 most_key = nil most_count = 0 name_hash.keys.sort.each do |k| # alphabetically first (keys are unordered) if name_hash[k].size > most_count most_count = name_hash[k].size most_key = k end end most_key end # @return [String] def country_name name_from_geopolitical_hash(countries_hash) end # @return [String] def state_or_province_name name_from_geopolitical_hash(states_hash) end # @return [String] def state_name state_or_province_name end # @return [String] def county_or_equivalent_name name_from_geopolitical_hash(counties_hash) end alias county_name county_or_equivalent_name # @return [Symbol, nil] # prioritizes and identifies the source of the latitude/longitude values that # will be calculated for DWCA and primary display def lat_long_source if preferred_georeference :georeference elsif verbatim_latitude && verbatim_longitude :verbatim elsif geographic_area && geographic_area.has_shape? :geographic_area else nil end end =begin # @todo @mjy: please fill in any other paths you can think of for the acquisition of information for the seven below listed items ce.georeference.geographic_item.centroid ce.georeference.error_geographic_item.centroid ce.verbatim_georeference ce.preferred_georeference ce.georeference.first ce.verbatim_lat/ee.verbatim_lng ce.verbatim_locality ce.geographic_area.geographic_item.centroid There are a number of items we can try to get data for to complete the geolocate parameter string: 'country' can come from: GeographicArea through ce.country_name 'state' can come from: GeographicArea through ce.state_or_province_name 'county' can come from: GeographicArea through ce.county_or_equivalent_name 'locality' can come from: ce.verbatim_locality 'Latitude', 'Longitude' can come from: GeographicItem through ce.georeferences.geographic_item.centroid GeographicItem through ce.georeferences.error_geographic_item.centroid GeographicArea through ce.geographic_area.geographic_area_map_focus 'Placename' can come from: ? Copy of 'locality' =end # rubocop:disable Style/StringHashKeys # @return [Hash] # parameters from collecting event that are of use to geolocate def geolocate_attributes parameters = { 'country' => country_name, 'state' => state_or_province_name, 'county' => county_or_equivalent_name, 'locality' => verbatim_locality, 'Placename' => verbatim_locality, } focus = case lat_long_source when :georeference preferred_georeference.geographic_item when :geographic_area geographic_area.geographic_area_map_focus else nil end parameters.merge!( 'Longitude' => focus.point.x, 'Latitude' => focus.point.y ) unless focus.nil? parameters end def latitude verbatim_map_center.try(:y) end def longitude verbatim_map_center.try(:x) end # @return [Hash] # a complete set of params necessary to form a request string def geolocate_ui_params Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_hash end # @return [String] def geolocate_ui_params_string Georeference::GeoLocate::RequestUI.new(geolocate_attributes).request_params_string end # @return [GeoJSON::Feature] # the first geographic item of the first georeference on this collecting event def to_geo_json_feature # !! avoid loading the whole geographic item, just grab the bits we need: # self.georeferences(true) # do this to to_simple_json_feature.merge({ 'properties' => { 'collecting_event' => { 'id' => self.id, 'tag' => "Collecting event #{self.id}." } } }) end # TODO: parametrize to include gazeteer # i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string') def to_simple_json_feature base = { 'type' => 'Feature', 'properties' => {} } if geographic_items.any? geo_item_id = geographic_items.select(:id).first.id query = "ST_AsGeoJSON(#{GeographicItem::GEOMETRY_SQL.to_sql}::geometry) geo_json" base['geometry'] = JSON.parse(GeographicItem.select(query).find(geo_item_id).geo_json) end base end # rubocop:enable Style/StringHashKeys # @return [CollectingEvent] # return the next collecting event without a georeference in this collecting events project sort order # 1. verbatim_locality # 2. geography_id # 3. start_date_year # 4. updated_on # 5. id def next_without_georeference CollectingEvent.not_including(self). includes(:georeferences). where(project_id: self.project_id, georeferences: {collecting_event_id: nil}). order(:verbatim_locality, :geographic_area_id, :start_date_year, :updated_at, :id). first end # @param [Float] delta_z, will be used to fill in the z coordinate of the point # @return [RGeo::Geographic::ProjectedPointImpl, nil] # for the *verbatim* latitude/longitude only def verbatim_map_center(delta_z = 0.0) retval = nil unless verbatim_latitude.blank? or verbatim_longitude.blank? lat = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_latitude.to_s) long = Utilities::Geo.degrees_minutes_seconds_to_decimal_degrees(verbatim_longitude.to_s) elev = Utilities::Geo.distance_in_meters(verbatim_elevation.to_s).to_f delta_z = elev unless elev == 0.0 # Meh, BAD! must be nil retval = Gis::FACTORY.point(long, lat, delta_z) end retval end # @return [Symbol, nil] # the name of the method that will return an Rgeo object that represent # the "preferred" centroid for this collecting event def map_center_method return :preferred_georeference if preferred_georeference # => { georeferenceProtocol => ? } return :verbatim_map_center if verbatim_map_center # => { } return :geographic_area if geographic_area.try(:has_shape?) nil end # @return [Rgeo::Geographic::ProjectedPointImpl, nil] def map_center case map_center_method when :preferred_georeference preferred_georeference.geographic_item.centroid when :verbatim_map_center verbatim_map_center when :geographic_area geographic_area.default_geographic_item.geo_object.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } end def georeference_latitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].latitude.to_f end retval.round(6) end def georeference_longitude retval = 0.0 if georeferences.count > 0 retval = Georeference.where(collecting_event_id: self.id).order(:position).limit(1)[0].longitude.to_f end retval.round(6) end # @return [String] # coordinates for centering a Google map def verbatim_center_coordinates if self.verbatim_latitude.blank? || self.verbatim_longitude.blank? 'POINT (0.0 0.0 0.0)' else self.verbatim_map_center.to_s end end def level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:country] end def level1_name return cached_level1_name if cached_level1_name cache_geographic_names[:state] end def level2_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end def cached_level0_name return cached_level0_name if cached_level0_name cache_geographic_names[:state] end # @return [CollectingEvent] # the instance may not be valid! def clone a = dup a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') roles.each do |r| a.collector_roles.build(person: r.person, position: r.position) end if georeferences.load.any? not_georeference_attributes = %w{created_at updated_at project_id updated_by_id created_by_id collecting_event_id id position} georeferences.each do |g| c = g.dup.attributes.select{|c| !not_georeference_attributes.include?(c) } a.georeferences.build(c) end end a.save a end # @return [String, nil] # a string used in DWC reportedBy and ultimately label generation # TODO: include initials when we find out a clean way of producing them # yes, it's a Helper def collector_names [Utilities::Strings.(collectors.collect{|a| a.last_name}), verbatim_collectors].compact.first end protected def set_cached v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = cached_geographic_name_classification.values.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') string = [name, place_date, verbatim_collectors, < |